mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
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
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,4 +34,5 @@ data class SelfSdkConfig(
|
||||
val appEndpoint: String? = null,
|
||||
val endpointType: String? = null,
|
||||
val chainID: Int? = null,
|
||||
val devServerUrl: String? = null,
|
||||
)
|
||||
|
||||
@@ -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 = () => (
|
||||
<Route path="/coming-soon" element={<ComingSoonScreen />} />
|
||||
<Route path="/tunnel/tour/:step" element={<TunnelTourScreen />} />
|
||||
<Route path="/tunnel/kyc" element={<TunnelKycWrapper />} />
|
||||
<Route path="/tunnel/kyc-failure" element={<TunnelKycFailureScreen />} />
|
||||
<Route path="/tunnel/kyc-success" element={<TunnelKycSuccessScreen />} />
|
||||
<Route path="/tunnel/registration/country" element={<TunnelCountryPickerScreen />} />
|
||||
<Route path="/tunnel/registration/id-type" element={<TunnelIDTypeScreen />} />
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
@@ -150,8 +162,20 @@ export const DevRouteMenu: React.FC = () => {
|
||||
>
|
||||
{group.title}
|
||||
</div>
|
||||
{group.description ? (
|
||||
<div
|
||||
style={{
|
||||
color: 'rgba(255, 255, 255, 0.65)',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.4,
|
||||
padding: '0 2px 6px',
|
||||
}}
|
||||
>
|
||||
{group.description}
|
||||
</div>
|
||||
) : null}
|
||||
{group.links.map(link => {
|
||||
const isActive = location.pathname === link.href;
|
||||
const isActive = `${location.pathname}${location.search}` === link.href;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -21,6 +21,7 @@ const CONTAINER_ID = 'didit-sdk-container';
|
||||
type Phase = 'loading' | 'active' | 'waiting' | 'error';
|
||||
|
||||
interface ProviderLaunchState {
|
||||
backPath?: string;
|
||||
countryCode?: string;
|
||||
documentType?: string;
|
||||
nextPath?: string;
|
||||
@@ -32,9 +33,10 @@ export const ProviderLaunchScreen: React.FC = () => {
|
||||
const { client, analytics, haptic, lifecycle } = useSelfClient();
|
||||
const { verificationId: ctxVerificationId, environment } = useVerificationRequest();
|
||||
|
||||
const { countryCode = '', documentType = '', nextPath } = (location.state as ProviderLaunchState) || {};
|
||||
const { backPath, countryCode = '', documentType = '', nextPath } = (location.state as ProviderLaunchState) || {};
|
||||
|
||||
const defaultNextPath = nextPath ?? '/onboarding/provider-result';
|
||||
const isTunnelFlow = defaultNextPath.startsWith('/tunnel/') || backPath?.startsWith('/tunnel/') === true;
|
||||
const verificationId = ctxVerificationId ?? `didit-${Date.now()}`;
|
||||
|
||||
const [phase, setPhase] = useState<Phase>('loading');
|
||||
@@ -222,13 +224,20 @@ export const ProviderLaunchScreen: React.FC = () => {
|
||||
countryCode,
|
||||
documentType,
|
||||
});
|
||||
|
||||
if (isTunnelFlow) {
|
||||
navigate(backPath ?? '/tunnel/tour/4', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
lifecycle.dismiss({ reason: 'back' });
|
||||
if (window.history.length > 1) {
|
||||
navigate(-1);
|
||||
} else {
|
||||
navigate('/', { state: { skipOnboardingRedirect: true } });
|
||||
return;
|
||||
}
|
||||
}, [analytics, countryCode, documentType, haptic, lifecycle, navigate]);
|
||||
|
||||
navigate('/', { state: { skipOnboardingRedirect: true } });
|
||||
}, [analytics, backPath, countryCode, documentType, haptic, isTunnelFlow, lifecycle, navigate]);
|
||||
|
||||
const handleRetry = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { LaunchRecoveryScreen as EuclidLaunchRecoveryScreen, LeftArrowIcon } from '@selfxyz/euclid';
|
||||
|
||||
@@ -12,19 +12,26 @@ import { useSelfClient } from '../../providers/SelfClientProvider';
|
||||
import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
|
||||
export const LaunchRecoveryScreen: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { analytics, haptic } = useSelfClient();
|
||||
const backPath = (location.state as { backPath?: string } | null)?.backPath ?? '/settings/security';
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
navigate('/settings/security');
|
||||
}, [navigate, haptic]);
|
||||
navigate(backPath, { replace: true });
|
||||
}, [backPath, navigate, haptic]);
|
||||
|
||||
const isTunnelFlow = backPath.startsWith('/tunnel/');
|
||||
|
||||
const onEnterRecoveryPhrase = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('recovery_enter_phrase_pressed');
|
||||
navigate('/recovery/phrase-input');
|
||||
}, [navigate, haptic, analytics]);
|
||||
const target = isTunnelFlow
|
||||
? `/recovery/phrase-input?returnTo=${encodeURIComponent(backPath)}`
|
||||
: '/recovery/phrase-input';
|
||||
navigate(target);
|
||||
}, [backPath, isTunnelFlow, navigate, haptic, analytics]);
|
||||
|
||||
return (
|
||||
<div className="launch-recovery-screen">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { LaunchTour1Screen, LaunchTour2Screen, LaunchTour3Screen, LaunchTour4Screen } from '@selfxyz/euclid';
|
||||
import { loadSelectedDocument } from '@selfxyz/mobile-sdk-alpha/browser';
|
||||
@@ -14,13 +14,15 @@ import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
|
||||
export const TourScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { step } = useParams<{ step: string }>();
|
||||
const stepNum = parseInt(step ?? '1', 10);
|
||||
const { client } = useSelfClient();
|
||||
const mockParam = import.meta.env.DEV ? location.search : '';
|
||||
|
||||
const onNext = useCallback(async () => {
|
||||
if (stepNum < 4) {
|
||||
navigate(`/tunnel/tour/${stepNum + 1}`);
|
||||
navigate(`/tunnel/tour/${stepNum + 1}${mockParam}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,12 +36,12 @@ export const TourScreen: React.FC = () => {
|
||||
// Fall through to KYC when document state is unavailable.
|
||||
}
|
||||
|
||||
navigate('/tunnel/kyc');
|
||||
}, [navigate, stepNum, client]);
|
||||
navigate(`/tunnel/kyc${mockParam}`);
|
||||
}, [navigate, stepNum, client, mockParam]);
|
||||
|
||||
const onRestore = useCallback(() => {
|
||||
navigate('/recovery');
|
||||
}, []);
|
||||
navigate('/recovery', { state: { backPath: `/tunnel/tour/${step ?? '1'}` } });
|
||||
}, [navigate, step]);
|
||||
|
||||
switch (step) {
|
||||
case '1':
|
||||
|
||||
@@ -51,7 +51,7 @@ export const TunnelIDTypeScreen: React.FC = () => {
|
||||
countryName={getCountryName(countryCode)}
|
||||
idTypes={idTypes}
|
||||
onIDTypeSelect={onIDTypeSelect}
|
||||
onBack={() => navigate(-1)}
|
||||
onBack={() => navigate('/tunnel/registration/country')}
|
||||
renderFlag={renderFlag}
|
||||
renderIDTypeIcon={renderIDTypeIcon}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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.
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { KycFailureScreen as EuclidKycFailureScreen } from '@selfxyz/euclid';
|
||||
|
||||
import { useSelfClient } from '../../providers/SelfClientProvider';
|
||||
import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
|
||||
export const TunnelKycFailureScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { analytics, haptic } = useSelfClient();
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('tunnel_kyc_failure_dismissed');
|
||||
navigate('/tunnel/tour/4', { replace: true });
|
||||
}, [analytics, haptic, navigate]);
|
||||
|
||||
const handleTryAgain = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('tunnel_kyc_failure_retry_pressed');
|
||||
navigate('/tunnel/kyc', { replace: true });
|
||||
}, [analytics, haptic, navigate]);
|
||||
|
||||
return <EuclidKycFailureScreen {...WEB_SAFE_AREA} onDismiss={handleDismiss} onTryAgain={handleTryAgain} />;
|
||||
};
|
||||
@@ -24,15 +24,15 @@ export const TunnelKycSuccessScreen: React.FC = () => {
|
||||
if (!providerResult) return;
|
||||
|
||||
if (providerResult.status === 'cancel') {
|
||||
navigate(-1);
|
||||
navigate('/tunnel/tour/4', { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (providerResult.status === 'error') {
|
||||
if (providerResult.error?.retryable === false) {
|
||||
navigate('/onboarding/failure', { replace: true });
|
||||
navigate('/tunnel/tour/4', { replace: true });
|
||||
} else {
|
||||
navigate('/tunnel/kyc', { replace: true });
|
||||
navigate('/tunnel/kyc-failure', { replace: true, state: { providerResult } });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,29 @@
|
||||
import type React from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { createMockProviderResult, getMockOutcomeFromSearch } from '../../utils/mockOnboardingFlow';
|
||||
|
||||
/**
|
||||
* Redirects `/tunnel/kyc` to `ProviderLaunchScreen` at `/onboarding/provider`
|
||||
* with the tunnel-specific `nextPath` injected into navigation state.
|
||||
*
|
||||
* In dev mode, supports `?mock=kyc-failure|registration-failure|cancel|success`
|
||||
* to skip the real provider and jump straight to the result screen.
|
||||
*/
|
||||
export const TunnelKycWrapper: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const incomingState = (location.state as Record<string, unknown>) ?? {};
|
||||
const mockOutcome = getMockOutcomeFromSearch(location.search);
|
||||
|
||||
if (import.meta.env.DEV && location.search.includes('mock=')) {
|
||||
return (
|
||||
<Navigate
|
||||
to="/tunnel/kyc-success"
|
||||
replace
|
||||
state={{ providerResult: createMockProviderResult({ outcome: mockOutcome }) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Navigate
|
||||
@@ -19,6 +35,7 @@ export const TunnelKycWrapper: React.FC = () => {
|
||||
replace
|
||||
state={{
|
||||
...incomingState,
|
||||
backPath: '/tunnel/tour/4',
|
||||
nextPath: '/tunnel/kyc-success',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { ProofRequestScreen, SelfLogo } from '@selfxyz/euclid';
|
||||
|
||||
@@ -14,9 +14,14 @@ import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
import { titleCaseDisclosure } from '../../utils/provingUtils';
|
||||
|
||||
export const TunnelProofReceiptScreen: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { analytics, haptic } = useSelfClient();
|
||||
const { displayLabels, request, appName, appEndpoint, timestamp } = useVerificationRequest();
|
||||
const { backPath = '/tunnel/proof/result', backState } =
|
||||
(location.state as { backPath?: string; backState?: Record<string, unknown> } | null) ?? {};
|
||||
|
||||
const showConfirm = backPath !== '/tunnel/proof/result' || backState?.success === true;
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
@@ -36,15 +41,15 @@ export const TunnelProofReceiptScreen: React.FC = () => {
|
||||
const onClose = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('tunnel_proof_receipt_closed');
|
||||
navigate(-1);
|
||||
}, [navigate, haptic, analytics]);
|
||||
navigate(backPath, { replace: true, state: backState });
|
||||
}, [analytics, backPath, backState, haptic, navigate]);
|
||||
|
||||
return (
|
||||
<ProofRequestScreen
|
||||
{...WEB_SAFE_AREA}
|
||||
variant="default"
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
onConfirm={showConfirm ? onConfirm : undefined}
|
||||
appIcon={<SelfLogo size={40} />}
|
||||
appName={appName}
|
||||
appEndpoint={appEndpoint}
|
||||
|
||||
@@ -12,10 +12,11 @@ import { useSelfClient } from '../../providers/SelfClientProvider';
|
||||
import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
|
||||
const TUNNEL_RECOVERY_RETURN_PATH = '/tunnel/proof/generating';
|
||||
const TUNNEL_RECOVERY_BACK_PATH = '/tunnel/tour/4';
|
||||
|
||||
export const TunnelRecoveryRequiredScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { analytics, haptic, lifecycle } = useSelfClient();
|
||||
const { analytics, haptic } = useSelfClient();
|
||||
|
||||
const onRecoverWithPhrase = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
@@ -26,9 +27,8 @@ export const TunnelRecoveryRequiredScreen: React.FC = () => {
|
||||
const onCancel = useCallback(() => {
|
||||
haptic.trigger('selection');
|
||||
analytics.trackEvent('tunnel_recovery_cancelled');
|
||||
lifecycle.dismiss({ reason: 'back' });
|
||||
navigate('/', { replace: true });
|
||||
}, [analytics, haptic, lifecycle, navigate]);
|
||||
navigate(TUNNEL_RECOVERY_BACK_PATH, { replace: true });
|
||||
}, [analytics, haptic, navigate]);
|
||||
|
||||
return (
|
||||
<EuclidConflictDetectedScreen
|
||||
|
||||
@@ -16,9 +16,33 @@ import { WEB_SAFE_AREA } from '../../utils/insets';
|
||||
interface TunnelResultState {
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
source?: 'proving' | 'disclose';
|
||||
source?: 'disclose' | 'kyc' | 'proving';
|
||||
}
|
||||
|
||||
const getTunnelBackPath = (source: TunnelResultState['source']): string => {
|
||||
switch (source) {
|
||||
case 'disclose':
|
||||
return '/tunnel/proof/disclose';
|
||||
case 'kyc':
|
||||
return '/tunnel/kyc';
|
||||
case 'proving':
|
||||
default:
|
||||
return '/tunnel/proof/generating';
|
||||
}
|
||||
};
|
||||
|
||||
const getTunnelClosePath = (source: TunnelResultState['source']): string => {
|
||||
switch (source) {
|
||||
case 'disclose':
|
||||
return '/tunnel/proof/disclose';
|
||||
case 'kyc':
|
||||
return '/tunnel/kyc';
|
||||
case 'proving':
|
||||
default:
|
||||
return '/tunnel/tour/4';
|
||||
}
|
||||
};
|
||||
|
||||
export const TunnelResultScreen: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@@ -51,17 +75,18 @@ export const TunnelResultScreen: React.FC = () => {
|
||||
}, [request.userId, verificationId, lifecycle, analytics]);
|
||||
|
||||
const onRetry = useCallback(() => {
|
||||
navigate(source === 'disclose' ? '/tunnel/proof/disclose' : '/tunnel/proof/generating');
|
||||
navigate(getTunnelBackPath(source), { replace: true });
|
||||
}, [navigate, source]);
|
||||
|
||||
const onViewDetails = useCallback(() => {
|
||||
navigate('/tunnel/proof/receipt');
|
||||
}, [navigate]);
|
||||
navigate('/tunnel/proof/receipt', {
|
||||
state: { backPath: location.pathname, backState: location.state },
|
||||
});
|
||||
}, [location.pathname, location.state, navigate]);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
lifecycle.dismiss({ reason: 'back' });
|
||||
navigate('/');
|
||||
}, [lifecycle, navigate]);
|
||||
navigate(getTunnelClosePath(source), { replace: true });
|
||||
}, [navigate, source]);
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,12 @@ import type React from 'react';
|
||||
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { LaunchRecoveryScreen } from '../../../src/screens/recovery/LaunchRecoveryScreen';
|
||||
import { TourScreen as TunnelTourScreen } from '../../../src/screens/tunnel/TourScreen';
|
||||
import { TunnelDiscloseScreen } from '../../../src/screens/tunnel/TunnelDiscloseScreen';
|
||||
import { TunnelKycFailureScreen } from '../../../src/screens/tunnel/TunnelKycFailureScreen';
|
||||
import { TunnelKycSuccessScreen } from '../../../src/screens/tunnel/TunnelKycSuccessScreen';
|
||||
import { TunnelProofReceiptScreen } from '../../../src/screens/tunnel/TunnelProofReceiptScreen';
|
||||
import { TunnelProvingScreen } from '../../../src/screens/tunnel/TunnelProvingScreen';
|
||||
import { TunnelRecoveryRequiredScreen } from '../../../src/screens/tunnel/TunnelRecoveryRequiredScreen';
|
||||
import { TunnelResultScreen } from '../../../src/screens/tunnel/TunnelResultScreen';
|
||||
@@ -84,6 +89,18 @@ vi.mock('@selfxyz/euclid', () => ({
|
||||
SelfLogo: () => null,
|
||||
ProofGenerationScreen: ({ step }: { step: string }) => <div>{`proof-generation:${step}`}</div>,
|
||||
ProofProgressScreen: ({ step }: { step: string }) => <div>{`proof-progress:${step}`}</div>,
|
||||
ProofRequestScreen: ({ onClose, onConfirm }: { onClose: () => void; onConfirm?: () => void }) => (
|
||||
<div>
|
||||
<button onClick={onClose} type="button">
|
||||
Close receipt
|
||||
</button>
|
||||
{onConfirm && (
|
||||
<button onClick={onConfirm} type="button">
|
||||
Confirm receipt
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
ProofSuccessScreen: ({ onContinue, onViewDetails }: { onContinue: () => void; onViewDetails: () => void }) => (
|
||||
<div>
|
||||
<button onClick={onContinue} type="button">
|
||||
@@ -131,6 +148,46 @@ vi.mock('@selfxyz/euclid', () => ({
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
KycVerificationSuccessScreen: ({ onGenerateProof }: { onGenerateProof: () => void }) => (
|
||||
<button onClick={onGenerateProof} type="button">
|
||||
Generate proof
|
||||
</button>
|
||||
),
|
||||
KycFailureScreen: ({ onDismiss, onTryAgain }: { onDismiss: () => void; onTryAgain: () => void }) => (
|
||||
<div>
|
||||
<button onClick={onDismiss} type="button">
|
||||
Dismiss KYC failure
|
||||
</button>
|
||||
<button onClick={onTryAgain} type="button">
|
||||
Retry KYC
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
LaunchTour1Screen: ({ onRestore }: { onRestore: () => void }) => (
|
||||
<button onClick={onRestore} type="button">
|
||||
Restore tour 1
|
||||
</button>
|
||||
),
|
||||
LaunchTour2Screen: () => <div>tour-2</div>,
|
||||
LaunchTour3Screen: () => <div>tour-3</div>,
|
||||
LaunchTour4Screen: () => <div>tour-4</div>,
|
||||
LaunchRecoveryScreen: ({
|
||||
onClose,
|
||||
onEnterRecoveryPhrase,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onEnterRecoveryPhrase: () => void;
|
||||
}) => (
|
||||
<div>
|
||||
<button onClick={onClose} type="button">
|
||||
Back from recovery
|
||||
</button>
|
||||
<button onClick={onEnterRecoveryPhrase} type="button">
|
||||
Enter recovery phrase
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
LeftArrowIcon: () => null,
|
||||
}));
|
||||
|
||||
const LocationDisplay: React.FC = () => {
|
||||
@@ -138,6 +195,16 @@ const LocationDisplay: React.FC = () => {
|
||||
return <div data-testid="location">{`${location.pathname}${location.search}`}</div>;
|
||||
};
|
||||
|
||||
const StateDisplay: React.FC = () => {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<>
|
||||
<div data-testid="location">{`${location.pathname}${location.search}`}</div>
|
||||
<div data-testid="location-state">{JSON.stringify(location.state)}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderResultRoute = (
|
||||
initialEntry:
|
||||
| string
|
||||
@@ -150,6 +217,8 @@ const renderResultRoute = (
|
||||
<MemoryRouter initialEntries={[initialEntry]}>
|
||||
<Routes>
|
||||
<Route path="/tunnel/proof/result" element={<TunnelResultScreen />} />
|
||||
<Route path="/tunnel/tour/4" element={<LocationDisplay />} />
|
||||
<Route path="/tunnel/kyc" element={<LocationDisplay />} />
|
||||
<Route path="/tunnel/proof/generating" element={<LocationDisplay />} />
|
||||
<Route path="/tunnel/proof/disclose" element={<LocationDisplay />} />
|
||||
<Route path="/tunnel/proof/receipt" element={<LocationDisplay />} />
|
||||
@@ -159,6 +228,25 @@ const renderResultRoute = (
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const renderReceiptRoute = (
|
||||
initialEntry:
|
||||
| string
|
||||
| {
|
||||
pathname: string;
|
||||
state?: unknown;
|
||||
},
|
||||
) =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={[initialEntry]}>
|
||||
<Routes>
|
||||
<Route path="/tunnel/proof/receipt" element={<TunnelProofReceiptScreen />} />
|
||||
<Route path="/tunnel/kyc-success" element={<LocationDisplay />} />
|
||||
<Route path="/tunnel/proof/result" element={<LocationDisplay />} />
|
||||
</Routes>
|
||||
<LocationDisplay />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const renderProvingRoute = () =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/tunnel/proof/generating']}>
|
||||
@@ -183,13 +271,59 @@ const renderDiscloseRoute = () =>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const renderKycSuccessRoute = (
|
||||
initialEntry:
|
||||
| string
|
||||
| {
|
||||
pathname: string;
|
||||
state?: unknown;
|
||||
},
|
||||
) =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={[initialEntry]}>
|
||||
<Routes>
|
||||
<Route path="/tunnel/kyc-success" element={<TunnelKycSuccessScreen />} />
|
||||
<Route path="/tunnel/kyc" element={<LocationDisplay />} />
|
||||
<Route path="/tunnel/kyc-failure" element={<LocationDisplay />} />
|
||||
<Route path="/tunnel/tour/4" element={<LocationDisplay />} />
|
||||
<Route path="/tunnel/proof/generating" element={<LocationDisplay />} />
|
||||
</Routes>
|
||||
<LocationDisplay />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const renderKycFailureRoute = () =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/tunnel/kyc-failure']}>
|
||||
<Routes>
|
||||
<Route path="/tunnel/kyc-failure" element={<TunnelKycFailureScreen />} />
|
||||
<Route path="/tunnel/kyc" element={<LocationDisplay />} />
|
||||
<Route path="/tunnel/tour/4" element={<LocationDisplay />} />
|
||||
</Routes>
|
||||
<LocationDisplay />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const renderRecoveryRequiredRoute = () =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/tunnel/recovery-required']}>
|
||||
<Routes>
|
||||
<Route path="/tunnel/recovery-required" element={<TunnelRecoveryRequiredScreen />} />
|
||||
<Route path="/recovery/phrase-input" element={<LocationDisplay />} />
|
||||
<Route path="/" element={<LocationDisplay />} />
|
||||
<Route path="/tunnel/tour/4" element={<LocationDisplay />} />
|
||||
</Routes>
|
||||
<LocationDisplay />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const renderTourRestoreRoute = () =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/tunnel/tour/1']}>
|
||||
<Routes>
|
||||
<Route path="/tunnel/tour/:step" element={<TunnelTourScreen />} />
|
||||
<Route path="/recovery" element={<LaunchRecoveryScreen />} />
|
||||
<Route path="/recovery/phrase-input" element={<LocationDisplay />} />
|
||||
<Route path="/settings/security" element={<LocationDisplay />} />
|
||||
</Routes>
|
||||
<LocationDisplay />
|
||||
</MemoryRouter>,
|
||||
@@ -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(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
{
|
||||
pathname: '/tunnel/proof/receipt',
|
||||
state: { backPath: '/tunnel/proof/result', backState: resultState },
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/tunnel/proof/receipt" element={<TunnelProofReceiptScreen />} />
|
||||
<Route path="/tunnel/proof/result" element={<StateDisplay />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
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(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
{
|
||||
pathname: '/tunnel/proof/receipt',
|
||||
state: { backPath: '/tunnel/proof/result' },
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/tunnel/proof/receipt" element={<TunnelProofReceiptScreen />} />
|
||||
<Route path="/tunnel/proof/result" element={<LocationDisplay />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: /confirm receipt/i })).toBeNull();
|
||||
});
|
||||
|
||||
it('hides confirm button on receipt when opened from failure context', () => {
|
||||
render(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
{
|
||||
pathname: '/tunnel/proof/receipt',
|
||||
state: { backPath: '/tunnel/proof/result', backState: { success: false, error: 'TEE down' } },
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/tunnel/proof/receipt" element={<TunnelProofReceiptScreen />} />
|
||||
<Route path="/tunnel/proof/result" element={<LocationDisplay />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
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(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
{
|
||||
pathname: '/tunnel/proof/receipt',
|
||||
state: { backPath: '/tunnel/proof/result', backState: { success: true } },
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/tunnel/proof/receipt" element={<TunnelProofReceiptScreen />} />
|
||||
<Route path="/tunnel/proof/result" element={<LocationDisplay />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -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'] },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user