diff --git a/package-lock.json b/package-lock.json index 4d0a3fb..df59eb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18974,7 +18974,7 @@ "name": "toolkit-demo", "version": "0.0.0", "dependencies": { - "@userfront/toolkit": "^1.0.0-alpha.17", + "@userfront/toolkit": "file:../package", "@xstate/inspect": "^0.7.0", "@xstate/react": "^3.0.1", "lodash.get": "^4.4.2", @@ -19024,35 +19024,6 @@ "node": ">=12" } }, - "site/node_modules/@userfront/toolkit": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@userfront/toolkit/-/toolkit-1.0.5.tgz", - "integrity": "sha512-+6xLCscpiivoPKrtLPX9Wtl050D7YWl57AdrdNpdH1T0yn4a/eT5Cp+HL8mTOzryQGkpQM7u0L7jzPX4IMgTWQ==", - "dependencies": { - "@r2wc/react-to-web-component": "^2.0.2", - "@react-hook/resize-observer": "^1.2.6", - "@userfront/core": "^0.6.7", - "@xstate/react": "3.0.1", - "lodash": "^4.17.21", - "react-icons": "^4.4.0", - "react-phone-input-2": "^2.15.1", - "urlon": "^3.1.0", - "xstate": "4.33.6" - }, - "peerDependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - } - }, - "site/node_modules/@userfront/toolkit/node_modules/xstate": { - "version": "4.33.6", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.33.6.tgz", - "integrity": "sha512-A5R4fsVKADWogK2a43ssu8Fz1AF077SfrKP1ZNyDBD8lNa/l4zfR//Luofp5GSWehOQr36Jp0k2z7b+sH2ivyg==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/xstate" - } - }, "site/node_modules/@vitejs/plugin-react": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-2.2.0.tgz", @@ -31281,7 +31252,7 @@ "@types/lodash.get": "^4.4.7", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", - "@userfront/toolkit": "^1.0.0-alpha.17", + "@userfront/toolkit": "file:../package", "@vitejs/plugin-react": "^2.1.0", "@xstate/inspect": "^0.7.0", "@xstate/react": "^3.0.1", @@ -31308,29 +31279,6 @@ "dev": true, "optional": true }, - "@userfront/toolkit": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@userfront/toolkit/-/toolkit-1.0.5.tgz", - "integrity": "sha512-+6xLCscpiivoPKrtLPX9Wtl050D7YWl57AdrdNpdH1T0yn4a/eT5Cp+HL8mTOzryQGkpQM7u0L7jzPX4IMgTWQ==", - "requires": { - "@r2wc/react-to-web-component": "^2.0.2", - "@react-hook/resize-observer": "^1.2.6", - "@userfront/core": "^0.6.7", - "@xstate/react": "3.0.1", - "lodash": "^4.17.21", - "react-icons": "^4.4.0", - "react-phone-input-2": "^2.15.1", - "urlon": "^3.1.0", - "xstate": "4.33.6" - }, - "dependencies": { - "xstate": { - "version": "4.33.6", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.33.6.tgz", - "integrity": "sha512-A5R4fsVKADWogK2a43ssu8Fz1AF077SfrKP1ZNyDBD8lNa/l4zfR//Luofp5GSWehOQr36Jp0k2z7b+sH2ivyg==" - } - } - }, "@vitejs/plugin-react": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-2.2.0.tgz", diff --git a/package/src/forms/UniversalForm.jsx b/package/src/forms/UniversalForm.jsx index 76dc196..d5f1364 100644 --- a/package/src/forms/UniversalForm.jsx +++ b/package/src/forms/UniversalForm.jsx @@ -154,10 +154,11 @@ const strings = { }, }, general: { + disabled: "Authentication is disabled", redirecting: "Redirecting...", + unhandledError: "Oops, something went wrong", verified: "Verified", welcome: "Welcome", - unhandledError: "Oops, something went wrong", }, }; @@ -186,6 +187,14 @@ const componentForStep = (state) => { case "beginFlow": case "showPreviewAndFetchFlow": case "showPlaceholderAndFetchFlow": + case "disabled": + return { + title: strings.general.disabled, + Component: GeneralErrorMessage, + props: { + message: "Please contact an administrator for assistance", + }, + }; case "initPasswordReset": if (canShowFlow) { return { @@ -206,7 +215,6 @@ const componentForStep = (state) => { Component: Placeholder, }; } - case "selectFirstFactor.showForm": return { title: strings[type].title, @@ -491,7 +499,7 @@ const componentForStep = (state) => { case "missingFlowInDevModeError": case "missingFlowInLocalModeError": case "missingFlowFromServerError": - case "UnhandledError": + case "unhandledError": return { title: strings.general.unhandledError, Component: GeneralErrorMessage, diff --git a/package/src/models/config/guards.ts b/package/src/models/config/guards.ts index d4332e3..0b14227 100644 --- a/package/src/models/config/guards.ts +++ b/package/src/models/config/guards.ts @@ -14,20 +14,20 @@ import { getUserfrontPropertySync } from "../../services/userfront"; // GUARDS / PREDICATES +const ssoStrategies = [ + "apple", + "azure", + "google", + "github", + "twitter", + "facebook", + "linkedin", + "okta", +]; + // Is this factor an SSO provider? -export const isSsoProvider = (factor: Factor) => { - return ( - factor.channel === "email" && - (factor.strategy === "apple" || - factor.strategy === "azure" || - factor.strategy === "google" || - factor.strategy === "github" || - factor.strategy === "twitter" || - factor.strategy === "facebook" || - factor.strategy === "linkedin" || - factor.strategy === "okta") - ); -}; +export const isSsoProvider = (factor: Factor) => + factor.channel === "email" && ssoStrategies.includes(factor.strategy); // Is a second factor required to complete the signup or login? export const secondFactorRequired = ( diff --git a/package/src/models/forms/universal.ts b/package/src/models/forms/universal.ts index 943995b..87e2d1f 100644 --- a/package/src/models/forms/universal.ts +++ b/package/src/models/forms/universal.ts @@ -30,22 +30,22 @@ import emailCodeConfig from "../views/emailCode"; import emailLinkConfig from "../views/emailLink"; import setNewPasswordConfig from "../views/setNewPassword"; import { - isSsoProvider, + hasLinkQueryParams, hasNoActiveFactor, isLocalMode, + isLocalModeWithoutFlow, + isLoggedIn, + isLoggedInOrHasLinkCredentials, isMissingFlow, isMissingFlowFromServer, - isLocalModeWithoutFlow, isMissingTenantId, + isPasswordReset, + isSecondFactor, + isSetup, + isSsoProvider, passwordsMatch, - hasLinkQueryParams, secondFactorRequired, secondFactorRequiredFromView, - isLoggedIn, - isSecondFactor, - isPasswordReset, - isLoggedInOrHasLinkCredentials, - isSetup, } from "../config/guards"; import { setActiveFactor, @@ -92,7 +92,12 @@ export const defaultOptions = { // Predicates for first factors: // Does the flow have multiple first factors? hasMultipleFirstFactors: (context: AuthContext, event: any) => { - return (context.config.flow?.firstFactors?.length ?? 0) > 1; + const firstFactors = context.config.flow?.firstFactors ?? []; + return firstFactors.length > 1; + }, + hasNoFirstFactors: (context: AuthContext) => { + const firstFactors = context.config.flow?.firstFactors ?? []; + return firstFactors.length === 0; }, // Is the flow only one factor? One predicate per factor type. // If there's only one allowed factor, allows us to skip the select screen @@ -186,54 +191,54 @@ export const defaultOptions = { return isSsoProvider(event.factor); }, + hasLinkQueryParams, hasNoActiveFactor, isLocalMode, + isLocalModeWithoutFlow, + isLoggedIn, + isLoggedInOrHasLinkCredentials, isMissingFlow, isMissingFlowFromServer, - isLocalModeWithoutFlow, isMissingTenantId, + isPasswordReset, + isSecondFactor, + isSetup, passwordsMatch, - hasLinkQueryParams, secondFactorRequired, secondFactorRequiredFromView, - isLoggedIn, - isSecondFactor, - isPasswordReset, - isLoggedInOrHasLinkCredentials, - isSetup, }, actions: { - setActiveFactor, + clearError, + clearResentMessage, + disableBack, + enableBack, + markAsSecondFactor, + markQueryParamsInvalid, + readQueryParams, + redirectIfLoggedIn, + redirectOnLoad, resumeIfNeeded, - setFlowFromUserfrontApi, + setActiveFactor, + setAllowedSecondFactors, + setAllowedSecondFactorsFromView, + setCode, setEmail, + setErrorForPasswordMismatch, + setErrorFromApiError, + setFirstFactorAction, + setFlowFromUserfrontApi, setPassword, + setPasswordForReset, setPhoneNumber, + setQrCode, + setResentMessage, + setSecondFactorAction, + setShowEmailOrUsernameIfFirstFactor, setTenantIdIfPresent, setTotpCode, + setupView, setUseBackupCode, - setQrCode, - redirectIfLoggedIn, - redirectOnLoad, - setCode, - setErrorFromApiError, - clearError, - setErrorForPasswordMismatch, - setShowEmailOrUsernameIfFirstFactor, - markAsSecondFactor, - setAllowedSecondFactors, - setAllowedSecondFactorsFromView, storeFactorResponse, - disableBack, - enableBack, - setupView, - readQueryParams, - markQueryParamsInvalid, - setResentMessage, - clearResentMessage, - setFirstFactorAction, - setSecondFactorAction, - setPasswordForReset, }, }; @@ -520,6 +525,11 @@ const universalMachineConfig: AuthMachineConfig = { "setupView", ], always: [ + // Handle no first factors + { + target: "noFirstFactors", + cond: "hasNoFirstFactors", + }, // If we're returning from a passwordless/email link or SSO first factor, attempt to use // the query params to proceed. { @@ -584,6 +594,22 @@ const universalMachineConfig: AuthMachineConfig = { ], }, + // Show the disabled view if there are no first factors + noFirstFactors: { + id: "noFirstFactors", + always: [ + { + target: "disabled", + }, + ], + }, + + // Show the disabled error view + disabled: { + id: "disabled", + type: "final", + }, + // Show the first factor selection view, non-compact // Parallel states for the Password view and the rest of the form selectFirstFactor: selectFactorConfig, diff --git a/package/src/views/GeneralErrorMessage.jsx b/package/src/views/GeneralErrorMessage.jsx index e5d4a8d..e1ef8c1 100644 --- a/package/src/views/GeneralErrorMessage.jsx +++ b/package/src/views/GeneralErrorMessage.jsx @@ -8,11 +8,14 @@ import ErrorMessage from "../components/ErrorMessage"; * @param {object} props * @param {object} error - a Userfront error to display */ -const GeneralErrorMessage = ({ error }) => { +const GeneralErrorMessage = ({ + error, + message = "An unhandled error occurred. Please try again later.", +}) => { return (
-

An unhandled error occurred. Please try again later.

- + {message &&

{message}

} + {error && }
); }; diff --git a/site/package.json b/site/package.json index 6431ab4..4f5b84e 100644 --- a/site/package.json +++ b/site/package.json @@ -11,7 +11,7 @@ "dependencies": { "@xstate/inspect": "^0.7.0", "@xstate/react": "^3.0.1", - "@userfront/toolkit": "^1.0.0-alpha.17", + "@userfront/toolkit": "file:../package", "lodash.get": "^4.4.2", "react": "^18.2.0", "react-dom": "^18.2.0",