Skip to content

Commit f297a87

Browse files
committed
fix: Fix passkey mfa sign in flow
1 parent a0c706b commit f297a87

File tree

9 files changed

+188
-83
lines changed

9 files changed

+188
-83
lines changed

lib/ts/recipe/webauthn/components/features/mfa/index.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const useFeatureReducer = (): [WebAuthnMFAState, React.Dispatch<WebAuthnM
6464
email: action.email,
6565
showBackButton: action.showBackButton,
6666
canRegisterPasskey: action.canRegisterPasskey,
67+
hasRegisteredPassKey: action.hasRegisteredPassKey,
6768
};
6869
default:
6970
return oldState;
@@ -73,6 +74,7 @@ export const useFeatureReducer = (): [WebAuthnMFAState, React.Dispatch<WebAuthnM
7374
error: undefined,
7475
deviceSupported: false,
7576
canRegisterPasskey: false,
77+
hasRegisteredPassKey: false,
7678
loaded: false,
7779
showBackButton: true,
7880
email: undefined,
@@ -242,7 +244,8 @@ export const MFAFeature: React.FC<
242244
<ComponentOverrideContext.Provider value={recipeComponentOverrides}>
243245
<FeatureWrapper
244246
useShadowDom={SuperTokens.getInstanceOrThrow().useShadowDom}
245-
defaultStore={defaultTranslationsWebauthn}>
247+
defaultStore={defaultTranslationsWebauthn}
248+
>
246249
<MFAFeatureInner {...props} />
247250
</FeatureWrapper>
248251
</ComponentOverrideContext.Provider>
@@ -348,12 +351,6 @@ function useOnLoad(
348351
}
349352
}
350353

351-
const alreadySetup = mfaInfo.factors.alreadySetup.includes(FactorIds.WEBAUTHN);
352-
if (alreadySetup) {
353-
dispatch({ type: "setError", accessDenied: true, error: "SOMETHING_WENT_WRONG_ERROR_RELOAD" });
354-
return;
355-
}
356-
357354
// If the next array only has a single option, it means the we were redirected here
358355
// automatically during the sign in process. In that case, anywhere the back button
359356
// could go would redirect back here, making it useless.
@@ -365,21 +362,23 @@ function useOnLoad(
365362
const mfaInfoEmails = mfaInfo.emails[FactorIds.WEBAUTHN];
366363
const email = mfaInfoEmails ? mfaInfoEmails[0] : undefined;
367364

368-
const canRegisterPasskey = !mfaInfo.factors.alreadySetup.includes(FactorIds.WEBAUTHN);
365+
const canRegisterPasskey = mfaInfo.factors.allowedToSetup.includes(FactorIds.WEBAUTHN);
366+
const hasRegisteredPassKey = mfaInfo.factors.alreadySetup.includes(FactorIds.WEBAUTHN);
369367
const browserSupportsWebauthnResponse = await props.recipe.webJSRecipe.doesBrowserSupportWebAuthn({
370368
userContext: userContext,
371369
});
372-
const browserSupportsWebauthn =
370+
const deviceSupported =
373371
browserSupportsWebauthnResponse.status === "OK" &&
374372
browserSupportsWebauthnResponse?.browserSupportsWebauthn;
375373

376374
dispatch({
377375
type: "load",
378376
canRegisterPasskey,
377+
hasRegisteredPassKey,
379378
error,
380379
showBackButton,
381380
email,
382-
deviceSupported: browserSupportsWebauthn,
381+
deviceSupported,
383382
});
384383
},
385384
[dispatch, recipeImplementation, props.recipe, userContext]

lib/ts/recipe/webauthn/components/themes/mfa/index.tsx

