Add recovery and backup screen surfaces (WV-15) (#1876)

* Add recovery and backup screens (SELF-2423)

Add 5 recovery/backup screen wrappers around euclid components: BackupMethodPickerScreen, RecoveryPhraseScreen, LaunchRecoveryScreen, SecretPhraseInputScreen, and RecoverySuccessScreen. Wire SecurityScreen actions to the new routes instead of /coming-soon. Register all routes in App.tsx.

* Add background image to LaunchRecoveryScreen and animation assets (SELF-2423)

Add dialogue-background.jpg and Lottie animation JSON files to public/ for proper screen rendering. Pass backgroundImage prop to LaunchRecoveryScreen for visual consistency with other dialogue screens.

* add test

* link up

* fixes

* revert fix. we need to fix in euclid

* update euclic

* dev menu dx, add password

* fix launch recovery screen

* fix recovery success screen

* fix recovery phrase tests

* updates

* fixes

* fixes

---------

Co-authored-by: Tranquil-Flow <tranquil_flow@protonmail.com>
This commit is contained in:
Justin Hernandez
2026-03-29 23:00:12 -07:00
committed by GitHub
parent 80488dd5c0
commit b433fe1b85
19 changed files with 1069 additions and 127 deletions

View File

@@ -17,8 +17,9 @@
"types": "tsc --noEmit"
},
"dependencies": {
"@selfxyz/euclid": "1.2.6",
"@selfxyz/euclid-core": "1.2.6",
"@scure/bip39": "^1.6.0",
"@selfxyz/euclid": "1.3.0",
"@selfxyz/euclid-core": "1.3.0",
"@selfxyz/mobile-sdk-alpha": "workspace:^",
"@selfxyz/webview-bridge": "workspace:^",
"@sumsub/websdk": "^2.0.0",

View File

@@ -0,0 +1,22 @@
<svg width="37" height="37" viewBox="0 0 37 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_16462_4504)">
<path d="M18.5051 14.1988H18.5C16.1245 14.1988 14.1987 16.1245 14.1987 18.5V18.5051C14.1987 20.8807 16.1245 22.8064 18.5 22.8064H18.5051C20.8806 22.8064 22.8064 20.8807 22.8064 18.5051V18.5C22.8064 16.1245 20.8806 14.1988 18.5051 14.1988Z" fill="#00FFB6"/>
<path d="M10.0619 14.5174C10.0619 11.9633 12.1329 9.89236 14.6869 9.89236H23.6183L33.5107 0H8.84917L0 8.84917V23.4076H10.0619V14.5122V14.5174Z" fill="url(#paint0_linear_16462_4504)"/>
<path d="M26.9381 13.5564V22.1435C26.9381 24.6975 24.8671 26.7685 22.3131 26.7685H13.726L3.48932 37.0051H28.1508L37 28.156V13.5615H26.9381V13.5564Z" fill="url(#paint1_linear_16462_4504)"/>
</g>
<defs>
<linearGradient id="paint0_linear_16462_4504" x1="0" y1="11.7038" x2="33.5107" y2="11.7038" gradientUnits="userSpaceOnUse">
<stop stop-color="#E2EDF8"/>
<stop offset="0.63" stop-color="white"/>
<stop offset="1" stop-color="#EAF1F9"/>
</linearGradient>
<linearGradient id="paint1_linear_16462_4504" x1="3.48932" y1="25.2808" x2="37" y2="25.2808" gradientUnits="userSpaceOnUse">
<stop stop-color="#E2EDF8"/>
<stop offset="0.63" stop-color="white"/>
<stop offset="1" stop-color="#EAF1F9"/>
</linearGradient>
<clipPath id="clip0_16462_4504">
<rect width="37" height="37" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -6,6 +6,7 @@ import type React from 'react';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { DevRouteMenu } from './components/DevRouteMenu';
import { PasswordGate } from './components/PasswordGate';
import { SelfClientProvider } from './providers/SelfClientProvider';
import { VerificationRequestProvider } from './providers/VerificationRequestProvider';
import { DevModeScreen } from './screens/account/DevModeScreen';
@@ -41,6 +42,11 @@ import { ProofSuccessBackupScreen } from './screens/proving/ProofSuccessBackupSc
import { ProvingScreen } from './screens/proving/ProvingScreen';
import { SimpleDialogueScreen } from './screens/proving/SimpleDialogueScreen';
import { VerificationResultScreen } from './screens/proving/VerificationResultScreen';
import { BackupMethodPickerScreen } from './screens/recovery/BackupMethodPickerScreen';
import { LaunchRecoveryScreen } from './screens/recovery/LaunchRecoveryScreen';
import { RecoveryPhraseScreen } from './screens/recovery/RecoveryPhraseScreen';
import { RecoverySuccessScreen } from './screens/recovery/RecoverySuccessScreen';
import { SecretPhraseInputScreen } from './screens/recovery/SecretPhraseInputScreen';
import { KycMockScreen } from './screens/tunnel/KycMockScreen';
import { TourScreen as TunnelTourScreen } from './screens/tunnel/TourScreen';
import { TunnelCountryPickerScreen } from './screens/tunnel/TunnelCountryPickerScreen';
@@ -50,55 +56,62 @@ import { TunnelProvingScreen } from './screens/tunnel/TunnelProvingScreen';
import { TunnelResultScreen } from './screens/tunnel/TunnelResultScreen';
export const App: React.FC = () => (
<BrowserRouter>
<VerificationRequestProvider>
<SelfClientProvider>
<Routes>
<Route path="/" element={<HomeScreen />} />
<Route path="/onboarding/tour/:step" element={<TourScreen />} />
<Route path="/onboarding/country" element={<CountryPickerScreen />} />
<Route path="/onboarding/id-type" element={<IDSelectionScreen />} />
<Route path="/onboarding/provider" element={<ProviderLaunchScreen />} />
<Route path="/onboarding/provider-result" element={<ProviderResultScreen />} />
<Route path="/onboarding/confirm" element={<ConfirmIdentificationScreen />} />
<Route path="/onboarding/success" element={<ScanSuccessScreen />} />
<Route path="/onboarding/failure" element={<RegistrationFailureScreen />} />
<Route path="/onboarding/kyc-failure" element={<KycFailureScreen />} />
<Route path="/proving" element={<ProvingScreen />} />
<Route path="/proving/result" element={<VerificationResultScreen />} />
<Route path="/settings" element={<SettingsScreen />} />
<Route path="/settings/security" element={<SecurityScreen />} />
<Route path="/settings/notifications" element={<NotificationPreferencesScreen />} />
<Route path="/settings/dev-mode" element={<DevModeScreen />} />
{import.meta.env.DEV && <Route path="/debug/keychain" element={<KeychainDebugScreen />} />}
<Route path="/onboarding/backup" element={<SocialSignOnMethodPickerScreen />} />
<Route path="/onboarding/signin" element={<SocialSignOnPickerScreen />} />
<Route path="/onboarding/conflict" element={<ConflictDetectedScreen />} />
<Route path="/onboarding/notifications" element={<PushNotificationPromptScreen />} />
<Route path="/proving/receipt" element={<ProofRequestReceiptScreen />} />
<Route path="/proving/history" element={<ProofHistoryScreen />} />
<Route path="/proving/dialogue" element={<SimpleDialogueScreen />} />
<Route path="/proving/dialogue-cta" element={<DialogueWithCtaScreen />} />
<Route path="/proving/generation-dialogue" element={<ProofGenerationDialogueScreen />} />
<Route path="/proving/generation-success" element={<ProofGenerationSuccessScreen />} />
<Route path="/proving/backup-prompt" element={<ProofSuccessBackupScreen />} />
<Route path="/proving/kyc-pending" element={<KycPendingScreen />} />
<Route path="/proving/kyc-success" element={<KycSuccessScreen />} />
<Route path="/account/verified" element={<VerificationResultScreen />} />
<Route path="/id-data" element={<IDDataScreen />} />
<Route path="/manage-documents" element={<ManageDocumentsScreen />} />
<Route path="/coming-soon" element={<ComingSoonScreen />} />
<Route path="/tunnel/tour/:step" element={<TunnelTourScreen />} />
<Route path="/tunnel/kyc" element={<KycMockScreen />} />
<Route path="/tunnel/registration/country" element={<TunnelCountryPickerScreen />} />
<Route path="/tunnel/registration/id-type" element={<TunnelIDTypeScreen />} />
<Route path="/tunnel/proof/receipt" element={<TunnelProofReceiptScreen />} />
<Route path="/tunnel/proof/generating" element={<TunnelProvingScreen />} />
<Route path="/tunnel/proof/result" element={<TunnelResultScreen />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
{import.meta.env.DEV && <DevRouteMenu />}
</SelfClientProvider>
</VerificationRequestProvider>
</BrowserRouter>
<PasswordGate>
<BrowserRouter>
<VerificationRequestProvider>
<SelfClientProvider>
<Routes>
<Route path="/" element={<HomeScreen />} />
<Route path="/onboarding/tour/:step" element={<TourScreen />} />
<Route path="/onboarding/country" element={<CountryPickerScreen />} />
<Route path="/onboarding/id-type" element={<IDSelectionScreen />} />
<Route path="/onboarding/provider" element={<ProviderLaunchScreen />} />
<Route path="/onboarding/provider-result" element={<ProviderResultScreen />} />
<Route path="/onboarding/confirm" element={<ConfirmIdentificationScreen />} />
<Route path="/onboarding/success" element={<ScanSuccessScreen />} />
<Route path="/onboarding/failure" element={<RegistrationFailureScreen />} />
<Route path="/onboarding/kyc-failure" element={<KycFailureScreen />} />
<Route path="/proving" element={<ProvingScreen />} />
<Route path="/proving/result" element={<VerificationResultScreen />} />
<Route path="/settings" element={<SettingsScreen />} />
<Route path="/settings/security" element={<SecurityScreen />} />
<Route path="/settings/notifications" element={<NotificationPreferencesScreen />} />
<Route path="/settings/dev-mode" element={<DevModeScreen />} />
{import.meta.env.DEV && <Route path="/debug/keychain" element={<KeychainDebugScreen />} />}
<Route path="/settings/backup" element={<BackupMethodPickerScreen />} />
<Route path="/settings/recovery-phrase" element={<RecoveryPhraseScreen />} />
<Route path="/recovery" element={<LaunchRecoveryScreen />} />
<Route path="/recovery/phrase-input" element={<SecretPhraseInputScreen />} />
<Route path="/recovery/success" element={<RecoverySuccessScreen />} />
<Route path="/onboarding/backup" element={<SocialSignOnMethodPickerScreen />} />
<Route path="/onboarding/signin" element={<SocialSignOnPickerScreen />} />
<Route path="/onboarding/conflict" element={<ConflictDetectedScreen />} />
<Route path="/onboarding/notifications" element={<PushNotificationPromptScreen />} />
<Route path="/proving/receipt" element={<ProofRequestReceiptScreen />} />
<Route path="/proving/history" element={<ProofHistoryScreen />} />
<Route path="/proving/dialogue" element={<SimpleDialogueScreen />} />
<Route path="/proving/dialogue-cta" element={<DialogueWithCtaScreen />} />
<Route path="/proving/generation-dialogue" element={<ProofGenerationDialogueScreen />} />
<Route path="/proving/generation-success" element={<ProofGenerationSuccessScreen />} />
<Route path="/proving/backup-prompt" element={<ProofSuccessBackupScreen />} />
<Route path="/proving/kyc-pending" element={<KycPendingScreen />} />
<Route path="/proving/kyc-success" element={<KycSuccessScreen />} />
<Route path="/account/verified" element={<VerificationResultScreen />} />
<Route path="/id-data" element={<IDDataScreen />} />
<Route path="/manage-documents" element={<ManageDocumentsScreen />} />
<Route path="/coming-soon" element={<ComingSoonScreen />} />
<Route path="/tunnel/tour/:step" element={<TunnelTourScreen />} />
<Route path="/tunnel/kyc" element={<KycMockScreen />} />
<Route path="/tunnel/registration/country" element={<TunnelCountryPickerScreen />} />
<Route path="/tunnel/registration/id-type" element={<TunnelIDTypeScreen />} />
<Route path="/tunnel/proof/receipt" element={<TunnelProofReceiptScreen />} />
<Route path="/tunnel/proof/generating" element={<TunnelProvingScreen />} />
<Route path="/tunnel/proof/result" element={<TunnelResultScreen />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
{import.meta.env.DEV && <DevRouteMenu />}
</SelfClientProvider>
</VerificationRequestProvider>
</BrowserRouter>
</PasswordGate>
);

View File

@@ -3,32 +3,95 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type React from 'react';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
const mockScreenLinks = [
{ href: '/settings/dev-mode', label: 'Dev Mode' },
{ href: '/manage-documents', label: 'Manage Documents' },
{ href: '/id-data', label: 'ID Data' },
{ href: '/proving/receipt', label: 'Proof Receipt' },
{ href: '/proving/history', label: 'Proof History' },
{ href: '/proving/dialogue', label: 'Simple Dialogue' },
{ href: '/proving/dialogue-cta', label: 'Dialogue With CTA' },
{ href: '/proving/generation-dialogue', label: 'Generation Dialogue' },
{ href: '/proving/generation-success', label: 'Generation Success' },
{ href: '/proving/backup-prompt', label: 'Backup Prompt' },
{ href: '/proving/kyc-pending', label: 'KYC Pending' },
{ href: '/proving/kyc-success', label: 'KYC Success' },
{ href: '/debug/keychain', label: 'Keychain Debug' },
interface DevScreenLink {
href: string;
label: string;
}
interface DevScreenGroup {
title: string;
links: DevScreenLink[];
}
const screenGroups: DevScreenGroup[] = [
{
title: 'Home & Documents',
links: [
{ href: '/manage-documents', label: 'Manage Documents' },
{ href: '/id-data', label: 'ID Data' },
],
},
{
title: 'Onboarding',
links: [
{ href: '/onboarding/tour/1', label: 'Tour' },
{ href: '/onboarding/country', label: 'Country Picker' },
{ href: '/onboarding/confirm', label: 'Confirm ID' },
{ href: '/onboarding/success', label: 'Scan Success' },
{ href: '/onboarding/failure', label: 'Registration Failure' },
{ href: '/onboarding/backup', label: 'Social Sign-On Method' },
{ href: '/onboarding/signin', label: 'Social Sign-On' },
{ href: '/onboarding/conflict', label: 'Conflict Detected' },
{ href: '/onboarding/notifications', label: 'Push Notification Prompt' },
],
},
{
title: 'Proving',
links: [
{ href: '/proving/receipt', label: 'Proof Receipt' },
{ href: '/proving/history', label: 'Proof History' },
{ href: '/proving/dialogue', label: 'Simple Dialogue' },
{ href: '/proving/dialogue-cta', label: 'Dialogue With CTA' },
{ href: '/proving/generation-dialogue', label: 'Generation Dialogue' },
{ href: '/proving/generation-success', label: 'Generation Success' },
{ href: '/proving/backup-prompt', label: 'Backup Prompt' },
{ href: '/proving/kyc-pending', label: 'KYC Pending' },
{ href: '/proving/kyc-success', label: 'KYC Success' },
],
},
{
title: 'Recovery',
links: [
{ href: '/settings/backup', label: 'Backup Method Picker' },
{ href: '/settings/recovery-phrase', label: 'Recovery Phrase' },
{ href: '/recovery', label: 'Launch Recovery' },
{ href: '/recovery/phrase-input', label: 'Secret Phrase Input' },
{ href: '/recovery/success', label: 'Recovery Success' },
],
},
{
title: 'Settings',
links: [
{ href: '/settings/dev-mode', label: 'Dev Mode' },
{ href: '/settings/security', label: 'Security' },
{ href: '/settings/notifications', label: 'Notification Preferences' },
],
},
{
title: 'Debug',
links: [{ href: '/debug/keychain', label: 'Keychain Debug' }],
},
];
const allLinks = screenGroups.flatMap(g => g.links);
export const DevRouteMenu: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);
const activeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen && activeRef.current) {
activeRef.current.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
}, [isOpen]);
const currentLabel = useMemo(
() => mockScreenLinks.find(link => link.href === location.pathname)?.label ?? 'Mock Screens',
() => allLinks.find(link => link.href === location.pathname)?.label ?? 'Dev Screens',
[location.pathname],
);
@@ -53,51 +116,57 @@ export const DevRouteMenu: React.FC = () => {
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 8,
gap: 4,
padding: 14,
borderRadius: 14,
backgroundColor: 'rgba(17, 24, 39, 0.95)',
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.3)',
}}
>
<div
style={{
color: '#fff',
fontSize: 12,
fontWeight: 700,
letterSpacing: '0.04em',
textTransform: 'uppercase',
}}
>
Mock Screens
</div>
{mockScreenLinks.map(link => {
const isActive = location.pathname === link.href;
return (
<button
key={link.href}
onClick={() => {
navigate(link.href);
setIsOpen(false);
}}
{screenGroups.map(group => (
<div key={group.title} style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<div
style={{
padding: '9px 10px',
borderRadius: 8,
border: isActive ? '1px solid #7c8aff' : '1px solid rgba(255, 255, 255, 0.12)',
backgroundColor: isActive ? 'rgba(124, 138, 255, 0.22)' : 'rgba(255, 255, 255, 0.08)',
color: '#fff',
fontSize: 13,
fontWeight: 600,
textAlign: 'left',
cursor: 'pointer',
color: 'rgba(255, 255, 255, 0.5)',
fontSize: 10,
fontWeight: 700,
letterSpacing: '0.06em',
textTransform: 'uppercase',
padding: '8px 2px 2px',
}}
type="button"
>
{link.label}
</button>
);
})}
{group.title}
</div>
{group.links.map(link => {
const isActive = location.pathname === link.href;
return (
<button
key={link.href}
ref={isActive ? activeRef : undefined}
onClick={() => {
navigate(link.href);
setIsOpen(false);
}}
style={{
padding: '7px 10px',
borderRadius: 8,
border: isActive ? '1px solid #7c8aff' : '1px solid rgba(255, 255, 255, 0.08)',
backgroundColor: isActive ? 'rgba(124, 138, 255, 0.22)' : 'rgba(255, 255, 255, 0.05)',
color: '#fff',
fontSize: 12,
fontWeight: 500,
textAlign: 'left',
cursor: 'pointer',
}}
type="button"
>
{link.label}
</button>
);
})}
</div>
))}
</div>
)}
<button
@@ -116,7 +185,7 @@ export const DevRouteMenu: React.FC = () => {
}}
type="button"
>
{isOpen ? 'Close Mock Screens' : currentLabel}
{isOpen ? 'Close' : currentLabel}
</button>
</div>
);

