diff --git a/.changeset/metal-zebras-jog.md b/.changeset/metal-zebras-jog.md new file mode 100644 index 00000000000..661861b2fbc --- /dev/null +++ b/.changeset/metal-zebras-jog.md @@ -0,0 +1,11 @@ +--- +'@clerk/tanstack-react-start': patch +'@clerk/react-router': patch +'@clerk/clerk-js': patch +'@clerk/nextjs': patch +'@clerk/clerk-react': patch +'@clerk/remix': patch +'@clerk/types': patch +--- + +Introduce `checkoutContinueUrl` option. diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index d24606d3c2d..baab3fe9030 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -4,7 +4,7 @@ { "path": "./dist/clerk.browser.js", "maxSize": "68KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "110KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "52KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "102KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "102.5KB" }, { "path": "./dist/vendors*.js", "maxSize": "39KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 2e693490fd0..25b5ad13860 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -175,6 +175,7 @@ const defaultOptions: ClerkOptions = { signInForceRedirectUrl: undefined, signUpForceRedirectUrl: undefined, treatPendingAsSignedOut: true, + __experimental_checkoutContinueUrl: undefined, }; export class Clerk implements ClerkInterface { @@ -1377,6 +1378,14 @@ export class Clerk implements ClerkInterface { return this.buildUrlWithAuth(this.#options.afterSignOutUrl); } + public __experimental_buildCheckoutContinueUrl(): string { + if (!this.#options.__experimental_checkoutContinueUrl) { + return this.buildAfterSignInUrl(); + } + + return this.#options.__experimental_checkoutContinueUrl; + } + public buildWaitlistUrl(options?: { initialValues?: Record }): string { if (!this.environment || !this.environment.displayConfig) { return ''; diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx index d8ce3e714f8..39465d51b01 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx @@ -1,10 +1,13 @@ import type { __experimental_CommerceCheckoutResource } from '@clerk/types'; +import { useCheckoutContext } from '../../contexts'; import { Box, Button, descriptors, Heading, localizationKeys, Span, Text } from '../../customizables'; import { Drawer, LineItems, useDrawerContext } from '../../elements'; import { transitionDurationValues, transitionTiming } from '../../foundations/transitions'; +import { useRouter } from '../../router'; import { animations } from '../../styledSystem'; import { formatDate } from '../../utils'; + const capitalize = (name: string) => name[0].toUpperCase() + name.slice(1); export const CheckoutComplete = ({ @@ -14,9 +17,14 @@ export const CheckoutComplete = ({ checkout: __experimental_CommerceCheckoutResource; isMotionSafe: boolean; }) => { + const router = useRouter(); const { setIsOpen } = useDrawerContext(); + const { __experimental_checkoutContinueUrl } = useCheckoutContext(); const handleClose = () => { + if (__experimental_checkoutContinueUrl) { + void router.navigate(__experimental_checkoutContinueUrl); + } if (setIsOpen) { setIsOpen(false); } diff --git a/packages/clerk-js/src/ui/contexts/components/Checkout.ts b/packages/clerk-js/src/ui/contexts/components/Checkout.ts index 58809fa6688..ea85472fe80 100644 --- a/packages/clerk-js/src/ui/contexts/components/Checkout.ts +++ b/packages/clerk-js/src/ui/contexts/components/Checkout.ts @@ -1,20 +1,36 @@ -import { createContext, useContext } from 'react'; +import { useClerk } from '@clerk/shared/react'; +import { createContext, useContext, useMemo } from 'react'; import type { __experimental_CheckoutCtx } from '../../types'; - export const __experimental_CheckoutContext = createContext<__experimental_CheckoutCtx | null>(null); export const useCheckoutContext = () => { const context = useContext(__experimental_CheckoutContext); + const clerk = useClerk(); if (!context || context.componentName !== 'Checkout') { throw new Error('Clerk: useCheckoutContext called outside Checkout.'); } + const checkoutContinueUrl = useMemo(() => { + // When we're rendered via the PricingTable with mode = 'modal' we provide a `portalRoot` value + // we want to keep users within the context of the modal, so we do this to prevent navigating away + if (context.portalRoot) { + return undefined; + } + + if (context.__experimental_checkoutContinueUrl) { + return context.__experimental_checkoutContinueUrl; + } + + return clerk.__experimental_buildCheckoutContinueUrl?.(); + }, [context.portalRoot, context.__experimental_checkoutContinueUrl, clerk]); + const { componentName, ...ctx } = context; return { ...ctx, componentName, + __experimental_checkoutContinueUrl: checkoutContinueUrl, }; }; diff --git a/packages/clerk-js/src/ui/hooks/useCheckout.ts b/packages/clerk-js/src/ui/hooks/useCheckout.ts index 5dac8c76cb9..1098186a229 100644 --- a/packages/clerk-js/src/ui/hooks/useCheckout.ts +++ b/packages/clerk-js/src/ui/hooks/useCheckout.ts @@ -7,7 +7,7 @@ import { useFetch } from './useFetch'; export const useCheckout = (props: __experimental_CheckoutProps) => { const { planId, planPeriod, subscriberType = 'user' } = props; - const { __experimental_commerce } = useClerk(); + const clerk = useClerk(); const { organization } = useOrganization(); const [currentCheckout, setCurrentCheckout] = useState<__experimental_CommerceCheckoutResource | null>(null); @@ -19,7 +19,7 @@ export const useCheckout = (props: __experimental_CheckoutProps) => { revalidate, error: _error, } = useFetch( - __experimental_commerce?.__experimental_billing.startCheckout, + clerk.__experimental_commerce?.__experimental_billing.startCheckout, { planId, planPeriod, diff --git a/packages/clerk-js/src/ui/lazyModules/MountedCheckoutDrawer.tsx b/packages/clerk-js/src/ui/lazyModules/MountedCheckoutDrawer.tsx index b92f8eecaf1..b232a8d39b5 100644 --- a/packages/clerk-js/src/ui/lazyModules/MountedCheckoutDrawer.tsx +++ b/packages/clerk-js/src/ui/lazyModules/MountedCheckoutDrawer.tsx @@ -42,6 +42,7 @@ export function MountedCheckoutDrawer({ planPeriod={checkoutDrawer.props.planPeriod} subscriberType={checkoutDrawer.props.subscriberType} onSubscriptionComplete={checkoutDrawer.props.onSubscriptionComplete} + portalRoot={checkoutDrawer.props.portalRoot} /> )} diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 66e6061196d..af7aef1ef07 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -1,4 +1,5 @@ import type { + __experimental_CheckoutContinueUrl, __experimental_CheckoutProps, __experimental_CommerceInvoiceResource, __experimental_CommercePlanResource, @@ -118,7 +119,7 @@ export type __experimental_PricingTableCtx = __experimental_PricingTableProps & export type __experimental_CheckoutCtx = __experimental_CheckoutProps & { componentName: 'Checkout'; -}; +} & __experimental_CheckoutContinueUrl; export type __experimental_PaymentSourcesCtx = { componentName: 'PaymentSources'; diff --git a/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts index 08d121031e7..80ea636ca2d 100644 --- a/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts +++ b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts @@ -25,6 +25,8 @@ export const mergeNextClerkPropsWithEnv = (props: Omit { signUpFallbackRedirectUrl: getValue('CLERK_SIGN_UP_FALLBACK_REDIRECT_URL'), afterSignInUrl: getValue('CLERK_AFTER_SIGN_IN_URL'), afterSignUpUrl: getValue('CLERK_AFTER_SIGN_UP_URL'), + __experimental_checkoutContinueUrl: getValue('CLERK_CHECKOUT_CONTINUE_URL'), }; }; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 86907ce5c04..73239079b99 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -323,6 +323,15 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; + __experimental_buildCheckoutContinueUrl = (): string | void => { + const callback = () => this.clerkjs?.__experimental_buildCheckoutContinueUrl() || ''; + if (this.clerkjs && this.loaded) { + return callback(); + } else { + this.premountMethodCalls.set('__experimental_buildCheckoutContinueUrl', callback); + } + }; + buildAfterMultiSessionSingleSignOutUrl = (): string | void => { const callback = () => this.clerkjs?.buildAfterMultiSessionSingleSignOutUrl() || ''; if (this.clerkjs && this.loaded) { diff --git a/packages/remix/src/ssr/loadOptions.ts b/packages/remix/src/ssr/loadOptions.ts index d65fbea3562..cce8f2c6153 100644 --- a/packages/remix/src/ssr/loadOptions.ts +++ b/packages/remix/src/ssr/loadOptions.ts @@ -45,6 +45,8 @@ export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderO overrides.signUpFallbackRedirectUrl || getEnvVariable('CLERK_SIGN_UP_FALLBACK_REDIRECT_URL', context) || ''; const afterSignInUrl = overrides.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || ''; const afterSignUpUrl = overrides.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || ''; + const __experimental_checkoutContinueUrl = + overrides.__experimental_checkoutContinueUrl || getEnvVariable('CLERK_CHECKOUT_CONTINUE_URL', context) || ''; let proxyUrl; if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) { @@ -81,5 +83,6 @@ export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderO signUpForceRedirectUrl, signInFallbackRedirectUrl, signUpFallbackRedirectUrl, + __experimental_checkoutContinueUrl, }; }; diff --git a/packages/remix/src/ssr/types.ts b/packages/remix/src/ssr/types.ts index fda77ed7f76..0f5e6bc0615 100644 --- a/packages/remix/src/ssr/types.ts +++ b/packages/remix/src/ssr/types.ts @@ -1,6 +1,7 @@ import type { AuthObject, Organization, Session, User, VerifyTokenOptions } from '@clerk/backend'; import type { RequestState } from '@clerk/backend/internal'; import type { + __experimental_CheckoutContinueUrl, LegacyRedirectProps, MultiDomainAndOrProxy, SignInFallbackRedirectUrl, @@ -36,6 +37,7 @@ export type RootAuthLoaderOptions = { SignInFallbackRedirectUrl & SignUpForceRedirectUrl & SignUpFallbackRedirectUrl & + __experimental_CheckoutContinueUrl & LegacyRedirectProps; export type RequestStateWithRedirectUrls = RequestState & @@ -43,6 +45,7 @@ export type RequestStateWithRedirectUrls = RequestState & SignInFallbackRedirectUrl & SignUpForceRedirectUrl & SignUpFallbackRedirectUrl & + __experimental_CheckoutContinueUrl & LegacyRedirectProps; export type RootAuthLoaderCallback = ( diff --git a/packages/remix/src/ssr/utils.ts b/packages/remix/src/ssr/utils.ts index e1e1f129296..56d43875342 100644 --- a/packages/remix/src/ssr/utils.ts +++ b/packages/remix/src/ssr/utils.ts @@ -94,6 +94,7 @@ export function getResponseClerkState(requestState: RequestStateWithRedirectUrls __signUpForceRedirectUrl: requestState.signUpForceRedirectUrl, __signInFallbackRedirectUrl: requestState.signInFallbackRedirectUrl, __signUpFallbackRedirectUrl: requestState.signUpFallbackRedirectUrl, + __experimental_checkoutContinueUrl: requestState.__experimental_checkoutContinueUrl, __clerk_debug: debugRequestState(requestState), __clerkJSUrl: getEnvVariable('CLERK_JS', context), __clerkJSVersion: getEnvVariable('CLERK_JS_VERSION', context), diff --git a/packages/tanstack-react-start/src/utils/env.ts b/packages/tanstack-react-start/src/utils/env.ts index 8c4c6d42dea..fa1bc745207 100644 --- a/packages/tanstack-react-start/src/utils/env.ts +++ b/packages/tanstack-react-start/src/utils/env.ts @@ -21,5 +21,6 @@ export const getPublicEnvVariables = (context?: H3EventContext) => { telemetryDebug: isTruthy(getValue('CLERK_TELEMETRY_DEBUG')), afterSignInUrl: getValue('CLERK_AFTER_SIGN_IN_URL'), afterSignUpUrl: getValue('CLERK_AFTER_SIGN_UP_URL'), + __experimental_checkoutContinueUrl: getValue('CLERK_CHECKOUT_CONTINUE_URL'), } as const; }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 2a25b82e7dd..862aa28e4f8 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -30,6 +30,7 @@ import type { OAuthProvider, OAuthScope } from './oauth'; import type { OrganizationResource } from './organization'; import type { OrganizationCustomRoleKey } from './organizationMembership'; import type { + __experimental_CheckoutContinueUrl, AfterMultiSessionSingleSignOutUrl, AfterSignOutUrl, LegacyRedirectProps, @@ -565,6 +566,11 @@ export interface Clerk { */ buildAfterSignOutUrl(): string; + /** + * Returns the configured checkoutContinueUrl of the instance. + */ + __experimental_buildCheckoutContinueUrl(): string; + /** * Returns the configured afterMultiSessionSingleSignOutUrl of the instance. */ @@ -814,6 +820,7 @@ export type ClerkOptions = PendingSessionOptions & SignInFallbackRedirectUrl & SignUpForceRedirectUrl & SignUpFallbackRedirectUrl & + __experimental_CheckoutContinueUrl & LegacyRedirectProps & AfterSignOutUrl & AfterMultiSessionSingleSignOutUrl & { @@ -1563,6 +1570,7 @@ export type WaitlistModalProps = WaitlistProps; type __experimental_PricingTableDefaultProps = { ctaPosition?: 'top' | 'bottom'; collapseFeatures?: boolean; + __experimental_checkoutContinueUrl?: string; }; type __experimental_PricingTableBaseProps = { @@ -1584,6 +1592,11 @@ export type __experimental_CheckoutProps = { onSubscriptionComplete?: () => void; portalId?: string; portalRoot?: PortalRoot; + /** + * Full URL or path to navigate to after checkout is complete and the user clicks the "Continue" button. + * @default undefined + */ + __experimental_checkoutContinueUrl?: string; }; export type __experimental_PlanDetailsProps = { diff --git a/packages/types/src/redirects.ts b/packages/types/src/redirects.ts index 59f593d827c..f5b6867bd1a 100644 --- a/packages/types/src/redirects.ts +++ b/packages/types/src/redirects.ts @@ -121,3 +121,10 @@ export type SignInForceRedirectUrl = { */ signInForceRedirectUrl?: string | null; }; + +export type __experimental_CheckoutContinueUrl = { + /** + * The URL to navigate to after the user completes the checkout and clicks the "Continue" button. + */ + __experimental_checkoutContinueUrl?: string | null; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e27425af1e8..47ebc2e2094 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3855,6 +3855,7 @@ packages: '@oxc-parser/wasm@0.60.0': resolution: {integrity: sha512-Dkf9/D87WGBCW3L0+1DtpAfL4SrNsgeRvxwjpKCtbH7Kf6K+pxrT0IridaJfmWKu1Ml+fDvj+7HEyBcfUC/TXQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@oxc-project/types@0.56.5': resolution: {integrity: sha512-skY3kOJwp22W4RkaadH1hZ3hqFHjkRrIIE0uQ4VUg+/Chvbl+2pF+B55IrIk2dgsKXS57YEUsJuN6I6s4rgFjA==}