Skip to content

Commit 3b6b6dd

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

File tree

9 files changed

+245
-108
lines changed

9 files changed

+245
-108
lines changed

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

Lines changed: 19 additions & 14 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>
@@ -310,10 +313,9 @@ function useOnLoad(
310313
[userContext]
311314
);
312315

313-
const handleLoadError = React.useCallback(
314-
() => dispatch({ type: "setError", accessDenied: true, error: "SOMETHING_WENT_WRONG_ERROR_RELOAD" }),
315-
[dispatch]
316-
);
316+
const handleLoadError = React.useCallback(() => {
317+
dispatch({ type: "setError", accessDenied: true, error: "SOMETHING_WENT_WRONG_ERROR_RELOAD" });
318+
}, [dispatch]);
317319

318320
const onLoad = React.useCallback(
319321
async (mfaInfo: Awaited<ReturnType<typeof fetchMFAInfo>>) => {
@@ -348,12 +350,6 @@ function useOnLoad(
348350
}
349351
}
350352

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-
357353
// If the next array only has a single option, it means the we were redirected here
358354
// automatically during the sign in process. In that case, anywhere the back button
359355
// could go would redirect back here, making it useless.
@@ -365,21 +361,30 @@ function useOnLoad(
365361
const mfaInfoEmails = mfaInfo.emails[FactorIds.WEBAUTHN];
366362
const email = mfaInfoEmails ? mfaInfoEmails[0] : undefined;
367363

368-
const canRegisterPasskey = !mfaInfo.factors.alreadySetup.includes(FactorIds.WEBAUTHN);
364+
const canRegisterPasskey = mfaInfo.factors.allowedToSetup.includes(FactorIds.WEBAUTHN);
365+
const hasRegisteredPassKey = mfaInfo.factors.alreadySetup.includes(FactorIds.WEBAUTHN);
366+
if (!hasRegisteredPassKey && !canRegisterPasskey) {
367+
dispatch({
368+
type: "setError",
369+
accessDenied: true,
370+
error: "SOMETHING_WENT_WRONG_ERROR",
371+
});
372+
}
369373
const browserSupportsWebauthnResponse = await props.recipe.webJSRecipe.doesBrowserSupportWebAuthn({
370374
userContext: userContext,
371375
});
372-
const browserSupportsWebauthn =
376+
const deviceSupported =
373377
browserSupportsWebauthnResponse.status === "OK" &&
374378
browserSupportsWebauthnResponse?.browserSupportsWebauthn;
375379

376380
dispatch({
377381
type: "load",
378382
canRegisterPasskey,
383+
hasRegisteredPassKey,
379384
error,
380385
showBackButton,
381386
email,
382-
deviceSupported: browserSupportsWebauthn,
387+
deviceSupported,
383388
});
384389
},
385390
[dispatch, recipeImplementation, props.recipe, userContext]

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

Lines changed: 113 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,91 @@ 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+
]);
128+
129+
const showBackButtonOnSignUpConfirmation = React.useMemo(() => {
130+
return (
131+
!!props.featureState.email || props.featureState.hasRegisteredPassKey || props.featureState.showBackButton
132+
);
133+
}, [props.featureState.email, props.featureState.hasRegisteredPassKey, props.featureState.showBackButton]);
84134

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]);
135+
const onSignUp = React.useCallback(async () => {
136+
await props.onSignUp(signUpEmail);
137+
}, [props.onSignUp, signUpEmail]);
92138

93139
const onFetchError = React.useCallback(() => {
94140
onError("SOMETHING_WENT_WRONG_ERROR");
95141
}, [onError]);
96142

