Skip to content

Commit c11d690

Browse files
committed
feat: add a option to choose recovery type during onboarding
1 parent 1164709 commit c11d690

File tree

12 files changed

+139
-17
lines changed

12 files changed

+139
-17
lines changed

public/_locales/en/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,13 @@
411411
"chooseYourAccountType": "Choose Your Account Type",
412412
"chooseAccountTypeDescription": "Select how you want to interact with the blockchain -- on-chain or off-chain.",
413413
"canAddMultipleAccountsLater": "You can add and manage multiple account types later within the wallet.",
414+
"chooseRecoveryMethod": "Set Up Account Recovery",
415+
"chooseRecoveryMethodDescription": "Choose how you want to recover your account if you lose access.",
416+
"guardianRecovery": "Guardian",
417+
"guardianRecoveryDescription": "Recommended. Guardian-based recovery for your account.",
418+
"fullyPrivateRecovery": "Fully Private",
419+
"fullyPrivateRecoveryDescription": "Local only. No recovery — losing your device can permanently lose funds.",
420+
"default": "Default",
414421
"transactionFile": "Transaction file",
415422
"verificationFailed": "Verification Failed",
416423
"transactionVerifiedSuccessfully": "The transaction has been successfully verified. You can now claim your tokens.",

src/app/pages/Welcome.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useWalletStore } from 'lib/store';
1313
import { fetchStateFromBackend } from 'lib/store/hooks/useIntercomSync';
1414
import { navigate, useLocation } from 'lib/woozie';
1515
import { OnboardingFlow } from 'screens/onboarding/navigator';
16-
import { ImportType, OnboardingAction, OnboardingStep, OnboardingType } from 'screens/onboarding/types';
16+
import { ImportType, OnboardingAction, OnboardingStep, OnboardingType, WalletType } from 'screens/onboarding/types';
1717

