From c2190dadfabf34b194f1402bf9bbd24f5ce4d907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mih=C3=A1ly=20Lengyel?= Date: Sat, 24 Aug 2024 01:31:55 +0200 Subject: [PATCH] feat!: make passwordless link sent, otp input and mfa screens clear the login attempt info if it is missing some props (#852) --- CHANGELOG.md | 3 + lib/build/passwordless-shared.js | 11 +++ lib/build/passwordlessprebuiltui.js | 88 +++++++++++++++---- lib/build/recipe/passwordless/utils.d.ts | 3 +- .../components/features/mfa/index.tsx | 7 +- lib/ts/recipe/passwordless/prebuiltui.tsx | 15 ++++ lib/ts/recipe/passwordless/utils.ts | 13 ++- 7 files changed, 118 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d73fc63e..311957299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## [0.46.0] - 2024-08-26 + ### Breaking changes +- The prebuilt UI now clears the login attempt info if the stored data doesn't contain all the required properties. This should help migration from a custom UI to the prebuilt UI. - Changed `redirectToFactor` to accept an object instead of multiple arguments. - Changed `redirectToFactorChooser` to accept an object instead of multiple arguments. - Made MFA related screens do a success redirection if MFA is already completed and the `stepUp` query param is not set to `true`. diff --git a/lib/build/passwordless-shared.js b/lib/build/passwordless-shared.js index 9a5e31150..afcb41941 100644 --- a/lib/build/passwordless-shared.js +++ b/lib/build/passwordless-shared.js @@ -401,6 +401,16 @@ function normalisePasswordlessBaseConfig(config) { style: style, }); } +function checkAdditionalLoginAttemptInfoProperties(loginAttemptInfo) { + if ( + loginAttemptInfo.contactInfo === undefined || + loginAttemptInfo.contactMethod === undefined || + loginAttemptInfo.lastResend === undefined + ) { + return false; + } + return true; +} /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. * @@ -532,6 +542,7 @@ var Passwordless = /** @class */ (function (_super) { exports.Passwordless = Passwordless; exports.Provider = Provider; +exports.checkAdditionalLoginAttemptInfoProperties = checkAdditionalLoginAttemptInfoProperties; exports.defaultValidate = defaultValidate; exports.useContext = useContext; exports.userInputCodeValidate = userInputCodeValidate; diff --git a/lib/build/passwordlessprebuiltui.js b/lib/build/passwordlessprebuiltui.js index 6b89e70f5..003ddbe49 100644 --- a/lib/build/passwordlessprebuiltui.js +++ b/lib/build/passwordlessprebuiltui.js @@ -4751,7 +4751,13 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { loginAttemptInfo = _c.sent(); factorId = props.contactMethod === "EMAIL" ? types.FactorIds.OTP_EMAIL : types.FactorIds.OTP_PHONE; - if (!(loginAttemptInfo && props.contactMethod !== loginAttemptInfo.contactMethod)) + if ( + !( + loginAttemptInfo && + (props.contactMethod !== loginAttemptInfo.contactMethod || + !recipe$2.checkAdditionalLoginAttemptInfoProperties(loginAttemptInfo)) + ) + ) return [3 /*break*/, 3]; return [ 4 /*yield*/, @@ -6972,11 +6978,11 @@ var PasswordlessPreBuiltUI = /** @class */ (function (_super) { { type: "FULL_PAGE", preloadInfoAndRunChecks: function (firstFactors, userContext) { - var _b, _c; + var _b, _c, _d; return genericComponentOverrideContext.__awaiter(this, void 0, void 0, function () { var loginAttemptInfo; - return genericComponentOverrideContext.__generator(this, function (_d) { - switch (_d.label) { + return genericComponentOverrideContext.__generator(this, function (_e) { + switch (_e.label) { case 0: return [ 4 /*yield*/, @@ -6987,8 +6993,8 @@ var PasswordlessPreBuiltUI = /** @class */ (function (_super) { ), ]; case 1: - loginAttemptInfo = _d.sent(); - if (!(loginAttemptInfo !== undefined)) return [3 /*break*/, 5]; + loginAttemptInfo = _e.sent(); + if (!(loginAttemptInfo !== undefined)) return [3 /*break*/, 7]; if ( !( loginAttemptInfo.contactMethod === "PHONE" && @@ -7005,9 +7011,9 @@ var PasswordlessPreBuiltUI = /** @class */ (function (_super) { : _b.clearLoginAttemptInfo({ userContext: userContext }), ]; case 2: - _d.sent(); + _e.sent(); loginAttemptInfo = undefined; - return [3 /*break*/, 5]; + return [3 /*break*/, 7]; case 3: if ( !( @@ -7025,10 +7031,32 @@ var PasswordlessPreBuiltUI = /** @class */ (function (_super) { : _c.clearLoginAttemptInfo({ userContext: userContext }), ]; case 4: - _d.sent(); + _e.sent(); loginAttemptInfo = undefined; - _d.label = 5; + return [3 /*break*/, 7]; case 5: + if (!!recipe$2.checkAdditionalLoginAttemptInfoProperties(loginAttemptInfo)) + return [3 /*break*/, 7]; + // If these properties are not set, it means that the user likely started logging in + // using a custom UI and then switched to the pre-built UI. In that case, we should clear + // the login attempt info so that the user is prompted to login again, since the pre-built UI + // requires these properties to be set in order to show the correct UI. + return [ + 4 /*yield*/, + (_d = recipe$2.Passwordless.getInstanceOrThrow().webJSRecipe) === + null || _d === void 0 + ? void 0 + : _d.clearLoginAttemptInfo({ userContext: userContext }), + ]; + case 6: + // If these properties are not set, it means that the user likely started logging in + // using a custom UI and then switched to the pre-built UI. In that case, we should clear + // the login attempt info so that the user is prompted to login again, since the pre-built UI + // requires these properties to be set in order to show the correct UI. + _e.sent(); + loginAttemptInfo = undefined; + _e.label = 7; + case 7: if ( loginAttemptInfo === undefined || loginAttemptInfo.flowType !== "MAGIC_LINK" @@ -7068,11 +7096,11 @@ var PasswordlessPreBuiltUI = /** @class */ (function (_super) { { type: "FULL_PAGE", preloadInfoAndRunChecks: function (firstFactors, userContext) { - var _b, _c; + var _b, _c, _d; return genericComponentOverrideContext.__awaiter(this, void 0, void 0, function () { var loginAttemptInfo; - return genericComponentOverrideContext.__generator(this, function (_d) { - switch (_d.label) { + return genericComponentOverrideContext.__generator(this, function (_e) { + switch (_e.label) { case 0: return [ 4 /*yield*/, @@ -7083,8 +7111,8 @@ var PasswordlessPreBuiltUI = /** @class */ (function (_super) { ), ]; case 1: - loginAttemptInfo = _d.sent(); - if (!(loginAttemptInfo !== undefined)) return [3 /*break*/, 5]; + loginAttemptInfo = _e.sent(); + if (!(loginAttemptInfo !== undefined)) return [3 /*break*/, 7]; if ( !( loginAttemptInfo.contactMethod === "PHONE" && @@ -7101,9 +7129,9 @@ var PasswordlessPreBuiltUI = /** @class */ (function (_super) { : _b.clearLoginAttemptInfo({ userContext: userContext }), ]; case 2: - _d.sent(); + _e.sent(); loginAttemptInfo = undefined; - return [3 /*break*/, 5]; + return [3 /*break*/, 7]; case 3: if ( !( @@ -7121,10 +7149,32 @@ var PasswordlessPreBuiltUI = /** @class */ (function (_super) { : _c.clearLoginAttemptInfo({ userContext: userContext }), ]; case 4: - _d.sent(); + _e.sent(); loginAttemptInfo = undefined; - _d.label = 5; + return [3 /*break*/, 7]; case 5: + if (!!recipe$2.checkAdditionalLoginAttemptInfoProperties(loginAttemptInfo)) + return [3 /*break*/, 7]; + // If these properties are not set, it means that the user likely started logging in + // using a custom UI and then switched to the pre-built UI. In that case, we should clear + // the login attempt info so that the user is prompted to login again, since the pre-built UI + // requires these properties to be set in order to show the correct UI. + return [ + 4 /*yield*/, + (_d = recipe$2.Passwordless.getInstanceOrThrow().webJSRecipe) === + null || _d === void 0 + ? void 0 + : _d.clearLoginAttemptInfo({ userContext: userContext }), + ]; + case 6: + // If these properties are not set, it means that the user likely started logging in + // using a custom UI and then switched to the pre-built UI. In that case, we should clear + // the login attempt info so that the user is prompted to login again, since the pre-built UI + // requires these properties to be set in order to show the correct UI. + _e.sent(); + loginAttemptInfo = undefined; + _e.label = 7; + case 7: if ( loginAttemptInfo === undefined || loginAttemptInfo.flowType === "MAGIC_LINK" diff --git a/lib/build/recipe/passwordless/utils.d.ts b/lib/build/recipe/passwordless/utils.d.ts index a5b28e889..31d54d735 100644 --- a/lib/build/recipe/passwordless/utils.d.ts +++ b/lib/build/recipe/passwordless/utils.d.ts @@ -1,2 +1,3 @@ -import type { Config, NormalisedConfig } from "./types"; +import type { Config, LoginAttemptInfo, NormalisedConfig } from "./types"; export declare function normalisePasswordlessConfig(config: Config): NormalisedConfig; +export declare function checkAdditionalLoginAttemptInfoProperties(loginAttemptInfo: LoginAttemptInfo): boolean; diff --git a/lib/ts/recipe/passwordless/components/features/mfa/index.tsx b/lib/ts/recipe/passwordless/components/features/mfa/index.tsx index 55571bfe5..c742fc27e 100644 --- a/lib/ts/recipe/passwordless/components/features/mfa/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/mfa/index.tsx @@ -43,6 +43,7 @@ import SessionRecipe from "../../../../session/recipe"; import Session from "../../../../session/recipe"; import { defaultPhoneNumberValidator } from "../../../defaultPhoneNumberValidator"; import { getPhoneNumberUtils } from "../../../phoneNumberUtils"; +import { checkAdditionalLoginAttemptInfoProperties } from "../../../utils"; import MFAThemeWrapper from "../../themes/mfa"; import { defaultTranslationsPasswordless } from "../../themes/translations"; @@ -315,7 +316,11 @@ function useOnLoad( const factorId = props.contactMethod === "EMAIL" ? FactorIds.OTP_EMAIL : FactorIds.OTP_PHONE; - if (loginAttemptInfo && props.contactMethod !== loginAttemptInfo.contactMethod) { + if ( + loginAttemptInfo && + (props.contactMethod !== loginAttemptInfo.contactMethod || + !checkAdditionalLoginAttemptInfoProperties(loginAttemptInfo)) + ) { await recipeImplementation?.clearLoginAttemptInfo({ userContext }); loginAttemptInfo = undefined; } diff --git a/lib/ts/recipe/passwordless/prebuiltui.tsx b/lib/ts/recipe/passwordless/prebuiltui.tsx index b3a0ecb1c..9143ba4f9 100644 --- a/lib/ts/recipe/passwordless/prebuiltui.tsx +++ b/lib/ts/recipe/passwordless/prebuiltui.tsx @@ -18,6 +18,7 @@ import UserInputCodeFeature from "./components/features/userInputCode"; import MFAThemeWrapper from "./components/themes/mfa"; import { defaultTranslationsPasswordless } from "./components/themes/translations"; import Passwordless from "./recipe"; +import { checkAdditionalLoginAttemptInfoProperties } from "./utils"; import type { AdditionalLoginAttemptInfoProperties, LoginAttemptInfo } from "./types"; import type { GenericComponentOverrideMap } from "../../components/componentOverride/componentOverrideContext"; @@ -187,6 +188,13 @@ export class PasswordlessPreBuiltUI extends RecipeRouter { ) { await Passwordless.getInstanceOrThrow().webJSRecipe?.clearLoginAttemptInfo({ userContext }); loginAttemptInfo = undefined; + } else if (!checkAdditionalLoginAttemptInfoProperties(loginAttemptInfo)) { + // If these properties are not set, it means that the user likely started logging in + // using a custom UI and then switched to the pre-built UI. In that case, we should clear + // the login attempt info so that the user is prompted to login again, since the pre-built UI + // requires these properties to be set in order to show the correct UI. + await Passwordless.getInstanceOrThrow().webJSRecipe?.clearLoginAttemptInfo({ userContext }); + loginAttemptInfo = undefined; } } @@ -234,6 +242,13 @@ export class PasswordlessPreBuiltUI extends RecipeRouter { ) { await Passwordless.getInstanceOrThrow().webJSRecipe?.clearLoginAttemptInfo({ userContext }); loginAttemptInfo = undefined; + } else if (!checkAdditionalLoginAttemptInfoProperties(loginAttemptInfo)) { + // If these properties are not set, it means that the user likely started logging in + // using a custom UI and then switched to the pre-built UI. In that case, we should clear + // the login attempt info so that the user is prompted to login again, since the pre-built UI + // requires these properties to be set in order to show the correct UI. + await Passwordless.getInstanceOrThrow().webJSRecipe?.clearLoginAttemptInfo({ userContext }); + loginAttemptInfo = undefined; } } if (loginAttemptInfo === undefined || loginAttemptInfo.flowType === "MAGIC_LINK") { diff --git a/lib/ts/recipe/passwordless/utils.ts b/lib/ts/recipe/passwordless/utils.ts index 341b04529..8404896d4 100644 --- a/lib/ts/recipe/passwordless/utils.ts +++ b/lib/ts/recipe/passwordless/utils.ts @@ -17,7 +17,7 @@ import { normaliseAuthRecipe } from "../authRecipe/utils"; import { defaultEmailValidator } from "./validators"; -import type { Config, NormalisedConfig, SignInUpFeatureConfigInput } from "./types"; +import type { Config, LoginAttemptInfo, NormalisedConfig, SignInUpFeatureConfigInput } from "./types"; import type { FeatureBaseConfig, NormalisedBaseConfig } from "../../types"; import type { RecipeInterface } from "supertokens-web-js/recipe/passwordless"; @@ -115,3 +115,14 @@ function normalisePasswordlessBaseConfig(config?: T & FeatureBaseConfig): T & style, }; } + +export function checkAdditionalLoginAttemptInfoProperties(loginAttemptInfo: LoginAttemptInfo) { + if ( + loginAttemptInfo.contactInfo === undefined || + loginAttemptInfo.contactMethod === undefined || + loginAttemptInfo.lastResend === undefined + ) { + return false; + } + return true; +}