mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,4 +23,6 @@ interface WebViewProvider {
|
||||
fun isBridgeRequestAllowed(): Boolean
|
||||
|
||||
fun configureRemoteLoading(remoteWebAppBaseURL: String?) {}
|
||||
|
||||
fun configureDevServer(devServerUrl: String?) {}
|
||||
}
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user