1818
/**
1919
* Check if hardware security is available for vault key protection.
@@ -71,6 +71,7 @@ const Welcome: FC = () => {
7171
const [onboardingType, setOnboardingType] = useState<OnboardingType | null>(null);
7272
const [importType, setImportType] = useState<ImportType | null>(null);
7373
const [password, setPassword] = useState<string | null>(null);
74+
const [walletType, setWalletType] = useState<WalletType>(WalletType.Psm);
7475
const [importedWithFile, setImportedWithFile] = useState(false);
7576
const [isLoading, setIsLoading] = useState(false);
7677
const [useBiometric, setUseBiometric] = useState(true);
@@ -94,14 +95,14 @@ const Welcome: FC = () => {
9495
// For hardware-only wallets, pass undefined as password
9596
const actualPassword = password === '__HARDWARE_ONLY__' ? undefined : password;
9697
if (!importedWithFile) {
97-
await registerWallet(actualPassword, seedPhraseFormatted, onboardingType === OnboardingType.Import);
98+
await registerWallet(walletType, actualPassword, seedPhraseFormatted, onboardingType === OnboardingType.Import);
9899
} else {
99100
await importWalletFromClient(actualPassword, seedPhraseFormatted);
100101
}
101102
} else {
102103
throw new Error('Missing password or seed phrase');
103104
}
104-
}, [password, seedPhrase, importedWithFile, registerWallet, onboardingType, importWalletFromClient]);
105+
}, [password, seedPhrase, importedWithFile, registerWallet, onboardingType, importWalletFromClient, walletType]);
105106

106107
const onAction = async (action: OnboardingAction) => {
107108
let eventCategory = AnalyticsEventCategory.ButtonPress;
@@ -180,6 +181,14 @@ const Welcome: FC = () => {
180181
setPassword(action.payload.password);
181182
eventCategory = AnalyticsEventCategory.FormSubmit;
182183
// Hardware protection is automatically set up in Vault.spawn() when available
184+
if (onboardingType === OnboardingType.Create) {
185+
navigate('/#select-recovery-method');
186+
} else {
187+
navigate('/#confirmation');
188+
}
189+
break;
190+
case 'select-recovery-method':
191+
setWalletType(action.payload);
183192
navigate('/#confirmation');
184193
break;
185194
case 'confirmation':
@@ -231,6 +240,8 @@ const Welcome: FC = () => {
231240
navigate('/#import-from-seed');
232241
}
233242
}
243+
} else if (step === OnboardingStep.SelectRecoveryMethod) {
244+
navigate('/#create-password');
234245
} else if (step === OnboardingStep.ImportFromFile || step === OnboardingStep.ImportFromSeed) {
235246
navigate('/#select-import-type');
236247
}
@@ -271,6 +282,9 @@ const Welcome: FC = () => {
271282
case '#create-password':
272283
setStep(OnboardingStep.CreatePassword);
273284
break;
285+
case '#select-recovery-method':
286+
setStep(OnboardingStep.SelectRecoveryMethod);
287+
break;
274288
case '#confirmation':
275289
if (!password) {
276290
navigate('/');

src/lib/miden/back/actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ export async function isDAppEnabled() {
7373
return bools.every(Boolean);
7474
}
7575

76-
export function registerNewWallet(password?: string, mnemonic?: string, ownMnemonic?: boolean) {
76+
export function registerNewWallet(walletType: WalletType, password?: string, mnemonic?: string, ownMnemonic?: boolean) {
7777
return withInited(async () => {
7878
console.log('[Actions.registerNewWallet] Starting...');
7979
// Password may be undefined for hardware-only wallets (mobile/desktop with Secure Enclave)
8080
// Vault.spawn() will handle this by using hardware protection instead
8181
// spawn() returns the vault directly, avoiding a second biometric prompt from unlock()
82-
const vault = await Vault.spawn(password ?? '', mnemonic, ownMnemonic);
82+
const vault = await Vault.spawn(walletType, password ?? '', mnemonic, ownMnemonic);
8383
console.log('[Actions.registerNewWallet] Vault.spawn completed, initializing state...');
8484
const accounts = await vault.fetchAccounts();
8585
const settings = await vault.fetchSettings();

src/lib/miden/back/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async function processRequest(req: WalletRequest, port: Runtime.Port): Promise<W
3636
state
3737
};
3838
case WalletMessageType.NewWalletRequest:
39-
await Actions.registerNewWallet(req.password, req.mnemonic, req.ownMnemonic);
39+
await Actions.registerNewWallet(req.walletType, req.password, req.mnemonic, req.ownMnemonic);
4040
return { type: WalletMessageType.NewWalletResponse };
4141
case WalletMessageType.ImportFromClientRequest:
4242
await Actions.registerImportedWallet(req.password, req.mnemonic);

src/lib/miden/back/vault.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ export class Vault {
185185
return passKey;
186186
}
187187

188-
static async spawn(password: string, mnemonic?: string, ownMnemonic?: boolean): Promise<Vault> {
188+
static async spawn(
189+
walletType: WalletType,
190+
password: string,
191+
mnemonic?: string,
192+
ownMnemonic?: boolean
193+
): Promise<Vault> {
189194
return withError('Failed to create wallet', async (): Promise<Vault> => {
190195
// Generate random vault key (256-bit)
191196
const vaultKeyBytes = Passworder.generateVaultKey();
@@ -233,7 +238,7 @@ export class Vault {
233238
insertKeyCallback
234239
};
235240
const hdAccIndex = 0;
236-
const walletSeed = deriveClientSeed(WalletType.Psm, mnemonic, 0);
241+
const walletSeed = deriveClientSeed(walletType, mnemonic, 0);
237242

238243
// Wrap WASM client operations in a lock to prevent concurrent access
239244
const accPublicKey = await withWasmClientLock(async () => {
@@ -243,20 +248,20 @@ export class Vault {
243248
return await midenClient.importPublicMidenWalletFromSeed(walletSeed);
244249
} catch (e) {
245250
console.error('Failed to import wallet from seed in spawn, creating new wallet instead', e);
246-
return await midenClient.createMidenWallet(WalletType.Psm, walletSeed);
251+
return await midenClient.createMidenWallet(walletType, walletSeed);
247252
}
248253
} else {
249254
// Sync to chain tip BEFORE creating first account (no accounts = no tags = fast sync)
250255
await midenClient.syncState();
251-
return await midenClient.createMidenWallet(WalletType.Psm, walletSeed);
256+
return await midenClient.createMidenWallet(walletType, walletSeed);
252257
}
253258
});
254259

255260
const initialAccount: WalletAccount = {
256261
publicKey: accPublicKey,
257262
name: 'Miden Account 1',
258263
isPublic: true,
259-
type: WalletType.Psm,
264+
type: walletType,
260265
hdIndex: hdAccIndex
261266
};
262267

src/lib/miden/front/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ export const [MidenContextProvider, useMidenContext] = constate(() => {
9393

9494
// Wrap store actions in useCallback for stable references
9595
const registerWallet = useCallback(
96-
async (password: string | undefined, mnemonic?: string, ownMnemonic?: boolean) => {
97-
await storeRegisterWallet(password, mnemonic, ownMnemonic);
96+
async (walletType: WalletType, password: string | undefined, mnemonic: string, ownMnemonic: boolean) => {
97+
await storeRegisterWallet(walletType, password, mnemonic, ownMnemonic);
9898
},
9999
[storeRegisterWallet]
100100
);

src/lib/shared/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export interface NewWalletRequest extends WalletMessageBase {
177177
password?: string; // Optional for hardware-only wallets (mobile/desktop with Secure Enclave)
178178
mnemonic?: string;
179179
ownMnemonic?: boolean;
180+
walletType: WalletType;
180181
}
181182

182183
export interface NewWalletResponse extends WalletMessageBase {

src/lib/store/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,10 @@ export const useWalletStore = create<WalletStore>()(
115115
},
116116

117117
// Auth actions
118-
registerWallet: async (password, mnemonic, ownMnemonic) => {
118+
registerWallet: async (walletType, password, mnemonic, ownMnemonic) => {
119119
const res = await request({
120120
type: WalletMessageType.NewWalletRequest,
121+
walletType,
121122
password,
122123
mnemonic,
123124
ownMnemonic

src/lib/store/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,12 @@ export interface WalletActions {
9898
syncFromBackend: (state: MidenState) => void;
9999

100100
// Auth actions
101-
registerWallet: (password: string | undefined, mnemonic?: string, ownMnemonic?: boolean) => Promise<void>;
101+
registerWallet: (
102+
walletType: WalletType,
103+
password: string | undefined,
104+
mnemonic: string,
105+
ownMnemonic: boolean
106+
) => Promise<void>;
102107
importWalletFromClient: (password: string | undefined, mnemonic: string) => Promise<void>;
103108
unlock: (password?: string) => Promise<void>;
104109

@@ -116,7 +121,7 @@ export interface WalletActions {
116121
signTransaction: (publicKey: string, signingInputs: string) => Promise<Uint8Array>;
117122
signWord: (publicKey: string, wordHex: string) => Promise<string>;
118123
getAuthSecretKey: (key: string) => Promise<string>;
119-
getPublicKeyForCommitment: (publicKeyCommitment: string) => Promise<string>;
124+
etPublicKeyForCommitment: (publicKeyCommitment: string) => Promise<string>;
120125

121126
// DApp actions
122127
getDAppPayload: (id: string) => Promise<any>;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React, { useMemo } from 'react';
2+
3+
import classNames from 'clsx';
4+
import { useTranslation } from 'react-i18next';
5+
6+
import { ReactComponent as ArrowRightIcon } from 'app/icons/arrow-right.svg';
7+
8+
import { WalletType } from '../types';
9+
10+
export interface SelectRecoveryMethodScreenProps extends Omit<React.ButtonHTMLAttributes<HTMLDivElement>, 'onSubmit'> {
11+
onSubmit?: (payload: WalletType) => void;
12+
}
13+
14+
type RecoveryOption = {
15+
id: WalletType;
16+
title: string;
17+
description: string;
18+
isDefault?: boolean;
19+
isLast?: boolean;
20+
};
21+
22+
export const SelectRecoveryMethodScreen = ({ onSubmit, ...props }: SelectRecoveryMethodScreenProps) => {
23+
const { t } = useTranslation();
24+
const options: RecoveryOption[] = useMemo(
25+
() => [
26+
{
27+
id: WalletType.Psm,
28+
title: t('guardianRecovery'),
29+
description: t('guardianRecoveryDescription'),
30+
isDefault: true
31+
},
32+
{
33+
id: WalletType.OffChain,
34+
title: t('fullyPrivateRecovery'),
35+
description: t('fullyPrivateRecoveryDescription'),
36+
isLast: true
37+
}
38+
],
39+
[t]
40+
);
41+
42+
return (
43+
<div className="flex-1 flex flex-col items-center bg-transparent pt-6 h-full" {...props}>
44+
<div className="flex flex-col items-center">
45+
<h1 className="font-semibold text-2xl lh-title">{t('chooseRecoveryMethod')}</h1>
46+
<p className="text-base text-center lh-title">{t('chooseRecoveryMethodDescription')}</p>
47+
</div>
48+
{options.map(option => (
49+
<div
50+
key={option.id}
51+
className={classNames('flex flex-col border p-4 rounded-lg cursor-pointer', {
52+
'mb-2': !option.isLast,
53+
'mb-8': option.isLast
54+
})}
55+
onClick={() => onSubmit?.(option.id)}
56+
>
57+
<div className="flex flex-row justify-between items-center">
58+
<div className="flex items-center gap-2">
59+
<h2 className="font-medium text-base">{option.title}</h2>
60+
{option.isDefault && (
61+
<span className="bg-pure-black text-pure-white text-xs font-medium px-2 py-0.5 rounded-full">
62+
{t('default')}
63+
</span>
64+
)}
65+
</div>
66+
<ArrowRightIcon fill="currentColor" height={'20px'} width={'20px'} />
67+
</div>
68+
<p className="text-grey-600">{option.description}</p>
69+
</div>
70+
))}
71+
</div>
72+
);
73+
};

0 commit comments

Comments
 (0)