diff --git a/.changeset/loud-balloons-grow.md b/.changeset/loud-balloons-grow.md
new file mode 100644
index 00000000000..abd8cb131b9
--- /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/integration/presets/envs.ts b/integration/presets/envs.ts
index ef44f865f3a..068a9845b5b 100644
--- a/integration/presets/envs.ts
+++ b/integration/presets/envs.ts
@@ -113,6 +113,15 @@ const withWaitlistdMode = withEmailCodes
.setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-waitlist-mode').sk)
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-waitlist-mode').pk);
+const withCombinedFlow = withEmailCodes
+ .clone()
+ .setId('withCombinedFlow')
+ .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-email-codes').sk)
+ .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-codes').pk)
+ .setEnvVariable('public', 'EXPERIMENTAL_COMBINED_FLOW', 'true')
+ .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in')
+ .setEnvVariable('public', 'CLERK_SIGN_UP_URL', '/sign-in');
+
export const envs = {
base,
withEmailCodes,
@@ -129,4 +138,5 @@ export const envs = {
withRestrictedMode,
withLegalConsent,
withWaitlistdMode,
+ withCombinedFlow,
} as const;
diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts
index 3c310184f4e..f6de9ce0992 100644
--- a/integration/presets/longRunningApps.ts
+++ b/integration/presets/longRunningApps.ts
@@ -32,6 +32,7 @@ export const createLongRunningApps = () => {
},
{ id: 'next.appRouter.withCustomRoles', config: next.appRouter, env: envs.withCustomRoles },
{ id: 'next.appRouter.withReverification', config: next.appRouter, env: envs.withReverification },
+ { id: 'next.appRouter.withCombinedFlow', config: next.appRouter, env: envs.withCombinedFlow },
{ id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart },
{ id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes },
{ id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles },
diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx
index b8b377146ce..0c43679815c 100644
--- a/integration/templates/next-app-router/src/app/layout.tsx
+++ b/integration/templates/next-app-router/src/app/layout.tsx
@@ -13,6 +13,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
);
diff --git a/integration/tests/combined-sign-in-flow.test.ts b/integration/tests/combined-sign-in-flow.test.ts
new file mode 100644
index 00000000000..548ea47ef63
--- /dev/null
+++ b/integration/tests/combined-sign-in-flow.test.ts
@@ -0,0 +1,160 @@
+import { expect, test } from '@playwright/test';
+
+import { appConfigs } from '../presets';
+import { createTestUtils, type FakeUser, testAgainstRunningApps } from '../testUtils';
+
+testAgainstRunningApps({ withEnv: [appConfigs.envs.withCombinedFlow] })('combined sign in flow @nextjs', ({ app }) => {
+ test.describe.configure({ mode: 'serial' });
+
+ let fakeUser: FakeUser;
+
+ test.beforeAll(async () => {
+ const u = createTestUtils({ app });
+ fakeUser = u.services.users.createFakeUser({
+ withPhoneNumber: true,
+ withUsername: true,
+ });
+ await u.services.users.createBapiUser(fakeUser);
+ });
+
+ test.afterAll(async () => {
+ await fakeUser.deleteIfExists();
+ await app.teardown();
+ });
+
+ test('flows are combined', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+
+ await expect(u.page.getByText(`Don’t have an account?`)).toBeHidden();
+ });
+
+ test('sign in with email and password', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.setIdentifier(fakeUser.email);
+ await u.po.signIn.continue();
+ await u.po.signIn.setPassword(fakeUser.password);
+ await u.po.signIn.continue();
+ await u.po.expect.toBeSignedIn();
+ });
+
+ test('sign in with email and instant password', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+ });
+
+ test('sign in with email code', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.getIdentifierInput().fill(fakeUser.email);
+ await u.po.signIn.continue();
+ await u.po.signIn.getUseAnotherMethodLink().click();
+ await u.po.signIn.getAltMethodsEmailCodeButton().click();
+ await u.po.signIn.enterTestOtpCode();
+ await u.po.expect.toBeSignedIn();
+ });
+
+ test('sign in with phone number and password', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.usePhoneNumberIdentifier().click();
+ await u.po.signIn.getIdentifierInput().fill(fakeUser.phoneNumber);
+ await u.po.signIn.setPassword(fakeUser.password);
+ await u.po.signIn.continue();
+ await u.po.expect.toBeSignedIn();
+ });
+
+ test('sign in only with phone number', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ const fakeUserWithoutPassword = u.services.users.createFakeUser({
+ fictionalEmail: true,
+ withPassword: false,
+ withPhoneNumber: true,
+ });
+ await u.services.users.createBapiUser(fakeUserWithoutPassword);
+ await u.po.signIn.goTo();
+ await u.po.signIn.usePhoneNumberIdentifier().click();
+ await u.po.signIn.getIdentifierInput().fill(fakeUserWithoutPassword.phoneNumber);
+ await u.po.signIn.continue();
+ await u.po.signIn.enterTestOtpCode();
+ await u.po.expect.toBeSignedIn();
+
+ await fakeUserWithoutPassword.deleteIfExists();
+ });
+
+ test('sign in with username and password', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.getIdentifierInput().fill(fakeUser.username);
+ await u.po.signIn.setPassword(fakeUser.password);
+ await u.po.signIn.continue();
+ await u.po.expect.toBeSignedIn();
+ });
+
+ test('can reset password', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ const fakeUserWithPasword = u.services.users.createFakeUser({
+ fictionalEmail: true,
+ withPassword: true,
+ });
+ await u.services.users.createBapiUser(fakeUserWithPasword);
+
+ await u.po.signIn.goTo();
+ await u.po.signIn.getIdentifierInput().fill(fakeUserWithPasword.email);
+ await u.po.signIn.continue();
+ await u.po.signIn.getForgotPassword().click();
+ await u.po.signIn.getResetPassword().click();
+ await u.po.signIn.enterTestOtpCode();
+ await u.po.signIn.setPassword(`${fakeUserWithPasword.password}_reset`);
+ await u.po.signIn.setPasswordConfirmation(`${fakeUserWithPasword.password}_reset`);
+ await u.po.signIn.getResetPassword().click();
+ await u.po.expect.toBeSignedIn();
+
+ await fakeUserWithPasword.deleteIfExists();
+ });
+
+ test('cannot sign in with wrong password', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ await u.po.signIn.goTo();
+ await u.po.signIn.getIdentifierInput().fill(fakeUser.email);
+ await u.po.signIn.continue();
+ await u.po.signIn.setPassword('wrong-password');
+ await u.po.signIn.continue();
+ await expect(u.page.getByText(/^password is incorrect/i)).toBeVisible();
+
+ await u.po.expect.toBeSignedOut();
+ });
+
+ test('cannot sign in with wrong password but can sign in with email', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ await u.po.signIn.goTo();
+ await u.po.signIn.getIdentifierInput().fill(fakeUser.email);
+ await u.po.signIn.continue();
+ await u.po.signIn.setPassword('wrong-password');
+ await u.po.signIn.continue();
+
+ await expect(u.page.getByText(/^password is incorrect/i)).toBeVisible();
+
+ await u.po.signIn.getUseAnotherMethodLink().click();
+ await u.po.signIn.getAltMethodsEmailCodeButton().click();
+ await u.po.signIn.enterTestOtpCode();
+
+ await u.po.expect.toBeSignedIn();
+ });
+
+ test('access protected page @express', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+
+ expect(await u.page.locator("data-test-id='protected-api-response'").count()).toEqual(0);
+ await u.page.goToRelative('/protected');
+ await u.page.isVisible("data-test-id='protected-api-response'");
+ });
+});
diff --git a/integration/tests/combined-sign-up-flow.test.ts b/integration/tests/combined-sign-up-flow.test.ts
new file mode 100644
index 00000000000..e2c3d8adc29
--- /dev/null
+++ b/integration/tests/combined-sign-up-flow.test.ts
@@ -0,0 +1,118 @@
+import { expect, test } from '@playwright/test';
+
+import { appConfigs } from '../presets';
+import { createTestUtils, testAgainstRunningApps } from '../testUtils';
+
+testAgainstRunningApps({ withEnv: [appConfigs.envs.withCombinedFlow] })('combined sign up flow @nextjs', ({ app }) => {
+ test.describe.configure({ mode: 'serial' });
+
+ test.afterAll(async () => {
+ await app.teardown();
+ });
+
+ test('sign up with email and password', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ const fakeUser = u.services.users.createFakeUser({
+ fictionalEmail: true,
+ withPassword: true,
+ });
+
+ // Go to sign in page
+ await u.po.signIn.goTo();
+
+ // Fill in sign in form
+ await u.po.signIn.setIdentifier(fakeUser.email);
+ await u.po.signIn.continue();
+
+ // Verify email
+ await u.po.signUp.enterTestOtpCode();
+
+ await u.page.waitForAppUrl('/sign-in/create/continue');
+
+ await u.po.signUp.setPassword(fakeUser.password);
+ await u.po.signUp.continue();
+
+ // Check if user is signed in
+ await u.po.expect.toBeSignedIn();
+
+ await fakeUser.deleteIfExists();
+ });
+
+ test('sign up with username, email, and password', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ const fakeUser = u.services.users.createFakeUser({
+ fictionalEmail: true,
+ withPassword: true,
+ withUsername: true,
+ });
+
+ await u.po.signIn.goTo();
+ await u.po.signIn.setIdentifier(fakeUser.username);
+ await u.po.signIn.continue();
+ await u.page.waitForAppUrl('/sign-in/create');
+
+ const prefilledUsername = await u.po.signUp.getUsernameInput().inputValue();
+ expect(prefilledUsername).toBe(fakeUser.username);
+
+ await u.po.signUp.setEmailAddress(fakeUser.email);
+ await u.po.signUp.setPassword(fakeUser.password);
+ await u.po.signUp.continue();
+
+ await u.po.signUp.enterTestOtpCode();
+
+ await u.po.expect.toBeSignedIn();
+
+ await fakeUser.deleteIfExists();
+ });
+
+ test('sign up, sign out and sign in again', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ const fakeUser = u.services.users.createFakeUser({
+ fictionalEmail: true,
+ withPhoneNumber: true,
+ withUsername: true,
+ });
+
+ // Go to sign in page
+ await u.po.signIn.goTo();
+
+ // Fill in sign in form
+ await u.po.signIn.setIdentifier(fakeUser.email);
+ await u.po.signIn.continue();
+
+ // Verify email
+ await u.po.signUp.enterTestOtpCode();
+
+ await u.page.waitForAppUrl('/sign-in/create/continue');
+
+ await u.po.signUp.setPassword(fakeUser.password);
+ await u.po.signUp.continue();
+
+ // Check if user is signed in
+ await u.po.expect.toBeSignedIn();
+
+ // Toggle user button
+ await u.po.userButton.toggleTrigger();
+ await u.po.userButton.waitForPopover();
+
+ // Click sign out
+ await u.po.userButton.triggerSignOut();
+
+ // Check if user is signed out
+ await u.po.expect.toBeSignedOut();
+
+ // Go to sign in page
+ await u.po.signIn.goTo();
+
+ // Fill in sign in form
+ await u.po.signIn.signInWithEmailAndInstantPassword({
+ email: fakeUser.email,
+ password: fakeUser.password,
+ });
+
+ // Check if user is signed in
+ await u.po.expect.toBeSignedIn();
+
+ await fakeUser.deleteIfExists();
+ });
+});
diff --git a/packages/clerk-js/sandbox/template.html b/packages/clerk-js/sandbox/template.html
index 356b25b08b8..cc57518fc10 100644
--- a/packages/clerk-js/sandbox/template.html
+++ b/packages/clerk-js/sandbox/template.html
@@ -265,7 +265,10 @@
-
+
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index b5e0b5b826a..46a1f967f8b 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -358,6 +358,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;
@@ -1052,14 +1056,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 {
@@ -2051,10 +2054,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 4048385e631..bcc5c01bd13 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 9f0c08110c9..e67d25be897 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 212dfeca9dc..347b45bf9a2 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 = '__experimental' in ctx && ctx.__experimental?.combinedProps;
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 bef7b6e6eb6..fbce6789212 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 52052503f9a..483cfd5eb23 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;
const supportEmail = useSupportEmail();
const identifierAttributes = useMemo(
() => groupIdentifiers(userSettings.enabledFirstFactorIdentifiers),
@@ -332,15 +336,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);
}
@@ -373,8 +419,14 @@ export function _SignInStart(): JSX.Element {
-
-
+ {isCombinedFlow ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
{card.error}
{/*TODO: extract main in its own component */}
@@ -423,7 +475,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 ced616f462e..b76aceb2942 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/SignUpContinue.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx
index 37444a1ed8a..63b3748a2a3 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx
@@ -1,7 +1,7 @@
import { useClerk } from '@clerk/shared/react';
import React, { useEffect, useMemo } from 'react';
-import { useCoreSignUp, useEnvironment, useSignUpContext } from '../../contexts';
+import { SignInContext, useCoreSignUp, useEnvironment, useOptions, useSignUpContext } from '../../contexts';
import { descriptors, Flex, Flow, localizationKeys } from '../../customizables';
import {
Card,
@@ -33,6 +33,9 @@ function _SignUpContinue() {
const { attributes } = userSettings;
const { afterSignUpUrl, signInUrl, unsafeMetadata, initialValues = {} } = useSignUpContext();
const signUp = useCoreSignUp();
+ const options = useOptions();
+ const isWithinSignInContext = !!React.useContext(SignInContext);
+ const isCombinedFlow = !!(options.experimental?.combinedFlow && !!isWithinSignInContext);
const isProgressiveSignUp = userSettings.signUp.progressive;
const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState(
getInitialActiveIdentifier(attributes, userSettings.signUp.progressive),
@@ -218,13 +221,15 @@ function _SignUpContinue() {
-
-
-
-
+ {!isCombinedFlow ? (
+
+
+
+
+ ) : null}
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx
index 9a425e3ea70..2fe85d65aff 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 { SignInContext, useCoreSignUp, useEnvironment, useOptions, useSignUpContext } from '../../contexts';
import { descriptors, Flex, Flow, localizationKeys, useAppearance, useLocalizations } from '../../customizables';
import {
Card,
@@ -39,7 +39,10 @@ function _SignUpStart(): JSX.Element {
const { attributes } = userSettings;
const { setActive } = useClerk();
const ctx = useSignUpContext();
+ const options = useOptions();
+ const isWithinSignInContext = !!React.useContext(SignInContext);
const { afterSignUpUrl, signInUrl, unsafeMetadata } = ctx;
+ const isCombinedFlow = !!(options.experimental?.combinedFlow && !!isWithinSignInContext);
const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState(
getInitialActiveIdentifier(attributes, userSettings.signUp.progressive),
);
@@ -315,7 +318,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 358ff0fae1f..3f0ae424894 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 398b4707e02..ded89c5caf2 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 1d45b1edb02..c717bec4dab 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
@@ -37,7 +37,8 @@ export const useSignInContext = (): SignInContextType => {
throw new Error(`Clerk: useSignInContext called outside of the mounted SignIn component.`);
}
- const { componentName, ...ctx } = context;
+ const { componentName, ..._ctx } = context;
+ const ctx = _ctx.__experimental?.combinedProps || _ctx;
const initialValuesFromQueryParams = useMemo(
() => getInitialValuesFromQueryParams(queryString, SIGN_IN_INITIAL_VALUE_KEYS),
@@ -71,6 +72,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) {
+ 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 6a00f335b7a..767a37ee12d 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 baf951eba21..b18f34940d1 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 ef5f77f3b1c..7163b8247dd 100644
--- a/packages/localizations/src/ar-SA.ts
+++ b/packages/localizations/src/ar-SA.ts
@@ -460,6 +460,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 0a03674b0f4..12ac2267e0f 100644
--- a/packages/localizations/src/be-BY.ts
+++ b/packages/localizations/src/be-BY.ts
@@ -464,6 +464,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 1627394c2d4..f29526fe37d 100644
--- a/packages/localizations/src/bg-BG.ts
+++ b/packages/localizations/src/bg-BG.ts
@@ -460,6 +460,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 12ec9f02448..a53a5fcb4fd 100644
--- a/packages/localizations/src/cs-CZ.ts
+++ b/packages/localizations/src/cs-CZ.ts
@@ -459,6 +459,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 0c65ca601a8..c569e202532 100644
--- a/packages/localizations/src/da-DK.ts
+++ b/packages/localizations/src/da-DK.ts
@@ -460,6 +460,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 6555b908882..e786ca62ca2 100644
--- a/packages/localizations/src/de-DE.ts
+++ b/packages/localizations/src/de-DE.ts
@@ -466,6 +466,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 93c4613a7f7..0b69da0e26a 100644
--- a/packages/localizations/src/el-GR.ts
+++ b/packages/localizations/src/el-GR.ts
@@ -462,6 +462,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 e53c4ac6e11..928c6045eae 100644
--- a/packages/localizations/src/en-US.ts
+++ b/packages/localizations/src/en-US.ts
@@ -450,6 +450,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 1fbdef0393f..1a154a9b76d 100644
--- a/packages/localizations/src/es-ES.ts
+++ b/packages/localizations/src/es-ES.ts
@@ -461,6 +461,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 36e167f6fff..2f69c0711c6 100644
--- a/packages/localizations/src/es-MX.ts
+++ b/packages/localizations/src/es-MX.ts
@@ -465,6 +465,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 f3615c6bacf..6c972fc358f 100644
--- a/packages/localizations/src/fi-FI.ts
+++ b/packages/localizations/src/fi-FI.ts
@@ -462,6 +462,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 22f6861bfa6..34d4248c69a 100644
--- a/packages/localizations/src/fr-FR.ts
+++ b/packages/localizations/src/fr-FR.ts
@@ -464,6 +464,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 b193a5e41a4..5480320c19c 100644
--- a/packages/localizations/src/he-IL.ts
+++ b/packages/localizations/src/he-IL.ts
@@ -454,6 +454,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 24628d7518f..3effa0b4aa9 100644
--- a/packages/localizations/src/hu-HU.ts
+++ b/packages/localizations/src/hu-HU.ts
@@ -461,6 +461,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 3e5fe66d596..9629fc1164a 100644
--- a/packages/localizations/src/is-IS.ts
+++ b/packages/localizations/src/is-IS.ts
@@ -463,6 +463,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 b7a977abf2c..9c4aabdb4a2 100644
--- a/packages/localizations/src/it-IT.ts
+++ b/packages/localizations/src/it-IT.ts
@@ -461,6 +461,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 78e0faf5e54..dbdcdd34e24 100644
--- a/packages/localizations/src/ja-JP.ts
+++ b/packages/localizations/src/ja-JP.ts
@@ -461,6 +461,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 f247fd4b836..4dc49672099 100644
--- a/packages/localizations/src/ko-KR.ts
+++ b/packages/localizations/src/ko-KR.ts
@@ -456,6 +456,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 816baadd1db..885d199a807 100644
--- a/packages/localizations/src/mn-MN.ts
+++ b/packages/localizations/src/mn-MN.ts
@@ -462,6 +462,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 07b4b869c38..4956900b257 100644
--- a/packages/localizations/src/nb-NO.ts
+++ b/packages/localizations/src/nb-NO.ts
@@ -461,6 +461,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 b7e02eed51f..27693e3846c 100644
--- a/packages/localizations/src/nl-NL.ts
+++ b/packages/localizations/src/nl-NL.ts
@@ -461,6 +461,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 2a4a9ffd533..d6bdb6b7db8 100644
--- a/packages/localizations/src/pl-PL.ts
+++ b/packages/localizations/src/pl-PL.ts
@@ -460,6 +460,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 431b00cf945..5d9b56e8db5 100644
--- a/packages/localizations/src/pt-BR.ts
+++ b/packages/localizations/src/pt-BR.ts
@@ -461,6 +461,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 12f672df683..8f50b8fb2ea 100644
--- a/packages/localizations/src/pt-PT.ts
+++ b/packages/localizations/src/pt-PT.ts
@@ -459,6 +459,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 49b78a51fdf..456dbb18424 100644
--- a/packages/localizations/src/ro-RO.ts
+++ b/packages/localizations/src/ro-RO.ts
@@ -463,6 +463,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 c4bdb233903..e302f1075a5 100644
--- a/packages/localizations/src/ru-RU.ts
+++ b/packages/localizations/src/ru-RU.ts
@@ -470,6 +470,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 033ec3d6578..1354051d097 100644
--- a/packages/localizations/src/sk-SK.ts
+++ b/packages/localizations/src/sk-SK.ts
@@ -459,6 +459,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 1d949a48111..2b9ee9a2802 100644
--- a/packages/localizations/src/sr-RS.ts
+++ b/packages/localizations/src/sr-RS.ts
@@ -460,6 +460,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 f9f033e38b3..0d39cdbd5f8 100644
--- a/packages/localizations/src/sv-SE.ts
+++ b/packages/localizations/src/sv-SE.ts
@@ -463,6 +463,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 5d0bc6ffeb9..8cd4b69adfc 100644
--- a/packages/localizations/src/th-TH.ts
+++ b/packages/localizations/src/th-TH.ts
@@ -457,6 +457,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 3bea5c5a03a..9a9aeeee5f6 100644
--- a/packages/localizations/src/tr-TR.ts
+++ b/packages/localizations/src/tr-TR.ts
@@ -462,6 +462,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 26d9eae3815..7e8408d0491 100644
--- a/packages/localizations/src/uk-UA.ts
+++ b/packages/localizations/src/uk-UA.ts
@@ -459,6 +459,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 b246c585b88..b81c4b8c490 100644
--- a/packages/localizations/src/vi-VN.ts
+++ b/packages/localizations/src/vi-VN.ts
@@ -459,6 +459,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 530649b8716..2f472355eb0 100644
--- a/packages/localizations/src/zh-CN.ts
+++ b/packages/localizations/src/zh-CN.ts
@@ -450,6 +450,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 3ded2234266..c727b29bccc 100644
--- a/packages/localizations/src/zh-TW.ts
+++ b/packages/localizations/src/zh-TW.ts
@@ -456,6 +456,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 9265e8f63b9..6cb2a0438a1 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;
/**
*
@@ -748,6 +748,7 @@ export type ClerkOptions = ClerkOptionsNavigation &
{
persistClient: boolean;
rethrowOfflineNetworkErrors: boolean;
+ combinedFlow: boolean;
},
Record
>;
@@ -900,6 +901,55 @@ export type SignInProps = RoutingOptions & {
* Initial values that are used to prefill the sign in form.
*/
initialValues?: SignInInitialValues;
+ /**
+ * 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; combinedProps?: SignInCombinedProps };
+ /**
+ * Full URL or path to for the waitlist process.
+ * Used to fill the "Join waitlist" link in the SignUp component.
+ */
+ waitlistUrl?: string;
+} & TransferableOption &
+ SignUpForceRedirectUrl &
+ SignUpFallbackRedirectUrl &
+ LegacyRedirectProps &
+ AfterSignOutUrl;
+
+export type SignInCombinedProps = RoutingOptions & {
+ /**
+ * Full URL or path to navigate after successful sign in.
+ * This value has precedence over other redirect props, environment variables or search params.
+ * Use this prop to override the redirect URL when needed.
+ * @default undefined
+ */
+ forceRedirectUrl?: string | null;
+ /**
+ * Full URL or path to navigate after successful sign in.
+ * This value is used when no other redirect props, environment variables or search params are present.
+ * @default undefined
+ */
+ fallbackRedirectUrl?: string | null;
+ /**
+ * Full URL or path to for the sign in process.
+ * Used to fill the "Sign in" link in the SignUp component.
+ */
+ signInUrl?: string;
+ /**
+ * Full URL or path to for the sign up process.
+ * Used to fill the "Sign up" link in the SignUp component.
+ */
+ signUpUrl?: string;
+ /**
+ * Customisation options to fully match the Clerk components to your own brand.
+ * These options serve as overrides and will be merged with the global `appearance`
+ * prop of ClerkProvider (if one is provided)
+ */
+ appearance?: SignInTheme;
+ /**
+ * Initial values that are used to prefill the sign in or up forms.
+ */
+ initialValues?: SignInInitialValues & SignUpInitialValues;
/**
* 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.
*/
@@ -909,6 +959,10 @@ export type SignInProps = RoutingOptions & {
* Used to fill the "Join waitlist" link in the SignUp component.
*/
waitlistUrl?: string;
+ /**
+ * Additional arbitrary metadata to be stored alongside the User object
+ */
+ unsafeMetadata?: SignUpUnsafeMetadata;
} & TransferableOption &
SignUpForceRedirectUrl &
SignUpFallbackRedirectUrl &
diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts
index 076782e9432..302e6115d28 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;