diff --git a/messaginginapp/api/messaginginapp.api b/messaginginapp/api/messaginginapp.api index 0c4ea540..5727ca99 100644 --- a/messaginginapp/api/messaginginapp.api +++ b/messaginginapp/api/messaginginapp.api @@ -402,6 +402,10 @@ public final class io/customer/messaginginapp/state/InAppMessagingAction$SetUser public fun toString ()Ljava/lang/String; } +public final class io/customer/messaginginapp/state/InAppMessagingActionKt { + public static final fun shouldMarkMessageAsShown (Lio/customer/messaginginapp/state/InAppMessagingAction;)Z +} + public final class io/customer/messaginginapp/state/InAppMessagingManager { public fun ()V public fun (Lio/customer/messaginginapp/gist/presentation/GistListener;)V diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/GistEnvironment.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/GistEnvironment.kt index 8cdd7cd9..79e6d413 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/GistEnvironment.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/GistEnvironment.kt @@ -10,7 +10,7 @@ enum class GistEnvironment : GistEnvironmentEndpoints { DEV { override fun getGistQueueApiUrl() = "https://gist-queue-consumer-api.cloud.dev.gist.build" override fun getEngineApiUrl() = "https://engine.api.dev.gist.build" - override fun getGistRendererUrl() = "https://renderer.gist.build/2.0" + override fun getGistRendererUrl() = "https://renderer.gist.build/3.0" }, LOCAL { @@ -22,6 +22,6 @@ enum class GistEnvironment : GistEnvironmentEndpoints { PROD { override fun getGistQueueApiUrl() = "https://gist-queue-consumer-api.cloud.gist.build" override fun getEngineApiUrl() = "https://engine.api.gist.build" - override fun getGistRendererUrl() = "https://renderer.gist.build/2.0" + override fun getGistRendererUrl() = "https://renderer.gist.build/3.0" } } diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistView.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistView.kt index a35879f7..d7af070a 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistView.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistView.kt @@ -116,12 +116,16 @@ class GistView @JvmOverloads constructor( try { shouldLogAction = false logger.debug("Dismissing from system action: $action") - inAppMessagingManager.dispatch(InAppMessagingAction.DismissMessage(message = message, shouldLog = false)) val intent = Intent(Intent.ACTION_VIEW) intent.data = Uri.parse(action) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP startActivity(context, intent, null) + + // launch system action first otherwise there is a possibility + // that due to lifecycle change and message still being in queue to be displayed + // the message will be displayed again, putting GistActivity before the system action in stack + inAppMessagingManager.dispatch(InAppMessagingAction.DismissMessage(message = message, shouldLog = false)) } catch (e: ActivityNotFoundException) { logger.debug("System action not handled: $action") } diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/engine/EngineWebView.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/engine/EngineWebView.kt index a40eb458..3da3185c 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/engine/EngineWebView.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/engine/EngineWebView.kt @@ -5,7 +5,6 @@ import android.content.Context import android.graphics.Color import android.net.http.SslError import android.util.AttributeSet -import android.util.Base64 import android.webkit.SslErrorHandler import android.webkit.WebResourceError import android.webkit.WebResourceRequest @@ -23,7 +22,6 @@ import io.customer.messaginginapp.gist.data.model.engine.EngineWebConfiguration import io.customer.messaginginapp.gist.utilities.ElapsedTimer import io.customer.messaginginapp.state.InAppMessagingState import io.customer.sdk.core.di.SDKComponent -import java.io.UnsupportedEncodingException import java.util.Timer import java.util.TimerTask @@ -77,83 +75,76 @@ internal class EngineWebView @JvmOverloads constructor( @SuppressLint("SetJavaScriptEnabled") fun setup(configuration: EngineWebConfiguration) { setupTimeout() - val jsonString = Gson().toJson(configuration) - encodeToBase64(jsonString)?.let { options -> - elapsedTimer.start("Engine render for message: ${configuration.messageId}") - val messageUrl = - "${state.environment.getGistRendererUrl()}/index.html?options=$options" - logger.debug("Rendering message with URL: $messageUrl") - webView?.let { - it.loadUrl(messageUrl) - it.settings.javaScriptEnabled = true - it.settings.allowFileAccess = true - it.settings.allowContentAccess = true - it.settings.domStorageEnabled = true - it.settings.textZoom = 100 - it.setBackgroundColor(Color.TRANSPARENT) - - findViewTreeLifecycleOwner()?.lifecycle?.addObserver(this) ?: run { - logger.error("Lifecycle owner not found, attaching interface to WebView manually") - engineWebViewInterface.attach(webView = it) - } + elapsedTimer.start("Engine render for message: ${configuration.messageId}") + val messageData = mapOf("options" to configuration) + val jsonString = Gson().toJson(messageData) + val messageUrl = + "${state.environment.getGistRendererUrl()}/index.html" + logger.debug("Rendering message with URL: $messageUrl") + webView?.let { + it.settings.javaScriptEnabled = true + it.settings.allowFileAccess = true + it.settings.allowContentAccess = true + it.settings.domStorageEnabled = true + it.settings.textZoom = 100 + it.setBackgroundColor(Color.TRANSPARENT) + + findViewTreeLifecycleOwner()?.lifecycle?.addObserver(this) ?: run { + logger.error("Lifecycle owner not found, attaching interface to WebView manually") + engineWebViewInterface.attach(webView = it) + } - it.webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView, url: String?) { - view.loadUrl("javascript:window.parent.postMessage = function(message) {window.${EngineWebViewInterface.JAVASCRIPT_INTERFACE_NAME}.postMessage(JSON.stringify(message))}") + it.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String?) { + // post message to webview with the configuration data so that the message can be rendered + val script = """ + window.postMessage($jsonString, '*'); + """.trim() + view.evaluateJavascript(script) { result -> + logger.debug("JavaScript execution result: $result") } + } - override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { - return !url.startsWith("https://code.gist.build") - } + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + return !url.startsWith("https://code.gist.build") + } - override fun onReceivedError( - view: WebView?, - errorCod: Int, - description: String, - failingUrl: String? - ) { - listener?.error() - } + override fun onReceivedError( + view: WebView?, + errorCod: Int, + description: String, + failingUrl: String? + ) { + listener?.error() + } - override fun onReceivedHttpError( - view: WebView?, - request: WebResourceRequest?, - errorResponse: WebResourceResponse? - ) { - listener?.error() - } + override fun onReceivedHttpError( + view: WebView?, + request: WebResourceRequest?, + errorResponse: WebResourceResponse? + ) { + listener?.error() + } - override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, - error: WebResourceError? - ) { - listener?.error() - } + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? + ) { + listener?.error() + } - override fun onReceivedSslError( - view: WebView?, - handler: SslErrorHandler?, - error: SslError? - ) { - listener?.error() - } + override fun onReceivedSslError( + view: WebView?, + handler: SslErrorHandler?, + error: SslError? + ) { + listener?.error() } } - } ?: run { - listener?.error() - } - } - private fun encodeToBase64(text: String): String? { - val data: ByteArray? - try { - data = text.toByteArray(charset("UTF-8")) - } catch (ex: UnsupportedEncodingException) { - logger.debug("Unsupported encoding exception") - return null + it.loadUrl(messageUrl) } - return Base64.encodeToString(data, Base64.URL_SAFE) } private fun setupTimeout() { diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessageReducer.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessageReducer.kt index f14e8203..ae7bcd13 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessageReducer.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessageReducer.kt @@ -12,20 +12,39 @@ val inAppMessagingReducer: Reducer = { state, action -> is InAppMessagingAction.ProcessMessageQueue -> state.copy(messagesInQueue = action.messages.toSet()) is InAppMessagingAction.SetPollingInterval -> state.copy(pollInterval = action.interval) - is InAppMessagingAction.DismissMessage -> state.copy(currentMessageState = MessageState.Dismissed(action.message)) is InAppMessagingAction.EngineAction.MessageLoadingFailed -> state.copy(currentMessageState = MessageState.Dismissed(action.message)) is InAppMessagingAction.LoadMessage -> state.copy(currentMessageState = MessageState.Loading(action.message)) is InAppMessagingAction.Reset -> InAppMessagingState(siteId = state.siteId, dataCenter = state.dataCenter, environment = state.environment) is InAppMessagingAction.DisplayMessage -> { action.message.queueId?.let { queueId -> + // If the message should be tracked shown when it is displayed, add the queueId to shownMessageQueueIds. + val shownMessageQueueIds = if (action.shouldMarkMessageAsShown()) { + state.shownMessageQueueIds + queueId + } else { + state.shownMessageQueueIds + } + state.copy( currentMessageState = MessageState.Displayed(action.message), - shownMessageQueueIds = state.shownMessageQueueIds + queueId, + shownMessageQueueIds = shownMessageQueueIds, messagesInQueue = state.messagesInQueue.filterNot { it.queueId == queueId }.toSet() ) } ?: state } + is InAppMessagingAction.DismissMessage -> { + var shownMessageQueueIds = state.shownMessageQueueIds + // If the message should be tracked shown when it is dismissed, add the queueId to shownMessageQueueIds. + if (action.shouldMarkMessageAsShown() && action.message.queueId != null) { + shownMessageQueueIds = shownMessageQueueIds + action.message.queueId + } + + state.copy( + currentMessageState = MessageState.Dismissed(action.message), + shownMessageQueueIds = shownMessageQueueIds + ) + } + else -> state } val changes = state.diff(newState) diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessagingAction.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessagingAction.kt index 6d7b58dc..b930c3ee 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessagingAction.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessagingAction.kt @@ -24,3 +24,19 @@ sealed class InAppMessagingAction { object ClearMessageQueue : InAppMessagingAction() object Reset : InAppMessagingAction() } + +fun InAppMessagingAction.shouldMarkMessageAsShown(): Boolean { + return when (this) { + is InAppMessagingAction.DisplayMessage -> { + // Mark the message as shown if it's not persistent + !message.gistProperties.persistent + } + + is InAppMessagingAction.DismissMessage -> { + // Mark the message as shown if it's persistent and should be logged and dismissed via close action only + message.gistProperties.persistent && shouldLog && viaCloseAction + } + + else -> false + } +} diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessagingMiddlewares.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessagingMiddlewares.kt index 0f8cd72a..20f84388 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessagingMiddlewares.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/state/InAppMessagingMiddlewares.kt @@ -45,18 +45,23 @@ internal fun gistLoggingMessageMiddleware() = middleware { } private fun handleMessageDismissal(action: InAppMessagingAction.DismissMessage, next: (Any) -> Any) { - if (action.shouldLog) { - if (action.viaCloseAction) { - SDKComponent.gistQueue.logView(action.message) - } + // Log message close only if message should be tracked as shown on dismiss action + if (action.shouldMarkMessageAsShown()) { + SDKComponent.logger.debug("Persistent message dismissed, logging view for message: ${action.message}, shouldLog: ${action.shouldLog}, viaCloseAction: ${action.viaCloseAction}") + SDKComponent.gistQueue.logView(action.message) + } else { + SDKComponent.logger.debug("Message dismissed, not logging view for message: ${action.message}, shouldLog: ${action.shouldLog}, viaCloseAction: ${action.viaCloseAction}") } next(action) } private fun handleMessageDisplay(action: InAppMessagingAction.DisplayMessage, next: (Any) -> Any) { - val gistProperties = action.message.gistProperties - if (!gistProperties.persistent) { + // Log message view only if message should be tracked as shown on display action + if (action.shouldMarkMessageAsShown()) { + SDKComponent.logger.debug("Message shown, logging view for message: ${action.message}") SDKComponent.gistQueue.logView(action.message) + } else { + SDKComponent.logger.debug("Persistent message shown, not logging view for message: ${action.message}") } next(action) }