Skip to content

Commit 4fde11d

Browse files
Add support for handling recovery token flow
1 parent 25d2833 commit 4fde11d

File tree

5 files changed

+215
-18
lines changed

5 files changed

+215
-18
lines changed

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

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/build/webauthnprebuiltui.js

Lines changed: 121 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ import { SuperTokensBranding } from "../../../../../components/SuperTokensBrandi
2020
import SuperTokens from "../../../../../superTokens";
2121
import { AuthPageFooter, AuthPageHeader } from "../../../../../ui";
2222
import UserContextWrapper from "../../../../../usercontext/userContextWrapper";
23+
import { handleCallAPI } from "../../../../../utils";
2324
import { PasskeyConfirmation } from "../signUp/confirmation";
2425
import { ThemeBase } from "../themeBase";
2526

2627
import { PasskeyRecoverAccountSuccess } from "./success";
2728

29+
import type { FieldState } from "../../../../emailpassword/components/library/formBase";
2830
import type { RecoverAccountWithTokenThemeProps } from "../../../types";
2931

3032
export enum RecoverAccountScreen {
@@ -48,6 +50,7 @@ function PasskeyRecoverAccountWithTokenTheme(props: RecoverAccountWithTokenTheme
4850
const [errorMessageLabel, setErrorMessageLabel] = useState<string | null>(null);
4951
const [activeScreen, setActiveScreen] = useState<RecoverAccountScreen>(RecoverAccountScreen.ContinueWithPasskey);
5052
const [registerOptions, setRegisterOptions] = useState<RegisterOptions | null>(null);
53+
const [isLoading, setIsLoading] = useState(false);
5154

5255
const onResetFactorList = () => {
5356
throw new Error("Should never come here as we don't have back functionality");
@@ -99,10 +102,7 @@ function PasskeyRecoverAccountWithTokenTheme(props: RecoverAccountWithTokenTheme
99102
void fetchAndStoreRegisterOptions();
100103
}, []);
101104

102-
const onContinueClick = useCallback(async () => {
103-
// TODO: Add support to make the network call and show the next screen based
104-
// on that result.
105-
//
105+
const callAPI = useCallback(async () => {
106106
// We will do the following things in the order when the user clicks on the continue
107107
// button.
108108
// 1. Check if the fetched register options have expired
@@ -122,9 +122,85 @@ function PasskeyRecoverAccountWithTokenTheme(props: RecoverAccountWithTokenTheme
122122
await fetchAndStoreRegisterOptions();
123123
}
124124

125-
// TODO: Do rest of the logic once recover flow is ready in core.
126-
setActiveScreen(RecoverAccountScreen.Success);
127-
}, [setActiveScreen]);
125+
if (props.token === null) {
126+
// The token should not be null because while fetching the register options
127+
// we already checked for null and redirected to the sign in page if it is null.
128+
throw new Error("Should never come here");
129+
}
130+
131+
// Use the register options to register the credential and recover the account.
132+
// We should have received a valid registration options response.
133+
const registerCredentialResponse = await props.recipeImplementation.registerCredential({
134+
registrationOptions: registerOptions,
135+
});
136+
if (registerCredentialResponse.status !== "OK") {
137+
return registerCredentialResponse;
138+
}
139+
140+
const recoverAccountResponse = await props.recipeImplementation.recoverAccount({
141+
token: props.token,
142+
webauthnGeneratedOptionsId: registerOptions.webauthnGeneratedOptionsId,
143+
credential: registerCredentialResponse.registrationResponse,
144+
userContext: props.userContext,
145+
});
146+
147+
return recoverAccountResponse;
148+
}, [fetchAndStoreRegisterOptions, props, registerOptions]);
149+
150+
const onContinueClick = useCallback(async () => {
151+
const fieldUpdates: FieldState[] = [];
152+
setIsLoading(true);
153+
154+
try {
155+
const { result, generalError, fetchError } = await handleCallAPI<any>({
156+
apiFields: [],
157+
fieldUpdates,
158+
callAPI: callAPI,
159+
});
160+
161+
if (generalError !== undefined || fetchError !== undefined) {
162+
setErrorMessageLabel("WEBAUTHN_ACCOUNT_RECOVERY_GENERAL_ERROR");
163+
} else {
164+
// If successful
165+
if (result.status === "OK") {
166+
if (setIsLoading) {
167+
setIsLoading(false);
168+
}
169+
setActiveScreen(RecoverAccountScreen.Success);
170+
} else {
171+
switch (result.status) {
172+
case "RECOVER_ACCOUNT_TOKEN_INVALID_ERROR":
173+
setErrorMessageLabel("WEBAUTHN_ACCOUNT_RECOVERY_TOKEN_INVALID_ERROR");
174+
break;
175+
case "GENERAL_ERROR":
176+
setErrorMessageLabel("WEBAUTHN_ACCOUNT_RECOVERY_GENERAL_ERROR");
177+
break;
178+
case "INVALID_GENERATED_OPTIONS_ERROR":
179+
setErrorMessageLabel("WEBAUTHN_ACCOUNT_RECOVERY_INVALID_GENERATED_OPTIONS_ERROR");
180+
break;
181+
case "INVALID_CREDENTIALS_ERROR":
182+
setErrorMessageLabel("WEBAUTHN_ACCOUNT_RECOVERY_INVALID_CREDENTIALS_ERROR");
183+
break;
184+
case "GENERATED_OPTIONS_NOT_FOUND_ERROR":
185+
setErrorMessageLabel("WEBAUTHN_ACCOUNT_RECOVERY_GENERATED_OPTIONS_NOT_FOUND_ERROR");
186+
break;
187+
case "INVALID_AUTHENTICATOR_ERROR":
188+
setErrorMessageLabel("WEBAUTHN_ACCOUNT_RECOVERY_INVALID_AUTHENTICATOR_ERROR");
189+
break;
190+
default:
191+
throw new Error("Should never come here");
192+
}
193+
return;
194+
}
195+
}
196+
} catch (e) {
197+
setErrorMessageLabel("WEBAUTHN_ACCOUNT_RECOVERY_GENERAL_ERROR");
198+
} finally {
199+
if (setIsLoading) {
200+
setIsLoading(false);
201+
}
202+
}
203+
}, [callAPI]);
128204

129205
return (
130206
<UserContextWrapper userContext={props.userContext}>
@@ -155,6 +231,7 @@ function PasskeyRecoverAccountWithTokenTheme(props: RecoverAccountWithTokenTheme
155231
errorMessageLabel={errorMessageLabel}
156232
email={registerOptions?.user.name || null}
157233
isContinueDisabled={registerOptions === null}
234+
isLoading={isLoading}
158235
/>
159236
{activeScreen !== RecoverAccountScreen.Success && (
160237
<AuthPageFooter
@@ -180,6 +257,7 @@ const RecoverAccountThemeInner = (
180257
errorMessageLabel: string | null;
181258
email: string | null;
182259
isContinueDisabled: boolean;
260+
isLoading: boolean;
183261
}
184262
) => {
185263
return props.activeScreen === RecoverAccountScreen.ContinueWithPasskey ? (
@@ -188,7 +266,7 @@ const RecoverAccountThemeInner = (
188266
email={props.email || undefined}
189267
onContinueClick={props.onContinueClick}
190268
errorMessageLabel={props.errorMessageLabel || undefined}
191-
isLoading={false}
269+
isLoading={props.isLoading}
192270
hideContinueWithoutPasskey
193271
isContinueDisabled={props.isContinueDisabled}
194272
/>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,9 @@ export const defaultTranslationsWebauthn = {
4040
WEBAUTHN_ACCOUNT_RECOVERY_FETCH_ERROR: "Something went wrong, please refresh the page or reach out to support.",
4141
WEBAUTHN_SIGN_UP_CAUTION_MESSAGE_LABEL:
4242
"Make sure your email is correct—we’ll use it to help you recover your account.",
43+
WEBAUTHN_ACCOUNT_RECOVERY_INVALID_CREDENTIALS_ERROR:
44+
"The passkey is invalid, please try again, possibly with a different device.",
45+
WEBAUTHN_ACCOUNT_RECOVERY_GENERATED_OPTIONS_NOT_FOUND_ERROR: "Failed to recover account, please try again.",
46+
WEBAUTHN_ACCOUNT_RECOVERY_INVALID_AUTHENTICATOR_ERROR: "Invalid authenticator, please try again.",
4347
},
4448
};

stories/allrecipes.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export const RecoverAccountWithToken: Story = {
138138
"multifactorauth.initialized": false,
139139
"passwordless.initialized": false,
140140
"webauthn.initialized": true,
141-
query: "token=asdf",
141+
query: "token=MmEyYjVhYjdiMjA2OTc1NjMwYzM0ZGU5NDliNjhlZTQxYjQ5ZDlkMTQ5YjlhYTc5YmM1N2UxM2U0NzU4ODJmYzMzNTY0MTcyYjE1ZGYxMTMxZjg2YTUzNzNiMDkzZTU2",
142142
},
143143
};
144144

0 commit comments

Comments
 (0)