From f7aef19ad5ef65c1663cbdd5b17ff408d8b82e4a Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Sat, 16 Aug 2025 13:57:18 +0200 Subject: [PATCH 01/22] feat: init the authenticator --- apps/next/src/pages/Settings/Settings.tsx | 7 + .../pages/Settings/components/Home/Home.tsx | 6 +- .../Authenticator/Authenticator.tsx | 69 ++++++++++ .../AuthenticatorVerifyScreen.tsx | 19 +++ .../RecoveryMethods/Authenticator/index.ts | 1 + .../RecoveryMethods/RecoveryMethods.tsx | 122 ++++++++++++++++++ .../components/RecoveryMethods/index.ts | 1 + apps/next/src/popup/app.tsx | 2 + .../services/seedless/SeedlessMfaService.ts | 3 + 9 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/Authenticator.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/index.ts create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/index.ts diff --git a/apps/next/src/pages/Settings/Settings.tsx b/apps/next/src/pages/Settings/Settings.tsx index 56c7fb56c..3003f42be 100644 --- a/apps/next/src/pages/Settings/Settings.tsx +++ b/apps/next/src/pages/Settings/Settings.tsx @@ -5,6 +5,8 @@ import { ChangePassword } from './components/ChangePassword'; import { ConnectedSites } from './components/ConnectedSites'; import { SettingsHomePage } from './components/Home'; import { RecoveryPhrase } from './components/RecoveryPhrase'; +import { RecoveryMethods } from './components/RecoveryMethods'; +import { Authenticator } from './components/RecoveryMethods/Authenticator'; export const Settings: FC = () => { const { path } = useRouteMatch(); @@ -14,6 +16,11 @@ export const Settings: FC = () => { + + ); diff --git a/apps/next/src/pages/Settings/components/Home/Home.tsx b/apps/next/src/pages/Settings/components/Home/Home.tsx index e38b9783e..40d2925f3 100644 --- a/apps/next/src/pages/Settings/components/Home/Home.tsx +++ b/apps/next/src/pages/Settings/components/Home/Home.tsx @@ -186,7 +186,11 @@ export const SettingsHomePage = () => { /> )} - + { + const { t } = useTranslation(); + const { + initAuthenticatorChange, + completeAuthenticatorChange, + hasFidoConfigured, + hasTotpConfigured, + } = useSeedlessMfaManager(); + const [totpChallenge, setTotpChallenge] = useState(); + console.log('totpChallenge: ', totpChallenge); + const goBack = useGoBack(); + + const initChange = useCallback(async () => { + console.log('initChange: '); + // if (hasFidoConfigured) { + // browser.tabs.create({ + // url: `${ContextContainer.FULLSCREEN}#/update-recovery-methods`, + // }); + // return; + // } + + // setState(State.Initiated); + try { + console.log('try: '); + const challenge = await initAuthenticatorChange(); + console.log('challenge: ', challenge); + setTotpChallenge(challenge); + // setState(State.Pending); + } catch (e) { + console.log('catch: ', e); + setTotpChallenge(undefined); + // setState(State.Failure); + } + }, [initAuthenticatorChange]); + + useEffect(() => { + console.log('init'); + initChange(); + }, [initChange]); + + return ( + + + {t( + 'Open any authenticator app and scan the QR code below or enter the code manually', + )} + + {/* console.log('Next clicked')} + /> */} + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx new file mode 100644 index 000000000..9caa9d833 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx @@ -0,0 +1,19 @@ +import { useTheme } from '@avalabs/k2-alpine'; +import QRCode from 'qrcode.react'; + +export const AuthenticatorVerifyScreen = ({ totpChallenge }) => { + const theme = useTheme(); + return ( +
+

Authenticator Verify Screen

+ +
+ ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/index.ts b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/index.ts new file mode 100644 index 000000000..82c26be9a --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/index.ts @@ -0,0 +1 @@ +export * from './Authenticator'; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx new file mode 100644 index 000000000..353c06ab9 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx @@ -0,0 +1,122 @@ +import { Page } from '@/components/Page'; +import { + Box, + ChevronRightIcon, + Divider, + EncryptedIcon, + IconButton, + Paper, + PasswordIcon, + SecurityKeyIcon, + Typography, +} from '@avalabs/k2-alpine'; +import { useAnalyticsContext } from '@core/ui'; +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHistory, useRouteMatch } from 'react-router-dom'; + +export const RecoveryMethods: FC = () => { + const { t } = useTranslation(); + const history = useHistory(); + const { capture } = useAnalyticsContext(); + const { path } = useRouteMatch(); + console.log('path: ', path); + + const cards = [ + { + icon: , + title: t('Passkey'), + description: t( + 'Passkeys are used for quick, password-free recovery and enhanced security.', + ), + to: '/onboarding/import', + analyticsKey: 'AddPasskeyClicked', + }, + { + icon: , + title: t('Authenticator app'), + description: t( + 'Authenticator apps generate secure, time-based codes for wallet recovery.', + ), + to: `${path}/authenticator`, + analyticsKey: 'AddAuthenticatorClicked', + }, + { + icon: , + title: t('Yubikey'), + description: t( + 'YubiKeys are physical, hardware-based protection and strong authentication.', + ), + to: '/onboarding/import', + analyticsKey: 'AddYubikeyClicked', + }, + ]; + + return ( + + + {cards.map((card, idx) => { + console.log('card: ', card); + return ( + { + capture(card.analyticsKey); + history.push(card.to); + }} + key={card.title} + > + + {card.icon} + + + + {card.title} + + + {card.description} + + + + + + {idx < cards.length - 1 && ( + + )} + + ); + })} + + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/index.ts b/apps/next/src/pages/Settings/components/RecoveryMethods/index.ts new file mode 100644 index 000000000..1251a3f5b --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/index.ts @@ -0,0 +1 @@ +export * from './RecoveryMethods'; diff --git a/apps/next/src/popup/app.tsx b/apps/next/src/popup/app.tsx index 20f36782d..0ad65f114 100644 --- a/apps/next/src/popup/app.tsx +++ b/apps/next/src/popup/app.tsx @@ -15,6 +15,7 @@ import { NetworkContextProvider, OnboardingContextProvider, PermissionContextProvider, + SeedlessMfaManagementProvider, usePageHistory, usePreferredColorScheme, WalletContextProvider, @@ -94,6 +95,7 @@ export function App() { , , , + , toast.error(message)} LoadingComponent={CircularProgress} diff --git a/packages/service-worker/src/services/seedless/SeedlessMfaService.ts b/packages/service-worker/src/services/seedless/SeedlessMfaService.ts index acb38a552..887ecdaad 100644 --- a/packages/service-worker/src/services/seedless/SeedlessMfaService.ts +++ b/packages/service-worker/src/services/seedless/SeedlessMfaService.ts @@ -168,9 +168,12 @@ export class SeedlessMfaService implements OnUnlock, OnLock { async initAuthenticatorChange(tabId?: number): Promise { const session = await this.#getSession(); const response = await session.resetTotpStart(TOTP_ISSUER); + console.log('response: ', response); + console.log('response.requiresMfa(): ', response.requiresMfa()); if (response.requiresMfa()) { const methods = await this.getRecoveryMethods(); + console.log('methods: ', methods); const method = methods.length === 1 ? methods[0] From 5b6f11b67601d592ee33ea8eec272cf47bc9975f Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Mon, 25 Aug 2025 14:13:41 +0200 Subject: [PATCH 02/22] feat: add card --- .../screens/SeedlessChooseSetupMethod.tsx | 10 ++- .../pages/Settings/components/Home/Home.tsx | 11 +++ .../RecoveryMethods/RecoveryMethods.tsx | 69 +++++-------------- .../SeedlessMfaManagementProvider.tsx | 13 +++- 4 files changed, 49 insertions(+), 54 deletions(-) diff --git a/apps/next/src/pages/Onboarding/flows/SeedlessFlow/screens/SeedlessChooseSetupMethod.tsx b/apps/next/src/pages/Onboarding/flows/SeedlessFlow/screens/SeedlessChooseSetupMethod.tsx index 23ed9e37d..937cb39a2 100644 --- a/apps/next/src/pages/Onboarding/flows/SeedlessFlow/screens/SeedlessChooseSetupMethod.tsx +++ b/apps/next/src/pages/Onboarding/flows/SeedlessFlow/screens/SeedlessChooseSetupMethod.tsx @@ -1,7 +1,13 @@ import { FC, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { MdOutlinePassword } from 'react-icons/md'; -import { Divider, EncryptedIcon, Stack, StackProps } from '@avalabs/k2-alpine'; +import { + Divider, + EncryptedIcon, + SecurityKeyIcon, + Stack, + StackProps, +} from '@avalabs/k2-alpine'; import { FullscreenModalActions, @@ -63,7 +69,7 @@ export const SeedlessChooseSetupMethod: FC = ({ /> onMethodChosen('yubikey')} - icon={} // TODO: replace with YubiKey icon + icon={} text={t('Yubikey')} description={t( `Use a YubiKey for physical, hardware-based protection and strong authentication.`, diff --git a/apps/next/src/pages/Settings/components/Home/Home.tsx b/apps/next/src/pages/Settings/components/Home/Home.tsx index 40d2925f3..96cdf0f77 100644 --- a/apps/next/src/pages/Settings/components/Home/Home.tsx +++ b/apps/next/src/pages/Settings/components/Home/Home.tsx @@ -14,6 +14,7 @@ import { useContactsContext, useSettingsContext, useWalletContext, + useSeedlessMfaManager, } from '@core/ui'; import { LanguageSelector } from '@/components/LanguageSelector'; @@ -52,6 +53,8 @@ export const SettingsHomePage = () => { const [isPrivacyMode, setIsPrivacyMode] = useState(false); const [isCoreAiEnabled, setIsCoreAiEnabled] = useState(false); const { showTrendingTokens, setShowTrendingTokens } = useSettingsContext(); + const { isMfaSetupPromptVisible } = useSeedlessMfaManager(); + console.log('isMfaSetupPromptVisible: ', isMfaSetupPromptVisible); return ( { )} /> + + {isMfaSetupPromptVisible && ( + + )} + { @@ -64,58 +66,23 @@ export const RecoveryMethods: FC = () => { borderRadius: 2, }} > - {cards.map((card, idx) => { - console.log('card: ', card); - return ( - { - capture(card.analyticsKey); - history.push(card.to); - }} - key={card.title} - > - }> + {cards.map((card, idx) => { + console.log('card: ', card); + return ( + { + capture(card.analyticsKey); + history.push(card.to); }} - > - {card.icon} - - - - {card.title} - - - {card.description} - - - - - - {idx < cards.length - 1 && ( - - )} - - ); - })} + icon={card.icon} + text={card.title} + description={card.description} + key={idx} + /> + ); + })} + ); diff --git a/packages/ui/src/contexts/SeedlessMfaManagementProvider.tsx b/packages/ui/src/contexts/SeedlessMfaManagementProvider.tsx index 454518e0d..81fbb54ad 100644 --- a/packages/ui/src/contexts/SeedlessMfaManagementProvider.tsx +++ b/packages/ui/src/contexts/SeedlessMfaManagementProvider.tsx @@ -79,6 +79,8 @@ export const SeedlessMfaManagementProvider = ({ }: SeedlessMfaManagementContextProps) => { const { events, request } = useConnectionContext(); const { walletDetails } = useWalletContext(); + // TODO: FIX has no walletDetails + console.log('walletDetails: ', walletDetails); const { isFlagEnabled } = useFeatureFlagContext(); const areMfaSettingsAvailable = isFlagEnabled( FeatureGates.SEEEDLESS_MFA_SETTINGS, @@ -93,6 +95,11 @@ export const SeedlessMfaManagementProvider = ({ const [recoveryMethods, setRecoveryMethods] = useState([]); const isMfaSetupPromptVisible = useMemo(() => { + // TODO: FIX the values + console.log('areMfaSettingsAvailable: ', areMfaSettingsAvailable); + console.log('hasLoadedRecoveryMethods: ', hasLoadedRecoveryMethods); + console.log(' walletDetails?.type: ', walletDetails?.type); + console.log('recoveryMethods: ', recoveryMethods); return ( areMfaSettingsAvailable && hasLoadedRecoveryMethods && @@ -110,6 +117,7 @@ export const SeedlessMfaManagementProvider = ({ setIsLoadingRecoveryMethods(true); try { + console.log('try: '); const methods = await incrementalPromiseResolve( () => request({ @@ -121,12 +129,15 @@ export const SeedlessMfaManagementProvider = ({ (err) => err === 'Forbidden', ); + console.log('methods: ', methods); setRecoveryMethods(methods); setHasLoadedRecoveryMethods(true); - } catch { + } catch (e) { + console.log('e: ', e); setRecoveryMethods([]); setHasLoadedRecoveryMethods(false); } finally { + console.log('finally: '); setIsLoadingRecoveryMethods(false); } }, [request]); From 4640cadee24d30e5bf8eae49a29cd09905c7333b Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Mon, 25 Aug 2025 14:56:31 +0200 Subject: [PATCH 03/22] fix: providers order --- .../src/pages/Settings/components/Home/Home.tsx | 1 + apps/next/src/popup/app.tsx | 2 +- .../src/contexts/SeedlessMfaManagementProvider.tsx | 13 +------------ 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/apps/next/src/pages/Settings/components/Home/Home.tsx b/apps/next/src/pages/Settings/components/Home/Home.tsx index 96cdf0f77..8e129b0c5 100644 --- a/apps/next/src/pages/Settings/components/Home/Home.tsx +++ b/apps/next/src/pages/Settings/components/Home/Home.tsx @@ -44,6 +44,7 @@ export const SettingsHomePage = () => { const { t } = useTranslation(); const { lockWallet } = useSettingsContext(); const { walletDetails } = useWalletContext(); + console.log('SettingsHomePage walletDetails: ', walletDetails); const { contacts } = useContactsContext(); const { path } = useRouteMatch(); const { push } = useHistory(); diff --git a/apps/next/src/popup/app.tsx b/apps/next/src/popup/app.tsx index e10703a0e..27f145582 100644 --- a/apps/next/src/popup/app.tsx +++ b/apps/next/src/popup/app.tsx @@ -99,7 +99,6 @@ export function App() { , , , - , , , , @@ -113,6 +112,7 @@ export function App() { OnboardingScreen={Onboarding} />, , + , , , ]) as ReactElement[] diff --git a/packages/ui/src/contexts/SeedlessMfaManagementProvider.tsx b/packages/ui/src/contexts/SeedlessMfaManagementProvider.tsx index 81fbb54ad..454518e0d 100644 --- a/packages/ui/src/contexts/SeedlessMfaManagementProvider.tsx +++ b/packages/ui/src/contexts/SeedlessMfaManagementProvider.tsx @@ -79,8 +79,6 @@ export const SeedlessMfaManagementProvider = ({ }: SeedlessMfaManagementContextProps) => { const { events, request } = useConnectionContext(); const { walletDetails } = useWalletContext(); - // TODO: FIX has no walletDetails - console.log('walletDetails: ', walletDetails); const { isFlagEnabled } = useFeatureFlagContext(); const areMfaSettingsAvailable = isFlagEnabled( FeatureGates.SEEEDLESS_MFA_SETTINGS, @@ -95,11 +93,6 @@ export const SeedlessMfaManagementProvider = ({ const [recoveryMethods, setRecoveryMethods] = useState([]); const isMfaSetupPromptVisible = useMemo(() => { - // TODO: FIX the values - console.log('areMfaSettingsAvailable: ', areMfaSettingsAvailable); - console.log('hasLoadedRecoveryMethods: ', hasLoadedRecoveryMethods); - console.log(' walletDetails?.type: ', walletDetails?.type); - console.log('recoveryMethods: ', recoveryMethods); return ( areMfaSettingsAvailable && hasLoadedRecoveryMethods && @@ -117,7 +110,6 @@ export const SeedlessMfaManagementProvider = ({ setIsLoadingRecoveryMethods(true); try { - console.log('try: '); const methods = await incrementalPromiseResolve( () => request({ @@ -129,15 +121,12 @@ export const SeedlessMfaManagementProvider = ({ (err) => err === 'Forbidden', ); - console.log('methods: ', methods); setRecoveryMethods(methods); setHasLoadedRecoveryMethods(true); - } catch (e) { - console.log('e: ', e); + } catch { setRecoveryMethods([]); setHasLoadedRecoveryMethods(false); } finally { - console.log('finally: '); setIsLoadingRecoveryMethods(false); } }, [request]); From aab20b54b032554dedad952ef452fd47b5a0b1ad Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Wed, 27 Aug 2025 16:18:25 +0200 Subject: [PATCH 04/22] feat: add methods list, methods and details screen --- apps/next/src/pages/Settings/Settings.tsx | 8 +- .../pages/Settings/components/Home/Home.tsx | 23 +++- .../Authenticator/Authenticator.tsx | 33 ++++- .../Authenticator/AuthenticatorDetails.tsx | 14 ++ .../Authenticator/AuthenticatorVerifyCode.tsx | 31 +++++ .../AuthenticatorVerifyScreen.tsx | 53 +++++--- .../RecoveryMethods/FIDO/FIDODetails.tsx | 13 ++ .../RecoveryMethods/RecoveryMethod.tsx | 36 +++++ .../RecoveryMethods/RecoveryMethodCard.tsx | 68 ++++++++++ .../RecoveryMethods/RecoveryMethodList.tsx | 69 ++++++++++ .../RecoveryMethods/RecoveryMethods.tsx | 123 ++++++++++++------ 11 files changed, 399 insertions(+), 72 deletions(-) create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx diff --git a/apps/next/src/pages/Settings/Settings.tsx b/apps/next/src/pages/Settings/Settings.tsx index 3003f42be..29872c70c 100644 --- a/apps/next/src/pages/Settings/Settings.tsx +++ b/apps/next/src/pages/Settings/Settings.tsx @@ -10,6 +10,7 @@ import { Authenticator } from './components/RecoveryMethods/Authenticator'; export const Settings: FC = () => { const { path } = useRouteMatch(); + console.log('path: ', path); return ( @@ -19,8 +20,13 @@ export const Settings: FC = () => { + - ); diff --git a/apps/next/src/pages/Settings/components/Home/Home.tsx b/apps/next/src/pages/Settings/components/Home/Home.tsx index 8e129b0c5..d4595db94 100644 --- a/apps/next/src/pages/Settings/components/Home/Home.tsx +++ b/apps/next/src/pages/Settings/components/Home/Home.tsx @@ -15,6 +15,7 @@ import { useSettingsContext, useWalletContext, useSeedlessMfaManager, + useFeatureFlagContext, } from '@core/ui'; import { LanguageSelector } from '@/components/LanguageSelector'; @@ -29,7 +30,7 @@ import { } from '@/config'; import { getContactsPath } from '@/config/routes'; -import { SecretType } from '@core/types'; +import { FeatureGates, SecretType } from '@core/types'; import { AvatarButton, Footer, @@ -55,6 +56,9 @@ export const SettingsHomePage = () => { const [isCoreAiEnabled, setIsCoreAiEnabled] = useState(false); const { showTrendingTokens, setShowTrendingTokens } = useSettingsContext(); const { isMfaSetupPromptVisible } = useSeedlessMfaManager(); + const { featureFlags } = useFeatureFlagContext(); + const areMfaSettingsAvailable = + featureFlags[FeatureGates.SEEEDLESS_MFA_SETTINGS]; console.log('isMfaSetupPromptVisible: ', isMfaSetupPromptVisible); return ( @@ -85,6 +89,7 @@ export const SettingsHomePage = () => { )} /> + {isMfaSetupPromptVisible && ( { /> )} + { /> )} - + + {walletDetails?.type === SecretType.Seedless && + areMfaSettingsAvailable && ( + + )} + { const { t } = useTranslation(); @@ -17,6 +19,7 @@ export const Authenticator: FC = () => { hasTotpConfigured, } = useSeedlessMfaManager(); const [totpChallenge, setTotpChallenge] = useState(); + const [showSecret, setShowSecret] = useState(false); console.log('totpChallenge: ', totpChallenge); const goBack = useGoBack(); @@ -44,10 +47,17 @@ export const Authenticator: FC = () => { }, [initAuthenticatorChange]); useEffect(() => { - console.log('init'); initChange(); }, [initChange]); + const totpSecret = useMemo(() => { + if (!totpChallenge) { + return ''; + } + + return new URL(totpChallenge.totpUrl).searchParams.get('secret') ?? ''; + }, [totpChallenge]); + return ( { 'Open any authenticator app and scan the QR code below or enter the code manually', )} - {/* console.log('Next clicked')} - /> */} + {totpChallenge && ( + console.log('Back clicked')} + totpChallenge={totpChallenge} + onNextClick={() => console.log('Next clicked')} + onShowSecret={() => setShowSecret(true)} + /> + )} + {showSecret && } + + {/* console.log('next')} + /> */} ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx new file mode 100644 index 000000000..bae76b3a7 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx @@ -0,0 +1,14 @@ +import { Page } from '@/components/Page'; +import { Button } from '@avalabs/k2-alpine'; +import { useTranslation } from 'react-i18next'; + +export const AuthenticatorDetails = ({ method }) => { + const { t } = useTranslation(); + return ( + <> +
AuthenticatorDetails
+ + + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx new file mode 100644 index 000000000..26987db1b --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx @@ -0,0 +1,31 @@ +import { Stack, toast, Typography, useTheme } from '@avalabs/k2-alpine'; +import QRCode from 'qrcode.react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const AuthenticatorVerifyCode = ({ totpSecret }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + return ( +
+ + + {t('Alternatively, open any authenticator app and enter this code:')} + + { + navigator.clipboard.writeText(totpSecret); + toast.success(t('Code copied to clipboard')); + }} + > + {totpSecret} + + +
+ ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx index 9caa9d833..b66a366ed 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx @@ -1,19 +1,42 @@ -import { useTheme } from '@avalabs/k2-alpine'; +import { Button, Stack, toast, Typography, useTheme } from '@avalabs/k2-alpine'; import QRCode from 'qrcode.react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; export const AuthenticatorVerifyScreen = ({ totpChallenge }) => { - const theme = useTheme(); - return ( -
-

Authenticator Verify Screen

- -
- ); + const theme = useTheme(); + const { t } = useTranslation(); + + return ( +
+ + + {/* + + {t( + 'Alternatively, open any authenticator app and enter this code:', + )} + + { + navigator.clipboard.writeText(totpSecret); + toast.success(t('Code copied to clipboard')); + }} + > + {totpSecret} + + */} +
+ ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx new file mode 100644 index 000000000..6d2744928 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx @@ -0,0 +1,13 @@ +import { Page } from '@/components/Page'; +import { Button } from '@avalabs/k2-alpine'; +import { useTranslation } from 'react-i18next'; + +export const FIDODetails = ({ method }) => { + const { t } = useTranslation(); + return ( + <> +
FIDODetails
+ + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx new file mode 100644 index 000000000..eb8044279 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx @@ -0,0 +1,36 @@ +import { Page } from '@/components/Page'; +import { Button } from '@avalabs/k2-alpine'; +import { useSeedlessMfaManager } from '@core/ui'; +import { useTranslation } from 'react-i18next'; +import { AuthenticatorDetails } from './Authenticator/AuthenticatorDetails'; +import { FIDODetails } from './FIDO/FIDODetails'; + +export const RecoveryMethod = ({ method }) => { + console.log('RecoveryMethod: ', method); + const { t } = useTranslation(); + const { hasTotpConfigured } = useSeedlessMfaManager(); + return ( + + {method.type === 'totp' && } + {method.type === 'fido' && } + + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx new file mode 100644 index 000000000..94109c353 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx @@ -0,0 +1,68 @@ +import { CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; +import { Button } from '@avalabs/k2-alpine'; +import { useAnalyticsContext } from '@core/ui'; +import { useTranslation } from 'react-i18next'; + +export const RecoveryMethodCard = ({ method, onClick, methodName }) => { + console.log('method: ', method); + const { capture } = useAnalyticsContext(); + const { t } = useTranslation(); + return ( + <> + + {/* */} + + // + //
+ // + // {method.type === 'totp' && t('Authenticator App')} + // {method.type === 'fido' && t('Security Key')} + // {method.type === 'passkey' && t('Passkey')} + // + // + // {t('Added on')} {new Date(method.addedAt).toLocaleDateString()} + // + //
+ //
+ // + //
+ //
+ ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx new file mode 100644 index 000000000..6c2c1b62d --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx @@ -0,0 +1,69 @@ +import { CardMenu, CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; +import { + Divider, + EncryptedIcon, + PasswordIcon, + SecurityKeyIcon, +} from '@avalabs/k2-alpine'; +import { useAnalyticsContext } from '@core/ui'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHistory, useRouteMatch } from 'react-router-dom'; + +export const RecoveryMethodList = () => { + const { t } = useTranslation(); + const history = useHistory(); + const { capture } = useAnalyticsContext(); + const { path } = useRouteMatch(); + + const recoveryMethodCards = [ + { + icon: , + title: t('Passkey'), + description: t( + 'Passkeys are used for quick, password-free recovery and enhanced security.', + ), + to: '/onboarding/import', + analyticsKey: 'AddPasskeyClicked', + }, + { + icon: , + title: t('Authenticator app'), + description: t( + 'Authenticator apps generate secure, time-based codes for wallet recovery.', + ), + to: `${path}/authenticator`, + analyticsKey: 'AddAuthenticatorClicked', + }, + { + icon: , + title: t('Yubikey'), + description: t( + 'YubiKeys are physical, hardware-based protection and strong authentication.', + ), + to: '/onboarding/import', + analyticsKey: 'AddYubikeyClicked', + }, + ]; + + return ( +
+ }> + {recoveryMethodCards.map((card, idx) => { + return ( + { + capture(card.analyticsKey); + history.push(card.to); + }} + icon={card.icon} + text={card.title} + description={card.description} + key={idx} + /> + ); + })} + +
+ ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx index c2911eb3b..04be4f610 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx @@ -2,6 +2,7 @@ import { Page } from '@/components/Page'; import { CardMenu, CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; import { Box, + Button, ChevronRightIcon, Divider, EncryptedIcon, @@ -9,13 +10,22 @@ import { Paper, PasswordIcon, SecurityKeyIcon, + Skeleton, Typography, } from '@avalabs/k2-alpine'; -import { useAnalyticsContext } from '@core/ui'; -import { FC } from 'react'; +import { FeatureGates } from '@core/types'; +import { + useAnalyticsContext, + useFeatureFlagContext, + useSeedlessMfaManager, +} from '@core/ui'; +import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { MdOutlinePassword } from 'react-icons/md'; import { useHistory, useRouteMatch } from 'react-router-dom'; +import { RecoveryMethodList } from './RecoveryMethodList'; +import { RecoveryMethodCard } from './RecoveryMethodCard'; +import { RecoveryMethod } from './RecoveryMethod'; export const RecoveryMethods: FC = () => { const { t } = useTranslation(); @@ -23,36 +33,24 @@ export const RecoveryMethods: FC = () => { const { capture } = useAnalyticsContext(); const { path } = useRouteMatch(); console.log('path: ', path); + const [selectedMethod, setSelectedMethod] = useState(null); + const { + isLoadingRecoveryMethods, + recoveryMethods, + hasFidoConfigured, + hasMfaConfigured, + hasTotpConfigured, + } = useSeedlessMfaManager(); + // console.log('isLoadingRecoveryMethods: ', isLoadingRecoveryMethods); + // console.log('recoveryMethods: ', recoveryMethods); + // console.log('hasFidoConfigured: ', hasFidoConfigured); + // console.log('hasMfaConfigured: ', hasMfaConfigured); + // console.log('hasTotpConfigured: ', hasTotpConfigured); - const cards = [ - { - icon: , - title: t('Passkey'), - description: t( - 'Passkeys are used for quick, password-free recovery and enhanced security.', - ), - to: '/onboarding/import', - analyticsKey: 'AddPasskeyClicked', - }, - { - icon: , - title: t('Authenticator app'), - description: t( - 'Authenticator apps generate secure, time-based codes for wallet recovery.', - ), - to: `${path}/authenticator`, - analyticsKey: 'AddAuthenticatorClicked', - }, - { - icon: , - title: t('Yubikey'), - description: t( - 'YubiKeys are physical, hardware-based protection and strong authentication.', - ), - to: '/onboarding/import', - analyticsKey: 'AddYubikeyClicked', - }, - ]; + console.log('selectedMethod: ', selectedMethod); + if (selectedMethod) { + return ; + } return ( { elevation={1} sx={{ borderRadius: 2, + overflow: 'hidden', }} > - }> - {cards.map((card, idx) => { - console.log('card: ', card); + {isLoadingRecoveryMethods && ( + <> + + + )} + {!isLoadingRecoveryMethods && !hasMfaConfigured ? ( + + ) : ( + recoveryMethods.map((method) => { + if (method.type === 'totp') { + return ( + { + capture('ConfigureTotpClicked'); + setSelectedMethod(method); + // setScreen(RecoveryMethodScreen.Authenticator); + }} + /> + ); + } + return ( - { - capture(card.analyticsKey); - history.push(card.to); + capture('ConfigureFidoClicked'); + console.log('method clicked: ', method); + setSelectedMethod(method); + // history.push(`${path}/recovery-method`); + // setFidoDetails(method); + // setScreen(RecoveryMethodScreen.FidoDetails); }} - icon={card.icon} - text={card.title} - description={card.description} - key={idx} /> ); - })} - + }) + )} + ); From 9b038b1f23669616d374690671aa6373f9146898 Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Wed, 27 Aug 2025 18:30:44 +0200 Subject: [PATCH 05/22] chore: general cleanup --- .../Authenticator/AuthenticatorDetails.tsx | 19 ++-- .../AuthenticatorVerifyScreen.tsx | 36 ++++---- .../RecoveryMethods/FIDO/FIDODetails.tsx | 16 ++-- .../RecoveryMethods/RecoveryMethodCard.tsx | 92 ++++++------------- .../RecoveryMethods/RecoveryMethodList.tsx | 13 ++- .../RecoveryMethods/RecoveryMethods.tsx | 68 ++++---------- packages/types/src/seedless.ts | 1 + 7 files changed, 93 insertions(+), 152 deletions(-) diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx index bae76b3a7..17b3122b2 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx @@ -1,14 +1,13 @@ -import { Page } from '@/components/Page'; -import { Button } from '@avalabs/k2-alpine'; -import { useTranslation } from 'react-i18next'; +import { getIconForMethod } from '../RecoveryMethodCard'; +import { CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; -export const AuthenticatorDetails = ({ method }) => { - const { t } = useTranslation(); +export const AuthenticatorDetails = ({ method, methodName }) => { return ( - <> -
AuthenticatorDetails
- - - + {}} + icon={getIconForMethod(method)} + text={methodName} + key={method.toString()} + /> ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx index b66a366ed..f49501524 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx @@ -4,21 +4,23 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const AuthenticatorVerifyScreen = ({ totpChallenge }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - return ( -
- - - {/* + const theme = useTheme(); + const { t } = useTranslation(); + + return ( +
+ + + {/* {t( 'Alternatively, open any authenticator app and enter this code:', @@ -37,6 +39,6 @@ export const AuthenticatorVerifyScreen = ({ totpChallenge }) => { {totpSecret} */} -
- ); +
+ ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx index 6d2744928..d5ed3b76b 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx @@ -1,13 +1,13 @@ -import { Page } from '@/components/Page'; -import { Button } from '@avalabs/k2-alpine'; -import { useTranslation } from 'react-i18next'; +import { CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; +import { getIconForMethod } from '../RecoveryMethodCard'; export const FIDODetails = ({ method }) => { - const { t } = useTranslation(); return ( - <> -
FIDODetails
- - + {}} + icon={getIconForMethod(method)} + text={method.name} + key={method.toString()} + /> ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx index 94109c353..a7a64b835 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx @@ -1,68 +1,34 @@ import { CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; -import { Button } from '@avalabs/k2-alpine'; -import { useAnalyticsContext } from '@core/ui'; -import { useTranslation } from 'react-i18next'; +import { MethodIcons } from './RecoveryMethodList'; +import { RecoveryMethod } from '@core/types'; -export const RecoveryMethodCard = ({ method, onClick, methodName }) => { - console.log('method: ', method); - const { capture } = useAnalyticsContext(); - const { t } = useTranslation(); +interface RecoveryMethodCardProps { + method: RecoveryMethod; + onClick: () => void; + methodName?: string; +} + +export const getIconForMethod = (method) => { + if (method.type === 'totp') { + return MethodIcons.authenticator; + } + if (method.aaguid === '00000000-0000-0000-0000-000000000000') { + return MethodIcons.yubikey; + } + return MethodIcons.passkey; +}; + +export const RecoveryMethodCard = ({ + method, + onClick, + methodName, +}: RecoveryMethodCardProps) => { return ( - <> - - {/* */} - - // - //
- // - // {method.type === 'totp' && t('Authenticator App')} - // {method.type === 'fido' && t('Security Key')} - // {method.type === 'passkey' && t('Passkey')} - // - // - // {t('Added on')} {new Date(method.addedAt).toLocaleDateString()} - // - //
- //
- // - //
- //
+ ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx index 6c2c1b62d..f78198338 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx @@ -6,10 +6,15 @@ import { SecurityKeyIcon, } from '@avalabs/k2-alpine'; import { useAnalyticsContext } from '@core/ui'; -import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useRouteMatch } from 'react-router-dom'; +export const MethodIcons = { + passkey: , + authenticator: , + yubikey: , +}; + export const RecoveryMethodList = () => { const { t } = useTranslation(); const history = useHistory(); @@ -18,7 +23,7 @@ export const RecoveryMethodList = () => { const recoveryMethodCards = [ { - icon: , + icon: MethodIcons.passkey, title: t('Passkey'), description: t( 'Passkeys are used for quick, password-free recovery and enhanced security.', @@ -27,7 +32,7 @@ export const RecoveryMethodList = () => { analyticsKey: 'AddPasskeyClicked', }, { - icon: , + icon: MethodIcons.authenticator, title: t('Authenticator app'), description: t( 'Authenticator apps generate secure, time-based codes for wallet recovery.', @@ -36,7 +41,7 @@ export const RecoveryMethodList = () => { analyticsKey: 'AddAuthenticatorClicked', }, { - icon: , + icon: MethodIcons.yubikey, title: t('Yubikey'), description: t( 'YubiKeys are physical, hardware-based protection and strong authentication.', diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx index 04be4f610..c5356f7a4 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx @@ -1,51 +1,28 @@ import { Page } from '@/components/Page'; -import { CardMenu, CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; -import { - Box, - Button, - ChevronRightIcon, - Divider, - EncryptedIcon, - IconButton, - Paper, - PasswordIcon, - SecurityKeyIcon, - Skeleton, - Typography, -} from '@avalabs/k2-alpine'; -import { FeatureGates } from '@core/types'; -import { - useAnalyticsContext, - useFeatureFlagContext, - useSeedlessMfaManager, -} from '@core/ui'; +import { Button, Paper, Skeleton } from '@avalabs/k2-alpine'; +import { RecoveryMethod as RecoveryMethodType } from '@core/types'; +import { useAnalyticsContext, useSeedlessMfaManager } from '@core/ui'; import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { MdOutlinePassword } from 'react-icons/md'; -import { useHistory, useRouteMatch } from 'react-router-dom'; +import { useRouteMatch } from 'react-router-dom'; import { RecoveryMethodList } from './RecoveryMethodList'; import { RecoveryMethodCard } from './RecoveryMethodCard'; import { RecoveryMethod } from './RecoveryMethod'; export const RecoveryMethods: FC = () => { const { t } = useTranslation(); - const history = useHistory(); const { capture } = useAnalyticsContext(); const { path } = useRouteMatch(); console.log('path: ', path); - const [selectedMethod, setSelectedMethod] = useState(null); + const [selectedMethod, setSelectedMethod] = + useState(null); const { isLoadingRecoveryMethods, recoveryMethods, - hasFidoConfigured, + // hasFidoConfigured, hasMfaConfigured, - hasTotpConfigured, + // hasTotpConfigured, } = useSeedlessMfaManager(); - // console.log('isLoadingRecoveryMethods: ', isLoadingRecoveryMethods); - // console.log('recoveryMethods: ', recoveryMethods); - // console.log('hasFidoConfigured: ', hasFidoConfigured); - // console.log('hasMfaConfigured: ', hasMfaConfigured); - // console.log('hasTotpConfigured: ', hasTotpConfigured); console.log('selectedMethod: ', selectedMethod); if (selectedMethod) { @@ -80,11 +57,9 @@ export const RecoveryMethods: FC = () => { method={method} key="totp" methodName={t('Authenticator')} - // methodName={t('Authenticator')} onClick={() => { capture('ConfigureTotpClicked'); setSelectedMethod(method); - // setScreen(RecoveryMethodScreen.Authenticator); }} /> ); @@ -94,33 +69,26 @@ export const RecoveryMethods: FC = () => { { capture('ConfigureFidoClicked'); console.log('method clicked: ', method); setSelectedMethod(method); - // history.push(`${path}/recovery-method`); - // setFidoDetails(method); - // setScreen(RecoveryMethodScreen.FidoDetails); }} /> ); }) )} - + ); }; diff --git a/packages/types/src/seedless.ts b/packages/types/src/seedless.ts index 56160a66b..9f60d274a 100644 --- a/packages/types/src/seedless.ts +++ b/packages/types/src/seedless.ts @@ -125,6 +125,7 @@ export type RecoveryMethodFido = { id: string; name: string; type: MfaRequestType.Fido; + aaguid?: string; }; export type GetRecoveryMethodsOptions = { From 231673c6a266606a0ab8e45f05ecb5915be842b1 Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Fri, 29 Aug 2025 08:57:14 +0200 Subject: [PATCH 06/22] feaT: add fullscreen method placeholder --- apps/next/src/components/Page/Page.tsx | 3 + .../Authenticator/AuthenticatorDetails.tsx | 11 ++ .../RecoveryMethods/FIDO/FIDODetails.tsx | 7 + .../FullScreens/FullScreenContent.tsx | 164 ++++++++++++++++++ .../FullScreens/FullScreenRecoveryMethods.tsx | 25 +++ .../FullScreens/RemoveTotp.tsx | 127 ++++++++++++++ .../RecoveryMethods/RecoveryMethod.tsx | 42 ++++- .../RecoveryMethods/RecoveryMethods.tsx | 10 +- apps/next/src/routing/AppRoutes.tsx | 2 + 9 files changed, 385 insertions(+), 6 deletions(-) create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenRecoveryMethods.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx diff --git a/apps/next/src/components/Page/Page.tsx b/apps/next/src/components/Page/Page.tsx index 5075b6079..3c22d2ae4 100644 --- a/apps/next/src/components/Page/Page.tsx +++ b/apps/next/src/components/Page/Page.tsx @@ -7,6 +7,7 @@ type PageProps = { description?: string; children: React.ReactNode; withBackButton?: boolean; + onBackClicked?: () => void; contentProps?: StackProps; }; @@ -24,6 +25,7 @@ export const Page = ({ description, children, contentProps, + onBackClicked, ...htmlProps }: PageProps) => { const { ref, isIntersecting, isObserving } = useIsIntersecting(); @@ -41,6 +43,7 @@ export const Page = ({ isObserving={isObserving} isIntersecting={isIntersecting} title={title} + onBackClicked={onBackClicked} /> diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx index 17b3122b2..835839979 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx @@ -1,6 +1,17 @@ import { getIconForMethod } from '../RecoveryMethodCard'; import { CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; +export enum AuthenticatorState { + Initial = 'initial', + Initiated = 'initiated', + ConfirmChange = 'confirm-change', + ConfirmRemoval = 'confirm-removal', + Pending = 'pending', + Completing = 'completing', + VerifyCode = 'verify-code', + Failure = 'failure', +} + export const AuthenticatorDetails = ({ method, methodName }) => { return ( { return ( { + // const { + // authenticate, + // step, + // methods, + // chooseMfaMethod, + // mfaDeviceName, + // error, + // verifyTotpCode, + // completeFidoChallenge, + // } = useSeedlessAuth({ + // getOidcToken, + // setIsLoading, + // onSignerTokenObtained: onAuthSuccess, + // }); + + return ; + // const history = useHistory(); + // const { t } = useTranslation(); + // const { capture } = useAnalyticsContext(); + // const { setCurrent, setTotal, setIsBackButtonVisible } = + // useModalPageControl(); + // const { phase = 'connect-avax' } = useParams<{ phase: ImportRoute }>(); + // const { importLedger, isImporting } = useImportLedger(); + // const openApp = useOpenApp(); + + // const [publicKeys, setPublicKeys] = useState([]); + // const [extPublicKeys, setExtPublicKeys] = useState([]); + + // useEffect(() => { + // const step = PHASE_TO_STEP_NUMBER[phase]; + + // if (step) { + // setCurrent(step); + // setTotal(TOTAL_STEPS); + + // // We don't want to display the back button on the first screen + // // (it won't do anything, since history state is empty) + // setIsBackButtonVisible(step > 1); + // } else { + // // If we're on troubleshooting screens, hide the page indicator + // setTotal(0); + // } + // }, [phase, setCurrent, setIsBackButtonVisible, setTotal]); + + // const avalancheConnectorCallbacks = useMemo( + // () => ({ + // onConnectionSuccess: () => capture(`${ANALYTICS_EVENT_PREFIX}Connected`), + // onConnectionFailed: (err: Error) => + // err instanceof WalletExistsError + // ? capture(`${ANALYTICS_EVENT_PREFIX}DuplicateWallet`) + // : capture(`${ANALYTICS_EVENT_PREFIX}ConnectionFailed`), + // onConnectionRetry: () => capture(`${ANALYTICS_EVENT_PREFIX}Retry`), + // }), + // [capture], + // ); + + // const solanaConnectorCallbacks = useMemo( + // () => ({ + // onConnectionSuccess: () => + // capture(`${ANALYTICS_EVENT_PREFIX}SolanaKeysDerived`), + // onConnectionFailed: () => + // capture(`${ANALYTICS_EVENT_PREFIX}SolanaKeysFailed`), + // onConnectionRetry: () => + // capture(`${ANALYTICS_EVENT_PREFIX}SolanaKeysRetry`), + // }), + // [capture], + // ); + + // const onSave = useCallback( + // async (name: string) => { + // try { + // await importLedger({ + // name, + // addressPublicKeys: publicKeys, + // extendedPublicKeys: extPublicKeys, + // }); + // openApp(); + // window.close(); + // } catch (err) { + // toast.error(t('Unknown error has occurred. Please try again later.')); + // console.error(err); + // } + // }, + // [extPublicKeys, importLedger, publicKeys, openApp, t], + // ); + + return ( +
Fullscreen
+ // + // + // { + // setPublicKeys(addressPublicKeys.map(({ key }) => key)); + // setExtPublicKeys(extendedPublicKeys ?? []); + // history.push('/import-wallet/ledger/prompt-solana'); + // }} + // onTroubleshoot={() => { + // capture(`${ANALYTICS_EVENT_PREFIX}TroubleshootingAvalanche`); + // history.push(`${BASE_PATH}/troubleshooting-avalanche`); + // }} + // /> + // + // + // { + // capture(`${ANALYTICS_EVENT_PREFIX}SolanaSupportConfirmed`); + // history.push(`${BASE_PATH}/connect-solana`); + // }} + // onSkip={() => { + // capture(`${ANALYTICS_EVENT_PREFIX}SolanaSupportDenied`); + // history.push(`${BASE_PATH}/name`); + // }} + // /> + // + // + // { + // setPublicKeys((prev) => [ + // ...prev, + // ...addressPublicKeys.map(({ key }) => key), + // ]); + // history.push(`${BASE_PATH}/name`); + // }} + // onTroubleshoot={() => { + // capture(`${ANALYTICS_EVENT_PREFIX}TroubleshootingSolana`); + // history.push(`${BASE_PATH}/troubleshooting-solana`); + // }} + // /> + // + // + // { + // capture(`${ANALYTICS_EVENT_PREFIX}TroubleshootingAvalancheClosed`); + // history.push(`${BASE_PATH}/connect-avax`); + // }} + // /> + // + // + // { + // capture(`${ANALYTICS_EVENT_PREFIX}TroubleshootingSolanaClosed`); + // history.push(`${BASE_PATH}/connect-solana`); + // }} + // /> + // + // + // + // + // + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenRecoveryMethods.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenRecoveryMethods.tsx new file mode 100644 index 000000000..47d7f989c --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenRecoveryMethods.tsx @@ -0,0 +1,25 @@ +import { useHistory } from 'react-router-dom'; + +import { FullscreenModal } from '@/components/FullscreenModal'; +import { FullscreenAnimatedBackground } from '@/components/FullscreenAnimatedBackground'; + +import { FullScreenContent } from './FullScreenContent'; + +export const FullScreenRecoveryMethods = () => { + const history = useHistory(); + + return ( + <> + + + + + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx new file mode 100644 index 000000000..eba2812c7 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx @@ -0,0 +1,127 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + AlertCircleIcon, + Button, + CheckCircleIcon, + Stack, + Typography, +} from '@avalabs/core-k2-components'; + +import { useSeedlessMfaManager } from '@core/ui'; + +enum RemoveTotpState { + Loading = 'loading', + IncompatibleWallet = 'incompatible-wallet', + NameYourDevice = 'name-your-device', + AddAuthenticator = 'add-authenticator', + Success = 'success', + Failure = 'failure', +} + +export const RemoveTotp = () => { + const { t } = useTranslation(); + const { removeTotp } = useSeedlessMfaManager(); + const [state, setState] = useState(RemoveTotpState.Loading); + + const remove = useCallback(async () => { + try { + await removeTotp(); + setState(RemoveTotpState.Success); + } catch { + setState(RemoveTotpState.Failure); + } + }, [removeTotp]); + + useEffect(() => { + remove(); + }, [remove]); + + return ( + + {state === RemoveTotpState.Failure && ( + + + + + {t('Something Went Wrong')} + + + {t('We encountered an unexpected issue.')} + + {t('Please try again.')} + + + + + + )} + {state === RemoveTotpState.Success && ( + + + {t('Success!')} + + {t('Authenticator app removed successfully!')} + + + + + )} + {state === RemoveTotpState.Loading && ( + <> + <>Loading + {/* {renderMfaPrompt()} */} + + )} + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx index eb8044279..02a9069ec 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx @@ -4,19 +4,54 @@ import { useSeedlessMfaManager } from '@core/ui'; import { useTranslation } from 'react-i18next'; import { AuthenticatorDetails } from './Authenticator/AuthenticatorDetails'; import { FIDODetails } from './FIDO/FIDODetails'; +import { RecoveryMethod as RecoveryMethodType } from '@core/types'; +import { useCallback } from 'react'; +import { openFullscreenTab } from '@core/common'; -export const RecoveryMethod = ({ method }) => { +interface RecoveryMethodProps { + method: RecoveryMethodType; + onBackClicked: () => void; +} + +export const RecoveryMethod = ({ + method, + onBackClicked, +}: RecoveryMethodProps) => { console.log('RecoveryMethod: ', method); const { t } = useTranslation(); const { hasTotpConfigured } = useSeedlessMfaManager(); + + const openRemoveTotpPopup = useCallback(async () => { + openFullscreenTab('remove-totp'); + }, []); + return ( - {method.type === 'totp' && } + {method.type === 'totp' && ( + + )} {method.type === 'fido' && } + {method.type === 'totp' && ( + + )} diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx index c5356f7a4..dab8e8afc 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx @@ -4,7 +4,6 @@ import { RecoveryMethod as RecoveryMethodType } from '@core/types'; import { useAnalyticsContext, useSeedlessMfaManager } from '@core/ui'; import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useRouteMatch } from 'react-router-dom'; import { RecoveryMethodList } from './RecoveryMethodList'; import { RecoveryMethodCard } from './RecoveryMethodCard'; import { RecoveryMethod } from './RecoveryMethod'; @@ -12,8 +11,6 @@ import { RecoveryMethod } from './RecoveryMethod'; export const RecoveryMethods: FC = () => { const { t } = useTranslation(); const { capture } = useAnalyticsContext(); - const { path } = useRouteMatch(); - console.log('path: ', path); const [selectedMethod, setSelectedMethod] = useState(null); const { @@ -26,7 +23,12 @@ export const RecoveryMethods: FC = () => { console.log('selectedMethod: ', selectedMethod); if (selectedMethod) { - return ; + return ( + setSelectedMethod(null)} + /> + ); } return ( diff --git a/apps/next/src/routing/AppRoutes.tsx b/apps/next/src/routing/AppRoutes.tsx index f5cf0c096..a0c80984c 100644 --- a/apps/next/src/routing/AppRoutes.tsx +++ b/apps/next/src/routing/AppRoutes.tsx @@ -9,6 +9,7 @@ import { ImportLedgerFlow, ImportSeedphraseFlow } from '@/pages/Import'; import AccountManagement from '@/pages/AccountManagement/AccountManagement'; import { getContactsPath, getSendPath } from '@/config/routes'; +import { FullScreenRecoveryMethods } from '@/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenRecoveryMethods'; export const AppRoutes = () => ( @@ -19,6 +20,7 @@ export const AppRoutes = () => ( + ); From 165b318652b8a5395473f418a6b74e3af3c81eb2 Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Fri, 5 Sep 2025 17:12:49 +0200 Subject: [PATCH 07/22] feat: functionality --- apps/next/src/pages/Settings/Settings.tsx | 4 +- .../AuthenticatorVerifyScreen.tsx | 54 ++++----- .../Authenticator/AuthenticatorVerifyTotp.tsx | 25 ++++ .../RecoveryMethods/ConfiguredMethodList.tsx | 43 +++++++ .../components/RecoveryMethods/FIDO/FIDO.tsx | 89 ++++++++++++++ .../RecoveryMethods/FullScreens/AddFIDO.tsx | 94 +++++++++++++++ .../RecoveryMethods/FullScreens/AddTotp.tsx | 109 ++++++++++++++++++ .../FullScreens/FullScreenContent.tsx | 63 +++++++++- .../FullScreens/FullScreenRecoveryMethods.tsx | 25 ---- .../FullScreens/RecoveryMethodsFullScreen.tsx | 39 +++++++ .../FullScreens/RemoveTotp.tsx | 44 +++++-- .../RecoveryMethods/RecoveryMethod.tsx | 33 ++++-- .../RecoveryMethods/RecoveryMethodList.tsx | 25 +++- .../RecoveryMethods/RecoveryMethods.tsx | 79 ++++++++++++- .../components/ConfirmPage.tsx | 22 ++++ .../pages/MFA/components/FIDOChallenge.tsx | 16 ++- apps/next/src/routing/AppRoutes.tsx | 7 +- 17 files changed, 682 insertions(+), 89 deletions(-) create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyTotp.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDO.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddFIDO.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx delete mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenRecoveryMethods.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RecoveryMethodsFullScreen.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/components/ConfirmPage.tsx diff --git a/apps/next/src/pages/Settings/Settings.tsx b/apps/next/src/pages/Settings/Settings.tsx index ca00df7a0..2c4ad53ff 100644 --- a/apps/next/src/pages/Settings/Settings.tsx +++ b/apps/next/src/pages/Settings/Settings.tsx @@ -7,6 +7,7 @@ import { SettingsHomePage } from './components/Home'; import { RecoveryPhrase } from './components/RecoveryPhrase'; import { RecoveryMethods } from './components/RecoveryMethods'; import { Authenticator } from './components/RecoveryMethods/Authenticator'; +import { FIDO } from './components/RecoveryMethods/FIDO/FIDO'; export const Settings: FC = () => { const { path } = useRouteMatch(); @@ -18,10 +19,11 @@ export const Settings: FC = () => { + { +export const AuthenticatorVerifyScreen = ({ totpChallenge, onNext }) => { + console.log('AuthenticatorVerifyScreen: ', totpChallenge); const theme = useTheme(); const { t } = useTranslation(); return ( -
+ { level="H" size={188} /> - - {/* - - {t( - 'Alternatively, open any authenticator app and enter this code:', - )} - - { - navigator.clipboard.writeText(totpSecret); - toast.success(t('Code copied to clipboard')); - }} - > - {totpSecret} - - */} -
+ + + {t('Alternatively, open any authenticator app and enter this code:')} + + { + navigator.clipboard.writeText(totpChallenge.totpSecret); + toast.success(t('Code copied to clipboard')); + }} + > + {totpChallenge.totpSecret} + + + +
); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyTotp.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyTotp.tsx new file mode 100644 index 000000000..7ebff2cea --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyTotp.tsx @@ -0,0 +1,25 @@ +import { TotpCodeField } from '@/components/TotpCodeField'; +import { useKeyboardShortcuts, useTotpErrorMessage } from '@core/ui'; +import { useState } from 'react'; + +export const AuthenticatorVerifyTotp = ({ onChange, isLoading, error }) => { + const [isSubmitted, setIsSubmitted] = useState(false); + const totpError = useTotpErrorMessage(error); + // const keyboardShortcuts = useKeyboardShortcuts({ + // Enter: () => onSubmit(code), + // }); + + return ( + { + onChange(e.target.value); + if (error && !isLoading) { + setIsSubmitted(false); + } + }} + // {...keyboardShortcuts} + /> + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx new file mode 100644 index 000000000..5020759d0 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from 'react-i18next'; +import { RecoveryMethodCard } from './RecoveryMethodCard'; +import { useAnalyticsContext } from '@core/ui'; +import { RecoveryMethodScreen } from './RecoveryMethods'; + +export const ConfiguredMethodList = ({ + existingRecoveryMethods, + setSelectedMethod, + setScreen, +}) => { + const { t } = useTranslation(); + const { capture } = useAnalyticsContext(); + + return existingRecoveryMethods.map((method) => { + if (method.type === 'totp') { + return ( + { + capture('ConfigureTotpClicked'); + setSelectedMethod(method); + setScreen(RecoveryMethodScreen.Method); + }} + /> + ); + } + + return ( + { + capture('ConfigureFidoClicked'); + setSelectedMethod(method); + setScreen(RecoveryMethodScreen.Method); + }} + /> + ); + }); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDO.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDO.tsx new file mode 100644 index 000000000..b663256ff --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDO.tsx @@ -0,0 +1,89 @@ +import { Page } from '@/components/Page'; +import { Button, Typography } from '@avalabs/k2-alpine'; +import { useConnectionContext, useSeedlessMfaManager } from '@core/ui'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useParams } from 'react-router-dom'; +import { useMFAEvents } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAEvent'; +import { AuthErrorCode, ExtensionRequest, MfaResponseData } from '@core/types'; +import { TotpCodeField } from '@/components/TotpCodeField'; +import { SubmitMfaResponseHandler } from '~/services/seedless/handlers/submitMfaResponse'; + +export const FIDO = () => { + const { removeFidoDevice } = useSeedlessMfaManager(); + const { t } = useTranslation(); + const [error, setError] = useState(); + const [code, setCode] = useState(); + const { request } = useConnectionContext(); + console.log('code: ', code); + console.log('FIDO error: ', error); + const mfaEvents = useMFAEvents(setError); + console.log('mfaEvents: ', mfaEvents); + const { id } = useParams<{ id: string }>(); + const { hash } = useLocation(); + console.log('hash: ', hash); + const deviceId = `${id}${hash}`; + // const id = + // 'FidoKey#CWEYvVBjNFJSVB_Qjr3dcIs0mw6T6IcTASzEke_lbclVGTb-FRP5ZpUOXNMsRwBz7ZajS3NFeQH8pCa3h3mbJeUPWT8ocGMsdhF14ob2MB4dNBsGAfkchwRQTb1Vkv-B-t4KbPHtVD-dxncLdk6iwI6XVlXd2HAnekp_SB9fI-0='; + console.log('deviceId: ', deviceId); + + useEffect(() => { + removeFidoDevice(deviceId).then((result) => { + console.log('result: ', result); + }); + }, [deviceId, removeFidoDevice]); + + const submit = useCallback( + (params: MfaResponseData) => { + // setIsVerifying(true); + // onError(undefined); + + try { + request({ + method: ExtensionRequest.SEEDLESS_SUBMIT_MFA_RESPONSE, + params: [params], + }); + } catch { + // onError(AuthErrorCode.TotpVerificationError); + } finally { + // setIsVerifying(false); + } + }, + [request], + ); + + return ( + + + {t( + 'Open any authenticator app and scan the QR code below or enter the code manually', + )} + + {mfaEvents.challenge && mfaEvents.challenge.type === 'totp' && ( + <> + { + setCode(e.target.value); + }} + /> + + + )} + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddFIDO.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddFIDO.tsx new file mode 100644 index 000000000..2a0548d58 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddFIDO.tsx @@ -0,0 +1,94 @@ +import { + AuthErrorCode, + ExtensionRequest, + MfaResponseData, + RecoveryMethodTypes, + TotpResetChallenge, +} from '@core/types'; +import { + useConnectionContext, + useSeedlessActions, + useSeedlessMfaManager, +} from '@core/ui'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useMFAEvents } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAEvent'; +import { useMFAChoice } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAChoice'; +import { MfaChoicePrompt } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/MfaChoicePrompt'; +import { useSelectMFAMethod } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useSelectMFAMethod'; +import { MFA } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA'; +import { AuthenticatorVerifyScreen } from '../Authenticator/AuthenticatorVerifyScreen'; +import { Button, Stack } from '@avalabs/k2-alpine'; +import { AuthenticatorVerifyTotp } from '../Authenticator/AuthenticatorVerifyTotp'; +import { AuthenticatorState } from '../Authenticator/AuthenticatorDetails'; +import { SEEDLESS_ACTIONS_OPTIONS } from '@/pages/Onboarding/config'; +import { InProgress } from '../../RecoveryPhrase/components/ShowPhrase/components/InProgress'; +import { useTranslation } from 'react-i18next'; +import { FullscreenModalActions } from '@/components/FullscreenModal'; +import { SubmitMfaResponseHandler } from '~/services/seedless/handlers/submitMfaResponse'; +import { CompleteAuthenticatorChangeHandler } from '~/index'; +import { SeedlessNameFidoKey } from '@/pages/Onboarding/flows/SeedlessFlow/screens'; +import { KeyType } from '@core/types'; + +export const AddFIDO = ({ keyType }: { keyType: KeyType }) => { + const { request } = useConnectionContext(); + const [error, setError] = useState(); + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const { initAuthenticatorChange, addFidoDevice } = useSeedlessMfaManager(); + const [step, setStep] = useState<'name' | 'register' | 'verify'>('name'); + const [name, setName] = useState(''); + + const [totpChallenge, setTotpChallenge] = useState(); + const mfaChallenge = useMFAEvents(setError); + console.log('mfaChallenge: ', mfaChallenge); + const [screenState, setScreenState] = useState( + AuthenticatorState.Initiated, + ); + + // const [name, setName] = useState(''); + + const [code, setCode] = useState(''); + const submitButtonRef = useRef(null); + const [isSubmitted, setIsSubmitted] = useState(false); + const [isVerifying, setIsVerifying] = useState(false); + + const registerFidoKey = useCallback(async () => { + try { + console.log('registerFidoKey name: ', name); + const result = await addFidoDevice(name, keyType); + console.log('result: ', result); + // const challenge = await initAuthenticatorChange(); + // setTotpChallenge(challenge); + // setScreenState(AuthenticatorState.Pending); + } catch (e) { + console.log('e: ', e); + // setTotpChallenge(undefined); + setScreenState(AuthenticatorState.Failure); + } + }, [addFidoDevice, keyType, name]); + + useEffect(() => { + registerFidoKey(name); + }, [name, registerFidoKey]); + + return ( + + {isLoading && } + {!name && ( + { + setStep('register'); + setName(preferredName); + // registerFidoKey(); + }} + /> + )} + {step === 'register' && } + + {screenState === AuthenticatorState.Failure && ( +
Error occurred. Please try again.
+ )} +
+ ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx new file mode 100644 index 000000000..90c93667e --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx @@ -0,0 +1,109 @@ +import { + AuthErrorCode, + ExtensionRequest, + MfaResponseData, + TotpResetChallenge, +} from '@core/types'; +import { + useConnectionContext, + useSeedlessActions, + useSeedlessMfaManager, +} from '@core/ui'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useMFAEvents } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAEvent'; +import { useMFAChoice } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAChoice'; +import { MfaChoicePrompt } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/MfaChoicePrompt'; +import { useSelectMFAMethod } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useSelectMFAMethod'; +import { MFA } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA'; +import { AuthenticatorVerifyScreen } from '../Authenticator/AuthenticatorVerifyScreen'; +import { Button, Stack } from '@avalabs/k2-alpine'; +import { AuthenticatorVerifyTotp } from '../Authenticator/AuthenticatorVerifyTotp'; +import { AuthenticatorState } from '../Authenticator/AuthenticatorDetails'; +import { SEEDLESS_ACTIONS_OPTIONS } from '@/pages/Onboarding/config'; +import { InProgress } from '../../RecoveryPhrase/components/ShowPhrase/components/InProgress'; +import { useTranslation } from 'react-i18next'; +import { FullscreenModalActions } from '@/components/FullscreenModal'; +import { SubmitMfaResponseHandler } from '~/services/seedless/handlers/submitMfaResponse'; +import { CompleteAuthenticatorChangeHandler } from '~/index'; + +export const AddTotp = () => { + const { request } = useConnectionContext(); + const [error, setError] = useState(); + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const { initAuthenticatorChange, completeAuthenticatorChange } = + useSeedlessMfaManager(); + + const [totpChallenge, setTotpChallenge] = useState(); + console.log('totpChallenge: ', totpChallenge); + const mfaChallenge = useMFAEvents(setError); + console.log('mfaChallenge: ', mfaChallenge); + const [screenState, setScreenState] = useState( + AuthenticatorState.Initiated, + ); + + const [code, setCode] = useState(''); + console.log('code: ', code); + const submitButtonRef = useRef(null); + const [isSubmitted, setIsSubmitted] = useState(false); + const [isVerifying, setIsVerifying] = useState(false); + + const initChange = useCallback(async () => { + try { + const challenge = await initAuthenticatorChange(); + + setTotpChallenge(challenge); + setScreenState(AuthenticatorState.Pending); + } catch (e) { + setTotpChallenge(undefined); + setScreenState(AuthenticatorState.Failure); + } + }, [initAuthenticatorChange]); + + useEffect(() => { + initChange(); + }, [initChange]); + + return ( + + {isLoading && } + {!totpChallenge && } + {screenState === AuthenticatorState.Pending && totpChallenge && ( + setScreenState(AuthenticatorState.VerifyCode)} + /> + )} + {screenState === AuthenticatorState.VerifyCode && ( + <> + setCode(c)} + isLoading={isLoading} + error={error} + /> + + + + + )} + {screenState === AuthenticatorState.Failure && ( +
Error occurred. Please try again.
+ )} +
+ ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx index 579d68f92..7d2c0b427 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx @@ -1,7 +1,59 @@ import { useSeedlessAuth } from '@core/ui'; import { RemoveTotp } from './RemoveTotp'; +import { + FullscreenModalActions, + FullscreenModalTitle, +} from '@/components/FullscreenModal'; +import { useTranslation } from 'react-i18next'; +import { MfaChoicePrompt } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/MfaChoicePrompt'; +import { RecoveryMethodsFullScreenParams } from './RecoveryMethodsFullScreen'; +import { useMemo } from 'react'; +import { AddTotp } from './AddTotp'; +import { Button } from '@avalabs/k2-alpine'; +import { AddFIDO } from './AddFIDO'; -export const FullScreenContent = () => { +export type RecoveryMethodPages = 'removeTOTP' | 'addTOTP' | 'addFIDO'; // | 'addAuthenticator'; +export type FullScreenContentProps = { + [page in RecoveryMethodPages]: React.ReactNode; +}; + +export const FullScreenContent = ({ + mfaType, + action, + keyType, +}: RecoveryMethodsFullScreenParams) => { + const { t } = useTranslation(); + + const getPage = useMemo(() => { + if (mfaType === 'totp' && action === 'remove') { + return 'removeTOTP'; + } + if (mfaType === 'totp' && action === 'add') { + return 'addTOTP'; + } + if (mfaType === 'fido' && action === 'add') { + return 'addFIDO'; + } + }, [action, mfaType]); + + const page = getPage; + + const headline = { + removeTOTP: t('Authenticator removal'), + addTOTP: t('Add Authenticator'), + addFIDO: t('Add FIDO Device'), + }; + const content: FullScreenContentProps = { + removeTOTP: , + addTOTP: , + addFIDO: , + // MFAChoicePrompt: ( + // + // ), + }; // const { // authenticate, // step, @@ -17,7 +69,14 @@ export const FullScreenContent = () => { // onSignerTokenObtained: onAuthSuccess, // }); - return ; + return ( + <> + {headline[page]} + {mfaType === 'totp' && action === 'remove' && content[page]} + {mfaType === 'totp' && action === 'add' && content[page]} + {mfaType === 'fido' && action === 'add' && content[page]} + + ); // const history = useHistory(); // const { t } = useTranslation(); // const { capture } = useAnalyticsContext(); diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenRecoveryMethods.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenRecoveryMethods.tsx deleted file mode 100644 index 47d7f989c..000000000 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenRecoveryMethods.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useHistory } from 'react-router-dom'; - -import { FullscreenModal } from '@/components/FullscreenModal'; -import { FullscreenAnimatedBackground } from '@/components/FullscreenAnimatedBackground'; - -import { FullScreenContent } from './FullScreenContent'; - -export const FullScreenRecoveryMethods = () => { - const history = useHistory(); - - return ( - <> - - - - - - ); -}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RecoveryMethodsFullScreen.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RecoveryMethodsFullScreen.tsx new file mode 100644 index 000000000..d41a3c439 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RecoveryMethodsFullScreen.tsx @@ -0,0 +1,39 @@ +import { useHistory, useParams } from 'react-router-dom'; + +import { FullscreenModal } from '@/components/FullscreenModal'; +import { FullscreenAnimatedBackground } from '@/components/FullscreenAnimatedBackground'; + +import { FullScreenContent } from './FullScreenContent'; + +export interface RecoveryMethodsFullScreenParams { + mfaType?: 'totp' | 'fido'; + keyType?: 'yubikey' | 'passkey'; + action?: 'remove' | 'add'; +} + +export const RecoveryMethodsFullScreen = () => { + const history = useHistory(); + const { mfaType, action, keyType } = + useParams(); + console.log('mfaType: ', mfaType); + console.log('action: ', action); + + return ( + <> + + + + + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx index eba2812c7..0b5cfa8a0 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx @@ -4,11 +4,17 @@ import { AlertCircleIcon, Button, CheckCircleIcon, + CircularProgress, Stack, Typography, } from '@avalabs/core-k2-components'; import { useSeedlessMfaManager } from '@core/ui'; +import { AuthErrorCode, MfaRequestType } from '@core/types'; +import { useMFAChoice } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAChoice'; +import { useMFAEvents } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAEvent'; +import { InProgress } from '../../RecoveryPhrase/components/ShowPhrase/components/InProgress'; +import { FIDOChallenge } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge'; enum RemoveTotpState { Loading = 'loading', @@ -23,9 +29,16 @@ export const RemoveTotp = () => { const { t } = useTranslation(); const { removeTotp } = useSeedlessMfaManager(); const [state, setState] = useState(RemoveTotpState.Loading); + const [error, setError] = useState(); + console.log('error: ', error); + const mfaChoice = useMFAChoice(); + console.log('mfaChoice: ', mfaChoice); + const mfaChallenge = useMFAEvents(setError); + console.log('mfaChallenge: ', mfaChallenge); const remove = useCallback(async () => { try { + console.log('remove called: ', remove); await removeTotp(); setState(RemoveTotpState.Success); } catch { @@ -38,16 +51,7 @@ export const RemoveTotp = () => { }, [remove]); return ( - + {state === RemoveTotpState.Failure && ( { )} {state === RemoveTotpState.Loading && ( <> - <>Loading + + {/* Remove REFISGTER */} + {(mfaChallenge.challenge?.type === MfaRequestType.Fido || + mfaChallenge.challenge?.type === MfaRequestType.FidoRegister) && ( + + )} + {/* {mfaChallenge.challenge?.type === MfaRequestType.Totp && ( + + )} */} + {/* {renderMfaPrompt()} */} )} diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx index 26456c392..8c40aed90 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx @@ -1,5 +1,5 @@ import { Page } from '@/components/Page'; -import { Button } from '@avalabs/k2-alpine'; +import { Button, Stack } from '@avalabs/k2-alpine'; import { useSeedlessMfaManager } from '@core/ui'; import { useTranslation } from 'react-i18next'; import { AuthenticatorDetails } from './Authenticator/AuthenticatorDetails'; @@ -7,6 +7,7 @@ import { FIDODetails } from './FIDO/FIDODetails'; import { RecoveryMethod as RecoveryMethodType } from '@core/types'; import { useCallback } from 'react'; import { openFullscreenTab } from '@core/common'; +import { useHistory } from 'react-router-dom'; interface RecoveryMethodProps { method: RecoveryMethodType; @@ -20,18 +21,23 @@ export const RecoveryMethod = ({ console.log('RecoveryMethod: ', method); const { t } = useTranslation(); const { hasTotpConfigured } = useSeedlessMfaManager(); + const history = useHistory(); const openRemoveTotpPopup = useCallback(async () => { - openFullscreenTab('remove-totp'); + openFullscreenTab('update-recovery-method/totp/remove'); + }, []); + const openAddTotpPopup = useCallback(async () => { + openFullscreenTab('update-recovery-method/totp/add'); }, []); return ( - + // + {method.type === 'totp' && ( )} @@ -45,7 +51,9 @@ export const RecoveryMethod = ({ fullWidth // disabled={!isFormValid || isSubmitting} // loading={isSubmitting} - // onClick={isFormValid ? handleSubmit : undefined} + onClick={() => { + openAddTotpPopup(); + }} sx={{ mt: 'auto' }} disabled={!hasTotpConfigured} > @@ -64,11 +72,14 @@ export const RecoveryMethod = ({ sx={{ mt: 'auto' }} disabled={!hasTotpConfigured} onClick={() => { - openRemoveTotpPopup(); + return method.type === 'totp' + ? openRemoveTotpPopup() + : history.push(`/settings/recovery-method/fido/${method.id}`); }} > {t('Remove recovery method')} - + + // ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx index f78198338..d2e7c2ef3 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx @@ -5,6 +5,7 @@ import { PasswordIcon, SecurityKeyIcon, } from '@avalabs/k2-alpine'; +import { openFullscreenTab } from '@core/common'; import { useAnalyticsContext } from '@core/ui'; import { useTranslation } from 'react-i18next'; import { useHistory, useRouteMatch } from 'react-router-dom'; @@ -15,7 +16,11 @@ export const MethodIcons = { yubikey: , }; -export const RecoveryMethodList = () => { +export const RecoveryMethodList = ({ + hasTotpConfigured, +}: { + hasTotpConfigured: boolean; +}) => { const { t } = useTranslation(); const history = useHistory(); const { capture } = useAnalyticsContext(); @@ -28,8 +33,9 @@ export const RecoveryMethodList = () => { description: t( 'Passkeys are used for quick, password-free recovery and enhanced security.', ), - to: '/onboarding/import', + to: 'update-recovery-method/fido/add/passkey', analyticsKey: 'AddPasskeyClicked', + method: 'passkey', }, { icon: MethodIcons.authenticator, @@ -37,8 +43,10 @@ export const RecoveryMethodList = () => { description: t( 'Authenticator apps generate secure, time-based codes for wallet recovery.', ), - to: `${path}/authenticator`, + // add url for in-extension version + to: `update-recovery-method/totp/add`, analyticsKey: 'AddAuthenticatorClicked', + method: 'authenticator', }, { icon: MethodIcons.yubikey, @@ -46,8 +54,9 @@ export const RecoveryMethodList = () => { description: t( 'YubiKeys are physical, hardware-based protection and strong authentication.', ), - to: '/onboarding/import', + to: 'update-recovery-method/fido/add/yubikey', analyticsKey: 'AddYubikeyClicked', + method: 'yubikey', }, ]; @@ -55,11 +64,17 @@ export const RecoveryMethodList = () => {
}> {recoveryMethodCards.map((card, idx) => { + // Hide Authenticator option if TOTP is already configured + if (card.method === 'authenticator' && hasTotpConfigured) { + return null; + } + return ( { capture(card.analyticsKey); - history.push(card.to); + console.log('card.to: ', card.to); + openFullscreenTab(card.to); }} icon={card.icon} text={card.title} diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx index dab8e8afc..1584e7960 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx @@ -7,20 +7,88 @@ import { useTranslation } from 'react-i18next'; import { RecoveryMethodList } from './RecoveryMethodList'; import { RecoveryMethodCard } from './RecoveryMethodCard'; import { RecoveryMethod } from './RecoveryMethod'; +import { ConfiguredMethodList } from './ConfiguredMethodList'; + +export enum RecoveryMethodScreen { + ConfiguredList = 'configured-list', + NewList = 'new-list', + Method = 'method', + // AddNew = 'add-new', + // Authenticator = 'authenticator', + // FidoDetails = 'fido-details', +} export const RecoveryMethods: FC = () => { const { t } = useTranslation(); const { capture } = useAnalyticsContext(); const [selectedMethod, setSelectedMethod] = useState(null); + console.log('selectedMethod: ', selectedMethod); const { isLoadingRecoveryMethods, - recoveryMethods, + recoveryMethods: existingRecoveryMethods, // hasFidoConfigured, hasMfaConfigured, - // hasTotpConfigured, + hasTotpConfigured, } = useSeedlessMfaManager(); + const [screen, setScreen] = useState(RecoveryMethodScreen.ConfiguredList); + + return ( + + + {isLoadingRecoveryMethods && ( + <> + + + )} + + {screen === RecoveryMethodScreen.NewList && ( + + )} + + {screen === RecoveryMethodScreen.ConfiguredList && ( + + )} + + {screen === RecoveryMethodScreen.Method && selectedMethod && ( + setSelectedMethod(null)} + /> + )} + + {screen === RecoveryMethodScreen.ConfiguredList && ( + + )} + + ); console.log('selectedMethod: ', selectedMethod); if (selectedMethod) { return ( @@ -50,9 +118,9 @@ export const RecoveryMethods: FC = () => { )} {!isLoadingRecoveryMethods && !hasMfaConfigured ? ( - + ) : ( - recoveryMethods.map((method) => { + existingRecoveryMethods.map((method) => { if (method.type === 'totp') { return ( { size="extension" fullWidth sx={{ mt: 'auto' }} + onClick={() => { + capture('AddRecoveryMethodClicked'); + }} > {t('Add recovery method')} diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/components/ConfirmPage.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/components/ConfirmPage.tsx new file mode 100644 index 000000000..6020d47e4 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/components/ConfirmPage.tsx @@ -0,0 +1,22 @@ +import { SlideUpDialog } from '@/components/Dialog'; +import { Page } from '@/components/Page'; +import { Button } from '@avalabs/k2-alpine'; +import { useTranslation } from 'react-i18next'; + +export const ConfirmPage = ({ onNext, onBack }) => { + const { t } = useTranslation(); + return ( + + + + + + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx index f264225d3..d8e1c1109 100644 --- a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx @@ -18,6 +18,8 @@ import { MdErrorOutline } from 'react-icons/md'; import { SubmitMfaResponseHandler } from '~/services/seedless/handlers/submitMfaResponse'; import { InProgress } from '../../../../InProgress'; import { ChallengeComponentProps } from '../../../types'; +import { useParams } from 'react-router-dom'; +import { RecoveryMethodsFullScreenParams } from '@/pages/Settings/components/RecoveryMethods/FullScreens/RecoveryMethodsFullScreen'; type Props = ChallengeComponentProps< MfaRequestType.Fido | MfaRequestType.FidoRegister @@ -34,6 +36,7 @@ export const FIDOChallenge: FC = ({ const { request } = useConnectionContext(); const [force, setForce] = useState(false); const [isVerifying, setIsVerifying] = useState(false); + const { keyType } = useParams(); useEffect(() => { if (isVerifying && !force) { @@ -44,8 +47,15 @@ export const FIDOChallenge: FC = ({ setForce(false); onError(undefined); - launchFidoFlow(FIDOApiEndpoint.Authenticate, challenge.options) + launchFidoFlow( + challenge.type === MfaRequestType.Fido + ? FIDOApiEndpoint.Authenticate + : FIDOApiEndpoint.Register, + challenge.options, + challenge.type === MfaRequestType.FidoRegister ? keyType : undefined, + ) .then((answer) => { + console.log('answer: ', answer); request({ method: ExtensionRequest.SEEDLESS_SUBMIT_MFA_RESPONSE, params: [ @@ -61,9 +71,11 @@ export const FIDOChallenge: FC = ({ onError, request, isVerifying, - challenge.mfaId, + challenge?.mfaId, force, challenge.options, + challenge.type, + keyType, ]); return ( diff --git a/apps/next/src/routing/AppRoutes.tsx b/apps/next/src/routing/AppRoutes.tsx index a0c80984c..2d4da2171 100644 --- a/apps/next/src/routing/AppRoutes.tsx +++ b/apps/next/src/routing/AppRoutes.tsx @@ -9,7 +9,7 @@ import { ImportLedgerFlow, ImportSeedphraseFlow } from '@/pages/Import'; import AccountManagement from '@/pages/AccountManagement/AccountManagement'; import { getContactsPath, getSendPath } from '@/config/routes'; -import { FullScreenRecoveryMethods } from '@/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenRecoveryMethods'; +import { RecoveryMethodsFullScreen } from '@/pages/Settings/components/RecoveryMethods/FullScreens/RecoveryMethodsFullScreen'; export const AppRoutes = () => ( @@ -20,7 +20,10 @@ export const AppRoutes = () => ( - + ); From 52904d1e90fd02c926b488f8ca020d4e9a51c60d Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Thu, 11 Sep 2025 19:14:29 +0200 Subject: [PATCH 08/22] feat: the recovery method management flow --- .../pages/Onboarding/components/CardMenu.tsx | 22 +- .../screens/SeedlessNameFidoKey.tsx | 10 +- .../subflows/SeedlessMfaLoginFlow.tsx | 2 + .../pages/Settings/components/Home/Home.tsx | 47 ++-- .../Home/components/SettingsNavItem.tsx | 14 +- .../Authenticator/Authenticator.tsx | 134 ++++++++---- .../Authenticator/AuthenticatorVerifyCode.tsx | 57 +++-- .../AuthenticatorVerifyScreen.tsx | 58 ++--- .../Authenticator/AuthenticatorVerifyTotp.tsx | 60 ++++-- .../RecoveryMethods/ConfiguredMethodList.tsx | 68 +++--- .../components/RecoveryMethods/FIDO/FIDO.tsx | 89 +++++--- .../RecoveryMethods/FullScreens/AddFIDO.tsx | 118 ++++------- .../RecoveryMethods/FullScreens/AddTotp.tsx | 81 +++---- .../FullScreens/DefaultContent.tsx | 68 ++++++ .../FullScreens/FullScreenContent.tsx | 200 ++---------------- .../FullScreens/RemoveTotp.tsx | 25 +-- .../RecoveryMethods/RecoveryMethod.tsx | 99 +++++---- .../RecoveryMethods/RecoveryMethodCard.tsx | 1 + .../RecoveryMethods/RecoveryMethodList.tsx | 92 +++++--- .../RecoveryMethods/RecoveryMethods.tsx | 158 +++++--------- .../components/RecoveryMethodFailure.tsx | 17 ++ .../components/SeedlessFlow/pages/MFA/MFA.tsx | 2 +- .../pages/MFA/components/FIDOChallenge.tsx | 5 +- .../pages/MFA/components/TOTPChallenge.tsx | 2 +- 24 files changed, 743 insertions(+), 686 deletions(-) create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/DefaultContent.tsx create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx diff --git a/apps/next/src/pages/Onboarding/components/CardMenu.tsx b/apps/next/src/pages/Onboarding/components/CardMenu.tsx index 881a9f6b3..8b80bf1f0 100644 --- a/apps/next/src/pages/Onboarding/components/CardMenu.tsx +++ b/apps/next/src/pages/Onboarding/components/CardMenu.tsx @@ -7,6 +7,7 @@ import { StackProps, styled, Typography, + useTheme, } from '@avalabs/k2-alpine'; import { FC, ReactNode, type ReactElement } from 'react'; import { useHistory } from 'react-router-dom'; @@ -26,6 +27,8 @@ type CardMenuItemProps = { icon: ReactElement; text: string; description?: string; + size?: string; + itemGap?: string; } & ( | { link: string; @@ -39,22 +42,32 @@ export const CardMenuItem: FC = ({ icon, text, description, + size, ...props }) => { const history = useHistory(); + const theme = useTheme(); const onClick = 'onClick' in props ? props.onClick : () => history.push(props.link); return ( - + {icon} - {text} + + {text} + {description && ( @@ -78,10 +91,7 @@ export const CardMenuItem: FC = ({ const CardMenuItemContainer = styled(MenuItem)(({ theme }) => ({ flexDirection: 'row', justifyContent: 'space-between', - gap: theme.spacing(3), color: theme.palette.text.primary, - paddingLeft: theme.spacing(2.5), - paddingRight: theme.spacing(2.5), transition: 'background-color .15s ease-in-out', '& .CardLikeMenuItem-chevron': { diff --git a/apps/next/src/pages/Onboarding/flows/SeedlessFlow/screens/SeedlessNameFidoKey.tsx b/apps/next/src/pages/Onboarding/flows/SeedlessFlow/screens/SeedlessNameFidoKey.tsx index cf97c5da7..f7431d9cd 100644 --- a/apps/next/src/pages/Onboarding/flows/SeedlessFlow/screens/SeedlessNameFidoKey.tsx +++ b/apps/next/src/pages/Onboarding/flows/SeedlessFlow/screens/SeedlessNameFidoKey.tsx @@ -15,11 +15,13 @@ import { NavButton } from '@/pages/Onboarding/components/NavButton'; type SeedlessNameFidoKeyProps = { keyType: 'passkey' | 'yubikey'; onNext: (name: string) => void; + required?: boolean; }; export const SeedlessNameFidoKey: FC = ({ keyType, onNext, + required, }) => { const { t } = useTranslation(); const [name, setName] = useState(''); @@ -50,9 +52,11 @@ export const SeedlessNameFidoKey: FC = ({ - onNext('')}> - {t(`Skip`)} - + {!required && ( + onNext('')}> + {t(`Skip`)} + + )} = ({ const history = useHistory(); const { registerBackButtonHandler, setTotal } = useModalPageControl(); const { oidcToken, setSeedlessSignerToken } = useOnboardingContext(); + // TODO: remove + console.log('oidcToken: ', oidcToken); const [isLoading, setIsLoading] = useState(false); useEffect(() => { diff --git a/apps/next/src/pages/Settings/components/Home/Home.tsx b/apps/next/src/pages/Settings/components/Home/Home.tsx index 2464e6fd7..852626076 100644 --- a/apps/next/src/pages/Settings/components/Home/Home.tsx +++ b/apps/next/src/pages/Settings/components/Home/Home.tsx @@ -4,6 +4,7 @@ import { Stack, Switch, Typography, + useTheme, } from '@avalabs/k2-alpine'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -41,8 +42,10 @@ import { import { CurrencySelector } from '../CurrencySelector'; import { ThemeSelector } from '../ThemeSelector'; import { ViewPreferenceSelector } from '../ViewPreferenceSelector'; +import { Card } from '@/components/Card'; export const SettingsHomePage = () => { + const theme = useTheme(); const { t } = useTranslation(); const { lockWallet } = useSettingsContext(); const { isDeveloperMode, setDeveloperMode } = useNetworkContext(); @@ -91,14 +94,31 @@ export const SettingsHomePage = () => { /> - - {isMfaSetupPromptVisible && ( + {isMfaSetupPromptVisible && ( + + {t('Set up ')} + + } + labelTpyographyVariant="subtitle3" + descriptionTpyographyVariant="caption2" + sx={{ borderBottom: 'none' }} /> - )} - + + )} { divider href={`${path}/change-password`} /> - {(walletDetails?.type === SecretType.Mnemonic || - walletDetails?.type === SecretType.Seedless) && ( - - )} + {!isMfaSetupPromptVisible && + (walletDetails?.type === SecretType.Mnemonic || + walletDetails?.type === SecretType.Seedless) && ( + + )} {walletDetails?.type === SecretType.Seedless && diff --git a/apps/next/src/pages/Settings/components/Home/components/SettingsNavItem.tsx b/apps/next/src/pages/Settings/components/Home/components/SettingsNavItem.tsx index e0555bbe3..ecbaf8d2b 100644 --- a/apps/next/src/pages/Settings/components/Home/components/SettingsNavItem.tsx +++ b/apps/next/src/pages/Settings/components/Home/components/SettingsNavItem.tsx @@ -11,7 +11,13 @@ import { FC } from 'react'; import { openNewTab } from '@core/common'; import { useHistory } from 'react-router-dom'; -type OwnProps = { href?: string; label: string; description?: string }; +type OwnProps = { + href?: string; + label: string; + description?: string; + labelTpyographyVariant?: 'subtitle3'; + descriptionTpyographyVariant?: 'caption2'; +}; type SettingsNavItemProps = Omit & OwnProps; export const SettingsNavItem: FC = ({ @@ -20,6 +26,8 @@ export const SettingsNavItem: FC = ({ children, href, secondaryAction, + labelTpyographyVariant, // --- IGNORE --- + descriptionTpyographyVariant, // --- IGNORE --- ...props }) => { const history = useHistory(); @@ -56,10 +64,10 @@ export const SettingsNavItem: FC = ({ { const { t } = useTranslation(); - const { - initAuthenticatorChange, - completeAuthenticatorChange, - hasFidoConfigured, - hasTotpConfigured, - } = useSeedlessMfaManager(); + const history = useHistory(); + const { initAuthenticatorChange, completeAuthenticatorChange } = + useSeedlessMfaManager(); const [totpChallenge, setTotpChallenge] = useState(); const [showSecret, setShowSecret] = useState(false); - console.log('totpChallenge: ', totpChallenge); + const [screenState, setScreenState] = useState( + AuthenticatorState.Initial, + ); + const [code, setCode] = useState(''); + const [isSubmitted, setIsSubmitted] = useState(false); + const [error, setError] = useState(); + const goBack = useGoBack(); const initChange = useCallback(async () => { - console.log('initChange: '); - // if (hasFidoConfigured) { - // browser.tabs.create({ - // url: `${ContextContainer.FULLSCREEN}#/update-recovery-methods`, - // }); - // return; - // } - - // setState(State.Initiated); try { - console.log('try: '); const challenge = await initAuthenticatorChange(); - console.log('challenge: ', challenge); + setTotpChallenge(challenge); - // setState(State.Pending); - } catch (e) { - console.log('catch: ', e); + setScreenState(AuthenticatorState.Initiated); + } catch { setTotpChallenge(undefined); - // setState(State.Failure); + setScreenState(AuthenticatorState.Failure); } }, [initAuthenticatorChange]); @@ -58,31 +53,82 @@ export const Authenticator: FC = () => { return new URL(totpChallenge.totpUrl).searchParams.get('secret') ?? ''; }, [totpChallenge]); + const headline = { + [AuthenticatorState.Initial]: t('Scan QR code'), + [AuthenticatorState.Initiated]: t('Scan QR code'), + [AuthenticatorState.VerifyCode]: t('Verify code'), + [AuthenticatorState.Failure]: t('Something went wrong'), + }; + + const description = { + [AuthenticatorState.Initial]: t('Setting up your authenticator app.'), + [AuthenticatorState.Initiated]: t( + 'Open any authenticator app and scan the QR code below or enter the code manually', + ), + [AuthenticatorState.VerifyCode]: t( + 'Enter the code generated from the authenticator app', + ), + }; + + const onBack = useCallback(() => { + goBack(); + }, [goBack]); + + const onCodeSubmit = useCallback(async () => { + setIsSubmitted(true); + if (!totpChallenge) { + setScreenState(AuthenticatorState.Failure); + return; + } + try { + await completeAuthenticatorChange(totpChallenge.totpId, code); + toast.success(t('Authenticator added!')); + history.push('/settings/recovery-methods'); + } catch (e) { + setError(e as AuthErrorCode); + } finally { + setIsSubmitted(false); + } + }, [code, completeAuthenticatorChange, history, t, totpChallenge]); + return ( - - {t( - 'Open any authenticator app and scan the QR code below or enter the code manually', + {description[screenState]} + {screenState === AuthenticatorState.Initial && ( + + )} + {screenState === AuthenticatorState.Initiated && + !showSecret && + totpChallenge && ( + setScreenState(AuthenticatorState.VerifyCode)} + onShowSecret={() => setShowSecret(true)} + /> )} - - {totpChallenge && ( - console.log('Back clicked')} - totpChallenge={totpChallenge} - onNextClick={() => console.log('Next clicked')} - onShowSecret={() => setShowSecret(true)} + {screenState === AuthenticatorState.Initiated && showSecret && ( + setScreenState(AuthenticatorState.VerifyCode)} /> )} - {showSecret && } - - {/* console.log('next')} - /> */} + {totpChallenge && screenState === AuthenticatorState.VerifyCode && ( + { + setCode(c); + setError(undefined); + }} + error={error} + onSubmit={onCodeSubmit} + isSubmitted={isSubmitted} + /> + )} + {screenState === AuthenticatorState.Failure && } ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx index 26987db1b..afb17d3d9 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx @@ -1,31 +1,54 @@ -import { Stack, toast, Typography, useTheme } from '@avalabs/k2-alpine'; -import QRCode from 'qrcode.react'; -import { useMemo } from 'react'; +import { Button, Paper, Stack, toast, Typography } from '@avalabs/k2-alpine'; import { useTranslation } from 'react-i18next'; -export const AuthenticatorVerifyCode = ({ totpSecret }) => { - const theme = useTheme(); +export const AuthenticatorVerifyCode = ({ totpSecret, onNext }) => { const { t } = useTranslation(); return ( -
- - - {t('Alternatively, open any authenticator app and enter this code:')} - + + + {totpSecret} + +
+ {t('Copy')} + + + {/** TODO: Put the description sections with ICONS after they put in the k2 alpine */} + + ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx index d1d5aac61..fd1edb198 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyScreen.tsx @@ -1,46 +1,46 @@ -import { Button, Stack, toast, Typography, useTheme } from '@avalabs/k2-alpine'; +import { Button, Stack, useTheme } from '@avalabs/k2-alpine'; import QRCode from 'qrcode.react'; import { useTranslation } from 'react-i18next'; -export const AuthenticatorVerifyScreen = ({ totpChallenge, onNext }) => { +export const AuthenticatorVerifyScreen = ({ + totpChallenge, + onShowSecret, + onNext, +}) => { console.log('AuthenticatorVerifyScreen: ', totpChallenge); const theme = useTheme(); const { t } = useTranslation(); return ( - - - - {t('Alternatively, open any authenticator app and enter this code:')} - - { - navigator.clipboard.writeText(totpChallenge.totpSecret); - toast.success(t('Code copied to clipboard')); - }} - > - {totpChallenge.totpSecret} - + + + + + - + ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyTotp.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyTotp.tsx index 7ebff2cea..0fb00a192 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyTotp.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyTotp.tsx @@ -1,25 +1,51 @@ import { TotpCodeField } from '@/components/TotpCodeField'; +import { Button, Stack } from '@avalabs/k2-alpine'; +import { AuthErrorCode } from '@core/types'; import { useKeyboardShortcuts, useTotpErrorMessage } from '@core/ui'; -import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; -export const AuthenticatorVerifyTotp = ({ onChange, isLoading, error }) => { - const [isSubmitted, setIsSubmitted] = useState(false); +interface AuthenticatorVerifyTotpProps { + onChange: (code: string) => void; + error?: AuthErrorCode; + onSubmit?: () => void; + isSubmitted?: boolean; +} +export const AuthenticatorVerifyTotp = ({ + onChange, + error, + onSubmit, + isSubmitted, +}: AuthenticatorVerifyTotpProps) => { + console.log('error: ', error); const totpError = useTotpErrorMessage(error); - // const keyboardShortcuts = useKeyboardShortcuts({ - // Enter: () => onSubmit(code), - // }); + console.log('totpError: ', totpError); + const keyboardShortcuts = useKeyboardShortcuts({ + Enter: () => onSubmit && onSubmit(), + }); + const { t } = useTranslation(); return ( - { - onChange(e.target.value); - if (error && !isLoading) { - setIsSubmitted(false); - } - }} - // {...keyboardShortcuts} - /> + + { + onChange(e.target.value); + }} + {...keyboardShortcuts} + /> + {onSubmit && ( + + )} + ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx index 5020759d0..3cf4cb874 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx @@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next'; import { RecoveryMethodCard } from './RecoveryMethodCard'; import { useAnalyticsContext } from '@core/ui'; import { RecoveryMethodScreen } from './RecoveryMethods'; +import { Paper } from '@avalabs/k2-alpine'; export const ConfiguredMethodList = ({ existingRecoveryMethods, @@ -11,33 +12,44 @@ export const ConfiguredMethodList = ({ const { t } = useTranslation(); const { capture } = useAnalyticsContext(); - return existingRecoveryMethods.map((method) => { - if (method.type === 'totp') { - return ( - { - capture('ConfigureTotpClicked'); - setSelectedMethod(method); - setScreen(RecoveryMethodScreen.Method); - }} - /> - ); - } + return ( + + {existingRecoveryMethods.map((method) => { + if (method.type === 'totp') { + return ( + { + capture('ConfigureTotpClicked'); + setSelectedMethod(method); + setScreen(RecoveryMethodScreen.Method); + }} + /> + ); + } - return ( - { - capture('ConfigureFidoClicked'); - setSelectedMethod(method); - setScreen(RecoveryMethodScreen.Method); - }} - /> - ); - }); + return ( + { + capture('ConfigureFidoClicked'); + setSelectedMethod(method); + setScreen(RecoveryMethodScreen.Method); + }} + /> + ); + })} + + ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDO.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDO.tsx index b663256ff..d648d8db9 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDO.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDO.tsx @@ -1,57 +1,74 @@ import { Page } from '@/components/Page'; -import { Button, Typography } from '@avalabs/k2-alpine'; -import { useConnectionContext, useSeedlessMfaManager } from '@core/ui'; -import { useCallback, useEffect, useState } from 'react'; +import { Button, Stack, toast, Typography } from '@avalabs/k2-alpine'; +import { + useConnectionContext, + useKeyboardShortcuts, + useSeedlessMfaManager, + useTotpErrorMessage, +} from '@core/ui'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation, useParams } from 'react-router-dom'; +import { useHistory, useLocation, useParams } from 'react-router-dom'; import { useMFAEvents } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAEvent'; import { AuthErrorCode, ExtensionRequest, MfaResponseData } from '@core/types'; import { TotpCodeField } from '@/components/TotpCodeField'; import { SubmitMfaResponseHandler } from '~/services/seedless/handlers/submitMfaResponse'; export const FIDO = () => { + const history = useHistory(); const { removeFidoDevice } = useSeedlessMfaManager(); const { t } = useTranslation(); const [error, setError] = useState(); - const [code, setCode] = useState(); + const totpError = useTotpErrorMessage(error); + const [code, setCode] = useState(''); + const [isVerifying, setIsVerifying] = useState(false); + const { request } = useConnectionContext(); - console.log('code: ', code); - console.log('FIDO error: ', error); + const mfaEvents = useMFAEvents(setError); - console.log('mfaEvents: ', mfaEvents); + const { id } = useParams<{ id: string }>(); const { hash } = useLocation(); - console.log('hash: ', hash); + + const submitButtonRef = useRef(null); + const keyboardShortcuts = useKeyboardShortcuts({ + Enter: () => submitButtonRef.current?.click(), + }); + const deviceId = `${id}${hash}`; - // const id = - // 'FidoKey#CWEYvVBjNFJSVB_Qjr3dcIs0mw6T6IcTASzEke_lbclVGTb-FRP5ZpUOXNMsRwBz7ZajS3NFeQH8pCa3h3mbJeUPWT8ocGMsdhF14ob2MB4dNBsGAfkchwRQTb1Vkv-B-t4KbPHtVD-dxncLdk6iwI6XVlXd2HAnekp_SB9fI-0='; - console.log('deviceId: ', deviceId); + + const remove = useCallback(async () => { + try { + await removeFidoDevice(deviceId); + toast.success(t('FIDO device removed!')); + history.push('/settings/recovery-methods'); + } catch { + toast.error(t('Error occurred. Please try again.')); + history.push('/settings/recovery-methods'); + } + }, [deviceId, history, removeFidoDevice, t]); useEffect(() => { - removeFidoDevice(deviceId).then((result) => { - console.log('result: ', result); - }); - }, [deviceId, removeFidoDevice]); + remove(); + }, [deviceId, remove, removeFidoDevice]); - const submit = useCallback( - (params: MfaResponseData) => { - // setIsVerifying(true); - // onError(undefined); + const submitCode = useCallback( + async (params: MfaResponseData) => { + setIsVerifying(true); try { - request({ + await request({ method: ExtensionRequest.SEEDLESS_SUBMIT_MFA_RESPONSE, params: [params], }); } catch { - // onError(AuthErrorCode.TotpVerificationError); + setError(AuthErrorCode.TotpVerificationError); } finally { - // setIsVerifying(false); + setIsVerifying(false); } }, [request], ); - return ( { )} {mfaEvents.challenge && mfaEvents.challenge.type === 'totp' && ( - <> + { setCode(e.target.value); + setError(undefined); }} /> - + )} ); diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddFIDO.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddFIDO.tsx index 2a0548d58..6f863651d 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddFIDO.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddFIDO.tsx @@ -1,93 +1,67 @@ -import { - AuthErrorCode, - ExtensionRequest, - MfaResponseData, - RecoveryMethodTypes, - TotpResetChallenge, -} from '@core/types'; -import { - useConnectionContext, - useSeedlessActions, - useSeedlessMfaManager, -} from '@core/ui'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useMFAEvents } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAEvent'; -import { useMFAChoice } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAChoice'; -import { MfaChoicePrompt } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/MfaChoicePrompt'; -import { useSelectMFAMethod } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useSelectMFAMethod'; +import { useSeedlessMfaManager } from '@core/ui'; +import { useCallback, useState } from 'react'; import { MFA } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA'; -import { AuthenticatorVerifyScreen } from '../Authenticator/AuthenticatorVerifyScreen'; -import { Button, Stack } from '@avalabs/k2-alpine'; -import { AuthenticatorVerifyTotp } from '../Authenticator/AuthenticatorVerifyTotp'; -import { AuthenticatorState } from '../Authenticator/AuthenticatorDetails'; -import { SEEDLESS_ACTIONS_OPTIONS } from '@/pages/Onboarding/config'; -import { InProgress } from '../../RecoveryPhrase/components/ShowPhrase/components/InProgress'; +import { Stack, toast, Typography } from '@avalabs/k2-alpine'; import { useTranslation } from 'react-i18next'; -import { FullscreenModalActions } from '@/components/FullscreenModal'; -import { SubmitMfaResponseHandler } from '~/services/seedless/handlers/submitMfaResponse'; -import { CompleteAuthenticatorChangeHandler } from '~/index'; import { SeedlessNameFidoKey } from '@/pages/Onboarding/flows/SeedlessFlow/screens'; import { KeyType } from '@core/types'; +import { useHistory } from 'react-router-dom'; + +export enum AddFIDOState { + Initial = 'initial', + Initiated = 'initiated', + Pending = 'pending', + Failure = 'failure', + Success = 'success', +} export const AddFIDO = ({ keyType }: { keyType: KeyType }) => { - const { request } = useConnectionContext(); - const [error, setError] = useState(); const { t } = useTranslation(); - const [isLoading, setIsLoading] = useState(false); - const { initAuthenticatorChange, addFidoDevice } = useSeedlessMfaManager(); - const [step, setStep] = useState<'name' | 'register' | 'verify'>('name'); - const [name, setName] = useState(''); + const { addFidoDevice } = useSeedlessMfaManager(); + const history = useHistory(); - const [totpChallenge, setTotpChallenge] = useState(); - const mfaChallenge = useMFAEvents(setError); - console.log('mfaChallenge: ', mfaChallenge); - const [screenState, setScreenState] = useState( - AuthenticatorState.Initiated, + const [screenState, setScreenState] = useState( + AddFIDOState.Initial, ); - // const [name, setName] = useState(''); - - const [code, setCode] = useState(''); - const submitButtonRef = useRef(null); - const [isSubmitted, setIsSubmitted] = useState(false); - const [isVerifying, setIsVerifying] = useState(false); - - const registerFidoKey = useCallback(async () => { - try { - console.log('registerFidoKey name: ', name); - const result = await addFidoDevice(name, keyType); - console.log('result: ', result); - // const challenge = await initAuthenticatorChange(); - // setTotpChallenge(challenge); - // setScreenState(AuthenticatorState.Pending); - } catch (e) { - console.log('e: ', e); - // setTotpChallenge(undefined); - setScreenState(AuthenticatorState.Failure); - } - }, [addFidoDevice, keyType, name]); - - useEffect(() => { - registerFidoKey(name); - }, [name, registerFidoKey]); + const registerFidoKey = useCallback( + async (deviceName: string) => { + try { + await addFidoDevice(deviceName, keyType); + toast.success(t(`${deviceName} (${keyType}) added!`), { + duration: Infinity, + }); + history.push('/update-recovery-method'); + return; + } catch (e) { + console.log('e: ', e); + setScreenState(AddFIDOState.Failure); + } + }, + [addFidoDevice, history, keyType, t], + ); return ( - - {isLoading && } - {!name && ( + + {/* */} + {screenState === AddFIDOState.Initial && ( { - setStep('register'); - setName(preferredName); - // registerFidoKey(); + onNext={(deviceName) => { + setScreenState(AddFIDOState.Initiated); + registerFidoKey(deviceName); }} /> )} - {step === 'register' && } + {screenState === AddFIDOState.Initiated && } - {screenState === AuthenticatorState.Failure && ( -
Error occurred. Please try again.
+ {screenState === AddFIDOState.Failure && ( + + + {t('Error occurred. Please try again.')} + + )}
); diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx index 90c93667e..8e65513dd 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx @@ -1,36 +1,23 @@ -import { - AuthErrorCode, - ExtensionRequest, - MfaResponseData, - TotpResetChallenge, -} from '@core/types'; -import { - useConnectionContext, - useSeedlessActions, - useSeedlessMfaManager, -} from '@core/ui'; +import { AuthErrorCode, TotpResetChallenge } from '@core/types'; +import { useSeedlessMfaManager } from '@core/ui'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useMFAEvents } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAEvent'; -import { useMFAChoice } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAChoice'; -import { MfaChoicePrompt } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/MfaChoicePrompt'; -import { useSelectMFAMethod } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useSelectMFAMethod'; import { MFA } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA'; -import { AuthenticatorVerifyScreen } from '../Authenticator/AuthenticatorVerifyScreen'; -import { Button, Stack } from '@avalabs/k2-alpine'; +import { Button, Stack, toast } from '@avalabs/k2-alpine'; import { AuthenticatorVerifyTotp } from '../Authenticator/AuthenticatorVerifyTotp'; import { AuthenticatorState } from '../Authenticator/AuthenticatorDetails'; -import { SEEDLESS_ACTIONS_OPTIONS } from '@/pages/Onboarding/config'; import { InProgress } from '../../RecoveryPhrase/components/ShowPhrase/components/InProgress'; import { useTranslation } from 'react-i18next'; import { FullscreenModalActions } from '@/components/FullscreenModal'; -import { SubmitMfaResponseHandler } from '~/services/seedless/handlers/submitMfaResponse'; -import { CompleteAuthenticatorChangeHandler } from '~/index'; +import { SeedlessTotpQRCode } from '@/pages/Onboarding/flows/SeedlessFlow/screens'; +import { RecoveryMethodFailure } from '../components/RecoveryMethodFailure'; +import { useHistory } from 'react-router-dom'; export const AddTotp = () => { - const { request } = useConnectionContext(); const [error, setError] = useState(); + const history = useHistory(); const { t } = useTranslation(); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); const { initAuthenticatorChange, completeAuthenticatorChange } = useSeedlessMfaManager(); @@ -45,55 +32,71 @@ export const AddTotp = () => { const [code, setCode] = useState(''); console.log('code: ', code); const submitButtonRef = useRef(null); - const [isSubmitted, setIsSubmitted] = useState(false); const [isVerifying, setIsVerifying] = useState(false); const initChange = useCallback(async () => { try { const challenge = await initAuthenticatorChange(); - setTotpChallenge(challenge); setScreenState(AuthenticatorState.Pending); - } catch (e) { + } catch { setTotpChallenge(undefined); setScreenState(AuthenticatorState.Failure); } }, [initAuthenticatorChange]); + const onVerify = useCallback(async () => { + console.log('totpChallenge: ', totpChallenge); + if (!totpChallenge) { + return; + } + try { + setIsVerifying(true); + await completeAuthenticatorChange(totpChallenge.totpId, code); + toast.success(t('Authenticator added!'), { + duration: Infinity, + }); + history.push('update-recovery-method'); + } catch (e) { + setError(e as AuthErrorCode); + } finally { + setIsVerifying(false); + } + }, [code, completeAuthenticatorChange, history, t, totpChallenge]); + useEffect(() => { initChange(); + setIsLoading(false); }, [initChange]); return ( - + {isLoading && } {!totpChallenge && } {screenState === AuthenticatorState.Pending && totpChallenge && ( - setScreenState(AuthenticatorState.VerifyCode)} /> )} - {screenState === AuthenticatorState.VerifyCode && ( + {screenState === AuthenticatorState.VerifyCode && totpChallenge && ( <> setCode(c)} - isLoading={isLoading} + onChange={(c) => { + setCode(c); + setError(undefined); + }} error={error} /> + )} + + + + + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx index 7d2c0b427..f43cae200 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx @@ -1,18 +1,17 @@ -import { useSeedlessAuth } from '@core/ui'; import { RemoveTotp } from './RemoveTotp'; -import { - FullscreenModalActions, - FullscreenModalTitle, -} from '@/components/FullscreenModal'; -import { useTranslation } from 'react-i18next'; -import { MfaChoicePrompt } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/MfaChoicePrompt'; +import { FullscreenModalTitle } from '@/components/FullscreenModal'; import { RecoveryMethodsFullScreenParams } from './RecoveryMethodsFullScreen'; import { useMemo } from 'react'; import { AddTotp } from './AddTotp'; -import { Button } from '@avalabs/k2-alpine'; import { AddFIDO } from './AddFIDO'; +import { DefaultContent } from './DefaultContent'; + +export type RecoveryMethodPages = + | 'defaultContent' + | 'removeTOTP' + | 'addTOTP' + | 'addFIDO'; -export type RecoveryMethodPages = 'removeTOTP' | 'addTOTP' | 'addFIDO'; // | 'addAuthenticator'; export type FullScreenContentProps = { [page in RecoveryMethodPages]: React.ReactNode; }; @@ -22,8 +21,8 @@ export const FullScreenContent = ({ action, keyType, }: RecoveryMethodsFullScreenParams) => { - const { t } = useTranslation(); - + console.log('mfaType: ', mfaType); + console.log('action: ', action); const getPage = useMemo(() => { if (mfaType === 'totp' && action === 'remove') { return 'removeTOTP'; @@ -34,190 +33,25 @@ export const FullScreenContent = ({ if (mfaType === 'fido' && action === 'add') { return 'addFIDO'; } + return 'defaultContent'; }, [action, mfaType]); const page = getPage; - const headline = { - removeTOTP: t('Authenticator removal'), - addTOTP: t('Add Authenticator'), - addFIDO: t('Add FIDO Device'), - }; + const headline = {}; const content: FullScreenContentProps = { removeTOTP: , addTOTP: , addFIDO: , - // MFAChoicePrompt: ( - // - // ), + defaultContent: , }; - // const { - // authenticate, - // step, - // methods, - // chooseMfaMethod, - // mfaDeviceName, - // error, - // verifyTotpCode, - // completeFidoChallenge, - // } = useSeedlessAuth({ - // getOidcToken, - // setIsLoading, - // onSignerTokenObtained: onAuthSuccess, - // }); return ( <> - {headline[page]} - {mfaType === 'totp' && action === 'remove' && content[page]} - {mfaType === 'totp' && action === 'add' && content[page]} - {mfaType === 'fido' && action === 'add' && content[page]} + {headline[page] && ( + {headline[page]} + )} + {content[page]} ); - // const history = useHistory(); - // const { t } = useTranslation(); - // const { capture } = useAnalyticsContext(); - // const { setCurrent, setTotal, setIsBackButtonVisible } = - // useModalPageControl(); - // const { phase = 'connect-avax' } = useParams<{ phase: ImportRoute }>(); - // const { importLedger, isImporting } = useImportLedger(); - // const openApp = useOpenApp(); - - // const [publicKeys, setPublicKeys] = useState([]); - // const [extPublicKeys, setExtPublicKeys] = useState([]); - - // useEffect(() => { - // const step = PHASE_TO_STEP_NUMBER[phase]; - - // if (step) { - // setCurrent(step); - // setTotal(TOTAL_STEPS); - - // // We don't want to display the back button on the first screen - // // (it won't do anything, since history state is empty) - // setIsBackButtonVisible(step > 1); - // } else { - // // If we're on troubleshooting screens, hide the page indicator - // setTotal(0); - // } - // }, [phase, setCurrent, setIsBackButtonVisible, setTotal]); - - // const avalancheConnectorCallbacks = useMemo( - // () => ({ - // onConnectionSuccess: () => capture(`${ANALYTICS_EVENT_PREFIX}Connected`), - // onConnectionFailed: (err: Error) => - // err instanceof WalletExistsError - // ? capture(`${ANALYTICS_EVENT_PREFIX}DuplicateWallet`) - // : capture(`${ANALYTICS_EVENT_PREFIX}ConnectionFailed`), - // onConnectionRetry: () => capture(`${ANALYTICS_EVENT_PREFIX}Retry`), - // }), - // [capture], - // ); - - // const solanaConnectorCallbacks = useMemo( - // () => ({ - // onConnectionSuccess: () => - // capture(`${ANALYTICS_EVENT_PREFIX}SolanaKeysDerived`), - // onConnectionFailed: () => - // capture(`${ANALYTICS_EVENT_PREFIX}SolanaKeysFailed`), - // onConnectionRetry: () => - // capture(`${ANALYTICS_EVENT_PREFIX}SolanaKeysRetry`), - // }), - // [capture], - // ); - - // const onSave = useCallback( - // async (name: string) => { - // try { - // await importLedger({ - // name, - // addressPublicKeys: publicKeys, - // extendedPublicKeys: extPublicKeys, - // }); - // openApp(); - // window.close(); - // } catch (err) { - // toast.error(t('Unknown error has occurred. Please try again later.')); - // console.error(err); - // } - // }, - // [extPublicKeys, importLedger, publicKeys, openApp, t], - // ); - - return ( -
Fullscreen
- // - // - // { - // setPublicKeys(addressPublicKeys.map(({ key }) => key)); - // setExtPublicKeys(extendedPublicKeys ?? []); - // history.push('/import-wallet/ledger/prompt-solana'); - // }} - // onTroubleshoot={() => { - // capture(`${ANALYTICS_EVENT_PREFIX}TroubleshootingAvalanche`); - // history.push(`${BASE_PATH}/troubleshooting-avalanche`); - // }} - // /> - // - // - // { - // capture(`${ANALYTICS_EVENT_PREFIX}SolanaSupportConfirmed`); - // history.push(`${BASE_PATH}/connect-solana`); - // }} - // onSkip={() => { - // capture(`${ANALYTICS_EVENT_PREFIX}SolanaSupportDenied`); - // history.push(`${BASE_PATH}/name`); - // }} - // /> - // - // - // { - // setPublicKeys((prev) => [ - // ...prev, - // ...addressPublicKeys.map(({ key }) => key), - // ]); - // history.push(`${BASE_PATH}/name`); - // }} - // onTroubleshoot={() => { - // capture(`${ANALYTICS_EVENT_PREFIX}TroubleshootingSolana`); - // history.push(`${BASE_PATH}/troubleshooting-solana`); - // }} - // /> - // - // - // { - // capture(`${ANALYTICS_EVENT_PREFIX}TroubleshootingAvalancheClosed`); - // history.push(`${BASE_PATH}/connect-avax`); - // }} - // /> - // - // - // { - // capture(`${ANALYTICS_EVENT_PREFIX}TroubleshootingSolanaClosed`); - // history.push(`${BASE_PATH}/connect-solana`); - // }} - // /> - // - // - // - // - // - ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx index 0b5cfa8a0..213cbc380 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx @@ -4,8 +4,8 @@ import { AlertCircleIcon, Button, CheckCircleIcon, - CircularProgress, Stack, + toast, Typography, } from '@avalabs/core-k2-components'; @@ -13,8 +13,8 @@ import { useSeedlessMfaManager } from '@core/ui'; import { AuthErrorCode, MfaRequestType } from '@core/types'; import { useMFAChoice } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAChoice'; import { useMFAEvents } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAEvent'; -import { InProgress } from '../../RecoveryPhrase/components/ShowPhrase/components/InProgress'; import { FIDOChallenge } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge'; +import { useHistory } from 'react-router-dom'; enum RemoveTotpState { Loading = 'loading', @@ -26,6 +26,7 @@ enum RemoveTotpState { } export const RemoveTotp = () => { + const history = useHistory(); const { t } = useTranslation(); const { removeTotp } = useSeedlessMfaManager(); const [state, setState] = useState(RemoveTotpState.Loading); @@ -39,12 +40,15 @@ export const RemoveTotp = () => { const remove = useCallback(async () => { try { console.log('remove called: ', remove); + // throw new Error('Test error'); await removeTotp(); setState(RemoveTotpState.Success); + toast.success('Recovery method removed!', { duration: 20000 }); + history.push('update-recovery-method'); } catch { setState(RemoveTotpState.Failure); } - }, [removeTotp]); + }, [history, removeTotp]); useEffect(() => { remove(); @@ -122,26 +126,13 @@ export const RemoveTotp = () => { )} {state === RemoveTotpState.Loading && ( <> - - {/* Remove REFISGTER */} - {(mfaChallenge.challenge?.type === MfaRequestType.Fido || - mfaChallenge.challenge?.type === MfaRequestType.FidoRegister) && ( + {mfaChallenge.challenge?.type === MfaRequestType.Fido && ( )} - {/* {mfaChallenge.challenge?.type === MfaRequestType.Totp && ( - - )} */} - - {/* {renderMfaPrompt()} */} )}
diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx index 8c40aed90..682dca021 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx @@ -1,5 +1,4 @@ -import { Page } from '@/components/Page'; -import { Button, Stack } from '@avalabs/k2-alpine'; +import { Button, Paper, Stack } from '@avalabs/k2-alpine'; import { useSeedlessMfaManager } from '@core/ui'; import { useTranslation } from 'react-i18next'; import { AuthenticatorDetails } from './Authenticator/AuthenticatorDetails'; @@ -11,18 +10,17 @@ import { useHistory } from 'react-router-dom'; interface RecoveryMethodProps { method: RecoveryMethodType; - onBackClicked: () => void; } -export const RecoveryMethod = ({ - method, - onBackClicked, -}: RecoveryMethodProps) => { - console.log('RecoveryMethod: ', method); +export const RecoveryMethod = ({ method }: RecoveryMethodProps) => { const { t } = useTranslation(); - const { hasTotpConfigured } = useSeedlessMfaManager(); + const { hasTotpConfigured, hasFidoConfigured, recoveryMethods } = + useSeedlessMfaManager(); + console.log('recoveryMethods: ', recoveryMethods); const history = useHistory(); + const isRemovable = + recoveryMethods.length > 1 && hasFidoConfigured && hasTotpConfigured; const openRemoveTotpPopup = useCallback(async () => { openFullscreenTab('update-recovery-method/totp/remove'); }, []); @@ -31,55 +29,56 @@ export const RecoveryMethod = ({ }, []); return ( - // - - {method.type === 'totp' && ( - - )} - {method.type === 'fido' && } - {method.type === 'totp' && ( + <> + + + {method.type === 'totp' && ( + + )} + {method.type === 'fido' && } + + + + {method.type === 'totp' && ( + + )} - )} - - - // +
+ ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx index a7a64b835..3e3aa0a02 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx @@ -29,6 +29,7 @@ export const RecoveryMethodCard = ({ icon={getIconForMethod(method)} text={methodName || method.type} key={method.toString()} + size="small" /> ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx index d2e7c2ef3..c26b78827 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx @@ -1,30 +1,36 @@ import { CardMenu, CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; import { + Button, Divider, EncryptedIcon, + Paper, PasswordIcon, SecurityKeyIcon, } from '@avalabs/k2-alpine'; import { openFullscreenTab } from '@core/common'; import { useAnalyticsContext } from '@core/ui'; import { useTranslation } from 'react-i18next'; -import { useHistory, useRouteMatch } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; export const MethodIcons = { - passkey: , - authenticator: , - yubikey: , + passkey: , + authenticator: , + yubikey: , }; export const RecoveryMethodList = ({ hasTotpConfigured, + hasMFAConfigured, + onNext, }: { hasTotpConfigured: boolean; + hasMFAConfigured: boolean; + onNext: () => void; }) => { + console.log('hasTotpConfigured: ', hasTotpConfigured); const { t } = useTranslation(); const history = useHistory(); const { capture } = useAnalyticsContext(); - const { path } = useRouteMatch(); const recoveryMethodCards = [ { @@ -44,9 +50,12 @@ export const RecoveryMethodList = ({ 'Authenticator apps generate secure, time-based codes for wallet recovery.', ), // add url for in-extension version - to: `update-recovery-method/totp/add`, + to: !hasMFAConfigured + ? '/settings/recovery-method/authenticator' + : 'update-recovery-method/totp/add', analyticsKey: 'AddAuthenticatorClicked', method: 'authenticator', + newTab: !hasMFAConfigured ? false : true, }, { icon: MethodIcons.yubikey, @@ -61,29 +70,54 @@ export const RecoveryMethodList = ({ ]; return ( -
- }> - {recoveryMethodCards.map((card, idx) => { - // Hide Authenticator option if TOTP is already configured - if (card.method === 'authenticator' && hasTotpConfigured) { - return null; - } + <> + + }> + {recoveryMethodCards.map((card, idx) => { + console.log('card: ', card); + // Hide Authenticator option if TOTP is already configured + if (card.method === 'authenticator' && hasTotpConfigured) { + return null; + } - return ( - { - capture(card.analyticsKey); - console.log('card.to: ', card.to); - openFullscreenTab(card.to); - }} - icon={card.icon} - text={card.title} - description={card.description} - key={idx} - /> - ); - })} - -
+ return ( + { + capture(card.analyticsKey); + console.log('card.to: ', card.to); + if (card.newTab === false) { + history.push(card.to); + return; + } + openFullscreenTab(card.to); + }} + icon={card.icon} + text={card.title} + description={card.description} + key={idx} + size="small" + /> + ); + })} +
+ + + ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx index 1584e7960..f727ecee1 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx @@ -2,12 +2,12 @@ import { Page } from '@/components/Page'; import { Button, Paper, Skeleton } from '@avalabs/k2-alpine'; import { RecoveryMethod as RecoveryMethodType } from '@core/types'; import { useAnalyticsContext, useSeedlessMfaManager } from '@core/ui'; -import { FC, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { RecoveryMethodList } from './RecoveryMethodList'; -import { RecoveryMethodCard } from './RecoveryMethodCard'; import { RecoveryMethod } from './RecoveryMethod'; import { ConfiguredMethodList } from './ConfiguredMethodList'; +import { useHistory } from 'react-router-dom'; export enum RecoveryMethodScreen { ConfiguredList = 'configured-list', @@ -21,9 +21,9 @@ export enum RecoveryMethodScreen { export const RecoveryMethods: FC = () => { const { t } = useTranslation(); const { capture } = useAnalyticsContext(); + const history = useHistory(); const [selectedMethod, setSelectedMethod] = useState(null); - console.log('selectedMethod: ', selectedMethod); const { isLoadingRecoveryMethods, recoveryMethods: existingRecoveryMethods, @@ -31,33 +31,66 @@ export const RecoveryMethods: FC = () => { hasMfaConfigured, hasTotpConfigured, } = useSeedlessMfaManager(); + console.log('hasMfaConfigured: ', hasMfaConfigured); - const [screen, setScreen] = useState(RecoveryMethodScreen.ConfiguredList); + const [screen, setScreen] = useState(); + console.log('screen: ', screen); + + useEffect(() => { + if (isLoadingRecoveryMethods) { + return; + } + if (!hasMfaConfigured) { + setScreen(RecoveryMethodScreen.NewList); + } + if (hasMfaConfigured) { + setScreen(RecoveryMethodScreen.ConfiguredList); + } + }, [hasMfaConfigured, isLoadingRecoveryMethods]); return ( { + if (screen !== RecoveryMethodScreen.Method) { + history.push('/settings'); + return; + } + if (hasMfaConfigured) { + setScreen(RecoveryMethodScreen.ConfiguredList); + return; + } + setScreen(RecoveryMethodScreen.NewList); + }} > - - {isLoadingRecoveryMethods && ( - <> - - - )} + {isLoadingRecoveryMethods && ( + + + + )} - {screen === RecoveryMethodScreen.NewList && ( - - )} + {!isLoadingRecoveryMethods && screen === RecoveryMethodScreen.NewList && ( + { + capture('AddRecoveryMethodClicked'); + setScreen(RecoveryMethodScreen.NewList); + }} + /> + )} - {screen === RecoveryMethodScreen.ConfiguredList && ( + {!isLoadingRecoveryMethods && + screen === RecoveryMethodScreen.ConfiguredList && ( { /> )} - {screen === RecoveryMethodScreen.Method && selectedMethod && ( - setSelectedMethod(null)} - /> - )} - + {!isLoadingRecoveryMethods && + screen === RecoveryMethodScreen.Method && + selectedMethod && } {screen === RecoveryMethodScreen.ConfiguredList && ( - - ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx new file mode 100644 index 000000000..18e71c179 --- /dev/null +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx @@ -0,0 +1,17 @@ +import { WarningMessage } from '@/components/WarningMessage'; +import { Stack } from '@avalabs/k2-alpine'; +import { useTranslation } from 'react-i18next'; + +export const RecoveryMethodFailure = () => { + const { t } = useTranslation(); + return ( + + {t('Error occurred. Please try again.')} + + ); +}; diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx index ae9add3c7..7f3b5ed9a 100644 --- a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx @@ -45,7 +45,7 @@ export const MFA: FC = () => { /> )} - {!mfaChoice.choice && ( + {!mfaChoice.choice && !mfaChallenge && ( {t('Fetching available authentication methods...')} diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx index d8e1c1109..4eb086377 100644 --- a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx @@ -52,10 +52,11 @@ export const FIDOChallenge: FC = ({ ? FIDOApiEndpoint.Authenticate : FIDOApiEndpoint.Register, challenge.options, - challenge.type === MfaRequestType.FidoRegister ? keyType : undefined, + challenge.type === MfaRequestType.FidoRegister && keyType + ? keyType + : undefined, ) .then((answer) => { - console.log('answer: ', answer); request({ method: ExtensionRequest.SEEDLESS_SUBMIT_MFA_RESPONSE, params: [ diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/TOTPChallenge.tsx b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/TOTPChallenge.tsx index 039bcf5cc..c75b3b114 100644 --- a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/TOTPChallenge.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/TOTPChallenge.tsx @@ -78,7 +78,7 @@ export const TOTPChallenge: FC = ({ error, challenge, onError }) => { diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/components/ConfirmPage.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/components/ConfirmPage.tsx index 6020d47e4..eabdf0529 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/components/ConfirmPage.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/components/ConfirmPage.tsx @@ -1,21 +1,42 @@ import { SlideUpDialog } from '@/components/Dialog'; import { Page } from '@/components/Page'; -import { Button } from '@avalabs/k2-alpine'; +import { Button, Stack } from '@avalabs/k2-alpine'; import { useTranslation } from 'react-i18next'; +import { RecoveryMethodFailure } from './RecoveryMethodFailure'; -export const ConfirmPage = ({ onNext, onBack }) => { +export const ConfirmPage = ({ onConfirm, onCancel, title, warning }) => { const { t } = useTranslation(); return ( - - + + + + + + + ); diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx index 18e71c179..d2df65e13 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx @@ -2,7 +2,7 @@ import { WarningMessage } from '@/components/WarningMessage'; import { Stack } from '@avalabs/k2-alpine'; import { useTranslation } from 'react-i18next'; -export const RecoveryMethodFailure = () => { +export const RecoveryMethodFailure = ({ text }: { text: string }) => { const { t } = useTranslation(); return ( { paddingBlock="28px 22px" paddingInline="0px 30px" > - {t('Error occurred. Please try again.')} + + {text || t('Error occurred. Please try again.')} + ); }; From 70513498c69532c7c16ba58b935ad6fb5811cdd2 Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Fri, 12 Sep 2025 15:29:48 +0200 Subject: [PATCH 15/22] chore: translation --- apps/next/src/localization/locales/en/translation.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/next/src/localization/locales/en/translation.json b/apps/next/src/localization/locales/en/translation.json index a3697920c..84b0804fb 100644 --- a/apps/next/src/localization/locales/en/translation.json +++ b/apps/next/src/localization/locales/en/translation.json @@ -61,9 +61,11 @@ "Approves all {{token}}": "Approves all {{token}}", "Approving will give this dApp access to your active account.": "Approving will give this dApp access to your active account.", "Are you sure that you want to cancel this request?": "Are you sure that you want to cancel this request?", + "Are you sure you want to change the authenticator?": "Are you sure you want to change the authenticator?", "Are you sure you want to delete selected accounts?": "Are you sure you want to delete selected accounts?", "Are you sure you want to delete this contact?": "Are you sure you want to delete this contact?", "Are you sure you want to delete {{name}} account?": "Are you sure you want to delete {{name}} account?", + "Are you sure you want to remove this recovery method?": "Are you sure you want to remove this recovery method?", "As a Core user, you have the option to opt-in for airdrop rewards based on your activity and engagement. Core will collect anonymous interaction data to power this feature.": "As a Core user, you have the option to opt-in for airdrop rewards based on your activity and engagement. Core will collect anonymous interaction data to power this feature.", "Assets": "Assets", "Attempted to use an unknown derivation path": "Attempted to use an unknown derivation path", @@ -655,6 +657,8 @@ "You pay": "You pay", "You receive": "You receive", "You will be prompted {{remaining}} more time(s).": "You will be prompted {{remaining}} more time(s).", + "You will no longer be able to use this authenticator once you switch. You will need to re-add an authenticator": "You will no longer be able to use this authenticator once you switch. You will need to re-add an authenticator", + "You will no longer be able to use this method once you removed.": "You will no longer be able to use this method once you removed.", "You're about to terminate this session": "You're about to terminate this session", "Your account's private key is a fixed password for accessing the\n specific addresses above. Keep it secure, anyone with this private key\n can access the account associated with it.": "Your account's private key is a fixed password for accessing the\n specific addresses above. Keep it secure, anyone with this private key\n can access the account associated with it.", "Your export request has expired. Please try again.": "Your export request has expired. Please try again.", From 0232170d80c98d648a279a6a480da39a4e70adbf Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Fri, 12 Sep 2025 17:08:03 +0200 Subject: [PATCH 16/22] fix: mfa loading --- .../ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx index dec354bc7..ec649cd21 100644 --- a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx @@ -44,7 +44,7 @@ export const MFA = () => { /> )} - {(!mfaChoice.choice || !mfaChallenge) && ( + {!mfaChoice.choice && !mfaChallenge && ( {t('Fetching available authentication methods...')} From ce82f1d78dd6c42616401e2d146df20fbb6c5c76 Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Fri, 12 Sep 2025 17:13:09 +0200 Subject: [PATCH 17/22] fix: typecheck --- .../RecoveryMethods/components/RecoveryMethodFailure.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx index d2df65e13..2c6fc938b 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/components/RecoveryMethodFailure.tsx @@ -2,7 +2,7 @@ import { WarningMessage } from '@/components/WarningMessage'; import { Stack } from '@avalabs/k2-alpine'; import { useTranslation } from 'react-i18next'; -export const RecoveryMethodFailure = ({ text }: { text: string }) => { +export const RecoveryMethodFailure = ({ text }: { text?: string }) => { const { t } = useTranslation(); return ( Date: Wed, 24 Sep 2025 10:32:02 +0200 Subject: [PATCH 18/22] refactor: code reorganization --- .../pages/Onboarding/components/CardMenu.tsx | 12 ++++-- .../pages/Settings/components/Home/Home.tsx | 4 +- .../Home/components/SettingsNavItem.tsx | 12 +++--- .../Authenticator/Authenticator.tsx | 6 +-- .../Authenticator/AuthenticatorDetails.tsx | 1 - .../Authenticator/AuthenticatorVerifyCode.tsx | 40 ++++++++++-------- .../Authenticator/AuthenticatorVerifyTotp.tsx | 2 +- .../components/RecoveryMethods/FIDO/FIDO.tsx | 6 +-- .../RecoveryMethods/FIDO/FIDODetails.tsx | 1 - .../FullScreens/FullScreenContent.tsx | 7 ++-- .../FullScreens/RemoveTotp.tsx | 4 +- .../RecoveryMethods/RecoveryMethod.tsx | 20 ++++----- .../components/ConfirmPage.tsx | 41 +++++++++---------- .../components/SeedlessFlow/pages/MFA/MFA.tsx | 8 ++-- .../components => common}/FIDOChallenge.tsx | 4 +- .../pages/MFA/hooks => common}/useMFAEvent.ts | 0 16 files changed, 83 insertions(+), 85 deletions(-) rename apps/next/src/pages/Settings/components/{RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components => common}/FIDOChallenge.tsx (94%) rename apps/next/src/pages/Settings/components/{RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks => common}/useMFAEvent.ts (100%) diff --git a/apps/next/src/pages/Onboarding/components/CardMenu.tsx b/apps/next/src/pages/Onboarding/components/CardMenu.tsx index 8b80bf1f0..6a219ec0e 100644 --- a/apps/next/src/pages/Onboarding/components/CardMenu.tsx +++ b/apps/next/src/pages/Onboarding/components/CardMenu.tsx @@ -34,7 +34,7 @@ type CardMenuItemProps = { link: string; } | { - onClick: () => void; + onClick?: () => void; } ); @@ -49,14 +49,18 @@ export const CardMenuItem: FC = ({ const theme = useTheme(); const onClick = - 'onClick' in props ? props.onClick : () => history.push(props.link); + 'onClick' in props + ? props.onClick + : 'link' in props + ? () => history.push(props.link) + : undefined; return ( {icon} diff --git a/apps/next/src/pages/Settings/components/Home/Home.tsx b/apps/next/src/pages/Settings/components/Home/Home.tsx index fd9df2659..1109e61e0 100644 --- a/apps/next/src/pages/Settings/components/Home/Home.tsx +++ b/apps/next/src/pages/Settings/components/Home/Home.tsx @@ -111,8 +111,8 @@ export const SettingsHomePage = () => { {t('Set up ')} } - labelTpyographyVariant="subtitle3" - descriptionTpyographyVariant="caption2" + labelVariant="subtitle3" + descriptionVariant="caption2" sx={{ borderBottom: 'none' }} /> diff --git a/apps/next/src/pages/Settings/components/Home/components/SettingsNavItem.tsx b/apps/next/src/pages/Settings/components/Home/components/SettingsNavItem.tsx index ecbaf8d2b..685fa453f 100644 --- a/apps/next/src/pages/Settings/components/Home/components/SettingsNavItem.tsx +++ b/apps/next/src/pages/Settings/components/Home/components/SettingsNavItem.tsx @@ -15,8 +15,8 @@ type OwnProps = { href?: string; label: string; description?: string; - labelTpyographyVariant?: 'subtitle3'; - descriptionTpyographyVariant?: 'caption2'; + labelVariant?: 'subtitle3'; + descriptionVariant?: 'caption2'; }; type SettingsNavItemProps = Omit & OwnProps; @@ -26,8 +26,8 @@ export const SettingsNavItem: FC = ({ children, href, secondaryAction, - labelTpyographyVariant, // --- IGNORE --- - descriptionTpyographyVariant, // --- IGNORE --- + labelVariant, + descriptionVariant, ...props }) => { const history = useHistory(); @@ -64,10 +64,10 @@ export const SettingsNavItem: FC = ({ { ), }; - const onBack = useCallback(() => { - goBack(); - }, [goBack]); - const onCodeSubmit = useCallback(async () => { setIsSubmitted(true); if (!totpChallenge) { @@ -96,7 +92,7 @@ export const Authenticator: FC = () => { title={headline[screenState]} withBackButton contentProps={{ justifyContent: 'flex-start', alignItems: 'start' }} - onBack={onBack} + onBack={goBack} > {description[screenState]} {screenState === AuthenticatorState.Initial && ( diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx index 835839979..a8c0e42e3 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx @@ -15,7 +15,6 @@ export enum AuthenticatorState { export const AuthenticatorDetails = ({ method, methodName }) => { return ( {}} icon={getIconForMethod(method)} text={methodName} key={method.toString()} diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx index afb17d3d9..df3d56877 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorVerifyCode.tsx @@ -1,6 +1,26 @@ -import { Button, Paper, Stack, toast, Typography } from '@avalabs/k2-alpine'; +import { + Button, + Paper, + Stack, + styled, + toast, + Typography, +} from '@avalabs/k2-alpine'; import { useTranslation } from 'react-i18next'; +const CodePaper = styled(Paper)(() => ({ + borderRadius: 2, + overflow: 'hidden', + width: '100%', + flexDirection: 'row', + px: 1.5, + py: 2, + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + mt: 4, +})); + export const AuthenticatorVerifyCode = ({ totpSecret, onNext }) => { const { t } = useTranslation(); @@ -11,21 +31,7 @@ export const AuthenticatorVerifyCode = ({ totpSecret, onNext }) => { height="100%" width="100%" > - + { > {t('Copy')} - + {/** TODO: Put the description sections with ICONS after they put in the k2 alpine */} - - + + + + diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx index ec649cd21..d1d461236 100644 --- a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx @@ -1,16 +1,16 @@ import { FullscreenModal } from '@/components/FullscreenModal'; import { AuthErrorCode, MfaRequestType } from '@core/types'; -import { useState } from 'react'; +import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { InProgress } from '../../../InProgress'; -import { FIDOChallenge } from './components/FIDOChallenge'; +import { FIDOChallenge } from '../../../../../../../common/FIDOChallenge'; import { MfaChoicePrompt } from './components/MfaChoicePrompt'; import { TOTPChallenge } from './components/TOTPChallenge'; import { useMFAChoice } from './hooks/useMFAChoice'; -import { useMFAEvents } from './hooks/useMFAEvent'; +import { useMFAEvents } from '../../../../../../../common/useMFAEvent'; import { useSelectMFAMethod } from './hooks/useSelectMFAMethod'; -export const MFA = () => { +export const MFA: FC = () => { const [error, setError] = useState(); const mfaChoice = useMFAChoice(); const mfaChallenge = useMFAEvents(setError); diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx b/apps/next/src/pages/Settings/components/common/FIDOChallenge.tsx similarity index 94% rename from apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx rename to apps/next/src/pages/Settings/components/common/FIDOChallenge.tsx index ebc9b5e08..4fc0f91f6 100644 --- a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/components/FIDOChallenge.tsx +++ b/apps/next/src/pages/Settings/components/common/FIDOChallenge.tsx @@ -17,8 +17,8 @@ import { FC, useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { MdErrorOutline } from 'react-icons/md'; import { SubmitMfaResponseHandler } from '~/services/seedless/handlers/submitMfaResponse'; -import { InProgress } from '../../../../InProgress'; -import { ChallengeComponentProps } from '../../../types'; +import { InProgress } from '../RecoveryPhrase/components/ShowPhrase/components/InProgress'; +import { ChallengeComponentProps } from '../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/types'; import { useParams } from 'react-router-dom'; import { RecoveryMethodsFullScreenParams } from '@/pages/Settings/components/RecoveryMethods/FullScreens/RecoveryMethodsFullScreen'; diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAEvent.ts b/apps/next/src/pages/Settings/components/common/useMFAEvent.ts similarity index 100% rename from apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/hooks/useMFAEvent.ts rename to apps/next/src/pages/Settings/components/common/useMFAEvent.ts From e21b54328ff0db87abc27495d852d4e7500b080e Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Thu, 25 Sep 2025 10:16:16 +0200 Subject: [PATCH 19/22] feat: handle feature flag changes --- .../subflows/SeedlessMfaLoginFlow.tsx | 2 - .../pages/Settings/components/Home/Home.tsx | 4 +- .../RecoveryMethods/RecoveryMethodList.tsx | 77 ++++++++++++------- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/apps/next/src/pages/Onboarding/flows/SeedlessFlow/subflows/SeedlessMfaLoginFlow.tsx b/apps/next/src/pages/Onboarding/flows/SeedlessFlow/subflows/SeedlessMfaLoginFlow.tsx index 37d3268de..5ba0b45a5 100644 --- a/apps/next/src/pages/Onboarding/flows/SeedlessFlow/subflows/SeedlessMfaLoginFlow.tsx +++ b/apps/next/src/pages/Onboarding/flows/SeedlessFlow/subflows/SeedlessMfaLoginFlow.tsx @@ -26,8 +26,6 @@ export const SeedlessMfaLoginFlow: FC = ({ const history = useHistory(); const { registerBackButtonHandler, setTotal } = useModalPageControl(); const { oidcToken, setSeedlessSignerToken } = useOnboardingContext(); - // TODO: remove - console.log('oidcToken: ', oidcToken); const [isLoading, setIsLoading] = useState(false); useEffect(() => { diff --git a/apps/next/src/pages/Settings/components/Home/Home.tsx b/apps/next/src/pages/Settings/components/Home/Home.tsx index 1109e61e0..fc46e70a5 100644 --- a/apps/next/src/pages/Settings/components/Home/Home.tsx +++ b/apps/next/src/pages/Settings/components/Home/Home.tsx @@ -60,7 +60,7 @@ export const SettingsHomePage = () => { const { showTrendingTokens, setShowTrendingTokens } = useSettingsContext(); const { isMfaSetupPromptVisible } = useSeedlessMfaManager(); const { featureFlags } = useFeatureFlagContext(); - const areMfaSettingsAvailable = + const isMfaSettingsAvailable = featureFlags[FeatureGates.SEEEDLESS_MFA_SETTINGS]; return ( @@ -232,7 +232,7 @@ export const SettingsHomePage = () => { {walletDetails?.type === SecretType.Seedless && - areMfaSettingsAvailable && ( + isMfaSettingsAvailable && ( - }> - {recoveryMethodCards.map((card, idx) => { - // Hide Authenticator option if TOTP is already configured - if (card.method === 'authenticator' && hasTotpConfigured) { - return null; - } + {!noMFAMethodsAvailable && ( + }> + {recoveryMethodCards.map((card, idx) => { + if ( + (card.method === 'authenticator' && hasTotpConfigured) || + !card.isOn + ) { + return null; + } - return ( - { - capture(card.analyticsKey); - if (card.newTab === false) { - history.push(card.to); - return; - } - openFullscreenTab(card.to); - }} - icon={card.icon} - text={card.title} - description={card.description} - key={idx} - size="small" - /> - ); - })} - + return ( + { + capture(card.analyticsKey); + if (card.newTab === false) { + history.push(card.to); + return; + } + openFullscreenTab(card.to); + }} + icon={card.icon} + text={card.title} + description={card.description} + key={idx} + size="small" + /> + ); + })} + + )} + {noMFAMethodsAvailable && ( + + {t( + 'You cannot add a new recovery method for your wallet! Try again later!', + )} + + )} From 0cec53164fb322ed0cbb615d0f8bba7571b8473a Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Thu, 25 Sep 2025 10:21:35 +0200 Subject: [PATCH 20/22] chore: localization --- apps/next/src/localization/locales/en/translation.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/next/src/localization/locales/en/translation.json b/apps/next/src/localization/locales/en/translation.json index 8e89e44ad..42511c7ba 100644 --- a/apps/next/src/localization/locales/en/translation.json +++ b/apps/next/src/localization/locales/en/translation.json @@ -724,6 +724,7 @@ "You are about to exit the recovery phrase export process. Are you sure you want to exit?": "You are about to exit the recovery phrase export process. Are you sure you want to exit?", "You can now close this window and continue using Core.": "You can now close this window and continue using Core.", "You can now start buying, swapping, sending, receiving crypto and collectibles with no added fees": "You can now start buying, swapping, sending, receiving crypto and collectibles with no added fees", + "You cannot add a new recovery method for your wallet! Try again later!": "You cannot add a new recovery method for your wallet! Try again later!", "You do not have enough funds to cover the network fees.": "You do not have enough funds to cover the network fees.", "You may need to enable popups to continue, you can find this setting near the address bar.": "You may need to enable popups to continue, you can find this setting near the address bar.", "You must allow access to scan the QR code.": "You must allow access to scan the QR code.", From e145939ddef1cf3287c5d6164ab0ca18335e80cc Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Mon, 20 Oct 2025 20:07:53 +0200 Subject: [PATCH 21/22] refactor: file move, type move --- .../Authenticator/Authenticator.tsx | 38 ++++++++----- .../Authenticator/AuthenticatorDetails.tsx | 11 ---- .../components/RecoveryMethods/FIDO/FIDO.tsx | 2 +- .../RecoveryMethods/FullScreens/AddTotp.tsx | 2 +- .../FullScreens/FullScreenContent.tsx | 55 +++++++++++-------- .../FullScreens/RemoveTotp.tsx | 6 +- .../components/SeedlessFlow/pages/Loading.tsx | 2 +- .../components/SeedlessFlow/pages/MFA/MFA.tsx | 2 +- .../components/common/FIDOChallenge.tsx | 2 +- .../components => common}/InProgress.tsx | 0 10 files changed, 62 insertions(+), 58 deletions(-) rename apps/next/src/pages/Settings/components/{RecoveryPhrase/components/ShowPhrase/components => common}/InProgress.tsx (100%) diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/Authenticator.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/Authenticator.tsx index 0d8333c86..a0b88d41b 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/Authenticator.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/Authenticator.tsx @@ -7,12 +7,22 @@ import { useState } from 'react'; import { useGoBack, useSeedlessMfaManager } from '@core/ui'; import { AuthenticatorVerifyScreen } from './AuthenticatorVerifyScreen'; import { AuthenticatorVerifyCode } from './AuthenticatorVerifyCode'; -import { AuthenticatorState } from './AuthenticatorDetails'; -import { InProgress } from '../../RecoveryPhrase/components/ShowPhrase/components/InProgress'; +import { InProgress } from '../../common/InProgress'; import { RecoveryMethodFailure } from '../components/RecoveryMethodFailure'; import { AuthenticatorVerifyTotp } from './AuthenticatorVerifyTotp'; import { useHistory } from 'react-router-dom'; +export enum AuthenticatorState { + Initial = 'initial', + Initiated = 'initiated', + ConfirmChange = 'confirm-change', + ConfirmRemoval = 'confirm-removal', + Pending = 'pending', + Completing = 'completing', + VerifyCode = 'verify-code', + Failure = 'failure', +} + export const Authenticator: FC = () => { const { t } = useTranslation(); const history = useHistory(); @@ -29,21 +39,19 @@ export const Authenticator: FC = () => { const goBack = useGoBack(); - const initChange = useCallback(async () => { - try { - const challenge = await initAuthenticatorChange(); - - setTotpChallenge(challenge); - setScreenState(AuthenticatorState.Initiated); - } catch { - setTotpChallenge(undefined); - setScreenState(AuthenticatorState.Failure); - } - }, [initAuthenticatorChange]); - useEffect(() => { + const initChange = async () => { + try { + const challenge = await initAuthenticatorChange(); + setTotpChallenge(challenge); + setScreenState(AuthenticatorState.Initiated); + } catch { + setTotpChallenge(undefined); + setScreenState(AuthenticatorState.Failure); + } + }; initChange(); - }, [initChange]); + }, [initAuthenticatorChange]); const totpSecret = useMemo(() => { if (!totpChallenge) { diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx index a8c0e42e3..75ad1c2ba 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/Authenticator/AuthenticatorDetails.tsx @@ -1,17 +1,6 @@ import { getIconForMethod } from '../RecoveryMethodCard'; import { CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; -export enum AuthenticatorState { - Initial = 'initial', - Initiated = 'initiated', - ConfirmChange = 'confirm-change', - ConfirmRemoval = 'confirm-removal', - Pending = 'pending', - Completing = 'completing', - VerifyCode = 'verify-code', - Failure = 'failure', -} - export const AuthenticatorDetails = ({ method, methodName }) => { return ( { const history = useHistory(); diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx index 496e18461..a6e077f1a 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/AddTotp.tsx @@ -4,7 +4,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { MFA } from '../../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA'; import { Button, Stack, toast } from '@avalabs/k2-alpine'; import { AuthenticatorVerifyTotp } from '../Authenticator/AuthenticatorVerifyTotp'; -import { AuthenticatorState } from '../Authenticator/AuthenticatorDetails'; import { useTranslation } from 'react-i18next'; import { FullscreenModalActions, @@ -14,6 +13,7 @@ import { import { SeedlessTotpQRCode } from '@/pages/Onboarding/flows/SeedlessFlow/screens'; import { RecoveryMethodFailure } from '../components/RecoveryMethodFailure'; import { useHistory } from 'react-router-dom'; +import { AuthenticatorState } from '../Authenticator'; export const AddTotp = () => { const [error, setError] = useState(); diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx index cd28277e9..557f57743 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/FullScreenContent.tsx @@ -5,6 +5,7 @@ import { AddTotp } from './AddTotp'; import { AddFIDO } from './AddFIDO'; import { DefaultContent } from './DefaultContent'; import { KeyType } from '@core/types'; +import { useMemo } from 'react'; export type RecoveryMethodPages = | 'defaultContent' @@ -16,37 +17,43 @@ export type FullScreenContentProps = { [page in RecoveryMethodPages]: React.ReactNode; }; +const getFullScreenContent = ( + mfaType?: 'totp' | 'fido', + action?: 'remove' | 'add', +): RecoveryMethodPages => { + if (mfaType === 'totp' && action === 'remove') { + return 'removeTOTP'; + } + if (mfaType === 'totp' && action === 'add') { + return 'addTOTP'; + } + if (mfaType === 'fido' && action === 'add') { + return 'addFIDO'; + } + return 'defaultContent'; +}; + export const FullScreenContent = ({ mfaType, action, keyType, }: RecoveryMethodsFullScreenParams) => { - const getPage = () => { - if (mfaType === 'totp' && action === 'remove') { - return 'removeTOTP'; - } - if (mfaType === 'totp' && action === 'add') { - return 'addTOTP'; - } - if (mfaType === 'fido' && action === 'add') { - return 'addFIDO'; - } - return 'defaultContent'; - }; - - const page = getPage(); + const page = getFullScreenContent(mfaType, action); const headline = {}; - const content: FullScreenContentProps = { - removeTOTP: , - addTOTP: , - addFIDO: keyType ? ( - - ) : ( - - ), - defaultContent: , - }; + const content: FullScreenContentProps = useMemo( + () => ({ + removeTOTP: , + addTOTP: , + addFIDO: keyType ? ( + + ) : ( + + ), + defaultContent: , + }), + [keyType], + ); return ( <> diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx index 5f0e1dd3e..04ececc92 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FullScreens/RemoveTotp.tsx @@ -11,10 +11,10 @@ import { import { useSeedlessMfaManager } from '@core/ui'; import { AuthErrorCode, MfaRequestType } from '@core/types'; -import { useMFAEvents } from '../../common/useMFAEvent'; -import { FIDOChallenge } from '../../common/FIDOChallenge'; +import { useMFAEvents } from '@/pages/Settings/components/common/useMFAEvent'; +import { FIDOChallenge } from '@/pages/Settings/components/common/FIDOChallenge'; import { useHistory } from 'react-router-dom'; -import { InProgress } from '../../RecoveryPhrase/components/ShowPhrase/components/InProgress'; +import { InProgress } from '@/pages/Settings/components/common/InProgress'; enum RemoveTotpState { Loading = 'loading', diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/Loading.tsx b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/Loading.tsx index bf4b19720..d64faf2d9 100644 --- a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/Loading.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/Loading.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { InProgress } from '../../InProgress'; +import { InProgress } from '../../../../../../common/InProgress'; import { StageProps } from '../types'; import { OmniViewPage } from './OmniViewPage'; diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx index d1d461236..00bf4803c 100644 --- a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/pages/MFA/MFA.tsx @@ -2,7 +2,7 @@ import { FullscreenModal } from '@/components/FullscreenModal'; import { AuthErrorCode, MfaRequestType } from '@core/types'; import { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { InProgress } from '../../../InProgress'; +import { InProgress } from '@/pages/Settings/components/common/InProgress'; import { FIDOChallenge } from '../../../../../../../common/FIDOChallenge'; import { MfaChoicePrompt } from './components/MfaChoicePrompt'; import { TOTPChallenge } from './components/TOTPChallenge'; diff --git a/apps/next/src/pages/Settings/components/common/FIDOChallenge.tsx b/apps/next/src/pages/Settings/components/common/FIDOChallenge.tsx index 4fc0f91f6..1dff875bf 100644 --- a/apps/next/src/pages/Settings/components/common/FIDOChallenge.tsx +++ b/apps/next/src/pages/Settings/components/common/FIDOChallenge.tsx @@ -17,7 +17,7 @@ import { FC, useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { MdErrorOutline } from 'react-icons/md'; import { SubmitMfaResponseHandler } from '~/services/seedless/handlers/submitMfaResponse'; -import { InProgress } from '../RecoveryPhrase/components/ShowPhrase/components/InProgress'; +import { InProgress } from './InProgress'; import { ChallengeComponentProps } from '../RecoveryPhrase/components/ShowPhrase/components/SeedlessFlow/types'; import { useParams } from 'react-router-dom'; import { RecoveryMethodsFullScreenParams } from '@/pages/Settings/components/RecoveryMethods/FullScreens/RecoveryMethodsFullScreen'; diff --git a/apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/InProgress.tsx b/apps/next/src/pages/Settings/components/common/InProgress.tsx similarity index 100% rename from apps/next/src/pages/Settings/components/RecoveryPhrase/components/ShowPhrase/components/InProgress.tsx rename to apps/next/src/pages/Settings/components/common/InProgress.tsx From 116e782cf3cc7c4da05c8bf389718c44b447b681 Mon Sep 17 00:00:00 2001 From: Viktor Vasas Date: Mon, 3 Nov 2025 17:32:47 +0100 Subject: [PATCH 22/22] fix: card style, back button --- .../pages/Settings/components/Home/Home.tsx | 3 ++ .../Authenticator/AuthenticatorDetails.tsx | 8 ++-- .../RecoveryMethods/ConfiguredMethodList.tsx | 3 +- .../RecoveryMethods/FIDO/FIDODetails.tsx | 7 +-- .../RecoveryMethods/RecoveryMethod.tsx | 3 +- .../RecoveryMethods/RecoveryMethodCard.tsx | 10 ++-- .../RecoveryMethods/RecoveryMethodList.tsx | 3 +- .../RecoveryMethods/RecoveryMethods.tsx | 2 +- .../RecoveryMethods/components/MethodCard.tsx | 48 +++++++++++++++++++ 9 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 apps/next/src/pages/Settings/components/RecoveryMethods/components/MethodCard.tsx diff --git a/apps/next/src/pages/Settings/components/Home/Home.tsx b/apps/next/src/pages/Settings/components/Home/Home.tsx index 4646e7789..69b534847 100644 --- a/apps/next/src/pages/Settings/components/Home/Home.tsx +++ b/apps/next/src/pages/Settings/components/Home/Home.tsx @@ -72,6 +72,9 @@ export const SettingsHomePage = () => { 'Manage and customize your Core experience to your liking.', )} withBackButton + onBack={() => { + push('/'); + }} > { return ( - ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx index 3cf4cb874..b0da030b8 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/ConfiguredMethodList.tsx @@ -14,11 +14,12 @@ export const ConfiguredMethodList = ({ return ( {existingRecoveryMethods.map((method) => { diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx index 4b3d7cf91..44b0f2248 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/FIDO/FIDODetails.tsx @@ -1,5 +1,5 @@ -import { CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; import { getIconForMethod } from '../RecoveryMethodCard'; +import { MethodCard } from '../components/MethodCard'; export enum FIDOState { Initial = 'initial', @@ -10,10 +10,11 @@ export enum FIDOState { export const FIDODetails = ({ method }) => { return ( - ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx index 7ee0fb842..13659000c 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethod.tsx @@ -60,11 +60,12 @@ export const RecoveryMethod = ({ method }: RecoveryMethodProps) => { /> )} diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx index 3e3aa0a02..3faf2317d 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodCard.tsx @@ -1,6 +1,6 @@ -import { CardMenuItem } from '@/pages/Onboarding/components/CardMenu'; import { MethodIcons } from './RecoveryMethodList'; import { RecoveryMethod } from '@core/types'; +import { MethodCard } from './components/MethodCard'; interface RecoveryMethodCardProps { method: RecoveryMethod; @@ -24,12 +24,10 @@ export const RecoveryMethodCard = ({ methodName, }: RecoveryMethodCardProps) => { return ( - ); }; diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx index ed3cdf126..85b213ce8 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethodList.tsx @@ -84,11 +84,12 @@ export const RecoveryMethodList = ({ return ( <> {!noMFAMethodsAvailable && ( diff --git a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx index 7f4007f44..e5119e69a 100644 --- a/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx +++ b/apps/next/src/pages/Settings/components/RecoveryMethods/RecoveryMethods.tsx @@ -61,7 +61,7 @@ export const RecoveryMethods: FC = () => { > {isLoadingRecoveryMethods && ( void; + showChevron?: boolean; +} + +export const MethodCard: FC = ({ + icon, + title, + onClick, + showChevron = true, +}) => { + return ( + + + {icon} + {title} + + {showChevron && ( + + )} + + ); +}; + +const StyledMethodCard = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: theme.spacing(1.5, 2), + color: theme.palette.text.primary, + cursor: 'pointer', + + '& .MethodCard-chevron': { + color: theme.palette.text.secondary, + }, +}));