View File

@@ -0,0 +1,86 @@
// 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, useState } from 'react';
const STORAGE_KEY = 'self-preview-auth';
export const PasswordGate: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const password = import.meta.env.VITE_WEBVIEW_APP_PREVIEW_PASSWORD;
const [authenticated, setAuthenticated] = useState(() => !password || sessionStorage.getItem(STORAGE_KEY) === 'true');
const [value, setValue] = useState('');
const [error, setError] = useState(false);
const onSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
if (value === password) {
sessionStorage.setItem(STORAGE_KEY, 'true');
setAuthenticated(true);
} else {
setError(true);
}
},
[value, password],
);
if (authenticated) return <>{children}</>;
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
backgroundColor: '#f8fafc',
}}
>
<form
onSubmit={onSubmit}
style={{
display: 'flex',
flexDirection: 'column',
gap: 12,
width: 280,
}}
>
<input
type="password"
placeholder="Password"
value={value}
onChange={e => {
setValue(e.target.value);
setError(false);
}}
style={{
padding: '10px 14px',
borderRadius: 8,
border: error ? '1px solid #ef4444' : '1px solid #d1d5db',
fontSize: 14,
outline: 'none',
}}
autoFocus
/>
<button
type="submit"
style={{
padding: '10px 14px',
borderRadius: 8,
border: 'none',
backgroundColor: '#111827',
color: '#fff',
fontSize: 14,
fontWeight: 600,
cursor: 'pointer',
}}
>
Enter
</button>
</form>
</div>
);
};

