From 1be9179957cff8f25281f61afb4ea642d0185d24 Mon Sep 17 00:00:00 2001 From: Keiran Flanigan Date: Thu, 16 Jan 2025 09:38:18 -0800 Subject: [PATCH 01/64] add pricing table basics --- packages/clerk-js/sandbox/app.ts | 5 ++++ packages/clerk-js/sandbox/template.html | 8 +++++++ packages/clerk-js/src/core/clerk.ts | 24 +++++++++++++++++++ .../components/PricingTable/PricingTable.tsx | 6 +++++ .../src/ui/components/PricingTable/index.ts | 1 + .../UserProfile/UserProfileRoutes.tsx | 8 +++++++ packages/clerk-js/src/ui/constants.ts | 1 + .../ui/contexts/ClerkUIComponentsContext.tsx | 9 ++++++- .../ui/contexts/components/PricingTable.ts | 20 ++++++++++++++++ .../src/ui/contexts/components/index.ts | 1 + .../clerk-js/src/ui/icons/credit-card.svg | 4 ++++ packages/clerk-js/src/ui/icons/index.ts | 1 + .../clerk-js/src/ui/lazyModules/components.ts | 6 +++++ packages/clerk-js/src/ui/types.ts | 10 +++++++- .../src/ui/utils/createCustomPages.tsx | 8 ++++++- packages/localizations/src/en-US.ts | 1 + packages/types/src/appearance.ts | 5 ++++ packages/types/src/clerk.ts | 21 ++++++++++++++++ packages/types/src/localization.ts | 1 + 19 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx create mode 100644 packages/clerk-js/src/ui/components/PricingTable/index.ts create mode 100644 packages/clerk-js/src/ui/contexts/components/PricingTable.ts create mode 100644 packages/clerk-js/src/ui/icons/credit-card.svg diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index f62ab2273ff..3a4817fbe48 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -167,6 +167,11 @@ function addCurrentRouteIndicator(currentRoute: string) { }, }); }, + '/pricing-table': () => { + Clerk.mountPricingTable(app, { + currency: '$', + }); + }, '/open-sign-in': () => { mountOpenSignInButton(app, componentControls.signIn.getProps() ?? {}); }, diff --git a/packages/clerk-js/sandbox/template.html b/packages/clerk-js/sandbox/template.html index cc57518fc10..fe7cf0e37d0 100644 --- a/packages/clerk-js/sandbox/template.html +++ b/packages/clerk-js/sandbox/template.html @@ -244,6 +244,14 @@ Accountless +
  • + + PricingTable + +
  • controls.unmountComponent({ node })); }; + public mountPricingTable = (node: HTMLDivElement, props?: 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 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/ui/components/PricingTable/PricingTable.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx new file mode 100644 index 00000000000..36d37b92f2a --- /dev/null +++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx @@ -0,0 +1,6 @@ +import { usePricingTableContext } from '../../contexts'; + +export const PricingTable = () => { + const { currency } = usePricingTableContext(); + return

    Pricing Table {currency}

    ; +}; 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/UserProfileRoutes.tsx b/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx index 15723090da1..d5454982ace 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx @@ -9,6 +9,7 @@ export const UserProfileRoutes = () => { const { pages } = useUserProfileContext(); 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 +45,13 @@ export const UserProfileRoutes = () => { + + + +
    Here
    +
    +
    +
    ); 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..d157cb8e311 100644 --- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx +++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx @@ -1,4 +1,4 @@ -import type { UserButtonProps, WaitlistProps } from '@clerk/types'; +import type { PricingTableProps, UserButtonProps, WaitlistProps } from '@clerk/types'; import type { ReactNode } from 'react'; import type { AvailableComponentName, AvailableComponentProps } from '../types'; @@ -8,6 +8,7 @@ import { OrganizationListContext, OrganizationProfileContext, OrganizationSwitcherContext, + PricingTableContext, SignInContext, SignUpContext, UserButtonContext, @@ -78,6 +79,12 @@ export function ComponentContextProvider({ {children} ); + case 'PricingTable': + return ( + + {children} + + ); default: throw new Error(`Unknown component context: ${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..b9843406cd5 --- /dev/null +++ b/packages/clerk-js/src/ui/contexts/components/PricingTable.ts @@ -0,0 +1,20 @@ +import { createContext, useContext } from 'react'; + +import type { PricingTableCtx } from '../../types'; + +export const PricingTableContext = createContext(null); + +export const usePricingTableContext = () => { + const context = useContext(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..9e3bc561537 100644 --- a/packages/clerk-js/src/ui/contexts/components/index.ts +++ b/packages/clerk-js/src/ui/contexts/components/index.ts @@ -10,3 +10,4 @@ export * from './OrganizationProfile'; export * from './CreateOrganization'; export * from './GoogleOneTap'; export * from './Waitlist'; +export * from './PricingTable'; 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..bffb9c0fe91 100644 --- a/packages/clerk-js/src/ui/icons/index.ts +++ b/packages/clerk-js/src/ui/icons/index.ts @@ -17,6 +17,7 @@ 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 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'; diff --git a/packages/clerk-js/src/ui/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts index 2669b2772d5..7befd6de03b 100644 --- a/packages/clerk-js/src/ui/lazyModules/components.ts +++ b/packages/clerk-js/src/ui/lazyModules/components.ts @@ -17,6 +17,7 @@ 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'), } as const; export const SignIn = lazy(() => componentImportPaths.SignIn().then(module => ({ default: module.SignIn }))); @@ -88,6 +89,10 @@ export const KeylessPrompt = lazy(() => componentImportPaths.KeylessPrompt().then(module => ({ default: module.KeylessPrompt })), ); +export const PricingTable = lazy(() => + componentImportPaths.PricingTable().then(module => ({ default: module.PricingTable })), +); + export const preloadComponent = async (component: unknown) => { return componentImportPaths[component as keyof typeof componentImportPaths]?.(); }; @@ -112,6 +117,7 @@ export const ClerkComponents = { Waitlist, WaitlistModal, BlankCaptchaModal, + PricingTable, }; export type ClerkComponentName = keyof typeof ClerkComponents; diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index e90ebfff249..6ccc3d43cf6 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -5,6 +5,7 @@ import type { OrganizationListProps, OrganizationProfileProps, OrganizationSwitcherProps, + PricingTableProps, SignInProps, SignUpProps, UserButtonProps, @@ -36,6 +37,7 @@ export type AvailableComponentProps = | CreateOrganizationProps | OrganizationListProps | WaitlistProps + | PricingTableProps | __internal_UserVerificationProps; type ComponentMode = 'modal' | 'mounted'; @@ -96,6 +98,11 @@ export type WaitlistCtx = WaitlistProps & { mode?: ComponentMode; }; +export type PricingTableCtx = PricingTableProps & { + componentName: 'PricingTable'; + mode?: ComponentMode; +}; + export type AvailableComponentCtx = | SignInCtx | SignUpCtx @@ -107,6 +114,7 @@ export type AvailableComponentCtx = | OrganizationSwitcherCtx | OrganizationListCtx | GoogleOneTapCtx - | WaitlistCtx; + | WaitlistCtx + | PricingTableCtx; 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..a3fbaf9a1df 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'; @@ -242,6 +242,12 @@ const getUserProfileDefaultRoutes = (): GetDefaultRoutesReturnType => { icon: TickShield, path: 'security', }, + { + name: localizationKeys('userProfile.navbar.billing'), + id: USER_PROFILE_NAVBAR_ROUTE_ID.BILLING, + icon: CreditCard, + path: 'billing', + }, ]; const pageToRootNavbarRouteMap: Record = { diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index f1d7bcc02fc..a1045cef5dc 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -760,6 +760,7 @@ export const enUS: LocalizationResource = { account: 'Profile', description: 'Manage your account info.', security: 'Security', + billing: 'Billing', title: 'Account', }, passkeyScreen: { diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index cb211dbe5ba..2e51060b430 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -638,6 +638,7 @@ export type OrganizationProfileTheme = Theme; export type CreateOrganizationTheme = Theme; export type UserVerificationTheme = Theme; export type WaitlistTheme = Theme; +export type PricingTableTheme = Theme; export type Appearance = T & { /** @@ -684,4 +685,8 @@ export type Appearance = T & { * Theme overrides that only apply to the `` component */ waitlist?: T; + /** + * Theme overrides that only apply to the `` component + */ + pricingTable?: T; }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 00cf8a46cb1..84988718923 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -4,6 +4,7 @@ import type { OrganizationListTheme, OrganizationProfileTheme, OrganizationSwitcherTheme, + PricingTableTheme, SignInTheme, SignUpTheme, UserButtonTheme, @@ -377,6 +378,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. + */ + mountPricingTable: (targetNode: HTMLDivElement, props?: 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. + */ + 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. @@ -1407,6 +1423,11 @@ export type WaitlistProps = { export type WaitlistModalProps = WaitlistProps; +export type PricingTableProps = { + currency: string; + appearance?: PricingTableTheme; +}; + export interface HandleEmailLinkVerificationParams { /** * Full URL or path to navigate after successful magic link verification diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts index f6679fd9aa7..daae108c2b1 100644 --- a/packages/types/src/localization.ts +++ b/packages/types/src/localization.ts @@ -379,6 +379,7 @@ type _LocalizationResource = { description: LocalizationValue; account: LocalizationValue; security: LocalizationValue; + billing: LocalizationValue; }; start: { headerTitle__account: LocalizationValue; From b23b7b457c3efcd516c1e80b46b3439927205571 Mon Sep 17 00:00:00 2001 From: Keiran Flanigan Date: Tue, 28 Jan 2025 21:34:15 -0800 Subject: [PATCH 02/64] pricing table as customizables / elements --- packages/clerk-js/sandbox/template.html | 2 +- packages/clerk-js/src/core/clerk.ts | 10 ++ .../src/core/modules/commerce/index.ts | 35 ++++ packages/clerk-js/src/core/resources/Base.ts | 2 +- .../src/core/resources/CommerceFeature.ts | 30 ++++ .../src/core/resources/CommercePlan.ts | 46 +++++ .../src/core/resources/CommerceProduct.ts | 30 ++++ .../clerk-js/src/core/resources/internal.ts | 3 + .../components/PricingTable/PricingTable.tsx | 157 +++++++++++++++++- .../ui/components/UserProfile/BillingPage.tsx | 57 +++++++ .../UserProfile/UserProfileRoutes.tsx | 3 +- .../clerk-js/src/ui/primitives/Button.tsx | 7 + packages/localizations/src/en-US.ts | 14 ++ packages/types/src/clerk.ts | 5 +- packages/types/src/commerce.ts | 47 ++++++ packages/types/src/index.ts | 1 + packages/types/src/json.ts | 35 ++++ packages/types/src/localization.ts | 14 ++ 18 files changed, 491 insertions(+), 7 deletions(-) create mode 100644 packages/clerk-js/src/core/modules/commerce/index.ts create mode 100644 packages/clerk-js/src/core/resources/CommerceFeature.ts create mode 100644 packages/clerk-js/src/core/resources/CommercePlan.ts create mode 100644 packages/clerk-js/src/core/resources/CommerceProduct.ts create mode 100644 packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx create mode 100644 packages/types/src/commerce.ts diff --git a/packages/clerk-js/sandbox/template.html b/packages/clerk-js/sandbox/template.html index fe7cf0e37d0..3ce8b3eec3f 100644 --- a/packages/clerk-js/sandbox/template.html +++ b/packages/clerk-js/sandbox/template.html @@ -283,7 +283,7 @@