Lines changed: 101 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -37,33 +37,64 @@ function MFAThemeWrapper(props: WebAuthnMFAProps): JSX.Element {
3737
export default MFAThemeWrapper;
3838

3939
export function MFATheme(props: WebAuthnMFAProps): JSX.Element {
40-
const { onBackButtonClicked, onSignIn } = props;
41-
const [activeScreen, setActiveScreen] = React.useState<MFAScreens>(MFAScreens.SignIn);
42-
const [signUpEmail, setSignUpEmail] = React.useState<string>("");
4340
const t = useTranslation();
4441

45-
const onRegisterPasskeyClick = React.useCallback(() => {
46-
if (!props.featureState.canRegisterPasskey) {
47-
return;
48-
}
49-
if (props.featureState.email) {
50-
setActiveScreen(MFAScreens.SignUpConfirmation);
51-
} else {
52-
setActiveScreen(MFAScreens.SignUp);
42+
if (!props.featureState.loaded) {
43+
return <WebauthnMFALoadingScreen />;
44+
}
45+
46+
if (props.featureState.accessDenied) {
47+
return (
48+
<AccessDeniedScreen
49+
useShadowDom={false /* We set this to false, because we are already inside a shadowDom (if required) */}
50+
error={t(props.featureState.error!)}
51+
/>
52+
);
53+
}
54+
55+
return (
56+
<div data-supertokens="container webauthn-mfa">
57+
<div data-supertokens="row">
58+
<MFAThemeRouter {...props} />
59+
</div>
60+
<SuperTokensBranding />
61+
</div>
62+
);
63+
}
64+
65+
function MFAThemeRouter(props: WebAuthnMFAProps): JSX.Element {
66+
const { onBackButtonClicked, onSignIn } = props;
67+
const [activeScreen, setActiveScreen] = React.useState<MFAScreens>(() => {
68+
if (!props.featureState.hasRegisteredPassKey) {
69+
return props.featureState.email ? MFAScreens.SignUpConfirmation : MFAScreens.SignUp;
5370
}
54-
}, [props.featureState.email, props.featureState.canRegisterPasskey]);
71+
return MFAScreens.SignIn;
72+
});
73+
const [email, setEmail] = React.useState<string>("");
74+
const signUpEmail = props.featureState.email || email;
5575

5676
const onSignUpContinue = React.useCallback(
5777
(email: string) => {
5878
if (!props.featureState.canRegisterPasskey) {
5979
return;
6080
}
6181
setActiveScreen(MFAScreens.SignUpConfirmation);
62-
setSignUpEmail(email);
82+
setEmail(email);
6383
},
6484
[props.featureState.canRegisterPasskey]
6585
);
6686

87+
const onRegisterPasskeyClick = React.useCallback(() => {
88+
if (!props.featureState.canRegisterPasskey) {
89+
return;
90+
}
91+
if (props.featureState.email) {
92+
setActiveScreen(MFAScreens.SignUpConfirmation);
93+
} else {
94+
setActiveScreen(MFAScreens.SignUp);
95+
}
96+
}, [props.featureState.email, props.featureState.canRegisterPasskey]);
97+
6798
const clearError = React.useCallback(() => {
6899
props.dispatch({ type: "setError", error: undefined });
69100
}, [props]);
@@ -75,71 +106,79 @@ export function MFATheme(props: WebAuthnMFAProps): JSX.Element {
75106
[props]
76107
);
77108

78-
const onClickSignUpBackButton = React.useCallback(() => {
79-
if (!props.featureState.canRegisterPasskey) {
109+
const onClickSignUpConfirmationBackButton = React.useCallback(() => {
110+
if (!props.featureState.email) {
111+
setActiveScreen(MFAScreens.SignUp);
112+
return;
113+
}
114+
if (!props.featureState.hasRegisteredPassKey && !props.featureState.showBackButton) {
115+
return;
116+
}
117+
if (!props.featureState.hasRegisteredPassKey) {
118+
onBackButtonClicked();
80119
return;
81120
}
82121
setActiveScreen(MFAScreens.SignIn);
83-
}, [props.featureState.canRegisterPasskey]);
122+
}, [
123+
props.featureState.email,
124+
props.featureState.hasRegisteredPassKey,
125+
props.featureState.showBackButton,
126+
onBackButtonClicked,
127+
]);
84128

85-
const onClickSignUpConfirmationBackButton = React.useCallback(() => {
86-
if (props.featureState.email) {
87-
setActiveScreen(MFAScreens.SignIn);
88-
} else {
89-
setActiveScreen(MFAScreens.SignUp);
90-
}
91-
}, [props.featureState.email]);
129+
const onSignUp = React.useCallback(async () => {
130+
await props.onSignUp(signUpEmail);
131+
}, [props.onSignUp, signUpEmail]);
92132

93133
const onFetchError = React.useCallback(() => {
94134
onError("SOMETHING_WENT_WRONG_ERROR");
95135
}, [onError]);
96136

97-
if (!props.featureState.loaded) {
98-
return <WebauthnMFALoadingScreen />;
137+
const onClickSignUpBackButton = React.useCallback(() => {
138+
if (!props.featureState.hasRegisteredPassKey && !props.featureState.showBackButton) {
139+
return;
140+
}
141+
if (!props.featureState.hasRegisteredPassKey) {
142+
onBackButtonClicked();
143+
return;
144+
}
145+
setActiveScreen(MFAScreens.SignIn);
146+
}, [props.featureState.hasRegisteredPassKey, props.featureState.showBackButton, onBackButtonClicked]);
147+
148+
if (activeScreen === MFAScreens.SignUp) {
149+
return (
150+
<WebauthnMFASignUp
151+
clearError={clearError}
152+
onError={onError}
153+
onFetchError={onFetchError}
154+
error={props.featureState.error}
155+
onContinueClick={onSignUpContinue}
156+
email={email}
157+
onRecoverAccountClick={props.onRecoverAccountClick}
158+
onBackButtonClicked={onClickSignUpBackButton}
159+
/>
160+
);
99161
}
100162

101-
if (props.featureState.accessDenied) {
163+
if (activeScreen === MFAScreens.SignUpConfirmation) {
102164
return (
103-
<AccessDeniedScreen
104-
useShadowDom={false /* We set this to false, because we are already inside a shadowDom (if required) */}
105-
error={t(props.featureState.error!)}
165+
<WebauthnMFASignUpConfirmation
166+
onSignUp={onSignUp}
167+
onBackButtonClicked={onClickSignUpConfirmationBackButton}
168+
email={signUpEmail}
169+
error={props.featureState.error}
106170
/>
107171
);
108172
}
109173

110174
return (
111-
<div data-supertokens="container webauthn-mfa">
112-
<div data-supertokens="row">
113-
{activeScreen === MFAScreens.SignIn ? (
114-
<WebauthnMFASignIn
115-
onBackButtonClicked={props.featureState.showBackButton ? onBackButtonClicked : undefined}
116-
canRegisterPasskey={props.featureState.canRegisterPasskey}
117-
onSignIn={onSignIn}
118-
error={props.featureState.error}
119-
onRegisterPasskeyClick={onRegisterPasskeyClick}
120-
deviceSupported={props.featureState.deviceSupported}
121-
/>
122-
) : activeScreen === MFAScreens.SignUp ? (
123-
<WebauthnMFASignUp
124-
clearError={clearError}
125-
onError={onError}
126-
onFetchError={onFetchError}
127-
error={props.featureState.error}
128-
onContinueClick={onSignUpContinue}
129-
email={signUpEmail}
130-
onRecoverAccountClick={props.onRecoverAccountClick}
131-
onBackButtonClicked={onClickSignUpBackButton}
132-
/>
133-
) : (
134-
<WebauthnMFASignUpConfirmation
135-
onSignUp={props.onSignUp}
136-
onBackButtonClicked={onClickSignUpConfirmationBackButton}
137-
email={props.featureState.email || signUpEmail}
138-
error={props.featureState.error}
139-
/>
140-
)}
141-
</div>
142-
<SuperTokensBranding />
143-
</div>
175+
<WebauthnMFASignIn
176+
onBackButtonClicked={props.featureState.showBackButton ? onBackButtonClicked : undefined}
177+
canRegisterPasskey={props.featureState.canRegisterPasskey}
178+
onSignIn={onSignIn}
179+
error={props.featureState.error}
180+
deviceSupported={props.featureState.deviceSupported}
181+
onRegisterPasskeyClick={onRegisterPasskeyClick}
182+
/>
144183
);
145184
}

lib/ts/recipe/webauthn/components/themes/mfa/signIn.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import { PasskeyNotSupportedError } from "../error/passkeyNotSupportedError";
1212
export type MFASignInProps = {
1313
onBackButtonClicked?: () => void;
1414
onSignIn: () => Promise<void>;
15-
onRegisterPasskeyClick: () => void;
16-
canRegisterPasskey: boolean;
1715
error: string | undefined;
1816
deviceSupported: boolean;
17+
canRegisterPasskey: boolean;
18+
onRegisterPasskeyClick: () => void;
1919
};
2020

2121
export const WebauthnMFASignIn = withOverride(
@@ -64,7 +64,7 @@ export const WebauthnMFASignIn = withOverride(
6464
</div>
6565
<div data-supertokens="headerSubtitle secondaryText">
6666
<span data-supertokens="link" onClick={props.onRegisterPasskeyClick}>
67-
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_LINK")}
67+
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_TITLE")}
6868
</span>
6969
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_SUBTITLE")}
7070
</div>

lib/ts/recipe/webauthn/components/themes/mfa/signUp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const WebauthnMFASignUp = withOverride(
3737
<Fragment>
3838
<div data-supertokens="headerTitle withBackButton webauthn-mfa">
3939
<BackButton onClick={props.onBackButtonClicked} />
40-
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_LINK")}
40+
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_TITLE")}
4141
<span data-supertokens="backButtonPlaceholder backButtonCommon">
4242
{/* empty span for spacing the back button */}
4343
</span>

lib/ts/recipe/webauthn/components/themes/mfa/signUpConfirmation.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import GeneralError from "../../../../emailpassword/components/library/generalEr
99
import { PasskeyFeatureBlockList } from "../signUp/featureBlocks";
1010

1111
export type MFASignUpConfirmationProps = {
12-
onSignUp: (email: string) => Promise<void>;
12+
onSignUp: () => Promise<void>;
1313
onBackButtonClicked: () => void;
1414
email: string;
1515
error?: string;
@@ -23,15 +23,15 @@ export const WebauthnMFASignUpConfirmation = withOverride(
2323

2424
const onClick = React.useCallback(async () => {
2525
setIsLoading(true);
26-
await props.onSignUp(props.email);
26+
await props.onSignUp();
2727
setIsLoading(false);
28-
}, [props]);
28+
}, [props.onSignUp]);
2929

3030
return (
3131
<Fragment>
3232
<div data-supertokens="headerTitle withBackButton webauthn-mfa">
3333
<BackButton onClick={props.onBackButtonClicked} />
34-
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_LINK")}
34+
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_TITLE")}
3535
<span data-supertokens="backButtonPlaceholder backButtonCommon">
3636
{/* empty span for spacing the back button */}
3737
</span>

lib/ts/recipe/webauthn/components/themes/signUp/featureBlocks.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export const PasskeyFeatureBlock = withOverride(
5858
export const PasskeyFeatureBlockList = () => {
5959
return (
6060
<div data-supertokens="passkeyFeatureBlocksContainer">
61-
{blockDetails.map((blockDetail) => (
62-
<PasskeyFeatureBlock {...blockDetail} />
61+
{blockDetails.map((blockDetail, index) => (
62+
<PasskeyFeatureBlock key={`${blockDetail.title}-${index}`} {...blockDetail} />
6363
))}
6464
</div>
6565
);

lib/ts/recipe/webauthn/components/themes/translations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,6 @@ export const defaultTranslationsWebauthn = {
5858
"To finish signing in, click the button and follow the browser instructions.",
5959
WEBAUTHN_MFA_DIVIDER: "or",
6060
WEBAUTHN_MFA_REGISTER_PASSKEY_SUBTITLE: "Set up a new authentication method to use for future logins.",
61-
WEBAUTHN_MFA_REGISTER_PASSKEY_LINK: "Register a passkey",
61+
WEBAUTHN_MFA_REGISTER_PASSKEY_TITLE: "Register a passkey",
6262
},
6363
};

lib/ts/recipe/webauthn/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ export type WebAuthnMFAAction =
275275
email: string | undefined;
276276
showBackButton: boolean;
277277
canRegisterPasskey: boolean;
278+
hasRegisteredPassKey: boolean;
278279
};
279280

280281
type WebAuthnMFAInitialState = {
@@ -284,6 +285,7 @@ type WebAuthnMFAInitialState = {
284285
deviceSupported: boolean;
285286
showBackButton: boolean;
286287
canRegisterPasskey: false;
288+
hasRegisteredPassKey: false;
287289
email: undefined;
288290
};
289291

@@ -294,6 +296,7 @@ type WebAuthnMFALoadedState = {
294296
deviceSupported: boolean;
295297
showBackButton: boolean;
296298
canRegisterPasskey: boolean;
299+
hasRegisteredPassKey: boolean;
297300
email: string | undefined;
298301
};
299302

0 commit comments

Comments
 (0)