View File

@@ -10,6 +10,7 @@ import { App } from './App';
import { BridgeProvider } from './providers/BridgeProvider';
import './fonts.css';
import './recovery.css';
import './reset.css';
globalThis.Buffer = Buffer;

View File

@@ -0,0 +1,12 @@
.launch-recovery-screen {
display: flex;
flex: 1;
min-height: 0;
}
.launch-recovery-screen img[src$='/backgrounds/restore.png'],
.launch-recovery-screen img[src$='restore.png'] {
height: auto !important;
object-fit: contain !important;
object-position: top center !important;
}

View File

@@ -31,19 +31,19 @@ export const SecurityScreen: React.FC = () => {
const onBackupAccount = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('security_backup_account_pressed');
navigate('/coming-soon');
navigate('/settings/backup');
}, [navigate, haptic, analytics]);
const onRevealRecoveryPhrase = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('security_reveal_phrase_pressed');
navigate('/coming-soon');
navigate('/settings/recovery-phrase');
}, [navigate, haptic, analytics]);
const onRestoreAccount = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('security_restore_account_pressed');
navigate('/coming-soon');
navigate('/recovery');
}, [navigate, haptic, analytics]);
const onDisableBackups = useCallback(() => {

View File

@@ -29,11 +29,9 @@ export const ManageDocumentsScreen: React.FC = () => {
const onDocumentPress = useCallback(() => {
haptic.trigger('selection');
setDialogue({
title: 'Manage Document',
description: 'View details or remove this document from your Self ID.',
});
}, [haptic]);
analytics.trackEvent('manage_docs_document_pressed');
navigate('/id-data');
}, [haptic, analytics, navigate]);
const onViewIdDetails = useCallback(() => {
haptic.trigger('selection');

View File

@@ -0,0 +1,73 @@
// 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 {
BackupMethodPickerScreen as EuclidBackupMethodPickerScreen,
CloudKeyIcon,
LeftArrowIcon,
LockIcon,
ZapShieldIcon,
} from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
import { WEB_SAFE_AREA } from '../../utils/insets';
export const BackupMethodPickerScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const onClose = useCallback(() => {
haptic.trigger('selection');
navigate('/settings/security');
}, [navigate, haptic]);
const onICloudBackup = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('backup_method_icloud_pressed');
navigate('/coming-soon');
}, [navigate, haptic, analytics]);
const onRecoveryPhrase = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('backup_method_phrase_pressed');
navigate('/settings/recovery-phrase');
}, [navigate, haptic, analytics]);
return (
<EuclidBackupMethodPickerScreen
insets={WEB_SAFE_AREA.insets}
title="Back up your account"
description="Choose how you'd like to secure your identity data. You can always change this later."
subtitle="Backup"
iconContainer={<CloudKeyIcon size={48} color="#000" />}
options={[
{
id: 'icloud',
label: 'iCloud Backup',
icon: <CloudKeyIcon size={24} color="#000" />,
onPress: onICloudBackup,
},
{
id: 'recovery-phrase',
label: 'Recovery Phrase',
icon: <LockIcon size={24} color="#000" />,
onPress: onRecoveryPhrase,
},
{
id: 'turnkey',
label: 'Turnkey Backup',
icon: <ZapShieldIcon size={24} color="#000" />,
onPress: () => navigate('/coming-soon'),
disabled: true,
},
]}
closeIcon={({ size, color }) => <LeftArrowIcon size={size} color={color} />}
onClose={onClose}
/>
);
};

