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 <justin.hernandez@self.xyz>
This commit is contained in:
Seshanth.S
2026-04-08 11:56:05 +05:30
committed by GitHub
parent 147b593e7f
commit 5ead228589
11 changed files with 151 additions and 18 deletions

View File

@@ -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,

View File

@@ -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
}

View File

@@ -18,6 +18,8 @@
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>WEBVIEW_DEV_URL</key>
<string>$(WEBVIEW_DEV_URL)</string>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
<string>A0000002471001</string>

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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

View File

@@ -23,4 +23,6 @@ interface WebViewProvider {
fun isBridgeRequestAllowed(): Boolean
fun configureRemoteLoading(remoteWebAppBaseURL: String?) {}
fun configureDevServer(devServerUrl: String?) {}
}

View File

@@ -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 ->

View File

@@ -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()
}

View File

@@ -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 &&

View File

@@ -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 (
<div
style={{
minHeight: '100vh',
height: '100vh',
display: 'flex',
flexDirection: 'column',
position: 'relative',
backgroundColor: colors.white,
overflow: 'hidden',
}}
>
{phase === 'waiting' && (
<KycPendingScreen
insets={{ top: 0, bottom: 0 }}
onCheckBackLater={handleBack}
onReceiveLiveUpdates={() => {
// TODO: wire up push notifications
<div
style={{
position: 'absolute',
inset: 0,
zIndex: 1,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
/>
>
<KycPendingScreen
insets={WEB_SAFE_AREA.insets}
onCheckBackLater={handleBack}
onReceiveLiveUpdates={() => {
// TODO: wire up push notifications
}}
/>
</div>
)}
{phase === 'loading' && (
<div
@@ -336,18 +350,18 @@ export const ProviderLaunchScreen: React.FC = () => {
</div>
)}
<style>{`
.shadow-card {
#${CONTAINER_ID} .shadow-card {
width: 100% !important;
max-width: 100% !important;
height: 100% !important;
max-height: 100% !important;
border-radius: 0 !important;
}
iframe[class*="in-iframe"] {
#${CONTAINER_ID} iframe[class*="in-iframe"] {
width: 100% !important;
height: 100% !important;
}
div[class*="size-full"] {
#${CONTAINER_ID} div[class*="size-full"] {
width: 100vw !important;
max-width: 100vw !important;
}