From 8ed1cc9a376193c9240ee55cc87347ef9c7179e6 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 16 Dec 2024 11:02:33 -0500 Subject: [PATCH 01/36] chore: remove experimental check --- packages/clerk-js/src/core/clerk.ts | 2 +- packages/clerk-js/src/ui/common/redirects.ts | 2 +- .../src/ui/components/SignIn/SignInStart.tsx | 6 ++---- .../src/ui/components/SignUp/SignUpContinue.tsx | 13 +++++++++---- .../src/ui/components/SignUp/SignUpStart.tsx | 7 +++---- .../clerk-js/src/ui/contexts/components/SignIn.ts | 3 +++ .../clerk-js/src/ui/contexts/components/SignUp.ts | 3 +++ 7 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index b1cfd5d5868..9d4223eb668 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -366,7 +366,7 @@ export class Clerk implements ClerkInterface { }; #isCombinedFlow(): boolean { - return this.#options.experimental?.combinedFlow && this.#options.signInUrl === this.#options.signUpUrl; + return this.#options.signInUrl === this.#options.signUpUrl; } public signOut: SignOut = async (callbackOrOptions?: SignOutCallback | SignOutOptions, options?: SignOutOptions) => { diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index 347b45bf9a2..55ed6d4a553 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -9,7 +9,7 @@ export function buildEmailLinkRedirectUrl( baseUrl: string | undefined = '', ): string { const { routing, authQueryString, path } = ctx; - const isCombinedFlow = '__experimental' in ctx && ctx.__experimental?.combinedProps; + const isCombinedFlow = 'isCombinedFlow' in ctx && ctx.isCombinedFlow; return buildRedirectUrl({ routing, baseUrl, diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index a35371b199b..edb876b4c07 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, useOptions, useSignInContext } from '../../contexts'; +import { useCoreSignIn, useEnvironment, useSignInContext } from '../../contexts'; import { Col, descriptors, Flow, localizationKeys } from '../../customizables'; import { Card, @@ -66,10 +66,8 @@ 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 { afterSignInUrl, signUpUrl, waitlistUrl, isCombinedFlow } = ctx; const supportEmail = useSupportEmail(); const identifierAttributes = useMemo( () => groupIdentifiers(userSettings.enabledFirstFactorIdentifiers), diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx index 63b3748a2a3..71fb2d6af4b 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 { SignInContext, useCoreSignUp, useEnvironment, useOptions, useSignUpContext } from '../../contexts'; +import { SignInContext, useCoreSignUp, useEnvironment, useSignUpContext } from '../../contexts'; import { descriptors, Flex, Flow, localizationKeys } from '../../customizables'; import { Card, @@ -31,11 +31,16 @@ function _SignUpContinue() { const { navigate } = useRouter(); const { displayConfig, userSettings } = useEnvironment(); const { attributes } = userSettings; - const { afterSignUpUrl, signInUrl, unsafeMetadata, initialValues = {} } = useSignUpContext(); + const { + afterSignUpUrl, + signInUrl, + unsafeMetadata, + initialValues = {}, + isCombinedFlow: _isCombinedFlow, + } = useSignUpContext(); const signUp = useCoreSignUp(); - const options = useOptions(); const isWithinSignInContext = !!React.useContext(SignInContext); - const isCombinedFlow = !!(options.experimental?.combinedFlow && !!isWithinSignInContext); + const isCombinedFlow = !!(_isCombinedFlow && !!isWithinSignInContext); const isProgressiveSignUp = userSettings.signUp.progressive; const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState( getInitialActiveIdentifier(attributes, userSettings.signUp.progressive), diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 3f000a1fe53..c4efcb42e74 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 { SignInContext, useCoreSignUp, useEnvironment, useOptions, useSignUpContext } from '../../contexts'; +import { SignInContext, useCoreSignUp, useEnvironment, useSignUpContext } from '../../contexts'; import { descriptors, Flex, Flow, localizationKeys, useAppearance, useLocalizations } from '../../customizables'; import { Card, @@ -39,10 +39,9 @@ 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 { afterSignUpUrl, signInUrl, unsafeMetadata, isCombinedFlow: _isCombinedFlow } = ctx; + const isCombinedFlow = !!(_isCombinedFlow && !!isWithinSignInContext); const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState( getInitialActiveIdentifier(attributes, userSettings.signUp.progressive), ); diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index e089e0f1b9c..eb3e4d7b051 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -21,6 +21,7 @@ export type SignInContextType = SignInCtx & { afterSignInUrl: string; transferable: boolean; waitlistUrl: string; + isCombinedFlow: boolean; }; export const SignInContext = createContext(null); @@ -32,6 +33,7 @@ export const useSignInContext = (): SignInContextType => { const { queryParams, queryString } = useRouter(); const options = useOptions(); const clerk = useClerk(); + const isCombinedFlow = options.signInUrl === options.signUpUrl; if (context === null || context.componentName !== 'SignIn') { throw new Error(`Clerk: useSignInContext called outside of the mounted SignIn component.`); @@ -96,5 +98,6 @@ export const useSignInContext = (): SignInContextType => { queryParams, initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams }, authQueryString: redirectUrls.toSearchParams().toString(), + isCombinedFlow, }; }; diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts index 643525c4ef9..48af9795061 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -20,6 +20,7 @@ export type SignUpContextType = SignUpCtx & { afterSignUpUrl: string; afterSignInUrl: string; waitlistUrl: string; + isCombinedFlow: boolean; }; export const SignUpContext = createContext(null); @@ -31,6 +32,7 @@ export const useSignUpContext = (): SignUpContextType => { const { queryParams, queryString } = useRouter(); const options = useOptions(); const clerk = useClerk(); + const isCombinedFlow = options.signInUrl === options.signUpUrl; const initialValuesFromQueryParams = useMemo( () => getInitialValuesFromQueryParams(queryString, SIGN_UP_INITIAL_VALUE_KEYS), @@ -87,5 +89,6 @@ export const useSignUpContext = (): SignUpContextType => { queryParams, initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams }, authQueryString: redirectUrls.toSearchParams().toString(), + isCombinedFlow, }; }; From 273f2344345158bbde00f4c7a3d4bc1135da2f6c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 16 Dec 2024 15:53:36 -0500 Subject: [PATCH 02/36] check for empty signUpUrl --- packages/clerk-js/src/core/clerk.ts | 2 +- packages/clerk-js/src/ui/contexts/components/SignIn.ts | 2 +- packages/clerk-js/src/ui/contexts/components/SignUp.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 9d4223eb668..e81f6f2024b 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -366,7 +366,7 @@ export class Clerk implements ClerkInterface { }; #isCombinedFlow(): boolean { - return this.#options.signInUrl === this.#options.signUpUrl; + return !this.#options.signUpUrl; } public signOut: SignOut = async (callbackOrOptions?: SignOutCallback | SignOutOptions, options?: SignOutOptions) => { diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index eb3e4d7b051..461ec7084a8 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -33,7 +33,7 @@ export const useSignInContext = (): SignInContextType => { const { queryParams, queryString } = useRouter(); const options = useOptions(); const clerk = useClerk(); - const isCombinedFlow = options.signInUrl === options.signUpUrl; + const isCombinedFlow = !options.signUpUrl; if (context === null || context.componentName !== 'SignIn') { throw new Error(`Clerk: useSignInContext called outside of the mounted SignIn component.`); diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts index 48af9795061..e37d8256a1a 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -32,7 +32,7 @@ export const useSignUpContext = (): SignUpContextType => { const { queryParams, queryString } = useRouter(); const options = useOptions(); const clerk = useClerk(); - const isCombinedFlow = options.signInUrl === options.signUpUrl; + const isCombinedFlow = !options.signUpUrl; const initialValuesFromQueryParams = useMemo( () => getInitialValuesFromQueryParams(queryString, SIGN_UP_INITIAL_VALUE_KEYS), From 37f3f575f1ebdfab543f83ca3ca0ebcaebd95865 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 16 Dec 2024 15:57:45 -0500 Subject: [PATCH 03/36] Update SignInProps --- packages/types/src/clerk.ts | 44 ------------------------------------- 1 file changed, 44 deletions(-) diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 464886a76fa..d4e19aa2911 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -895,50 +895,6 @@ export type RoutingOptions = | { path?: never; routing?: Extract }; export type SignInProps = 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 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 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. From 79cadb9c377ec8532dffbbb04a5da156073870d4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 16 Dec 2024 17:16:15 -0500 Subject: [PATCH 04/36] update checks --- packages/clerk-js/src/core/clerk.ts | 14 +++++++++----- .../clerk-js/src/ui/components/SignIn/SignIn.tsx | 4 +--- .../clerk-js/src/ui/contexts/components/SignIn.ts | 3 ++- .../clerk-js/src/ui/contexts/components/SignUp.ts | 3 ++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index e81f6f2024b..f7d0601430d 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -6,7 +6,7 @@ import { LocalStorageBroadcastChannel } from '@clerk/shared/localStorageBroadcas import { logger } from '@clerk/shared/logger'; import { isHttpOrHttps, isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/shared/proxy'; import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry'; -import { addClerkPrefix, stripScheme } from '@clerk/shared/url'; +import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; import { handleValueOrFn, noop } from '@clerk/shared/utils'; import type { __internal_UserVerificationModalProps, @@ -365,8 +365,8 @@ export class Clerk implements ClerkInterface { } }; - #isCombinedFlow(): boolean { - return !this.#options.signUpUrl; + #isCombinedSignInOrUpFlow(): boolean { + return Boolean(!this.#options.signUpUrl && this.#options.signInUrl && !isAbsoluteUrl(this.#options.signInUrl)); } public signOut: SignOut = async (callbackOrOptions?: SignOutCallback | SignOutOptions, options?: SignOutOptions) => { @@ -2112,13 +2112,17 @@ export class Clerk implements ClerkInterface { return ''; } - const signInOrUpUrl = this.#options[key] || this.environment.displayConfig[key]; + let signInOrUpUrl = this.#options[key] || this.environment.displayConfig[key]; + if (this.#isCombinedSignInOrUpFlow()) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- The isCombinedSignInOrUpFlow() function checks for the existence of signInUrl + signInOrUpUrl = this.#options.signInUrl!; + } const redirectUrls = new RedirectUrls(this.#options, options).toSearchParams(); const initValues = new URLSearchParams(_initValues || {}); const url = buildURL( { base: signInOrUpUrl, - hashPath: this.#isCombinedFlow() && key === 'signUpUrl' ? '/create' : '', + hashPath: this.#isCombinedSignInOrUpFlow() && key === 'signUpUrl' ? '/create' : '', hashSearchParams: [initValues, redirectUrls], }, { stringify: true }, diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index ea783fc013d..218eb7ba3db 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -6,7 +6,6 @@ import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../ import { SignInContext, SignUpContext, - useOptions, useSignInContext, useSignUpContext, withCoreSessionSwitchGuard, @@ -37,7 +36,6 @@ function RedirectToSignIn() { function SignInRoutes(): JSX.Element { const signInContext = useSignInContext(); const signUpContext = useSignUpContext(); - const options = useOptions(); return ( @@ -76,7 +74,7 @@ function SignInRoutes(): JSX.Element { redirectUrl='../factor-two' /> - {options.experimental?.combinedFlow && ( + {signInContext.isCombinedFlow && ( { const { queryParams, queryString } = useRouter(); const options = useOptions(); const clerk = useClerk(); - const isCombinedFlow = !options.signUpUrl; + const isCombinedFlow = Boolean(!options.signUpUrl && options.signInUrl && !isAbsoluteUrl(options.signInUrl)); if (context === null || context.componentName !== 'SignIn') { throw new Error(`Clerk: useSignInContext called outside of the mounted SignIn component.`); diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts index e37d8256a1a..30ed891f886 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -1,4 +1,5 @@ import { useClerk } from '@clerk/shared/react'; +import { isAbsoluteUrl } from '@clerk/shared/url'; import { createContext, useContext, useMemo } from 'react'; import { SIGN_UP_INITIAL_VALUE_KEYS } from '../../../core/constants'; @@ -32,7 +33,7 @@ export const useSignUpContext = (): SignUpContextType => { const { queryParams, queryString } = useRouter(); const options = useOptions(); const clerk = useClerk(); - const isCombinedFlow = !options.signUpUrl; + const isCombinedFlow = Boolean(!options.signUpUrl && options.signInUrl && !isAbsoluteUrl(options.signInUrl)); const initialValuesFromQueryParams = useMemo( () => getInitialValuesFromQueryParams(queryString, SIGN_UP_INITIAL_VALUE_KEYS), From 454f1cf2c19753015635c4f111f517ce1265da38 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 9 Jan 2025 10:37:01 -0500 Subject: [PATCH 05/36] add changeset --- .changeset/two-doors-visit.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/two-doors-visit.md diff --git a/.changeset/two-doors-visit.md b/.changeset/two-doors-visit.md new file mode 100644 index 00000000000..2f8f285cae4 --- /dev/null +++ b/.changeset/two-doors-visit.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/types': minor +--- + +Introduce sign-in-or-up flow. From ec51e412c79206afe11b4d4b3a17c4a02d849681 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 9 Jan 2025 11:25:25 -0500 Subject: [PATCH 06/36] update tests --- integration/presets/envs.ts | 4 +--- integration/tests/combined-sign-up-flow.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 068a9845b5b..348415808b5 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -118,9 +118,7 @@ const withCombinedFlow = withEmailCodes .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'); + .setEnvVariable('public', 'CLERK_SIGN_IN_URL', '/sign-in'); export const envs = { base, diff --git a/integration/tests/combined-sign-up-flow.test.ts b/integration/tests/combined-sign-up-flow.test.ts index 93818d0b95b..b0a5e0d09b3 100644 --- a/integration/tests/combined-sign-up-flow.test.ts +++ b/integration/tests/combined-sign-up-flow.test.ts @@ -23,7 +23,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCombinedFlow] })('combine // Fill in sign in form await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in/create'); + await u.page.waitForAppUrl('/sign-in#create'); const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); expect(prefilledEmail).toBe(fakeUser.email); @@ -51,7 +51,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCombinedFlow] })('combine await u.po.signIn.goTo(); await u.po.signIn.setIdentifier(fakeUser.username); await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in/create'); + await u.page.waitForAppUrl('/sign-in#create'); const prefilledUsername = await u.po.signUp.getUsernameInput().inputValue(); expect(prefilledUsername).toBe(fakeUser.username); @@ -81,7 +81,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCombinedFlow] })('combine // Fill in sign in form await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in/create'); + await u.page.waitForAppUrl('/sign-in#create'); const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); expect(prefilledEmail).toBe(fakeUser.email); From aa0245ab2b072be76421fbd439968d5790f37603 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 9 Jan 2025 11:37:58 -0500 Subject: [PATCH 07/36] fix path --- integration/tests/combined-sign-up-flow.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration/tests/combined-sign-up-flow.test.ts b/integration/tests/combined-sign-up-flow.test.ts index b0a5e0d09b3..93818d0b95b 100644 --- a/integration/tests/combined-sign-up-flow.test.ts +++ b/integration/tests/combined-sign-up-flow.test.ts @@ -23,7 +23,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCombinedFlow] })('combine // Fill in sign in form await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in#create'); + await u.page.waitForAppUrl('/sign-in/create'); const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); expect(prefilledEmail).toBe(fakeUser.email); @@ -51,7 +51,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCombinedFlow] })('combine await u.po.signIn.goTo(); await u.po.signIn.setIdentifier(fakeUser.username); await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in#create'); + await u.page.waitForAppUrl('/sign-in/create'); const prefilledUsername = await u.po.signUp.getUsernameInput().inputValue(); expect(prefilledUsername).toBe(fakeUser.username); @@ -81,7 +81,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCombinedFlow] })('combine // Fill in sign in form await u.po.signIn.setIdentifier(fakeUser.email); await u.po.signIn.continue(); - await u.page.waitForAppUrl('/sign-in#create'); + await u.page.waitForAppUrl('/sign-in/create'); const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); expect(prefilledEmail).toBe(fakeUser.email); From efc25c1915f6fe76bbecde12d6d446a6971979bd Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 9 Jan 2025 11:43:19 -0500 Subject: [PATCH 08/36] remove sign_up_url --- integration/presets/envs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 348415808b5..6d7f2f2b972 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -118,7 +118,7 @@ const withCombinedFlow = withEmailCodes .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', 'CLERK_SIGN_IN_URL', '/sign-in'); + .setEnvVariable('public', 'CLERK_SIGN_UP_URL', undefined); export const envs = { base, From 39aae7da125559491a3fe6cd3b709ef3bdaa7fac Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 13 Jan 2025 14:23:22 -0500 Subject: [PATCH 09/36] feat(clerk-js,react,types): Add option to enable combined flow at component level (#4799) --- .changeset/cuddly-shrimps-fold.md | 7 +++++ .../sign-in-or-up/[[...catchall]]/page.tsx | 15 +++++++++++ .../src/app/sign-in/[[...catchall]]/page.tsx | 3 --- .../tests/sign-in-or-up-component.test.ts | 27 +++++++++++++++++++ .../src/ui/contexts/components/SignIn.ts | 8 ++++-- .../react/src/components/SignInButton.tsx | 2 ++ packages/react/src/types.ts | 1 + packages/types/src/clerk.ts | 4 +++ 8 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 .changeset/cuddly-shrimps-fold.md create mode 100644 integration/templates/next-app-router/src/app/sign-in-or-up/[[...catchall]]/page.tsx create mode 100644 integration/tests/sign-in-or-up-component.test.ts diff --git a/.changeset/cuddly-shrimps-fold.md b/.changeset/cuddly-shrimps-fold.md new file mode 100644 index 00000000000..cc4c35984a8 --- /dev/null +++ b/.changeset/cuddly-shrimps-fold.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': patch +'@clerk/clerk-react': patch +'@clerk/types': patch +--- + +Add option to allow a `` component instance to opt into the sign-in-or-up flow. diff --git a/integration/templates/next-app-router/src/app/sign-in-or-up/[[...catchall]]/page.tsx b/integration/templates/next-app-router/src/app/sign-in-or-up/[[...catchall]]/page.tsx new file mode 100644 index 00000000000..f6c920b4e61 --- /dev/null +++ b/integration/templates/next-app-router/src/app/sign-in-or-up/[[...catchall]]/page.tsx @@ -0,0 +1,15 @@ +import { SignIn } from '@clerk/nextjs'; + +export default function Page() { + return ( +
+ Loading sign in} + withSignUp + /> +
+ ); +} diff --git a/integration/templates/next-app-router/src/app/sign-in/[[...catchall]]/page.tsx b/integration/templates/next-app-router/src/app/sign-in/[[...catchall]]/page.tsx index d574c6244f1..d193e28a464 100644 --- a/integration/templates/next-app-router/src/app/sign-in/[[...catchall]]/page.tsx +++ b/integration/templates/next-app-router/src/app/sign-in/[[...catchall]]/page.tsx @@ -8,9 +8,6 @@ export default function Page() { path={'/sign-in'} signUpUrl={'/sign-up'} fallback={<>Loading sign in} - __experimental={{ - combinedProps: {}, - }} /> ); diff --git a/integration/tests/sign-in-or-up-component.test.ts b/integration/tests/sign-in-or-up-component.test.ts new file mode 100644 index 00000000000..94dd73df059 --- /dev/null +++ b/integration/tests/sign-in-or-up-component.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from '@playwright/test'; + +import type { Application } from '../models/application'; +import { appConfigs } from '../presets'; +import { createTestUtils } from '../testUtils'; + +test.describe('sign-in-or-up component initialization flow @nextjs', () => { + test.describe.configure({ mode: 'parallel' }); + let app: Application; + + test.beforeAll(async () => { + app = await appConfigs.next.appRouter.clone().commit(); + await app.setup(); + await app.withEnv(appConfigs.envs.withEmailCodes); + await app.dev(); + }); + + test.afterAll(async () => { + await app.teardown(); + }); + + test('flows are combined', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/sign-in-or-up'); + await expect(u.page.getByText(`Don’t have an account?`)).toBeHidden(); + }); +}); diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 6ac8eb69171..60d811b4d5b 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -23,9 +23,9 @@ export type SignInContextType = SignInCtx & { afterSignInUrl: string; transferable: boolean; waitlistUrl: string; - isCombinedFlow: boolean; emailLinkRedirectUrl: string; ssoCallbackUrl: string; + isCombinedFlow: boolean; }; export const SignInContext = createContext(null); @@ -37,12 +37,16 @@ export const useSignInContext = (): SignInContextType => { const { queryParams, queryString } = useRouter(); const options = useOptions(); const clerk = useClerk(); - const isCombinedFlow = Boolean(!options.signUpUrl && options.signInUrl && !isAbsoluteUrl(options.signInUrl)); if (context === null || context.componentName !== 'SignIn') { throw new Error(`Clerk: useSignInContext called outside of the mounted SignIn component.`); } + const isCombinedFlow = + Boolean(!options.signUpUrl && options.signInUrl && !isAbsoluteUrl(options.signInUrl)) || + context.withSignUp || + false; + const { componentName, mode, ..._ctx } = context; const ctx = _ctx.__experimental?.combinedProps ? { ..._ctx, ..._ctx.__experimental?.combinedProps } : _ctx; const initialValuesFromQueryParams = useMemo( diff --git a/packages/react/src/components/SignInButton.tsx b/packages/react/src/components/SignInButton.tsx index 1036cbef43a..a18ba92e57c 100644 --- a/packages/react/src/components/SignInButton.tsx +++ b/packages/react/src/components/SignInButton.tsx @@ -14,6 +14,7 @@ export const SignInButton = withClerk( signUpForceRedirectUrl, mode, initialValues, + withSignUp, ...rest } = props; children = normalizeWithDefaultValue(children, 'Sign in'); @@ -26,6 +27,7 @@ export const SignInButton = withClerk( signUpFallbackRedirectUrl, signUpForceRedirectUrl, initialValues, + withSignUp, }; if (mode === 'modal') { diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 0c732dbc1c7..cab42fd9950 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -122,6 +122,7 @@ export type SignInButtonProps = ButtonProps & | 'signUpForceRedirectUrl' | 'signUpFallbackRedirectUrl' | 'initialValues' + | 'withSignUp' >; export type SignUpButtonProps = { diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index bcbb673e2b5..998a7a6cd88 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -940,6 +940,10 @@ export type SignInProps = RoutingOptions & { * Additional arbitrary metadata to be stored alongside the User object */ unsafeMetadata?: SignUpUnsafeMetadata; + /** + * Enable sign-in-or-up flow for `` component instance. + */ + withSignUp?: boolean; } & TransferableOption & SignUpForceRedirectUrl & SignUpFallbackRedirectUrl & From 5dc7940ea0b7408722b3ff2c6ded6de6950ed6a6 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 14 Jan 2025 09:06:00 -0500 Subject: [PATCH 10/36] refactor(e2e): Update sign-in-or-up tests (#4872) Co-authored-by: Dylan Staley <88163+dstaley@users.noreply.github.com> --- integration/presets/envs.ts | 14 +- integration/presets/longRunningApps.ts | 7 +- .../tests/combined-sign-in-flow.test.ts | 160 ---------- .../tests/combined-sign-up-flow.test.ts | 122 -------- integration/tests/sign-in-flow.test.ts | 2 +- .../sign-in-or-up-email-links-flow.test.ts | 69 +++++ integration/tests/sign-in-or-up-flow.test.ts | 286 ++++++++++++++++++ 7 files changed, 371 insertions(+), 289 deletions(-) delete mode 100644 integration/tests/combined-sign-in-flow.test.ts delete mode 100644 integration/tests/combined-sign-up-flow.test.ts create mode 100644 integration/tests/sign-in-or-up-email-links-flow.test.ts create mode 100644 integration/tests/sign-in-or-up-flow.test.ts diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 6d7f2f2b972..5d60c29d114 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -113,11 +113,14 @@ 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 +const withSignInOrUpFlow = 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) + .setId('withSignInOrUpFlow') + .setEnvVariable('public', 'CLERK_SIGN_UP_URL', undefined); + +const withSignInOrUpEmailLinksFlow = withEmailLinks + .clone() + .setId('withSignInOrUpEmailLinksFlow') .setEnvVariable('public', 'CLERK_SIGN_UP_URL', undefined); export const envs = { @@ -136,5 +139,6 @@ export const envs = { withRestrictedMode, withLegalConsent, withWaitlistdMode, - withCombinedFlow, + withSignInOrUpFlow, + withSignInOrUpEmailLinksFlow, } as const; diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index cd1f27cc19e..d5573f015e0 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -31,7 +31,12 @@ 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: 'next.appRouter.withSignInOrUpFlow', config: next.appRouter, env: envs.withSignInOrUpFlow }, + { + id: 'next.appRouter.withSignInOrUpEmailLinksFlow', + config: next.appRouter, + env: envs.withSignInOrUpEmailLinksFlow, + }, { 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/tests/combined-sign-in-flow.test.ts b/integration/tests/combined-sign-in-flow.test.ts deleted file mode 100644 index f0afacb4c4e..00000000000 --- a/integration/tests/combined-sign-in-flow.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -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 you entered 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 you entered 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 deleted file mode 100644 index 93818d0b95b..00000000000 --- a/integration/tests/combined-sign-up-flow.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -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(); - await u.page.waitForAppUrl('/sign-in/create'); - - const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); - expect(prefilledEmail).toBe(fakeUser.email); - - await u.po.signUp.setPassword(fakeUser.password); - await u.po.signUp.continue(); - - // Verify email - await u.po.signUp.enterTestOtpCode(); - - // 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, - withPassword: 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(); - await u.page.waitForAppUrl('/sign-in/create'); - - const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); - expect(prefilledEmail).toBe(fakeUser.email); - - await u.po.signUp.setPassword(fakeUser.password); - await u.po.signUp.continue(); - - // Verify email - await u.po.signUp.enterTestOtpCode(); - - // 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/integration/tests/sign-in-flow.test.ts b/integration/tests/sign-in-flow.test.ts index 29243df5c45..1a6a833cd87 100644 --- a/integration/tests/sign-in-flow.test.ts +++ b/integration/tests/sign-in-flow.test.ts @@ -141,7 +141,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in f await u.po.expect.toBeSignedIn(); }); - test('access protected page @express', async ({ page, context }) => { + test('access protected page', 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 }); diff --git a/integration/tests/sign-in-or-up-email-links-flow.test.ts b/integration/tests/sign-in-or-up-email-links-flow.test.ts new file mode 100644 index 00000000000..a49a867e1f7 --- /dev/null +++ b/integration/tests/sign-in-or-up-email-links-flow.test.ts @@ -0,0 +1,69 @@ +import { expect, test } from '@playwright/test'; + +import { appConfigs } from '../presets'; +import type { FakeUser } from '../testUtils'; +import { createTestUtils, testAgainstRunningApps } from '../testUtils'; + +testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpEmailLinksFlow] })( + 'sign-in-or-up email links flow @nextjs', + ({ app }) => { + test.describe.configure({ mode: 'serial' }); + + let fakeUser: FakeUser; + + test.beforeAll(() => { + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser({ + fictionalEmail: false, + withPassword: true, + }); + }); + + test.afterAll(async () => { + await app.teardown(); + }); + + test('sign up with email link', 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.page.waitForAppUrl('/sign-in/create'); + + const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); + expect(prefilledEmail).toBe(fakeUser.email); + + await u.po.signUp.setPassword(fakeUser.password); + await u.po.signUp.continue(); + + await u.po.signUp.waitForEmailVerificationScreen(); + await u.tabs.runInNewTab(async u => { + const verificationLink = await u.services.email.getVerificationLinkForEmailAddress(fakeUser.email); + await u.page.goto(verificationLink); + await u.po.expect.toBeSignedIn(); + await u.page.close(); + }); + await u.po.expect.toBeSignedIn(); + }); + + test('sign in with email link', 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.page.waitForAppUrl('/sign-in/factor-one'); + // Defaults to password, so we need to switch to email link + await u.page.getByRole('link', { name: /Use another method/i }).click(); + await u.page.getByRole('button', { name: /Email link to/i }).click(); + await page.getByRole('heading', { name: /Check your email/i }).waitFor(); + await u.tabs.runInNewTab(async u => { + const verificationLink = await u.services.email.getVerificationLinkForEmailAddress(fakeUser.email); + await u.page.goto(verificationLink); + await u.po.expect.toBeSignedIn(); + await u.page.close(); + }); + await u.po.expect.toBeSignedIn(); + await fakeUser.deleteIfExists(); + }); + }, +); diff --git a/integration/tests/sign-in-or-up-flow.test.ts b/integration/tests/sign-in-or-up-flow.test.ts new file mode 100644 index 00000000000..2fdcbb55723 --- /dev/null +++ b/integration/tests/sign-in-or-up-flow.test.ts @@ -0,0 +1,286 @@ +import { expect, test } from '@playwright/test'; + +import { appConfigs } from '../presets'; +import { createTestUtils, type FakeUser, testAgainstRunningApps } from '../testUtils'; + +testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('sign-in-or-up flow @nextjs', ({ app }) => { + test.describe.configure({ mode: 'serial' }); + + test.afterAll(async () => { + await app.teardown(); + }); + + test.describe('sign-in', () => { + 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(); + }); + + 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 you entered 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 you entered 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', 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'"); + }); + + 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(); + await u.page.waitForAppUrl('/sign-in/create'); + + const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); + expect(prefilledEmail).toBe(fakeUser.email); + + await u.po.signUp.setPassword(fakeUser.password); + await u.po.signUp.continue(); + + // Verify email + await u.po.signUp.enterTestOtpCode(); + + // Check if user is signed in + await u.po.expect.toBeSignedIn(); + + await fakeUser.deleteIfExists(); + }); + }); + + test.describe('sign-up', () => { + 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, + withPassword: 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(); + await u.page.waitForAppUrl('/sign-in/create'); + + const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); + expect(prefilledEmail).toBe(fakeUser.email); + + await u.po.signUp.setPassword(fakeUser.password); + await u.po.signUp.continue(); + + // Verify email + await u.po.signUp.enterTestOtpCode(); + + // 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(); + }); + + test('sign in with ticket renders sign up', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo({ + searchParams: new URLSearchParams({ __clerk_ticket: '123', __clerk_status: 'sign_up' }), + }); + await u.page.waitForAppUrl('/sign-in/create'); + await expect(u.page.getByText(`Create your account`)).toBeVisible(); + }); + }); +}); From a549423f39221b3eccaccd583b288caf51155577 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 14 Jan 2025 09:47:15 -0500 Subject: [PATCH 11/36] add express tag --- integration/tests/sign-in-flow.test.ts | 289 +++++++++++++------------ 1 file changed, 146 insertions(+), 143 deletions(-) diff --git a/integration/tests/sign-in-flow.test.ts b/integration/tests/sign-in-flow.test.ts index 1a6a833cd87..d7c5760e06d 100644 --- a/integration/tests/sign-in-flow.test.ts +++ b/integration/tests/sign-in-flow.test.ts @@ -4,151 +4,154 @@ import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in flow @generic @nextjs', ({ app }) => { - test.describe.configure({ mode: 'serial' }); +testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })( + 'sign in flow @generic @nextjs @express', + ({ 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(); + }); - let fakeUser: FakeUser; + 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.beforeAll(async () => { - const u = createTestUtils({ app }); - fakeUser = u.services.users.createFakeUser({ - withPhoneNumber: true, - withUsername: true, + 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(); }); - await u.services.users.createBapiUser(fakeUser); - }); - - test.afterAll(async () => { - await fakeUser.deleteIfExists(); - await app.teardown(); - }); - - 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, + + 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(); }); - 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, + + 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 you entered 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 you entered 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', 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'"); }); - 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 you entered 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 you entered 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', 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'"); - }); -}); + }, +); From 0e6fbcb7ceaa0fb20e5b56983a02367afee09a5d Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 14 Jan 2025 09:59:30 -0500 Subject: [PATCH 12/36] move back to protected page test --- integration/tests/sign-in-flow.test.ts | 289 ++++++++++++------------- 1 file changed, 143 insertions(+), 146 deletions(-) diff --git a/integration/tests/sign-in-flow.test.ts b/integration/tests/sign-in-flow.test.ts index d7c5760e06d..29243df5c45 100644 --- a/integration/tests/sign-in-flow.test.ts +++ b/integration/tests/sign-in-flow.test.ts @@ -4,154 +4,151 @@ import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })( - 'sign in flow @generic @nextjs @express', - ({ 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(); - }); +testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in flow @generic @nextjs', ({ app }) => { + test.describe.configure({ mode: 'serial' }); - 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(); - }); + let fakeUser: FakeUser; - 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.beforeAll(async () => { + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser({ + withPhoneNumber: true, + withUsername: true, }); - - 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(); + await u.services.users.createBapiUser(fakeUser); + }); + + test.afterAll(async () => { + await fakeUser.deleteIfExists(); + await app.teardown(); + }); + + 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, }); - - 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 you entered 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 you entered 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', 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'"); + 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 you entered 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 you entered 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'"); + }); +}); From d7e23834bf1688fa5e462fdaefc2510a66860cb4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 14 Jan 2025 11:55:33 -0500 Subject: [PATCH 13/36] remove combined props usage --- packages/clerk-js/src/ui/components/SignIn/SignIn.tsx | 1 - packages/clerk-js/src/ui/contexts/components/SignIn.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 20aac20f096..ebabb568944 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -147,7 +147,6 @@ function SignInRoot() { const signInContext = useSignInContext(); const normalizedSignUpContext = { componentName: 'SignUp', - ...signInContext.__experimental?.combinedProps, emailLinkRedirectUrl: signInContext.emailLinkRedirectUrl, ssoCallbackUrl: signInContext.ssoCallbackUrl, ...normalizeRoutingOptions({ routing: signInContext?.routing, path: signInContext?.path }), diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 60d811b4d5b..0e06f1caa43 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -47,8 +47,7 @@ export const useSignInContext = (): SignInContextType => { context.withSignUp || false; - const { componentName, mode, ..._ctx } = context; - const ctx = _ctx.__experimental?.combinedProps ? { ..._ctx, ..._ctx.__experimental?.combinedProps } : _ctx; + const { componentName, mode, ...ctx } = context; const initialValuesFromQueryParams = useMemo( () => getInitialValuesFromQueryParams(queryString, SIGN_IN_INITIAL_VALUE_KEYS), [], From a8deaa3471ff584c8c33b76222106c73a712dd92 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:10:40 -0800 Subject: [PATCH 14/36] fix(clerk-js): Use correct email link redirect_url based on intent (#4892) --- .../src/ui/common/__tests__/redirects.test.ts | 140 ++++++++++++------ packages/clerk-js/src/ui/common/redirects.ts | 16 +- .../SignIn/SignInFactorOneEmailLinkCard.tsx | 2 +- .../components/UserProfile/VerifyWithLink.tsx | 2 +- 4 files changed, 106 insertions(+), 54 deletions(-) diff --git a/packages/clerk-js/src/ui/common/__tests__/redirects.test.ts b/packages/clerk-js/src/ui/common/__tests__/redirects.test.ts index 20b32825fd4..a706b340de6 100644 --- a/packages/clerk-js/src/ui/common/__tests__/redirects.test.ts +++ b/packages/clerk-js/src/ui/common/__tests__/redirects.test.ts @@ -2,128 +2,174 @@ import { buildEmailLinkRedirectUrl, buildSSOCallbackURL } from '../redirects'; describe('buildEmailLinkRedirectUrl(routing, baseUrl)', () => { it('defaults to hash based routing strategy on empty routing', function () { - expect(buildEmailLinkRedirectUrl({ path: '', authQueryString: '' } as any, '')).toBe('http://localhost/#/verify'); + expect( + buildEmailLinkRedirectUrl({ ctx: { path: '', authQueryString: '' } as any, baseUrl: '', intent: 'sign-in' }), + ).toBe('http://localhost/#/verify'); }); it('returns the magic link redirect url for components using path based routing ', function () { - expect(buildEmailLinkRedirectUrl({ routing: 'path', authQueryString: '' } as any, '')).toBe( - 'http://localhost/verify', - ); + expect( + buildEmailLinkRedirectUrl({ + ctx: { routing: 'path', authQueryString: '' } as any, + baseUrl: '', + intent: 'sign-in', + }), + ).toBe('http://localhost/verify'); - expect(buildEmailLinkRedirectUrl({ routing: 'path', path: '/sign-in', authQueryString: '' } as any, '')).toBe( - 'http://localhost/sign-in/verify', - ); + expect( + buildEmailLinkRedirectUrl({ + ctx: { routing: 'path', path: '/sign-in', authQueryString: '' } as any, + baseUrl: '', + intent: 'sign-in', + }), + ).toBe('http://localhost/sign-in/verify'); expect( - buildEmailLinkRedirectUrl( - { + buildEmailLinkRedirectUrl({ + ctx: { routing: 'path', path: '', authQueryString: 'redirectUrl=https://clerk.com', } as any, - '', - ), + baseUrl: '', + intent: 'sign-in', + }), ).toBe('http://localhost/verify?redirectUrl=https://clerk.com'); expect( - buildEmailLinkRedirectUrl( - { + buildEmailLinkRedirectUrl({ + ctx: { routing: 'path', path: '/sign-in', authQueryString: 'redirectUrl=https://clerk.com', } as any, - '', - ), + baseUrl: '', + intent: 'sign-in', + }), ).toBe('http://localhost/sign-in/verify?redirectUrl=https://clerk.com'); expect( - buildEmailLinkRedirectUrl( - { + buildEmailLinkRedirectUrl({ + ctx: { routing: 'path', path: '/sign-in', authQueryString: 'redirectUrl=https://clerk.com', } as any, - 'https://accounts.clerk.com/sign-in', - ), + baseUrl: 'https://accounts.clerk.com/sign-in', + intent: 'sign-in', + }), ).toBe('http://localhost/sign-in/verify?redirectUrl=https://clerk.com'); }); it('returns the magic link redirect url for components using hash based routing ', function () { expect( - buildEmailLinkRedirectUrl( - { + buildEmailLinkRedirectUrl({ + ctx: { routing: 'hash', authQueryString: '', } as any, - '', - ), + baseUrl: '', + intent: 'sign-in', + }), ).toBe('http://localhost/#/verify'); expect( - buildEmailLinkRedirectUrl( - { + buildEmailLinkRedirectUrl({ + ctx: { routing: 'hash', path: '/sign-in', authQueryString: null, } as any, - '', - ), + baseUrl: '', + intent: 'sign-in', + }), ).toBe('http://localhost/#/verify'); expect( - buildEmailLinkRedirectUrl( - { + buildEmailLinkRedirectUrl({ + ctx: { routing: 'hash', path: '', authQueryString: 'redirectUrl=https://clerk.com', } as any, - '', - ), + baseUrl: '', + intent: 'sign-in', + }), ).toBe('http://localhost/#/verify?redirectUrl=https://clerk.com'); expect( - buildEmailLinkRedirectUrl( - { + buildEmailLinkRedirectUrl({ + ctx: { routing: 'hash', path: '/sign-in', authQueryString: 'redirectUrl=https://clerk.com', } as any, - '', - ), + baseUrl: '', + intent: 'sign-in', + }), ).toBe('http://localhost/#/verify?redirectUrl=https://clerk.com'); expect( - buildEmailLinkRedirectUrl( - { + buildEmailLinkRedirectUrl({ + ctx: { routing: 'hash', path: '/sign-in', authQueryString: 'redirectUrl=https://clerk.com', } as any, - 'https://accounts.clerk.com/sign-in', - ), + baseUrl: 'https://accounts.clerk.com/sign-in', + intent: 'sign-in', + }), ).toBe('http://localhost/#/verify?redirectUrl=https://clerk.com'); }); it('returns the magic link redirect url for components using virtual routing ', function () { expect( - buildEmailLinkRedirectUrl( - { + buildEmailLinkRedirectUrl({ + ctx: { routing: 'virtual', authQueryString: 'redirectUrl=https://clerk.com', } as any, - 'https://accounts.clerk.com/sign-in', - ), + baseUrl: 'https://accounts.clerk.com/sign-in', + intent: 'sign-in', + }), ).toBe('https://accounts.clerk.com/sign-in#/verify?redirectUrl=https://clerk.com'); expect( - buildEmailLinkRedirectUrl( - { + buildEmailLinkRedirectUrl({ + ctx: { routing: 'virtual', } as any, - 'https://accounts.clerk.com/sign-in', - ), + baseUrl: 'https://accounts.clerk.com/sign-in', + intent: 'sign-in', + }), ).toBe('https://accounts.clerk.com/sign-in#/verify'); }); + + it('returns the magic link redirect url for components using the combined flow based on intent', function () { + expect( + buildEmailLinkRedirectUrl({ + ctx: { + routing: 'path', + path: '/sign-up', + isCombinedFlow: true, + } as any, + baseUrl: '', + intent: 'sign-up', + }), + ).toBe('http://localhost/sign-up/create/verify'); + + expect( + buildEmailLinkRedirectUrl({ + ctx: { + routing: 'path', + path: '/sign-in', + isCombinedFlow: true, + } as any, + baseUrl: '', + intent: 'sign-in', + }), + ).toBe('http://localhost/sign-in/verify'); + }); }); describe('buildSSOCallbackURL(ctx, baseUrl)', () => { diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index 02d162787fb..a708887d9aa 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -4,10 +4,15 @@ import type { SignInContextType, SignUpContextType, UserProfileContextType } fro export const SSO_CALLBACK_PATH_ROUTE = '/sso-callback'; export const MAGIC_LINK_VERIFY_PATH_ROUTE = '/verify'; -export function buildEmailLinkRedirectUrl( - ctx: SignInContextType | SignUpContextType | UserProfileContextType, - baseUrl: string | undefined = '', -): string { +export function buildEmailLinkRedirectUrl({ + ctx, + baseUrl = '', + intent = 'sign-in', +}: { + ctx: SignInContextType | SignUpContextType | UserProfileContextType; + baseUrl: string | undefined; + intent?: 'sign-in' | 'sign-up' | 'profile'; +}): string { const { routing, authQueryString, path } = ctx; const isCombinedFlow = 'isCombinedFlow' in ctx && ctx.isCombinedFlow; return buildRedirectUrl({ @@ -15,7 +20,8 @@ export function buildEmailLinkRedirectUrl( baseUrl, authQueryString, path, - endpoint: isCombinedFlow ? `/create${MAGIC_LINK_VERIFY_PATH_ROUTE}` : MAGIC_LINK_VERIFY_PATH_ROUTE, + endpoint: + isCombinedFlow && intent === 'sign-up' ? `/create${MAGIC_LINK_VERIFY_PATH_ROUTE}` : MAGIC_LINK_VERIFY_PATH_ROUTE, }); } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx index f9275a02837..54e7c6ad006 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInFactorOneEmailLinkCard.tsx @@ -45,7 +45,7 @@ export const SignInFactorOneEmailLinkCard = (props: SignInFactorOneEmailLinkCard const startEmailLinkVerification = () => { startEmailLinkFlow({ emailAddressId: props.factor.emailAddressId, - redirectUrl: buildEmailLinkRedirectUrl(signInContext, signInUrl), + redirectUrl: buildEmailLinkRedirectUrl({ ctx: signInContext, baseUrl: signInUrl, intent: 'sign-in' }), }) .then(res => handleVerificationResult(res)) .catch(err => { diff --git a/packages/clerk-js/src/ui/components/UserProfile/VerifyWithLink.tsx b/packages/clerk-js/src/ui/components/UserProfile/VerifyWithLink.tsx index 4c693b71936..be2213d71da 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/VerifyWithLink.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/VerifyWithLink.tsx @@ -37,7 +37,7 @@ export const VerifyWithLink = (props: VerifyWithLinkProps) => { const { routing } = profileContext; const baseUrl = routing === 'virtual' ? displayConfig.userProfileUrl : ''; - const redirectUrl = buildEmailLinkRedirectUrl(profileContext, baseUrl); + const redirectUrl = buildEmailLinkRedirectUrl({ ctx: profileContext, baseUrl, intent: 'profile' }); startEmailLinkFlow({ redirectUrl }) .then(() => nextStep()) .catch(err => handleError(err, [], card.setError)); From d516c26d11fc1708404e6a248395f188dbb2f6bc Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 14 Jan 2025 15:02:28 -0500 Subject: [PATCH 15/36] fix email link paths --- packages/clerk-js/src/ui/components/SignIn/SignIn.tsx | 2 ++ packages/clerk-js/src/ui/contexts/components/SignIn.ts | 6 ++---- packages/clerk-js/src/ui/contexts/components/SignUp.ts | 6 ++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index ebabb568944..cd26298bd7f 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -149,6 +149,8 @@ function SignInRoot() { componentName: 'SignUp', emailLinkRedirectUrl: signInContext.emailLinkRedirectUrl, ssoCallbackUrl: signInContext.ssoCallbackUrl, + forceRedirectUrl: signInContext.signUpForceRedirectUrl, + fallbackRedirectUrl: signInContext.signUpFallbackRedirectUrl, ...normalizeRoutingOptions({ routing: signInContext?.routing, path: signInContext?.path }), } as SignUpContextType; diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 0e06f1caa43..fed4ad6e959 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -87,16 +87,14 @@ export const useSignInContext = (): SignInContextType => { baseUrl: signUpUrl, authQueryString: '', path: ctx.path, - endpoint: options.experimental?.combinedFlow - ? '/create' + MAGIC_LINK_VERIFY_PATH_ROUTE - : MAGIC_LINK_VERIFY_PATH_ROUTE, + endpoint: isCombinedFlow ? '/create' + MAGIC_LINK_VERIFY_PATH_ROUTE : MAGIC_LINK_VERIFY_PATH_ROUTE, }); const ssoCallbackUrl = buildRedirectUrl({ routing: ctx.routing, baseUrl: signUpUrl, authQueryString: '', path: ctx.path, - endpoint: options.experimental?.combinedFlow ? '/create' + SSO_CALLBACK_PATH_ROUTE : SSO_CALLBACK_PATH_ROUTE, + endpoint: isCombinedFlow ? '/create' + SSO_CALLBACK_PATH_ROUTE : SSO_CALLBACK_PATH_ROUTE, }); if (isCombinedFlow) { diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts index c1e112e612a..b34ce7a8171 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -84,9 +84,7 @@ export const useSignUpContext = (): SignUpContextType => { baseUrl: signUpUrl, authQueryString: '', path: ctx.path, - endpoint: options.experimental?.combinedFlow - ? '/create' + MAGIC_LINK_VERIFY_PATH_ROUTE - : MAGIC_LINK_VERIFY_PATH_ROUTE, + endpoint: isCombinedFlow ? '/create' + MAGIC_LINK_VERIFY_PATH_ROUTE : MAGIC_LINK_VERIFY_PATH_ROUTE, }); const ssoCallbackUrl = ctx.ssoCallbackUrl ?? @@ -95,7 +93,7 @@ export const useSignUpContext = (): SignUpContextType => { baseUrl: signUpUrl, authQueryString: '', path: ctx.path, - endpoint: options.experimental?.combinedFlow ? '/create' + SSO_CALLBACK_PATH_ROUTE : SSO_CALLBACK_PATH_ROUTE, + endpoint: isCombinedFlow ? '/create' + SSO_CALLBACK_PATH_ROUTE : SSO_CALLBACK_PATH_ROUTE, }); // TODO: Avoid building this url again to remove duplicate code. Get it from window.Clerk instead. From e14de2adbf05800248165a5535923368e00e7a48 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 10:07:36 -0500 Subject: [PATCH 16/36] Update packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx Co-authored-by: Jacek Radko --- packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 3b4638ed4a4..7279d2c074e 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -40,8 +40,8 @@ function _SignUpStart(): JSX.Element { const { setActive } = useClerk(); const ctx = useSignUpContext(); const isWithinSignInContext = !!React.useContext(SignInContext); - const { afterSignUpUrl, signInUrl, unsafeMetadata, isCombinedFlow: _isCombinedFlow } = ctx; - const isCombinedFlow = !!(_isCombinedFlow && !!isWithinSignInContext); + const { afterSignUpUrl, signInUrl, unsafeMetadata } = ctx; + const isCombinedFlow = !!(ctx.isCombinedFlow && !!isWithinSignInContext); const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState( getInitialActiveIdentifier(attributes, userSettings.signUp.progressive), ); From 055fdb72d811d47761f5e70b9592a44f6e557e2b Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 10:07:45 -0500 Subject: [PATCH 17/36] Update packages/clerk-js/src/ui/common/redirects.ts Co-authored-by: Jacek Radko --- packages/clerk-js/src/ui/common/redirects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index a708887d9aa..7f4058d84ed 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -10,7 +10,7 @@ export function buildEmailLinkRedirectUrl({ intent = 'sign-in', }: { ctx: SignInContextType | SignUpContextType | UserProfileContextType; - baseUrl: string | undefined; + baseUrl?: string; intent?: 'sign-in' | 'sign-up' | 'profile'; }): string { const { routing, authQueryString, path } = ctx; From 361cf900cf194b67ad45b84fa7c3a084db290d0c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 10:07:55 -0500 Subject: [PATCH 18/36] Update packages/clerk-js/src/ui/contexts/components/SignIn.ts Co-authored-by: Jacek Radko --- packages/clerk-js/src/ui/contexts/components/SignIn.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 0e06f1caa43..028a2ab17bc 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -44,8 +44,7 @@ export const useSignInContext = (): SignInContextType => { const isCombinedFlow = Boolean(!options.signUpUrl && options.signInUrl && !isAbsoluteUrl(options.signInUrl)) || - context.withSignUp || - false; + context.withSignUp const { componentName, mode, ...ctx } = context; const initialValuesFromQueryParams = useMemo( From c77b8e7b6e617dcce4d7f93eb6c37b80ac8a1e97 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 10:12:44 -0500 Subject: [PATCH 19/36] format --- packages/clerk-js/src/ui/contexts/components/SignIn.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 69bfd4210e2..8cde57cdc72 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -43,8 +43,7 @@ export const useSignInContext = (): SignInContextType => { } const isCombinedFlow = - Boolean(!options.signUpUrl && options.signInUrl && !isAbsoluteUrl(options.signInUrl)) || - context.withSignUp + Boolean(!options.signUpUrl && options.signInUrl && !isAbsoluteUrl(options.signInUrl)) || context.withSignUp; const { componentName, mode, ...ctx } = context; const initialValuesFromQueryParams = useMemo( From 68324d71e8eb2548c50ce1e1275e18845f7326f2 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 10:21:03 -0500 Subject: [PATCH 20/36] bring back fallback --- packages/clerk-js/src/ui/contexts/components/SignIn.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 8cde57cdc72..fed4ad6e959 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -43,7 +43,9 @@ export const useSignInContext = (): SignInContextType => { } const isCombinedFlow = - Boolean(!options.signUpUrl && options.signInUrl && !isAbsoluteUrl(options.signInUrl)) || context.withSignUp; + Boolean(!options.signUpUrl && options.signInUrl && !isAbsoluteUrl(options.signInUrl)) || + context.withSignUp || + false; const { componentName, mode, ...ctx } = context; const initialValuesFromQueryParams = useMemo( From 4545e7786a27faffe742dbdb01c61374bd9c8554 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 14 Jan 2025 18:37:43 +0200 Subject: [PATCH 21/36] fix(nextjs): Handle dynamicIO errors when request apis are accessed on prerender (#4836) --- .changeset/brown-kids-camp.md | 5 +++ .../src/app-router/server/ClerkProvider.tsx | 39 ++++++++++++------- .../nextjs/src/app-router/server/utils.ts | 4 +- 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 .changeset/brown-kids-camp.md diff --git a/.changeset/brown-kids-camp.md b/.changeset/brown-kids-camp.md new file mode 100644 index 00000000000..8b8248b3152 --- /dev/null +++ b/.changeset/brown-kids-camp.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Handle `dynamicIO` errors when request apis are accessed on prerender. This fixes issues with `ppr: true, dynamicIO: true` when using ``. diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 69f3693a568..badcbd19519 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -1,4 +1,3 @@ -import type { AuthObject } from '@clerk/backend'; import type { InitialState, Without } from '@clerk/types'; import { headers } from 'next/headers'; import React from 'react'; @@ -27,21 +26,33 @@ export async function ClerkProvider( props: Without, ) { const { children, dynamic, ...rest } = props; - let statePromise: Promise = Promise.resolve(null); - let nonce = Promise.resolve(''); - if (dynamic) { + async function generateStatePromise() { + if (!dynamic) { + return Promise.resolve(null); + } + if (isNext13) { + /** + * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. + * Without the await here, Next will throw a DynamicServerError during build. + */ + return Promise.resolve(await getDynamicClerkState()); + } + return getDynamicClerkState(); + } + + async function generateNonce() { + if (!dynamic) { + return Promise.resolve(''); + } if (isNext13) { /** * For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked. * Without the await here, Next will throw a DynamicServerError during build. */ - statePromise = Promise.resolve(await getDynamicClerkState()); - nonce = Promise.resolve(await getNonceFromCSPHeader()); - } else { - statePromise = getDynamicClerkState(); - nonce = getNonceFromCSPHeader(); + return Promise.resolve(await getNonceFromCSPHeader()); } + return getNonceFromCSPHeader(); } const propsWithEnvs = mergeNextClerkPropsWithEnv({ @@ -51,8 +62,8 @@ export async function ClerkProvider( let output = ( {children} @@ -75,8 +86,8 @@ export async function ClerkProvider( __internal_claimKeylessApplicationUrl: newOrReadKeys.claimUrl, __internal_copyInstanceKeysUrl: newOrReadKeys.apiKeysUrl, })} - nonce={await nonce} - initialState={await statePromise} + nonce={await generateNonce()} + initialState={await generateStatePromise()} > {children} @@ -88,7 +99,7 @@ export async function ClerkProvider( if (dynamic) { return ( // TODO: fix types so AuthObject is compatible with InitialState - }> + }> {output} ); diff --git a/packages/nextjs/src/app-router/server/utils.ts b/packages/nextjs/src/app-router/server/utils.ts index 7e8c220155f..490166d3027 100644 --- a/packages/nextjs/src/app-router/server/utils.ts +++ b/packages/nextjs/src/app-router/server/utils.ts @@ -19,7 +19,7 @@ export const isPrerenderingBailout = (e: unknown) => { return routeRegex.test(message) || dynamicServerUsage || bailOutPrerendering; }; -export async function buildRequestLike() { +export async function buildRequestLike(): Promise { try { // Dynamically import next/headers, otherwise Next12 apps will break // @ts-expect-error: Cannot find module 'next/headers' or its corresponding type declarations.ts(2307) @@ -67,7 +67,7 @@ export function getScriptNonceFromHeader(cspHeaderValue: string): string | undef // Grab the nonce by trimming the 'nonce-' prefix. ?.slice(7, -1); - // If we could't find the nonce, then we're done. + // If we couldn't find the nonce, then we're done. if (!nonce) { return; } From b38298d95fd785c50b4597e46567f526c3acd858 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Wed, 15 Jan 2025 15:00:46 +0200 Subject: [PATCH 22/36] fix(localizations): Force interpolation for `socialButtonsBlockButtonManyInView` (#4887) --- .changeset/lemon-ducks-hug.md | 6 ++++++ packages/localizations/src/be-BY.ts | 2 +- packages/localizations/src/cs-CZ.ts | 2 +- packages/localizations/src/da-DK.ts | 2 +- packages/localizations/src/de-DE.ts | 3 +-- packages/localizations/src/es-ES.ts | 2 +- packages/localizations/src/fr-FR.ts | 2 +- packages/localizations/src/it-IT.ts | 3 +-- packages/localizations/src/pt-PT.ts | 2 +- packages/types/src/localization.ts | 7 ++++++- 10 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 .changeset/lemon-ducks-hug.md diff --git a/.changeset/lemon-ducks-hug.md b/.changeset/lemon-ducks-hug.md new file mode 100644 index 00000000000..d5dace44565 --- /dev/null +++ b/.changeset/lemon-ducks-hug.md @@ -0,0 +1,6 @@ +--- +'@clerk/localizations': patch +'@clerk/types': patch +--- + +Update `socialButtonsBlockButtonManyInView` to only accept `'${string}{{provider|titleize}}${string}'` or `undefined`. diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index d6a60baf1bb..7cdd57ed5ef 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -547,7 +547,7 @@ export const beBY: LocalizationResource = { }, }, socialButtonsBlockButton: 'Працягнуць з дапамогай {{provider|titleize}}', - socialButtonsBlockButtonManyInView: 'Увядзіце дадатковыя спосабы ўваходу', + socialButtonsBlockButtonManyInView: undefined, unstable__errors: { already_a_member_in_organization: 'Вы ўжо з’яўляецеся членам гэтай арганізацыі.', diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index 7086e711955..d1e25970485 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -540,7 +540,7 @@ export const csCZ: LocalizationResource = { }, }, socialButtonsBlockButton: 'Pokračovat s {{provider|titleize}}', - socialButtonsBlockButtonManyInView: 'Máme více možností přihlášení.', + socialButtonsBlockButtonManyInView: undefined, unstable__errors: { already_a_member_in_organization: 'Již jste členem organizace.', captcha_invalid: diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index f39b1294041..0e27a70a07d 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -542,7 +542,7 @@ export const daDK: LocalizationResource = { }, }, socialButtonsBlockButton: 'Forsæt med {{provider|titleize}}', - socialButtonsBlockButtonManyInView: 'Vælg en metode til at fortsætte', + socialButtonsBlockButtonManyInView: undefined, unstable__errors: { already_a_member_in_organization: undefined, captcha_invalid: diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index 045f7f66794..36e7839a4f2 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -547,8 +547,7 @@ export const deDE: LocalizationResource = { }, }, socialButtonsBlockButton: 'Weiter mit {{provider|titleize}}', - socialButtonsBlockButtonManyInView: - 'Zu viele Buttons angezeigt. Reduzieren Sie die Anzahl der Buttons, um fortzufahren.', + socialButtonsBlockButtonManyInView: undefined, unstable__errors: { already_a_member_in_organization: 'Sie sind bereits Mitglied in dieser Organisation.', captcha_invalid: diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index 9d6d81f382e..4c525fd4a54 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -549,7 +549,7 @@ export const esES: LocalizationResource = { }, }, socialButtonsBlockButton: 'Continuar con {{provider|titleize}}', - socialButtonsBlockButtonManyInView: 'Demasiados botones sociales visibles. Desplázate para ver más.', + socialButtonsBlockButtonManyInView: undefined, unstable__errors: { already_a_member_in_organization: '{{email}} ya es miembro de la organización.', captcha_invalid: diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index 18142568d30..12993a30aaf 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -548,7 +548,7 @@ export const frFR: LocalizationResource = { }, }, socialButtonsBlockButton: 'Continuer avec {{provider|titleize}}', - socialButtonsBlockButtonManyInView: 'Afficher plus de boutons sociaux', + socialButtonsBlockButtonManyInView: undefined, unstable__errors: { already_a_member_in_organization: 'Vous êtes déjà membre de cette organisation.', captcha_invalid: diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index 4fc2a641433..568882d7bce 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -543,8 +543,7 @@ export const itIT: LocalizationResource = { }, }, socialButtonsBlockButton: 'Continua con {{provider|titleize}}', - socialButtonsBlockButtonManyInView: 'Continua con uno dei seguenti provider', - + socialButtonsBlockButtonManyInView: undefined, unstable__errors: { already_a_member_in_organization: 'Sei già un membro di questa organizzazione.', captcha_invalid: diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index 157be4453a9..93006ec09eb 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -543,7 +543,7 @@ export const ptPT: LocalizationResource = { }, }, socialButtonsBlockButton: 'Continuar com {{provider|titleize}}', - socialButtonsBlockButtonManyInView: 'Escolha uma das opções abaixo', + socialButtonsBlockButtonManyInView: undefined, unstable__errors: { already_a_member_in_organization: 'Já é membro nesta organização.', captcha_invalid: diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts index da660b2de33..f6679fd9aa7 100644 --- a/packages/types/src/localization.ts +++ b/packages/types/src/localization.ts @@ -25,7 +25,12 @@ type _LocalizationResource = { [r: string]: LocalizationValue; }; socialButtonsBlockButton: LocalizationValue; - socialButtonsBlockButtonManyInView: LocalizationValue; + /** + * It should be used to provide a shorter variation of `socialButtonsBlockButton`. + * It is explicitly typed, in order to avoid contributions that use LLM tools to generate + * translations that misinterpret the correct usage of this property. + */ + socialButtonsBlockButtonManyInView: `${string}{{provider|titleize}}${string}`; dividerText: LocalizationValue; formFieldLabel__emailAddress: LocalizationValue; formFieldLabel__emailAddresses: LocalizationValue; From 5c842f8d1d20eb9bb1e582e89176beb631b5e05f Mon Sep 17 00:00:00 2001 From: Lennart Date: Wed, 15 Jan 2025 14:25:15 +0100 Subject: [PATCH 23/36] fix(chrome-extension): Bundle @clerk/shared to properly tree-shake (#4899) --- .changeset/friendly-news-fix.md | 5 +++++ packages/chrome-extension/tsup.config.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/friendly-news-fix.md diff --git a/.changeset/friendly-news-fix.md b/.changeset/friendly-news-fix.md new file mode 100644 index 00000000000..c02f872b4c6 --- /dev/null +++ b/.changeset/friendly-news-fix.md @@ -0,0 +1,5 @@ +--- +'@clerk/chrome-extension': patch +--- + +Fix issue "Including remotely hosted code in a Manifest V3 item" that you might have seen during audit. The affected code is now bundled with the package and as such any offending code properly tree-shaken. diff --git a/packages/chrome-extension/tsup.config.ts b/packages/chrome-extension/tsup.config.ts index 317efbfa3c8..f05c0a91df2 100644 --- a/packages/chrome-extension/tsup.config.ts +++ b/packages/chrome-extension/tsup.config.ts @@ -16,7 +16,7 @@ export default defineConfig(overrideOptions => { sourcemap: true, legacyOutput: true, treeshake: true, - noExternal: ['@clerk/clerk-react'], + noExternal: ['@clerk/clerk-react', '@clerk/shared'], external: ['use-sync-external-store'], define: { PACKAGE_NAME: `"${name}"`, From e0f4a6ec8f3dd0d08a16af96c376dd90f0e08a11 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 10:28:31 -0500 Subject: [PATCH 24/36] Delete .changeset/cuddly-shrimps-fold.md --- .changeset/cuddly-shrimps-fold.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .changeset/cuddly-shrimps-fold.md diff --git a/.changeset/cuddly-shrimps-fold.md b/.changeset/cuddly-shrimps-fold.md deleted file mode 100644 index cc4c35984a8..00000000000 --- a/.changeset/cuddly-shrimps-fold.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@clerk/clerk-js': patch -'@clerk/clerk-react': patch -'@clerk/types': patch ---- - -Add option to allow a `` component instance to opt into the sign-in-or-up flow. From e1aef0232bd787ccf333934eb8057ae8b015c4d4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 10:29:52 -0500 Subject: [PATCH 25/36] add changeset --- .changeset/tough-bugs-vanish.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/tough-bugs-vanish.md diff --git a/.changeset/tough-bugs-vanish.md b/.changeset/tough-bugs-vanish.md new file mode 100644 index 00000000000..953977ed2ec --- /dev/null +++ b/.changeset/tough-bugs-vanish.md @@ -0,0 +1,10 @@ +--- +'@clerk/chrome-extension': minor +'@clerk/localizations': minor +'@clerk/clerk-js': minor +'@clerk/nextjs': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +Introduce sign-in-or-up flow. From cd9a4a41a6f8b319128bd68ab5aca5680b4bbceb Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 10:30:21 -0500 Subject: [PATCH 26/36] Delete .changeset/two-doors-visit.md --- .changeset/two-doors-visit.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .changeset/two-doors-visit.md diff --git a/.changeset/two-doors-visit.md b/.changeset/two-doors-visit.md deleted file mode 100644 index 2f8f285cae4..00000000000 --- a/.changeset/two-doors-visit.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@clerk/clerk-js': minor -'@clerk/types': minor ---- - -Introduce sign-in-or-up flow. From 6e8a4ab39a073eae02761f35cc2b5d3bdaee4ee4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 10:35:17 -0500 Subject: [PATCH 27/36] Update .changeset/tough-bugs-vanish.md Co-authored-by: Jacek Radko --- .changeset/tough-bugs-vanish.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.changeset/tough-bugs-vanish.md b/.changeset/tough-bugs-vanish.md index 953977ed2ec..abc08c6f02e 100644 --- a/.changeset/tough-bugs-vanish.md +++ b/.changeset/tough-bugs-vanish.md @@ -1,8 +1,5 @@ --- -'@clerk/chrome-extension': minor -'@clerk/localizations': minor '@clerk/clerk-js': minor -'@clerk/nextjs': minor '@clerk/clerk-react': minor '@clerk/types': minor --- From bee4012e9aa4d2d9a0e31997289f1a3305c707a6 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 11:39:33 -0500 Subject: [PATCH 28/36] add withSignUp prop to vue SignInButton --- packages/vue/src/components/SignInButton.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/vue/src/components/SignInButton.ts b/packages/vue/src/components/SignInButton.ts index c6f375dd039..cbed4cf7149 100644 --- a/packages/vue/src/components/SignInButton.ts +++ b/packages/vue/src/components/SignInButton.ts @@ -6,7 +6,12 @@ import { assertSingleChild, normalizeWithDefaultValue } from '../utils'; type SignInButtonProps = Pick< SignInProps, - 'fallbackRedirectUrl' | 'forceRedirectUrl' | 'signUpForceRedirectUrl' | 'signUpFallbackRedirectUrl' | 'initialValues' + | 'fallbackRedirectUrl' + | 'forceRedirectUrl' + | 'signUpForceRedirectUrl' + | 'signUpFallbackRedirectUrl' + | 'initialValues' + | 'withSignUp' >; export const SignInButton = defineComponent( @@ -49,6 +54,7 @@ export const SignInButton = defineComponent( 'forceRedirectUrl', 'mode', 'initialValues', + 'withSignUp', ], }, ); From f559bd1f1e886bf6781d5656d0bc851fa8a343a3 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 11:43:13 -0500 Subject: [PATCH 29/36] Update .changeset/tough-bugs-vanish.md --- .changeset/tough-bugs-vanish.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/tough-bugs-vanish.md b/.changeset/tough-bugs-vanish.md index abc08c6f02e..92bcc78c96d 100644 --- a/.changeset/tough-bugs-vanish.md +++ b/.changeset/tough-bugs-vanish.md @@ -2,6 +2,7 @@ '@clerk/clerk-js': minor '@clerk/clerk-react': minor '@clerk/types': minor +'@clerk/vue': minor --- Introduce sign-in-or-up flow. From ae62b50e69cc38735779aa031aef200c1f27afd1 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 12:56:45 -0500 Subject: [PATCH 30/36] remove combinedFlow prop from provider --- integration/templates/next-app-router/src/app/layout.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx index 0c43679815c..b8b377146ce 100644 --- a/integration/templates/next-app-router/src/app/layout.tsx +++ b/integration/templates/next-app-router/src/app/layout.tsx @@ -13,9 +13,6 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( Date: Wed, 15 Jan 2025 15:23:27 -0600 Subject: [PATCH 31/36] fix(clerk-js): Preserve __clerk_ticket for internal sign-in navigation (#4903) --- .../clerk-js/src/ui/components/SignIn/SignInStart.tsx | 10 +++++----- .../ui/components/SignIn/handleCombinedFlowTransfer.ts | 5 +++-- packages/clerk-js/src/ui/router/Route.tsx | 9 ++++++--- packages/clerk-js/src/ui/router/RouteContext.tsx | 2 +- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index 7a6e5352688..d24ae2addd7 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -173,9 +173,13 @@ export function _SignInStart(): JSX.Element { } if (clerkStatus === 'sign_up') { + const paramsToForward = new URLSearchParams(); + if (organizationTicket) { + paramsToForward.set('__clerk_ticket', organizationTicket); + } // We explicitly navigate to 'create' in the combined flow to trigger a client-side navigation. Navigating to // signUpUrl triggers a full page reload when used with the hash router. - navigate(isCombinedFlow ? 'create' : signUpUrl); + void navigate(isCombinedFlow ? `create` : signUpUrl, { searchParams: paramsToForward }); return; } @@ -374,10 +378,6 @@ export function _SignInStart(): JSX.Element { } 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 || '/'; diff --git a/packages/clerk-js/src/ui/components/SignIn/handleCombinedFlowTransfer.ts b/packages/clerk-js/src/ui/components/SignIn/handleCombinedFlowTransfer.ts index 60ff47cb996..1337057372b 100644 --- a/packages/clerk-js/src/ui/components/SignIn/handleCombinedFlowTransfer.ts +++ b/packages/clerk-js/src/ui/components/SignIn/handleCombinedFlowTransfer.ts @@ -1,13 +1,14 @@ import type { LoadedClerk, SignUpModes, SignUpResource } from '@clerk/types'; import { SIGN_UP_MODES } from '../../../core/constants'; +import type { RouteContextValue } from '../../router/RouteContext'; import { completeSignUpFlow } from '../SignUp/util'; type HandleCombinedFlowTransferProps = { identifierAttribute: 'emailAddress' | 'phoneNumber' | 'username'; identifierValue: string; signUpMode: SignUpModes; - navigate: (to: string) => Promise; + navigate: RouteContextValue['navigate']; organizationTicket?: string; afterSignUpUrl: string; clerk: LoadedClerk; @@ -78,7 +79,7 @@ export function handleCombinedFlowTransfer({ .catch(err => handleError(err)); } - return navigate(`create?${paramsToForward.toString()}`); + return navigate(`create`, { searchParams: paramsToForward }); } function hasOptionalFields(signUp: SignUpResource) { diff --git a/packages/clerk-js/src/ui/router/Route.tsx b/packages/clerk-js/src/ui/router/Route.tsx index c68ab28c4e4..1e0e046f75c 100644 --- a/packages/clerk-js/src/ui/router/Route.tsx +++ b/packages/clerk-js/src/ui/router/Route.tsx @@ -58,8 +58,11 @@ export function Route(props: RouteProps): JSX.Element | null { const [indexPath, fullPath] = newPaths(router.indexPath, router.fullPath, props.path, props.index); - const resolve = (to: string) => { + const resolve = (to: string, { searchParams }: { searchParams?: URLSearchParams } = {}) => { const url = new URL(to, window.location.origin + fullPath + '/'); + if (searchParams) { + url.search = searchParams.toString(); + } url.pathname = trimTrailingSlash(url.pathname); return url; }; @@ -109,8 +112,8 @@ export function Route(props: RouteProps): JSX.Element | null { return newGetMatchData(path, index) ? true : false; }, resolve: resolve, - navigate: (to: string) => { - const toURL = resolve(to); + navigate: (to: string, { searchParams } = {}) => { + const toURL = resolve(to, { searchParams }); return router.baseNavigate(toURL); }, refresh: router.refresh, diff --git a/packages/clerk-js/src/ui/router/RouteContext.tsx b/packages/clerk-js/src/ui/router/RouteContext.tsx index 8496c211856..08da052e9c0 100644 --- a/packages/clerk-js/src/ui/router/RouteContext.tsx +++ b/packages/clerk-js/src/ui/router/RouteContext.tsx @@ -9,7 +9,7 @@ export interface RouteContextValue { currentPath: string; matches: (path?: string, index?: boolean) => boolean; baseNavigate: (toURL: URL) => Promise; - navigate: (to: string) => Promise; + navigate: (to: string, options?: { searchParams?: URLSearchParams }) => Promise; resolve: (to: string) => URL; refresh: () => void; params: { [key: string]: string }; From c1d59b52488552719647da5e13f087378d0162b5 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 15 Jan 2025 19:13:16 -0500 Subject: [PATCH 32/36] fix test --- integration/tests/sign-in-or-up-flow.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/tests/sign-in-or-up-flow.test.ts b/integration/tests/sign-in-or-up-flow.test.ts index 2fdcbb55723..952c240a362 100644 --- a/integration/tests/sign-in-or-up-flow.test.ts +++ b/integration/tests/sign-in-or-up-flow.test.ts @@ -279,8 +279,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('sign- await u.po.signIn.goTo({ searchParams: new URLSearchParams({ __clerk_ticket: '123', __clerk_status: 'sign_up' }), }); - await u.page.waitForAppUrl('/sign-in/create'); - await expect(u.page.getByText(`Create your account`)).toBeVisible(); + await u.page.waitForAppUrl('/sign-in/create?__clerk_ticket=123'); + await expect(u.page.getByText(/Create your account/i)).toBeVisible(); }); }); }); From 64587078ecfa4204bd3867c7c78ac48b8a584b68 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 16 Jan 2025 09:05:03 -0500 Subject: [PATCH 33/36] remove email link test changes --- integration/tests/email-link.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/integration/tests/email-link.test.ts b/integration/tests/email-link.test.ts index d330d07102a..67d029aca07 100644 --- a/integration/tests/email-link.test.ts +++ b/integration/tests/email-link.test.ts @@ -5,8 +5,8 @@ import type { Application } from '../models/application'; import { appConfigs } from '../presets'; import { createTestUtils } from '../testUtils'; -test.describe('sign up and sign in using email link @generic', () => { - const configs = [appConfigs.next.appRouter, appConfigs.react.vite]; +test.describe('sign up and sign in using email link', () => { + const configs = []; configs.forEach(config => { test.describe(`${config.name}`, () => { @@ -81,7 +81,7 @@ const performSignUpVerificationLinkSameDevice = async ( searchParams?: URLSearchParams, ) => { const u = createTestUtils({ app, page, context }); - const fakeUser = u.services.users.createFakeUser({ fictionalEmail: false, withPassword: true }); + const fakeUser = u.services.users.createFakeUser(); await u.po.signUp.goTo({ searchParams }); await u.po.signUp.signUpWithEmailAndPassword({ email: fakeUser.email, password: fakeUser.password }); await u.po.signUp.waitForEmailVerificationScreen(); @@ -103,7 +103,7 @@ const performSignUpVerificationLinkDifferentDevice = async ( searchParams?: URLSearchParams, ) => { const u = createTestUtils({ app, page, context, browser }); - const fakeUser = u.services.users.createFakeUser({ fictionalEmail: false, withPassword: true }); + const fakeUser = u.services.users.createFakeUser(); await u.po.signUp.goTo({ searchParams }); await u.po.signUp.signUpWithEmailAndPassword({ email: fakeUser.email, password: fakeUser.password }); await u.po.signUp.waitForEmailVerificationScreen(); From 658a7bf5a8091e77f5297fcf56560b3753175aa1 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 16 Jan 2025 10:03:53 -0500 Subject: [PATCH 34/36] temp disable email link tests --- .../sign-in-or-up-email-links-flow.test.ts | 99 +++++++++---------- 1 file changed, 46 insertions(+), 53 deletions(-) diff --git a/integration/tests/sign-in-or-up-email-links-flow.test.ts b/integration/tests/sign-in-or-up-email-links-flow.test.ts index a49a867e1f7..575b0e5da94 100644 --- a/integration/tests/sign-in-or-up-email-links-flow.test.ts +++ b/integration/tests/sign-in-or-up-email-links-flow.test.ts @@ -1,69 +1,62 @@ import { expect, test } from '@playwright/test'; -import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpEmailLinksFlow] })( - 'sign-in-or-up email links flow @nextjs', - ({ app }) => { - test.describe.configure({ mode: 'serial' }); +testAgainstRunningApps({ withEnv: [] })('sign-in-or-up email links flow', ({ app }) => { + test.describe.configure({ mode: 'serial' }); - let fakeUser: FakeUser; + let fakeUser: FakeUser; - test.beforeAll(() => { - const u = createTestUtils({ app }); - fakeUser = u.services.users.createFakeUser({ - fictionalEmail: false, - withPassword: true, - }); - }); + test.beforeAll(() => { + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser(); + }); - test.afterAll(async () => { - await app.teardown(); - }); + test.afterAll(async () => { + await app.teardown(); + }); - test('sign up with email link', 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.page.waitForAppUrl('/sign-in/create'); + test('sign up with email link', 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.page.waitForAppUrl('/sign-in/create'); - const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); - expect(prefilledEmail).toBe(fakeUser.email); + const prefilledEmail = await u.po.signUp.getEmailAddressInput().inputValue(); + expect(prefilledEmail).toBe(fakeUser.email); - await u.po.signUp.setPassword(fakeUser.password); - await u.po.signUp.continue(); + await u.po.signUp.setPassword(fakeUser.password); + await u.po.signUp.continue(); - await u.po.signUp.waitForEmailVerificationScreen(); - await u.tabs.runInNewTab(async u => { - const verificationLink = await u.services.email.getVerificationLinkForEmailAddress(fakeUser.email); - await u.page.goto(verificationLink); - await u.po.expect.toBeSignedIn(); - await u.page.close(); - }); + await u.po.signUp.waitForEmailVerificationScreen(); + await u.tabs.runInNewTab(async u => { + const verificationLink = await u.services.email.getVerificationLinkForEmailAddress(fakeUser.email); + await u.page.goto(verificationLink); await u.po.expect.toBeSignedIn(); + await u.page.close(); }); - - test('sign in with email link', 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.page.waitForAppUrl('/sign-in/factor-one'); - // Defaults to password, so we need to switch to email link - await u.page.getByRole('link', { name: /Use another method/i }).click(); - await u.page.getByRole('button', { name: /Email link to/i }).click(); - await page.getByRole('heading', { name: /Check your email/i }).waitFor(); - await u.tabs.runInNewTab(async u => { - const verificationLink = await u.services.email.getVerificationLinkForEmailAddress(fakeUser.email); - await u.page.goto(verificationLink); - await u.po.expect.toBeSignedIn(); - await u.page.close(); - }); + await u.po.expect.toBeSignedIn(); + }); + + test('sign in with email link', 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.page.waitForAppUrl('/sign-in/factor-one'); + // Defaults to password, so we need to switch to email link + await u.page.getByRole('link', { name: /Use another method/i }).click(); + await u.page.getByRole('button', { name: /Email link to/i }).click(); + await page.getByRole('heading', { name: /Check your email/i }).waitFor(); + await u.tabs.runInNewTab(async u => { + const verificationLink = await u.services.email.getVerificationLinkForEmailAddress(fakeUser.email); + await u.page.goto(verificationLink); await u.po.expect.toBeSignedIn(); - await fakeUser.deleteIfExists(); + await u.page.close(); }); - }, -); + await u.po.expect.toBeSignedIn(); + await fakeUser.deleteIfExists(); + }); +}); From 63162f69e2e10bb01caa7a19d312dd7200ab58de Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 16 Jan 2025 12:03:12 -0500 Subject: [PATCH 35/36] Update SignInButton.vue --- packages/vue/src/components/SignInButton.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/vue/src/components/SignInButton.vue b/packages/vue/src/components/SignInButton.vue index fbcae81a3fc..b1a20f1a668 100644 --- a/packages/vue/src/components/SignInButton.vue +++ b/packages/vue/src/components/SignInButton.vue @@ -6,7 +6,12 @@ import { assertSingleChild, normalizeWithDefaultValue } from '../utils'; type SignInButtonProps = Pick< SignInProps, - 'fallbackRedirectUrl' | 'forceRedirectUrl' | 'signUpForceRedirectUrl' | 'signUpFallbackRedirectUrl' | 'initialValues' + | 'fallbackRedirectUrl' + | 'forceRedirectUrl' + | 'signUpForceRedirectUrl' + | 'signUpFallbackRedirectUrl' + | 'initialValues' + | 'withSignUp' > & { mode?: 'modal' | 'redirect'; }; From e1772b3aa4f96ba528a3e33422eb9e2672090f8a Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 16 Jan 2025 11:55:46 -0600 Subject: [PATCH 36/36] Fix vue options passing. --- packages/vue/src/components/SignInButton.ts | 4 +++- packages/vue/src/components/SignInButton.vue | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/vue/src/components/SignInButton.ts b/packages/vue/src/components/SignInButton.ts index cbed4cf7149..d90bfc40f20 100644 --- a/packages/vue/src/components/SignInButton.ts +++ b/packages/vue/src/components/SignInButton.ts @@ -30,8 +30,10 @@ export const SignInButton = defineComponent( return clerk.value?.openSignIn(opts); } + const { withSignUp, ...redirectOpts } = opts; + void clerk.value?.redirectToSignIn({ - ...opts, + ...redirectOpts, signInFallbackRedirectUrl: props.fallbackRedirectUrl, signInForceRedirectUrl: props.forceRedirectUrl, }); diff --git a/packages/vue/src/components/SignInButton.vue b/packages/vue/src/components/SignInButton.vue index b1a20f1a668..42aed6a6e4e 100644 --- a/packages/vue/src/components/SignInButton.vue +++ b/packages/vue/src/components/SignInButton.vue @@ -34,8 +34,10 @@ function clickHandler() { return clerk.value?.openSignIn(opts); } + const { withSignUp, ...redirectOpts } = opts; + void clerk.value?.redirectToSignIn({ - ...opts, + ...redirectOpts, signInFallbackRedirectUrl: props.fallbackRedirectUrl, signInForceRedirectUrl: props.forceRedirectUrl, });