View File

@@ -0,0 +1,42 @@
// 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 { LaunchRecoveryScreen as EuclidLaunchRecoveryScreen, LeftArrowIcon } from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
import { WEB_SAFE_AREA } from '../../utils/insets';
export const LaunchRecoveryScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const onClose = useCallback(() => {
haptic.trigger('selection');
navigate('/settings/security');
}, [navigate, haptic]);
const onEnterRecoveryPhrase = useCallback(() => {
haptic.trigger('selection');
analytics.trackEvent('recovery_enter_phrase_pressed');
navigate('/recovery/phrase-input');
}, [navigate, haptic, analytics]);
return (
<div className="launch-recovery-screen">
<EuclidLaunchRecoveryScreen
insets={WEB_SAFE_AREA.insets}
escapeIcon={({ size, color }) => <LeftArrowIcon size={size} color={color} />}
onClose={onClose}
onAppleBackup={() => navigate('/coming-soon')}
onGoogleBackup={() => navigate('/coming-soon')}
onEnterRecoveryPhrase={onEnterRecoveryPhrase}
backgroundImage="/backgrounds/restore.png"
/>
</div>
);
};

View File

@@ -0,0 +1,88 @@
// 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, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import type { RecoveryPhraseVariant } from '@selfxyz/euclid';
import { RecoveryPhraseScreen as EuclidRecoveryPhraseScreen } from '@selfxyz/euclid';
import { bridgeStorageAdapter } from '@selfxyz/webview-bridge/adapters';
import { useBridge } from '../../providers/BridgeProvider';
import { useSelfClient } from '../../providers/SelfClientProvider';
import { WEB_SAFE_AREA } from '../../utils/insets';
const MNEMONIC_KEY = 'secret';
function parseMnemonicWords(raw: string | null): string[] | undefined {
if (!raw) {
return undefined;
}
const parsed = JSON.parse(raw) as string | { phrase?: string };
const phrase = typeof parsed === 'string' ? parsed : parsed.phrase;
const words = phrase?.trim().split(/\s+/).filter(Boolean);
return words && words.length > 0 ? words : undefined;
}
export const RecoveryPhraseScreen: React.FC = () => {
const navigate = useNavigate();
const bridge = useBridge();
const storage = useRef(bridgeStorageAdapter(bridge)).current;
const { analytics, haptic } = useSelfClient();
const [variant, setVariant] = useState<RecoveryPhraseVariant>('hidden');
const [words, setWords] = useState<string[] | undefined>();
const onBack = useCallback(() => {
haptic.trigger('selection');
navigate(-1);
}, [navigate, haptic]);
const onReveal = useCallback(async () => {
haptic.trigger('selection');
analytics.trackEvent('recovery_phrase_revealed');
let resolvedWords: string[] | undefined;
try {
resolvedWords = parseMnemonicWords(await storage.get(MNEMONIC_KEY));
} catch {
// Storage or parsing failed — words stay undefined, Euclid shows placeholders.
}
setWords(resolvedWords);
setVariant('revealed');
}, [haptic, analytics, storage]);
const onCopy = useCallback(async () => {
analytics.trackEvent('recovery_phrase_copied');
if (!words?.length || !navigator.clipboard) {
return;
}
try {
await navigator.clipboard.writeText(words.join(' '));
haptic.trigger('success');
setVariant('copied');
} catch {
haptic.trigger('error');
}
}, [haptic, analytics, words]);
return (
<EuclidRecoveryPhraseScreen
insets={WEB_SAFE_AREA.insets}
words={words}
variant={variant}
onBack={onBack}
onReveal={onReveal}
onCopy={onCopy}
onAppleBackup={() => navigate('/coming-soon')}
onGoogleBackup={() => navigate('/coming-soon')}
/>
);
};

