diff --git a/examples/for-tests/src/App.js b/examples/for-tests/src/App.js index 75ef6a7f1..07c35a5ff 100644 --- a/examples/for-tests/src/App.js +++ b/examples/for-tests/src/App.js @@ -208,63 +208,7 @@ window.resetTOTP = () => { }; let recipeList = [ TOTP.init({ - override: { - functions: (oI) => ({ - ...oI, - listDevices: async () => ({ devices: totpDevices, status: "OK" }), - removeDevice: async ({ deviceName }) => { - return { status: "OK", didDeviceExist: removeTOTPDevice(deviceName) }; - }, - createDevice: async ({ deviceName }) => { - deviceName = deviceName ?? `totp-${Date.now()}`; - addTOTPDevice(deviceName); - return { - status: "OK", - deviceName: deviceName, - issuerName: "st", - qrCodeString: deviceName, - secret: `secret-${deviceName}`, - }; - }, - verifyCode: async ({ totp }) => { - const dev = totpDevices.find((d) => d.deviceName.endsWith(totp) && d.verified); - if (dev) { - await fetch("http://localhost:8082/completeFactor", { - method: "POST", - body: JSON.stringify({ id: "totp" }), - headers: new Headers([["Content-Type", "application/json"]]), - }); - return { status: "OK" }; - } - - if (++tryCount > 3) { - return { status: "LIMIT_REACHED_ERROR", retryAfterMs: 30000 }; - } - return { status: "INVALID_TOTP_ERROR" }; - }, - verifyDevice: async ({ deviceName, totp }) => { - const dev = totpDevices.find((d) => d.deviceName === deviceName); - if (!dev) { - return { status: "UNKNOWN_DEVICE_ERROR" }; - } - if (deviceName.endsWith(totp)) { - const wasAlreadyVerified = dev.verified; - verifyTOTPDevice(deviceName); - await fetch("http://localhost:8082/completeFactor", { - method: "POST", - body: JSON.stringify({ id: "totp" }), - headers: new Headers([["Content-Type", "application/json"]]), - }); - return { status: "OK", wasAlreadyVerified }; - } - - if (++tryCount > 3) { - return { status: "LIMIT_REACHED_ERROR", retryAfterMs: 30000 }; - } - return { status: "INVALID_TOTP_ERROR" }; - }, - }), - }, + override: {}, }), MultiFactorAuth.init({ firstFactors: testContext.firstFactors, diff --git a/lib/build/authRecipe-shared2.js b/lib/build/authRecipe-shared2.js index 25c539965..949bb434a 100644 --- a/lib/build/authRecipe-shared2.js +++ b/lib/build/authRecipe-shared2.js @@ -26,6 +26,7 @@ var Redirector = function (props) { var _a = React.useState(false), alwaysShow = _a[0], updateAlwaysShow = _a[1]; + var rethrowInRender = genericComponentOverrideContext.useRethrowInRender(); React.useEffect( function () { // we want to do this just once, so we supply it with only the loading state. @@ -39,19 +40,21 @@ var Redirector = function (props) { props.authRecipe.config.onHandleEvent({ action: "SESSION_ALREADY_EXISTS", }); - void recipe.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: props.authRecipe.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: false, - user: undefined, - redirectToPath: genericComponentOverrideContext.getRedirectToPathFromURL(), + recipe.Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: props.authRecipe.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: false, + user: undefined, + redirectToPath: genericComponentOverrideContext.getRedirectToPathFromURL(), + }, }, - }, - userContext, - props.history - ); + userContext, + props.history + ) + .catch(rethrowInRender); } } else { // this means even if a session exists, we will still show the children diff --git a/lib/build/emailpassword-shared7.js b/lib/build/emailpassword-shared7.js index 73b9b6e38..33b23d200 100644 --- a/lib/build/emailpassword-shared7.js +++ b/lib/build/emailpassword-shared7.js @@ -1123,25 +1123,28 @@ function useChildProps(recipe$1, state, dispatch, history) { [recipe$1] ); var userContext = uiEntry.useUserContext(); + var rethrowInRender = genericComponentOverrideContext.useRethrowInRender(); var onSignInSuccess = React.useCallback( function (response) { return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () { return genericComponentOverrideContext.__generator(this, function (_a) { return [ 2 /*return*/, - recipe.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: recipe$1.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: false, - user: response.user, - redirectToPath: genericComponentOverrideContext.getRedirectToPathFromURL(), + recipe.Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: recipe$1.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: false, + user: response.user, + redirectToPath: genericComponentOverrideContext.getRedirectToPathFromURL(), + }, }, - }, - userContext, - history - ), + userContext, + history + ) + .catch(rethrowInRender), ]; }); }); @@ -1154,19 +1157,21 @@ function useChildProps(recipe$1, state, dispatch, history) { return genericComponentOverrideContext.__generator(this, function (_a) { return [ 2 /*return*/, - recipe.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: recipe$1.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: true, - user: response.user, - redirectToPath: genericComponentOverrideContext.getRedirectToPathFromURL(), + recipe.Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: recipe$1.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: true, + user: response.user, + redirectToPath: genericComponentOverrideContext.getRedirectToPathFromURL(), + }, }, - }, - userContext, - history - ), + userContext, + history + ) + .catch(rethrowInRender), ]; }); }); diff --git a/lib/build/emailpassword-shared9.js b/lib/build/emailpassword-shared9.js index 74aeaa408..e0997491a 100644 --- a/lib/build/emailpassword-shared9.js +++ b/lib/build/emailpassword-shared9.js @@ -676,7 +676,12 @@ var FormBase = function (props) { return jsxRuntime.jsxs( "form", genericComponentOverrideContext.__assign( - { autoComplete: "on", noValidate: true, onSubmit: onFormSubmit }, + { + autoComplete: "on", + noValidate: true, + onSubmit: onFormSubmit, + "data-supertokens": props.formDataSupertokens, + }, { children: [ formFields.map(function (field) { diff --git a/lib/build/emailverificationprebuiltui.js b/lib/build/emailverificationprebuiltui.js index c5be31f23..06ca5d884 100644 --- a/lib/build/emailverificationprebuiltui.js +++ b/lib/build/emailverificationprebuiltui.js @@ -757,6 +757,7 @@ var EmailVerification$1 = function (props) { status = _b[0], setStatus = _b[1]; var userContext = uiEntry.useUserContext(); + var rethrowInRender = genericComponentOverrideContext.useRethrowInRender(); var recipeComponentOverrides = props.useComponentOverrides(); var redirectToAuthWithHistory = React.useCallback( function () { @@ -808,11 +809,9 @@ var EmailVerification$1 = function (props) { return genericComponentOverrideContext.__generator(this, function (_a) { return [ 2 /*return*/, - recipe.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - undefined, - userContext, - props.history - ), + recipe.Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection(undefined, userContext, props.history) + .catch(rethrowInRender), ]; }); }); diff --git a/lib/build/genericComponentOverrideContext.js b/lib/build/genericComponentOverrideContext.js index d8ad564ec..3fa13f12b 100644 --- a/lib/build/genericComponentOverrideContext.js +++ b/lib/build/genericComponentOverrideContext.js @@ -824,6 +824,15 @@ var useOnMountAPICall = function (fetch, handleResponse, handleError, startLoadi throw error; } }; +function useRethrowInRender() { + var _a = React.useState(undefined), + error = _a[0], + setError = _a[1]; + if (error) { + throw error; + } + return setError; +} var BaseRecipeModule = /** @class */ (function () { /* @@ -1391,4 +1400,5 @@ exports.redirectWithFullPageReload = redirectWithFullPageReload; exports.removeFromLocalStorage = removeFromLocalStorage; exports.setLocalStorage = setLocalStorage; exports.useOnMountAPICall = useOnMountAPICall; +exports.useRethrowInRender = useRethrowInRender; exports.validateForm = validateForm; diff --git a/lib/build/multifactorauth-shared.js b/lib/build/multifactorauth-shared.js index 5618818b5..8832912d9 100644 --- a/lib/build/multifactorauth-shared.js +++ b/lib/build/multifactorauth-shared.js @@ -50,7 +50,7 @@ var MultiFactorAuthClaimClass = /** @class */ (function () { var defaultOnFailureRedirection = function (_a) { var reason = _a.reason, userContext = _a.userContext; - if (reason.nextFactorOptions) { + if (reason.nextFactorOptions !== undefined) { if (reason.nextFactorOptions.length === 1) { return getRedirectURL( { action: "GO_TO_FACTOR", factorId: reason.nextFactorOptions[0] }, @@ -60,7 +60,11 @@ var MultiFactorAuthClaimClass = /** @class */ (function () { return getRedirectURL({ action: "FACTOR_CHOOSER" }, userContext); } } - return getRedirectURL({ action: "GO_TO_FACTOR", factorId: reason.factorId }, userContext); + if (reason.factorId !== undefined) { + return getRedirectURL({ action: "GO_TO_FACTOR", factorId: reason.factorId }, userContext); + } + // This should basically never happen, but it will show the access denied screen + return undefined; }; this.validators = genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, this.webJSClaim.validators), diff --git a/lib/build/passwordless-shared4.js b/lib/build/passwordless-shared4.js index 6d41f4972..2570537ee 100644 --- a/lib/build/passwordless-shared4.js +++ b/lib/build/passwordless-shared4.js @@ -16,6 +16,7 @@ var button = require("./emailpassword-shared2.js"); var windowHandler = require("supertokens-web-js/utils/windowHandler"); var recipe$1 = require("./multifactorauth-shared.js"); var validators$1 = require("./passwordless-shared3.js"); +var recipe$2 = require("./passwordless-shared2.js"); var SuperTokensBranding = require("./SuperTokensBranding.js"); var generalError = require("./emailpassword-shared.js"); var sessionprebuiltui = require("./sessionprebuiltui.js"); @@ -25,7 +26,6 @@ var validators = require("./emailpassword-shared6.js"); var arrowLeftIcon = require("./arrowLeftIcon.js"); var multifactorauth = require("./multifactorauth-shared2.js"); var backButton = require("./emailpassword-shared8.js"); -var recipe$2 = require("./passwordless-shared2.js"); function _interopDefault(e) { return e && e.__esModule ? e : { default: e }; @@ -252,6 +252,7 @@ var defaultTranslationsPasswordless = { var LinkClickedScreen = function (props) { var userContext = uiEntry.useUserContext(); + var rethrowInRender = genericComponentOverrideContext.useRethrowInRender(); var _a = React.useState(false), requireUserInteraction = _a[0], setRequireUserInteraction = _a[1]; @@ -363,22 +364,24 @@ var LinkClickedScreen = function (props) { _a.sent(); return [ 2 /*return*/, - recipe.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: props.recipe.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: response.createdNewRecipeUser, - user: response.user, - redirectToPath: - loginAttemptInfo === null || loginAttemptInfo === void 0 - ? void 0 - : loginAttemptInfo.redirectToPath, + recipe.Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: props.recipe.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: response.createdNewRecipeUser, + user: response.user, + redirectToPath: + loginAttemptInfo === null || loginAttemptInfo === void 0 + ? void 0 + : loginAttemptInfo.redirectToPath, + }, }, - }, - userContext, - props.history - ), + userContext, + props.history + ) + .catch(rethrowInRender), ]; case 3: return [2 /*return*/]; @@ -3132,6 +3135,33 @@ var UserInputCodeForm = uiEntry.withOverride( } ); +var OTPLoadingScreen = function () { + return jsxRuntime.jsx( + "div", + genericComponentOverrideContext.__assign( + { "data-supertokens": "container delayedRender pwless-mfa loadingScreen" }, + { + children: jsxRuntime.jsx( + "div", + genericComponentOverrideContext.__assign( + { "data-supertokens": "row" }, + { + children: jsxRuntime.jsx( + "div", + genericComponentOverrideContext.__assign( + { "data-supertokens": "spinner delayedRender" }, + { children: jsxRuntime.jsx(uiEntry.SpinnerIcon, {}) } + ) + ), + } + ) + ), + } + ) + ); +}; +var LoadingScreen = uiEntry.withOverride("OTPLoadingScreen", OTPLoadingScreen); + var MFAFooter = uiEntry.withOverride("PasswordlessMFAFooter", function PasswordlessMFAFooter(props) { var _a, _b; var t = translationContext.useTranslation(); @@ -3139,7 +3169,7 @@ var MFAFooter = uiEntry.withOverride("PasswordlessMFAFooter", function Passwordl return jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "footerLinkGroupVert pwlessMFAFooter" }, + { "data-supertokens": "footerLinkGroupVert pwless-mfa footer" }, { children: [ claim.loading === false && @@ -3186,7 +3216,7 @@ var MFAHeader = uiEntry.withOverride("PasswordlessMFAHeader", function Passwordl jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "headerTitle withBackButton" }, + { "data-supertokens": "headerTitle withBackButton pwless-mfa header" }, { children: [ claim.loading === false && @@ -3221,7 +3251,7 @@ var MFAOTPFooter = uiEntry.withOverride("PasswordlessMFAOTPFooter", function Pas return jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "footerLinkGroupVert pwlessMFAOTPFooter" }, + { "data-supertokens": "footerLinkGroupVert pwless-mfa otpFooter" }, { children: [ isSetupAllowed @@ -3291,7 +3321,7 @@ var MFAOTPHeader = uiEntry.withOverride("PasswordlessMFAOTPHeader", function Pas jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "headerTitle withBackButton" }, + { "data-supertokens": "headerTitle withBackButton pwless-mfa otpHeader" }, { children: [ claim.loading === false && @@ -3353,7 +3383,7 @@ var MFATheme = function (_a) { error: featureState.error, }; if (!featureState.loaded) { - return null; + return jsxRuntime.jsx(LoadingScreen, {}); } return activeScreen === MFAScreens.CloseTab ? jsxRuntime.jsx(CloseTabScreen, genericComponentOverrideContext.__assign({}, commonProps)) @@ -3362,7 +3392,7 @@ var MFATheme = function (_a) { : jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "container pwlessMFA" }, + { "data-supertokens": "container pwless-mfa" }, { children: [ jsxRuntime.jsx( @@ -3637,6 +3667,7 @@ var useFeatureReducer$1 = function () { }; function useChildProps$1(recipe$2, recipeImplementation, state, contactMethod, userContext, history) { var _this = this; + var rethrowInRender = genericComponentOverrideContext.useRethrowInRender(); return React.useMemo( function () { return { @@ -3654,11 +3685,9 @@ function useChildProps$1(recipe$2, recipeImplementation, state, contactMethod, u redirectToPath: redirectToPath, }, }; - return recipe.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - redirectInfo, - userContext, - history - ); + return recipe.Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection(redirectInfo, userContext, history) + .catch(rethrowInRender); }, onSignOutClicked: function () { return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () { @@ -3713,8 +3742,7 @@ function useChildProps$1(recipe$2, recipeImplementation, state, contactMethod, u [contactMethod, state, recipeImplementation] ); } -var MFAFeature = function (props) { - var recipeComponentOverrides = props.useComponentOverrides(); +var MFAFeatureInner = function (props) { var userContext = uiEntry.useUserContext(); var callingConsumeCodeRef = React.useRef(false); var _a = useFeatureReducer$1(), @@ -3744,6 +3772,34 @@ var MFAFeature = function (props) { props.history ); useSuccessInAnotherTabChecker$1(callingConsumeCodeRef, recipeImplementation, state, dispatch, userContext); + return jsxRuntime.jsxs(React.Fragment, { + children: [ + props.children === undefined && + jsxRuntime.jsx( + MFAThemeWrapper, + genericComponentOverrideContext.__assign({}, childProps, { + featureState: state, + dispatch: dispatch, + }) + ), + props.children && + React__namespace.Children.map(props.children, function (child) { + if (React__namespace.isValidElement(child)) { + return React__namespace.cloneElement( + child, + genericComponentOverrideContext.__assign( + genericComponentOverrideContext.__assign({}, childProps), + { featureState: state, dispatch: dispatch } + ) + ); + } + return child; + }), + ], + }); +}; +var MFAFeature = function (props) { + var recipeComponentOverrides = props.useComponentOverrides(); return jsxRuntime.jsx( uiEntry.ComponentOverrideContext.Provider, genericComponentOverrideContext.__assign( @@ -3757,31 +3813,10 @@ var MFAFeature = function (props) { defaultStore: defaultTranslationsPasswordless, }, { - children: jsxRuntime.jsxs(React.Fragment, { - children: [ - props.children === undefined && - jsxRuntime.jsx( - MFAThemeWrapper, - genericComponentOverrideContext.__assign({}, childProps, { - featureState: state, - dispatch: dispatch, - }) - ), - props.children && - React__namespace.Children.map(props.children, function (child) { - if (React__namespace.isValidElement(child)) { - return React__namespace.cloneElement( - child, - genericComponentOverrideContext.__assign( - genericComponentOverrideContext.__assign({}, childProps), - { featureState: state, dispatch: dispatch } - ) - ); - } - return child; - }), - ], - }), + children: jsxRuntime.jsx( + MFAFeatureInner, + genericComponentOverrideContext.__assign({}, props) + ), } ) ), @@ -3812,10 +3847,18 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { }, [dispatch] ); + var dynamicLoginMethods = uiEntry.useDynamicLoginMethods(); var onLoad = React__namespace.useCallback( function (mfaInfo) { return genericComponentOverrideContext.__awaiter(_this, void 0, void 0, function () { - var error, errorQueryParam, doSetup, loginAttemptInfo, isAlreadySetup, isAllowedToSetup; + var error, + errorQueryParam, + doSetup, + loginAttemptInfo, + isAlreadySetup, + isAllowedToSetup, + enabledContactMethods, + createResp; return genericComponentOverrideContext.__generator(this, function (_a) { switch (_a.label) { case 0: @@ -3841,13 +3884,30 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { props.contactMethod === "EMAIL" ? mfaInfo.factors.isAllowedToSetup.includes("otp-email") : mfaInfo.factors.isAllowedToSetup.includes("otp-phone"); - if (!!loginAttemptInfo) return [3 /*break*/, 15]; - if (!(props.contactMethod === "EMAIL")) return [3 /*break*/, 8]; - if (!(isAlreadySetup && doSetup !== "true")) return [3 /*break*/, 6]; - _a.label = 2; + enabledContactMethods = recipe$2.getEnabledContactMethods( + props.recipe.config.contactMethod, + dynamicLoginMethods + ); + if (!(loginAttemptInfo && !enabledContactMethods.includes(loginAttemptInfo.contactMethod))) + return [3 /*break*/, 3]; + return [ + 4 /*yield*/, + recipeImplementation === null || recipeImplementation === void 0 + ? void 0 + : recipeImplementation.clearLoginAttemptInfo({ userContext: userContext }), + ]; case 2: - _a.trys.push([2, 4, , 5]); - // createCode also dispatches the necessary events + _a.sent(); + loginAttemptInfo = undefined; + _a.label = 3; + case 3: + if (!!loginAttemptInfo) return [3 /*break*/, 17]; + if (!(props.contactMethod === "EMAIL")) return [3 /*break*/, 10]; + if (!(isAlreadySetup && doSetup !== "true")) return [3 /*break*/, 8]; + createResp = void 0; + _a.label = 4; + case 4: + _a.trys.push([4, 6, , 7]); return [ 4 /*yield*/, recipeImplementation.createCode({ @@ -3855,17 +3915,20 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { userContext: userContext, }), ]; - case 3: - // createCode also dispatches the necessary events - _a.sent(); - return [3 /*break*/, 5]; - case 4: - _a.sent(); - dispatch({ type: "setError", error: "SOMETHING_WENT_WRONG_ERROR" }); - return [3 /*break*/, 5]; case 5: + // createCode also dispatches the necessary events + createResp = _a.sent(); return [3 /*break*/, 7]; case 6: + _a.sent(); + dispatch({ type: "setError", error: "SOMETHING_WENT_WRONG_ERROR" }); + return [2 /*return*/]; + case 7: + if ((createResp === null || createResp === void 0 ? void 0 : createResp.status) !== "OK") { + dispatch({ type: "setError", error: "SOMETHING_WENT_WRONG_ERROR" }); + } + return [3 /*break*/, 9]; + case 8: if (!mfaInfo.factors.isAllowedToSetup.includes("otp-email")) { dispatch({ type: "load", @@ -3881,14 +3944,14 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { isAllowedToSetup: true, }); // since loginAttemptInfo is undefined, this will ask the user for the email } - _a.label = 7; - case 7: - return [3 /*break*/, 14]; - case 8: - if (!(isAlreadySetup && doSetup !== "true")) return [3 /*break*/, 13]; _a.label = 9; case 9: - _a.trys.push([9, 11, , 12]); + return [3 /*break*/, 16]; + case 10: + if (!(isAlreadySetup && doSetup !== "true")) return [3 /*break*/, 15]; + _a.label = 11; + case 11: + _a.trys.push([11, 13, , 14]); // createCode also dispatches the necessary events return [ 4 /*yield*/, @@ -3897,17 +3960,17 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { userContext: userContext, }), ]; - case 10: + case 12: // createCode also dispatches the necessary events _a.sent(); - return [3 /*break*/, 12]; - case 11: + return [3 /*break*/, 14]; + case 13: _a.sent(); dispatch({ type: "setError", error: "SOMETHING_WENT_WRONG_ERROR" }); - return [3 /*break*/, 12]; - case 12: return [3 /*break*/, 14]; - case 13: + case 14: + return [3 /*break*/, 16]; + case 15: if (!mfaInfo.factors.isAllowedToSetup.includes("otp-phone")) { dispatch({ type: "load", @@ -3923,10 +3986,10 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { isAllowedToSetup: true, }); // since loginAttemptInfo is undefined, this will ask the user for the phone number } - _a.label = 14; - case 14: - return [3 /*break*/, 16]; - case 15: + _a.label = 16; + case 16: + return [3 /*break*/, 18]; + case 17: // No need to check if the component is unmounting, since this has no effect then. dispatch({ type: "load", @@ -3934,8 +3997,8 @@ function useOnLoad(props, recipeImplementation, dispatch, userContext) { error: error, isAllowedToSetup: isAllowedToSetup, }); - _a.label = 16; - case 16: + _a.label = 18; + case 18: return [2 /*return*/]; } }); @@ -4980,7 +5043,8 @@ var useSuccessInAnotherTabChecker = function (state, dispatch, userContext) { ); return callingConsumeCodeRef; }; -var useFeatureReducer = function (recipeImpl, userContext) { +var useFeatureReducer = function (recipeImpl, contactMethod, userContext) { + var dynamicLoginMethods = uiEntry.useDynamicLoginMethods(); var _a = React__namespace.useReducer( function (oldState, action) { switch (action.type) { @@ -5062,7 +5126,7 @@ var useFeatureReducer = function (recipeImpl, userContext) { } function load() { return genericComponentOverrideContext.__awaiter(this, void 0, void 0, function () { - var error, errorQueryParam, messageQueryParam, loginAttemptInfo; + var error, errorQueryParam, messageQueryParam, loginAttemptInfo, enabledContactMethods; return genericComponentOverrideContext.__generator(this, function (_a) { switch (_a.label) { case 0: @@ -5088,6 +5152,28 @@ var useFeatureReducer = function (recipeImpl, userContext) { ]; case 1: loginAttemptInfo = _a.sent(); + enabledContactMethods = recipe$2.getEnabledContactMethods( + contactMethod, + dynamicLoginMethods + ); + if ( + !( + loginAttemptInfo && + !enabledContactMethods.includes(loginAttemptInfo.contactMethod) + ) + ) + return [3 /*break*/, 3]; + return [ + 4 /*yield*/, + recipeImpl === null || recipeImpl === void 0 + ? void 0 + : recipeImpl.clearLoginAttemptInfo({ userContext: userContext }), + ]; + case 2: + _a.sent(); + loginAttemptInfo = undefined; + _a.label = 3; + case 3: // No need to check if the component is unmounting, since this has no effect then. dispatch({ type: "load", loginAttemptInfo: loginAttemptInfo, error: error }); return [2 /*return*/]; @@ -5113,6 +5199,7 @@ function useChildProps(recipe$1, dispatch, state, callingConsumeCodeRef, userCon }, [recipe$1] ); + var rethrowInRender = genericComponentOverrideContext.useRethrowInRender(); return React.useMemo( function () { if (!recipe$1 || !recipeImplementation) { @@ -5120,19 +5207,21 @@ function useChildProps(recipe$1, dispatch, state, callingConsumeCodeRef, userCon } return { onSuccess: function (result) { - return recipe.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: recipe$1.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: result.createdNewRecipeUser, - user: result.user, - redirectToPath: genericComponentOverrideContext.getRedirectToPathFromURL(), + return recipe.Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: recipe$1.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: result.createdNewRecipeUser, + user: result.user, + redirectToPath: genericComponentOverrideContext.getRedirectToPathFromURL(), + }, }, - }, - userContext, - history - ); + userContext, + history + ) + .catch(rethrowInRender); }, recipeImplementation: recipeImplementation, config: recipe$1.config, @@ -5141,14 +5230,41 @@ function useChildProps(recipe$1, dispatch, state, callingConsumeCodeRef, userCon [state, recipeImplementation] ); } -var SignInUpFeature = function (props) { - var recipeComponentOverrides = props.useComponentOverrides(); +var SignInUpFeatureInner = function (props) { var userContext = uiEntry.useUserContext(); - var _a = useFeatureReducer(props.recipe.webJSRecipe, userContext), + var _a = useFeatureReducer(props.recipe.webJSRecipe, props.recipe.config.contactMethod, userContext), state = _a[0], dispatch = _a[1]; var callingConsumeCodeRef = useSuccessInAnotherTabChecker(state, dispatch, userContext); var childProps = useChildProps(props.recipe, dispatch, state, callingConsumeCodeRef, userContext, props.history); + return jsxRuntime.jsxs(React.Fragment, { + children: [ + props.children === undefined && + jsxRuntime.jsx( + SignInUpThemeWrapper, + genericComponentOverrideContext.__assign({}, childProps, { + featureState: state, + dispatch: dispatch, + }) + ), + props.children && + React__namespace.Children.map(props.children, function (child) { + if (React__namespace.isValidElement(child)) { + return React__namespace.cloneElement( + child, + genericComponentOverrideContext.__assign( + genericComponentOverrideContext.__assign({}, childProps), + { featureState: state, dispatch: dispatch } + ) + ); + } + return child; + }), + ], + }); +}; +var SignInUpFeature = function (props) { + var recipeComponentOverrides = props.useComponentOverrides(); return jsxRuntime.jsx( uiEntry.ComponentOverrideContext.Provider, genericComponentOverrideContext.__assign( @@ -5162,31 +5278,10 @@ var SignInUpFeature = function (props) { defaultStore: defaultTranslationsPasswordless, }, { - children: jsxRuntime.jsxs(React.Fragment, { - children: [ - props.children === undefined && - jsxRuntime.jsx( - SignInUpThemeWrapper, - genericComponentOverrideContext.__assign({}, childProps, { - featureState: state, - dispatch: dispatch, - }) - ), - props.children && - React__namespace.Children.map(props.children, function (child) { - if (React__namespace.isValidElement(child)) { - return React__namespace.cloneElement( - child, - genericComponentOverrideContext.__assign( - genericComponentOverrideContext.__assign({}, childProps), - { featureState: state, dispatch: dispatch } - ) - ); - } - return child; - }), - ], - }), + children: jsxRuntime.jsx( + SignInUpFeatureInner, + genericComponentOverrideContext.__assign({}, props) + ), } ) ), diff --git a/lib/build/recipe/emailpassword/types.d.ts b/lib/build/recipe/emailpassword/types.d.ts index d819f58c8..7ab116c87 100644 --- a/lib/build/recipe/emailpassword/types.d.ts +++ b/lib/build/recipe/emailpassword/types.d.ts @@ -212,6 +212,7 @@ export declare type SubmitNewPasswordProps = FormThemeBaseProps & { export declare type EnterEmailStatus = "READY" | "SENT"; export declare type SubmitNewPasswordStatus = "READY" | "SUCCESS"; export declare type FormBaseProps = { + formDataSupertokens?: string; footer?: JSX.Element; formFields: FormFieldThemeProps[]; showLabels: boolean; diff --git a/lib/build/recipe/passwordless/components/features/signInAndUp/index.d.ts b/lib/build/recipe/passwordless/components/features/signInAndUp/index.d.ts index 02b7a8daa..229b33eb2 100644 --- a/lib/build/recipe/passwordless/components/features/signInAndUp/index.d.ts +++ b/lib/build/recipe/passwordless/components/features/signInAndUp/index.d.ts @@ -2,7 +2,7 @@ import * as React from "react"; import type { FeatureBaseProps } from "../../../../../types"; import type Recipe from "../../../recipe"; import type { ComponentOverrideMap } from "../../../types"; -import type { PasswordlessSignInUpAction, SignInUpState, SignInUpChildProps } from "../../../types"; +import type { PasswordlessSignInUpAction, SignInUpState, SignInUpChildProps, NormalisedConfig } from "../../../types"; import type { RecipeInterface } from "supertokens-web-js/recipe/passwordless"; export declare const useSuccessInAnotherTabChecker: ( state: SignInUpState, @@ -11,6 +11,7 @@ export declare const useSuccessInAnotherTabChecker: ( ) => React.MutableRefObject; export declare const useFeatureReducer: ( recipeImpl: RecipeInterface | undefined, + contactMethod: NormalisedConfig["contactMethod"], userContext: any ) => [SignInUpState, React.Dispatch]; export declare function useChildProps( diff --git a/lib/build/recipe/passwordless/components/themes/mfa/loadingScreen.d.ts b/lib/build/recipe/passwordless/components/themes/mfa/loadingScreen.d.ts new file mode 100644 index 000000000..0c6f7dd5c --- /dev/null +++ b/lib/build/recipe/passwordless/components/themes/mfa/loadingScreen.d.ts @@ -0,0 +1,4 @@ +/// +export declare const LoadingScreen: import("react").ComponentType<{ + children?: import("react").ReactNode; +}>; diff --git a/lib/build/recipe/totp/index.d.ts b/lib/build/recipe/totp/index.d.ts index 764c5f41a..cfc215871 100644 --- a/lib/build/recipe/totp/index.d.ts +++ b/lib/build/recipe/totp/index.d.ts @@ -9,7 +9,7 @@ export default class Wrapper { ): import("../../types").RecipeInitResult< GetRedirectionURLContext, import("./types").PreAndPostAPIHookAction, - OnHandleEventContext, + never, import("./types").NormalisedConfig >; static createDevice(input: { deviceName?: string; options?: RecipeFunctionOptions; userContext?: any }): Promise< diff --git a/lib/build/recipe/totp/types.d.ts b/lib/build/recipe/totp/types.d.ts index 6bac1fc77..a10f3d95e 100644 --- a/lib/build/recipe/totp/types.d.ts +++ b/lib/build/recipe/totp/types.d.ts @@ -37,6 +37,7 @@ export declare type TOTPMFAAction = type: "load"; showBackButton: boolean; deviceInfo: TOTPDeviceInfo | undefined; + showAccessDenied: boolean; error: string | undefined; } | { @@ -50,6 +51,7 @@ export declare type TOTPMFAAction = } | { type: "setError"; + showAccessDenied: boolean; error: string | undefined; maxAttemptCount?: number; currAttemptCount?: number; @@ -74,6 +76,7 @@ export declare type TOTPMFAState = { error: string | undefined; maxAttemptCount?: number; currAttemptCount?: number; + showAccessDenied: boolean; }; export declare type TOTPMFACommonProps = { featureState: TOTPMFAState; diff --git a/lib/build/thirdparty-shared2.js b/lib/build/thirdparty-shared2.js index 854029f67..e191d4ca3 100644 --- a/lib/build/thirdparty-shared2.js +++ b/lib/build/thirdparty-shared2.js @@ -494,6 +494,7 @@ var SignInAndUpCallbackTheme = function (props) { var SignInAndUpCallback$1 = function (props) { var userContext = uiEntry.useUserContext(); + var rethrowInRender = genericComponentOverrideContext.useRethrowInRender(); var verifyCode = React.useCallback( function () { return props.recipe.webJSRecipe.signInAndUp({ @@ -539,19 +540,21 @@ var SignInAndUpCallback$1 = function (props) { redirectToPath = stateResponse === undefined ? undefined : stateResponse.redirectToPath; return [ 2 /*return*/, - recipe$1.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: props.recipe.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: response.createdNewRecipeUser, - user: response.user, - redirectToPath: redirectToPath, + recipe$1.Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: props.recipe.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: response.createdNewRecipeUser, + user: response.user, + redirectToPath: redirectToPath, + }, }, - }, - userContext, - props.history - ), + userContext, + props.history + ) + .catch(rethrowInRender), ]; } return [2 /*return*/]; diff --git a/lib/build/thirdpartypasswordlessprebuiltui.js b/lib/build/thirdpartypasswordlessprebuiltui.js index 18d55201a..46fdbac6a 100644 --- a/lib/build/thirdpartypasswordlessprebuiltui.js +++ b/lib/build/thirdpartypasswordlessprebuiltui.js @@ -384,6 +384,7 @@ var SignInAndUp$1 = function (props) { var userContext = uiEntry.useUserContext(); var _c = passwordlessprebuiltui.useFeatureReducer( (_a = props.recipe.passwordlessRecipe) === null || _a === void 0 ? void 0 : _a.webJSRecipe, + props.recipe.config.passwordlessConfig.contactMethod, userContext ), pwlessState = _c[0], diff --git a/lib/build/totpprebuiltui.js b/lib/build/totpprebuiltui.js index 353b6b9ec..2dcddd280 100644 --- a/lib/build/totpprebuiltui.js +++ b/lib/build/totpprebuiltui.js @@ -184,7 +184,7 @@ var TOTPBlockedScreen = function (props) { return jsxRuntime.jsx( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "container totp" }, + { "data-supertokens": "container totp-mfa blockedScreen" }, { children: jsxRuntime.jsxs( "div", @@ -249,7 +249,7 @@ var TOTPLoadingScreen = function () { return jsxRuntime.jsx( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "container delayedRender totp" }, + { "data-supertokens": "container delayedRender totp-mfa loadingScreen" }, { children: jsxRuntime.jsx( "div", @@ -277,6 +277,7 @@ var CodeForm = uiEntry.withOverride("TOTPCodeForm", function TOTPCodeForm(props) var userContext = uiEntry.useUserContext(); return jsxRuntime.jsx(React__namespace.default.Fragment, { children: jsxRuntime.jsx(formBase.FormBase, { + formDataSupertokens: "totp-mfa codeForm", clearError: props.clearError, onError: props.onError, formFields: [ @@ -365,7 +366,7 @@ var CodeVerificationFooter = uiEntry.withOverride( return jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "footerLinkGroupVert totpMFAVerificationFooter" }, + { "data-supertokens": "footerLinkGroupVert totp-mfa codeVerificationFooter" }, { children: [ claim.loading === false && @@ -413,7 +414,7 @@ var CodeVerificationHeader = uiEntry.withOverride( jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "headerTitle withBackButton" }, + { "data-supertokens": "headerTitle withBackButton totp-mfa codeVerificationHeader" }, { children: [ props.showBackButton @@ -3373,7 +3374,7 @@ var DeviceInfoSection = uiEntry.withOverride("TOTPDeviceInfoSection", function T jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "totpDeviceInfoWithQR" }, + { "data-supertokens": "totpDeviceInfoWithQR totp-mfa deviceInfoSection" }, { children: [ jsxRuntime.jsx(_default, { @@ -3430,7 +3431,7 @@ var DeviceSetupFooter = uiEntry.withOverride("TOTPDeviceSetupFooter", function T return jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "footerLinkGroupVert totpMFASetupFooter" }, + { "data-supertokens": "footerLinkGroupVert totp-mfa deviceSetupFooter" }, { children: [ claim.loading === false && @@ -3475,7 +3476,7 @@ var DeviceSetupHeader = uiEntry.withOverride("TOTPDeviceSetupHeader", function T jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "headerTitle withBackButton" }, + { "data-supertokens": "headerTitle withBackButton totp-mfa deviceSetupHeader" }, { children: [ props.showBackButton @@ -3519,10 +3520,10 @@ var TOTPMFATheme = function (_a) { recipeImplementation: props.recipeImplementation, config: props.config, clearError: function () { - return props.dispatch({ type: "setError", error: undefined }); + return props.dispatch({ type: "setError", showAccessDenied: false, error: undefined }); }, onError: function (error) { - return props.dispatch({ type: "setError", error: error }); + return props.dispatch({ type: "setError", showAccessDenied: false, error: error }); }, }; return activeScreen === TOTPMFAScreens.Blocked @@ -3538,7 +3539,7 @@ var TOTPMFATheme = function (_a) { : jsxRuntime.jsxs( "div", genericComponentOverrideContext.__assign( - { "data-supertokens": "container totp" }, + { "data-supertokens": "container totp-mfa" }, { children: [ jsxRuntime.jsx( @@ -3662,7 +3663,7 @@ function getActiveScreen(props) { return TOTPMFAScreens.Blocked; } else if (props.featureState.loaded === false) { return TOTPMFAScreens.Loading; - } else if (props.featureState.error === "TOTP_MFA_NOT_ALLOWED_TO_SETUP") { + } else if (props.featureState.showAccessDenied) { return TOTPMFAScreens.AccessDenied; } else if (props.featureState.deviceInfo) { return TOTPMFAScreens.DeviceSetup; @@ -3727,6 +3728,7 @@ var useFeatureReducer = function () { error: action.error, deviceInfo: action.deviceInfo, showBackButton: action.showBackButton, + showAccessDenied: action.showAccessDenied, isBlocked: false, showSecret: false, }; @@ -3738,7 +3740,7 @@ var useFeatureReducer = function () { case "setError": return genericComponentOverrideContext.__assign( genericComponentOverrideContext.__assign({}, oldState), - { error: action.error } + { loaded: true, showAccessDenied: action.showAccessDenied, error: action.error } ); case "createDevice": return genericComponentOverrideContext.__assign( @@ -3772,6 +3774,7 @@ var useFeatureReducer = function () { showSecret: false, isBlocked: false, showBackButton: false, + showAccessDenied: false, }, function (initArg) { var error = undefined; @@ -3804,7 +3807,7 @@ function useOnLoad(recipeImpl, dispatch, userContext) { ); var handleLoadError = React__namespace.useCallback( function () { - return dispatch({ type: "setError", error: "SOMETHING_WENT_WRONG_ERROR" }); + return dispatch({ type: "setError", showAccessDenied: true, error: "SOMETHING_WENT_WRONG_ERROR" }); }, [dispatch] ); @@ -3821,9 +3824,9 @@ function useOnLoad(recipeImpl, dispatch, userContext) { mfaClaim, nextLength, showBackButton; - var _a; - return genericComponentOverrideContext.__generator(this, function (_b) { - switch (_b.label) { + var _b; + return genericComponentOverrideContext.__generator(this, function (_c) { + switch (_c.label) { case 0: error = undefined; errorQueryParam = genericComponentOverrideContext.getQueryParams("error"); @@ -3838,6 +3841,7 @@ function useOnLoad(recipeImpl, dispatch, userContext) { type: "load", deviceInfo: undefined, showBackButton: true, + showAccessDenied: true, error: "TOTP_MFA_NOT_ALLOWED_TO_SETUP", }); return [2 /*return*/]; @@ -3847,21 +3851,45 @@ function useOnLoad(recipeImpl, dispatch, userContext) { type: "load", deviceInfo: undefined, showBackButton: true, + showAccessDenied: true, error: "TOTP_MFA_NOT_ALLOWED_TO_SETUP", }); return [2 /*return*/]; } - if (!(isAllowedToSetup && (doSetup || !isAlreadySetup))) return [3 /*break*/, 2]; - return [4 /*yield*/, recipeImpl.createDevice({ userContext: userContext })]; + if (!(isAllowedToSetup && (doSetup || !isAlreadySetup))) return [3 /*break*/, 5]; + createResp = void 0; + _c.label = 1; case 1: - createResp = _b.sent(); - if ((createResp === null || createResp === void 0 ? void 0 : createResp.status) !== "OK") { - throw new Error("TOTP device creation failed with duplicate name; should never happen"); + _c.trys.push([1, 3, , 4]); + return [4 /*yield*/, recipeImpl.createDevice({ userContext: userContext })]; + case 2: + createResp = _c.sent(); + return [3 /*break*/, 4]; + case 3: + _c.sent(); + dispatch({ + type: "load", + deviceInfo: undefined, + showBackButton: true, + showAccessDenied: true, + error: "SOMETHING_WENT_WRONG_ERROR", + }); + return [2 /*return*/]; + case 4: + if (createResp.status !== "OK") { + dispatch({ + type: "load", + deviceInfo: undefined, + showBackButton: true, + showAccessDenied: true, + error: "SOMETHING_WENT_WRONG_ERROR", + }); + return [2 /*return*/]; } deviceInfo = genericComponentOverrideContext.__assign({}, createResp); delete deviceInfo.status; - _b.label = 2; - case 2: + _c.label = 5; + case 5: return [ 4 /*yield*/, recipe$1.Session.getInstanceOrThrow().getClaimValue({ @@ -3869,12 +3897,12 @@ function useOnLoad(recipeImpl, dispatch, userContext) { userContext: userContext, }), ]; - case 3: - mfaClaim = _b.sent(); + case 6: + mfaClaim = _c.sent(); nextLength = - (_a = mfaClaim === null || mfaClaim === void 0 ? void 0 : mfaClaim.n.length) !== null && - _a !== void 0 - ? _a + (_b = mfaClaim === null || mfaClaim === void 0 ? void 0 : mfaClaim.n.length) !== null && + _b !== void 0 + ? _b : 0; showBackButton = nextLength === 0; // No need to check if the component is unmounting, since this has no effect then. @@ -3883,6 +3911,7 @@ function useOnLoad(recipeImpl, dispatch, userContext) { deviceInfo: deviceInfo, error: error, showBackButton: showBackButton, + showAccessDenied: false, }); return [2 /*return*/]; } @@ -3895,6 +3924,7 @@ function useOnLoad(recipeImpl, dispatch, userContext) { } function useChildProps(recipe$2, recipeImplementation, state, dispatch, userContext, history) { var _this = this; + var rethrowInRender = genericComponentOverrideContext.useRethrowInRender(); return React.useMemo( function () { return { @@ -3945,13 +3975,7 @@ function useChildProps(recipe$2, recipeImplementation, state, dispatch, userCont return genericComponentOverrideContext.__generator(this, function (_a) { switch (_a.label) { case 0: - return [ - 4 /*yield*/, - recipe$1.Session.getInstanceOrThrow().signOut({ userContext: userContext }), - ]; - case 1: - _a.sent(); - if (!state.deviceInfo) return [3 /*break*/, 3]; + if (!state.deviceInfo) return [3 /*break*/, 2]; return [ 4 /*yield*/, recipeImplementation.removeDevice({ @@ -3959,10 +3983,16 @@ function useChildProps(recipe$2, recipeImplementation, state, dispatch, userCont userContext: userContext, }), ]; - case 2: + case 1: _a.sent(); - _a.label = 3; + _a.label = 2; + case 2: + return [ + 4 /*yield*/, + recipe$1.Session.getInstanceOrThrow().signOut({ userContext: userContext }), + ]; case 3: + _a.sent(); return [ 4 /*yield*/, uiEntry.redirectToAuth({ redirectBack: false, history: history }), @@ -3990,11 +4020,9 @@ function useChildProps(recipe$2, recipeImplementation, state, dispatch, userCont userContext: userContext, }, }; - return recipe$1.Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - redirectInfo, - userContext, - history - ); + return recipe$1.Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection(redirectInfo, userContext, history) + .catch(rethrowInRender); }, recipeImplementation: recipeImplementation, config: recipe$2.config, @@ -4100,6 +4128,7 @@ function getModifiedRecipeImplementation(originalImpl, dispatch) { dispatch({ type: "setError", error: "ERROR_TOTP_INVALID_CODE", + showAccessDenied: false, maxAttemptCount: res.maxNumberOfFailedAttempts, currAttemptCount: res.currentNumberOfFailedAttempts, }); @@ -4130,6 +4159,7 @@ function getModifiedRecipeImplementation(originalImpl, dispatch) { dispatch({ type: "setError", error: "ERROR_TOTP_INVALID_CODE", + showAccessDenied: false, maxAttemptCount: res.maxNumberOfFailedAttempts, currAttemptCount: res.currentNumberOfFailedAttempts, }); diff --git a/lib/build/utils.d.ts b/lib/build/utils.d.ts index 315ccce60..16f112348 100644 --- a/lib/build/utils.d.ts +++ b/lib/build/utils.d.ts @@ -1,3 +1,4 @@ +/// import NormalisedURLDomain from "supertokens-web-js/utils/normalisedURLDomain"; import NormalisedURLPath from "supertokens-web-js/utils/normalisedURLPath"; import type { FormFieldError } from "./recipe/emailpassword/types"; @@ -51,3 +52,4 @@ export declare const useOnMountAPICall: ( handleError?: ((err: unknown, consumeResp: T | undefined) => void) | undefined, startLoading?: boolean ) => void; +export declare function useRethrowInRender(): import("react").Dispatch>; diff --git a/lib/ts/recipe/authRecipe/authWidgetWrapper.tsx b/lib/ts/recipe/authRecipe/authWidgetWrapper.tsx index 052000fe9..47f06cf4a 100644 --- a/lib/ts/recipe/authRecipe/authWidgetWrapper.tsx +++ b/lib/ts/recipe/authRecipe/authWidgetWrapper.tsx @@ -19,7 +19,7 @@ import React, { useEffect, useContext, useState } from "react"; import { useUserContext } from "../../usercontext"; -import { getRedirectToPathFromURL } from "../../utils"; +import { getRedirectToPathFromURL, useRethrowInRender } from "../../utils"; import { SessionAuth, SessionContext } from "../session"; import Session from "../session/recipe"; @@ -60,6 +60,7 @@ const Redirector = { // we want to do this just once, so we supply it with only the loading state. // if we supply it with props, sessionContext, then once the user signs in, then this will route the @@ -72,19 +73,21 @@ const Redirector = recipe && getModifiedRecipeImplementation(recipe.webJSRecipe), [recipe]); const userContext = useUserContext(); + const rethrowInRender = useRethrowInRender(); const onSignInSuccess = useCallback( async (response: { user: User }): Promise => { - return Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: recipe!.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: false, - user: response.user, - redirectToPath: getRedirectToPathFromURL(), + return Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: recipe!.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: false, + user: response.user, + redirectToPath: getRedirectToPathFromURL(), + }, }, - }, - userContext, - history - ); + userContext, + history + ) + .catch(rethrowInRender); }, [recipe, userContext, history] ); const onSignUpSuccess = useCallback( async (response: { user: User }): Promise => { - return Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: recipe!.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: true, - user: response.user, - redirectToPath: getRedirectToPathFromURL(), + return Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: recipe!.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: true, + user: response.user, + redirectToPath: getRedirectToPathFromURL(), + }, }, - }, - userContext, - history - ); + userContext, + history + ) + .catch(rethrowInRender); }, [recipe, userContext, history] ); diff --git a/lib/ts/recipe/emailpassword/components/library/formBase.tsx b/lib/ts/recipe/emailpassword/components/library/formBase.tsx index 1dabbd92a..ffa43eb4b 100644 --- a/lib/ts/recipe/emailpassword/components/library/formBase.tsx +++ b/lib/ts/recipe/emailpassword/components/library/formBase.tsx @@ -181,7 +181,7 @@ export const FormBase: React.FC> = (props) => { ); return ( -
+ {formFields.map((field) => { let type = "text"; // If email or password, replace field type. diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index 253329077..003d4afbc 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -391,6 +391,7 @@ export type EnterEmailStatus = "READY" | "SENT"; export type SubmitNewPasswordStatus = "READY" | "SUCCESS"; export type FormBaseProps = { + formDataSupertokens?: string; footer?: JSX.Element; formFields: FormFieldThemeProps[]; diff --git a/lib/ts/recipe/emailverification/components/features/emailVerification/index.tsx b/lib/ts/recipe/emailverification/components/features/emailVerification/index.tsx index 81fcab665..8b561e776 100644 --- a/lib/ts/recipe/emailverification/components/features/emailVerification/index.tsx +++ b/lib/ts/recipe/emailverification/components/features/emailVerification/index.tsx @@ -23,7 +23,7 @@ import { redirectToAuth } from "../../../../.."; import { ComponentOverrideContext } from "../../../../../components/componentOverride/componentOverrideContext"; import FeatureWrapper from "../../../../../components/featureWrapper"; import { useUserContext } from "../../../../../usercontext"; -import { clearQueryParams, getQueryParams, useOnMountAPICall } from "../../../../../utils"; +import { clearQueryParams, getQueryParams, useOnMountAPICall, useRethrowInRender } from "../../../../../utils"; import { SessionContext } from "../../../../session"; import Session from "../../../../session/recipe"; import EmailVerificationTheme from "../../themes/emailVerification"; @@ -40,6 +40,7 @@ export const EmailVerification: React.FC = (props) => { const sessionContext = useContext(SessionContext); const [status, setStatus] = useState("LOADING"); const userContext = useUserContext(); + const rethrowInRender = useRethrowInRender(); const recipeComponentOverrides = props.useComponentOverrides(); const redirectToAuthWithHistory = useCallback(async () => { @@ -59,11 +60,9 @@ export const EmailVerification: React.FC = (props) => { ); const onSuccess = useCallback(async () => { - return Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - undefined, - userContext, - props.history - ); + return Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection(undefined, userContext, props.history) + .catch(rethrowInRender); }, [props.recipe, props.history, userContext]); const fetchIsEmailVerified = useCallback(async () => { diff --git a/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts b/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts index 347e2e9a4..d9e6862c1 100644 --- a/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts +++ b/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts @@ -43,7 +43,7 @@ export class MultiFactorAuthClaimClass { this.id = this.webJSClaim.id; const defaultOnFailureRedirection = ({ reason, userContext }: any) => { - if (reason.nextFactorOptions) { + if (reason.nextFactorOptions !== undefined) { if (reason.nextFactorOptions.length === 1) { return getRedirectURL( { action: "GO_TO_FACTOR", factorId: reason.nextFactorOptions[0] }, @@ -53,7 +53,12 @@ export class MultiFactorAuthClaimClass { return getRedirectURL({ action: "FACTOR_CHOOSER" }, userContext); } } - return getRedirectURL({ action: "GO_TO_FACTOR", factorId: reason.factorId }, userContext); + if (reason.factorId !== undefined) { + return getRedirectURL({ action: "GO_TO_FACTOR", factorId: reason.factorId }, userContext); + } + + // This should basically never happen, but it will show the access denied screen + return undefined; }; this.validators = { diff --git a/lib/ts/recipe/passwordless/components/features/linkClickedScreen/index.tsx b/lib/ts/recipe/passwordless/components/features/linkClickedScreen/index.tsx index 99181dee9..53da90c7d 100644 --- a/lib/ts/recipe/passwordless/components/features/linkClickedScreen/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/linkClickedScreen/index.tsx @@ -23,7 +23,7 @@ import { ComponentOverrideContext } from "../../../../../components/componentOve import FeatureWrapper from "../../../../../components/featureWrapper"; import SuperTokens from "../../../../../superTokens"; import { useUserContext } from "../../../../../usercontext"; -import { getQueryParams, getURLHash, useOnMountAPICall } from "../../../../../utils"; +import { getQueryParams, getURLHash, useOnMountAPICall, useRethrowInRender } from "../../../../../utils"; import Session from "../../../../session/recipe"; import { LinkClickedScreen as LinkClickedScreenTheme } from "../../themes/linkClickedScreen"; import { defaultTranslationsPasswordless } from "../../themes/translations"; @@ -37,6 +37,7 @@ type PropType = FeatureBaseProps & { recipe: Recipe; useComponentOverrides: () = const LinkClickedScreen: React.FC = (props) => { const userContext = useUserContext(); + const rethrowInRender = useRethrowInRender(); const [requireUserInteraction, setRequireUserInteraction] = useState(false); const consumeCodeAtMount = useCallback(async () => { @@ -104,19 +105,21 @@ const LinkClickedScreen: React.FC = (props) => { await props.recipe.webJSRecipe.clearLoginAttemptInfo({ userContext, }); - return Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: props.recipe.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: response.createdNewRecipeUser, - user: response.user, - redirectToPath: loginAttemptInfo?.redirectToPath, + return Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: props.recipe.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: response.createdNewRecipeUser, + user: response.user, + redirectToPath: loginAttemptInfo?.redirectToPath, + }, }, - }, - userContext, - props.history - ); + userContext, + props.history + ) + .catch(rethrowInRender); } }, [props.history, props.recipe] diff --git a/lib/ts/recipe/passwordless/components/features/mfa/index.tsx b/lib/ts/recipe/passwordless/components/features/mfa/index.tsx index 000a8e2eb..e078c8f0e 100644 --- a/lib/ts/recipe/passwordless/components/features/mfa/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/mfa/index.tsx @@ -31,10 +31,13 @@ import { getQueryParams, getRedirectToPathFromURL, useOnMountAPICall, + useRethrowInRender, } from "../../../../../utils"; import MultiFactorAuth from "../../../../multifactorauth/recipe"; +import { useDynamicLoginMethods } from "../../../../multitenancy/dynamicLoginMethodsContext"; import SessionRecipe from "../../../../session/recipe"; import { getPhoneNumberUtils } from "../../../phoneNumberUtils"; +import { getEnabledContactMethods } from "../../../utils"; import MFAThemeWrapper from "../../themes/mfa"; import { defaultTranslationsPasswordless } from "../../themes/translations"; @@ -159,6 +162,7 @@ export function useChildProps( userContext: any, history: any ): MFAChildProps { + const rethrowInRender = useRethrowInRender(); return useMemo(() => { return { onSuccess: () => { @@ -176,11 +180,9 @@ export function useChildProps( }, }; - return SessionRecipe.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - redirectInfo, - userContext, - history - ); + return SessionRecipe.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection(redirectInfo, userContext, history) + .catch(rethrowInRender); }, onSignOutClicked: async () => { await SessionRecipe.getInstanceOrThrow().signOut({ userContext }); @@ -209,7 +211,7 @@ export function useChildProps( }, [contactMethod, state, recipeImplementation]); } -export const MFAFeature: React.FC< +const MFAFeatureInner: React.FC< FeatureBaseProps & { contactMethod: "PHONE" | "EMAIL"; flowType: PasswordlessFlowType; @@ -217,7 +219,6 @@ export const MFAFeature: React.FC< useComponentOverrides: () => ComponentOverrideMap; } > = (props) => { - const recipeComponentOverrides = props.useComponentOverrides(); const userContext = useUserContext(); const callingConsumeCodeRef = useRef(false); @@ -246,30 +247,45 @@ export const MFAFeature: React.FC< )!; useSuccessInAnotherTabChecker(callingConsumeCodeRef, recipeImplementation, state, dispatch, userContext); + return ( + + {/* No custom theme, use default. */} + {props.children === undefined && ( + + )} + + {/* Otherwise, custom theme is provided, propagate props. */} + {props.children && + React.Children.map(props.children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + ...childProps, + featureState: state, + dispatch: dispatch, + }); + } + return child; + })} + + ); +}; + +export const MFAFeature: React.FC< + FeatureBaseProps & { + contactMethod: "PHONE" | "EMAIL"; + flowType: PasswordlessFlowType; + recipe: Recipe; + useComponentOverrides: () => ComponentOverrideMap; + } +> = (props) => { + const recipeComponentOverrides = props.useComponentOverrides(); + return ( - - {/* No custom theme, use default. */} - {props.children === undefined && ( - - )} - - {/* Otherwise, custom theme is provided, propagate props. */} - {props.children && - React.Children.map(props.children, (child) => { - if (React.isValidElement(child)) { - return React.cloneElement(child, { - ...childProps, - featureState: state, - dispatch: dispatch, - }); - } - return child; - })} - + ); @@ -298,6 +314,7 @@ function useOnLoad( () => dispatch({ type: "setError", error: "SOMETHING_WENT_WRONG_ERROR" }), [dispatch] ); + const dynamicLoginMethods = useDynamicLoginMethods(); const onLoad = React.useCallback( async (mfaInfo: { factors: MFAFactorInfo; email?: string; phoneNumber?: string }) => { let error: string | undefined = undefined; @@ -306,10 +323,11 @@ function useOnLoad( if (errorQueryParam !== null) { error = "SOMETHING_WENT_WRONG_ERROR"; } - const loginAttemptInfo = - await recipeImplementation.getLoginAttemptInfo({ + let loginAttemptInfo = await recipeImplementation.getLoginAttemptInfo( + { userContext, - }); + } + ); const isAlreadySetup = props.contactMethod === "EMAIL" @@ -320,17 +338,31 @@ function useOnLoad( ? mfaInfo.factors.isAllowedToSetup.includes("otp-email") : mfaInfo.factors.isAllowedToSetup.includes("otp-phone"); + const enabledContactMethods = getEnabledContactMethods( + props.recipe.config.contactMethod, + dynamicLoginMethods + ); + if (loginAttemptInfo && !enabledContactMethods.includes(loginAttemptInfo.contactMethod)) { + await recipeImplementation?.clearLoginAttemptInfo({ userContext }); + loginAttemptInfo = undefined; + } + if (!loginAttemptInfo) { if (props.contactMethod === "EMAIL") { if (isAlreadySetup && doSetup !== "true") { + let createResp; try { // createCode also dispatches the necessary events - await recipeImplementation!.createCode({ + createResp = await recipeImplementation!.createCode({ email: mfaInfo.email!, // We can assume this is set here, since the mfaInfo states that otp-email has been set up userContext, }); } catch (err) { dispatch({ type: "setError", error: "SOMETHING_WENT_WRONG_ERROR" }); + return; + } + if (createResp?.status !== "OK") { + dispatch({ type: "setError", error: "SOMETHING_WENT_WRONG_ERROR" }); } } else if (!mfaInfo.factors.isAllowedToSetup.includes("otp-email")) { dispatch({ diff --git a/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx b/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx index 49eb6f92f..f22ba625e 100644 --- a/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx +++ b/lib/ts/recipe/passwordless/components/features/signInAndUp/index.tsx @@ -24,10 +24,17 @@ import { useEffect } from "react"; import { ComponentOverrideContext } from "../../../../../components/componentOverride/componentOverrideContext"; import FeatureWrapper from "../../../../../components/featureWrapper"; import { useUserContext } from "../../../../../usercontext"; -import { clearErrorQueryParam, getQueryParams, getRedirectToPathFromURL } from "../../../../../utils"; +import { + clearErrorQueryParam, + getQueryParams, + getRedirectToPathFromURL, + useRethrowInRender, +} from "../../../../../utils"; +import { useDynamicLoginMethods } from "../../../../multitenancy/dynamicLoginMethodsContext"; import Session from "../../../../session"; import SessionRecipe from "../../../../session/recipe"; import { getPhoneNumberUtils } from "../../../phoneNumberUtils"; +import { getEnabledContactMethods } from "../../../utils"; import SignInUpThemeWrapper from "../../themes/signInUp"; import { defaultTranslationsPasswordless } from "../../themes/translations"; @@ -72,8 +79,10 @@ export const useSuccessInAnotherTabChecker = ( export const useFeatureReducer = ( recipeImpl: RecipeInterface | undefined, + contactMethod: NormalisedConfig["contactMethod"], userContext: any ): [SignInUpState, React.Dispatch] => { + const dynamicLoginMethods = useDynamicLoginMethods(); const [state, dispatch] = React.useReducer( (oldState: SignInUpState, action: PasswordlessSignInUpAction) => { switch (action.type) { @@ -164,9 +173,14 @@ export const useFeatureReducer = ( error = messageQueryParam; } } - const loginAttemptInfo = await recipeImpl?.getLoginAttemptInfo({ + let loginAttemptInfo = await recipeImpl?.getLoginAttemptInfo({ userContext, }); + const enabledContactMethods = getEnabledContactMethods(contactMethod, dynamicLoginMethods); + if (loginAttemptInfo && !enabledContactMethods.includes(loginAttemptInfo.contactMethod)) { + await recipeImpl?.clearLoginAttemptInfo({ userContext }); + loginAttemptInfo = undefined; + } // No need to check if the component is unmounting, since this has no effect then. dispatch({ type: "load", loginAttemptInfo, error }); } @@ -209,6 +223,7 @@ export function useChildProps( getModifiedRecipeImplementation(recipe.webJSRecipe, recipe.config, dispatch, callingConsumeCodeRef), [recipe] ); + const rethrowInRender = useRethrowInRender(); return useMemo(() => { if (!recipe || !recipeImplementation) { @@ -216,19 +231,21 @@ export function useChildProps( } return { onSuccess: (result: { createdNewRecipeUser: boolean; user: User }) => { - return SessionRecipe.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: recipe.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: result.createdNewRecipeUser, - user: result.user, - redirectToPath: getRedirectToPathFromURL(), + return SessionRecipe.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: recipe.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: result.createdNewRecipeUser, + user: result.user, + redirectToPath: getRedirectToPathFromURL(), + }, }, - }, - userContext, - history - ); + userContext, + history + ) + .catch(rethrowInRender); }, recipeImplementation: recipeImplementation, config: recipe.config, @@ -236,42 +253,58 @@ export function useChildProps( }, [state, recipeImplementation]); } -export const SignInUpFeature: React.FC< +const SignInUpFeatureInner: React.FC< FeatureBaseProps & { recipe: Recipe; useComponentOverrides: () => ComponentOverrideMap; } > = (props) => { - const recipeComponentOverrides = props.useComponentOverrides(); const userContext = useUserContext(); - const [state, dispatch] = useFeatureReducer(props.recipe.webJSRecipe, userContext); + const [state, dispatch] = useFeatureReducer( + props.recipe.webJSRecipe, + props.recipe.config.contactMethod, + userContext + ); const callingConsumeCodeRef = useSuccessInAnotherTabChecker(state, dispatch, userContext); const childProps = useChildProps(props.recipe, dispatch, state, callingConsumeCodeRef, userContext, props.history)!; + return ( + + {/* No custom theme, use default. */} + {props.children === undefined && ( + + )} + + {/* Otherwise, custom theme is provided, propagate props. */} + {props.children && + React.Children.map(props.children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + ...childProps, + featureState: state, + dispatch: dispatch, + }); + } + return child; + })} + + ); +}; + +export const SignInUpFeature: React.FC< + FeatureBaseProps & { + recipe: Recipe; + useComponentOverrides: () => ComponentOverrideMap; + } +> = (props) => { + const recipeComponentOverrides = props.useComponentOverrides(); + return ( - - {/* No custom theme, use default. */} - {props.children === undefined && ( - - )} - - {/* Otherwise, custom theme is provided, propagate props. */} - {props.children && - React.Children.map(props.children, (child) => { - if (React.isValidElement(child)) { - return React.cloneElement(child, { - ...childProps, - featureState: state, - dispatch: dispatch, - }); - } - return child; - })} - + ); diff --git a/lib/ts/recipe/passwordless/components/themes/mfa/index.tsx b/lib/ts/recipe/passwordless/components/themes/mfa/index.tsx index 75c608b97..bf3432bcc 100644 --- a/lib/ts/recipe/passwordless/components/themes/mfa/index.tsx +++ b/lib/ts/recipe/passwordless/components/themes/mfa/index.tsx @@ -26,6 +26,7 @@ import { PhoneForm } from "../signInUp/phoneForm"; import { UserInputCodeForm } from "../signInUp/userInputCodeForm"; import { ThemeBase } from "../themeBase"; +import { LoadingScreen } from "./loadingScreen"; import { MFAFooter } from "./mfaFooter"; import { MFAHeader } from "./mfaHeader"; import { MFAOTPFooter } from "./mfaOTPFooter"; @@ -57,7 +58,7 @@ const MFATheme: React.FC = ({ }; if (!featureState.loaded) { - return null; + return ; } return activeScreen === MFAScreens.CloseTab ? ( @@ -65,7 +66,7 @@ const MFATheme: React.FC = ({ ) : activeScreen === MFAScreens.AccessDenied ? ( ) : ( -
+
{ diff --git a/lib/ts/recipe/passwordless/components/themes/mfa/loadingScreen.tsx b/lib/ts/recipe/passwordless/components/themes/mfa/loadingScreen.tsx new file mode 100644 index 000000000..f73385883 --- /dev/null +++ b/lib/ts/recipe/passwordless/components/themes/mfa/loadingScreen.tsx @@ -0,0 +1,30 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import SpinnerIcon from "../../../../../components/assets/spinnerIcon"; +import { withOverride } from "../../../../../components/componentOverride/withOverride"; + +const OTPLoadingScreen: React.FC = () => { + return ( +
+
+
+ +
+
+
+ ); +}; + +export const LoadingScreen = withOverride("OTPLoadingScreen", OTPLoadingScreen); diff --git a/lib/ts/recipe/passwordless/components/themes/mfa/mfaFooter.tsx b/lib/ts/recipe/passwordless/components/themes/mfa/mfaFooter.tsx index 8d9f9636e..e2b83c724 100644 --- a/lib/ts/recipe/passwordless/components/themes/mfa/mfaFooter.tsx +++ b/lib/ts/recipe/passwordless/components/themes/mfa/mfaFooter.tsx @@ -28,7 +28,7 @@ export const MFAFooter = withOverride( const claim = useClaimValue(MultiFactorAuthClaim); return ( -
+
{claim.loading === false && (claim.value?.n.length ?? 0) > 1 && (
{t("PWLESS_MFA_FOOTER_CHOOSER_ANOTHER")} diff --git a/lib/ts/recipe/passwordless/components/themes/mfa/mfaHeader.tsx b/lib/ts/recipe/passwordless/components/themes/mfa/mfaHeader.tsx index c2ac7399b..bbc779a6f 100644 --- a/lib/ts/recipe/passwordless/components/themes/mfa/mfaHeader.tsx +++ b/lib/ts/recipe/passwordless/components/themes/mfa/mfaHeader.tsx @@ -31,7 +31,7 @@ export const MFAHeader = withOverride( return ( -
+
{claim.loading === false && claim.value?.n.length === 0 ? ( ) : ( diff --git a/lib/ts/recipe/passwordless/components/themes/mfa/mfaOTPFooter.tsx b/lib/ts/recipe/passwordless/components/themes/mfa/mfaOTPFooter.tsx index 0a04ddc6e..695ee9501 100644 --- a/lib/ts/recipe/passwordless/components/themes/mfa/mfaOTPFooter.tsx +++ b/lib/ts/recipe/passwordless/components/themes/mfa/mfaOTPFooter.tsx @@ -35,7 +35,7 @@ export const MFAOTPFooter = withOverride( const userContext = useUserContext(); return ( -
+
{isSetupAllowed ? (
-
+
{claim.loading === false && claim.value?.n.length === 0 && isSetupAllowed === false ? ( ) : ( diff --git a/lib/ts/recipe/thirdparty/components/features/signInAndUpCallback/index.tsx b/lib/ts/recipe/thirdparty/components/features/signInAndUpCallback/index.tsx index 689eae38e..0595103df 100644 --- a/lib/ts/recipe/thirdparty/components/features/signInAndUpCallback/index.tsx +++ b/lib/ts/recipe/thirdparty/components/features/signInAndUpCallback/index.tsx @@ -22,7 +22,7 @@ import { ComponentOverrideContext } from "../../../../../components/componentOve import FeatureWrapper from "../../../../../components/featureWrapper"; import SuperTokens from "../../../../../superTokens"; import { useUserContext } from "../../../../../usercontext"; -import { useOnMountAPICall } from "../../../../../utils"; +import { useOnMountAPICall, useRethrowInRender } from "../../../../../utils"; import Session from "../../../../session/recipe"; import { SignInAndUpCallbackTheme } from "../../themes/signInAndUpCallback"; import { defaultTranslationsThirdParty } from "../../themes/translations"; @@ -35,6 +35,7 @@ type PropType = FeatureBaseProps & { recipe: Recipe; useComponentOverrides: () = const SignInAndUpCallback: React.FC = (props) => { const userContext = useUserContext(); + const rethrowInRender = useRethrowInRender(); const verifyCode = useCallback(() => { return props.recipe.webJSRecipe.signInAndUp({ @@ -71,19 +72,21 @@ const SignInAndUpCallback: React.FC = (props) => { }); const redirectToPath = stateResponse === undefined ? undefined : stateResponse.redirectToPath; - return Session.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - { - rid: props.recipe.config.recipeId, - successRedirectContext: { - action: "SUCCESS", - isNewRecipeUser: response.createdNewRecipeUser, - user: response.user, - redirectToPath, + return Session.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection( + { + rid: props.recipe.config.recipeId, + successRedirectContext: { + action: "SUCCESS", + isNewRecipeUser: response.createdNewRecipeUser, + user: response.user, + redirectToPath, + }, }, - }, - userContext, - props.history - ); + userContext, + props.history + ) + .catch(rethrowInRender); } }, [props.recipe, props.history, userContext] diff --git a/lib/ts/recipe/thirdpartypasswordless/components/features/signInAndUp/index.tsx b/lib/ts/recipe/thirdpartypasswordless/components/features/signInAndUp/index.tsx index 561b30f16..af3fa0e05 100644 --- a/lib/ts/recipe/thirdpartypasswordless/components/features/signInAndUp/index.tsx +++ b/lib/ts/recipe/thirdpartypasswordless/components/features/signInAndUp/index.tsx @@ -49,6 +49,7 @@ const SignInAndUp: React.FC = (props) => { const userContext = useUserContext(); const [pwlessState, pwlessDispatch] = usePasswordlessFeatureReducer( props.recipe.passwordlessRecipe?.webJSRecipe, + props.recipe.config.passwordlessConfig.contactMethod, userContext ); diff --git a/lib/ts/recipe/totp/components/features/mfa/index.tsx b/lib/ts/recipe/totp/components/features/mfa/index.tsx index 072fc1c75..475bc2952 100644 --- a/lib/ts/recipe/totp/components/features/mfa/index.tsx +++ b/lib/ts/recipe/totp/components/features/mfa/index.tsx @@ -24,7 +24,7 @@ import { redirectToAuth } from "../../../../.."; import { ComponentOverrideContext } from "../../../../../components/componentOverride/componentOverrideContext"; import FeatureWrapper from "../../../../../components/featureWrapper"; import { useUserContext } from "../../../../../usercontext"; -import { getQueryParams, getRedirectToPathFromURL, useOnMountAPICall } from "../../../../../utils"; +import { getQueryParams, getRedirectToPathFromURL, useOnMountAPICall, useRethrowInRender } from "../../../../../utils"; import { MultiFactorAuthClaim } from "../../../../multifactorauth"; import MultiFactorAuth from "../../../../multifactorauth/recipe"; import SessionRecipe from "../../../../session/recipe"; @@ -53,6 +53,7 @@ export const useFeatureReducer = (): [TOTPMFAState, React.Dispatch { let error: string | undefined = undefined; @@ -123,7 +127,7 @@ function useOnLoad(recipeImpl: RecipeInterface, dispatch: React.Dispatch dispatch({ type: "setError", error: "SOMETHING_WENT_WRONG_ERROR" }), + () => dispatch({ type: "setError", showAccessDenied: true, error: "SOMETHING_WENT_WRONG_ERROR" }), [dispatch] ); const onLoad = React.useCallback( @@ -141,6 +145,7 @@ function useOnLoad(recipeImpl: RecipeInterface, dispatch: React.Dispatch { return { onShowSecretClick: () => { @@ -218,10 +245,10 @@ export function useChildProps( dispatch({ type: "restartFlow", error: undefined }); }, onSignOutClicked: async () => { - await SessionRecipe.getInstanceOrThrow().signOut({ userContext }); if (state.deviceInfo) { await recipeImplementation.removeDevice({ deviceName: state.deviceInfo.deviceName, userContext }); } + await SessionRecipe.getInstanceOrThrow().signOut({ userContext }); await redirectToAuth({ redirectBack: false, history: history }); }, onFactorChooserButtonClicked: () => { @@ -241,11 +268,9 @@ export function useChildProps( }, }; - return SessionRecipe.getInstanceOrThrow().validateGlobalClaimsAndHandleSuccessRedirection( - redirectInfo, - userContext, - history - ); + return SessionRecipe.getInstanceOrThrow() + .validateGlobalClaimsAndHandleSuccessRedirection(redirectInfo, userContext, history) + .catch(rethrowInRender); }, recipeImplementation: recipeImplementation, config: recipe.config, @@ -330,6 +355,7 @@ function getModifiedRecipeImplementation( dispatch({ type: "setError", error: "ERROR_TOTP_INVALID_CODE", + showAccessDenied: false, maxAttemptCount: res.maxNumberOfFailedAttempts, currAttemptCount: res.currentNumberOfFailedAttempts, }); @@ -353,6 +379,7 @@ function getModifiedRecipeImplementation( dispatch({ type: "setError", error: "ERROR_TOTP_INVALID_CODE", + showAccessDenied: false, maxAttemptCount: res.maxNumberOfFailedAttempts, currAttemptCount: res.currentNumberOfFailedAttempts, }); diff --git a/lib/ts/recipe/totp/components/themes/mfa/blockedScreen.tsx b/lib/ts/recipe/totp/components/themes/mfa/blockedScreen.tsx index c8f0de8bc..1dbecbebd 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/blockedScreen.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/blockedScreen.tsx @@ -26,7 +26,7 @@ const TOTPBlockedScreen: React.FC<{ nextRetryAt: number; onRetry: () => void; on const t = useTranslation(); return ( -
+
{t("TOTP_BLOCKED_TITLE")}
diff --git a/lib/ts/recipe/totp/components/themes/mfa/index.tsx b/lib/ts/recipe/totp/components/themes/mfa/index.tsx index bdf947bde..54792b863 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/index.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/index.tsx @@ -52,8 +52,8 @@ const TOTPMFATheme: React.FC = featureState, recipeImplementation: props.recipeImplementation, config: props.config, - clearError: () => props.dispatch({ type: "setError", error: undefined }), - onError: (error: string) => props.dispatch({ type: "setError", error }), + clearError: () => props.dispatch({ type: "setError", showAccessDenied: false, error: undefined }), + onError: (error: string) => props.dispatch({ type: "setError", showAccessDenied: false, error }), }; return activeScreen === TOTPMFAScreens.Blocked ? ( @@ -67,7 +67,7 @@ const TOTPMFATheme: React.FC = ) : activeScreen === TOTPMFAScreens.Loading ? ( ) : ( -
+
{featureState.loaded && ( @@ -154,7 +154,7 @@ export function getActiveScreen(props: Pick { return ( -
+
diff --git a/lib/ts/recipe/totp/components/themes/mfa/totpCodeForm.tsx b/lib/ts/recipe/totp/components/themes/mfa/totpCodeForm.tsx index 71d4e4f73..b95974ce0 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/totpCodeForm.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/totpCodeForm.tsx @@ -39,6 +39,7 @@ export const CodeForm = withOverride( return ( +
{claim.loading === false && (claim.value?.n.length ?? 0) > 1 && (
{t("TOTP_MFA_FOOTER_CHOOSER_ANOTHER")} diff --git a/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.tsx b/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.tsx index 8e0e8fc27..b8229c1d6 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/totpCodeVerificationHeader.tsx @@ -29,7 +29,7 @@ export const CodeVerificationHeader = withOverride( return ( -
+
{props.showBackButton ? ( ) : ( diff --git a/lib/ts/recipe/totp/components/themes/mfa/totpDeviceInfoSection.tsx b/lib/ts/recipe/totp/components/themes/mfa/totpDeviceInfoSection.tsx index 8b30bf113..324968533 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/totpDeviceInfoSection.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/totpDeviceInfoSection.tsx @@ -34,7 +34,7 @@ export const DeviceInfoSection = withOverride( return ( <> -
+
{t("TOTP_SHOW_SECRET_START")} diff --git a/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.tsx b/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.tsx index 7b2d71da2..f2eb4d1e8 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupFooter.tsx @@ -34,7 +34,7 @@ export const DeviceSetupFooter = withOverride( const t = useTranslation(); return ( -
+
{claim.loading === false && (claim.value?.n.length ?? 0) > 1 && (
{t("TOTP_MFA_FOOTER_CHOOSER_ANOTHER")} diff --git a/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.tsx b/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.tsx index 6854c5963..9fbdf5cd4 100644 --- a/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.tsx +++ b/lib/ts/recipe/totp/components/themes/mfa/totpDeviceSetupHeader.tsx @@ -29,7 +29,7 @@ export const DeviceSetupHeader = withOverride( return ( -
+
{props.showBackButton ? ( ) : ( diff --git a/lib/ts/recipe/totp/types.ts b/lib/ts/recipe/totp/types.ts index 2614ce03e..3a8b9ad4e 100644 --- a/lib/ts/recipe/totp/types.ts +++ b/lib/ts/recipe/totp/types.ts @@ -55,6 +55,7 @@ export type TOTPMFAAction = type: "load"; showBackButton: boolean; deviceInfo: TOTPDeviceInfo | undefined; + showAccessDenied: boolean; error: string | undefined; } | { @@ -68,6 +69,7 @@ export type TOTPMFAAction = } | { type: "setError"; + showAccessDenied: boolean; error: string | undefined; maxAttemptCount?: number; currAttemptCount?: number; @@ -93,6 +95,7 @@ export type TOTPMFAState = { error: string | undefined; maxAttemptCount?: number; currAttemptCount?: number; + showAccessDenied: boolean; }; export type TOTPMFACommonProps = { diff --git a/lib/ts/utils.ts b/lib/ts/utils.ts index 3e42e0766..bbdc0ad41 100644 --- a/lib/ts/utils.ts +++ b/lib/ts/utils.ts @@ -452,3 +452,13 @@ export const useOnMountAPICall = ( throw error; } }; + +export function useRethrowInRender() { + const [error, setError] = useState(undefined); + + if (error) { + throw error; + } + + return setError; +}