diff --git a/mindbox-common/src/androidMain/kotlin/cloud/mindbox/mobile_sdk/inapp/webview/WebViewAndroid.kt b/mindbox-common/src/androidMain/kotlin/cloud/mindbox/mobile_sdk/inapp/webview/WebViewAndroid.kt new file mode 100644 index 0000000..7fc1a09 --- /dev/null +++ b/mindbox-common/src/androidMain/kotlin/cloud/mindbox/mobile_sdk/inapp/webview/WebViewAndroid.kt @@ -0,0 +1,161 @@ +package cloud.mindbox.mobile_sdk.inapp.webview + +import android.annotation.SuppressLint +import android.graphics.Color +import android.os.Build +import android.view.View +import android.webkit.* +import cloud.mindbox.mobile_sdk.annotations.InternalMindboxApi + +@InternalMindboxApi +public actual typealias WebViewPlatformView = View + +@InternalMindboxApi +public fun WebViewController.Companion.create( + context: android.content.Context, + isDebugEnabled: Boolean +): WebViewController { + return AndroidWebViewController(context, isDebugEnabled) +} + +@OptIn(InternalMindboxApi::class) +private class AndroidWebViewController( + context: android.content.Context, + isDebugEnabled: Boolean +) : WebViewController { + + private val webView: WebView = WebView(context) + private var eventListener: WebViewEventListener? = null + + init { + WebView.setWebContentsDebuggingEnabled(isDebugEnabled) + configureWebView() + webView.webViewClient = createWebViewClient() + } + + override val view: WebViewPlatformView + get() = webView + + override fun loadContent(content: WebViewHtmlContent) { + webView.loadDataWithBaseURL( + content.baseUrl, + content.html, + "text/html", + "UTF-8", + null + ) + } + + override fun setVisibility(isVisible: Boolean) { + webView.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE + } + + override fun setUserAgentSuffix(suffix: String) { + val currentUserAgent: String = webView.settings.userAgentString ?: "" + if (currentUserAgent.contains(suffix)) { + return + } + webView.settings.userAgentString = "$currentUserAgent $suffix".trim() + } + + override fun setJsBridge(bridge: WebViewJsBridge, bridgeName: String) { + webView.removeJavascriptInterface(bridgeName) + webView.addJavascriptInterface(AndroidWebViewJsBridge(bridge), bridgeName) + } + + override fun setEventListener(listener: WebViewEventListener?) { + eventListener = listener + } + + override fun executeOnViewThread(action: () -> Unit) { + webView.post(action) + } + + override fun destroy() { + webView.stopLoading() + webView.loadUrl("about:blank") + webView.clearHistory() + webView.removeAllViews() + webView.destroy() + } + + @SuppressLint("SetJavaScriptEnabled") + private fun configureWebView() { + with(webView.settings) { + javaScriptEnabled = true + domStorageEnabled = true + loadWithOverviewMode = true + builtInZoomControls = true + displayZoomControls = false + defaultTextEncodingName = "utf-8" + cacheMode = WebSettings.LOAD_NO_CACHE + allowContentAccess = true + } + webView.setBackgroundColor(Color.TRANSPARENT) + } + + private fun createWebViewClient(): WebViewClient { + return object : WebViewClient() { + + + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? + ) { + val webViewError: WebViewError = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + WebViewError( + code = error?.errorCode, + description = error?.description?.toString(), + url = request?.url?.toString(), + isForMainFrame = request?.isForMainFrame + ) + } else { + WebViewError( + code = null, + description = null, + url = request?.url?.toString(), + isForMainFrame = request?.isForMainFrame + ) + } + eventListener?.onError(webViewError) + } + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onReceivedError( + view: WebView?, + errorCode: Int, + description: String?, + failingUrl: String? + ) { + val webViewError: WebViewError = WebViewError( + code = errorCode, + description = description, + url = failingUrl, + isForMainFrame = failingUrl == view?.originalUrl + ) + eventListener?.onError(webViewError) + } + + override fun onPageFinished(view: WebView?, url: String?) { + eventListener?.onPageFinished(url) + } + } + } + + private class AndroidWebViewJsBridge( + private val bridge: WebViewJsBridge + ) { + @JavascriptInterface + fun receiveParam(key: String): String? { + return bridge.getParam(key) + } + + @JavascriptInterface + fun postMessage(action: String, data: String) { + bridge.onAction(action, data) + } + } +} + diff --git a/mindbox-common/src/commonMain/kotlin/cloud/mindbox/mobile_sdk/annotations/InternalMindboxApi.kt b/mindbox-common/src/commonMain/kotlin/cloud/mindbox/mobile_sdk/annotations/InternalMindboxApi.kt new file mode 100644 index 0000000..b48bc17 --- /dev/null +++ b/mindbox-common/src/commonMain/kotlin/cloud/mindbox/mobile_sdk/annotations/InternalMindboxApi.kt @@ -0,0 +1,14 @@ +package cloud.mindbox.mobile_sdk.annotations + +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.TYPEALIAS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY +) +@RequiresOptIn( + message = "Internal API. Use only inside Mindbox SDK.", + level = RequiresOptIn.Level.WARNING +) +public annotation class InternalMindboxApi + diff --git a/mindbox-common/src/commonMain/kotlin/cloud/mindbox/mobile_sdk/inapp/webview/WebViewInterfaces.kt b/mindbox-common/src/commonMain/kotlin/cloud/mindbox/mobile_sdk/inapp/webview/WebViewInterfaces.kt new file mode 100644 index 0000000..eb91edc --- /dev/null +++ b/mindbox-common/src/commonMain/kotlin/cloud/mindbox/mobile_sdk/inapp/webview/WebViewInterfaces.kt @@ -0,0 +1,60 @@ +package cloud.mindbox.mobile_sdk.inapp.webview + +import cloud.mindbox.mobile_sdk.annotations.InternalMindboxApi + +@InternalMindboxApi +public expect class WebViewPlatformView + +@InternalMindboxApi +public data class WebViewHtmlContent( + val baseUrl: String, + val html: String +) + +@InternalMindboxApi +public data class WebViewError( + val code: Int?, + val description: String?, + val url: String?, + val isForMainFrame: Boolean? +) + +@InternalMindboxApi +public interface WebViewJsBridge { + public fun getParam(key: String): String? + + public fun onAction(action: String, data: String) { + } +} + +@InternalMindboxApi +public interface WebViewEventListener { + public fun onPageFinished(url: String?) + + public fun onError(error: WebViewError) { + } +} + +@InternalMindboxApi +public interface WebViewController { + public val view: WebViewPlatformView + + public fun loadContent(content: WebViewHtmlContent) + + public fun setVisibility(isVisible: Boolean) + + public fun setUserAgentSuffix(suffix: String) + + public fun setJsBridge(bridge: WebViewJsBridge, bridgeName: String = DEFAULT_WEBVIEW_BRIDGE_NAME) + + public fun setEventListener(listener: WebViewEventListener?) + + public fun executeOnViewThread(action: () -> Unit) + + public fun destroy() + + public companion object +} + +public const val DEFAULT_WEBVIEW_BRIDGE_NAME: String = "SdkBridge" + diff --git a/mindbox-common/src/iosMain/kotlin/cloud/mindbox/mobile_sdk/inapp/webview/WebViewIos.kt b/mindbox-common/src/iosMain/kotlin/cloud/mindbox/mobile_sdk/inapp/webview/WebViewIos.kt new file mode 100644 index 0000000..5e9a3dc --- /dev/null +++ b/mindbox-common/src/iosMain/kotlin/cloud/mindbox/mobile_sdk/inapp/webview/WebViewIos.kt @@ -0,0 +1,6 @@ +package cloud.mindbox.mobile_sdk.inapp.webview + +import cloud.mindbox.mobile_sdk.annotations.InternalMindboxApi + +@InternalMindboxApi +public actual typealias WebViewPlatformView = Any