View File

@@ -0,0 +1,34 @@
// 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 { LeftArrowIcon, RecoverySuccessScreen as EuclidRecoverySuccessScreen } from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
import { WEB_SAFE_AREA } from '../../utils/insets';
export const RecoverySuccessScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const onClose = useCallback(() => {
haptic.trigger('success');
analytics.trackEvent('recovery_success_continue_pressed');
navigate('/');
}, [navigate, haptic, analytics]);
return (
<EuclidRecoverySuccessScreen
insets={WEB_SAFE_AREA.insets}
escapeIcon={({ size, color }) => <LeftArrowIcon size={size} color={color} />}
logo={<img src="/logos/self.svg" alt="" width={64} height={64} aria-hidden="true" />}
onClose={onClose}
onAppleBackup={() => navigate('/coming-soon')}
onGoogleBackup={() => navigate('/coming-soon')}
/>
);
};

View File

@@ -0,0 +1,53 @@
// 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 { LeftArrowIcon, SecretPhraseInputScreen as EuclidSecretPhraseInputScreen } from '@selfxyz/euclid';
import { useSelfClient } from '../../providers/SelfClientProvider';
import { WEB_SAFE_AREA } from '../../utils/insets';
import { validateMnemonic } from '@scure/bip39';
import { wordlist as bip39EnglishWordlist } from '@scure/bip39/wordlists/english';
const VALID_WORDS = new Set(bip39EnglishWordlist);
const VALID_LENGTHS = new Set([12, 15, 18, 21, 24]);
export const SecretPhraseInputScreen: React.FC = () => {
const navigate = useNavigate();
const { analytics, haptic } = useSelfClient();
const onBack = useCallback(() => {
haptic.trigger('selection');
navigate(-1);
}, [navigate, haptic]);
const onSubmit = useCallback(
(words: string[]) => {
if (!VALID_LENGTHS.has(words.length) || !validateMnemonic(words.join(' '), bip39EnglishWordlist)) {
haptic.trigger('error');
analytics.trackEvent('recovery_phrase_rejected', { wordCount: words.length });
return;
}
haptic.trigger('success');
analytics.trackEvent('recovery_phrase_submitted', { wordCount: words.length });
navigate('/recovery/success');
},
[navigate, haptic, analytics],
);
return (
<EuclidSecretPhraseInputScreen
insets={WEB_SAFE_AREA.insets}
escapeIcon={({ size, color }) => <LeftArrowIcon size={size} color={color} />}
onBack={onBack}
onSubmit={onSubmit}
validWords={VALID_WORDS}
/>
);
};