97-
if (!props.featureState.loaded) {
98-
return <WebauthnMFALoadingScreen />;
143+
const onClickSignUpBackButton = React.useCallback(() => {
144+
if (!props.featureState.hasRegisteredPassKey && !props.featureState.showBackButton) {
145+
return;
146+
}
147+
if (!props.featureState.hasRegisteredPassKey) {
148+
onBackButtonClicked();
149+
return;
150+
}
151+
setActiveScreen(MFAScreens.SignIn);
152+
}, [props.featureState.hasRegisteredPassKey, props.featureState.showBackButton, onBackButtonClicked]);
153+
154+
const showBackButtonOnSignUp = React.useMemo(() => {
155+
return props.featureState.hasRegisteredPassKey || props.featureState.showBackButton;
156+
}, [props.featureState.email, props.featureState.showBackButton]);
157+
158+
if (activeScreen === MFAScreens.SignUp) {
159+
return (
160+
<WebauthnMFASignUp
161+
clearError={clearError}
162+
onError={onError}
163+
onFetchError={onFetchError}
164+
error={props.featureState.error}
165+
onContinueClick={onSignUpContinue}
166+
email={email}
167+
onRecoverAccountClick={props.onRecoverAccountClick}
168+
onBackButtonClicked={showBackButtonOnSignUp ? onClickSignUpBackButton : undefined}
169+
/>
170+
);
99171
}
100172

101-
if (props.featureState.accessDenied) {
173+
if (activeScreen === MFAScreens.SignUpConfirmation) {
102174
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!)}
175+
<WebauthnMFASignUpConfirmation
176+
onSignUp={onSignUp}
177+
onBackButtonClicked={
178+
showBackButtonOnSignUpConfirmation ? onClickSignUpConfirmationBackButton : undefined
179+
}
180+
email={signUpEmail}
181+
error={props.featureState.error}
106182
/>
107183
);
108184
}
109185

110186
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>
187+
<WebauthnMFASignIn
188+
onBackButtonClicked={props.featureState.showBackButton ? onBackButtonClicked : undefined}
189+
canRegisterPasskey={props.featureState.canRegisterPasskey}
190+
onSignIn={onSignIn}
191+
error={props.featureState.error}
192+
deviceSupported={props.featureState.deviceSupported}
193+
onRegisterPasskeyClick={onRegisterPasskeyClick}
194+
/>
144195
);
145196
}

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: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type MFASignUpProps = {
1818
onError: (error: string) => void;
1919
onFetchError?: (error: Response) => void;
2020
onRecoverAccountClick: () => void;
21-
onBackButtonClicked: () => void;
21+
onBackButtonClicked?: () => void;
2222
};
2323

2424
export const WebauthnMFASignUp = withOverride(
@@ -35,13 +35,17 @@ export const WebauthnMFASignUp = withOverride(
3535

3636
return (
3737
<Fragment>
38-
<div data-supertokens="headerTitle withBackButton webauthn-mfa">
39-
<BackButton onClick={props.onBackButtonClicked} />
40-
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_LINK")}
41-
<span data-supertokens="backButtonPlaceholder backButtonCommon">
42-
{/* empty span for spacing the back button */}
43-
</span>
44-
</div>
38+
{props.onBackButtonClicked ? (
39+
<div data-supertokens="headerTitle withBackButton webauthn-mfa">
40+
<BackButton onClick={props.onBackButtonClicked} />
41+
{t("WEBAUTHN_MFA_REGISTER_PASSKEY_TITLE")}
42+
<span data-supertokens="backButtonPlaceholder backButtonCommon">
43+
{/* empty span for spacing the back button */}
44+
</span>
45+
</div>
46+
) : (
47+
<div data-supertokens="headerTitle webauthn-mfa">{t("WEBAUTHN_MFA_REGISTER_PASSKEY_TITLE")}</div>
48+
)}
4549
{props.error !== undefined && <GeneralError error={props.error} />}
4650
<div data-supertokens="signUpFormInnerContainer">
4751
<div data-supertokens="cautionMessage">{t("WEBAUTHN_SIGN_UP_CAUTION_MESSAGE_LABEL")}</div>
@@ -58,7 +62,8 @@ export const WebauthnMFASignUp = withOverride(
5862
<Label value={"WEBAUTHN_SIGN_UP_LABEL"} data-supertokens="emailInputLabel" />
5963
<a
6064
onClick={props.onRecoverAccountClick}
61-
data-supertokens="link linkButton formLabelLinkBtn recoverAccountTrigger">
65+
data-supertokens="link linkButton formLabelLinkBtn recoverAccountTrigger"
66+
>
6267
{t("WEBAUTHN_RECOVER_ACCOUNT_LABEL")}
6368
</a>
6469
</div>

0 commit comments

Comments
 (0)