From 5ead22858968dd2d0f07931f6ea885948d330b8a Mon Sep 17 00:00:00 2001 From: "Seshanth.S" <35675963+seshanthS@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:56:05 +0530 Subject: [PATCH] Fix WebView layout, Android insets, and iOS dev server support (#1935) * fix version * Fix: webview displays over camera and statusbar * Fix: Update ProviderLaunchScreen layout * fixes * more fixes * add ios webview dev url capabilities * fix ios building * pr feedback --------- Co-authored-by: Justin Hernandez --- .../self/testapp/screens/SdkLaunchScreen.kt | 1 + .../self/testapp/screens/DevServerUrl.ios.kt | 8 ++++- .../kmp-sdk-test-app/iosApp/iosApp/Info.plist | 2 ++ packages/kmp-sdk-test-app/scripts/run-ios.sh | 10 +++--- .../sdk/webview/SelfVerificationActivity.kt | 35 ++++++++++++++++++- .../kotlin/xyz/self/sdk/api/SelfSdk.ios.kt | 8 ++++- .../xyz/self/sdk/providers/WebViewProvider.kt | 2 ++ .../xyz/self/sdk/webview/IosWebViewHost.kt | 2 ++ .../sdk/webview/SelfVerificationActivity.kt | 35 ++++++++++++++++++- .../Providers/WebViewProviderImpl.swift | 32 +++++++++++++++++ .../onboarding/ProviderLaunchScreen.tsx | 34 ++++++++++++------ 11 files changed, 151 insertions(+), 18 deletions(-) diff --git a/packages/kmp-sdk-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/SdkLaunchScreen.kt b/packages/kmp-sdk-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/SdkLaunchScreen.kt index 599fe5704..da881eaac 100644 --- a/packages/kmp-sdk-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/SdkLaunchScreen.kt +++ b/packages/kmp-sdk-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/SdkLaunchScreen.kt @@ -74,6 +74,7 @@ fun SdkLaunchScreen(navController: NavController) { SelfSdkConfig( environment = environment, debug = true, + version = if (useMockDocument) 1 else 2, appName = appName.ifBlank { null }, appEndpoint = appEndpoint.ifBlank { null }, devServerUrl = devServerUrl, diff --git a/packages/kmp-sdk-test-app/composeApp/src/iosMain/kotlin/xyz/self/testapp/screens/DevServerUrl.ios.kt b/packages/kmp-sdk-test-app/composeApp/src/iosMain/kotlin/xyz/self/testapp/screens/DevServerUrl.ios.kt index e29bd98f3..b906a73f8 100644 --- a/packages/kmp-sdk-test-app/composeApp/src/iosMain/kotlin/xyz/self/testapp/screens/DevServerUrl.ios.kt +++ b/packages/kmp-sdk-test-app/composeApp/src/iosMain/kotlin/xyz/self/testapp/screens/DevServerUrl.ios.kt @@ -4,4 +4,10 @@ package xyz.self.testapp.screens -actual fun getDevServerUrl(): String? = null +import platform.Foundation.NSBundle + +actual fun getDevServerUrl(): String? { + val value = NSBundle.mainBundle.objectForInfoDictionaryKey("WEBVIEW_DEV_URL") as? String + if (value.isNullOrBlank() || value.startsWith("$(")) return null + return value +} diff --git a/packages/kmp-sdk-test-app/iosApp/iosApp/Info.plist b/packages/kmp-sdk-test-app/iosApp/iosApp/Info.plist index 771d1c27a..27bf89dbc 100644 --- a/packages/kmp-sdk-test-app/iosApp/iosApp/Info.plist +++ b/packages/kmp-sdk-test-app/iosApp/iosApp/Info.plist @@ -18,6 +18,8 @@ NSAllowsLocalNetworking + WEBVIEW_DEV_URL + $(WEBVIEW_DEV_URL) com.apple.developer.nfc.readersession.iso7816.select-identifiers A0000002471001 diff --git a/packages/kmp-sdk-test-app/scripts/run-ios.sh b/packages/kmp-sdk-test-app/scripts/run-ios.sh index 744444a86..18af4e756 100755 --- a/packages/kmp-sdk-test-app/scripts/run-ios.sh +++ b/packages/kmp-sdk-test-app/scripts/run-ios.sh @@ -13,7 +13,7 @@ cd "$KMP_SDK_DIR" # --- Resolve Xcode project dependencies --- cd "$IOS_DIR" echo "📦 Resolving package dependencies..." -xcodebuild -project iosApp.xcodeproj -resolvePackageDependencies -quiet 2>/dev/null || true +xcodebuild -workspace iosApp.xcworkspace -resolvePackageDependencies -quiet 2>/dev/null || true # --- Find an available iOS Simulator --- echo "📱 Finding iOS Simulator..." @@ -50,20 +50,22 @@ fi # --- Build the app --- echo "🔨 Building iOS app..." -xcodebuild -project iosApp.xcodeproj \ +xcodebuild -workspace iosApp.xcworkspace \ -scheme iosApp \ -sdk iphonesimulator \ -destination "id=$SIMULATOR_ID" \ ONLY_ACTIVE_ARCH=YES \ ARCHS=arm64 \ + WEBVIEW_DEV_URL="${WEBVIEW_DEV_URL:-}" \ build \ 2>&1 | tail -5 # --- Find and install the app --- -BUILD_DIR=$(xcodebuild -project iosApp.xcodeproj \ +BUILD_DIR=$(xcodebuild -workspace iosApp.xcworkspace \ -scheme iosApp \ -sdk iphonesimulator \ - -showBuildSettings 2>/dev/null | grep ' BUILT_PRODUCTS_DIR' | awk '{print $3}') + -showBuildSettings -json 2>/dev/null \ + | jq -r '.[] | select(.target == "iosApp") | .buildSettings.BUILT_PRODUCTS_DIR') APP_PATH="$BUILD_DIR/iosApp.app" if [ ! -d "$APP_PATH" ]; then diff --git a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt index 251a2f1be..815ed24d1 100644 --- a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt +++ b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt @@ -8,8 +8,13 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle +import android.view.ViewGroup import android.webkit.WebChromeClient +import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import xyz.self.sdk.bridge.MessageRouter import xyz.self.sdk.handlers.CryptoBridgeHandler import xyz.self.sdk.handlers.LifecycleBridgeHandler @@ -21,9 +26,11 @@ import xyz.self.sdk.providers.SdkProviderRegistry class SelfVerificationActivity : AppCompatActivity() { private lateinit var webViewHost: AndroidWebViewHost private lateinit var router: MessageRouter + private var container: FrameLayout? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) initVerificationFlow() } @@ -74,7 +81,32 @@ class SelfVerificationActivity : AppCompatActivity() { val devServerUrl = intent.getStringExtra(EXTRA_DEV_SERVER_URL) webViewHost = AndroidWebViewHost(this, router, isDebugMode, remoteWebAppBaseUrl, devServerUrl) val webView = webViewHost.createWebView(queryParams) - setContentView(webView) + val wrapper = + FrameLayout(this).apply { + addView( + webView, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ), + ) + } + this.container = wrapper + setContentView(wrapper) + + ViewCompat.setOnApplyWindowInsetsListener(wrapper) { view, insets -> + val systemInsets = + insets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(), + ) + view.setPadding( + systemInsets.left, + systemInsets.top, + systemInsets.right, + systemInsets.bottom, + ) + WindowInsetsCompat.CONSUMED + } } private fun registerHandlers() { @@ -178,6 +210,7 @@ class SelfVerificationActivity : AppCompatActivity() { } override fun onDestroy() { + container?.let { ViewCompat.setOnApplyWindowInsetsListener(it, null) } if (::webViewHost.isInitialized) { webViewHost.destroy() } diff --git a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt index 2a8a2dda1..cbafb9bac 100644 --- a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt +++ b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt @@ -100,7 +100,13 @@ actual class SelfSdk private constructor( val queryParams = buildQueryParams(request) // Create WebView host and the web view - webViewHost = IosWebViewHost(router!!, config.debug, remoteWebAppBaseUrl = config.remoteWebAppBaseUrl) + webViewHost = + IosWebViewHost( + router!!, + config.debug, + remoteWebAppBaseUrl = config.remoteWebAppBaseUrl, + devServerUrl = config.devServerUrl, + ) webViewHost!!.createWebView(queryParams) // Get the ViewController from the WebView provider and present it diff --git a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/providers/WebViewProvider.kt b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/providers/WebViewProvider.kt index 1d74a9558..a60f9c286 100644 --- a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/providers/WebViewProvider.kt +++ b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/providers/WebViewProvider.kt @@ -23,4 +23,6 @@ interface WebViewProvider { fun isBridgeRequestAllowed(): Boolean fun configureRemoteLoading(remoteWebAppBaseURL: String?) {} + + fun configureDevServer(devServerUrl: String?) {} } diff --git a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/webview/IosWebViewHost.kt b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/webview/IosWebViewHost.kt index adc87c526..fbd7ed0c8 100644 --- a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/webview/IosWebViewHost.kt +++ b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/webview/IosWebViewHost.kt @@ -15,6 +15,7 @@ class IosWebViewHost( private val router: MessageRouter, private val isDebugMode: Boolean = false, private val remoteWebAppBaseUrl: String = "https://self-app-alpha.vercel.app", + private val devServerUrl: String? = null, ) { fun createWebView(queryParams: String? = null): UIView { val provider = @@ -22,6 +23,7 @@ class IosWebViewHost( ?: throw IllegalStateException("WebView provider not configured") provider.configureRemoteLoading(remoteWebAppBaseUrl) + provider.configureDevServer(devServerUrl) return provider.createWebView( onMessageReceived = { rawJson -> diff --git a/packages/native-shell-android/src/main/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt b/packages/native-shell-android/src/main/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt index 42a0fea25..6fba51c74 100644 --- a/packages/native-shell-android/src/main/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt +++ b/packages/native-shell-android/src/main/kotlin/xyz/self/sdk/webview/SelfVerificationActivity.kt @@ -5,8 +5,13 @@ package xyz.self.sdk.webview import android.content.Intent import android.net.Uri import android.os.Bundle +import android.view.ViewGroup import android.webkit.WebChromeClient +import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import xyz.self.sdk.api.SelfSdk import xyz.self.sdk.bridge.MessageRouter import xyz.self.sdk.handlers.CryptoHandler @@ -16,9 +21,11 @@ import xyz.self.sdk.handlers.SecureStorageHandler class SelfVerificationActivity : AppCompatActivity() { private lateinit var webViewHost: AndroidWebViewHost private lateinit var router: MessageRouter + private var container: FrameLayout? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) val isDebugMode = intent.getBooleanExtra(EXTRA_DEBUG_MODE, false) val environment = intent.getStringExtra(EXTRA_ENVIRONMENT) ?: "prod" @@ -93,7 +100,32 @@ class SelfVerificationActivity : AppCompatActivity() { } val webView = webViewHost.createWebView(queryParams) - setContentView(webView) + val wrapper = + FrameLayout(this).apply { + addView( + webView, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ), + ) + } + container = wrapper + setContentView(wrapper) + + ViewCompat.setOnApplyWindowInsetsListener(wrapper) { view, insets -> + val systemInsets = + insets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(), + ) + view.setPadding( + systemInsets.left, + systemInsets.top, + systemInsets.right, + systemInsets.bottom, + ) + WindowInsetsCompat.CONSUMED + } } override fun onRequestPermissionsResult( @@ -135,6 +167,7 @@ class SelfVerificationActivity : AppCompatActivity() { } override fun onDestroy() { + container?.let { ViewCompat.setOnApplyWindowInsetsListener(it, null) } if (::webViewHost.isInitialized) { webViewHost.destroy() } diff --git a/packages/self-sdk-swift/Sources/SelfSdkSwift/Providers/WebViewProviderImpl.swift b/packages/self-sdk-swift/Sources/SelfSdkSwift/Providers/WebViewProviderImpl.swift index 588cfe5d0..d6594822b 100644 --- a/packages/self-sdk-swift/Sources/SelfSdkSwift/Providers/WebViewProviderImpl.swift +++ b/packages/self-sdk-swift/Sources/SelfSdkSwift/Providers/WebViewProviderImpl.swift @@ -19,6 +19,7 @@ public class WebViewProviderImpl: NSObject { private var onMessageReceived: ((String) -> Void)? private var isDebugMode: Bool = false private var remoteWebAppBaseURL: URL = WebViewProviderImpl.defaultRemoteBaseURL + private var devServerUrl: String? /// Weak proxy to avoid retain cycles with WKScriptMessageHandler private var messageProxy: WeakScriptMessageProxy? @@ -110,6 +111,11 @@ public class WebViewProviderImpl: NSObject { self.remoteWebAppBaseURL = remoteWebAppBaseURL.flatMap { URL(string: $0) } ?? Self.defaultRemoteBaseURL } + + @objc(configureDevServerDevServerUrl:) + public func configureDevServer(devServerUrl: String?) { + self.devServerUrl = devServerUrl + } } // MARK: - Host VC that embeds the WKWebView with proper Auto Layout @@ -185,6 +191,20 @@ extension WebViewProviderImpl: WKScriptMessageHandler { extension WebViewProviderImpl { func initialContentURL(queryParams: String?) -> URL? { + #if DEBUG + if isDebugMode, let devUrl = devServerUrl, !devUrl.isEmpty, + let baseURL = URL(string: devUrl.hasSuffix("/") ? String(devUrl.dropLast()) : devUrl) { + var components = URLComponents() + components.scheme = baseURL.scheme + components.host = baseURL.host + components.port = baseURL.port + components.path = "/tunnel/tour/1" + if let queryParams, !queryParams.isEmpty { + components.percentEncodedQuery = queryParams + } + return components.url + } + if isDebugMode { var components = URLComponents() components.scheme = "http" @@ -196,6 +216,7 @@ extension WebViewProviderImpl { } return components.url } + #endif guard remoteWebAppBaseURL.scheme == "https" else { return nil } var components = URLComponents() @@ -218,18 +239,29 @@ extension WebViewProviderImpl { func isTrustedBridgeURL(_ url: URL?) -> Bool { guard let url else { return false } + #if DEBUG if isDebugMode { + if let devUrl = devServerUrl, !devUrl.isEmpty, let devBase = URL(string: devUrl) { + return url.scheme == devBase.scheme && url.host == devBase.host && resolvedPort(for: url) == resolvedPort(for: devBase) + } return url.scheme == "http" && url.host == Self.loopbackHost && url.port == Int(Self.debugPort) } + #endif return url.scheme == remoteWebAppBaseURL.scheme && url.host == remoteWebAppBaseURL.host && resolvedPort(for: url) == resolvedPort(for: remoteWebAppBaseURL) } func isTrustedBridgeFrameInfo(_ origin: WKSecurityOrigin) -> Bool { + #if DEBUG if isDebugMode { + if let devUrl = devServerUrl, !devUrl.isEmpty, let devBase = URL(string: devUrl) { + let expectedPort = resolvedPort(for: devBase) + return origin.protocol == devBase.scheme && origin.host == devBase.host && resolvedSecurityOriginPort(origin) == expectedPort + } return origin.protocol == "http" && origin.host == Self.loopbackHost && origin.port == Int(Self.debugPort) } + #endif let expectedPort = resolvedPort(for: remoteWebAppBaseURL) return origin.protocol == remoteWebAppBaseURL.scheme && origin.host == remoteWebAppBaseURL.host && diff --git a/packages/webview-app/src/screens/onboarding/ProviderLaunchScreen.tsx b/packages/webview-app/src/screens/onboarding/ProviderLaunchScreen.tsx index bd49c93f3..061365e42 100644 --- a/packages/webview-app/src/screens/onboarding/ProviderLaunchScreen.tsx +++ b/packages/webview-app/src/screens/onboarding/ProviderLaunchScreen.tsx @@ -15,6 +15,7 @@ import type { KycProviderResult } from '../../types/kycProvider'; import { buildKycDocument } from '../../utils/buildKycDocument'; import { waitForAttestation } from '../../utils/diditAttestation'; import { createDiditSession, launchDiditWebSdk } from '../../utils/diditProvider'; +import { WEB_SAFE_AREA } from '../../utils/insets'; const CONTAINER_ID = 'didit-sdk-container'; @@ -294,20 +295,33 @@ export const ProviderLaunchScreen: React.FC = () => { return (
{phase === 'waiting' && ( - { - // TODO: wire up push notifications +
+ > + { + // TODO: wire up push notifications + }} + /> +
)} {phase === 'loading' && (
{
)}