diff --git a/.changeset/large-feet-hammer.md b/.changeset/large-feet-hammer.md new file mode 100644 index 00000000000..388b3ddbed2 --- /dev/null +++ b/.changeset/large-feet-hammer.md @@ -0,0 +1,8 @@ +--- +'@clerk/localizations': patch +'@clerk/clerk-js': patch +'@clerk/clerk-react': patch +'@clerk/types': patch +--- + +Introduce experimental billing APIs and components diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 657dc3f9b11..d9133f5333a 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,10 +1,10 @@ { "files": [ - { "path": "./dist/clerk.js", "maxSize": "560kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "75kB" }, - { "path": "./dist/clerk.headless.js", "maxSize": "48.3KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "89KB" }, - { "path": "./dist/vendors*.js", "maxSize": "25.1KB" }, + { "path": "./dist/clerk.js", "maxSize": "570kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "76kB" }, + { "path": "./dist/clerk.headless.js", "maxSize": "50KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "92KB" }, + { "path": "./dist/vendors*.js", "maxSize": "26.5KB" }, { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, { "path": "./dist/impersonationfab*.js", "maxSize": "5KB" }, @@ -18,6 +18,8 @@ { "path": "./dist/userverification*.js", "maxSize": "5KB" }, { "path": "./dist/onetap*.js", "maxSize": "1KB" }, { "path": "./dist/waitlist*.js", "maxSize": "1.3KB" }, - { "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" } + { "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }, + { "path": "./dist/pricingTable*.js", "maxSize": "3.5KB" }, + { "path": "./dist/checkout*.js", "maxSize": "8.8KB" } ] } diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index 1bd43fe38ce..3f1649111db 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -63,6 +63,8 @@ "@floating-ui/react": "0.25.4", "@floating-ui/react-dom": "^2.0.2", "@formkit/auto-animate": "^0.8.1", + "@stripe/react-stripe-js": "3.1.1", + "@stripe/stripe-js": "5.6.0", "@swc/helpers": "^0.5.13", "@zxcvbn-ts/core": "3.0.4", "@zxcvbn-ts/language-common": "3.0.4", diff --git a/packages/clerk-js/rspack.config.js b/packages/clerk-js/rspack.config.js index 463496049db..ba490c9eadc 100644 --- a/packages/clerk-js/rspack.config.js +++ b/packages/clerk-js/rspack.config.js @@ -93,6 +93,15 @@ const common = ({ mode, disableRHC = false }) => { name: 'signup', test: module => module.resource && module.resource.includes('/ui/components/SignUp'), }, + checkout: { + minChunks: 1, + name: 'checkout', + test: module => + module.resource && + (module.resource.includes('/ui/components/Checkout') || + // Include `@stripe/react-stripe-js` and `@stripe/stripe-js` in the checkout chunk + module.resource.includes('/node_modules/@stripe')), + }, common: { minChunks: 1, name: 'ui-common', diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index 780e1817edd..f6aabd78f40 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -16,6 +16,7 @@ const AVAILABLE_COMPONENTS = [ 'organizationProfile', 'organizationSwitcher', 'waitlist', + 'pricingTable', ] as const; const COMPONENT_PROPS_NAMESPACE = 'clerk-js-sandbox'; @@ -73,6 +74,7 @@ const componentControls: Record<(typeof AVAILABLE_COMPONENTS)[number], Component organizationProfile: buildComponentControls('organizationProfile'), organizationSwitcher: buildComponentControls('organizationSwitcher'), waitlist: buildComponentControls('waitlist'), + pricingTable: buildComponentControls('pricingTable'), }; declare global { @@ -95,7 +97,7 @@ const app = document.getElementById('app') as HTMLDivElement; function mountIndex(element: HTMLDivElement) { assertClerkIsLoaded(Clerk); const user = Clerk.user; - element.innerHTML = `
${JSON.stringify({ user }, null, 2)}
`; + element.innerHTML = `
${JSON.stringify({ user }, null, 2)}
`; } function mountOpenSignInButton(element: HTMLDivElement, props) { @@ -167,6 +169,9 @@ function addCurrentRouteIndicator(currentRoute: string) { }, }); }, + '/pricing-table': () => { + Clerk.__experimental_mountPricingTable(app, componentControls.pricingTable.getProps() ?? {}); + }, '/open-sign-in': () => { mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {}); }, @@ -183,6 +188,7 @@ function addCurrentRouteIndicator(currentRoute: string) { ...(componentControls.clerk.getProps() ?? {}), signInUrl: '/sign-in', signUpUrl: '/sign-up', + experimental: { commerce: true }, }); renderCurrentRoute(); } else { diff --git a/packages/clerk-js/sandbox/template.html b/packages/clerk-js/sandbox/template.html index 9193428e975..5fa762e57d8 100644 --- a/packages/clerk-js/sandbox/template.html +++ b/packages/clerk-js/sandbox/template.html @@ -249,6 +249,14 @@ Keyless +
  • + + PricingTable + +
  • diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index cf8258e7f07..454792854d8 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -13,6 +13,8 @@ import { import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url'; import { handleValueOrFn, noop } from '@clerk/shared/utils'; import type { + __experimental_CommerceNamespace, + __experimental_PricingTableProps, __internal_UserVerificationModalProps, AuthenticateWithCoinbaseWalletParams, AuthenticateWithGoogleOneTapParams, @@ -119,6 +121,7 @@ import { import { eventBus, events } from './events'; import type { FapiClient, FapiRequestCallback } from './fapiClient'; import { createFapiClient } from './fapiClient'; +import { __experimental_Commerce } from './modules/commerce'; import { BaseResource, Client, @@ -166,6 +169,7 @@ export class Clerk implements ClerkInterface { version: __PKG_VERSION__, environment: process.env.NODE_ENV || 'production', }; + private static _commerce: __experimental_CommerceNamespace; public client: ClientResource | undefined; public session: SignedInSessionResource | null | undefined; @@ -287,6 +291,19 @@ export class Clerk implements ClerkInterface { return this.#options.standardBrowser || false; } + get __experimental_commerce(): __experimental_CommerceNamespace { + if (!this.#options.experimental?.commerce) { + throw new Error( + 'Clerk: commerce functionality is currently in an experimental state. To enable, pass `experimental.commerce = true`.', + ); + } + + if (!Clerk._commerce) { + Clerk._commerce = new __experimental_Commerce(); + } + return Clerk._commerce; + } + public __internal_getOption(key: K): ClerkOptions[K] { return this.#options[key]; } @@ -886,6 +903,29 @@ export class Clerk implements ClerkInterface { void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node })); }; + public __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps): void => { + this.assertComponentsReady(this.#componentControls); + void this.#componentControls.ensureMounted({ preloadHint: 'PricingTable' }).then(controls => + controls.mountComponent({ + name: 'PricingTable', + appearanceKey: 'pricingTable', + node, + props, + }), + ); + + this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props)); + }; + + public __experimental_unmountPricingTable = (node: HTMLDivElement): void => { + this.assertComponentsReady(this.#componentControls); + void this.#componentControls.ensureMounted().then(controls => + controls.unmountComponent({ + node, + }), + ); + }; + /** * `setActive` can be used to set the active session and/or organization. */ diff --git a/packages/clerk-js/src/core/modules/commerce/Commerce.ts b/packages/clerk-js/src/core/modules/commerce/Commerce.ts new file mode 100644 index 00000000000..5a0c5c8afd1 --- /dev/null +++ b/packages/clerk-js/src/core/modules/commerce/Commerce.ts @@ -0,0 +1,46 @@ +import type { + __experimental_AddPaymentSourceParams, + __experimental_CommerceBillingNamespace, + __experimental_CommerceNamespace, + __experimental_CommercePaymentSourceJSON, + ClerkPaginatedResponse, +} from '@clerk/types'; + +import { __experimental_CommercePaymentSource, BaseResource } from '../../resources/internal'; +import { __experimental_CommerceBilling } from './CommerceBilling'; + +export class __experimental_Commerce implements __experimental_CommerceNamespace { + private static _billing: __experimental_CommerceBillingNamespace; + + get __experimental_billing(): __experimental_CommerceBillingNamespace { + if (!__experimental_Commerce._billing) { + __experimental_Commerce._billing = new __experimental_CommerceBilling(); + } + return __experimental_Commerce._billing; + } + + addPaymentSource = async (params: __experimental_AddPaymentSourceParams) => { + const json = ( + await BaseResource._fetch({ + path: `/me/commerce/payment_sources`, + method: 'POST', + body: params as any, + }) + )?.response as unknown as __experimental_CommercePaymentSourceJSON; + return new __experimental_CommercePaymentSource(json); + }; + + getPaymentSources = async () => { + return await BaseResource._fetch({ + path: `/me/commerce/payment_sources`, + method: 'GET', + }).then(res => { + const { data: paymentSources, total_count } = + res as unknown as ClerkPaginatedResponse<__experimental_CommercePaymentSourceJSON>; + return { + total_count, + data: paymentSources.map(paymentSource => new __experimental_CommercePaymentSource(paymentSource)), + }; + }); + }; +} diff --git a/packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts b/packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts new file mode 100644 index 00000000000..639441bab83 --- /dev/null +++ b/packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts @@ -0,0 +1,37 @@ +import type { + __experimental_CommerceBillingNamespace, + __experimental_CommerceCheckoutJSON, + __experimental_CommercePlanResource, + __experimental_CommerceProductJSON, + __experimental_CreateCheckoutParams, + __experimental_GetPlansParams, + ClerkPaginatedResponse, +} from '@clerk/types'; + +import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams'; +import { __experimental_CommerceCheckout, __experimental_CommercePlan, BaseResource } from '../../resources/internal'; + +export class __experimental_CommerceBilling implements __experimental_CommerceBillingNamespace { + getPlans = async (params?: __experimental_GetPlansParams): Promise<__experimental_CommercePlanResource[]> => { + const { data: products } = (await BaseResource._fetch({ + path: `/commerce/products`, + method: 'GET', + search: convertPageToOffsetSearchParams(params), + })) as unknown as ClerkPaginatedResponse<__experimental_CommerceProductJSON>; + + const defaultProduct = products.find(product => product.is_default); + return defaultProduct?.plans.map(plan => new __experimental_CommercePlan(plan)) || []; + }; + + startCheckout = async (params: __experimental_CreateCheckoutParams) => { + const json = ( + await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({ + path: `/me/commerce/checkouts`, + method: 'POST', + body: params as any, + }) + )?.response as unknown as __experimental_CommerceCheckoutJSON; + + return new __experimental_CommerceCheckout(json); + }; +} diff --git a/packages/clerk-js/src/core/modules/commerce/index.ts b/packages/clerk-js/src/core/modules/commerce/index.ts new file mode 100644 index 00000000000..8a1ec90e17c --- /dev/null +++ b/packages/clerk-js/src/core/modules/commerce/index.ts @@ -0,0 +1,2 @@ +export * from './Commerce'; +export * from './CommerceBilling'; diff --git a/packages/clerk-js/src/core/resources/Base.ts b/packages/clerk-js/src/core/resources/Base.ts index 105e255a37c..3db191e9e74 100644 --- a/packages/clerk-js/src/core/resources/Base.ts +++ b/packages/clerk-js/src/core/resources/Base.ts @@ -62,7 +62,7 @@ export abstract class BaseResource { return !this.id; } - protected static async _fetch( + static async _fetch( requestInit: FapiRequestInit, opts: BaseFetchOptions = {}, ): Promise | null> { diff --git a/packages/clerk-js/src/core/resources/CommerceCheckout.ts b/packages/clerk-js/src/core/resources/CommerceCheckout.ts new file mode 100644 index 00000000000..29c4fa38932 --- /dev/null +++ b/packages/clerk-js/src/core/resources/CommerceCheckout.ts @@ -0,0 +1,63 @@ +import type { + __experimental_CommerceCheckoutJSON, + __experimental_CommerceCheckoutResource, + __experimental_CommerceTotals, + __experimental_ConfirmCheckoutParams, +} from '@clerk/types'; + +import { commerceTotalsFromJSON } from '../../utils'; +import { + __experimental_CommerceInvoice, + __experimental_CommercePaymentSource, + __experimental_CommercePlan, + __experimental_CommerceSubscription, + BaseResource, +} from './internal'; + +export class __experimental_CommerceCheckout extends BaseResource implements __experimental_CommerceCheckoutResource { + pathRoot = '/me/commerce/checkouts'; + + id!: string; + externalClientSecret!: string; + externalGatewayId!: string; + invoice?: __experimental_CommerceInvoice; + paymentSource?: __experimental_CommercePaymentSource; + plan!: __experimental_CommercePlan; + planPeriod!: string; + status!: string; + subscription?: __experimental_CommerceSubscription; + totals!: __experimental_CommerceTotals; + + constructor(data: __experimental_CommerceCheckoutJSON) { + super(); + this.fromJSON(data); + } + + protected fromJSON(data: __experimental_CommerceCheckoutJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.externalClientSecret = data.external_client_secret; + this.externalGatewayId = data.external_gateway_id; + this.invoice = data.invoice ? new __experimental_CommerceInvoice(data.invoice) : undefined; + this.paymentSource = data.payment_source + ? new __experimental_CommercePaymentSource(data.payment_source) + : undefined; + this.plan = new __experimental_CommercePlan(data.plan); + this.planPeriod = data.plan_period; + this.status = data.status; + this.subscription = data.subscription ? new __experimental_CommerceSubscription(data.subscription) : undefined; + this.totals = commerceTotalsFromJSON(data.totals); + + return this; + } + + confirm = (params?: __experimental_ConfirmCheckoutParams): Promise => { + return this._basePatch({ + path: this.path('confirm'), + body: params as any, + }); + }; +} diff --git a/packages/clerk-js/src/core/resources/CommerceFeature.ts b/packages/clerk-js/src/core/resources/CommerceFeature.ts new file mode 100644 index 00000000000..52c3ea37a85 --- /dev/null +++ b/packages/clerk-js/src/core/resources/CommerceFeature.ts @@ -0,0 +1,30 @@ +import type { __experimental_CommerceFeatureJSON, __experimental_CommerceFeatureResource } from '@clerk/types'; + +import { BaseResource } from './internal'; + +export class __experimental_CommerceFeature extends BaseResource implements __experimental_CommerceFeatureResource { + id!: string; + name!: string; + description!: string; + slug!: string; + avatarUrl!: string; + + constructor(data: __experimental_CommerceFeatureJSON) { + super(); + this.fromJSON(data); + } + + protected fromJSON(data: __experimental_CommerceFeatureJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.name = data.name; + this.description = data.description; + this.slug = data.slug; + this.avatarUrl = data.avatar_url; + + return this; + } +} diff --git a/packages/clerk-js/src/core/resources/CommerceInvoice.ts b/packages/clerk-js/src/core/resources/CommerceInvoice.ts new file mode 100644 index 00000000000..c718b231cec --- /dev/null +++ b/packages/clerk-js/src/core/resources/CommerceInvoice.ts @@ -0,0 +1,39 @@ +import type { + __experimental_CommerceInvoiceJSON, + __experimental_CommerceInvoiceResource, + __experimental_CommerceTotals, +} from '@clerk/types'; + +import { commerceTotalsFromJSON } from '../../utils'; +import { BaseResource } from './internal'; + +export class __experimental_CommerceInvoice extends BaseResource implements __experimental_CommerceInvoiceResource { + id!: string; + paymentSourceId!: string; + planId!: string; + paymentDueOn!: number; + paidOn!: number; + status!: string; + totals!: __experimental_CommerceTotals; + + constructor(data: __experimental_CommerceInvoiceJSON) { + super(); + this.fromJSON(data); + } + + protected fromJSON(data: __experimental_CommerceInvoiceJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.paymentSourceId = data.payment_source_id; + this.planId = data.plan_id; + this.paymentDueOn = data.payment_due_on; + this.paidOn = data.paid_on; + this.status = data.status; + this.totals = commerceTotalsFromJSON(data.totals); + + return this; + } +} diff --git a/packages/clerk-js/src/core/resources/CommercePaymentSource.ts b/packages/clerk-js/src/core/resources/CommercePaymentSource.ts new file mode 100644 index 00000000000..b7faf86881c --- /dev/null +++ b/packages/clerk-js/src/core/resources/CommercePaymentSource.ts @@ -0,0 +1,34 @@ +import type { + __experimental_CommercePaymentSourceJSON, + __experimental_CommercePaymentSourceResource, +} from '@clerk/types'; + +import { BaseResource } from './internal'; + +export class __experimental_CommercePaymentSource + extends BaseResource + implements __experimental_CommercePaymentSourceResource +{ + id!: string; + last4!: string; + paymentMethod!: string; + cardType!: string; + + constructor(data: __experimental_CommercePaymentSourceJSON) { + super(); + this.fromJSON(data); + } + + protected fromJSON(data: __experimental_CommercePaymentSourceJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.last4 = data.last4; + this.paymentMethod = data.payment_method; + this.cardType = data.card_type; + + return this; + } +} diff --git a/packages/clerk-js/src/core/resources/CommercePlan.ts b/packages/clerk-js/src/core/resources/CommercePlan.ts new file mode 100644 index 00000000000..c3acf458714 --- /dev/null +++ b/packages/clerk-js/src/core/resources/CommercePlan.ts @@ -0,0 +1,54 @@ +import type { __experimental_CommercePlanJSON, __experimental_CommercePlanResource } from '@clerk/types'; + +import { __experimental_CommerceFeature, BaseResource } from './internal'; + +export class __experimental_CommercePlan extends BaseResource implements __experimental_CommercePlanResource { + id!: string; + name!: string; + amount!: number; + amountFormatted!: string; + annualMonthlyAmount!: number; + annualMonthlyAmountFormatted!: string; + currencySymbol!: string; + currency!: string; + description!: string; + isActiveForPayer!: boolean; + isRecurring!: boolean; + hasBaseFee!: boolean; + payerType!: string[]; + publiclyVisible!: boolean; + slug!: string; + avatarUrl!: string; + features!: __experimental_CommerceFeature[]; + + constructor(data: __experimental_CommercePlanJSON) { + super(); + this.fromJSON(data); + } + + protected fromJSON(data: __experimental_CommercePlanJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.name = data.name; + this.amount = data.amount; + this.amountFormatted = data.amount_formatted; + this.annualMonthlyAmount = data.annual_monthly_amount; + this.annualMonthlyAmountFormatted = data.annual_monthly_amount_formatted; + this.currencySymbol = data.currency_symbol; + this.currency = data.currency; + this.description = data.description; + this.isActiveForPayer = data.is_active_for_payer; + this.isRecurring = data.is_recurring; + this.hasBaseFee = data.has_base_fee; + this.payerType = data.payer_type; + this.publiclyVisible = data.publicly_visible; + this.slug = data.slug; + this.avatarUrl = data.avatar_url; + this.features = data.features.map(feature => new __experimental_CommerceFeature(feature)); + + return this; + } +} diff --git a/packages/clerk-js/src/core/resources/CommerceProduct.ts b/packages/clerk-js/src/core/resources/CommerceProduct.ts new file mode 100644 index 00000000000..cf7de1b42cb --- /dev/null +++ b/packages/clerk-js/src/core/resources/CommerceProduct.ts @@ -0,0 +1,30 @@ +import type { __experimental_CommerceProductJSON, __experimental_CommerceProductResource } from '@clerk/types'; + +import { __experimental_CommercePlan, BaseResource } from './internal'; + +export class __experimental_CommerceProduct extends BaseResource implements __experimental_CommerceProductResource { + id!: string; + slug!: string; + currency!: string; + isDefault!: boolean; + plans!: __experimental_CommercePlan[]; + + constructor(data: __experimental_CommerceProductJSON) { + super(); + this.fromJSON(data); + } + + protected fromJSON(data: __experimental_CommerceProductJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.slug = data.slug; + this.currency = data.currency; + this.isDefault = data.is_default; + this.plans = data.plans.map(plan => new __experimental_CommercePlan(plan)); + + return this; + } +} diff --git a/packages/clerk-js/src/core/resources/CommerceSettings.ts b/packages/clerk-js/src/core/resources/CommerceSettings.ts new file mode 100644 index 00000000000..5c0e0db1928 --- /dev/null +++ b/packages/clerk-js/src/core/resources/CommerceSettings.ts @@ -0,0 +1,35 @@ +import type { + __experimental_CommerceSettingsJSON, + __experimental_CommerceSettingsJSONSnapshot, + __experimental_CommerceSettingsResource, +} from '@clerk/types'; + +import { BaseResource } from './internal'; + +/** + * @internal + */ +export class __experimental_CommerceSettings extends BaseResource implements __experimental_CommerceSettingsResource { + stripePublishableKey!: string; + + public constructor(data: __experimental_CommerceSettingsJSON | __experimental_CommerceSettingsJSONSnapshot) { + super(); + this.fromJSON(data); + } + + protected fromJSON( + data: __experimental_CommerceSettingsJSON | __experimental_CommerceSettingsJSONSnapshot | null, + ): this { + if (!data) { + return this; + } + this.stripePublishableKey = data.stripe_publishable_key; + return this; + } + + public __internal_toSnapshot(): __experimental_CommerceSettingsJSONSnapshot { + return { + stripe_publishable_key: this.stripePublishableKey, + } as unknown as __experimental_CommerceSettingsJSONSnapshot; + } +} diff --git a/packages/clerk-js/src/core/resources/CommerceSubscription.ts b/packages/clerk-js/src/core/resources/CommerceSubscription.ts new file mode 100644 index 00000000000..b7f7d5bdf94 --- /dev/null +++ b/packages/clerk-js/src/core/resources/CommerceSubscription.ts @@ -0,0 +1,46 @@ +import type { + __experimental_CommerceSubscriptionJSON, + __experimental_CommerceSubscriptionResource, +} from '@clerk/types'; + +import { __experimental_CommercePlan, BaseResource } from './internal'; + +export class __experimental_CommerceSubscription + extends BaseResource + implements __experimental_CommerceSubscriptionResource +{ + id!: string; + paymentSourceId!: string; + plan!: __experimental_CommercePlan; + planPeriod!: string; + status!: string; + + constructor(data: __experimental_CommerceSubscriptionJSON) { + super(); + this.fromJSON(data); + } + + protected fromJSON(data: __experimental_CommerceSubscriptionJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.paymentSourceId = data.payment_source_id; + this.plan = new __experimental_CommercePlan(data.plan); + this.planPeriod = data.plan_period; + this.status = data.status; + + return this; + } + + public async cancel() { + const json = ( + await BaseResource._fetch({ + path: `/me/commerce/subscriptions/${this.id}`, + method: 'DELETE', + }) + )?.response; + return json; + } +} diff --git a/packages/clerk-js/src/core/resources/Environment.ts b/packages/clerk-js/src/core/resources/Environment.ts index 7a9d9b379cc..ae033e5ecb1 100644 --- a/packages/clerk-js/src/core/resources/Environment.ts +++ b/packages/clerk-js/src/core/resources/Environment.ts @@ -1,4 +1,5 @@ import type { + __experimental_CommerceSettingsResource, AuthConfigResource, DisplayConfigResource, EnvironmentJSON, @@ -8,7 +9,7 @@ import type { UserSettingsResource, } from '@clerk/types'; -import { AuthConfig, BaseResource, DisplayConfig, UserSettings } from './internal'; +import { __experimental_CommerceSettings, AuthConfig, BaseResource, DisplayConfig, UserSettings } from './internal'; import { OrganizationSettings } from './OrganizationSettings'; export class Environment extends BaseResource implements EnvironmentResource { @@ -19,6 +20,7 @@ export class Environment extends BaseResource implements EnvironmentResource { displayConfig!: DisplayConfigResource; userSettings!: UserSettingsResource; organizationSettings!: OrganizationSettingsResource; + __experimental_commerceSettings!: __experimental_CommerceSettingsResource; maintenanceMode!: boolean; public static getInstance(): Environment { @@ -60,6 +62,7 @@ export class Environment extends BaseResource implements EnvironmentResource { protected fromJSON(data: EnvironmentJSONSnapshot | EnvironmentJSON | null): this { if (data) { this.authConfig = new AuthConfig(data.auth_config); + this.__experimental_commerceSettings = new __experimental_CommerceSettings(data.commerce_settings); this.displayConfig = new DisplayConfig(data.display_config); this.userSettings = new UserSettings(data.user_settings); this.organizationSettings = new OrganizationSettings(data.organization_settings); @@ -76,6 +79,7 @@ export class Environment extends BaseResource implements EnvironmentResource { display_config: this.displayConfig.__internal_toSnapshot(), user_settings: this.userSettings.__internal_toSnapshot(), organization_settings: this.organizationSettings.__internal_toSnapshot(), + commerce_settings: this.__experimental_commerceSettings.__internal_toSnapshot(), maintenance_mode: this.maintenanceMode, }; } diff --git a/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Environment.test.ts.snap b/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Environment.test.ts.snap index 5cce8ad56f0..bee8b3b9ac2 100644 --- a/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Environment.test.ts.snap +++ b/packages/clerk-js/src/core/resources/__tests__/__snapshots__/Environment.test.ts.snap @@ -9,6 +9,9 @@ exports[`Environment __internal_toSnapshot() 1`] = ` "reverification": true, "single_session_mode": true, }, + "commerce_settings": { + "stripe_publishable_key": undefined, + }, "display_config": { "after_create_organization_url": "", "after_join_waitlist_url": "", @@ -267,6 +270,9 @@ exports[`Environment __internal_toSnapshot() 1`] = ` exports[`Environment has the same initial properties 1`] = ` Environment { + "__experimental_commerceSettings": __experimental_CommerceSettings { + "pathRoot": "", + }, "authConfig": AuthConfig { "claimedAt": null, "pathRoot": "", diff --git a/packages/clerk-js/src/core/resources/internal.ts b/packages/clerk-js/src/core/resources/internal.ts index 860dd16786e..5516809ee9d 100644 --- a/packages/clerk-js/src/core/resources/internal.ts +++ b/packages/clerk-js/src/core/resources/internal.ts @@ -1,8 +1,16 @@ export type { Clerk } from '../clerk'; export * from './Base'; export * from './UserSettings'; +export * from './CommerceSettings'; export * from './AuthConfig'; export * from './Client'; +export * from './CommerceCheckout'; +export * from './CommerceFeature'; +export * from './CommerceInvoice'; +export * from './CommercePaymentSource'; +export * from './CommercePlan'; +export * from './CommerceProduct'; +export * from './CommerceSubscription'; export * from './DeletedObject'; export * from './DisplayConfig'; export * from './EmailAddress'; diff --git a/packages/clerk-js/src/ui/common/CommerceBlade.tsx b/packages/clerk-js/src/ui/common/CommerceBlade.tsx new file mode 100644 index 00000000000..360e9a55b09 --- /dev/null +++ b/packages/clerk-js/src/ui/common/CommerceBlade.tsx @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react'; + +import { Box } from '../customizables'; +import { animations } from '../styledSystem'; + +interface CommerceBladeProps { + isOpen: boolean; + isFullscreen?: boolean; + children: React.ReactNode; +} + +export const CommerceBlade = ({ isOpen, isFullscreen, children }: CommerceBladeProps) => { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + if (isOpen) { + setMounted(true); + return; + } else { + const timer = setTimeout(() => { + setMounted(false); + }, 280); + return () => clearTimeout(timer); + } + }, [isOpen, mounted]); + + if (!mounted && !isOpen) { + return null; + } + + return ( + + {children} + + ); +}; + +const CommerceBladeContent = ({ isOpen, isFullscreen, children }: CommerceBladeProps) => { + return ( + <> + ({ + position: 'absolute', + zIndex: t.zIndices.$modal, + inset: 0, + backgroundColor: t.colors.$whiteAlpha300, + animation: `${isOpen ? animations.fadeIn : animations.fadeOut} ${t.transitionDuration.$slower} ${t.transitionTiming.$common}`, + })} + /> + ({ + position: isFullscreen ? 'fixed' : 'absolute', + width: t.sizes.$100, + inset: isFullscreen ? t.space.$3 : 0, + insetInlineStart: 'auto', + overflow: 'hidden', + backgroundColor: t.colors.$colorBackground, + borderRadius: `${t.radii.$xl} ${isFullscreen ? t.radii.$xl : 0} ${isFullscreen ? t.radii.$xl : 0} ${t.radii.$xl}`, + boxShadow: + '0px 0px 0px 1px rgba(25, 28, 33, 0.06), 0px 15px 35px -5px rgba(25, 28, 33, 0.20), 0px 5px 15px 0px rgba(0, 0, 0, 0.08)', + zIndex: t.zIndices.$modal, + animation: `${isOpen ? animations.drawerSlideIn : animations.drawerSlideOut} ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}`, + })} + > + {children} + + + ); +}; diff --git a/packages/clerk-js/src/ui/common/index.ts b/packages/clerk-js/src/ui/common/index.ts index bea7cf9e442..4641ea459bf 100644 --- a/packages/clerk-js/src/ui/common/index.ts +++ b/packages/clerk-js/src/ui/common/index.ts @@ -1,5 +1,6 @@ export * from './BlockButtons'; export * from './CalloutWithAction'; +export * from './CommerceBlade'; export * from './constants'; export * from './EmailLinkStatusCard'; export * from './EmailLinkVerify'; diff --git a/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx b/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx new file mode 100644 index 00000000000..81ad97b6828 --- /dev/null +++ b/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx @@ -0,0 +1,34 @@ +import type { __experimental_CheckoutProps } from '@clerk/types'; + +import { CommerceBlade } from '../../common'; +import { useCheckoutContext, withCoreUserGuard } from '../../contexts'; +import { Flow } from '../../customizables'; +import { Route, Switch } from '../../router'; +import { CheckoutPage } from './CheckoutPage'; + +export const __experimental_Checkout = (props: __experimental_CheckoutProps) => { + return ( + + + + + + + + + + ); +}; + +const AuthenticatedRoutes = withCoreUserGuard((props: __experimental_CheckoutProps) => { + const { mode = 'mounted', isShowingBlade = false } = useCheckoutContext(); + + return ( + + + + ); +}); diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx new file mode 100644 index 00000000000..13a43e04fba --- /dev/null +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx @@ -0,0 +1,210 @@ +import type { __experimental_CommerceCheckoutResource } from '@clerk/types'; + +import { useCheckoutContext } from '../../contexts'; +import { Box, Button, Col, Flex, Icon, Text } from '../../customizables'; +import { LineItems } from '../../elements'; +import { Check } from '../../icons'; +import type { ThemableCssProp } from '../../styledSystem'; + +export const CheckoutComplete = ({ + checkout, + sx, +}: { + checkout: __experimental_CommerceCheckoutResource; + sx?: ThemableCssProp; +}) => { + const { handleCloseBlade = () => {} } = useCheckoutContext(); + + return ( + ({ + width: '100%', + padding: t.space.$4, + }), + sx, + ]} + > + + + + + {/* TODO(@COMMERCE): needs localization */} + Payment was successful! + ({ textAlign: 'center', paddingInline: t.space.$8 })}> + {/* TODO(@COMMERCE): needs localization */} + Minim adipisicing enim fugiat enim est ad nisi exercitation nisi exercitation quis culpa. + + + + ({ + flex: 0, + paddingTop: t.space.$4, + borderTopWidth: t.borderWidths.$normal, + borderTopStyle: t.borderStyles.$solid, + borderTopColor: t.colors.$neutralAlpha100, + })} + > + + + Total paid + + {checkout.invoice + ? `${checkout.invoice.totals.grandTotal.currencySymbol}${checkout.invoice.totals.grandTotal.amountFormatted}` + : '–'} + + + + {/* TODO(@COMMERCE): needs localization */} + Payment method + + {checkout.paymentSource ? `${checkout.paymentSource.cardType} ⋯ ${checkout.paymentSource.last4}` : '–'} + + + + {/* TODO(@COMMERCE): needs localization */} + Invoice ID + {checkout.invoice ? checkout.invoice.id : '–'} + + + + + + ); +}; + +const SuccessCircle = () => { + return ( + ({ + position: 'relative', + width: '100%', + height: t.sizes.$16, + })} + > + {/* rings */} + + ({ + position: 'absolute', + top: `-${t.sizes.$8}`, + bottom: `-${t.sizes.$8}`, + left: '50%', + translate: '-50% 0', + aspectRatio: '1/1', + borderWidth: 1, + borderStyle: 'solid', + borderColor: t.colors.$neutralAlpha150, + borderRadius: t.radii.$circle, + })} + /> + ({ + position: 'absolute', + top: `-${t.sizes.$24}`, + bottom: `-${t.sizes.$24}`, + left: '50%', + translate: '-50% 0', + aspectRatio: '1/1', + borderWidth: 1, + borderStyle: 'solid', + borderColor: t.colors.$neutralAlpha200, + borderRadius: t.radii.$circle, + })} + /> + ({ + position: 'absolute', + top: `-${t.sizes.$40}`, + bottom: `-${t.sizes.$40}`, + left: '50%', + translate: '-50% 0', + aspectRatio: '1/1', + borderWidth: 1, + borderStyle: 'solid', + borderColor: t.colors.$neutralAlpha200, + borderRadius: t.radii.$circle, + })} + /> + + + {/* fade overlays */} + ({ + position: 'absolute', + width: '120%', + aspectRatio: '1/1', + top: '50%', + translate: '0 -50%', + backgroundImage: `linear-gradient(to bottom, ${t.colors.$colorBackground} 35%, transparent 48%, transparent 52%, ${t.colors.$colorBackground} 65%)`, + })} + /> + + {/* coin */} + ({ + position: 'relative', + width: t.sizes.$16, + height: t.sizes.$16, + borderRadius: t.radii.$circle, + backgroundImage: + 'linear-gradient(180deg, rgba(255, 255, 255, 0.30) 0%, rgba(0, 0, 0, 0.12) 50%, rgba(0, 0, 0, 0.30) 95.31%)', + })} + > + ({ + position: 'relative', + width: t.sizes.$16, + height: t.sizes.$16, + borderRadius: t.radii.$circle, + backgroundImage: 'linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.00) 60.94%)', + backgroundBlendMode: 'plus-lighter, normal', + boxShadow: '0px 4px 12px 0px rgba(0, 0, 0, 0.35), 0px 1px 0px 0px rgba(255, 255, 255, 0.05) inset', + })} + > + ({ + position: 'absolute', + inset: t.space.$1, + borderRadius: t.radii.$circle, + backgroundColor: t.colors.$colorBackground, + })} + > + + + + + + ); +}; diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx new file mode 100644 index 00000000000..2d6cb7e122d --- /dev/null +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx @@ -0,0 +1,340 @@ +import { useClerk } from '@clerk/shared/react'; +import type { + __experimental_CommerceCheckoutResource, + __experimental_CommerceMoney, + __experimental_CommercePaymentSourceResource, + ClerkAPIError, + ClerkRuntimeError, +} from '@clerk/types'; +import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js'; +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { Button, Col, Flex, Form, Icon, Text } from '../../customizables'; +import { Alert, Disclosure, Divider, Select, SelectButton, SelectOptionList } from '../../elements'; +import { useFetch } from '../../hooks'; +import { ArrowUpDown, CreditCard } from '../../icons'; +import { animations } from '../../styledSystem'; +import { handleError } from '../../utils'; + +export const CheckoutForm = ({ + checkout, + onCheckoutComplete, +}: { + checkout: __experimental_CommerceCheckoutResource; + onCheckoutComplete: (checkout: __experimental_CommerceCheckoutResource) => void; +}) => { + const stripe = useStripe(); + const elements = useElements(); + const { __experimental_commerce } = useClerk(); + const [openAccountFundsDropDown, setOpenAccountFundsDropDown] = useState(true); + const [openAddNewSourceDropDown, setOpenAddNewSourceDropDown] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(); + + const { data } = useFetch(__experimental_commerce?.getPaymentSources, {}); + const { data: paymentSources } = data || { data: [] }; + + const didExpandStripePaymentMethods = useCallback(() => { + setOpenAccountFundsDropDown(false); + }, []); + + const confirmCheckout = async ({ paymentSourceId }: { paymentSourceId: string }) => { + return checkout + .confirm({ paymentSourceId }) + .then(newCheckout => { + onCheckoutComplete(newCheckout); + }) + .catch(error => { + throw error; + }); + }; + + const onPaymentSourceSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + setSubmitError(undefined); + + const data = new FormData(e.currentTarget); + const paymentSourceId = data.get('payment_source_id') as string; + + try { + await confirmCheckout({ paymentSourceId }); + } catch (error) { + handleError(error, [], setSubmitError); + } finally { + setIsSubmitting(false); + } + }; + + const onStripeSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!stripe || !elements) { + return; + } + setIsSubmitting(true); + setSubmitError(undefined); + + try { + const { setupIntent, error } = await stripe.confirmSetup({ + elements, + confirmParams: { + return_url: '', // TODO(@COMMERCE): need to figure this out + }, + redirect: 'if_required', + }); + if (error) { + return; + } + + const paymentSource = await __experimental_commerce.addPaymentSource({ + gateway: 'stripe', + paymentMethod: 'card', + paymentToken: setupIntent.payment_method as string, + }); + + await confirmCheckout({ paymentSourceId: paymentSource.id }); + } catch (error) { + console.log(error); + handleError(error, [], setSubmitError); + } finally { + setIsSubmitting(false); + } + }; + + return ( + ({ padding: t.space.$4 })} + > + {submitError && ( + ({ + animation: `${animations.textInBig} ${t.transitionDuration.$slow}`, + })} + > + {typeof submitError === 'string' ? submitError : submitError.message} + + )} + {paymentSources.length > 0 && ( + <> + + Account Funds + + + + + + + + + )} + + + Add a New Payment Source + + + + + + ); +}; + +const PaymentSourceMethods = ({ + totalDueNow, + paymentSources, + onPaymentSourceSubmit, + isSubmitting, +}: { + totalDueNow: __experimental_CommerceMoney; + paymentSources: __experimental_CommercePaymentSourceResource[]; + onPaymentSourceSubmit: React.FormEventHandler; + isSubmitting: boolean; +}) => { + const [selectedPaymentSource, setSelectedPaymentSource] = useState< + __experimental_CommercePaymentSourceResource | undefined + >(paymentSources.length > 0 ? paymentSources[0] : undefined); + + const options = useMemo(() => { + return paymentSources.map(source => { + return { + value: source.id, + label: `${source.cardType} ⋯ ${source.last4}`, + }; + }); + }, [paymentSources]); + + return ( +
    + + + ({ + justifyContent: 'space-between', + backgroundColor: t.colors.$colorBackground, + })} + > + {selectedPaymentSource && ( + + + + {selectedPaymentSource.cardType} ⋯ {selectedPaymentSource.last4} + + + )} + + + + + + + ); +}; + +const StripePaymentMethods = ({ + totalDueNow, + onStripeSubmit, + onExpand, + isSubmitting, +}: { + totalDueNow: __experimental_CommerceMoney; + onStripeSubmit: React.FormEventHandler; + onExpand: () => void; + isSubmitting: boolean; +}) => { + const [collapsed, setCollapsed] = useState(true); + + useEffect(() => { + if (!collapsed) { + onExpand(); + } + }, [collapsed, onExpand]); + + return ( +
    + + + {collapsed ? ( + <> + + + + + ) : ( + <> + + + + )} + + + ); +}; diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx new file mode 100644 index 00000000000..e1087cc7e22 --- /dev/null +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx @@ -0,0 +1,197 @@ +import type { + __experimental_CheckoutProps, + __experimental_CommercePlanResource, + __experimental_CommerceTotals, +} from '@clerk/types'; +import { Elements } from '@stripe/react-stripe-js'; +import type { Stripe } from '@stripe/stripe-js'; +import { loadStripe } from '@stripe/stripe-js'; +import { useEffect, useRef, useState } from 'react'; + +import { useCheckoutContext, useEnvironment } from '../../contexts'; +import { Alert, Box, Button, Col, Flex, Heading, Icon, Spinner } from '../../customizables'; +import { LineItems } from '../../elements'; +import { useCheckout } from '../../hooks'; +import { Close } from '../../icons'; +import { CheckoutComplete } from './CheckoutComplete'; +import { CheckoutForm } from './CheckoutForm'; + +export const CheckoutPage = (props: __experimental_CheckoutProps) => { + const { planId, planPeriod } = props; + const stripePromiseRef = useRef | null>(null); + const [stripe, setStripe] = useState(null); + const { __experimental_commerceSettings } = useEnvironment(); + + const { checkout, updateCheckout, isLoading } = useCheckout({ + planId, + planPeriod, + }); + + useEffect(() => { + if ( + !stripePromiseRef.current && + checkout?.externalGatewayId && + __experimental_commerceSettings.stripePublishableKey + ) { + stripePromiseRef.current = loadStripe(__experimental_commerceSettings.stripePublishableKey, { + stripeAccount: checkout.externalGatewayId, + }); + void stripePromiseRef.current.then(stripeInstance => { + setStripe(stripeInstance); + }); + } + }, [checkout?.externalGatewayId, __experimental_commerceSettings]); + + return ( + <> + + + {isLoading ? ( + + + + ) : !checkout ? ( + + {/* TODO(@COMMERCE): needs localization */} + There was a problem, please try again later. + + ) : checkout.status === 'completed' ? ( + ({ height: `calc(100% - ${t.space.$12})` })} + /> + ) : ( + ({ + overflowY: 'auto', + /* minus the height of the header */ + height: `calc(100% - ${t.space.$12})`, + overflowX: 'hidden', + })} + > + ({ + padding: t.space.$4, + backgroundColor: t.colors.$neutralAlpha25, + borderBottomWidth: t.borderWidths.$normal, + borderBottomStyle: t.borderStyles.$solid, + borderBottomColor: t.colors.$neutralAlpha100, + })} + > + + + + {stripe && ( + + + + )} + + )} + + ); +}; + +const CheckoutHeader = ({ title }: { title: string }) => { + const { handleCloseBlade = () => {} } = useCheckoutContext(); + + return ( + ({ + position: 'sticky', + top: 0, + width: '100%', + height: t.space.$12, + paddingInline: `${t.space.$5} ${t.space.$2}`, + borderBottomWidth: t.borderWidths.$normal, + borderBottomStyle: t.borderStyles.$solid, + borderBottomColor: t.colors.$neutralAlpha100, + })} + > + {title} + + + ); +}; + +// TODO(@COMMERCE): needs localization +const CheckoutPlanRows = ({ + plan, + planPeriod, + totals, +}: { + plan: __experimental_CommercePlanResource; + planPeriod: string; + totals: __experimental_CommerceTotals; +}) => { + return ( + + + {plan.name} + + {plan.currencySymbol} + {planPeriod === 'month' ? plan.amountFormatted : plan.annualMonthlyAmountFormatted} + + + + Subtotal + + {totals.subtotal.currencySymbol} + {totals.subtotal.amountFormatted} + + + + Tax + + {totals.taxTotal.currencySymbol} + {totals.taxTotal.amountFormatted} + + + + Total{totals.totalDueNow ? ' Due Today' : ''} + + {totals.totalDueNow + ? `${totals.totalDueNow.currencySymbol}${totals.totalDueNow.amountFormatted}` + : `${totals.grandTotal.currencySymbol}${totals.grandTotal.amountFormatted}`} + + + + ); +}; diff --git a/packages/clerk-js/src/ui/components/Checkout/index.ts b/packages/clerk-js/src/ui/components/Checkout/index.ts new file mode 100644 index 00000000000..9c0182126ac --- /dev/null +++ b/packages/clerk-js/src/ui/components/Checkout/index.ts @@ -0,0 +1 @@ +export * from './Checkout'; diff --git a/packages/clerk-js/src/ui/components/PricingTable/PlanCard.tsx b/packages/clerk-js/src/ui/components/PricingTable/PlanCard.tsx new file mode 100644 index 00000000000..8cfae45beea --- /dev/null +++ b/packages/clerk-js/src/ui/components/PricingTable/PlanCard.tsx @@ -0,0 +1,323 @@ +import type { __experimental_CommercePlanResource, __experimental_PricingTableProps } from '@clerk/types'; +import * as React from 'react'; + +import { + Badge, + Box, + Button, + descriptors, + Flex, + Heading, + Icon, + localizationKeys, + SimpleButton, + Span, + Text, + useAppearance, +} from '../../customizables'; +import { Avatar, SegmentedControl } from '../../elements'; +import { usePrefersReducedMotion } from '../../hooks'; +import { Check, InformationCircle, Minus, Plus } from '../../icons'; +import type { ThemableCssProp } from '../../styledSystem'; +import { common } from '../../styledSystem'; +import { colors } from '../../utils'; + +interface PlanCardProps { + plan: __experimental_CommercePlanResource; + period: string; + setPeriod: (k: string) => void; + onSelect: (plan: __experimental_CommercePlanResource) => void; + isCompact?: boolean; + props: __experimental_PricingTableProps; +} + +export function PlanCard(props: PlanCardProps) { + const { plan, period, setPeriod, onSelect, props: pricingTableProps, isCompact = false } = props; + const { ctaPosition = 'top', collapseFeatures = false } = pricingTableProps; + const [showAllFeatures, setShowAllFeatures] = React.useState(false); + const totalFeatures = plan.features.length; + const hasFeatures = totalFeatures > 0; + const canToggleFeatures = isCompact && totalFeatures > 3; + const isActivePlan = plan.isActiveForPayer; + const prefersReducedMotion = usePrefersReducedMotion(); + const { animations: appearanceAnimations } = useAppearance().parsedLayout; + const planCardFeePeriodNoticeAnimation: ThemableCssProp = t => ({ + transition: + appearanceAnimations && !prefersReducedMotion + ? `grid-template-rows ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}` + : 'none', + }); + + const toggleFeatures = () => { + setShowAllFeatures(prev => !prev); + }; + + return ( + ({ + display: 'flex', + flexDirection: 'column', + background: common.mergedColorsBackground( + colors.setAlpha(t.colors.$colorBackground, 1), + t.colors.$neutralAlpha50, + ), + borderWidth: t.borderWidths.$normal, + borderStyle: t.borderStyles.$solid, + borderColor: t.colors.$neutralAlpha100, + boxShadow: !isCompact ? t.shadows.$cardBoxShadow : undefined, + borderRadius: t.radii.$xl, + overflow: 'hidden', + textAlign: 'left', + })} + > + ({ + padding: isCompact ? t.space.$3 : t.space.$4, + })} + > + + 40} + title={plan.name} + initials={plan.name[0]} + rounded={false} + imageUrl={plan.avatarUrl} + /> + {isActivePlan ? ( + + ) : null} + + ({ + marginTop: t.space.$3, + })} + > + {plan.name} + + {!isCompact && plan.description ? ( + + {plan.description} + + ) : null} + ({ + marginTop: isCompact ? t.space.$2 : t.space.$3, + columnGap: t.space.$1x5, + })} + > + {plan.hasBaseFee ? ( + <> + + {plan.currencySymbol} + {period === 'month' ? plan.amountFormatted : plan.annualMonthlyAmountFormatted} + + ({ + textTransform: 'lowercase', + ':before': { + content: '"/"', + marginInlineEnd: t.space.$1, + }, + })} + localizationKey={localizationKeys('__experimental_commerce.month')} + /> + ({ + width: '100%', + display: 'grid', + gridTemplateRows: period === 'annual' ? '1fr' : '0fr', + }), + planCardFeePeriodNoticeAnimation, + ]} + // @ts-ignore - Needed until React 19 support + inert={period !== 'annual' ? 'true' : undefined} + > + + ({ + width: '100%', + display: 'flex', + alignItems: 'center', + columnGap: t.space.$1, + })} + > + {' '} + + + + + + ) : ( + + )} + + {plan.hasBaseFee ? ( + ({ + display: 'flex', + marginTop: t.space.$3, + })} + > + + Monthly + Annually + + + ) : null} + + + {!collapseFeatures && hasFeatures ? ( + ({ + display: 'flex', + flexDirection: 'column', + flex: '1', + padding: isCompact ? t.space.$3 : t.space.$4, + backgroundColor: t.colors.$colorBackground, + borderTopWidth: t.borderWidths.$normal, + borderTopStyle: t.borderStyles.$solid, + borderTopColor: t.colors.$neutralAlpha100, + })} + > + ({ + display: 'grid', + flex: '1', + rowGap: isCompact ? t.space.$2 : t.space.$3, + })} + > + {plan.features.slice(0, showAllFeatures ? totalFeatures : 3).map(feature => ( + + + + {feature.description || feature.name} + + + ))} + + {canToggleFeatures && ( + ({ + marginBlockStart: t.space.$2, + gap: t.space.$1, + })} + > + + {showAllFeatures ? 'Hide features' : 'See all features'} + + )} + + ) : null} + ({ + marginTop: 'auto', + padding: isCompact ? t.space.$3 : t.space.$4, + borderTopWidth: t.borderWidths.$normal, + borderTopStyle: t.borderStyles.$solid, + borderTopColor: t.colors.$neutralAlpha100, + background: collapseFeatures || !hasFeatures ? t.colors.$colorBackground : undefined, + })} + > + + 40} + title={plan.name} + initials={plan.name[0]} + rounded={false} + imageUrl={plan.avatarUrl} + /> + + {plan.name} + {plan.hasBaseFee ? ( + + + {plan.currencySymbol} + {plan.amountFormatted} + + + + / + + + + + ) : ( + + )} + + + {plan.description} + + + + ({ + flex: 1, + padding: t.space.$4, + overflowY: 'auto', + overflowX: 'hidden', + })} + > + + Available features + + {plan.features.map(feature => ( + + 24} + title={feature.name} + rounded={false} + imageUrl={feature.avatarUrl} + /> + + {feature.name} + + {feature.description} + + + + ))} + + + + + + ); +}; + +const CancelFooter = ({ plan }: { plan: __experimental_CommercePlanResource; handleClose: () => void }) => { + // const { __experimental_commerce } = useClerk(); + const [showConfirmation, setShowConfirmation] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [hasError, setHasError] = useState(false); + + const cancelSubscription = async () => { + setHasError(false); + setIsSubmitting(true); + + // TODO: we need to get a handle on the subscription object in order to cancel it, + // but this method doesn't exist yet. + // + // await subscription.cancel().then(() => { + // setIsSubmitting(false); + // handleClose(); + // }).catch(() => { setHasError(true); setIsSubmitting(false); }); + }; + + // TODO: remove when we can hook up cancel button + // return null; + + return ( + ({ + flex: 0, + padding: t.space.$4, + borderTopWidth: t.borderWidths.$normal, + borderTopStyle: t.borderStyles.$solid, + borderTopColor: t.colors.$neutralAlpha100, + backgroundColor: t.colors.$neutralAlpha50, + })} + > + {showConfirmation ? ( + + Cancel {plan.name} Subscription? + + You can keep using “{plan.name}” features until [DATE], after which you will no longer have + access. + + {hasError && ( + There was a problem canceling your subscription, please try again. + )} + + {!isSubmitting && ( + + )} + + + + ) : ( + + )} + + ); +}; diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx new file mode 100644 index 00000000000..6b788ae4472 --- /dev/null +++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx @@ -0,0 +1,91 @@ +import { useClerk } from '@clerk/shared/react'; +import type { __experimental_CommercePlanResource, __experimental_PricingTableProps } from '@clerk/types'; +import { useState } from 'react'; + +import { __experimental_CheckoutContext, usePricingTableContext } from '../../contexts'; +import { Box, descriptors } from '../../customizables'; +import { useFetch } from '../../hooks'; +import { InternalThemeProvider } from '../../styledSystem'; +import { __experimental_Checkout } from '../Checkout'; +import { PlanCard } from './PlanCard'; +import { PlanDetailBlade } from './PlanDetailBlade'; + +export const __experimental_PricingTable = (props: __experimental_PricingTableProps) => { + const { __experimental_commerce } = useClerk(); + const { mode = 'mounted' } = usePricingTableContext(); + const [planPeriod, setPlanPeriod] = useState('month'); + const [selectedPlan, setSelectedPlan] = useState<__experimental_CommercePlanResource>(); + const [showCheckout, setShowCheckout] = useState(false); + const [showPlanDetail, setShowPlanDetail] = useState(false); + const isCompact = mode === 'modal'; + + const { data: plans } = useFetch(__experimental_commerce?.__experimental_billing.getPlans, 'commerce-plans'); + + const selectPlan = (plan: __experimental_CommercePlanResource) => { + setSelectedPlan(plan); + if (plan.isActiveForPayer) { + setShowPlanDetail(true); + } else { + setShowCheckout(true); + } + }; + + return ( + + ({ + // Sets the minimum width a column can be before wrapping + '--grid-min-size': isCompact ? '11.75rem' : '20rem', + // Set a max amount of columns before they start wrapping to new rows. + '--grid-max-columns': 'infinity', + // Set the default gap, use `--grid-gap-y` to override the row gap + '--grid-gap': t.space.$4, + // Derived from the maximum column size based on the grid configuration + '--max-column-width': '100% / var(--grid-max-columns, infinity) - var(--grid-gap)', + // Derived from `--max-column-width` and ensures it respects the minimum size and maximum width constraints + '--column-width': 'max(var(--max-column-width), min(var(--grid-min-size, 10rem), 100%))', + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(var(--column-width), 1fr))', + gap: `var(--grid-gap-y, var(--grid-gap, ${t.space.$4})) var(--grid-gap, ${t.space.$4})`, + alignItems: 'start', + width: '100%', + minWidth: '0', + })} + > + {plans?.map(plan => ( + + ))} + + <__experimental_CheckoutContext.Provider + value={{ + componentName: 'Checkout', + mode, + isShowingBlade: showCheckout, + handleCloseBlade: () => setShowCheckout(false), + }} + > + {/*TODO: Used by InvisibleRootBox, can we simplify? */} +
    + <__experimental_Checkout + planPeriod={planPeriod} + planId={selectedPlan?.id} + /> +
    + + setShowPlanDetail(false)} + plan={selectedPlan} + /> +
    + ); +}; diff --git a/packages/clerk-js/src/ui/components/PricingTable/index.ts b/packages/clerk-js/src/ui/components/PricingTable/index.ts new file mode 100644 index 00000000000..295e4209bf5 --- /dev/null +++ b/packages/clerk-js/src/ui/components/PricingTable/index.ts @@ -0,0 +1 @@ +export * from './PricingTable'; diff --git a/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx new file mode 100644 index 00000000000..a803a3f5430 --- /dev/null +++ b/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx @@ -0,0 +1,65 @@ +import { __experimental_PricingTableContext } from '../../contexts'; +import { Col, descriptors, localizationKeys } from '../../customizables'; +import { + Card, + Header, + Tab, + TabPanel, + TabPanels, + Tabs, + TabsList, + useCardState, + withCardStateProvider, +} from '../../elements'; +import { __experimental_PricingTable } from '../PricingTable'; + +export const BillingPage = withCardStateProvider(() => { + const card = useCardState(); + + return ( + ({ gap: t.space.$8, color: t.colors.$colorText })} + > + + + + + + {card.error} + + + ({ gap: t.space.$6 })}> + + + + + + + <__experimental_PricingTableContext.Provider value={{ componentName: 'PricingTable', mode: 'modal' }}> + <__experimental_PricingTable /> + + + Invoices + Payment Sources + + + + + ); +}); diff --git a/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx b/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx index 15723090da1..7e86c7b76f3 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx @@ -1,14 +1,18 @@ import { CustomPageContentContainer } from '../../common/CustomPageContentContainer'; import { USER_PROFILE_NAVBAR_ROUTE_ID } from '../../constants'; -import { useUserProfileContext } from '../../contexts'; +import { useOptions, useUserProfileContext } from '../../contexts'; import { Route, Switch } from '../../router'; import { AccountPage } from './AccountPage'; +import { BillingPage } from './BillingPage'; import { SecurityPage } from './SecurityPage'; export const UserProfileRoutes = () => { const { pages } = useUserProfileContext(); + const { experimental } = useOptions(); + const isAccountPageRoot = pages.routes[0].id === USER_PROFILE_NAVBAR_ROUTE_ID.ACCOUNT; const isSecurityPageRoot = pages.routes[0].id === USER_PROFILE_NAVBAR_ROUTE_ID.SECURITY; + const isBillingPageRoot = pages.routes[0].id === USER_PROFILE_NAVBAR_ROUTE_ID.BILLING; const customPageRoutesWithContents = pages.contents?.map((customPage, index) => { const shouldFirstCustomItemBeOnRoot = !isAccountPageRoot && !isSecurityPageRoot && index === 0; @@ -44,6 +48,15 @@ export const UserProfileRoutes = () => { + {experimental?.commerce && ( + + + + + + + + )} ); diff --git a/packages/clerk-js/src/ui/constants.ts b/packages/clerk-js/src/ui/constants.ts index f897754e188..1752d530dfc 100644 --- a/packages/clerk-js/src/ui/constants.ts +++ b/packages/clerk-js/src/ui/constants.ts @@ -1,6 +1,7 @@ export const USER_PROFILE_NAVBAR_ROUTE_ID = { ACCOUNT: 'account', SECURITY: 'security', + BILLING: 'billing', }; export const ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID = { diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx index e4f3f7b171b..4f8996744f2 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -1,8 +1,15 @@ -import type { UserButtonProps, WaitlistProps } from '@clerk/types'; +import type { + __experimental_CheckoutProps, + __experimental_PricingTableProps, + UserButtonProps, + WaitlistProps, +} from '@clerk/types'; import type { ReactNode } from 'react'; import type { AvailableComponentName, AvailableComponentProps } from '../types'; import { + __experimental_CheckoutContext, + __experimental_PricingTableContext, CreateOrganizationContext, GoogleOneTapContext, OrganizationListContext, @@ -78,6 +85,20 @@ export function ComponentContextProvider({ {children} ); + case 'PricingTable': + return ( + <__experimental_PricingTableContext.Provider + value={{ componentName, ...(props as __experimental_PricingTableProps) }} + > + {children} + + ); + case 'Checkout': + return ( + <__experimental_CheckoutContext.Provider value={{ componentName, ...(props as __experimental_CheckoutProps) }}> + {children} + + ); default: throw new Error(`Unknown component context: ${componentName}`); } diff --git a/packages/clerk-js/src/ui/contexts/components/Checkout.ts b/packages/clerk-js/src/ui/contexts/components/Checkout.ts new file mode 100644 index 00000000000..58809fa6688 --- /dev/null +++ b/packages/clerk-js/src/ui/contexts/components/Checkout.ts @@ -0,0 +1,20 @@ +import { createContext, useContext } 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); + + if (!context || context.componentName !== 'Checkout') { + throw new Error('Clerk: useCheckoutContext called outside Checkout.'); + } + + const { componentName, ...ctx } = context; + + return { + ...ctx, + componentName, + }; +}; diff --git a/packages/clerk-js/src/ui/contexts/components/PricingTable.ts b/packages/clerk-js/src/ui/contexts/components/PricingTable.ts new file mode 100644 index 00000000000..862a0921c2d --- /dev/null +++ b/packages/clerk-js/src/ui/contexts/components/PricingTable.ts @@ -0,0 +1,20 @@ +import { createContext, useContext } from 'react'; + +import type { __experimental_PricingTableCtx } from '../../types'; + +export const __experimental_PricingTableContext = createContext<__experimental_PricingTableCtx | null>(null); + +export const usePricingTableContext = () => { + const context = useContext(__experimental_PricingTableContext); + + if (!context || context.componentName !== 'PricingTable') { + throw new Error('Clerk: usePricingTableContext called outside PricingTable.'); + } + + const { componentName, ...ctx } = context; + + return { + ...ctx, + componentName, + }; +}; diff --git a/packages/clerk-js/src/ui/contexts/components/index.ts b/packages/clerk-js/src/ui/contexts/components/index.ts index c1f6b5474c3..099c80876e2 100644 --- a/packages/clerk-js/src/ui/contexts/components/index.ts +++ b/packages/clerk-js/src/ui/contexts/components/index.ts @@ -10,3 +10,5 @@ export * from './OrganizationProfile'; export * from './CreateOrganization'; export * from './GoogleOneTap'; export * from './Waitlist'; +export * from './PricingTable'; +export * from './Checkout'; diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts index 3ebc8557552..a109052d87a 100644 --- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts +++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts @@ -26,6 +26,23 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'footerItem', 'popoverBox', + 'disclosureRoot', + 'disclosureTrigger', + 'disclosureContentRoot', + 'disclosureContentInner', + 'disclosureContent', + + 'lineItemsRoot', + 'lineItemsDivider', + 'lineItemsGroup', + 'lineItemsTitle', + 'lineItemsTitleDescription', + 'lineItemsDescription', + 'lineItemsDescriptionInner', + 'lineItemsDescriptionSuffix', + 'lineItemsDescriptionPrefix', + 'lineItemsDescriptionText', + 'actionCard', 'logoBox', @@ -196,6 +213,31 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'accountSwitcherActionButtonIconBox', 'accountSwitcherActionButtonIcon', + 'planCard', + 'planCardDefault', + 'planCardCompact', + 'planCardHeader', + 'planCardTitle', + 'planCardDescription', + 'planCardAvatarContainer', + 'planCardAvatar', + 'planCardFeatures', + 'planCardFeaturesList', + 'planCardFeaturesListItem', + 'planCardAction', + 'planCardPeriodToggle', + 'planCardFeeContainer', + 'planCardFee', + 'planCardFeePeriod', + 'planCardFeePeriodNotice', + 'planCardFeePeriodNoticeInner', + 'planCardFeePeriodNoticeLabel', + + 'pricingTable', + + 'segmentedControlRoot', + 'segmentedControlButton', + 'alert', 'alertIcon', 'alertText', diff --git a/packages/clerk-js/src/ui/customizables/index.ts b/packages/clerk-js/src/ui/customizables/index.ts index 9398fc998d5..acd5ad0fc55 100644 --- a/packages/clerk-js/src/ui/customizables/index.ts +++ b/packages/clerk-js/src/ui/customizables/index.ts @@ -61,4 +61,8 @@ export const Tr = makeCustomizable(sanitizeDomProps(Primitives.Tr)); export const Th = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.Th))); export const Td = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.Td))); +export const Dl = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.Dl))); +export const Dd = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.Dd))); +export const Dt = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.Dt))); + export const Span = makeCustomizable(makeLocalizable(sanitizeDomProps(Primitives.Span))); diff --git a/packages/clerk-js/src/ui/elements/Disclosure.tsx b/packages/clerk-js/src/ui/elements/Disclosure.tsx new file mode 100644 index 00000000000..b667ce39f8b --- /dev/null +++ b/packages/clerk-js/src/ui/elements/Disclosure.tsx @@ -0,0 +1,189 @@ +import * as React from 'react'; + +import { Box, descriptors, Icon, SimpleButton, useAppearance } from '../customizables'; +import { usePrefersReducedMotion } from '../hooks'; +import { ChevronDown } from '../icons'; +import type { ThemableCssProp } from '../styledSystem'; +import { common } from '../styledSystem'; +import { colors } from '../utils'; + +/* ------------------------------------------------------------------------------------------------- + * Disclosure Context + * -----------------------------------------------------------------------------------------------*/ + +interface DisclosureContextValue { + isOpen: boolean; + onToggle: () => void; + id: string; +} + +const DisclosureContext = React.createContext(undefined); + +/* ------------------------------------------------------------------------------------------------- + * Disclosure.Root + * -----------------------------------------------------------------------------------------------*/ + +interface RootProps { + children: React.ReactNode; + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; +} + +const Root = React.forwardRef( + ({ children, defaultOpen = false, open: controlledOpen, onOpenChange }, ref) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen); + const isControlled = controlledOpen !== undefined; + const isOpen = isControlled ? controlledOpen : uncontrolledOpen; + const id = React.useId(); + + const onToggle = React.useCallback(() => { + if (isControlled) { + onOpenChange?.(!isOpen); + } else { + setUncontrolledOpen(!isOpen); + } + }, [isControlled, isOpen, onOpenChange]); + + return ( + + ({ + width: '100%', + borderRadius: t.radii.$lg, + boxShadow: `inset 0 0 0 1px ${t.colors.$neutralAlpha100}`, + backgroundColor: t.colors.$colorBackground, + isolation: 'isolate', + })} + > + {children} + + + ); + }, +); + +Root.displayName = 'Disclosure.Root'; + +/* ------------------------------------------------------------------------------------------------- + * Disclosure.Trigger + * -----------------------------------------------------------------------------------------------*/ + +interface TriggerProps { + children: React.ReactNode; +} + +const Trigger = React.forwardRef(({ children }, ref) => { + const context = React.useContext(DisclosureContext); + if (!context) { + throw new Error('Disclosure.Trigger must be used within Disclosure.Root'); + } + + return ( + ({ + width: '100%', + fontSize: t.fontSizes.$md, + justifyContent: 'space-between', + padding: t.sizes.$3, + color: t.colors.$colorText, + borderRadius: t.radii.$lg, + zIndex: 2, + })} + > + {children} + + + ); +}); + +Trigger.displayName = 'Disclosure.Trigger'; + +/* ------------------------------------------------------------------------------------------------- + * Disclosure.Content + * -----------------------------------------------------------------------------------------------*/ + +interface ContentProps { + children: React.ReactNode; +} + +const Content = React.forwardRef(({ children }, ref) => { + const context = React.useContext(DisclosureContext); + if (!context) { + throw new Error('Disclosure.Content must be used within Disclosure.Root'); + } + const { isOpen, id } = context; + const prefersReducedMotion = usePrefersReducedMotion(); + const { animations: appearanceAnimations } = useAppearance().parsedLayout; + const animation: ThemableCssProp = t => ({ + transition: + appearanceAnimations && !prefersReducedMotion + ? `grid-template-rows ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}` + : 'none', + }); + + return ( + ({ + display: 'grid', + gridTemplateRows: isOpen ? '1fr' : '0fr', + zIndex: 1, + }), + animation, + ]} + // @ts-ignore - Needed until React 19 support + inert={isOpen ? undefined : 'true'} + > + + ({ + padding: t.space.$3, + borderRadius: t.radii.$lg, + borderWidth: t.borderWidths.$normal, + borderStyle: t.borderStyles.$solid, + borderColor: t.colors.$neutralAlpha100, + background: common.mergedColorsBackground( + colors.setAlpha(t.colors.$colorBackground, 1), + t.colors.$neutralAlpha50, + ), + })} + > + {children} + + + + ); +}); + +Content.displayName = 'Disclosure.Content'; + +export const Disclosure = { + Root, + Trigger, + Content, +}; diff --git a/packages/clerk-js/src/ui/elements/LineItems.tsx b/packages/clerk-js/src/ui/elements/LineItems.tsx new file mode 100644 index 00000000000..9f6e6263ec5 --- /dev/null +++ b/packages/clerk-js/src/ui/elements/LineItems.tsx @@ -0,0 +1,198 @@ +import * as React from 'react'; + +import { Box, Dd, descriptors, Dl, Dt, Span } from '../customizables'; +import { common } from '../styledSystem'; + +/* ------------------------------------------------------------------------------------------------- + * LineItems.Root + * -----------------------------------------------------------------------------------------------*/ + +interface RootProps { + children: React.ReactNode; +} + +function Root({ children }: RootProps) { + return ( +
    ({ + display: 'grid', + gridRowGap: t.space.$2, + })} + > + {children} +
    + ); +} + +/* ------------------------------------------------------------------------------------------------- + * LineItems.Group + * -----------------------------------------------------------------------------------------------*/ + +type GroupVariant = 'primary' | 'secondary' | 'tertiary'; + +interface GroupContextValue { + variant: GroupVariant; +} + +const GroupContext = React.createContext(undefined); + +interface GroupProps { + children: React.ReactNode; + /** + * @default `false` + */ + borderTop?: boolean; + variant?: GroupVariant; +} + +function Group({ children, borderTop = false, variant = 'primary' }: GroupProps) { + return ( + + ({ + display: 'grid', + gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', + ...(borderTop + ? { + borderTopWidth: t.borderWidths.$normal, + borderTopStyle: t.borderStyles.$solid, + borderTopColor: t.colors.$neutralAlpha100, + paddingTop: t.space.$2, + } + : {}), + })} + > + {children} + + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * LineItems.Title + * -----------------------------------------------------------------------------------------------*/ + +interface TitleProps { + children: React.ReactNode; + description?: React.ReactNode; +} + +function Title({ children, description }: TitleProps) { + const context = React.useContext(GroupContext); + if (!context) { + throw new Error('LineItems.Title must be used within LineItems.Group'); + } + const { variant } = context; + const textVariant = variant === 'primary' ? 'subtitle' : 'caption'; + return ( +
    ({ + display: 'grid', + color: variant === 'primary' ? t.colors.$colorText : t.colors.$colorTextSecondary, + marginTop: variant !== 'primary' ? t.space.$0x25 : undefined, + ...common.textVariants(t)[textVariant], + })} + > + {children} + {description ? ( + ({ + fontSize: t.fontSizes.$sm, + color: t.colors.$colorTextSecondary, + })} + > + {description} + + ) : null} +
    + ); +} + +/* ------------------------------------------------------------------------------------------------- + * LineItems.Description + * -----------------------------------------------------------------------------------------------*/ + +interface DescriptionProps { + children: React.ReactNode; + /** + * Render a piece of text before the description text. + */ + prefix?: React.ReactNode; + /** + * Render a note below the description text. + */ + suffix?: React.ReactNode; +} + +function Description({ children, prefix, suffix }: DescriptionProps) { + const context = React.useContext(GroupContext); + if (!context) { + throw new Error('LineItems.Description must be used within LineItems.Group'); + } + const { variant } = context; + return ( +
    ({ + display: 'grid', + justifyContent: 'end', + color: variant === 'tertiary' ? t.colors.$colorTextSecondary : t.colors.$colorText, + })} + > + ({ + display: 'inline-flex', + justifyContent: 'flex-end', + alignItems: 'center', + gap: t.space.$1, + })} + > + {prefix ? ( + ({ + color: t.colors.$colorTextSecondary, + ...common.textVariants(t).caption, + })} + > + {prefix} + + ) : null} + ({ + ...common.textVariants(t).body, + })} + > + {children} + + + {suffix ? ( + ({ + color: t.colors.$colorTextSecondary, + ...common.textVariants(t).caption, + })} + > + {suffix} + + ) : null} +
    + ); +} + +export const LineItems = { + Root, + Group, + Title, + Description, +}; diff --git a/packages/clerk-js/src/ui/elements/SegmentedControl.tsx b/packages/clerk-js/src/ui/elements/SegmentedControl.tsx new file mode 100644 index 00000000000..c3f9d2d03ae --- /dev/null +++ b/packages/clerk-js/src/ui/elements/SegmentedControl.tsx @@ -0,0 +1,135 @@ +import { Composite, CompositeItem } from '@floating-ui/react'; +import React, { createContext, useContext, useState } from 'react'; + +import { descriptors, Flex, SimpleButton } from '../customizables'; + +/* ------------------------------------------------------------------------------------------------- + * SegmentedControl Context + * -----------------------------------------------------------------------------------------------*/ + +type SegmentedControlContextType = { + currentValue: string | undefined; + onValueChange: (value: string) => void; +}; + +const SegmentedControlContext = createContext(null); + +function useSegmentedControlContext() { + const context = useContext(SegmentedControlContext); + if (!context) { + throw new Error('SegmentedControl.Button must be used within SegmentedControl.Root'); + } + return context; +} + +/* ------------------------------------------------------------------------------------------------- + * SegmentedControl.Root + * -----------------------------------------------------------------------------------------------*/ + +interface RootProps { + children: React.ReactNode; + 'aria-label': string; + value?: string; + defaultValue?: string; + onChange?: (value: string) => void; +} + +const Root = React.forwardRef( + ({ children, value: controlledValue, defaultValue, onChange, 'aria-label': ariaLabel }, ref) => { + const [internalValue, setInternalValue] = useState(defaultValue); + const isControlled = controlledValue !== undefined; + const currentValue = isControlled ? controlledValue : internalValue; + + const handleValueChange = (newValue: string) => { + if (!isControlled) { + setInternalValue(newValue); + } + onChange?.(newValue); + }; + + return ( + + ({ + backgroundColor: t.colors.$neutralAlpha50, + borderRadius: t.radii.$md, + borderWidth: t.borderWidths.$normal, + borderStyle: t.borderStyles.$solid, + borderColor: t.colors.$neutralAlpha100, + isolation: 'isolate', + })} + /> + } + > + {children} + + + ); + }, +); + +Root.displayName = 'SegmentedControl.Root'; + +/* ------------------------------------------------------------------------------------------------- + * SegmentedControl.Button + * -----------------------------------------------------------------------------------------------*/ + +interface ButtonProps { + children: React.ReactNode; + value: string; +} + +const Button = React.forwardRef(({ children, value }, ref) => { + const { currentValue, onValueChange } = useSegmentedControlContext(); + const isSelected = value === currentValue; + + return ( + { + return ( + onValueChange(value)} + isActive={isSelected} + sx={t => ({ + position: 'relative', + padding: `${t.space.$1} ${t.space.$2x5}`, + backgroundColor: isSelected ? t.colors.$colorBackground : 'transparent', + color: isSelected ? t.colors.$colorText : t.colors.$colorTextSecondary, + fontSize: t.fontSizes.$xs, + minHeight: t.sizes.$6, + boxShadow: isSelected ? t.shadows.$segmentedControl : 'none', + borderRadius: `calc(${t.radii.$md} - ${t.borderWidths.$normal})`, + zIndex: 1, + ':focus-visible': { + zIndex: 2, + }, + })} + > + {children} + + ); + }} + /> + ); +}); + +Button.displayName = 'SegmentedControl.Button'; + +export const SegmentedControl = { + Root, + Button, +}; diff --git a/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx b/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx index a6937d6387a..92523042b4f 100644 --- a/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx +++ b/packages/clerk-js/src/ui/elements/contexts/FlowMetadataContext.tsx @@ -14,7 +14,8 @@ type FlowMetadata = { | 'organizationList' | 'oneTap' | 'blankCaptcha' - | 'waitlist'; + | 'waitlist' + | 'checkout'; part?: | 'start' | 'emailCode' diff --git a/packages/clerk-js/src/ui/elements/index.ts b/packages/clerk-js/src/ui/elements/index.ts index 5c01080716f..6810df6281d 100644 --- a/packages/clerk-js/src/ui/elements/index.ts +++ b/packages/clerk-js/src/ui/elements/index.ts @@ -13,6 +13,7 @@ export * from './CodeControl'; export * from './contexts'; export * from './DevModeNotice'; export * from './Divider'; +export * from './Disclosure'; export * from './ErrorCard'; export * from './Form'; export * from './FormattedPhoneNumber'; @@ -28,6 +29,7 @@ export * from './InformationBox'; export * from './InputWithIcon'; export * from './InvisibleRootBox'; export * from './LegalConsentCheckbox'; +export * from './LineItems'; export * from './LoadingCard'; export * from './Menu'; export * from './Modal'; @@ -56,3 +58,4 @@ export * from './UserPreview'; export * from './VerificationCodeCard'; export * from './VerificationLinkCard'; export * from './withAvatarShimmer'; +export * from './SegmentedControl'; diff --git a/packages/clerk-js/src/ui/foundations/shadows.ts b/packages/clerk-js/src/ui/foundations/shadows.ts index a9138d2c87f..3c1327b06b0 100644 --- a/packages/clerk-js/src/ui/foundations/shadows.ts +++ b/packages/clerk-js/src/ui/foundations/shadows.ts @@ -11,4 +11,5 @@ export const shadows = Object.freeze({ focusRing: '0px 0px 0px 4px {{color}}', badge: '0px 2px 0px -1px rgba(0, 0, 0, 0.04)', tableBodyShadow: '0px 0px 1px 0px rgba(0, 0, 0, 0.08), 0px 1px 2px 0px rgba(0, 0, 0, 0.12)', + segmentedControl: '0px 1px 2px 0px rgba(0, 0, 0, 0.08)', } as const); diff --git a/packages/clerk-js/src/ui/hooks/index.ts b/packages/clerk-js/src/ui/hooks/index.ts index 58996c9c9fa..1e0b862ae4e 100644 --- a/packages/clerk-js/src/ui/hooks/index.ts +++ b/packages/clerk-js/src/ui/hooks/index.ts @@ -18,3 +18,4 @@ export * from './useDebounce'; export * from './useClerkModalStateParams'; export * from './useNavigateToFlowStart'; export * from './useEnterpriseSSOLink'; +export * from './useCheckout'; diff --git a/packages/clerk-js/src/ui/hooks/useCheckout.ts b/packages/clerk-js/src/ui/hooks/useCheckout.ts new file mode 100644 index 00000000000..e0c218e2850 --- /dev/null +++ b/packages/clerk-js/src/ui/hooks/useCheckout.ts @@ -0,0 +1,32 @@ +import { useClerk } from '@clerk/shared/react'; +import type { __experimental_CheckoutProps, __experimental_CommerceCheckoutResource } from '@clerk/types'; +import { useCallback, useEffect, useState } from 'react'; + +import { useFetch } from './useFetch'; + +export const useCheckout = (props: __experimental_CheckoutProps) => { + const { planId, planPeriod } = props; + const { __experimental_commerce } = useClerk(); + const [currentCheckout, setCurrentCheckout] = useState<__experimental_CommerceCheckoutResource | null>(null); + + const { data: initialCheckout, isLoading } = useFetch(__experimental_commerce?.__experimental_billing.startCheckout, { + planId, + planPeriod, + }); + + useEffect(() => { + if (initialCheckout && !currentCheckout) { + setCurrentCheckout(initialCheckout); + } + }, [initialCheckout, currentCheckout]); + + const updateCheckout = useCallback((newCheckout: __experimental_CommerceCheckoutResource) => { + setCurrentCheckout(newCheckout); + }, []); + + return { + checkout: currentCheckout || initialCheckout, + updateCheckout, + isLoading, + }; +}; diff --git a/packages/clerk-js/src/ui/icons/credit-card.svg b/packages/clerk-js/src/ui/icons/credit-card.svg new file mode 100644 index 00000000000..ba479bb56b8 --- /dev/null +++ b/packages/clerk-js/src/ui/icons/credit-card.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/clerk-js/src/ui/icons/index.ts b/packages/clerk-js/src/ui/icons/index.ts index 861f4795e66..4be5e4ecc05 100644 --- a/packages/clerk-js/src/ui/icons/index.ts +++ b/packages/clerk-js/src/ui/icons/index.ts @@ -6,25 +6,35 @@ */ export { default as Add } from './add.svg'; export { default as ArrowLeftIcon } from './arrow-left.svg'; -export { default as ArrowRightIcon } from './arrow-right.svg'; export { default as ArrowRightButtonIcon } from './arrow-right-button.svg'; +export { default as ArrowRightIcon } from './arrow-right.svg'; +export { default as ArrowUpDown } from './arrow-up-down.svg'; export { default as AuthApp } from './auth-app.svg'; export { default as Billing } from './billing.svg'; +export { default as Block } from './block.svg'; export { default as Caret } from './caret.svg'; +export { default as CaretLeft } from './caret-left.svg'; +export { default as CaretRight } from './caret-right.svg'; export { default as ChatAltIcon } from './chat-alt.svg'; +export { default as Check } from './check.svg'; export { default as CheckCircle } from './check-circle.svg'; +export { default as CheckmarkFilled } from './checkmark-filled.svg'; export { default as ChevronDown } from './chevron-down.svg'; export { default as Clipboard } from './clipboard.svg'; export { default as Close } from './close.svg'; export { default as CogFilled } from './cog-filled.svg'; +export { default as Copy } from './copy.svg'; +export { default as CreditCard } from './credit-card.svg'; export { default as DeviceLaptop } from './device-laptop.svg'; export { default as DeviceMobile } from './device-mobile.svg'; export { default as DotCircle } from './dot-circle-horizontal.svg'; +export { default as Download } from './download.svg'; export { default as Email } from './email.svg'; export { default as ExclamationCircle } from './exclamation-circle.svg'; export { default as ExclamationTriangle } from './exclamation-triangle.svg'; -export { default as EyeSlash } from './eye-slash.svg'; export { default as Eye } from './eye.svg'; +export { default as EyeSlash } from './eye-slash.svg'; +export { default as Fingerprint } from './fingerprint.svg'; export { default as Folder } from './folder.svg'; export { default as InformationCircle } from './information-circle.svg'; export { default as LinkIcon } from './link.svg'; @@ -32,17 +42,21 @@ export { default as LockClosedIcon } from './lock-closed.svg'; export { default as LogoMark } from './logo-mark-new.svg'; export { default as MagnifyingGlass } from './magnifying-glass.svg'; export { default as Menu } from './menu.svg'; +export { default as Minus } from './minus.svg'; export { default as Mobile } from './mobile-small.svg'; -export { default as PencilEdit } from './pencil-edit.svg'; +export { default as Organization } from './organization.svg'; export { default as Pencil } from './pencil.svg'; +export { default as PencilEdit } from './pencil-edit.svg'; export { default as Plus } from './plus.svg'; +export { default as Print } from './print.svg'; export { default as QuestionMark } from './question-mark.svg'; export { default as RequestAuthIcon } from './request-auth.svg'; export { default as Selector } from './selector.svg'; -export { default as SignOutDouble } from './signout-double.svg'; export { default as SignOut } from './signout.svg'; -export { default as SwitchArrows } from './switch-arrows.svg'; +export { default as SignOutDouble } from './signout-double.svg'; +export { default as SpinnerJumbo } from './spinner-jumbo.svg'; export { default as SwitchArrowRight } from './switch-arrow-right.svg'; +export { default as SwitchArrows } from './switch-arrows.svg'; export { default as ThreeDots } from './threeDots.svg'; export { default as TickShield } from './tick-shield.svg'; export { default as Times } from './times.svg'; @@ -50,16 +64,4 @@ export { default as Trash } from './trash.svg'; export { default as Upload } from './upload.svg'; export { default as User } from './user.svg'; export { default as UserAdd } from './userAdd.svg'; -export { default as Check } from './check.svg'; -export { default as ArrowUpDown } from './arrow-up-down.svg'; -export { default as CheckmarkFilled } from './checkmark-filled.svg'; -export { default as Copy } from './copy.svg'; -export { default as Download } from './download.svg'; -export { default as Print } from './print.svg'; -export { default as CaretLeft } from './caret-left.svg'; -export { default as CaretRight } from './caret-right.svg'; -export { default as Organization } from './organization.svg'; export { default as Users } from './users.svg'; -export { default as Fingerprint } from './fingerprint.svg'; -export { default as Block } from './block.svg'; -export { default as SpinnerJumbo } from './spinner-jumbo.svg'; diff --git a/packages/clerk-js/src/ui/icons/minus.svg b/packages/clerk-js/src/ui/icons/minus.svg new file mode 100644 index 00000000000..9c5b7a14ffa --- /dev/null +++ b/packages/clerk-js/src/ui/icons/minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/clerk-js/src/ui/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts index 2669b2772d5..51788d64910 100644 --- a/packages/clerk-js/src/ui/lazyModules/components.ts +++ b/packages/clerk-js/src/ui/lazyModules/components.ts @@ -17,6 +17,8 @@ const componentImportPaths = { UserVerification: () => import(/* webpackChunkName: "userverification" */ './../components/UserVerification'), Waitlist: () => import(/* webpackChunkName: "waitlist" */ './../components/Waitlist'), KeylessPrompt: () => import(/* webpackChunkName: "keylessPrompt" */ '../components/KeylessPrompt'), + PricingTable: () => import(/* webpackChunkName: "pricingTable" */ '../components/PricingTable'), + Checkout: () => import(/* webpackChunkName: "checkout" */ '../components/Checkout'), } as const; export const SignIn = lazy(() => componentImportPaths.SignIn().then(module => ({ default: module.SignIn }))); @@ -88,6 +90,10 @@ export const KeylessPrompt = lazy(() => componentImportPaths.KeylessPrompt().then(module => ({ default: module.KeylessPrompt })), ); +export const PricingTable = lazy(() => + componentImportPaths.PricingTable().then(module => ({ default: module.__experimental_PricingTable })), +); + export const preloadComponent = async (component: unknown) => { return componentImportPaths[component as keyof typeof componentImportPaths]?.(); }; @@ -112,6 +118,7 @@ export const ClerkComponents = { Waitlist, WaitlistModal, BlankCaptchaModal, + PricingTable, }; export type ClerkComponentName = keyof typeof ClerkComponents; diff --git a/packages/clerk-js/src/ui/polishedAppearance.ts b/packages/clerk-js/src/ui/polishedAppearance.ts index 623aeec6e80..5de90e4f628 100644 --- a/packages/clerk-js/src/ui/polishedAppearance.ts +++ b/packages/clerk-js/src/ui/polishedAppearance.ts @@ -145,6 +145,16 @@ export const polishedAppearance: Appearance = { ].toString(), }, }, + '&[data-variant="bordered"]': { + borderWidth: 0, + boxShadow: BUTTON_OUTLINE_SHADOW(theme.colors.$neutralAlpha100), + '&:focus': { + boxShadow: [ + BUTTON_OUTLINE_SHADOW(theme.colors.$neutralAlpha100), + theme.shadows.$focusRing.replace('{{color}}', theme.colors.$neutralAlpha200), + ].toString(), + }, + }, }, badge: { borderWidth: 0, @@ -218,6 +228,13 @@ export const polishedAppearance: Appearance = { card: { ...cardContentStyles(theme), }, + planCardDefault: { + borderWidth: 0, + boxShadow: `${theme.shadows.$cardBoxShadow}, ${BORDER_SHADOW_LENGTH} ${theme.colors.$neutralAlpha100}`, + }, + planCardCompact: { + boxShadow: 'none', + }, scrollBox: { ...cardContentStyles(theme), }, diff --git a/packages/clerk-js/src/ui/primitives/Badge.tsx b/packages/clerk-js/src/ui/primitives/Badge.tsx index aff8307c89c..f0636b485d4 100644 --- a/packages/clerk-js/src/ui/primitives/Badge.tsx +++ b/packages/clerk-js/src/ui/primitives/Badge.tsx @@ -26,6 +26,11 @@ const { applyVariants, filterProps } = createVariants(theme => ({ [vars.bg]: theme.colors.$neutralAlpha50, [vars.borderColor]: theme.colors.$neutralAlpha150, }, + secondary: { + [vars.accent]: theme.colors.$colorTextOnPrimaryBackground, + [vars.bg]: theme.colors.$primary500, + [vars.borderColor]: theme.colors.$primary500, + }, danger: { [vars.accent]: theme.colors.$danger500, [vars.bg]: theme.colors.$dangerAlpha50, diff --git a/packages/clerk-js/src/ui/primitives/Button.tsx b/packages/clerk-js/src/ui/primitives/Button.tsx index 4707f5640b0..b012d4d3e66 100644 --- a/packages/clerk-js/src/ui/primitives/Button.tsx +++ b/packages/clerk-js/src/ui/primitives/Button.tsx @@ -11,7 +11,7 @@ import { Flex } from './Flex'; const vars = createCssVariables('accent', 'accentHover', 'accentContrast', 'alpha', 'border'); const { applyVariants, filterProps } = createVariants( - (theme, props: OwnProps & { colorScheme?: 'primary' | 'neutral' | 'danger' }) => { + (theme, props: OwnProps & { colorScheme?: 'primary' | 'secondary' | 'neutral' | 'danger' }) => { return { base: { margin: 0, @@ -54,6 +54,13 @@ const { applyVariants, filterProps } = createVariants( [vars.accentContrast]: theme.colors.$colorTextOnPrimaryBackground, [vars.alpha]: theme.colors.$neutralAlpha50, }, + secondary: { + [vars.accent]: theme.colors.$colorBackground, + [vars.accentHover]: `color-mix(in srgb, ${vars.accent}, ${theme.colors.$neutralAlpha50})`, + [vars.border]: theme.colors.$primary500, + [vars.accentContrast]: theme.colors.$colorText, + [vars.alpha]: theme.colors.$neutralAlpha50, + }, neutral: { [vars.accent]: theme.colors.$neutralAlpha600, [vars.accentHover]: theme.colors.$neutralAlpha700, @@ -95,6 +102,15 @@ const { applyVariants, filterProps } = createVariants( '&:focus': props.hoverAsFocus ? { backgroundColor: theme.colors.$neutralAlpha50 } : undefined, boxShadow: theme.shadows.$outlineButtonShadow, }, + bordered: { + borderWidth: theme.borderWidths.$normal, + borderStyle: theme.borderStyles.$solid, + borderColor: theme.colors.$neutralAlpha100, + color: vars.accentContrast, + backgroundColor: vars.accent, + '&:hover': { backgroundColor: vars.accentHover }, + '&:focus': props.hoverAsFocus ? { backgroundColor: vars.accentHover } : undefined, + }, ghost: { color: vars.accent, '&:hover': { backgroundColor: vars.alpha, color: vars.accentHover }, diff --git a/packages/clerk-js/src/ui/primitives/Dd.tsx b/packages/clerk-js/src/ui/primitives/Dd.tsx new file mode 100644 index 00000000000..fa6d06e8f4a --- /dev/null +++ b/packages/clerk-js/src/ui/primitives/Dd.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export const Dd = React.forwardRef< + HTMLDListElement, + React.DetailedHTMLProps, HTMLDListElement> +>((props, ref) => { + return ( +
    + ); +}); diff --git a/packages/clerk-js/src/ui/primitives/Dl.tsx b/packages/clerk-js/src/ui/primitives/Dl.tsx new file mode 100644 index 00000000000..be90dbf1b79 --- /dev/null +++ b/packages/clerk-js/src/ui/primitives/Dl.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export const Dl = React.forwardRef< + HTMLDListElement, + React.DetailedHTMLProps, HTMLDListElement> +>((props, ref) => { + return ( +
    + ); +}); diff --git a/packages/clerk-js/src/ui/primitives/Dt.tsx b/packages/clerk-js/src/ui/primitives/Dt.tsx new file mode 100644 index 00000000000..ebe0ec18fde --- /dev/null +++ b/packages/clerk-js/src/ui/primitives/Dt.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export const Dt = React.forwardRef< + HTMLDListElement, + React.DetailedHTMLProps, HTMLDListElement> +>((props, ref) => { + return ( +
    + ); +}); diff --git a/packages/clerk-js/src/ui/primitives/Heading.tsx b/packages/clerk-js/src/ui/primitives/Heading.tsx index b8c8fa6a4dc..bf9b8c74fb1 100644 --- a/packages/clerk-js/src/ui/primitives/Heading.tsx +++ b/packages/clerk-js/src/ui/primitives/Heading.tsx @@ -17,7 +17,7 @@ const { applyVariants, filterProps } = createVariants(theme => ({ })); // @ts-ignore -export type HeadingProps = PrimitiveProps<'div'> & StyleVariants & { as?: 'h1' }; +export type HeadingProps = PrimitiveProps<'div'> & StyleVariants & { as?: 'h1' | 'h2' }; export const Heading = (props: HeadingProps) => { const { as: As = 'h1', ...rest } = props; diff --git a/packages/clerk-js/src/ui/primitives/index.ts b/packages/clerk-js/src/ui/primitives/index.ts index 4e0697a3357..94622d54c9c 100644 --- a/packages/clerk-js/src/ui/primitives/index.ts +++ b/packages/clerk-js/src/ui/primitives/index.ts @@ -3,6 +3,9 @@ export * from './AlertIcon'; export * from './Badge'; export * from './Box'; export * from './Button'; +export * from './Dl'; +export * from './Dd'; +export * from './Dt'; export * from './Flex'; export * from './Form'; export * from './FormErrorText'; diff --git a/packages/clerk-js/src/ui/styledSystem/animations.ts b/packages/clerk-js/src/ui/styledSystem/animations.ts index 7327659a795..1975ca4f3f8 100644 --- a/packages/clerk-js/src/ui/styledSystem/animations.ts +++ b/packages/clerk-js/src/ui/styledSystem/animations.ts @@ -33,6 +33,11 @@ const fadeIn = keyframes` 100% { opacity: 1; } `; +const fadeOut = keyframes` + 0% { opacity: 1; } + 100% { opacity: 0; } +`; + const inAnimation = keyframes` 0% { opacity: 0; @@ -123,11 +128,24 @@ const navbarSlideIn = keyframes` 100% {opacity: 1; transform: translateX(0);} `; +const drawerSlideIn = keyframes` + 0% { opacity: 0; translate: 100% 0; } + 10% { opacity: 1; } + 100% { opacity: 1; translate: 0; } +`; + +const drawerSlideOut = keyframes` + 0% { opacity: 1; translate: 0; } + 90% { opacity: 1; } + 100% { opacity: 0; translate: 100% 0; } +`; + export const animations = { spinning, dropdownSlideInScaleAndFade, modalSlideAndFade, fadeIn, + fadeOut, textInSmall, textInBig, blockBigIn, @@ -137,4 +155,6 @@ export const animations = { inDelayAnimation, outAnimation, notificationAnimation, + drawerSlideIn, + drawerSlideOut, }; diff --git a/packages/clerk-js/src/ui/styledSystem/types.ts b/packages/clerk-js/src/ui/styledSystem/types.ts index d07889b7d23..27c7984f1ab 100644 --- a/packages/clerk-js/src/ui/styledSystem/types.ts +++ b/packages/clerk-js/src/ui/styledSystem/types.ts @@ -31,6 +31,9 @@ type ElementProps = { th: React.JSX.IntrinsicElements['th']; tr: React.JSX.IntrinsicElements['tr']; td: React.JSX.IntrinsicElements['td']; + dl: React.JSX.IntrinsicElements['dl']; + dt: React.JSX.IntrinsicElements['dt']; + dd: React.JSX.IntrinsicElements['dd']; }; /** diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index e90ebfff249..743b154c78b 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -1,4 +1,6 @@ import type { + __experimental_CheckoutProps, + __experimental_PricingTableProps, __internal_UserVerificationProps, CreateOrganizationProps, GoogleOneTapProps, @@ -36,6 +38,8 @@ export type AvailableComponentProps = | CreateOrganizationProps | OrganizationListProps | WaitlistProps + | __experimental_PricingTableProps + | __experimental_CheckoutProps | __internal_UserVerificationProps; type ComponentMode = 'modal' | 'mounted'; @@ -96,6 +100,18 @@ export type WaitlistCtx = WaitlistProps & { mode?: ComponentMode; }; +export type __experimental_PricingTableCtx = __experimental_PricingTableProps & { + componentName: 'PricingTable'; + mode?: ComponentMode; +}; + +export type __experimental_CheckoutCtx = __experimental_CheckoutProps & { + componentName: 'Checkout'; + mode?: ComponentMode; + isShowingBlade?: boolean; + handleCloseBlade?: () => void; +}; + export type AvailableComponentCtx = | SignInCtx | SignUpCtx @@ -107,6 +123,8 @@ export type AvailableComponentCtx = | OrganizationSwitcherCtx | OrganizationListCtx | GoogleOneTapCtx - | WaitlistCtx; + | WaitlistCtx + | __experimental_PricingTableCtx + | __experimental_CheckoutCtx; export type AvailableComponentName = AvailableComponentCtx['componentName']; diff --git a/packages/clerk-js/src/ui/utils/createCustomPages.tsx b/packages/clerk-js/src/ui/utils/createCustomPages.tsx index ecfb19baacf..7f5c5b6c4ce 100644 --- a/packages/clerk-js/src/ui/utils/createCustomPages.tsx +++ b/packages/clerk-js/src/ui/utils/createCustomPages.tsx @@ -3,7 +3,7 @@ import type { CustomPage, LoadedClerk } from '@clerk/types'; import { isValidUrl } from '../../utils'; import { ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID, USER_PROFILE_NAVBAR_ROUTE_ID } from '../constants'; import type { NavbarRoute } from '../elements'; -import { Organization, TickShield, User, Users } from '../icons'; +import { CreditCard, Organization, TickShield, User, Users } from '../icons'; import { localizationKeys } from '../localization'; import { ExternalElementMounter } from './ExternalElementMounter'; import { isDevelopmentSDK } from './runtimeEnvironment'; @@ -43,7 +43,7 @@ type GetDefaultRoutesReturnType = { type CreateCustomPagesParams = { customPages: CustomPage[]; - getDefaultRoutes: () => GetDefaultRoutesReturnType; + getDefaultRoutes: ({ commerce }: { commerce: boolean }) => GetDefaultRoutesReturnType; setFirstPathToRoot: (routes: NavbarRoute[]) => NavbarRoute[]; excludedPathsFromDuplicateWarning: string[]; }; @@ -76,7 +76,9 @@ const createCustomPages = ( { customPages, getDefaultRoutes, setFirstPathToRoot, excludedPathsFromDuplicateWarning }: CreateCustomPagesParams, clerk: LoadedClerk, ) => { - const { INITIAL_ROUTES, pageToRootNavbarRouteMap, validReorderItemLabels } = getDefaultRoutes(); + const { INITIAL_ROUTES, pageToRootNavbarRouteMap, validReorderItemLabels } = getDefaultRoutes({ + commerce: clerk.sdkMetadata?.environment === 'test' ? false : clerk.__internal_getOption('experimental')?.commerce, + }); if (isDevelopmentSDK(clerk)) { checkForDuplicateUsageOfReorderingItems(customPages, validReorderItemLabels); @@ -228,7 +230,7 @@ const assertExternalLinkAsRoot = (routes: NavbarRoute[]) => { } }; -const getUserProfileDefaultRoutes = (): GetDefaultRoutesReturnType => { +const getUserProfileDefaultRoutes = ({ commerce }: { commerce: boolean }): GetDefaultRoutesReturnType => { const INITIAL_ROUTES: NavbarRoute[] = [ { name: localizationKeys('userProfile.navbar.account'), @@ -243,6 +245,14 @@ const getUserProfileDefaultRoutes = (): GetDefaultRoutesReturnType => { path: 'security', }, ]; + if (commerce) { + INITIAL_ROUTES.push({ + name: localizationKeys('userProfile.navbar.billing'), + id: USER_PROFILE_NAVBAR_ROUTE_ID.BILLING, + icon: CreditCard, + path: 'billing', + }); + } const pageToRootNavbarRouteMap: Record = { profile: INITIAL_ROUTES.find(r => r.id === USER_PROFILE_NAVBAR_ROUTE_ID.ACCOUNT) as NavbarRoute, @@ -260,7 +270,7 @@ const getUserProfileDefaultRoutes = (): GetDefaultRoutesReturnType => { return { INITIAL_ROUTES, pageToRootNavbarRouteMap, validReorderItemLabels }; }; -const getOrganizationProfileDefaultRoutes = (): GetDefaultRoutesReturnType => { +const getOrganizationProfileDefaultRoutes = ({ commerce }: { commerce: boolean }): GetDefaultRoutesReturnType => { const INITIAL_ROUTES: NavbarRoute[] = [ { name: localizationKeys('organizationProfile.navbar.general'), @@ -275,6 +285,17 @@ const getOrganizationProfileDefaultRoutes = (): GetDefaultRoutesReturnType => { path: 'organization-members', }, ]; + if (commerce) { + // TODO(@COMMERCE) Uncomment when OrgProfile is ready + // INITIAL_ROUTES.push( + // { + // name: localizationKeys('userProfile.navbar.billing'), + // id: USER_PROFILE_NAVBAR_ROUTE_ID.BILLING, + // icon: CreditCard, + // path: 'billing', + // }, + // ); + } const pageToRootNavbarRouteMap: Record = { 'invite-members': INITIAL_ROUTES.find(r => r.id === ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.MEMBERS) as NavbarRoute, diff --git a/packages/clerk-js/src/utils/commerce.ts b/packages/clerk-js/src/utils/commerce.ts new file mode 100644 index 00000000000..ed365850607 --- /dev/null +++ b/packages/clerk-js/src/utils/commerce.ts @@ -0,0 +1,24 @@ +import type { + __experimental_CommerceMoney, + __experimental_CommerceMoneyJSON, + __experimental_CommerceTotals, + __experimental_CommerceTotalsJSON, +} from '@clerk/types'; + +export const commerceMoneyFromJSON = (data: __experimental_CommerceMoneyJSON): __experimental_CommerceMoney => { + return { + amount: data.amount, + amountFormatted: data.amount_formatted, + currency: data.currency, + currencySymbol: data.currency_symbol, + }; +}; + +export const commerceTotalsFromJSON = (data: __experimental_CommerceTotalsJSON): __experimental_CommerceTotals => { + return { + grandTotal: commerceMoneyFromJSON(data.grand_total), + subtotal: commerceMoneyFromJSON(data.subtotal), + taxTotal: commerceMoneyFromJSON(data.tax_total), + totalDueNow: data.total_due_now ? commerceMoneyFromJSON(data.total_due_now) : undefined, + }; +}; diff --git a/packages/clerk-js/src/utils/index.ts b/packages/clerk-js/src/utils/index.ts index 892275e1cdf..b3999d638b1 100644 --- a/packages/clerk-js/src/utils/index.ts +++ b/packages/clerk-js/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './beforeUnloadTracker'; +export * from './commerce'; export * from './completeSignUpFlow'; export * from './componentGuards'; export * from './dynamicParamParser'; diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index 960268e42df..1f3d9de296d 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const arSA: LocalizationResource = { locale: 'ar-SA', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'الرجوع', + badge__currentPlan: undefined, badge__default: 'الأفتراضي', badge__otherImpersonatorDevice: 'جهاز منتحل آخر', badge__primary: 'الرئيسي', @@ -646,6 +655,14 @@ export const arSA: LocalizationResource = { action__signOutAll: 'تسجيل الخروج من جميع الحسابات', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'تم النسخ', actionLabel__copy: 'نسخ الكل', @@ -765,6 +782,7 @@ export const arSA: LocalizationResource = { mobileButton__menu: 'القائمة', navbar: { account: 'الملف الشخصي', + billing: undefined, description: 'إدارة معلومات ملفك الشخصي.', security: 'حماية', title: 'الملف الشخصي', diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index ae6d40bd144..a87119c2a12 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const beBY: LocalizationResource = { locale: 'be-BY', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Назад', + badge__currentPlan: undefined, badge__default: 'Па-змаўчанні', badge__otherImpersonatorDevice: 'Іншая прылада', badge__primary: 'Асноўная', @@ -654,6 +663,14 @@ export const beBY: LocalizationResource = { action__signOutAll: 'Выйсці з усіх уліковых запісаў', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Скапіравана!', actionLabel__copy: 'Скапіраваць усё', @@ -779,6 +796,7 @@ export const beBY: LocalizationResource = { mobileButton__menu: 'Меню', navbar: { account: 'Профіль', + billing: undefined, description: 'Кіруйце інфармацыяй аб вашым уліковым запісе.', security: 'Бяспека', title: 'Уліковы запіс', diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts index 5398c6b714f..a51d8bd342e 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const bgBG: LocalizationResource = { locale: 'bg-BG', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Назад', + badge__currentPlan: undefined, badge__default: 'По подразбиране', badge__otherImpersonatorDevice: 'Друго устройство за имитация', badge__primary: 'Основен', @@ -645,6 +654,14 @@ export const bgBG: LocalizationResource = { action__signOutAll: 'Изход от всички акаунти', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Копирано!', actionLabel__copy: 'Копиране на всички', @@ -769,6 +786,7 @@ export const bgBG: LocalizationResource = { mobileButton__menu: 'Меню', navbar: { account: 'Profile', + billing: undefined, description: 'Управлявайте информацията в профила си.', security: 'Security', title: 'Профил', diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts index 9dd02c5386e..ab9c1702628 100644 --- a/packages/localizations/src/ca-ES.ts +++ b/packages/localizations/src/ca-ES.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const caES: LocalizationResource = { locale: 'ca-ES', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Enrere', + badge__currentPlan: undefined, badge__default: 'Per defecte', badge__otherImpersonatorDevice: 'Un altre dispositiu impostor', badge__primary: 'Principal', @@ -649,6 +658,14 @@ export const caES: LocalizationResource = { action__signOutAll: 'Tanca sessió de tots els comptes', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Copiat!', actionLabel__copy: 'Copia tot', @@ -775,6 +792,7 @@ export const caES: LocalizationResource = { mobileButton__menu: 'Menú', navbar: { account: 'Perfil', + billing: undefined, description: 'Gestiona la informació del teu compte.', security: 'Seguretat', title: 'Compte', diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index 097a0ed404a..d5133f14838 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const csCZ: LocalizationResource = { locale: 'cs-CZ', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Zpět', + badge__currentPlan: undefined, badge__default: 'Výchozí', badge__otherImpersonatorDevice: 'Jiné zařízení představitele', badge__primary: 'Hlavní', @@ -645,6 +654,14 @@ export const csCZ: LocalizationResource = { action__signOutAll: 'Odhlásit se ze všech účtů', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Zkopírováno!', actionLabel__copy: 'Zkopírovat vše', @@ -768,6 +785,7 @@ export const csCZ: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profil', + billing: undefined, description: 'Spravujte své údaje.', security: 'Zabezpečení', title: 'Účet', diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index 2ab2e285620..da043ca0cc7 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const daDK: LocalizationResource = { locale: 'da-DK', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Tilbage', + badge__currentPlan: undefined, badge__default: 'Standard', badge__otherImpersonatorDevice: 'Anden enhed som efterligner', badge__primary: 'Primær', @@ -646,6 +655,14 @@ export const daDK: LocalizationResource = { action__signOutAll: 'Log ud af alle konti', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Kopieret!', actionLabel__copy: 'Kopier alle', @@ -769,6 +786,7 @@ export const daDK: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profil', + billing: undefined, description: 'Administrer dine kontooplysninger.', security: 'Sikkerhed', title: 'Konto', diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index 3fca460fdab..37af74effe2 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const deDE: LocalizationResource = { locale: 'de-DE', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Zurück', + badge__currentPlan: undefined, badge__default: 'Standard', badge__otherImpersonatorDevice: 'Anderes Imitationsgerät', badge__primary: 'Primär', @@ -659,6 +668,14 @@ export const deDE: LocalizationResource = { action__signOutAll: 'Melden Sie sich von allen Konten ab', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Kopiert!', actionLabel__copy: 'Kopiere alle', @@ -784,6 +801,7 @@ export const deDE: LocalizationResource = { mobileButton__menu: 'Menü', navbar: { account: 'Profil', + billing: undefined, description: 'Verwalten Sie Ihre Kontoinformationen.', security: 'Sicherheit', title: 'Benutzerkonto', diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts index a8569e6c66f..c550a26d250 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const elGR: LocalizationResource = { locale: 'el-GR', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Πίσω', + badge__currentPlan: undefined, badge__default: 'Προεπιλογή', badge__otherImpersonatorDevice: 'Άλλη συσκευή υποδυόμενου', badge__primary: 'Κύριο', @@ -654,6 +663,14 @@ export const elGR: LocalizationResource = { action__signOutAll: 'Αποσύνδεση από όλους τους λογαριασμούς', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Αντιγράφηκαν!', actionLabel__copy: 'Αντιγραφή όλων', @@ -779,6 +796,7 @@ export const elGR: LocalizationResource = { mobileButton__menu: 'Μενού', navbar: { account: 'Προφίλ', + billing: undefined, description: 'Διαχειριστείτε τις πληροφορίες του λογαριασμού σας.', security: 'Ασφάλεια', title: 'Λογαριασμός', diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts index 1211dc1d9fe..09439b3b46f 100644 --- a/packages/localizations/src/en-GB.ts +++ b/packages/localizations/src/en-GB.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const enGB: LocalizationResource = { locale: 'en-GB', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Back', + badge__currentPlan: undefined, badge__default: 'Default', badge__otherImpersonatorDevice: 'Other impersonator device', badge__primary: 'Primary', @@ -656,6 +665,14 @@ export const enGB: LocalizationResource = { action__signOutAll: 'Sign out of all accounts', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Copied!', actionLabel__copy: 'Copy all', @@ -779,6 +796,7 @@ export const enGB: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profile', + billing: undefined, description: 'Manage your account info.', security: 'Security', title: 'Account', diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index bfc84567c80..c3e438b53c4 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -2,7 +2,16 @@ import type { LocalizationResource } from '@clerk/types'; export const enUS: LocalizationResource = { locale: 'en-US', + __experimental_commerce: { + billedAnnually: 'Billed annually', + free: 'Free', + getStarted: 'Get started', + manageMembership: 'Manage membership', + month: 'Month', + switchPlan: 'Switch to this plan', + }, backButton: 'Back', + badge__currentPlan: 'Current Plan', badge__default: 'Default', badge__otherImpersonatorDevice: 'Other impersonator device', badge__primary: 'Primary', @@ -651,6 +660,14 @@ export const enUS: LocalizationResource = { action__signOutAll: 'Sign out of all accounts', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: 'Invoices', + headerTitle__paymentSources: 'Payment Sources', + headerTitle__plans: 'Plans', + }, + title: 'Billing & Payments', + }, backupCodePage: { actionLabel__copied: 'Copied!', actionLabel__copy: 'Copy all', @@ -774,6 +791,7 @@ export const enUS: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profile', + billing: 'Billing', description: 'Manage your account info.', security: 'Security', title: 'Account', diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index 70762ff565b..1187ed619d4 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const esES: LocalizationResource = { locale: 'es-ES', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Atrás', + badge__currentPlan: undefined, badge__default: 'Por defecto', badge__otherImpersonatorDevice: 'Otro dispositivo de imitación', badge__primary: 'Primario', @@ -651,6 +660,14 @@ export const esES: LocalizationResource = { action__signOutAll: 'Salir de todas las cuentas', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: '¡Copiado!', actionLabel__copy: 'Copiar todo', @@ -776,6 +793,7 @@ export const esES: LocalizationResource = { mobileButton__menu: 'Menú', navbar: { account: 'Perfil', + billing: undefined, description: 'Gestiona la información de tu cuenta.', security: 'Seguridad', title: 'Cuenta', diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts index d641268aa43..b2ead6ca8a0 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const esMX: LocalizationResource = { locale: 'es-MX', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Atrás', + badge__currentPlan: undefined, badge__default: 'Por defecto', badge__otherImpersonatorDevice: 'Otro dispositivo de imitación', badge__primary: 'Primario', @@ -652,6 +661,14 @@ export const esMX: LocalizationResource = { action__signOutAll: 'Salir de todas las cuentas', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Copiado!', actionLabel__copy: 'Copiar todo', @@ -777,6 +794,7 @@ export const esMX: LocalizationResource = { mobileButton__menu: 'Menú', navbar: { account: 'Perfil', + billing: undefined, description: 'Administra tu información de cuenta.', security: 'Seguridad', title: 'Cuenta', diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts index 7cfec16bcc0..fa1a48990a9 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const fiFI: LocalizationResource = { locale: 'fi-FI', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Takaisin', + badge__currentPlan: undefined, badge__default: 'Oletus', badge__otherImpersonatorDevice: 'Toinen jäljitelty laite', badge__primary: 'Ensisijainen', @@ -649,6 +658,14 @@ export const fiFI: LocalizationResource = { action__signOutAll: 'Kirjaudu ulos kaikista tileistä', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Kopioitu', actionLabel__copy: 'Kopioi', @@ -773,6 +790,7 @@ export const fiFI: LocalizationResource = { mobileButton__menu: 'Valikko', navbar: { account: 'Profiili', + billing: undefined, description: 'Hallitse tilisi tietoja', security: 'Turvallisuus', title: 'Tili', diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index d77e1c106b8..4174cb9099d 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const frFR: LocalizationResource = { locale: 'fr-FR', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Retour', + badge__currentPlan: undefined, badge__default: 'Défaut', badge__otherImpersonatorDevice: "Autre dispositif d'imitation", badge__primary: 'Principal', @@ -654,6 +663,14 @@ export const frFR: LocalizationResource = { action__signOutAll: 'Se déconnecter de tous les comptes', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Copié !', actionLabel__copy: 'Copier tous les codes', @@ -778,6 +795,7 @@ export const frFR: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Compte', + billing: undefined, description: 'Gérer votre compte.', security: 'Sécurité', title: 'Profil', diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index 5803d66308f..7cb3edc7c4f 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const heIL: LocalizationResource = { locale: 'he-IL', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'חזור', + badge__currentPlan: undefined, badge__default: 'ברירת מחדל', badge__otherImpersonatorDevice: 'מכשיר מחקה אחר', badge__primary: 'ראשי', @@ -639,6 +648,14 @@ export const heIL: LocalizationResource = { action__signOutAll: 'התנתק מכל החשבונות', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'הועתק!', actionLabel__copy: 'העתק הכל', @@ -754,6 +771,7 @@ export const heIL: LocalizationResource = { mobileButton__menu: 'תפריט', navbar: { account: 'פרופיל', + billing: undefined, description: 'נהל את פרטי החשבון שלך.', security: 'אבטחה', title: 'חשבון', diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts index d5e6644d240..0a1650ac7c4 100644 --- a/packages/localizations/src/hr-HR.ts +++ b/packages/localizations/src/hr-HR.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const hrHR: LocalizationResource = { locale: 'hr-HR', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Natrag', + badge__currentPlan: undefined, badge__default: 'Zadano', badge__otherImpersonatorDevice: 'Drugi uređaj za oponašanje', badge__primary: 'Primarno', @@ -654,6 +663,14 @@ export const hrHR: LocalizationResource = { action__signOutAll: 'Odjavi se sa svih računa', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Kopirano!', actionLabel__copy: 'Kopiraj sve', @@ -776,6 +793,7 @@ export const hrHR: LocalizationResource = { mobileButton__menu: 'Izbornik', navbar: { account: 'Profil', + billing: undefined, description: 'Upravljajte informacijama vašeg računa.', security: 'Sigurnost', title: 'Račun', diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts index 86d923e25ae..0894c133b1f 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const huHU: LocalizationResource = { locale: 'hu-HU', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Vissza', + badge__currentPlan: undefined, badge__default: 'Alapértelmezett', badge__otherImpersonatorDevice: 'Másik megszemélyesítő eszköz', badge__primary: 'Elsődleges', @@ -650,6 +659,14 @@ export const huHU: LocalizationResource = { action__signOutAll: 'Kijelentkezés minden fiókból', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Kimásolva!', actionLabel__copy: 'Az összes kimásolása', @@ -774,6 +791,7 @@ export const huHU: LocalizationResource = { mobileButton__menu: 'Menü', navbar: { account: 'Profil', + billing: undefined, description: 'Fiók információk kezelése', security: 'Biztonság', title: 'Fiók', diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts index 6188819a111..c712f05dfab 100644 --- a/packages/localizations/src/id-ID.ts +++ b/packages/localizations/src/id-ID.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const idID: LocalizationResource = { locale: 'id-ID', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Kembali', + badge__currentPlan: undefined, badge__default: 'Default', badge__otherImpersonatorDevice: 'Perangkat impersonator lain', badge__primary: 'Utama', @@ -658,6 +667,14 @@ export const idID: LocalizationResource = { action__signOutAll: 'Keluar dari semua akun', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Disalin!', actionLabel__copy: 'Salin semua', @@ -773,6 +790,7 @@ export const idID: LocalizationResource = { mobileButton__menu: undefined, navbar: { account: 'Profil', + billing: undefined, description: 'Kelola info akun Anda.', security: 'Keamanan', title: 'Akun', diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts index 2dd6e8ef58c..751152fa768 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const isIS: LocalizationResource = { locale: 'is-IS', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Til baka', + badge__currentPlan: undefined, badge__default: 'Sjálfgefið', badge__otherImpersonatorDevice: 'Önnur tæki sem herma eftir', badge__primary: 'Aðal', @@ -652,6 +661,14 @@ export const isIS: LocalizationResource = { action__signOutAll: 'Skrá út af öllum reikningum', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Afritað!', actionLabel__copy: 'Afrita allt', @@ -776,6 +793,7 @@ export const isIS: LocalizationResource = { mobileButton__menu: 'Valmynd', navbar: { account: 'Prófíll', + billing: undefined, description: 'Stjórna reikningsupplýsingum þínum.', security: 'Öryggi', title: 'Reikningur', diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index 212f5df5331..149f100bbc4 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const itIT: LocalizationResource = { locale: 'it-IT', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Indietro', + badge__currentPlan: undefined, badge__default: 'Predefinito', badge__otherImpersonatorDevice: 'Altro dispositivo impersonato', badge__primary: 'Primario', @@ -649,6 +658,14 @@ export const itIT: LocalizationResource = { action__signOutAll: 'Disconnetti da tutti gli accounts', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Copiati!', actionLabel__copy: 'Copia tutti', @@ -773,6 +790,7 @@ export const itIT: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profilo', + billing: undefined, description: 'Gestisci il tuo account.', security: 'Sicurezza', title: 'Account', diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index d31906a1d82..590adf7a174 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const jaJP: LocalizationResource = { locale: 'ja-JP', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: '戻る', + badge__currentPlan: undefined, badge__default: 'デフォルト', badge__otherImpersonatorDevice: '他の模倣者デバイス', badge__primary: 'プライマリ', @@ -648,6 +657,14 @@ export const jaJP: LocalizationResource = { action__signOutAll: '全てのアカウントからサインアウト', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'コピー済み!', actionLabel__copy: 'すべてコピー', @@ -767,6 +784,7 @@ export const jaJP: LocalizationResource = { mobileButton__menu: 'メニュー', navbar: { account: 'プロファイル', + billing: undefined, description: 'アカウント情報管理', security: 'セキュリティ', title: 'アカウント', diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts index 6f2beb63858..24a066b0d7b 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const koKR: LocalizationResource = { locale: 'ko-KR', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: '돌아가기', + badge__currentPlan: undefined, badge__default: '기본값', badge__otherImpersonatorDevice: '기타 사칭 장치', badge__primary: '기본', @@ -642,6 +651,14 @@ export const koKR: LocalizationResource = { action__signOutAll: '모든 계정에서 로그아웃', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: '복사 완료!', actionLabel__copy: '전체 복사', @@ -759,6 +776,7 @@ export const koKR: LocalizationResource = { mobileButton__menu: '메뉴', navbar: { account: '프로필', + billing: undefined, description: '계정 정보를 관리하세요.', security: '보안', title: '계정', diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts index d396f25a1f7..bd02af748a0 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const mnMN: LocalizationResource = { locale: 'mn-MN', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Буцах', + badge__currentPlan: undefined, badge__default: 'Анхдагч', badge__otherImpersonatorDevice: 'Бусад дуурайгч төхөөрөмж', badge__primary: 'Үндсэн', @@ -649,6 +658,14 @@ export const mnMN: LocalizationResource = { action__signOutAll: 'Бүх бүртгэлээс гарна уу', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Хуулсан!', actionLabel__copy: 'Бүгдийг хуулах', @@ -772,6 +789,7 @@ export const mnMN: LocalizationResource = { mobileButton__menu: 'Цэс', navbar: { account: 'Профайл', + billing: undefined, description: 'Бүртгэлийнхээ мэдээллийг удирдана уу.', security: 'Аюулгүй байдал', title: 'Бүртгэл', diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts index ad32d798c29..758b4f04fc5 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const nbNO: LocalizationResource = { locale: 'nb-NO', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Tilbake', + badge__currentPlan: undefined, badge__default: 'Standard', badge__otherImpersonatorDevice: 'Annen imitators enhet', badge__primary: 'Primær', @@ -648,6 +657,14 @@ export const nbNO: LocalizationResource = { action__signOutAll: 'Logg ut av alle kontoer', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Kopiert!', actionLabel__copy: 'Kopier alle', @@ -772,6 +789,7 @@ export const nbNO: LocalizationResource = { mobileButton__menu: 'Meny', navbar: { account: 'Profil', + billing: undefined, description: 'Administrer kontoinformasjonen din.', security: 'Sikkerhet', title: 'Konto', diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts index b6f583e7f05..44d2a50887b 100644 --- a/packages/localizations/src/nl-BE.ts +++ b/packages/localizations/src/nl-BE.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const nlBE: LocalizationResource = { locale: 'nl-NL', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Terug', + badge__currentPlan: undefined, badge__default: 'Standaard', badge__otherImpersonatorDevice: 'Ander impersonatie apparaat', badge__primary: 'Primair', @@ -647,6 +656,14 @@ export const nlBE: LocalizationResource = { action__signOutAll: 'Uitloggen uit alle accounts', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Gekopieerd!', actionLabel__copy: 'Kopieer', @@ -769,6 +786,7 @@ export const nlBE: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profiel', + billing: undefined, description: 'Beheer je account informatie.', security: 'Beveiliging', title: 'Account', diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts index 3df92a6726e..9e55ae128e5 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const nlNL: LocalizationResource = { locale: 'nl-NL', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Terug', + badge__currentPlan: undefined, badge__default: 'Standaard', badge__otherImpersonatorDevice: 'Ander impersonatie apparaat', badge__primary: 'Primair', @@ -647,6 +656,14 @@ export const nlNL: LocalizationResource = { action__signOutAll: 'Uitloggen uit alle accounts', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Gekopieerd!', actionLabel__copy: 'Kopieer', @@ -769,6 +786,7 @@ export const nlNL: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profiel', + billing: undefined, description: 'Beheer je account informatie.', security: 'Beveiliging', title: 'Account', diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts index 8815fff0042..b534dbc28ec 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const plPL: LocalizationResource = { locale: 'pl-PL', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Powrót', + badge__currentPlan: undefined, badge__default: 'Domyślny', badge__otherImpersonatorDevice: 'Inne urządzenie osobiste', badge__primary: 'Podstawowy', @@ -658,6 +667,14 @@ export const plPL: LocalizationResource = { action__signOutAll: 'Wyloguj ze wszystkich kont', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Skopiowane!', actionLabel__copy: 'Skopiuj wszystkie', @@ -782,6 +799,7 @@ export const plPL: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profil', + billing: undefined, description: 'Zarządzaj danymi konta.', security: 'Bezpieczeństwo', title: 'Konto', diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index 9443f8d41f0..16ca3167536 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const ptBR: LocalizationResource = { locale: 'pt-BR', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Voltar', + badge__currentPlan: undefined, badge__default: 'Padrão', badge__otherImpersonatorDevice: 'Personificar outro dispositivo', badge__primary: 'Principal', @@ -653,6 +662,14 @@ export const ptBR: LocalizationResource = { action__signOutAll: 'Sair de todas as contas', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Copiado!', actionLabel__copy: 'Copiar tudo', @@ -778,6 +795,7 @@ export const ptBR: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Perfil', + billing: undefined, description: 'Gerencie seus dados de perfil.', security: 'Segurança', title: 'Conta', diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index 2a5ec05c043..35d82f9289f 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const ptPT: LocalizationResource = { locale: 'pt-PT', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Voltar', + badge__currentPlan: undefined, badge__default: 'Padrão', badge__otherImpersonatorDevice: 'Personificar outro dispositivo', badge__primary: 'Principal', @@ -647,6 +656,14 @@ export const ptPT: LocalizationResource = { action__signOutAll: 'Terminar sessão de todas as contas', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Copiado!', actionLabel__copy: 'Copiar tudo', @@ -770,6 +787,7 @@ export const ptPT: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profile', + billing: undefined, description: 'Manage your account info.', security: 'Security', title: 'Account', diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts index 7521823a00b..4b5e588fedc 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const roRO: LocalizationResource = { locale: 'ro-RO', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Înapoi', + badge__currentPlan: undefined, badge__default: 'Implicit', badge__otherImpersonatorDevice: 'Alt dispozitiv de imitație', badge__primary: 'Principală', @@ -652,6 +661,14 @@ export const roRO: LocalizationResource = { action__signOutAll: 'Deconectați-vă din toate conturile', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Copiat!', actionLabel__copy: 'Copiați toate', @@ -778,6 +795,7 @@ export const roRO: LocalizationResource = { mobileButton__menu: 'Meniu', navbar: { account: 'Profile', + billing: undefined, description: 'Manage your account info.', security: 'Security', title: 'Account', diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts index ed6d95ea656..08e40b33d92 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const ruRU: LocalizationResource = { locale: 'ru-RU', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Назад', + badge__currentPlan: undefined, badge__default: 'По-умолчанию', badge__otherImpersonatorDevice: 'Другое устройство', badge__primary: 'Основной', @@ -662,6 +671,14 @@ export const ruRU: LocalizationResource = { action__signOutAll: 'Выйти из всех учетных записей', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Скопировано!', actionLabel__copy: 'Копировать все', @@ -787,6 +804,7 @@ export const ruRU: LocalizationResource = { mobileButton__menu: 'Меню', navbar: { account: 'Профиль', + billing: undefined, description: 'Управление информацией вашей учетной записи.', security: 'Безопасность', title: 'Учетная запись', diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts index 77d28858b90..7419a24f805 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const skSK: LocalizationResource = { locale: 'sk-SK', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Späť', + badge__currentPlan: undefined, badge__default: 'Predvolené', badge__otherImpersonatorDevice: 'Iné zariadenie zástupcu', badge__primary: 'Hlavný', @@ -645,6 +654,14 @@ export const skSK: LocalizationResource = { action__signOutAll: 'Odhlásiť sa zo všetkých účtov', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Skopírované!', actionLabel__copy: 'Kopírovať všetko', @@ -768,6 +785,7 @@ export const skSK: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profile', + billing: undefined, description: 'Manage your account info.', security: 'Security', title: 'Account', diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts index 712d9cba0bc..9dea3263d91 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const srRS: LocalizationResource = { locale: 'sr-RS', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Nazad', + badge__currentPlan: undefined, badge__default: 'Podrazumevano', badge__otherImpersonatorDevice: 'Drugi uređaj koji se predstavlja', badge__primary: 'Primarni', @@ -648,6 +657,14 @@ export const srRS: LocalizationResource = { action__signOutAll: 'Odjavi se sa svih naloga', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Kopirano!', actionLabel__copy: 'Kopiraj sve', @@ -770,6 +787,7 @@ export const srRS: LocalizationResource = { mobileButton__menu: 'Meni', navbar: { account: 'Profil', + billing: undefined, description: 'Upravljaj informacijama svog naloga.', security: 'Sigurnost', title: 'Nalog', diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts index 7ff6642cdbb..beb13ba03b9 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const svSE: LocalizationResource = { locale: 'sv-SE', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Tillbaka', + badge__currentPlan: undefined, badge__default: 'Standard', badge__otherImpersonatorDevice: 'Annans imitatörenhet', badge__primary: 'Primär', @@ -651,6 +660,14 @@ export const svSE: LocalizationResource = { action__signOutAll: 'Logga ut från alla konton', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Kopierat!', actionLabel__copy: 'Kopiera alla', @@ -773,6 +790,7 @@ export const svSE: LocalizationResource = { mobileButton__menu: 'Meny', navbar: { account: 'Profil', + billing: undefined, description: 'Hantera din kontoinformation.', security: 'Säkerhet', title: 'Konto', diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts index 100da49b9a8..89ff6f330b9 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const thTH: LocalizationResource = { locale: 'th-TH', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'กลับ', + badge__currentPlan: undefined, badge__default: 'ค่าเริ่มต้น', badge__otherImpersonatorDevice: 'อุปกรณ์ปลอมตัวอื่น', badge__primary: 'หลัก', @@ -647,6 +656,14 @@ export const thTH: LocalizationResource = { action__signOutAll: 'ออกจากระบบทุกบัญชี', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'คัดลอกแล้ว!', actionLabel__copy: 'คัดลอกทั้งหมด', @@ -766,6 +783,7 @@ export const thTH: LocalizationResource = { mobileButton__menu: 'เมนู', navbar: { account: 'โปรไฟล์', + billing: undefined, description: 'จัดการข้อมูลบัญชีของคุณ', security: 'ความปลอดภัย', title: 'บัญชี', diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts index 70db0133ddc..3fffbb72f8c 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const trTR: LocalizationResource = { locale: 'tr-TR', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Geri', + badge__currentPlan: undefined, badge__default: 'Varsayılan', badge__otherImpersonatorDevice: 'Diğer taklit eden cihaz', badge__primary: 'Birincil', @@ -651,6 +660,14 @@ export const trTR: LocalizationResource = { action__signOutAll: 'Tüm hesaplardan çıkış yap', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Kopyalandı!', actionLabel__copy: 'Hepsini kopyala', @@ -774,6 +791,7 @@ export const trTR: LocalizationResource = { mobileButton__menu: 'Menü', navbar: { account: 'Profil', + billing: undefined, description: 'Hesap bilgilerinizi yönetin.', security: 'Güvenlik', title: 'Hesap', diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts index 93f116a383e..3bd1da35e2f 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const ukUA: LocalizationResource = { locale: 'uk-UA', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Назад', + badge__currentPlan: undefined, badge__default: 'За замовчуванням', badge__otherImpersonatorDevice: 'Інший пристрій-двійник', badge__primary: 'Основний', @@ -645,6 +654,14 @@ export const ukUA: LocalizationResource = { action__signOutAll: 'Вийти з усіх акаунтів', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Скопійовано!', actionLabel__copy: 'Копіювати все', @@ -769,6 +786,7 @@ export const ukUA: LocalizationResource = { mobileButton__menu: 'Меню', navbar: { account: 'Profile', + billing: undefined, description: 'Manage your account info.', security: 'Security', title: 'Account', diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts index c94af0bd5d0..c398ac8f224 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const viVN: LocalizationResource = { locale: 'vi-VN', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: 'Quay lại', + badge__currentPlan: undefined, badge__default: 'Mặc định', badge__otherImpersonatorDevice: 'Thiết bị nhân danh khác', badge__primary: 'Chính', @@ -646,6 +655,14 @@ export const viVN: LocalizationResource = { action__signOutAll: 'Đăng xuất khỏi tất cả các tài khoản', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: 'Đã sao chép!', actionLabel__copy: 'Sao chép tất cả', @@ -769,6 +786,7 @@ export const viVN: LocalizationResource = { mobileButton__menu: 'Menu', navbar: { account: 'Profile', + billing: undefined, description: 'Manage your account info.', security: 'Security', title: 'Account', diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts index 999b862ed16..fd86a6655de 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const zhCN: LocalizationResource = { locale: 'zh-CN', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: '返回', + badge__currentPlan: undefined, badge__default: '默认', badge__otherImpersonatorDevice: '其他模拟器设备', badge__primary: '主要', @@ -633,6 +642,14 @@ export const zhCN: LocalizationResource = { action__signOutAll: '退出所有账户', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: '已复制!', actionLabel__copy: '复制全部', @@ -745,6 +762,7 @@ export const zhCN: LocalizationResource = { mobileButton__menu: '菜单', navbar: { account: '账户', + billing: undefined, description: '管理您的账户。', security: '安全', title: '账户', diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts index ac72683fc47..1d68db9349a 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -14,7 +14,16 @@ import type { LocalizationResource } from '@clerk/types'; export const zhTW: LocalizationResource = { locale: 'zh-TW', + __experimental_commerce: { + billedAnnually: undefined, + free: undefined, + getStarted: undefined, + manageMembership: undefined, + month: undefined, + switchPlan: undefined, + }, backButton: '返回', + badge__currentPlan: undefined, badge__default: '默認', badge__otherImpersonatorDevice: '其他模擬器設備', badge__primary: '主要', @@ -641,6 +650,14 @@ export const zhTW: LocalizationResource = { action__signOutAll: '退出所有帳戶', }, userProfile: { + __experimental_billingPage: { + start: { + headerTitle__invoices: undefined, + headerTitle__paymentSources: undefined, + headerTitle__plans: undefined, + }, + title: undefined, + }, backupCodePage: { actionLabel__copied: '已複製!', actionLabel__copy: '複製全部', @@ -755,6 +772,7 @@ export const zhTW: LocalizationResource = { mobileButton__menu: '菜單', navbar: { account: 'Profile', + billing: undefined, description: 'Manage your account info.', security: 'Security', title: 'Account', diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 85202601e3d..455b9a72aa8 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -2,6 +2,8 @@ import { inBrowser } from '@clerk/shared/browser'; import { loadClerkJsScript } from '@clerk/shared/loadClerkJsScript'; import { handleValueOrFn } from '@clerk/shared/utils'; import type { + __experimental_CommerceNamespace, + __experimental_PricingTableProps, __internal_UserVerificationModalProps, __internal_UserVerificationProps, AuthenticateWithCoinbaseWalletParams, @@ -86,9 +88,14 @@ type WithVoidReturnFunctions = { type IsomorphicLoadedClerk = Without< WithVoidReturnFunctions, - 'client' | '__internal_addNavigationListener' | '__internal_getCachedResources' | '__internal_reloadInitialResources' + | 'client' + | '__internal_addNavigationListener' + | '__internal_getCachedResources' + | '__internal_reloadInitialResources' + | '__experimental_commerce' > & { client: ClientResource | undefined; + __experimental_commerce: __experimental_CommerceNamespace | undefined; }; export class IsomorphicClerk implements IsomorphicLoadedClerk { @@ -114,6 +121,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { private premountOrganizationListNodes = new Map(); private premountMethodCalls = new Map, MethodCallback>(); private premountWaitlistNodes = new Map(); + private premountPricingTableNodes = new Map(); // A separate Map of `addListener` method calls to handle multiple listeners. private premountAddListenerCalls = new Map< ListenerCallback, @@ -512,6 +520,10 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { clerkjs.mountWaitlist(node, props); }); + this.premountPricingTableNodes.forEach((props, node) => { + clerkjs.__experimental_mountPricingTable(node, props); + }); + this.#loaded = true; this.emitLoaded(); return this.clerkjs; @@ -579,6 +591,10 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } } + get __experimental_commerce(): __experimental_CommerceNamespace | undefined { + return this.clerkjs?.__experimental_commerce; + } + __unstable__setEnvironment(...args: any): void { if (this.clerkjs && '__unstable__setEnvironment' in this.clerkjs) { (this.clerkjs as any).__unstable__setEnvironment(args); @@ -750,6 +766,22 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; + __experimental_mountPricingTable = (node: HTMLDivElement, props?: __experimental_PricingTableProps) => { + if (this.clerkjs && this.#loaded) { + this.clerkjs.__experimental_mountPricingTable(node, props); + } else { + this.premountPricingTableNodes.set(node, props); + } + }; + + __experimental_unmountPricingTable = (node: HTMLDivElement) => { + if (this.clerkjs && this.#loaded) { + this.clerkjs.__experimental_unmountPricingTable(node); + } else { + this.premountPricingTableNodes.delete(node); + } + }; + mountSignUp = (node: HTMLDivElement, props?: SignUpProps) => { if (this.clerkjs && this.#loaded) { this.clerkjs.mountSignUp(node, props); diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index e086a68f729..83ba7ab1775 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -146,6 +146,23 @@ export type ElementsConfig = { actionCard: WithOptions; popoverBox: WithOptions; + disclosureRoot: WithOptions; + disclosureTrigger: WithOptions; + disclosureContentRoot: WithOptions; + disclosureContentInner: WithOptions; + disclosureContent: WithOptions; + + lineItemsRoot: WithOptions; + lineItemsDivider: WithOptions; + lineItemsGroup: WithOptions<'primary' | 'secondary' | 'tertiary'>; + lineItemsTitle: WithOptions<'primary' | 'secondary' | 'tertiary'>; + lineItemsTitleDescription: WithOptions; + lineItemsDescription: WithOptions<'primary' | 'secondary' | 'tertiary'>; + lineItemsDescriptionInner: WithOptions; + lineItemsDescriptionText: WithOptions; + lineItemsDescriptionSuffix: WithOptions; + lineItemsDescriptionPrefix: WithOptions; + logoBox: WithOptions; logoImage: WithOptions; @@ -235,6 +252,9 @@ export type ElementsConfig = { phoneInputBox: WithOptions; formInputGroup: WithOptions; + segmentedControlRoot: WithOptions; + segmentedControlButton: WithOptions; + avatarBox: WithOptions; avatarImage: WithOptions; avatarImageActions: WithOptions; @@ -320,6 +340,27 @@ export type ElementsConfig = { accountSwitcherActionButtonIconBox: WithOptions<'addAccount' | 'signOutAll'>; accountSwitcherActionButtonIcon: WithOptions<'addAccount' | 'signOutAll'>; + pricingTable: WithOptions; + planCard: WithOptions; + planCardDefault: WithOptions; + planCardCompact: WithOptions; + planCardHeader: WithOptions; + planCardAvatarContainer: WithOptions; + planCardAvatar: WithOptions; + planCardTitle: WithOptions; + planCardDescription: WithOptions; + planCardFeatures: WithOptions; + planCardFeaturesList: WithOptions; + planCardFeaturesListItem: WithOptions; + planCardAction: WithOptions; + planCardPeriodToggle: WithOptions; + planCardFeeContainer: WithOptions; + planCardFee: WithOptions; + planCardFeePeriod: WithOptions; + planCardFeePeriodNotice: WithOptions; + planCardFeePeriodNoticeInner: WithOptions; + planCardFeePeriodNoticeLabel: WithOptions; + alert: WithOptions; alertIcon: WithOptions; alertText: WithOptions; @@ -666,6 +707,8 @@ export type OrganizationProfileTheme = Theme; export type CreateOrganizationTheme = Theme; export type UserVerificationTheme = Theme; export type WaitlistTheme = Theme; +export type PricingTableTheme = Theme; +export type CheckoutTheme = Theme; export type Appearance = T & { /** @@ -712,4 +755,12 @@ export type Appearance = T & { * Theme overrides that only apply to the `` component */ waitlist?: T; + /** + * Theme overrides that only apply to the `` component + */ + pricingTable?: T; + /** + * Theme overrides that only apply to the `` component + */ + checkout?: T; }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index ffae59d7759..29834688505 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -1,9 +1,11 @@ import type { Appearance, + CheckoutTheme, CreateOrganizationTheme, OrganizationListTheme, OrganizationProfileTheme, OrganizationSwitcherTheme, + PricingTableTheme, SignInTheme, SignUpTheme, UserButtonTheme, @@ -12,6 +14,7 @@ import type { WaitlistTheme, } from './appearance'; import type { ClientResource } from './client'; +import type { __experimental_CommerceNamespace } from './commerce'; import type { CustomMenuItem } from './customMenuItems'; import type { CustomPage } from './customPages'; import type { InstanceType } from './instance'; @@ -146,6 +149,9 @@ export interface Clerk { /** Current User. */ user: UserResource | null | undefined; + /** Commerce Object */ + __experimental_commerce: __experimental_CommerceNamespace; + telemetry: TelemetryCollector | undefined; __internal_country?: string | null; @@ -384,6 +390,21 @@ export interface Clerk { */ unmountWaitlist: (targetNode: HTMLDivElement) => void; + /** + * Mounts a pricing table component at the target element. + * @param targetNode Target node to mount the PricingTable component. + * @param props configuration parameters. + */ + __experimental_mountPricingTable: (targetNode: HTMLDivElement, props?: __experimental_PricingTableProps) => void; + + /** + * Unmount a pricing table component from the target element. + * If there is no component mounted at the target node, results in a noop. + * + * @param targetNode Target node to unmount the PricingTable component from. + */ + __experimental_unmountPricingTable: (targetNode: HTMLDivElement) => void; + /** * Register a listener that triggers a callback each time important Clerk resources are changed. * Allows to hook up at different steps in the sign up, sign in processes. @@ -781,6 +802,7 @@ export type ClerkOptions = ClerkOptionsNavigation & * Clerk will rethrow network errors that occur while the user is offline. */ rethrowOfflineNetworkErrors: boolean; + commerce: boolean; }, Record >; @@ -1414,6 +1436,20 @@ export type WaitlistProps = { export type WaitlistModalProps = WaitlistProps; +export type __experimental_PricingTableProps = { + appearance?: PricingTableTheme; + ctaPosition?: 'top' | 'bottom'; + collapseFeatures?: boolean; + layout?: 'default' | 'matrix'; +}; + +export type __experimental_CheckoutProps = { + appearance?: CheckoutTheme; + planId?: string; + planPeriod?: string; + checkoutId?: string; +}; + export interface HandleEmailLinkVerificationParams { /** * Full URL or path to navigate after successful magic link verification diff --git a/packages/types/src/commerce.ts b/packages/types/src/commerce.ts new file mode 100644 index 00000000000..b523cd68691 --- /dev/null +++ b/packages/types/src/commerce.ts @@ -0,0 +1,124 @@ +import type { ClerkPaginatedResponse } from './pagination'; +import type { ClerkResource } from './resource'; + +export interface __experimental_CommerceNamespace { + __experimental_billing: __experimental_CommerceBillingNamespace; + getPaymentSources: () => Promise>; + addPaymentSource: ( + params: __experimental_AddPaymentSourceParams, + ) => Promise<__experimental_CommercePaymentSourceResource>; +} + +export interface __experimental_CommerceBillingNamespace { + getPlans: () => Promise<__experimental_CommercePlanResource[]>; + startCheckout: (params: __experimental_CreateCheckoutParams) => Promise<__experimental_CommerceCheckoutResource>; +} + +export interface __experimental_CommerceProductResource extends ClerkResource { + id: string; + slug: string | null; + currency: string; + isDefault: boolean; + plans: __experimental_CommercePlanResource[]; +} + +export interface __experimental_GetPlansParams { + subscriberType?: string; +} + +export interface __experimental_CommercePlanResource extends ClerkResource { + id: string; + name: string; + amount: number; + amountFormatted: string; + annualMonthlyAmount: number; + annualMonthlyAmountFormatted: string; + currencySymbol: string; + currency: string; + description: string; + isActiveForPayer: boolean; + isRecurring: boolean; + hasBaseFee: boolean; + payerType: string[]; + publiclyVisible: boolean; + slug: string; + avatarUrl: string; + features: __experimental_CommerceFeatureResource[]; +} + +export interface __experimental_CommerceFeatureResource extends ClerkResource { + id: string; + name: string; + description: string; + slug: string; + avatarUrl: string; +} + +export interface __experimental_AddPaymentSourceParams { + gateway: 'stripe' | 'paypal'; + paymentMethod: string; + paymentToken: string; +} + +export interface __experimental_CommercePaymentSourceResource extends ClerkResource { + id: string; + last4: string; + paymentMethod: string; + cardType: string; +} + +export interface __experimental_CommerceInvoiceResource extends ClerkResource { + id: string; + planId: string; + paymentSourceId: string; + totals: __experimental_CommerceTotals; + paymentDueOn: number; + paidOn: number; + status: string; +} + +export interface __experimental_CommerceSubscriptionResource extends ClerkResource { + id: string; + paymentSourceId: string; + plan: __experimental_CommercePlanResource; + planPeriod: string; + status: string; + cancel: () => Promise; +} + +export interface __experimental_CommerceMoney { + amount: number; + amountFormatted: string; + currency: string; + currencySymbol: string; +} + +export interface __experimental_CommerceTotals { + subtotal: __experimental_CommerceMoney; + grandTotal: __experimental_CommerceMoney; + taxTotal: __experimental_CommerceMoney; + totalDueNow?: __experimental_CommerceMoney; +} + +export interface __experimental_CreateCheckoutParams { + planId: string; + planPeriod: string; +} + +export interface __experimental_ConfirmCheckoutParams { + paymentSourceId?: string; +} + +export interface __experimental_CommerceCheckoutResource extends ClerkResource { + id: string; + externalClientSecret: string; + externalGatewayId: string; + invoice?: __experimental_CommerceInvoiceResource; + paymentSource?: __experimental_CommercePaymentSourceResource; + plan: __experimental_CommercePlanResource; + planPeriod: string; + status: string; + totals: __experimental_CommerceTotals; + subscription?: __experimental_CommerceSubscriptionResource; + confirm: (params?: __experimental_ConfirmCheckoutParams) => Promise<__experimental_CommerceCheckoutResource>; +} diff --git a/packages/types/src/commerceSettings.ts b/packages/types/src/commerceSettings.ts new file mode 100644 index 00000000000..60f9ad42c89 --- /dev/null +++ b/packages/types/src/commerceSettings.ts @@ -0,0 +1,13 @@ +import type { __experimental_CommerceSettingsJSONSnapshot } from 'snapshots'; + +import type { ClerkResourceJSON } from './json'; +import type { ClerkResource } from './resource'; + +export interface __experimental_CommerceSettingsJSON extends ClerkResourceJSON { + stripe_publishable_key: string; +} + +export interface __experimental_CommerceSettingsResource extends ClerkResource { + stripePublishableKey: string; + __internal_toSnapshot: () => __experimental_CommerceSettingsJSONSnapshot; +} diff --git a/packages/types/src/environment.ts b/packages/types/src/environment.ts index a6aff8ca83b..f1b142ed2c6 100644 --- a/packages/types/src/environment.ts +++ b/packages/types/src/environment.ts @@ -1,4 +1,5 @@ import type { AuthConfigResource } from './authConfig'; +import type { __experimental_CommerceSettingsResource } from './commerceSettings'; import type { DisplayConfigResource } from './displayConfig'; import type { OrganizationSettingsResource } from './organizationSettings'; import type { ClerkResource } from './resource'; @@ -10,6 +11,7 @@ export interface EnvironmentResource extends ClerkResource { organizationSettings: OrganizationSettingsResource; authConfig: AuthConfigResource; displayConfig: DisplayConfigResource; + __experimental_commerceSettings: __experimental_CommerceSettingsResource; isSingleSession: () => boolean; isProduction: () => boolean; isDevelopmentOrStaging: () => boolean; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ea41eaa12aa..648cc9d7a96 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -6,6 +6,8 @@ export * from './authConfig'; export * from './backupCode'; export * from './clerk'; export * from './client'; +export * from './commerce'; +export * from './commerceSettings'; export * from './deletedObject'; export * from './displayConfig'; export * from './emailAddress'; diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 46f32e48fe1..21f378f0cf3 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -2,6 +2,7 @@ * Currently representing API DTOs in their JSON form. */ +import type { __experimental_CommerceSettingsJSON } from './commerceSettings'; import type { DisplayConfigJSON } from './displayConfig'; import type { EnterpriseProtocol, EnterpriseProvider } from './enterpriseAccount'; import type { ActJWTClaim } from './jwt'; @@ -57,6 +58,7 @@ export interface ImageJSON { export interface EnvironmentJSON extends ClerkResourceJSON { auth_config: AuthConfigJSON; + commerce_settings: __experimental_CommerceSettingsJSON; display_config: DisplayConfigJSON; user_settings: UserSettingsJSON; organization_settings: OrganizationSettingsJSON; @@ -572,3 +574,98 @@ export interface WaitlistJSON extends ClerkResourceJSON { created_at: number; updated_at: number; } + +export interface __experimental_CommerceFeatureJSON extends ClerkResourceJSON { + object: 'commerce_feature'; + id: string; + name: string; + description: string; + slug: string; + avatar_url: string; +} + +export interface __experimental_CommercePlanJSON extends ClerkResourceJSON { + object: 'commerce_plan'; + id: string; + name: string; + amount: number; + amount_formatted: string; + annual_monthly_amount: number; + annual_monthly_amount_formatted: string; + currency_symbol: string; + currency: string; + description: string; + is_active_for_payer: boolean; + is_recurring: boolean; + has_base_fee: boolean; + payer_type: string[]; + publicly_visible: boolean; + slug: string; + avatar_url: string; + features: __experimental_CommerceFeatureJSON[]; +} + +export interface __experimental_CommerceProductJSON extends ClerkResourceJSON { + object: 'commerce_product'; + id: string; + slug: string; + currency: string; + is_default: boolean; + plans: __experimental_CommercePlanJSON[]; +} + +export interface __experimental_CommercePaymentSourceJSON extends ClerkResourceJSON { + object: 'commerce_payment_source'; + id: string; + last4: string; + payment_method: string; + card_type: string; +} + +export interface __experimental_CommerceInvoiceJSON extends ClerkResourceJSON { + object: 'commerce_invoice'; + id: string; + paid_on: number; + payment_due_on: number; + payment_source_id: string; + plan_id: string; + status: string; + totals: __experimental_CommerceTotalsJSON; +} + +export interface __experimental_CommerceSubscriptionJSON extends ClerkResourceJSON { + object: 'commerce_subscription'; + id: string; + payment_source_id: string; + plan: __experimental_CommercePlanJSON; + plan_period: string; + status: string; +} + +export interface __experimental_CommerceMoneyJSON { + amount: number; + amount_formatted: string; + currency: string; + currency_symbol: string; +} + +export interface __experimental_CommerceTotalsJSON { + grand_total: __experimental_CommerceMoneyJSON; + subtotal: __experimental_CommerceMoneyJSON; + tax_total: __experimental_CommerceMoneyJSON; + total_due_now?: __experimental_CommerceMoneyJSON; +} + +export interface __experimental_CommerceCheckoutJSON extends ClerkResourceJSON { + object: 'commerce_checkout'; + id: string; + external_client_secret: string; + external_gateway_id: string; + invoice?: __experimental_CommerceInvoiceJSON; + payment_source?: __experimental_CommercePaymentSourceJSON; + plan: __experimental_CommercePlanJSON; + plan_period: string; + status: string; + subscription?: __experimental_CommerceSubscriptionJSON; + totals: __experimental_CommerceTotalsJSON; +} diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts index 3fc1c6fcf33..792b3815651 100644 --- a/packages/types/src/localization.ts +++ b/packages/types/src/localization.ts @@ -87,6 +87,7 @@ type _LocalizationResource = { badge__unverified: LocalizationValue; badge__requiresAction: LocalizationValue; badge__you: LocalizationValue; + badge__currentPlan: LocalizationValue; footerPageLink__help: LocalizationValue; footerPageLink__privacy: LocalizationValue; footerPageLink__terms: LocalizationValue; @@ -97,6 +98,15 @@ type _LocalizationResource = { membershipRole__admin: LocalizationValue; membershipRole__basicMember: LocalizationValue; membershipRole__guestMember: LocalizationValue; + __experimental_commerce: { + month: LocalizationValue; + free: LocalizationValue; + getStarted: LocalizationValue; + manageMembership: LocalizationValue; + switchPlan: LocalizationValue; + billedAnnually: LocalizationValue; + accountFunds: LocalizationValue; + }; signUp: { start: { title: LocalizationValue; @@ -387,6 +397,7 @@ type _LocalizationResource = { description: LocalizationValue; account: LocalizationValue; security: LocalizationValue; + billing: LocalizationValue; }; start: { headerTitle__account: LocalizationValue; @@ -642,6 +653,14 @@ type _LocalizationResource = { actionDescription: LocalizationValue; confirm: LocalizationValue; }; + __experimental_billingPage: { + title: LocalizationValue; + start: { + headerTitle__plans: LocalizationValue; + headerTitle__invoices: LocalizationValue; + headerTitle__paymentSources: LocalizationValue; + }; + }; }; userButton: { action__manageAccount: LocalizationValue; diff --git a/packages/types/src/snapshots.ts b/packages/types/src/snapshots.ts index 78c5715c21e..bc398bb633e 100644 --- a/packages/types/src/snapshots.ts +++ b/packages/types/src/snapshots.ts @@ -1,5 +1,6 @@ // this file contains the types returned by the __internal_toSnapshot method of the resources +import type { __experimental_CommerceSettingsJSON } from './commerceSettings'; import type { DisplayConfigJSON } from './displayConfig'; import type { AuthConfigJSON, @@ -182,3 +183,5 @@ export type Web3WalletJSONSnapshot = Override< >; export type PublicUserDataJSONSnapshot = PublicUserDataJSON; + +export type __experimental_CommerceSettingsJSONSnapshot = __experimental_CommerceSettingsJSON; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd5c0b2352f..8595d64de95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -449,6 +449,12 @@ importers: '@formkit/auto-animate': specifier: ^0.8.1 version: 0.8.2 + '@stripe/react-stripe-js': + specifier: 3.1.1 + version: 3.1.1(@stripe/stripe-js@5.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@stripe/stripe-js': + specifier: 5.6.0 + version: 5.6.0 '@swc/helpers': specifier: ^0.5.13 version: 0.5.15 @@ -4395,6 +4401,17 @@ packages: peerDependencies: xstate: ^5.5.1 + '@stripe/react-stripe-js@3.1.1': + resolution: {integrity: sha512-+JzYFgUivVD7koqYV7LmLlt9edDMAwKH7XhZAHFQMo7NeRC+6D2JmQGzp9tygWerzwttwFLlExGp4rAOvD6l9g==} + peerDependencies: + '@stripe/stripe-js': ^1.44.1 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + react: '>=16.8.0 <20.0.0' + react-dom: '>=16.8.0 <20.0.0' + + '@stripe/stripe-js@5.6.0': + resolution: {integrity: sha512-w8CEY73X/7tw2KKlL3iOk679V9bWseE4GzNz3zlaYxcTjmcmWOathRb0emgo/QQ3eoNzmq68+2Y2gxluAv3xGw==} + engines: {node: '>=12.16'} + '@svgr/babel-plugin-add-jsx-attribute@6.5.1': resolution: {integrity: sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==} engines: {node: '>=10'} @@ -18748,6 +18765,15 @@ snapshots: transitivePeerDependencies: - ws + '@stripe/react-stripe-js@3.1.1(@stripe/stripe-js@5.6.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@stripe/stripe-js': 5.6.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@stripe/stripe-js@5.6.0': {} + '@svgr/babel-plugin-add-jsx-attribute@6.5.1(@babel/core@7.26.9)': dependencies: '@babel/core': 7.26.9