diff --git a/packages/react-sdk/src/contexts/CorbadoAppContext.tsx b/packages/react-sdk/src/contexts/CorbadoAppContext.tsx index bcd72032..24dc4973 100644 --- a/packages/react-sdk/src/contexts/CorbadoAppContext.tsx +++ b/packages/react-sdk/src/contexts/CorbadoAppContext.tsx @@ -57,6 +57,7 @@ export interface CorbadoAppContextProps { getUserAuthMethods: (email: string) => Promise>; userExists(email: string): Promise>; logout: () => void; + setGlobalError: (error: NonRecoverableError | undefined) => void; } const missingImplementation = (): never => { @@ -86,6 +87,7 @@ export const initialContext: CorbadoAppContextProps = { getUserAuthMethods: missingImplementation, getProjectConfig: missingImplementation, userExists: missingImplementation, + setGlobalError: missingImplementation, }; export const CorbadoAppContext = createContext(initialContext); diff --git a/packages/react-sdk/src/contexts/CorbadoAppProvider.tsx b/packages/react-sdk/src/contexts/CorbadoAppProvider.tsx index e7cccb41..0d296386 100644 --- a/packages/react-sdk/src/contexts/CorbadoAppProvider.tsx +++ b/packages/react-sdk/src/contexts/CorbadoAppProvider.tsx @@ -9,10 +9,11 @@ type CorbadoAppProviderParams = PropsWithChildren<{ loading: boolean; isAuthenticated: boolean; globalError: NonRecoverableError | undefined; + setGlobalError: (error: NonRecoverableError | undefined) => void; }>; export const CorbadoAppProvider: FC = memo( - ({ children, corbadoApp, loading, globalError, isAuthenticated }) => { + ({ children, corbadoApp, loading, globalError, isAuthenticated, setGlobalError }) => { /** Passkey Authentication APIs */ const signUpWithPasskey = useCallback( (email: string, username: string) => { @@ -149,6 +150,7 @@ export const CorbadoAppProvider: FC = memo( getProjectConfig, userExists, logout, + setGlobalError, }} > {children} diff --git a/packages/react-sdk/src/contexts/index.tsx b/packages/react-sdk/src/contexts/index.tsx index 5930af89..d51e3a45 100644 --- a/packages/react-sdk/src/contexts/index.tsx +++ b/packages/react-sdk/src/contexts/index.tsx @@ -13,7 +13,7 @@ export const CorbadoProvider: FC = ({ children, corbadoAp const [corbadoApp] = useState(() => corbadoAppInstance ?? new CorbadoApp(corbadoParams)); const [loading, setLoading] = useState(true); const [user, setUser] = useState(); - const [globalError, setGlobalError] = useState(); + const [globalError, setGlobalErrorState] = useState(); const [isAuthenticated, setIsAuthenticated] = useState(false); const [shortSession, setShortSession] = useState(); const initialized = useRef(false); @@ -34,7 +34,7 @@ export const CorbadoProvider: FC = ({ children, corbadoAp }); corbadoApp.globalErrors.subscribe(value => { - setGlobalError(value); + setGlobalErrorState(value); }); corbadoApp.authService.authStateChanges.subscribe(value => { @@ -46,6 +46,10 @@ export const CorbadoProvider: FC = ({ children, corbadoAp }); }, []); + const setGlobalError = (error: NonRecoverableError | undefined) => { + corbadoApp.globalErrors.next(error); + }; + return ( = ({ children, corbadoAp loading={loading} isAuthenticated={isAuthenticated} globalError={globalError} + setGlobalError={setGlobalError} > {children} diff --git a/packages/react-sdk/src/index.ts b/packages/react-sdk/src/index.ts index 0a4e7568..facf03d2 100644 --- a/packages/react-sdk/src/index.ts +++ b/packages/react-sdk/src/index.ts @@ -3,6 +3,7 @@ import { InvalidFullnameError, InvalidOtpInputError, InvalidPasskeyError, + NonRecoverableError, NoPasskeyAvailableError, PasskeyChallengeCancelledError, UnknownUserError, @@ -20,4 +21,5 @@ export { UserAlreadyExistsError, InvalidFullnameError, InvalidEmailError, + NonRecoverableError, }; diff --git a/packages/react/src/components/authentication/AuthFormScreen.tsx b/packages/react/src/components/authentication/AuthFormScreen.tsx index 3a0f87bc..0ab7c6cc 100644 --- a/packages/react/src/components/authentication/AuthFormScreen.tsx +++ b/packages/react/src/components/authentication/AuthFormScreen.tsx @@ -9,7 +9,7 @@ import { PrimaryButton } from '../ui/buttons/Button'; export interface AuthFormScreenProps extends CustomizableComponent { headerText: string; subHeaderText: string; - flowChangeButtonText: string; + flowChangeButtonText?: string; submitButtonText: ReactNode; loading: boolean; onSubmit: () => void; @@ -26,15 +26,19 @@ export const AuthFormScreen: FC = memo( return ( <>
{headerText}
- - {subHeaderText} - - {flowChangeButtonText} - - + + {flowChangeButtonText ? ( + + {subHeaderText} + + {flowChangeButtonText} + + + ) : null} +
ScreenNames; emitEvent: (event?: FlowHandlerEvents, eventOptions?: FlowHandlerEventOptions) => Promise | undefined; changeFlow: () => void; @@ -28,6 +30,7 @@ export const initialContext: FlowHandlerContextProps = { currentUserState: {}, currentVerificationMethod: undefined, initialized: false, + projectConfig: undefined, navigateBack: () => ScreenNames.Start, emitEvent: () => Promise.reject(), changeFlow: () => void 0, diff --git a/packages/react/src/contexts/FlowHandlerProvider.tsx b/packages/react/src/contexts/FlowHandlerProvider.tsx index de3464bc..da05489b 100644 --- a/packages/react/src/contexts/FlowHandlerProvider.tsx +++ b/packages/react/src/contexts/FlowHandlerProvider.tsx @@ -8,6 +8,7 @@ import type { VerificationMethods, } from '@corbado/shared-ui'; import { FlowHandler, FlowHandlerEvents, ScreenNames } from '@corbado/shared-ui'; +import type { ProjectConfig } from '@corbado/types'; import i18n from 'i18next'; import type { FC, PropsWithChildren } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -33,6 +34,7 @@ export const FlowHandlerProvider: FC> = ({ const [currentUserState, setCurrentUserState] = useState({}); const [currentFlow, setCurrentFlow] = useState(); const [initialized, setInitialized] = useState(false); + const [projectConfig, setProjectConfig] = useState(undefined); const currentFlowType = useRef(); const verificationMethod = useRef(); const onFlowChangeCbId = useRef(0); @@ -44,12 +46,14 @@ export const FlowHandlerProvider: FC> = ({ } void (async () => { - const projectConfig = await getProjectConfig(); - if (projectConfig.err) { + const projectConfigResult = await getProjectConfig(); + if (projectConfigResult.err) { // currently there are no errors that can be thrown here return; } - const flowHandler = new FlowHandler(projectConfig.val, onLoggedIn, initialFlowType); + + const projectConfig = projectConfigResult.val; + const flowHandler = new FlowHandler(projectConfig, onLoggedIn, initialFlowType); onFlowChangeCbId.current = flowHandler.onFlowChange(updates => { updates.flowName && setCurrentFlow(updates.flowName); @@ -69,6 +73,8 @@ export const FlowHandlerProvider: FC> = ({ }); await flowHandler.init(corbadoApp, i18n); + + setProjectConfig(projectConfig); setFlowHandler(flowHandler); setInitialized(true); })(); @@ -106,6 +112,7 @@ export const FlowHandlerProvider: FC> = ({ currentUserState, currentVerificationMethod: verificationMethod.current, initialized, + projectConfig, changeFlow, navigateBack, emitEvent, diff --git a/packages/react/src/screens/base/authentication/login/Start.tsx b/packages/react/src/screens/base/authentication/login/Start.tsx index 39a79fa9..856b586d 100644 --- a/packages/react/src/screens/base/authentication/login/Start.tsx +++ b/packages/react/src/screens/base/authentication/login/Start.tsx @@ -6,7 +6,7 @@ import { AuthFormScreen, FormInput } from '../../../../components'; import useFlowHandler from '../../../../hooks/useFlowHandler'; export const Start = () => { - const { emitEvent, currentUserState } = useFlowHandler(); + const { emitEvent, currentUserState, projectConfig } = useFlowHandler(); const { t } = useTranslation('translation', { keyPrefix: `authentication.login.start` }); const [loading, setLoading] = useState(false); const initialized = useRef(false); @@ -28,7 +28,10 @@ export const Start = () => { const headerText = useMemo(() => t('header'), [t]); const subHeaderText = useMemo(() => t('subheader'), [t]); - const flowChangeButtonText = useMemo(() => t('button_signup'), [t]); + const flowChangeButtonText = useMemo( + () => (projectConfig?.allowUserRegistration ? t('button_signup') : undefined), + [t], + ); const emailFieldLabel = useMemo(() => t('textField_email'), [t]); const submitButtonText = useMemo(() => t('button_submit'), [t]); diff --git a/packages/react/src/screens/base/authentication/signup/Start.tsx b/packages/react/src/screens/base/authentication/signup/Start.tsx index f4b13463..8f5fd1cf 100644 --- a/packages/react/src/screens/base/authentication/signup/Start.tsx +++ b/packages/react/src/screens/base/authentication/signup/Start.tsx @@ -7,7 +7,7 @@ import { AuthFormScreen, FormInput } from '../../../../components'; import useFlowHandler from '../../../../hooks/useFlowHandler'; export const Start = () => { - const { currentUserState, emitEvent } = useFlowHandler(); + const { currentUserState, projectConfig, emitEvent } = useFlowHandler(); const { t } = useTranslation('translation', { keyPrefix: `authentication.signup.start` }); const [emailError, setEmailError] = useState(null); const [userNameError, setUserNameError] = useState(null); @@ -30,8 +30,11 @@ export const Start = () => { const handleSubmit = useCallback(() => { setLoading(true); + + const fullName = projectConfig?.userFullNameRequired ? fullNameRef.current?.value : emailRef.current?.value; + void emitEvent(FlowHandlerEvents.PrimaryButton, { - userStateUpdate: { email: emailRef.current?.value, fullName: fullNameRef.current?.value }, + userStateUpdate: { email: emailRef.current?.value, fullName }, }); }, [emitEvent]); @@ -45,12 +48,14 @@ export const Start = () => { submitButtonText={submitButtonText} loading={loading} > - el && (fullNameRef.current = el)} - /> + {projectConfig?.userFullNameRequired && ( + el && (fullNameRef.current = el)} + /> + )} = ({ onSignedUp, navigateToLogin }) => { + const { projectConfig } = useFlowHandler(); + const { setGlobalError } = useCorbado(); + + if (!projectConfig?.allowUserRegistration) { + setGlobalError(NonRecoverableError.userRegistrationNotAllowed()); + } + return (
void, initialFlowType: FlowType = FlowType.SignUp) { + if (!projectConfig.allowUserRegistration) { + initialFlowType = FlowType.Login; + } + this.#config = new FlowHandlerConfig(onLoggedIn, projectConfig, initialFlowType); this.#screenHistory = []; this.#currentScreen = this.#config.initialScreenName; diff --git a/packages/web-core/src/utils/errors/errors.ts b/packages/web-core/src/utils/errors/errors.ts index 3998a1e7..256a8114 100644 --- a/packages/web-core/src/utils/errors/errors.ts +++ b/packages/web-core/src/utils/errors/errors.ts @@ -253,6 +253,14 @@ export class NonRecoverableError extends CorbadoError { static client(message: string, link: string) { return new NonRecoverableError('client', message, link); } + + static userRegistrationNotAllowed() { + return new NonRecoverableError( + 'server', + 'User registration is not allowed for this project', + 'https://docs.corbado.com/overview/sign-up-and-login-with-passkeys/user-flow-configuration#id-2.-public-sign-ups', + ); + } } export class UserAlreadyExistsError extends RecoverableError { diff --git a/playground/react/.env b/playground/react/.env index d9bccac8..8875c052 100644 --- a/playground/react/.env +++ b/playground/react/.env @@ -1,11 +1,5 @@ # Flow: PasskeySignupWithEmailOTPFallback -REACT_APP_CORBADO_PROJECT_ID=pro-503401103218055321 - -# Flow: EmailOTPSignupWithPasskey -# REACT_APP_CORBADO_PROJECT_ID=pro-8793272752372175738 +REACT_APP_CORBADO_PROJECT_ID=pro-8793272752372175738 # Flow: PasskeySignupWithEmailOTPFallback + Email Link verification method -# REACT_APP_CORBADO_PROJECT_ID=pro-423122463392265807 - -# Flow: EmailOTPSignupWithPasskey + Email Link verification method -# REACT_APP_CORBADO_PROJECT_ID=pro-3962864614985263396 +# REACT_APP_CORBADO_PROJECT_ID=pro-423122463392265807 \ No newline at end of file