From 8cb8913e09294e58e56a6003787728f44fee5e9f Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Mon, 6 Apr 2026 23:21:44 -0700 Subject: [PATCH] Fix tunnel flow back-navigation leaking out of flow (#1916) * add new screens * fixes * cover additional gap * add webview dev url env var * better menu * updates --- .../composeApp/build.gradle.kts | 6 + .../testapp/screens/DevServerUrl.android.kt | 9 + .../self/testapp/screens/SdkLaunchScreen.kt | 4 + .../self/testapp/screens/DevServerUrl.ios.kt | 7 + .../xyz/self/sdk/api/SelfSdk.android.kt | 3 + .../self/sdk/webview/AndroidWebViewHost.kt | 28 +- .../sdk/webview/SelfVerificationActivity.kt | 4 +- .../kotlin/xyz/self/sdk/api/SelfSdkConfig.kt | 1 + packages/webview-app/src/App.tsx | 2 + .../src/components/DevRouteMenu.tsx | 40 +- .../onboarding/ProviderLaunchScreen.tsx | 17 +- .../screens/recovery/LaunchRecoveryScreen.tsx | 17 +- .../src/screens/tunnel/TourScreen.tsx | 14 +- .../src/screens/tunnel/TunnelIDTypeScreen.tsx | 2 +- .../screens/tunnel/TunnelKycFailureScreen.tsx | 31 ++ .../screens/tunnel/TunnelKycSuccessScreen.tsx | 6 +- .../src/screens/tunnel/TunnelKycWrapper.tsx | 17 + .../tunnel/TunnelProofReceiptScreen.tsx | 13 +- .../tunnel/TunnelRecoveryRequiredScreen.tsx | 8 +- .../src/screens/tunnel/TunnelResultScreen.tsx | 39 +- .../screens/tunnel/tunnelFlowScreens.test.tsx | 378 +++++++++++++++++- packages/webview-app/vite.config.ts | 2 +- 22 files changed, 600 insertions(+), 48 deletions(-) create mode 100644 packages/kmp-sdk-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/screens/DevServerUrl.android.kt create mode 100644 packages/kmp-sdk-test-app/composeApp/src/iosMain/kotlin/xyz/self/testapp/screens/DevServerUrl.ios.kt create mode 100644 packages/webview-app/src/screens/tunnel/TunnelKycFailureScreen.tsx diff --git a/packages/kmp-sdk-test-app/composeApp/build.gradle.kts b/packages/kmp-sdk-test-app/composeApp/build.gradle.kts index 9c729448c..5a4a06981 100644 --- a/packages/kmp-sdk-test-app/composeApp/build.gradle.kts +++ b/packages/kmp-sdk-test-app/composeApp/build.gradle.kts @@ -74,6 +74,12 @@ android { .toInt() versionCode = 1 versionName = "1.0.0" + + buildConfigField("String", "WEBVIEW_DEV_URL", "\"${System.getenv("WEBVIEW_DEV_URL") ?: ""}\"") + } + + buildFeatures { + buildConfig = true } compileOptions { diff --git a/packages/kmp-sdk-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/screens/DevServerUrl.android.kt b/packages/kmp-sdk-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/screens/DevServerUrl.android.kt new file mode 100644 index 000000000..334a76111 --- /dev/null +++ b/packages/kmp-sdk-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/screens/DevServerUrl.android.kt @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +package xyz.self.testapp.screens + +import xyz.self.testapp.BuildConfig + +actual fun getDevServerUrl(): String? = BuildConfig.WEBVIEW_DEV_URL.ifBlank { null } 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 0f0af5894..599fe5704 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 @@ -48,6 +48,8 @@ import xyz.self.sdk.api.SelfSdkError import xyz.self.sdk.api.VerificationRequest import xyz.self.sdk.api.VerificationResult +expect fun getDevServerUrl(): String? + @OptIn(ExperimentalMaterial3Api::class) @Composable fun SdkLaunchScreen(navController: NavController) { @@ -65,6 +67,7 @@ fun SdkLaunchScreen(navController: NavController) { val environment = if (useMockDocument) SelfEnvironment.STG else SelfEnvironment.PROD val coroutineScope = rememberCoroutineScope() + val devServerUrl = remember { getDevServerUrl() } val sdk = remember(environment, appName, appEndpoint) { SelfSdk.configure( @@ -73,6 +76,7 @@ fun SdkLaunchScreen(navController: NavController) { debug = true, 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 new file mode 100644 index 000000000..e29bd98f3 --- /dev/null +++ b/packages/kmp-sdk-test-app/composeApp/src/iosMain/kotlin/xyz/self/testapp/screens/DevServerUrl.ios.kt @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +package xyz.self.testapp.screens + +actual fun getDevServerUrl(): String? = null diff --git a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt index aa307f324..f0e689e11 100644 --- a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt +++ b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt @@ -124,6 +124,9 @@ actual class SelfSdk private constructor( putExtra(SelfVerificationActivity.EXTRA_DEBUG_MODE, config.debug) putExtra(SelfVerificationActivity.EXTRA_VERIFICATION_REQUEST, serializeRequest(request)) putExtra(SelfVerificationActivity.EXTRA_CONFIG, serializeConfig(config)) + if (config.devServerUrl != null) { + putExtra(SelfVerificationActivity.EXTRA_DEV_SERVER_URL, config.devServerUrl) + } } // Launch the verification activity diff --git a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt index 3fc66000b..2220869f9 100644 --- a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt +++ b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt @@ -27,6 +27,7 @@ class AndroidWebViewHost( private val context: Context, private val router: MessageRouter, private val isDebugMode: Boolean = false, + private val devServerUrl: String? = null, ) { private lateinit var webView: WebView var pendingPermissionRequest: PermissionRequest? = null @@ -55,9 +56,17 @@ class AndroidWebViewHost( request: WebResourceRequest?, ): Boolean { val uri = request?.url ?: return true + val devHost = devServerUrl?.let { Uri.parse(it) } val isAllowed = (uri.scheme == "https" && uri.host == "self-app-alpha.vercel.app") || - (isDebugMode && uri.scheme == "http" && uri.host == "127.0.0.1" && uri.port == 5173) + (isDebugMode && uri.scheme == "http" && uri.host == "127.0.0.1" && uri.port == 5173) || + ( + isDebugMode && + devHost != null && + uri.scheme == devHost.scheme && + uri.host == devHost.host && + uri.port == devHost.port + ) return !isAllowed } @@ -79,10 +88,18 @@ class AndroidWebViewHost( request.deny() return } + val devHost = devServerUrl?.let { Uri.parse(it) } val isTrusted = (origin.scheme == "https" && origin.host == "self-app-alpha.vercel.app") || (origin.scheme == "https" && origin.host == "verify.didit.me") || - (isDebugMode && origin.scheme == "http" && origin.host == "127.0.0.1") + (isDebugMode && origin.scheme == "http" && origin.host == "127.0.0.1" && origin.port == 5173) || + ( + isDebugMode && + devHost != null && + origin.scheme == devHost.scheme && + origin.host == devHost.host && + origin.port == devHost.port + ) if (!isTrusted) { request.deny() return @@ -156,7 +173,12 @@ class AndroidWebViewHost( addJavascriptInterface(BridgeJsInterface(), "SelfNativeAndroid") - val baseUrl = "https://self-app-alpha.vercel.app/tunnel/tour/1" + val baseUrl = + if (isDebugMode && devServerUrl != null) { + "${devServerUrl.trimEnd('/')}/tunnel/tour/1" + } else { + "https://self-app-alpha.vercel.app/tunnel/tour/1" + } val url = if (queryParams.isNotEmpty()) "$baseUrl?$queryParams" else baseUrl loadUrl(url) } 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 43d1fe2ca..1b6c4bf27 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 @@ -63,7 +63,8 @@ class SelfVerificationActivity : AppCompatActivity() { return } - webViewHost = AndroidWebViewHost(this, router, isDebugMode) + val devServerUrl = intent.getStringExtra(EXTRA_DEV_SERVER_URL) + webViewHost = AndroidWebViewHost(this, router, isDebugMode, devServerUrl) val webView = webViewHost.createWebView(queryParams) setContentView(webView) } @@ -179,6 +180,7 @@ class SelfVerificationActivity : AppCompatActivity() { const val EXTRA_DEBUG_MODE = "xyz.self.sdk.DEBUG_MODE" const val EXTRA_VERIFICATION_REQUEST = "xyz.self.sdk.VERIFICATION_REQUEST" const val EXTRA_CONFIG = "xyz.self.sdk.CONFIG" + const val EXTRA_DEV_SERVER_URL = "xyz.self.sdk.DEV_SERVER_URL" const val RESULT_CODE_SUCCESS = RESULT_OK const val RESULT_CODE_ERROR = RESULT_FIRST_USER diff --git a/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SelfSdkConfig.kt b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SelfSdkConfig.kt index 3aaac15f5..5e3188b4a 100644 --- a/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SelfSdkConfig.kt +++ b/packages/kmp-sdk/shared/src/commonMain/kotlin/xyz/self/sdk/api/SelfSdkConfig.kt @@ -34,4 +34,5 @@ data class SelfSdkConfig( val appEndpoint: String? = null, val endpointType: String? = null, val chainID: Int? = null, + val devServerUrl: String? = null, ) diff --git a/packages/webview-app/src/App.tsx b/packages/webview-app/src/App.tsx index 0e06f52aa..dbb53ed5a 100644 --- a/packages/webview-app/src/App.tsx +++ b/packages/webview-app/src/App.tsx @@ -54,6 +54,7 @@ import { TourScreen as TunnelTourScreen } from './screens/tunnel/TourScreen'; import { TunnelCountryPickerScreen } from './screens/tunnel/TunnelCountryPickerScreen'; import { TunnelDiscloseScreen } from './screens/tunnel/TunnelDiscloseScreen'; import { TunnelIDTypeScreen } from './screens/tunnel/TunnelIDTypeScreen'; +import { TunnelKycFailureScreen } from './screens/tunnel/TunnelKycFailureScreen'; import { TunnelKycSuccessScreen } from './screens/tunnel/TunnelKycSuccessScreen'; import { TunnelKycWrapper } from './screens/tunnel/TunnelKycWrapper'; import { TunnelProofReceiptScreen } from './screens/tunnel/TunnelProofReceiptScreen'; @@ -110,6 +111,7 @@ export const App: React.FC = () => ( } /> } /> } /> + } /> } /> } /> } /> diff --git a/packages/webview-app/src/components/DevRouteMenu.tsx b/packages/webview-app/src/components/DevRouteMenu.tsx index 1975f2faf..b00193be3 100644 --- a/packages/webview-app/src/components/DevRouteMenu.tsx +++ b/packages/webview-app/src/components/DevRouteMenu.tsx @@ -14,6 +14,7 @@ interface DevScreenLink { interface DevScreenGroup { title: string; links: DevScreenLink[]; + description?: string; } const screenGroups: DevScreenGroup[] = [ @@ -72,15 +73,26 @@ const screenGroups: DevScreenGroup[] = [ ], }, { - title: 'Tunnel', + title: 'Tunnel — Screens', links: [ { href: '/tunnel/tour/1', label: 'Tour' }, - { href: '/tunnel/kyc', label: 'KYC Mock' }, { href: '/tunnel/registration/country', label: 'Country Picker' }, { href: '/tunnel/registration/id-type', label: 'ID Type' }, - { href: '/tunnel/proof/receipt', label: 'Proof Receipt' }, + { href: '/tunnel/kyc-failure', label: 'KYC Failure' }, + { href: '/tunnel/recovery-required', label: 'Recovery Required' }, { href: '/tunnel/proof/generating', label: 'Proving' }, { href: '/tunnel/proof/result', label: 'Result' }, + { href: '/tunnel/proof/receipt', label: 'Proof Receipt' }, + ], + }, + { + title: 'Tunnel — Mock KYC', + description: 'Mocks diverge after /tunnel/kyc; some outcomes intentionally share the same final route.', + links: [ + { href: '/tunnel/tour/1?mock=success', label: 'Flow → KYC Success, Then Proof Failure' }, + { href: '/tunnel/tour/1?mock=kyc-failure', label: 'Flow → KYC Error (Retryable)' }, + { href: '/tunnel/tour/1?mock=registration-failure', label: 'Flow → KYC Error (Fatal → Tour Step 4)' }, + { href: '/tunnel/tour/1?mock=cancel', label: 'Flow → KYC Cancel → Tour Step 4' }, ], }, { @@ -103,10 +115,10 @@ export const DevRouteMenu: React.FC = () => { } }, [isOpen]); - const currentLabel = useMemo( - () => allLinks.find(link => link.href === location.pathname)?.label ?? 'Dev Screens', - [location.pathname], - ); + const currentLabel = useMemo(() => { + const fullPath = `${location.pathname}${location.search}`; + return allLinks.find(link => link.href === fullPath)?.label ?? 'Dev Screens'; + }, [location.pathname, location.search]); return (
{ > {group.title}
+ {group.description ? ( +
+ {group.description} +
+ ) : null} {group.links.map(link => { - const isActive = location.pathname === link.href; + const isActive = `${location.pathname}${location.search}` === link.href; return ( + {onConfirm && ( + + )} + + ), ProofSuccessScreen: ({ onContinue, onViewDetails }: { onContinue: () => void; onViewDetails: () => void }) => (
), + KycVerificationSuccessScreen: ({ onGenerateProof }: { onGenerateProof: () => void }) => ( + + ), + KycFailureScreen: ({ onDismiss, onTryAgain }: { onDismiss: () => void; onTryAgain: () => void }) => ( +
+ + +
+ ), + LaunchTour1Screen: ({ onRestore }: { onRestore: () => void }) => ( + + ), + LaunchTour2Screen: () =>
tour-2
, + LaunchTour3Screen: () =>
tour-3
, + LaunchTour4Screen: () =>
tour-4
, + LaunchRecoveryScreen: ({ + onClose, + onEnterRecoveryPhrase, + }: { + onClose: () => void; + onEnterRecoveryPhrase: () => void; + }) => ( +
+ + +
+ ), + LeftArrowIcon: () => null, })); const LocationDisplay: React.FC = () => { @@ -138,6 +195,16 @@ const LocationDisplay: React.FC = () => { return
{`${location.pathname}${location.search}`}
; }; +const StateDisplay: React.FC = () => { + const location = useLocation(); + return ( + <> +
{`${location.pathname}${location.search}`}
+
{JSON.stringify(location.state)}
+ + ); +}; + const renderResultRoute = ( initialEntry: | string @@ -150,6 +217,8 @@ const renderResultRoute = ( } /> + } /> + } /> } /> } /> } /> @@ -159,6 +228,25 @@ const renderResultRoute = ( , ); +const renderReceiptRoute = ( + initialEntry: + | string + | { + pathname: string; + state?: unknown; + }, +) => + render( + + + } /> + } /> + } /> + + + , + ); + const renderProvingRoute = () => render( @@ -183,13 +271,59 @@ const renderDiscloseRoute = () => , ); +const renderKycSuccessRoute = ( + initialEntry: + | string + | { + pathname: string; + state?: unknown; + }, +) => + render( + + + } /> + } /> + } /> + } /> + } /> + + + , + ); + +const renderKycFailureRoute = () => + render( + + + } /> + } /> + } /> + + + , + ); + const renderRecoveryRequiredRoute = () => render( } /> } /> - } /> + } /> + + + , + ); + +const renderTourRestoreRoute = () => + render( + + + } /> + } /> + } /> + } /> , @@ -263,6 +397,39 @@ describe('tunnel flow screens', () => { expectLocation('/tunnel/proof/receipt'); }); + it('routes proving failure close back to tunnel tour step 4', () => { + renderResultRoute({ + pathname: '/tunnel/proof/result', + state: { success: false, error: 'TEE down', source: 'proving' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /close/i })); + + expectLocation('/tunnel/tour/4'); + }); + + it('keeps disclose failure close inside the tunnel disclose route', () => { + renderResultRoute({ + pathname: '/tunnel/proof/result', + state: { success: false, error: 'TEE down', source: 'disclose' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /close/i })); + + expectLocation('/tunnel/proof/disclose'); + }); + + it('keeps kyc failure close inside the tunnel kyc route', () => { + renderResultRoute({ + pathname: '/tunnel/proof/result', + state: { success: false, error: 'Provider cancelled', source: 'kyc' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /close/i })); + + expectLocation('/tunnel/kyc'); + }); + it('routes account recovery choice to the recovery-required screen', async () => { storeState.currentState = 'account_recovery_choice'; @@ -292,6 +459,65 @@ describe('tunnel flow screens', () => { expectLocation('/recovery/phrase-input?returnTo=%2Ftunnel%2Fproof%2Fgenerating'); }); + it('keeps recovery required cancel inside the tunnel flow', () => { + renderRecoveryRequiredRoute(); + + fireEvent.click(screen.getByRole('button', { name: /cancel/i })); + + expectLocation('/tunnel/tour/4'); + }); + + it('keeps receipt close on the provided tunnel back path', () => { + renderReceiptRoute({ + pathname: '/tunnel/proof/receipt', + state: { backPath: '/tunnel/proof/result' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /close receipt/i })); + + expectLocation('/tunnel/proof/result'); + }); + + it('restores result state when closing receipt', () => { + const resultState = { success: true }; + + render( + + + } /> + } /> + + , + ); + + fireEvent.click(screen.getByRole('button', { name: /close receipt/i })); + + expect(screen.getByTestId('location-state').textContent).toBe(JSON.stringify(resultState)); + }); + + it('keeps kyc cancel inside the tunnel flow', async () => { + renderKycSuccessRoute({ + pathname: '/tunnel/kyc-success', + state: { + providerResult: { + provider: 'didit', + status: 'cancel', + }, + }, + }); + + await waitFor(() => { + expectLocation('/tunnel/tour/4'); + }); + }); + it('routes to error result when proving setup throws before init starts', async () => { const { initSelfAppFromRequest } = await import('../../../src/utils/selfAppContext'); vi.mocked(initSelfAppFromRequest).mockImplementationOnce(() => { @@ -307,6 +533,156 @@ describe('tunnel flow screens', () => { expect(analytics.trackEvent).toHaveBeenCalledWith('tunnel_proving_init_failed', { error: 'bad request' }); }); + it('routes retryable kyc error to the failure screen', async () => { + renderKycSuccessRoute({ + pathname: '/tunnel/kyc-success', + state: { + providerResult: { + provider: 'didit', + status: 'error', + error: { code: 'provider_unknown_error', message: 'Something went wrong', retryable: true }, + }, + }, + }); + + await waitFor(() => { + expectLocation('/tunnel/kyc-failure'); + }); + }); + + it('routes non-retryable kyc error back to the tour', async () => { + renderKycSuccessRoute({ + pathname: '/tunnel/kyc-success', + state: { + providerResult: { + provider: 'didit', + status: 'error', + error: { code: 'provider_declined', message: 'Declined', retryable: false }, + }, + }, + }); + + await waitFor(() => { + expectLocation('/tunnel/tour/4'); + }); + }); + + it('retries kyc failure back into the tunnel kyc step', () => { + renderKycFailureRoute(); + + fireEvent.click(screen.getByRole('button', { name: /retry kyc/i })); + + expectLocation('/tunnel/kyc'); + }); + + it('dismisses kyc failure back to the tunnel tour', () => { + renderKycFailureRoute(); + + fireEvent.click(screen.getByRole('button', { name: /dismiss kyc failure/i })); + + expectLocation('/tunnel/tour/4'); + }); + + it('falls back to result screen when receipt has no backPath', () => { + renderReceiptRoute({ + pathname: '/tunnel/proof/receipt', + }); + + fireEvent.click(screen.getByRole('button', { name: /close receipt/i })); + + expectLocation('/tunnel/proof/result'); + }); + + it('defaults missing failure source close to tunnel tour step 4', () => { + renderResultRoute({ + pathname: '/tunnel/proof/result', + state: { success: false, error: 'Unknown error' }, + }); + + fireEvent.click(screen.getByRole('button', { name: /close/i })); + + expectLocation('/tunnel/tour/4'); + }); + + it('keeps tour restore back button inside the tunnel flow', () => { + renderTourRestoreRoute(); + + fireEvent.click(screen.getByRole('button', { name: /restore tour 1/i })); + fireEvent.click(screen.getByRole('button', { name: /back from recovery/i })); + + expectLocation('/tunnel/tour/1'); + }); + + it('forwards returnTo when entering recovery phrase from tunnel tour', () => { + renderTourRestoreRoute(); + + fireEvent.click(screen.getByRole('button', { name: /restore tour 1/i })); + fireEvent.click(screen.getByRole('button', { name: /enter recovery phrase/i })); + + expectLocation(`/recovery/phrase-input?returnTo=${encodeURIComponent('/tunnel/tour/1')}`); + }); + + it('hides confirm button on receipt when backState is missing', () => { + render( + + + } /> + } /> + + , + ); + + expect(screen.queryByRole('button', { name: /confirm receipt/i })).toBeNull(); + }); + + it('hides confirm button on receipt when opened from failure context', () => { + render( + + + } /> + } /> + + , + ); + + expect(screen.queryByRole('button', { name: /confirm receipt/i })).toBeNull(); + expect(screen.getByRole('button', { name: /close receipt/i })).toBeTruthy(); + }); + + it('shows confirm button on receipt when opened from success context', () => { + render( + + + } /> + } /> + + , + ); + + expect(screen.getByRole('button', { name: /confirm receipt/i })).toBeTruthy(); + }); + it('routes to error result when disclose setup throws before init starts', async () => { const { initSelfAppFromRequest } = await import('../../../src/utils/selfAppContext'); vi.mocked(initSelfAppFromRequest).mockImplementationOnce(() => { diff --git a/packages/webview-app/vite.config.ts b/packages/webview-app/vite.config.ts index b31be1b04..f3d54f22f 100644 --- a/packages/webview-app/vite.config.ts +++ b/packages/webview-app/vite.config.ts @@ -85,5 +85,5 @@ export default defineConfig({ emptyOutDir: true, sourcemap: true, }, - server: { host: '0.0.0.0', port: 5173 }, + server: { host: '0.0.0.0', port: 5173, allowedHosts: ['.ngrok-free.app'] }, });