View File

@@ -0,0 +1,71 @@
// 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.
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { PasswordGate } from '../../src/components/PasswordGate';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
describe('PasswordGate', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.unstubAllEnvs();
sessionStorage.clear();
});
afterEach(() => {
cleanup();
vi.unstubAllEnvs();
});
it('renders children immediately when no preview password is configured', () => {
vi.stubEnv('VITE_WEBVIEW_APP_PREVIEW_PASSWORD', '');
render(
<PasswordGate>
<div>Unlocked content</div>
</PasswordGate>,
);
expect(screen.getByText('Unlocked content')).toBeTruthy();
expect(screen.queryByPlaceholderText('Password')).toBeNull();
});
it('unlocks after the correct password and persists the session', () => {
vi.stubEnv('VITE_WEBVIEW_APP_PREVIEW_PASSWORD', 'secret-preview');
render(
<PasswordGate>
<div>Unlocked content</div>
</PasswordGate>,
);
expect(screen.queryByText('Unlocked content')).toBeNull();
fireEvent.change(screen.getByPlaceholderText('Password'), {
target: { value: 'secret-preview' },
});
fireEvent.click(screen.getByRole('button', { name: /enter/i }));
expect(screen.getByText('Unlocked content')).toBeTruthy();
expect(sessionStorage.getItem('self-preview-auth')).toBe('true');
});
it('renders children immediately when the session is already authenticated', () => {
vi.stubEnv('VITE_WEBVIEW_APP_PREVIEW_PASSWORD', 'secret-preview');
sessionStorage.setItem('self-preview-auth', 'true');
render(
<PasswordGate>
<div>Unlocked content</div>
</PasswordGate>,
);
expect(screen.getByText('Unlocked content')).toBeTruthy();
expect(screen.queryByPlaceholderText('Password')).toBeNull();
});
});

View File

@@ -174,7 +174,6 @@ describe('WV-14 support screens', () => {
expect(screen.getByTestId('location').textContent).toBe('/manage-documents');
fireEvent.click(screen.getByRole('button', { name: /passport/i }));
fireEvent.click(screen.getByRole('button', { name: /view details/i }));
expect(screen.getByTestId('location').textContent).toBe('/id-data');
fireEvent.click(screen.getByRole('button', { name: /manage id/i }));
@@ -188,7 +187,7 @@ describe('WV-14 support screens', () => {
expect(screen.getByRole('button', { name: /open settings/i })).toBeTruthy();
});
fireEvent.click(screen.getByRole('button', { name: /mock screens/i }));
fireEvent.click(screen.getByRole('button', { name: /dev screens/i }));
expect(screen.getByRole('button', { name: 'Manage Documents' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'ID Data' })).toBeTruthy();

View File

@@ -0,0 +1,148 @@
// 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.
// @vitest-environment jsdom
import { MemoryRouter } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { RecoveryPhraseScreen } from '../../../src/screens/recovery/RecoveryPhraseScreen';
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
const analytics = { trackEvent: vi.fn() };
const haptic = { trigger: vi.fn() };
const storageGet = vi.fn<() => Promise<string | null>>();
vi.mock('../../../src/providers/SelfClientProvider', () => ({
useSelfClient: () => ({
analytics,
haptic,
}),
}));
vi.mock('../../../src/providers/BridgeProvider', () => ({
useBridge: () => ({}),
}));
vi.mock('@selfxyz/webview-bridge/adapters', () => ({
bridgeStorageAdapter: () => ({
get: storageGet,
set: vi.fn().mockResolvedValue(undefined),
remove: vi.fn().mockResolvedValue(undefined),
}),
}));
vi.mock('@selfxyz/euclid', () => ({
createSafeAreaProps: ({ top, bottom }: { top: number; bottom: number }) => ({
insets: { top, bottom, left: 0, right: 0 },
safeArea: { top, bottom, left: 0, right: 0 },
}),
RecoveryPhraseScreen: ({
words,
variant,
onReveal,
onCopy,
}: {
words?: string[];
variant: string;
onReveal: () => void;
onCopy: () => void;
}) => (
<div>
<div data-testid="variant">{variant}</div>
<div data-testid="words">{words?.join(' ') ?? ''}</div>
<button onClick={onReveal} type="button">
Reveal phrase
</button>
<button onClick={onCopy} type="button">
Copy phrase
</button>
</div>
),
}));
describe('RecoveryPhraseScreen', () => {
beforeEach(() => {
vi.clearAllMocks();
storageGet.mockResolvedValue(null);
Object.assign(navigator, {
clipboard: {
writeText: vi.fn().mockResolvedValue(undefined),
},
});
});
afterEach(() => {
cleanup();
});
it('reveals placeholders when secure storage is empty', async () => {
render(
<MemoryRouter>
<RecoveryPhraseScreen />
</MemoryRouter>,
);
fireEvent.click(screen.getByRole('button', { name: /reveal phrase/i }));
await waitFor(() => {
expect(screen.getByTestId('variant').textContent).toBe('revealed');
expect(screen.getByTestId('words').textContent).toBe('');
});
});
it('copies the resolved words to the clipboard', async () => {
storageGet.mockResolvedValue(JSON.stringify({ phrase: 'alpha beta gamma' }));
render(
<MemoryRouter>
<RecoveryPhraseScreen />
</MemoryRouter>,
);
fireEvent.click(screen.getByRole('button', { name: /reveal phrase/i }));
await waitFor(() => {
expect(screen.getByTestId('words').textContent).toBe('alpha beta gamma');
});
fireEvent.click(screen.getByRole('button', { name: /copy phrase/i }));
await waitFor(() => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('alpha beta gamma');
expect(screen.getByTestId('variant').textContent).toBe('copied');
});
});
it('does not switch to copied when clipboard write fails', async () => {
storageGet.mockResolvedValue(JSON.stringify({ phrase: 'alpha beta gamma' }));
Object.assign(navigator, {
clipboard: {
writeText: vi.fn().mockRejectedValue(new Error('denied')),
},
});
render(
<MemoryRouter>
<RecoveryPhraseScreen />
</MemoryRouter>,
);
fireEvent.click(screen.getByRole('button', { name: /reveal phrase/i }));
await waitFor(() => {
expect(screen.getByTestId('words').textContent).toBe('alpha beta gamma');
});
fireEvent.click(screen.getByRole('button', { name: /copy phrase/i }));
await waitFor(() => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('alpha beta gamma');
expect(screen.getByTestId('variant').textContent).toBe('revealed');
expect(haptic.trigger).toHaveBeenCalledWith('error');
});
});
});

