diff --git a/.changeset/loud-balloons-grow.md b/.changeset/loud-balloons-grow.md new file mode 100644 index 0000000000..abd8cb131b --- /dev/null +++ b/.changeset/loud-balloons-grow.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': patch +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Introduce experimental sign-in combined flow. diff --git a/packages/clerk-js/sandbox/app.js b/packages/clerk-js/sandbox/app.js index acf9bfbe62..f75bb31784 100644 --- a/packages/clerk-js/sandbox/app.js +++ b/packages/clerk-js/sandbox/app.js @@ -181,6 +181,7 @@ function addCurrentRouteIndicator(currentRoute) { ...(componentControls.clerk.getProps() ?? {}), signInUrl: '/sign-in', signUpUrl: '/sign-up', + waitlistUrl: '/waitlist', }); renderCurrentRoute(); } else { diff --git a/packages/clerk-js/sandbox/template.html b/packages/clerk-js/sandbox/template.html index fe78ed6bff..e8ada02e49 100644 --- a/packages/clerk-js/sandbox/template.html +++ b/packages/clerk-js/sandbox/template.html @@ -133,7 +133,7 @@ viewBox="0 0 62 18" fill="none" aria-hidden="true" - class="h-[1.125rem] text-gray-950 dark:text-white" + class="h-[1.125rem] text-gray-950" >
Sandbox
@@ -249,7 +249,10 @@
-
+
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 8885f4bfc6..51b58db4d1 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -356,6 +356,10 @@ export class Clerk implements ClerkInterface { } }; + #isCombinedFlow(): boolean { + return this.#options.experimental?.combinedFlow && this.#options.signInUrl === this.#options.signUpUrl; + } + public signOut: SignOut = async (callbackOrOptions?: SignOutCallback | SignOutOptions, options?: SignOutOptions) => { if (!this.client || this.client.sessions.length === 0) { return; @@ -1049,14 +1053,13 @@ export class Clerk implements ClerkInterface { return this.buildUrlWithAuth(this.#options.afterSignOutUrl); } - public buildWaitlistUrl(): string { + public buildWaitlistUrl(options?: { initialValues?: Record }): string { if (!this.environment || !this.environment.displayConfig) { return ''; } - const waitlistUrl = this.#options['waitlistUrl'] || this.environment.displayConfig.waitlistUrl; - - return buildURL({ base: waitlistUrl }, { stringify: true }); + const initValues = new URLSearchParams(options?.initialValues || {}); + return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } public buildAfterMultiSessionSingleSignOutUrl(): string { @@ -2028,10 +2031,18 @@ export class Clerk implements ClerkInterface { if (!key || !this.loaded || !this.environment || !this.environment.displayConfig) { return ''; } + const signInOrUpUrl = this.#options[key] || this.environment.displayConfig[key]; const redirectUrls = new RedirectUrls(this.#options, options).toSearchParams(); const initValues = new URLSearchParams(_initValues || {}); - const url = buildURL({ base: signInOrUpUrl, hashSearchParams: [initValues, redirectUrls] }, { stringify: true }); + const url = buildURL( + { + base: signInOrUpUrl, + hashPath: this.#isCombinedFlow() && key === 'signUpUrl' ? '/create' : '', + hashSearchParams: [initValues, redirectUrls], + }, + { stringify: true }, + ); return this.buildUrlWithAuth(url); }; diff --git a/packages/clerk-js/src/core/constants.ts b/packages/clerk-js/src/core/constants.ts index ae34145cb6..488568c4e6 100644 --- a/packages/clerk-js/src/core/constants.ts +++ b/packages/clerk-js/src/core/constants.ts @@ -32,6 +32,7 @@ export const ERROR_CODES = { ENTERPRISE_SSO_EMAIL_ADDRESS_DOMAIN_MISMATCH: 'enterprise_sso_email_address_domain_mismatch', ENTERPRISE_SSO_HOSTED_DOMAIN_MISMATCH: 'enterprise_sso_hosted_domain_mismatch', SAML_EMAIL_ADDRESS_DOMAIN_MISMATCH: 'saml_email_address_domain_mismatch', + INVITATION_ACCOUNT_NOT_EXISTS: 'invitation_account_not_exists', } as const; export const SIGN_IN_INITIAL_VALUE_KEYS = ['email_address', 'phone_number', 'username']; diff --git a/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx b/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx index 9f0c08110c..e67d25be89 100644 --- a/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx +++ b/packages/clerk-js/src/ui/common/EmailLinkVerify.tsx @@ -15,11 +15,12 @@ export type EmailLinkVerifyProps = { redirectUrl?: string; verifyEmailPath?: string; verifyPhonePath?: string; + continuePath?: string; texts: Record; }; export const EmailLinkVerify = (props: EmailLinkVerifyProps) => { - const { redirectUrl, redirectUrlComplete, verifyEmailPath, verifyPhonePath } = props; + const { redirectUrl, redirectUrlComplete, verifyEmailPath, verifyPhonePath, continuePath } = props; const { handleEmailLinkVerification } = useClerk(); const { navigate } = useRouter(); const signUp = useCoreSignUp(); @@ -36,6 +37,7 @@ export const EmailLinkVerify = (props: EmailLinkVerifyProps) => { signUp, verifyEmailPath, verifyPhonePath, + continuePath, navigate, }); } catch (err) { diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index 212dfeca9d..e54d09b142 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -9,12 +9,13 @@ export function buildEmailLinkRedirectUrl( baseUrl: string | undefined = '', ): string { const { routing, authQueryString, path } = ctx; + const isCombinedFlow = 'signInUrl' in ctx && 'signUpUrl' in ctx && ctx.signInUrl === ctx.signUpUrl; return buildRedirectUrl({ routing, baseUrl, authQueryString, path, - endpoint: MAGIC_LINK_VERIFY_PATH_ROUTE, + endpoint: isCombinedFlow ? `/create${MAGIC_LINK_VERIFY_PATH_ROUTE}` : MAGIC_LINK_VERIFY_PATH_ROUTE, }); } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index bef7b6e6eb..85b4a8fcf4 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -2,10 +2,22 @@ import { useClerk } from '@clerk/shared/react'; import type { SignInModalProps, SignInProps } from '@clerk/types'; import React from 'react'; -import { SignInEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; -import { SignInContext, useSignInContext, withCoreSessionSwitchGuard } from '../../contexts'; +import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; +import { + SignInContext, + SignUpContext, + useOptions, + useSignInContext, + useSignUpContext, + withCoreSessionSwitchGuard, +} from '../../contexts'; import { Flow } from '../../customizables'; import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; +import { SignUpContinue } from '../SignUp/SignUpContinue'; +import { SignUpSSOCallback } from '../SignUp/SignUpSSOCallback'; +import { SignUpStart } from '../SignUp/SignUpStart'; +import { SignUpVerifyEmail } from '../SignUp/SignUpVerifyEmail'; +import { SignUpVerifyPhone } from '../SignUp/SignUpVerifyPhone'; import { ResetPassword } from './ResetPassword'; import { ResetPasswordSuccess } from './ResetPasswordSuccess'; import { SignInAccountSwitcher } from './SignInAccountSwitcher'; @@ -24,6 +36,8 @@ function RedirectToSignIn() { function SignInRoutes(): JSX.Element { const signInContext = useSignInContext(); + const signUpContext = useSignUpContext(); + const options = useOptions(); return ( @@ -62,6 +76,62 @@ function SignInRoutes(): JSX.Element { redirectUrl='../factor-two' /> + {options.experimental?.combinedFlow && ( + + !!clerk.client.signUp.emailAddress} + > + + + !!clerk.client.signUp.phoneNumber} + > + + + + + + + + + + !!clerk.client.signUp.emailAddress} + > + + + !!clerk.client.signUp.phoneNumber} + > + + + + + + + + + + + )} @@ -73,9 +143,24 @@ function SignInRoutes(): JSX.Element { ); } +function SignInRoot() { + const signInContext = useSignInContext(); + + return ( + + + + ); +} + SignInRoutes.displayName = 'SignIn'; -export const SignIn: React.ComponentType = withCoreSessionSwitchGuard(SignInRoutes); +export const SignIn: React.ComponentType = withCoreSessionSwitchGuard(SignInRoot); export const SignInModal = (props: SignInModalProps): JSX.Element => { const signInProps = { diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index a8f1a5478b..8eb5d0ca79 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -9,7 +9,7 @@ import { getClerkQueryParam, removeClerkQueryParam } from '../../../utils'; import type { SignInStartIdentifier } from '../../common'; import { getIdentifierControlDisplayValues, groupIdentifiers, withRedirectToAfterSignIn } from '../../common'; import { buildSSOCallbackURL } from '../../common/redirects'; -import { useCoreSignIn, useEnvironment, useSignInContext } from '../../contexts'; +import { useCoreSignIn, useEnvironment, useOptions, useSignInContext } from '../../contexts'; import { Col, descriptors, Flow, localizationKeys } from '../../customizables'; import { Card, @@ -25,8 +25,10 @@ import { useSupportEmail } from '../../hooks/useSupportEmail'; import { useRouter } from '../../router'; import type { FormControlState } from '../../utils'; import { buildRequest, handleError, isMobileDevice, useFormControl } from '../../utils'; +import { handleCombinedFlowTransfer } from './handleCombinedFlowTransfer'; import { useHandleAuthenticateWithPasskey } from './shared'; import { SignInSocialButtons } from './SignInSocialButtons'; +import { getSignUpAttributeFromIdentifier } from './utils'; const useAutoFillPasskey = () => { const [isSupported, setIsSupported] = useState(false); @@ -64,8 +66,10 @@ export function _SignInStart(): JSX.Element { const { displayConfig, userSettings } = useEnvironment(); const signIn = useCoreSignIn(); const { navigate } = useRouter(); + const options = useOptions(); const ctx = useSignInContext(); const { afterSignInUrl, signUpUrl, waitlistUrl } = ctx; + const isCombinedFlow = (options?.experimental?.combinedFlow && options.signInUrl === options.signUpUrl) || false; const supportEmail = useSupportEmail(); const identifierAttributes = useMemo( () => groupIdentifiers(userSettings.enabledFirstFactorIdentifiers), @@ -325,15 +329,57 @@ export function _SignInStart(): JSX.Element { (e: ClerkAPIError) => e.code === ERROR_CODES.INVALID_STRATEGY_FOR_USER || e.code === ERROR_CODES.FORM_PASSWORD_INCORRECT, ); + const alreadySignedInError: ClerkAPIError = e.errors.find( (e: ClerkAPIError) => e.code === 'identifier_already_signed_in', ); + const accountDoesNotExistError: ClerkAPIError = e.errors.find( + (e: ClerkAPIError) => + e.code === ERROR_CODES.INVITATION_ACCOUNT_NOT_EXISTS || e.code === ERROR_CODES.FORM_IDENTIFIER_NOT_FOUND, + ); if (instantPasswordError) { await signInWithFields(identifierField); } else if (alreadySignedInError) { const sid = alreadySignedInError.meta!.sessionId!; await clerk.setActive({ session: sid, redirectUrl: afterSignInUrl }); + } else if (isCombinedFlow && accountDoesNotExistError) { + const attribute = getSignUpAttributeFromIdentifier(identifierField); + + if (userSettings.signUp.mode === SIGN_UP_MODES.WAITLIST) { + const waitlistUrl = clerk.buildWaitlistUrl( + attribute === 'emailAddress' + ? { + initialValues: { + [attribute]: identifierField.value, + }, + } + : {}, + ); + return navigate(waitlistUrl); + } + + clerk.client.signUp[attribute] = identifierField.value; + const paramsToForward = new URLSearchParams(); + if (organizationTicket) { + paramsToForward.set('__clerk_ticket', organizationTicket); + } + + const redirectUrl = buildSSOCallbackURL(ctx, displayConfig.signUpUrl); + const redirectUrlComplete = ctx.afterSignUpUrl || '/'; + + return handleCombinedFlowTransfer({ + afterSignUpUrl: ctx.afterSignUpUrl || '/', + clerk, + handleError: e => handleError(e, [identifierField, instantPasswordField], card.setError), + identifierAttribute: attribute, + identifierValue: identifierField.value, + navigate, + organizationTicket, + signUpMode: userSettings.signUp.mode, + redirectUrl, + redirectUrlComplete, + }); } else { handleError(e, [identifierField, instantPasswordField], card.setError); } @@ -366,8 +412,14 @@ export function _SignInStart(): JSX.Element { - - + {isCombinedFlow ? ( + + ) : ( + <> + + + + )} {card.error} {/*TODO: extract main in its own component */} @@ -416,7 +468,7 @@ export function _SignInStart(): JSX.Element { - {userSettings.signUp.mode === SIGN_UP_MODES.PUBLIC && ( + {userSettings.signUp.mode === SIGN_UP_MODES.PUBLIC && !isCombinedFlow && ( Promise; + organizationTicket?: string; + afterSignUpUrl: string; + clerk: LoadedClerk; + handleError: (err: any) => void; + redirectUrl?: string; + redirectUrlComplete?: string; +}; + +/** + * This function is used to handle transfering from a sign in to a sign up when SignIn is rendered as the combined flow. + * There is special logic to handle transfer email-based sign ups directly to verification, bypassing the initial sign up form. + */ +export function handleCombinedFlowTransfer({ + identifierAttribute, + identifierValue, + signUpMode, + navigate, + organizationTicket, + afterSignUpUrl, + clerk, + handleError, + redirectUrl, + redirectUrlComplete, +}: HandleCombinedFlowTransferProps): Promise | void { + if (signUpMode === SIGN_UP_MODES.WAITLIST) { + const waitlistUrl = clerk.buildWaitlistUrl( + identifierAttribute === 'emailAddress' + ? { + initialValues: { + [identifierAttribute]: identifierValue, + }, + } + : {}, + ); + return navigate(waitlistUrl); + } + + clerk.client.signUp[identifierAttribute] = identifierValue; + const paramsToForward = new URLSearchParams(); + if (organizationTicket) { + paramsToForward.set('__clerk_ticket', organizationTicket); + } + + // Attempt to transfer directly to sign up verification if email or phone was used. The signUp.create() call will + // inform us if the instance is eligible for moving directly to verification. + if (identifierAttribute === 'emailAddress' || identifierAttribute === 'phoneNumber') { + return clerk.client.signUp + .create({ + [identifierAttribute]: identifierValue, + }) + .then(res => { + return completeSignUpFlow({ + signUp: res, + verifyEmailPath: 'create/verify-email-address', + verifyPhonePath: 'create/verify-phone-number', + handleComplete: () => clerk.setActive({ session: res.createdSessionId, redirectUrl: afterSignUpUrl }), + navigate, + redirectUrl, + redirectUrlComplete, + }); + }) + .catch(err => handleError(err)); + } + + return navigate(`create?${paramsToForward.toString()}`); +} diff --git a/packages/clerk-js/src/ui/components/SignIn/utils.ts b/packages/clerk-js/src/ui/components/SignIn/utils.ts index ced616f462..b76aceb294 100644 --- a/packages/clerk-js/src/ui/components/SignIn/utils.ts +++ b/packages/clerk-js/src/ui/components/SignIn/utils.ts @@ -1,6 +1,7 @@ import { titleize } from '@clerk/shared/underscore'; import { isWebAuthnSupported } from '@clerk/shared/webauthn'; import type { PreferredSignInStrategy, SignInFactor, SignInResource, SignInStrategy } from '@clerk/types'; +import type { FormControlState } from 'ui/utils'; import { PREFERRED_SIGN_IN_STRATEGIES } from '../../common/constants'; import { otpPrefFactorComparator, passwordPrefFactorComparator } from '../../utils/factorSorting'; @@ -108,3 +109,16 @@ export function determineStartingSignInSecondFactor(secondFactors: SignInFactor[ const resetPasswordStrategies: SignInStrategy[] = ['reset_password_phone_code', 'reset_password_email_code']; export const isResetPasswordStrategy = (strategy: SignInStrategy | string | null | undefined) => !!strategy && resetPasswordStrategies.includes(strategy as SignInStrategy); + +const isEmail = (str: string) => /^\S+@\S+\.\S+$/.test(str); +export function getSignUpAttributeFromIdentifier(identifier: FormControlState<'identifier'>) { + if (identifier.type === 'tel') { + return 'phoneNumber'; + } + + if (isEmail(identifier.value)) { + return 'emailAddress'; + } + + return 'username'; +} diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 9a425e3ea7..d5758243c9 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { ERROR_CODES, SIGN_UP_MODES } from '../../../core/constants'; import { getClerkQueryParam, removeClerkQueryParam } from '../../../utils/getClerkQueryParam'; import { buildSSOCallbackURL, withRedirectToAfterSignUp } from '../../common'; -import { useCoreSignUp, useEnvironment, useSignUpContext } from '../../contexts'; +import { useCoreSignUp, useEnvironment, useOptions, useSignUpContext } from '../../contexts'; import { descriptors, Flex, Flow, localizationKeys, useAppearance, useLocalizations } from '../../customizables'; import { Card, @@ -39,7 +39,9 @@ function _SignUpStart(): JSX.Element { const { attributes } = userSettings; const { setActive } = useClerk(); const ctx = useSignUpContext(); - const { afterSignUpUrl, signInUrl, unsafeMetadata } = ctx; + const options = useOptions(); + const { afterSignUpUrl, signInUrl, signUpUrl, unsafeMetadata } = ctx; + const isCombinedFlow = (options.experimental?.combinedFlow && signInUrl === signUpUrl) || false; const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState( getInitialActiveIdentifier(attributes, userSettings.signUp.progressive), ); @@ -315,7 +317,7 @@ function _SignUpStart(): JSX.Element { diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpVerificationCodeForm.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpVerificationCodeForm.tsx index 358ff0fae1..3f0ae42489 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpVerificationCodeForm.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpVerificationCodeForm.tsx @@ -36,6 +36,7 @@ export const SignUpVerificationCodeForm = (props: SignInFactorOneCodeFormProps) signUp: res, verifyEmailPath: '../verify-email-address', verifyPhonePath: '../verify-phone-number', + continuePath: '../continue', handleComplete: () => setActive({ session: res.createdSessionId, redirectUrl: afterSignUpUrl }), navigate, }); diff --git a/packages/clerk-js/src/ui/components/Waitlist/Waitlist.tsx b/packages/clerk-js/src/ui/components/Waitlist/Waitlist.tsx index 398b4707e0..ded89c5caf 100644 --- a/packages/clerk-js/src/ui/components/Waitlist/Waitlist.tsx +++ b/packages/clerk-js/src/ui/components/Waitlist/Waitlist.tsx @@ -13,8 +13,10 @@ const _Waitlist = () => { const ctx = useWaitlistContext(); const { signInUrl } = ctx; + const initialValues = ctx.initialValues || {}; + const formState = { - emailAddress: useFormControl('emailAddress', '', { + emailAddress: useFormControl('emailAddress', initialValues.emailAddress || '', { type: 'email', label: localizationKeys('formFieldLabel__emailAddress'), placeholder: localizationKeys('formFieldInputPlaceholder__emailAddress'), diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 1d45b1edb0..68279677a3 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -71,6 +71,13 @@ export const useSignInContext = (): SignInContextType => { signUpUrl = buildURL({ base: signUpUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); waitlistUrl = buildURL({ base: waitlistUrl, hashSearchParams: [queryParams, preservedParams] }, { stringify: true }); + if (options.experimental?.combinedFlow && signInUrl === signUpUrl) { + signUpUrl = buildURL( + { base: signInUrl, hashPath: '/create', hashSearchParams: [queryParams, preservedParams] }, + { stringify: true }, + ); + } + const signUpContinueUrl = buildURL({ base: signUpUrl, hashPath: '/continue' }, { stringify: true }); return { diff --git a/packages/clerk-js/src/ui/contexts/components/Waitlist.ts b/packages/clerk-js/src/ui/contexts/components/Waitlist.ts index 6a00f335b7..767a37ee12 100644 --- a/packages/clerk-js/src/ui/contexts/components/Waitlist.ts +++ b/packages/clerk-js/src/ui/contexts/components/Waitlist.ts @@ -1,12 +1,17 @@ -import { createContext, useContext } from 'react'; +import { createContext, useContext, useMemo } from 'react'; import { buildURL } from '../../../utils'; import { useEnvironment, useOptions } from '../../contexts'; +import { useRouter } from '../../router'; import type { WaitlistCtx } from '../../types'; +import { getInitialValuesFromQueryParams } from '../utils'; + +const WAITLIST_INITIAL_VALUE_KEYS = ['email_address']; export type WaitlistContextType = WaitlistCtx & { signInUrl: string; afterJoinWaitlistUrl?: string; + initialValues: any; }; export const WaitlistContext = createContext(null); @@ -15,6 +20,12 @@ export const useWaitlistContext = (): WaitlistContextType => { const context = useContext(WaitlistContext); const { displayConfig } = useEnvironment(); const options = useOptions(); + const { queryString } = useRouter(); + + const initialValuesFromQueryParams = useMemo( + () => getInitialValuesFromQueryParams(queryString, WAITLIST_INITIAL_VALUE_KEYS), + [], + ); if (!context || context.componentName !== 'Waitlist') { throw new Error('Clerk: useWaitlistContext called outside Waitlist.'); @@ -29,5 +40,6 @@ export const useWaitlistContext = (): WaitlistContextType => { ...ctx, componentName, signInUrl, + initialValues: { ...initialValuesFromQueryParams }, }; }; diff --git a/packages/clerk-js/src/utils/completeSignUpFlow.ts b/packages/clerk-js/src/utils/completeSignUpFlow.ts index 6d9742ad30..b64d5c0db6 100644 --- a/packages/clerk-js/src/utils/completeSignUpFlow.ts +++ b/packages/clerk-js/src/utils/completeSignUpFlow.ts @@ -4,6 +4,7 @@ type CompleteSignUpFlowProps = { signUp: SignUpResource; verifyEmailPath?: string; verifyPhonePath?: string; + continuePath?: string; navigate: (to: string) => Promise; handleComplete?: () => Promise; redirectUrl?: string; @@ -14,6 +15,7 @@ export const completeSignUpFlow = ({ signUp, verifyEmailPath, verifyPhonePath, + continuePath, navigate, handleComplete, redirectUrl = '', @@ -37,6 +39,10 @@ export const completeSignUpFlow = ({ if (signUp.unverifiedFields?.includes('phone_number') && verifyPhonePath) { return navigate(verifyPhonePath); } + + if (continuePath) { + return navigate(continuePath); + } } return; }; diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index 65413ab0ca..3261e2d9bb 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -405,6 +405,7 @@ export const arSA: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'للمتابعة إلى {{applicationName}}', title: 'تسجيل الدخول', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'رمز التحقق', diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index 341724641e..9571a32ef1 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -409,6 +409,7 @@ export const beBY: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'каб працягнуць працу ў "{{applicationName}}"', title: 'Увайсці', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Код верыфікацыі', diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts index 8d0f455da9..ffdb2d99da 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -405,6 +405,7 @@ export const bgBG: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'Добре дошли обратно! Моля, влезте, за да продължите', title: 'Влезте в {{applicationName}}', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Код за потвърждение', diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index a21bbc090b..adb504b2cc 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -404,6 +404,7 @@ export const csCZ: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'pro pokračování do {{applicationName}}', title: 'Přihlásit se', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Ověřovací kód', diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index eb4320181d..0988a19138 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -405,6 +405,7 @@ export const daDK: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'Forsæt til {{applicationName}}', title: 'Log ind', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Bekræftelseskode', diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index 901f777349..0440c8a8f7 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -411,6 +411,7 @@ export const deDE: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'weiter zu {{applicationName}}', title: 'Einloggen', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Bestätigungscode', diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts index 7834baabe9..2ac4e97773 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -407,6 +407,7 @@ export const elGR: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'για να συνεχίσετε στο {{applicationName}}', title: 'Σύνδεση', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Κωδικός επαλήθευσης', diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 2a9ba51ee1..78f42c9fbc 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -394,6 +394,7 @@ export const enUS: LocalizationResource = { actionText__join_waitlist: 'Want early access?', subtitle: 'Welcome back! Please sign in to continue', title: 'Sign in to {{applicationName}}', + __experimental_titleCombined: 'Continue to {{applicationName}}', }, totpMfa: { formTitle: 'Verification code', diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index 9ab31051a2..5d77b912af 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -406,6 +406,7 @@ export const esES: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'para continuar a {{applicationName}}', title: 'Entrar', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Código de verificación', diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts index f9d09b9a50..ddbf6aecda 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -410,6 +410,7 @@ export const esMX: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'para continuar con {{applicationName}}', title: 'Iniciar sesión', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Código de verificación', diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts index 19a5bb15f5..31a587db34 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -407,6 +407,7 @@ export const fiFI: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'jatkaaksesi kohteeseen {{applicationName}}', title: 'Kirjaudu sisään', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Todennuskoodi', diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index 19184fe03a..e13b401ed7 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -409,6 +409,7 @@ export const frFR: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'pour continuer vers {{applicationName}}', title: "S'identifier", + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Le code de vérification', diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index 50c7cd6c51..e785677dcc 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -399,6 +399,7 @@ export const heIL: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'להמשיך אל {{applicationName}}', title: 'התחבר', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'קוד אימות', diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts index 357f84fdac..59ac7a558b 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -406,6 +406,7 @@ export const huHU: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'Üdv újra! A folytatáshoz kérlek jelentkezz be.', title: 'Bejelentkezés a(z) {{applicationName}} fiókba', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Visszaigazoló kód', diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts index d0bcfe071a..bd28a0e803 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -408,6 +408,7 @@ export const isIS: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'Velkomin aftur! Vinsamlegast skráðu þig inn til að halda áfram', title: 'Skrá inn í {{applicationName}}', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Staðfestingarkóði', diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index d4b0a51dd7..f1422071af 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -406,6 +406,7 @@ export const itIT: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'per continuare su {{applicationName}}', title: 'Accedi', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Codice di verifica', diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index 5be85cea0e..10bc986e7f 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -406,6 +406,7 @@ export const jaJP: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: '{{applicationName}}へのアクセスを続ける', title: 'サインイン', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: '検証コード', diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts index 9f9c2ad099..d807210a9e 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -401,6 +401,7 @@ export const koKR: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: '환영합니다! 계속하려면 로그인해 주세요', title: '{{applicationName}}에 로그인', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: '인증 코드', diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts index d0f8114423..824bf697a1 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -407,6 +407,7 @@ export const mnMN: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'Тавтай морил! Үргэлжлүүлэхийн тулд нэвтэрнэ үү', title: '{{applicationName}} руу нэвтрэх', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Баталгаажуулах код', diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts index 0c7f61c7e2..200f1c908b 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -406,6 +406,7 @@ export const nbNO: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'for å fortsette til {{applicationName}}', title: 'Logg inn', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Verifiseringskode', diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts index 0d0c3abd0d..cf6a40bf7b 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -405,6 +405,7 @@ export const nlNL: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'om door te gaan naar {{applicationName}}', title: 'Inloggen', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Verificatiecode', diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts index 473463fbe9..1a10f7e066 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -404,6 +404,7 @@ export const plPL: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'aby przejść do {{applicationName}}', title: 'Zaloguj się', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Kod weryfikacyjny', diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index eca2005b02..88c50ab8d0 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -406,6 +406,7 @@ export const ptBR: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'para continuar em {{applicationName}}', title: 'Entrar', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Código de verificação', diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index 95abaf4425..85a1250f40 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -404,6 +404,7 @@ export const ptPT: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'para continuar em {{applicationName}}', title: 'Entrar', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Código de verificação', diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts index ad53f53f4b..dcb2f3da15 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -408,6 +408,7 @@ export const roRO: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'pentru a continua la {{applicationName}}', title: 'Conectați-vă', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Cod de verificare', diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts index dc0cb118c4..8a3713bb02 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -414,6 +414,7 @@ export const ruRU: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'чтобы продолжить работу в "{{applicationName}}"', title: 'Войти', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Верификационный код', diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts index e2946a0bfd..601acc84bc 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -404,6 +404,7 @@ export const skSK: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'pre pokračovanie do {{applicationName}}', title: 'Prihlásiť sa', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Overovací kód', diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts index aebe7f8609..46681ae049 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -405,6 +405,7 @@ export const srRS: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'Dobro došao nazad! Molimo prijavi se da nastaviš', title: 'Prijavi se na {{applicationName}}', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Verifikacioni kod', diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts index 29f359a0e9..7022ac63e2 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -407,6 +407,7 @@ export const svSE: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'för att fortsätta till {{applicationName}}', title: 'Logga in', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Verifieringskod', diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts index 37220b73d3..a18149d4eb 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -402,6 +402,7 @@ export const thTH: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'ยินดีต้อนรับกลับ! กรุณาเข้าสู่ระบบเพื่อดำเนินการต่อ', title: 'เข้าสู่ระบบ {{applicationName}}', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'รหัสการตรวจสอบ', diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts index 9cffd38926..faac7b5963 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -407,6 +407,7 @@ export const trTR: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: '{{applicationName}} ile devam etmek için', title: 'Giriş yap', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Doğrulama kodu', diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts index 0b8e378228..a0ae374394 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -404,6 +404,7 @@ export const ukUA: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'щоб продовжити роботу в "{{applicationName}}"', title: 'Увійти', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Верифікаційний код', diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts index b8c8d02d53..29d76e8ce2 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -404,6 +404,7 @@ export const viVN: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: 'để tiếp tục với {{applicationName}}', title: 'Đăng nhập', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: 'Mã xác minh', diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts index 136b6e1955..a8bce74a97 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -395,6 +395,7 @@ export const zhCN: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: '继续使用 {{applicationName}}', title: '登录', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: '验证码', diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts index 2e3ba091d9..bcdad869ed 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -401,6 +401,7 @@ export const zhTW: LocalizationResource = { actionText__join_waitlist: undefined, subtitle: '繼續使用 {{applicationName}}', title: '登錄', + __experimental_titleCombined: undefined, }, totpMfa: { formTitle: '驗證碼', diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 9163ef2ec0..6b511a3f62 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -463,7 +463,7 @@ export interface Clerk { /** * Returns the configured url where is mounted or a custom waitlist page is rendered. */ - buildWaitlistUrl(): string; + buildWaitlistUrl(opts?: { initialValues?: Record }): string; /** * @@ -739,6 +739,7 @@ export type ClerkOptions = ClerkOptionsNavigation & { persistClient: boolean; rethrowOfflineNetworkErrors: boolean; + combinedFlow: boolean; }, Record >; @@ -894,7 +895,7 @@ export type SignInProps = RoutingOptions & { /** * Enable experimental flags to gain access to new features. These flags are not guaranteed to be stable and may change drastically in between patch or minor versions. */ - __experimental?: Record & { newComponents?: boolean }; + __experimental?: Record & { newComponents?: boolean; signUpProps?: SignUpProps }; /** * Full URL or path to for the waitlist process. * Used to fill the "Join waitlist" link in the SignUp component. diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts index 90693e4330..cf453a8a59 100644 --- a/packages/types/src/localization.ts +++ b/packages/types/src/localization.ts @@ -168,6 +168,7 @@ type _LocalizationResource = { signIn: { start: { title: LocalizationValue; + __experimental_titleCombined: LocalizationValue; subtitle: LocalizationValue; actionText: LocalizationValue; actionLink: LocalizationValue;