View File

@@ -0,0 +1,231 @@
// 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.
// @vitest-environment jsdom
import type React from 'react';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { SecurityScreen } from '../../../src/screens/account/SecurityScreen';
import { SettingsScreen } from '../../../src/screens/account/SettingsScreen';
import { BackupMethodPickerScreen } from '../../../src/screens/recovery/BackupMethodPickerScreen';
import { LaunchRecoveryScreen } from '../../../src/screens/recovery/LaunchRecoveryScreen';
import { RecoveryPhraseScreen } from '../../../src/screens/recovery/RecoveryPhraseScreen';
import { RecoverySuccessScreen } from '../../../src/screens/recovery/RecoverySuccessScreen';
import { SecretPhraseInputScreen } from '../../../src/screens/recovery/SecretPhraseInputScreen';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
const analytics = { trackEvent: vi.fn() };
const haptic = { trigger: vi.fn() };
const lifecycle = { dismiss: vi.fn() };
vi.mock('../../../src/providers/SelfClientProvider', () => ({
useSelfClient: () => ({
analytics,
haptic,
lifecycle,
}),
}));
vi.mock('../../../src/providers/BridgeProvider', () => ({
useBridge: () => ({}),
}));
vi.mock('@selfxyz/webview-bridge/adapters', () => ({
bridgeStorageAdapter: () => ({
get: vi.fn().mockResolvedValue(null),
set: vi.fn().mockResolvedValue(undefined),
remove: vi.fn().mockResolvedValue(undefined),
}),
}));
vi.mock('@selfxyz/euclid', () => ({
createSafeAreaProps: ({ top, bottom }: { top: number; bottom: number }) => ({
insets: { top, bottom, left: 0, right: 0 },
safeArea: { top, bottom, left: 0, right: 0 },
}),
ChatStrokeIcon: () => null,
CloudKeyIcon: () => null,
CodeIcon: () => null,
DocumentDetailsIcon: () => null,
LeftArrowIcon: () => null,
LockIcon: () => null,
NotificationIcon: () => null,
QuestionCircleStrokeIcon: () => null,
SelfLogo: () => null,
ShareIcon: () => null,
ZapShieldIcon: () => null,
SettingsViewScreen: ({ sections }: { sections: Array<{ items: Array<{ label: string; onPress: () => void }> }> }) => (
<div>
{sections.flatMap(section =>
section.items.map(item => (
<button key={item.label} onClick={item.onPress} type="button">
{item.label}
</button>
)),
)}
</div>
),
SecurityScreen: ({
onBackupAccount,
onRestoreAccount,
onRevealRecoveryPhrase,
}: {
onBackupAccount: () => void;
onRestoreAccount: () => void;
onRevealRecoveryPhrase: () => void;
}) => (
<div>
<button onClick={onBackupAccount} type="button">
Back up account
</button>
<button onClick={onRevealRecoveryPhrase} type="button">
Reveal recovery phrase
</button>
<button onClick={onRestoreAccount} type="button">
Restore account
</button>
</div>
),
BackupMethodPickerScreen: ({ options }: { options: Array<{ label: string; onPress: () => void }> }) => (
<div>
{options.map(option => (
<button key={option.label} onClick={option.onPress} type="button">
{option.label}
</button>
))}
</div>
),
RecoveryPhraseScreen: ({ onReveal, onCopy }: { onReveal: () => void; onCopy: () => void }) => (
<div>
<button onClick={onReveal} type="button">
Reveal phrase
</button>
<button onClick={onCopy} type="button">
Copy phrase
</button>
</div>
),
LaunchRecoveryScreen: ({
onEnterRecoveryPhrase,
onClose,
}: {
onEnterRecoveryPhrase: () => void;
onClose: () => void;
}) => (
<div>
<button onClick={onEnterRecoveryPhrase} type="button">
Enter recovery phrase
</button>
<button onClick={onClose} type="button">
Close recovery
</button>
</div>
),
SecretPhraseInputScreen: ({ onSubmit }: { onSubmit: (words: string[]) => void }) => (
<div>
<button
onClick={() => onSubmit('bacon rubber extend tonight rocket race ill wash flame expect oval street'.split(' '))}
type="button"
>
Submit valid phrase
</button>
<button onClick={() => onSubmit(['abandon', 'ability', 'able'])} type="button">
Submit invalid phrase
</button>
</div>
),
RecoverySuccessScreen: ({ onClose }: { onClose: () => void }) => (
<button onClick={onClose} type="button">
Finish recovery
</button>
),
}));
const LocationDisplay: React.FC = () => {
const location = useLocation();
return <div data-testid="location">{location.pathname}</div>;
};
const renderRoutes = (initialEntries: string[]) =>
render(
<MemoryRouter initialEntries={initialEntries}>
<Routes>
<Route path="/" element={<LocationDisplay />} />
<Route path="/settings" element={<SettingsScreen />} />
<Route path="/settings/security" element={<SecurityScreen />} />
<Route path="/settings/backup" element={<BackupMethodPickerScreen />} />
<Route path="/settings/recovery-phrase" element={<RecoveryPhraseScreen />} />
<Route path="/recovery" element={<LaunchRecoveryScreen />} />
<Route path="/recovery/phrase-input" element={<SecretPhraseInputScreen />} />
<Route path="/recovery/success" element={<RecoverySuccessScreen />} />
<Route path="/coming-soon" element={<LocationDisplay />} />
</Routes>
<LocationDisplay />
</MemoryRouter>,
);
describe('recovery support screens', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
cleanup();
});
const expectLocation = (expected: string) => {
expect(screen.getByTestId('location').textContent).toBe(expected);
};
it('stitches settings through security to recovery phrase backup flow', () => {
renderRoutes(['/settings']);
fireEvent.click(screen.getByRole('button', { name: /security/i }));
expectLocation('/settings/security');
fireEvent.click(screen.getByRole('button', { name: /back up account/i }));
expectLocation('/settings/backup');
fireEvent.click(screen.getByRole('button', { name: /recovery phrase/i }));
expectLocation('/settings/recovery-phrase');
});
it('stitches settings through security to recovery restore success flow', () => {
renderRoutes(['/settings']);
fireEvent.click(screen.getByRole('button', { name: /security/i }));
expectLocation('/settings/security');
fireEvent.click(screen.getByRole('button', { name: /restore account/i }));
expectLocation('/recovery');
fireEvent.click(screen.getByRole('button', { name: /enter recovery phrase/i }));
expectLocation('/recovery/phrase-input');
fireEvent.click(screen.getByRole('button', { name: /submit valid phrase/i }));
expectLocation('/recovery/success');
});
it('rejects an invalid mnemonic and stays on phrase input', () => {
renderRoutes(['/recovery/phrase-input']);
expectLocation('/recovery/phrase-input');
fireEvent.click(screen.getByRole('button', { name: /submit invalid phrase/i }));
expectLocation('/recovery/phrase-input');
expect(haptic.trigger).toHaveBeenCalledWith('error');
expect(analytics.trackEvent).toHaveBeenCalledWith('recovery_phrase_rejected', { wordCount: 3 });
});
it('launch recovery close returns to previous screen', () => {
renderRoutes(['/settings/security', '/recovery']);
expectLocation('/recovery');
fireEvent.click(screen.getByRole('button', { name: /close recovery/i }));
expectLocation('/settings/security');
});
});

View File

@@ -10784,26 +10784,26 @@ __metadata:
languageName: unknown
linkType: soft
"@selfxyz/euclid-core@npm:1.2.6, @selfxyz/euclid-core@npm:^1.2.6":
version: 1.2.6
resolution: "@selfxyz/euclid-core@npm:1.2.6"
"@selfxyz/euclid-core@npm:1.3.0, @selfxyz/euclid-core@npm:^1.3.0":
version: 1.3.0
resolution: "@selfxyz/euclid-core@npm:1.3.0"
peerDependencies:
react: ">=18.2.0"
checksum: 10c0/0496da70a4eb5e3b7bbae51f2379f8080ed21950eac6e09c3ad9a39020399ef0a7065798d626e86ac187a34ee40c04e0bac1d8186aa1507900c83b667ac6816a
checksum: 10c0/346831b2e2128014a1143f2af079229c40bfe64a622ef54600f0d927a8b8f78e8ad624fd4805488e03fbc2504171313fcc62c4123970afa30597c7380c817c3b
languageName: node
linkType: hard
"@selfxyz/euclid@npm:1.2.6":
version: 1.2.6
resolution: "@selfxyz/euclid@npm:1.2.6"
"@selfxyz/euclid@npm:1.3.0":
version: 1.3.0
resolution: "@selfxyz/euclid@npm:1.3.0"
dependencies:
"@lottiefiles/dotlottie-react": "npm:^0.18.4"
"@selfxyz/euclid-core": "npm:^1.2.6"
"@selfxyz/euclid-core": "npm:^1.3.0"
lottie-react: "npm:^2.4.1"
peerDependencies:
react: ">=18.2.0"
react-dom: ">=18.2.0"
checksum: 10c0/af56ea1abc20645dbe58f29d062ab4e2686f626de92fb5626cfbe7971b4ec31926edf8e0d598dd173a4f1b9749e6ccf690e3189e7694178e2653a2ddff195f4d
checksum: 10c0/056e53cfa6566056fc107fb553157de586dcdd384eedf101316b11b08d39d5800226321d53e119e38160977ef346632f6db16c27ad67f003d0bf085aed8a0f48
languageName: node
linkType: hard
@@ -11260,8 +11260,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@selfxyz/webview-app@workspace:packages/webview-app"
dependencies:
"@selfxyz/euclid": "npm:1.2.6"
"@selfxyz/euclid-core": "npm:1.2.6"
"@scure/bip39": "npm:^1.6.0"
"@selfxyz/euclid": "npm:1.3.0"
"@selfxyz/euclid-core": "npm:1.3.0"
"@selfxyz/mobile-sdk-alpha": "workspace:^"
"@selfxyz/webview-bridge": "workspace:^"
"@sumsub/websdk": "npm:^2.0.0"