diff --git a/.changeset/great-moons-occur.md b/.changeset/great-moons-occur.md new file mode 100644 index 00000000000..c5e7f4ffb75 --- /dev/null +++ b/.changeset/great-moons-occur.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +'@clerk/types': patch +--- + +Add Payment Sources to ``, hook up all org-related payment source and checkout methods to the org-specific endpoints diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 3a9f48d6527..b7754135697 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,9 +1,9 @@ { "files": [ { "path": "./dist/clerk.js", "maxSize": "590kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "72.5KB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "72.65KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "55KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "98KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "98.1KB" }, { "path": "./dist/vendors*.js", "maxSize": "36KB" }, { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, @@ -21,7 +21,7 @@ { "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" }, { "path": "./dist/pricingTable*.js", "maxSize": "5KB" }, { "path": "./dist/checkout*.js", "maxSize": "3KB" }, - { "path": "./dist/paymentSources*.js", "maxSize": "8KB" }, + { "path": "./dist/paymentSources*.js", "maxSize": "8.1KB" }, { "path": "./dist/up-billing-page*.js", "maxSize": "1KB" }, { "path": "./dist/op-billing-page*.js", "maxSize": "1KB" }, { "path": "./dist/sessionTasks*.js", "maxSize": "1KB" } diff --git a/packages/clerk-js/src/core/modules/commerce/Commerce.ts b/packages/clerk-js/src/core/modules/commerce/Commerce.ts index 02d22c14984..6ac7674ca63 100644 --- a/packages/clerk-js/src/core/modules/commerce/Commerce.ts +++ b/packages/clerk-js/src/core/modules/commerce/Commerce.ts @@ -9,6 +9,7 @@ import type { ClerkPaginatedResponse, } from '@clerk/types'; +import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams'; import { __experimental_CommerceInitializedPaymentSource, __experimental_CommercePaymentSource, @@ -27,35 +28,42 @@ export class __experimental_Commerce implements __experimental_CommerceNamespace } initializePaymentSource = async (params: __experimental_InitializePaymentSourceParams) => { + const { orgId, ...rest } = params; const json = ( await BaseResource._fetch({ - path: `/me/commerce/payment_sources/initialize`, + path: orgId + ? `/organizations/${orgId}/commerce/payment_sources/initialize` + : `/me/commerce/payment_sources/initialize`, method: 'POST', - body: params as any, + body: rest as any, }) )?.response as unknown as __experimental_CommerceInitializedPaymentSourceJSON; return new __experimental_CommerceInitializedPaymentSource(json); }; addPaymentSource = async (params: __experimental_AddPaymentSourceParams) => { + const { orgId, ...rest } = params; + const json = ( await BaseResource._fetch({ - path: `/me/commerce/payment_sources`, + path: orgId ? `/organizations/${orgId}/commerce/payment_sources` : `/me/commerce/payment_sources`, method: 'POST', - body: params as any, + body: rest as any, }) )?.response as unknown as __experimental_CommercePaymentSourceJSON; return new __experimental_CommercePaymentSource(json); }; getPaymentSources = async (params: __experimental_GetPaymentSourcesParams) => { + const { orgId, ...rest } = params; + return await BaseResource._fetch({ - path: `/me/commerce/payment_sources`, + path: orgId ? `/organizations/${orgId}/commerce/payment_sources` : `/me/commerce/payment_sources`, method: 'GET', - search: { orgId: params.orgId || '' }, + search: convertPageToOffsetSearchParams(rest), }).then(res => { const { data: paymentSources, total_count } = - res as unknown as ClerkPaginatedResponse<__experimental_CommercePaymentSourceJSON>; + res?.response 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 index f6b2ec40717..aacab3ecea0 100644 --- a/packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts +++ b/packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts @@ -7,9 +7,11 @@ import type { __experimental_CommerceSubscriptionResource, __experimental_CreateCheckoutParams, __experimental_GetPlansParams, + __experimental_GetSubscriptionsParams, ClerkPaginatedResponse, } from '@clerk/types'; +import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams'; import { __experimental_CommerceCheckout, __experimental_CommercePlan, @@ -29,10 +31,15 @@ export class __experimental_CommerceBilling implements __experimental_CommerceBi return defaultProduct?.plans.map(plan => new __experimental_CommercePlan(plan)) || []; }; - getSubscriptions = async (): Promise> => { + getSubscriptions = async ( + params: __experimental_GetSubscriptionsParams, + ): Promise> => { + const { orgId, ...rest } = params; + return await BaseResource._fetch({ - path: `/me/subscriptions`, + path: orgId ? `/organizations/${orgId}/subscriptions` : `/me/commerce/subscriptions`, method: 'GET', + search: convertPageToOffsetSearchParams(rest), }).then(res => { const { data: subscriptions, total_count } = res?.response as unknown as ClerkPaginatedResponse<__experimental_CommerceSubscriptionJSON>; @@ -45,11 +52,12 @@ export class __experimental_CommerceBilling implements __experimental_CommerceBi }; startCheckout = async (params: __experimental_CreateCheckoutParams) => { + const { orgId, ...rest } = params; const json = ( await BaseResource._fetch<__experimental_CommerceCheckoutJSON>({ - path: `/me/commerce/checkouts`, + path: orgId ? `/organizations/${orgId}/commerce/checkouts` : `/me/commerce/checkouts`, method: 'POST', - body: params as any, + body: rest as any, }) )?.response as unknown as __experimental_CommerceCheckoutJSON; diff --git a/packages/clerk-js/src/core/resources/CommerceCheckout.ts b/packages/clerk-js/src/core/resources/CommerceCheckout.ts index b1f5be96a37..e488905a6c2 100644 --- a/packages/clerk-js/src/core/resources/CommerceCheckout.ts +++ b/packages/clerk-js/src/core/resources/CommerceCheckout.ts @@ -16,8 +16,6 @@ import { } from './internal'; export class __experimental_CommerceCheckout extends BaseResource implements __experimental_CommerceCheckoutResource { - pathRoot = '/me/commerce/checkouts'; - id!: string; externalClientSecret!: string; externalGatewayId!: string; @@ -55,10 +53,13 @@ export class __experimental_CommerceCheckout extends BaseResource implements __e return this; } - confirm = (params?: __experimental_ConfirmCheckoutParams): Promise => { + confirm = (params: __experimental_ConfirmCheckoutParams): Promise => { + const { orgId, ...rest } = params; return this._basePatch({ - path: this.path('confirm'), - body: params as any, + path: orgId + ? `/organizations/${orgId}/commerce/checkouts/${this.id}/confirm` + : `/me/commerce/checkouts/${this.id}/confirm`, + body: rest as any, }); }; } diff --git a/packages/clerk-js/src/core/resources/CommercePaymentSource.ts b/packages/clerk-js/src/core/resources/CommercePaymentSource.ts index bcd0aca4bd0..5dbded2b4b1 100644 --- a/packages/clerk-js/src/core/resources/CommercePaymentSource.ts +++ b/packages/clerk-js/src/core/resources/CommercePaymentSource.ts @@ -4,6 +4,8 @@ import type { __experimental_CommercePaymentSourceJSON, __experimental_CommercePaymentSourceResource, __experimental_CommercePaymentSourceStatus, + __experimental_MakeDefaultPaymentSourceParams, + __experimental_RemovePaymentSourceParams, DeletedObjectJSON, } from '@clerk/types'; @@ -19,6 +21,7 @@ export class __experimental_CommercePaymentSource cardType!: string; isDefault!: boolean; status!: __experimental_CommercePaymentSourceStatus; + walletType: string | undefined; constructor(data: __experimental_CommercePaymentSourceJSON) { super(); @@ -34,21 +37,39 @@ export class __experimental_CommercePaymentSource this.last4 = data.last4; this.paymentMethod = data.payment_method; this.cardType = data.card_type; - this.isDefault = false; + this.isDefault = data.is_default; this.status = data.status; + this.walletType = data.wallet_type ?? undefined; + return this; } - public async remove() { + public async remove(params?: __experimental_RemovePaymentSourceParams) { + const { orgId } = params ?? {}; const json = ( await BaseResource._fetch({ - path: `/me/commerce/payment_sources/${this.id}`, + path: orgId + ? `/organizations/${orgId}/commerce/payment_sources/${this.id}` + : `/me/commerce/payment_sources/${this.id}`, method: 'DELETE', }) )?.response as unknown as DeletedObjectJSON; return new DeletedObject(json); } + + public async makeDefault(params?: __experimental_MakeDefaultPaymentSourceParams) { + const { orgId } = params ?? {}; + await BaseResource._fetch({ + path: orgId + ? `/organizations/${orgId}/commerce/payers/default_payment_source` + : `/me/commerce/payers/default_payment_source`, + method: 'PUT', + body: { payment_source_id: this.id } as any, + }); + + return null; + } } export class __experimental_CommerceInitializedPaymentSource diff --git a/packages/clerk-js/src/core/resources/CommerceSubscription.ts b/packages/clerk-js/src/core/resources/CommerceSubscription.ts index e25f95c8612..cb280bead0b 100644 --- a/packages/clerk-js/src/core/resources/CommerceSubscription.ts +++ b/packages/clerk-js/src/core/resources/CommerceSubscription.ts @@ -1,4 +1,5 @@ import type { + __experimental_CancelSubscriptionParams, __experimental_CommerceSubscriptionJSON, __experimental_CommerceSubscriptionPlanPeriod, __experimental_CommerceSubscriptionResource, @@ -37,10 +38,13 @@ export class __experimental_CommerceSubscription return this; } - public async cancel() { + public async cancel(params: __experimental_CancelSubscriptionParams) { + const { orgId } = params; const json = ( await BaseResource._fetch({ - path: `/me/commerce/subscriptions/${this.id}`, + path: orgId + ? `/organizations/${orgId}/commerce/subscriptions/${this.id}` + : `/me/commerce/subscriptions/${this.id}`, method: 'DELETE', }) )?.response as unknown as DeletedObjectJSON; diff --git a/packages/clerk-js/src/core/resources/Organization.ts b/packages/clerk-js/src/core/resources/Organization.ts index 9ca83f101a0..e452cb35742 100644 --- a/packages/clerk-js/src/core/resources/Organization.ts +++ b/packages/clerk-js/src/core/resources/Organization.ts @@ -240,16 +240,11 @@ export class Organization extends BaseResource implements OrganizationResource { __experimental_getSubscriptions = async ( getSubscriptionsParams?: __experimental_GetSubscriptionsParams, ): Promise> => { - return await BaseResource._fetch( - { - path: `/organizations/${this.id}/subscriptions`, - method: 'GET', - search: convertPageToOffsetSearchParams(getSubscriptionsParams), - }, - { - forceUpdateClient: true, - }, - ).then(res => { + return await BaseResource._fetch({ + path: `/organizations/${this.id}/subscriptions`, + method: 'GET', + search: convertPageToOffsetSearchParams(getSubscriptionsParams), + }).then(res => { const { data: subscriptions, total_count } = res?.response as unknown as ClerkPaginatedResponse<__experimental_CommerceSubscriptionJSON>; diff --git a/packages/clerk-js/src/ui/Components.tsx b/packages/clerk-js/src/ui/Components.tsx index ad400d13aa9..31d19687709 100644 --- a/packages/clerk-js/src/ui/Components.tsx +++ b/packages/clerk-js/src/ui/Components.tsx @@ -541,7 +541,7 @@ const Components = (props: ComponentsProps) => { diff --git a/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx b/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx index dd869dbc7d0..c3043848e7e 100644 --- a/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx @@ -12,6 +12,7 @@ export const __experimental_Checkout = (props: __experimental_CheckoutProps) => <__experimental_CheckoutContext.Provider value={{ componentName: 'Checkout', + ...props, }} > diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx index 2d98f773f06..0bd7b23a7f1 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/shared/react'; +import { useClerk, useOrganization } from '@clerk/shared/react'; import type { __experimental_CommerceCheckoutResource, __experimental_CommerceMoney, @@ -8,6 +8,7 @@ import type { } from '@clerk/types'; import { useMemo, useState } from 'react'; +import { __experimental_PaymentSourcesContext, useCheckoutContext } from '../../contexts'; import { Box, Button, Col, descriptors, Flex, Form, Icon, localizationKeys, Text } from '../../customizables'; import { Alert, Disclosure, Divider, Drawer, LineItems, Select, SelectButton, SelectOptionList } from '../../elements'; import { useFetch } from '../../hooks'; @@ -90,17 +91,29 @@ const CheckoutFormElements = ({ onCheckoutComplete: (checkout: __experimental_CommerceCheckoutResource) => void; }) => { const { __experimental_commerce } = useClerk(); + const { organization } = useOrganization(); + const { subscriberType } = useCheckoutContext(); 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, 'commerce-payment-sources'); + const { data } = useFetch( + __experimental_commerce?.getPaymentSources, + { + ...(subscriberType === 'org' ? { orgId: organization?.id } : {}), + }, + undefined, + 'commerce-payment-sources', + ); const { data: paymentSources } = data || { data: [] }; const confirmCheckout = async ({ paymentSourceId }: { paymentSourceId: string }) => { return checkout - .confirm({ paymentSourceId }) + .confirm({ + paymentSourceId, + ...(subscriberType === 'org' ? { orgId: organization?.id } : {}), + }) .then(newCheckout => { onCheckoutComplete(newCheckout); }) @@ -176,16 +189,18 @@ const CheckoutFormElements = ({ {/* TODO(@Commerce): needs localization */} - + <__experimental_PaymentSourcesContext.Provider value={{ componentName: 'PaymentSources', subscriberType }}> + + diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx index a24982f2e80..d8cda15a918 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx @@ -6,12 +6,12 @@ import { CheckoutComplete } from './CheckoutComplete'; import { CheckoutForm } from './CheckoutForm'; export const CheckoutPage = (props: __experimental_CheckoutProps) => { - const { planId, planPeriod, orgId, onSubscriptionComplete } = props; + const { planId, planPeriod, subscriberType, onSubscriptionComplete } = props; const { checkout, updateCheckout, isLoading } = useCheckout({ planId, planPeriod, - orgId, + subscriberType, }); const onCheckoutComplete = (newCheckout: __experimental_CommerceCheckoutResource) => { diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx index 6998ab2c89e..5b47fcf489e 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx @@ -1,4 +1,4 @@ -import { __experimental_PricingTableContext } from '../../contexts'; +import { __experimental_PaymentSourcesContext, __experimental_PricingTableContext } from '../../contexts'; import { Col, descriptors, localizationKeys } from '../../customizables'; import { Card, @@ -11,6 +11,7 @@ import { useCardState, withCardStateProvider, } from '../../elements'; +import { __experimental_PaymentSources } from '../PaymentSources/PaymentSources'; import { __experimental_PricingTable } from '../PricingTable'; export const OrganizationBillingPage = withCardStateProvider(() => { @@ -58,7 +59,13 @@ export const OrganizationBillingPage = withCardStateProvider(() => { Invoices - Payment Sources + + <__experimental_PaymentSourcesContext.Provider + value={{ componentName: 'PaymentSources', subscriberType: 'org' }} + > + <__experimental_PaymentSources /> + + diff --git a/packages/clerk-js/src/ui/components/PaymentSources/AddPaymentSource.tsx b/packages/clerk-js/src/ui/components/PaymentSources/AddPaymentSource.tsx index 5d61a47bd52..a727d392159 100644 --- a/packages/clerk-js/src/ui/components/PaymentSources/AddPaymentSource.tsx +++ b/packages/clerk-js/src/ui/components/PaymentSources/AddPaymentSource.tsx @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/shared/react'; +import { useClerk, useOrganization } from '@clerk/shared/react'; import type { __experimental_CommerceCheckoutResource, __experimental_CommercePaymentSourceResource, @@ -10,7 +10,7 @@ import type { Appearance as StripeAppearance, Stripe } from '@stripe/stripe-js'; import { loadStripe } from '@stripe/stripe-js'; import { useEffect, useRef, useState } from 'react'; -import { useEnvironment } from '../../contexts'; +import { useEnvironment, usePaymentSourcesContext } from '../../contexts'; import { descriptors, Flex, localizationKeys, Spinner, useAppearance } from '../../customizables'; import { Alert, Form, FormButtons, FormContainer, withCardStateProvider } from '../../elements'; import { useFetch } from '../../hooks/useFetch'; @@ -29,6 +29,8 @@ export const AddPaymentSource = (props: AddPaymentSourceProps) => { const { checkout, submitLabel, onSuccess, cancelAction } = props; const { __experimental_commerce } = useClerk(); const { __experimental_commerceSettings } = useEnvironment(); + const { organization } = useOrganization(); + const { subscriberType } = usePaymentSourcesContext(); const stripePromiseRef = useRef | null>(null); const [stripe, setStripe] = useState(null); @@ -61,7 +63,10 @@ export const AddPaymentSource = (props: AddPaymentSourceProps) => { !checkout ? __experimental_commerce.initializePaymentSource : undefined, { gateway: 'stripe', + ...(subscriberType === 'org' ? { orgId: organization?.id } : {}), }, + undefined, + 'commerce-payment-source-initialize', ); const externalGatewayId = checkout?.externalGatewayId ?? initializedPaymentSource?.externalGatewayId; @@ -124,6 +129,8 @@ const AddPaymentSourceForm = withCardStateProvider( const stripe = useStripe(); const elements = useElements(); const { displayConfig } = useEnvironment(); + const { organization } = useOrganization(); + const { subscriberType } = usePaymentSourcesContext(); const [submitError, setSubmitError] = useState(); const onSubmit = async (e: React.FormEvent) => { @@ -148,6 +155,7 @@ const AddPaymentSourceForm = withCardStateProvider( const paymentSource = await __experimental_commerce.addPaymentSource({ gateway: 'stripe', paymentToken: setupIntent.payment_method as string, + ...(subscriberType === 'org' ? { orgId: organization?.id } : {}), }); void onSuccess(paymentSource); diff --git a/packages/clerk-js/src/ui/components/PaymentSources/PaymentSources.tsx b/packages/clerk-js/src/ui/components/PaymentSources/PaymentSources.tsx index 5e6d0676ebb..64355c1915c 100644 --- a/packages/clerk-js/src/ui/components/PaymentSources/PaymentSources.tsx +++ b/packages/clerk-js/src/ui/components/PaymentSources/PaymentSources.tsx @@ -1,14 +1,15 @@ -import { useClerk } from '@clerk/shared/react'; +import { useClerk, useOrganization } from '@clerk/shared/react'; import type { __experimental_CommercePaymentSourceResource, __experimental_PaymentSourcesProps } from '@clerk/types'; import { Fragment, useRef } from 'react'; import { RemoveResourceForm } from '../../common'; +import { usePaymentSourcesContext } from '../../contexts'; import { Badge, Flex, Icon, localizationKeys, Text } from '../../customizables'; import { ProfileSection, ThreeDotsMenu, useCardState } from '../../elements'; import { Action } from '../../elements/Action'; import { useActionContext } from '../../elements/Action/ActionRoot'; import { useFetch } from '../../hooks'; -import { CreditCard } from '../../icons'; +import { ApplePay, CreditCard } from '../../icons'; import { handleError } from '../../utils'; import { AddPaymentSource } from './AddPaymentSource'; @@ -38,6 +39,8 @@ const RemoveScreen = ({ }) => { const { close } = useActionContext(); const card = useCardState(); + const { subscriberType } = usePaymentSourcesContext(); + const { organization } = useOrganization(); const ref = useRef( `${paymentSource.paymentMethod === 'card' ? paymentSource.cardType : paymentSource.paymentMethod} ${paymentSource.paymentMethod === 'card' ? `⋯ ${paymentSource.last4}` : '-'}`, ); @@ -48,7 +51,7 @@ const RemoveScreen = ({ const removePaymentSource = async () => { await paymentSource - .remove() + .remove({ orgId: subscriberType === 'org' ? organization?.id : undefined }) .then(revalidate) .catch((error: Error) => { handleError(error, [], card.setError); @@ -80,11 +83,17 @@ const RemoveScreen = ({ ); }; -export const __experimental_PaymentSources = (props: __experimental_PaymentSourcesProps) => { - const { orgId } = props; +const PaymentSources = (_: __experimental_PaymentSourcesProps) => { const { __experimental_commerce } = useClerk(); + const { organization } = useOrganization(); + const { subscriberType } = usePaymentSourcesContext(); - const { data, revalidate } = useFetch(__experimental_commerce?.getPaymentSources, { orgId }); + const { data, revalidate } = useFetch( + __experimental_commerce?.getPaymentSources, + { ...(subscriberType === 'org' ? { orgId: organization?.id } : {}) }, + undefined, + 'commerce-user-payment-sources', + ); const { data: paymentSources } = data || { data: [] }; return ( @@ -104,12 +113,10 @@ export const __experimental_PaymentSources = (props: __experimental_PaymentSourc gap={2} align='baseline' > - {paymentSource.paymentMethod === 'card' && ( - - )} + ({ color: t.colors.$colorText, textTransform: 'capitalize' })} truncate @@ -131,7 +138,10 @@ export const __experimental_PaymentSources = (props: __experimental_PaymentSourc /> )} - + @@ -161,8 +171,19 @@ export const __experimental_PaymentSources = (props: __experimental_PaymentSourc ); }; -const PaymentSourceMenu = ({ paymentSource }: { paymentSource: __experimental_CommercePaymentSourceResource }) => { +export const __experimental_PaymentSources = PaymentSources; + +const PaymentSourceMenu = ({ + paymentSource, + revalidate, +}: { + paymentSource: __experimental_CommercePaymentSourceResource; + revalidate: () => void; +}) => { const { open } = useActionContext(); + const card = useCardState(); + const { organization } = useOrganization(); + const { subscriberType } = usePaymentSourcesContext(); const actions = [ { @@ -172,5 +193,20 @@ const PaymentSourceMenu = ({ paymentSource }: { paymentSource: __experimental_Co }, ]; + if (!paymentSource.isDefault) { + actions.unshift({ + label: localizationKeys('userProfile.__experimental_billingPage.paymentSourcesSection.actionLabel__default'), + isDestructive: false, + onClick: () => { + paymentSource + .makeDefault({ orgId: subscriberType === 'org' ? organization?.id : undefined }) + .then(revalidate) + .catch((error: Error) => { + handleError(error, [], card.setError); + }); + }, + }); + } + return ; }; diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx index 58d37abf5cc..5c4ab77bfdb 100644 --- a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx +++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx @@ -1,4 +1,4 @@ -import { useClerk, useOrganization } from '@clerk/shared/react'; +import { useClerk } from '@clerk/shared/react'; import type { __experimental_CommercePlanResource, __experimental_CommerceSubscriptionPlanPeriod, @@ -15,10 +15,9 @@ import { PricingTableDefault } from './PricingTableDefault'; import { PricingTableMatrix } from './PricingTableMatrix'; import { SubscriptionDetailDrawer } from './SubscriptionDetailDrawer'; -export const __experimental_PricingTable = (props: __experimental_PricingTableProps) => { +const PricingTable = (props: __experimental_PricingTableProps) => { const clerk = useClerk(); - const { organization } = useOrganization(); - const { mode = 'mounted', subscriberType = 'user' } = usePricingTableContext(); + const { mode = 'mounted', subscriberType } = usePricingTableContext(); const isCompact = mode === 'modal'; const { plans, subscriptions, revalidate } = usePlans({ subscriberType }); @@ -40,7 +39,7 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr clerk.__internal_openCheckout({ planId: plan.id, planPeriod, - orgId: subscriberType === 'org' ? organization?.id : undefined, + subscriberType, onSubscriptionComplete: onSubscriptionChange, portalId: mode === 'modal' ? PROFILE_CARD_SCROLLBOX_ID : undefined, }); @@ -80,6 +79,7 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr isOpen={showSubscriptionDetailDrawer} setIsOpen={setShowSubscriptionDetailDrawer} subscription={detailSubscription} + subscriberType={subscriberType} setPlanPeriod={setPlanPeriod} strategy={mode === 'mounted' ? 'fixed' : 'absolute'} portalProps={{ @@ -91,3 +91,5 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr ); }; + +export const __experimental_PricingTable = PricingTable; diff --git a/packages/clerk-js/src/ui/components/PricingTable/SubscriptionDetailDrawer.tsx b/packages/clerk-js/src/ui/components/PricingTable/SubscriptionDetailDrawer.tsx index 6e845cc3f44..90cb9ad5992 100644 --- a/packages/clerk-js/src/ui/components/PricingTable/SubscriptionDetailDrawer.tsx +++ b/packages/clerk-js/src/ui/components/PricingTable/SubscriptionDetailDrawer.tsx @@ -1,5 +1,7 @@ +import { useOrganization } from '@clerk/shared/react'; import type { __experimental_CommercePlanResource, + __experimental_CommerceSubscriberType, __experimental_CommerceSubscriptionPlanPeriod, __experimental_CommerceSubscriptionResource, ClerkAPIError, @@ -33,6 +35,7 @@ type SubscriptionDetailDrawerProps = { portalProps?: DrawerRootProps['portalProps']; strategy: DrawerRootProps['strategy']; subscription?: __experimental_CommerceSubscriptionResource; + subscriberType: __experimental_CommerceSubscriberType; setPlanPeriod: (p: __experimental_CommerceSubscriptionPlanPeriod) => void; onSubscriptionCancel: () => void; }; @@ -43,9 +46,11 @@ export function SubscriptionDetailDrawer({ portalProps, strategy, subscription, + subscriberType, setPlanPeriod, onSubscriptionCancel, }: SubscriptionDetailDrawerProps) { + const { organization } = useOrganization(); const [showConfirmation, setShowConfirmation] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [cancelError, setCancelError] = useState(); @@ -59,7 +64,7 @@ export function SubscriptionDetailDrawer({ setIsSubmitting(true); await subscription - .cancel() + .cancel({ orgId: subscriberType === 'org' ? organization?.id : undefined }) .then(() => { setIsSubmitting(false); onSubscriptionCancel(); diff --git a/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx index 824dbce854a..222c72e1f05 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx @@ -1,4 +1,4 @@ -import { __experimental_PricingTableContext } from '../../contexts'; +import { __experimental_PaymentSourcesContext, __experimental_PricingTableContext } from '../../contexts'; import { Col, descriptors, localizationKeys } from '../../customizables'; import { Card, @@ -58,7 +58,9 @@ export const BillingPage = withCardStateProvider(() => { Invoices - <__experimental_PaymentSources /> + <__experimental_PaymentSourcesContext.Provider value={{ componentName: 'PaymentSources' }}> + <__experimental_PaymentSources /> + diff --git a/packages/clerk-js/src/ui/contexts/components/PaymentSources.ts b/packages/clerk-js/src/ui/contexts/components/PaymentSources.ts new file mode 100644 index 00000000000..6f011532189 --- /dev/null +++ b/packages/clerk-js/src/ui/contexts/components/PaymentSources.ts @@ -0,0 +1,21 @@ +import { createContext, useContext } from 'react'; + +import type { __experimental_PaymentSourcesCtx } from '../../types'; + +export const __experimental_PaymentSourcesContext = createContext<__experimental_PaymentSourcesCtx | null>(null); + +export const usePaymentSourcesContext = () => { + const context = useContext(__experimental_PaymentSourcesContext); + + if (!context || context.componentName !== 'PaymentSources') { + throw new Error('Clerk: usePaymentSourcesContext called outside PaymentSources.'); + } + + const { componentName, ...ctx } = context; + + return { + ...ctx, + componentName, + subscriberType: ctx.subscriberType || 'user', + }; +}; diff --git a/packages/clerk-js/src/ui/contexts/components/PricingTable.ts b/packages/clerk-js/src/ui/contexts/components/PricingTable.ts index 862a0921c2d..f989d523e4d 100644 --- a/packages/clerk-js/src/ui/contexts/components/PricingTable.ts +++ b/packages/clerk-js/src/ui/contexts/components/PricingTable.ts @@ -16,5 +16,6 @@ export const usePricingTableContext = () => { return { ...ctx, componentName, + subscriberType: ctx.subscriberType || 'user', }; }; diff --git a/packages/clerk-js/src/ui/contexts/components/index.ts b/packages/clerk-js/src/ui/contexts/components/index.ts index 099c80876e2..70d0afa6da4 100644 --- a/packages/clerk-js/src/ui/contexts/components/index.ts +++ b/packages/clerk-js/src/ui/contexts/components/index.ts @@ -12,3 +12,4 @@ export * from './GoogleOneTap'; export * from './Waitlist'; export * from './PricingTable'; export * from './Checkout'; +export * from './PaymentSources'; diff --git a/packages/clerk-js/src/ui/hooks/useCheckout.ts b/packages/clerk-js/src/ui/hooks/useCheckout.ts index 5f307d6f869..80b09fb79ba 100644 --- a/packages/clerk-js/src/ui/hooks/useCheckout.ts +++ b/packages/clerk-js/src/ui/hooks/useCheckout.ts @@ -1,18 +1,19 @@ -import { useClerk } from '@clerk/shared/react'; +import { useClerk, useOrganization } 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, orgId } = props; + const { planId, planPeriod, subscriberType = 'user' } = props; const { __experimental_commerce } = useClerk(); + const { organization } = useOrganization(); const [currentCheckout, setCurrentCheckout] = useState<__experimental_CommerceCheckoutResource | null>(null); const { data: initialCheckout, isLoading } = useFetch(__experimental_commerce?.__experimental_billing.startCheckout, { planId, planPeriod, - orgId, + ...(subscriberType === 'org' ? { orgId: organization?.id } : {}), }); useEffect(() => { diff --git a/packages/clerk-js/src/ui/hooks/usePlans.ts b/packages/clerk-js/src/ui/hooks/usePlans.ts index 5adf8501824..1642e3d1228 100644 --- a/packages/clerk-js/src/ui/hooks/usePlans.ts +++ b/packages/clerk-js/src/ui/hooks/usePlans.ts @@ -9,52 +9,46 @@ type UsePlansProps = { }; export const usePlans = (props: UsePlansProps) => { - const { subscriberType } = props; + const { subscriberType = 'user' } = props; const { __experimental_commerce } = useClerk(); + const { organization } = useOrganization(); - const { data: userSubscriptions, revalidate: revalidateUserSubscriptions } = useFetch( + const { data: subscriptions, revalidate: revalidateSubscriptions } = useFetch( __experimental_commerce?.__experimental_billing.getSubscriptions, - 'commerce-user-subscriptions', + { orgId: subscriberType === 'org' ? organization?.id : undefined }, + undefined, + 'commerce-subscriptions', ); - const { subscriptions: orgSubscriptions } = useOrganization({ subscriptions: true }); const { data: allPlans, revalidate: revalidatePlans } = useFetch( __experimental_commerce?.__experimental_billing.getPlans, { subscriberType }, ); - const activeSubscriptions = useMemo(() => { - if ((subscriberType === 'user' && !userSubscriptions) || (subscriberType === 'org' && !orgSubscriptions)) { - return undefined; - } - return [...(subscriberType === 'user' ? userSubscriptions?.data || [] : orgSubscriptions?.data || [])]; - }, [userSubscriptions, orgSubscriptions, subscriberType]); - const plans = useMemo(() => { - if (!activeSubscriptions) { + if (!subscriptions) { return []; } return ( allPlans?.map(plan => { - const activeSubscription = activeSubscriptions.find(sub => { - return sub.plan.id === plan.id; + const activeSubscription = subscriptions.data.find(sub => { + return sub.plan.id === plan.id && sub.status === 'active'; }); plan.subscriptionIdForCurrentSubscriber = activeSubscription?.id; return plan; }) || [] ); - }, [allPlans, activeSubscriptions]); + }, [allPlans, subscriptions]); - const revalidate = async () => { + const revalidate = () => { // Revalidate the plans and subscriptions - await orgSubscriptions?.revalidate?.(); - revalidateUserSubscriptions(); + revalidateSubscriptions(); revalidatePlans(); }; return { plans, - subscriptions: activeSubscriptions || [], + subscriptions: subscriptions?.data || [], revalidate, }; }; diff --git a/packages/clerk-js/src/ui/icons/apple-pay.svg b/packages/clerk-js/src/ui/icons/apple-pay.svg new file mode 100644 index 00000000000..3191e822be8 --- /dev/null +++ b/packages/clerk-js/src/ui/icons/apple-pay.svg @@ -0,0 +1,3 @@ + + + \ 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 4be5e4ecc05..0582091aba6 100644 --- a/packages/clerk-js/src/ui/icons/index.ts +++ b/packages/clerk-js/src/ui/icons/index.ts @@ -5,6 +5,7 @@ * The above no-check is safe, as webpack will not allow compilation if for example a file is not resolved. */ export { default as Add } from './add.svg'; +export { default as ApplePay } from './apple-pay.svg'; export { default as ArrowLeftIcon } from './arrow-left.svg'; export { default as ArrowRightButtonIcon } from './arrow-right-button.svg'; export { default as ArrowRightIcon } from './arrow-right.svg'; diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 3e65b2d8acd..6ef9aa22a54 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -111,6 +111,11 @@ export type __experimental_CheckoutCtx = __experimental_CheckoutProps & { componentName: 'Checkout'; }; +export type __experimental_PaymentSourcesCtx = { + componentName: 'PaymentSources'; + subscriberType?: __experimental_CommerceSubscriberType; +}; + export type SessionTasksCtx = { nextTask: () => Promise; redirectUrlComplete?: string; @@ -130,5 +135,4 @@ export type AvailableComponentCtx = | WaitlistCtx | __experimental_PricingTableCtx | __experimental_CheckoutCtx; - export type AvailableComponentName = AvailableComponentCtx['componentName']; diff --git a/packages/shared/src/react/hooks/useOrganization.tsx b/packages/shared/src/react/hooks/useOrganization.tsx index 94a0fc74685..7cafd3f6ba4 100644 --- a/packages/shared/src/react/hooks/useOrganization.tsx +++ b/packages/shared/src/react/hooks/useOrganization.tsx @@ -339,7 +339,6 @@ export function useOrganization(params?: T): Us const subscriptionsSafeValues = useWithSafeValues(subscriptionsListParams, { initialPage: 1, pageSize: 10, - status: undefined, keepPreviousData: false, infinite: false, }); @@ -391,7 +390,7 @@ export function useOrganization(params?: T): Us : { initialPage: subscriptionsSafeValues.initialPage, pageSize: subscriptionsSafeValues.pageSize, - status: subscriptionsSafeValues.status, + orgId: organization?.id, }; const domains = usePagesOrInfinite>( diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index ff2db91a5d8..3bff3424f25 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -14,7 +14,11 @@ import type { WaitlistTheme, } from './appearance'; import type { ClientResource } from './client'; -import type { __experimental_CommerceNamespace, __experimental_CommerceSubscriptionPlanPeriod } from './commerce'; +import type { + __experimental_CommerceNamespace, + __experimental_CommerceSubscriberType, + __experimental_CommerceSubscriptionPlanPeriod, +} from './commerce'; import type { CustomMenuItem } from './customMenuItems'; import type { CustomPage } from './customPages'; import type { InstanceType } from './instance'; @@ -1524,13 +1528,13 @@ export type __experimental_CheckoutProps = { appearance?: CheckoutTheme; planId?: string; planPeriod?: __experimental_CommerceSubscriptionPlanPeriod; - orgId?: string; + subscriberType?: __experimental_CommerceSubscriberType; onSubscriptionComplete?: () => void; portalId?: string; }; export type __experimental_PaymentSourcesProps = { - orgId?: string; + subscriberType?: __experimental_CommerceSubscriberType; }; export interface HandleEmailLinkVerificationParams { diff --git a/packages/types/src/commerce.ts b/packages/types/src/commerce.ts index 320b2cea108..9b3a190f5cf 100644 --- a/packages/types/src/commerce.ts +++ b/packages/types/src/commerce.ts @@ -2,6 +2,9 @@ import type { DeletedObjectResource } from './deletedObject'; import type { ClerkPaginatedResponse, ClerkPaginationParams } from './pagination'; import type { ClerkResource } from './resource'; +type WithOptionalOrgType = T & { + orgId?: string; +}; export interface __experimental_CommerceNamespace { __experimental_billing: __experimental_CommerceBillingNamespace; getPaymentSources: ( @@ -18,7 +21,7 @@ export interface __experimental_CommerceNamespace { export interface __experimental_CommerceBillingNamespace { getPlans: () => Promise<__experimental_CommercePlanResource[]>; getSubscriptions: ( - params?: __experimental_GetSubscriptionsParams, + params: __experimental_GetSubscriptionsParams, ) => Promise>; startCheckout: (params: __experimental_CreateCheckoutParams) => Promise<__experimental_CommerceCheckoutResource>; } @@ -69,18 +72,19 @@ export interface __experimental_CommerceFeatureResource extends ClerkResource { export type __experimental_CommercePaymentSourceStatus = 'active' | 'expired' | 'disconnected'; -export interface __experimental_InitializePaymentSourceParams { +export type __experimental_GetPaymentSourcesParams = WithOptionalOrgType; + +export type __experimental_InitializePaymentSourceParams = WithOptionalOrgType<{ gateway: 'stripe' | 'paypal'; -} +}>; -export interface __experimental_AddPaymentSourceParams { +export type __experimental_AddPaymentSourceParams = WithOptionalOrgType<{ gateway: 'stripe' | 'paypal'; paymentToken: string; -} +}>; -export interface __experimental_GetPaymentSourcesParams { - orgId?: string; -} +export type __experimental_RemovePaymentSourceParams = WithOptionalOrgType; +export type __experimental_MakeDefaultPaymentSourceParams = WithOptionalOrgType; export interface __experimental_CommercePaymentSourceResource extends ClerkResource { id: string; @@ -89,7 +93,9 @@ export interface __experimental_CommercePaymentSourceResource extends ClerkResou cardType: string; isDefault: boolean; status: __experimental_CommercePaymentSourceStatus; - remove: () => Promise; + walletType: string | undefined; + remove: (params?: __experimental_RemovePaymentSourceParams) => Promise; + makeDefault: (params?: __experimental_MakeDefaultPaymentSourceParams) => Promise; } export interface __experimental_CommerceInitializedPaymentSourceResource extends ClerkResource { @@ -107,9 +113,8 @@ export interface __experimental_CommerceInvoiceResource extends ClerkResource { status: string; } -export type __experimental_GetSubscriptionsParams = ClerkPaginationParams<{ - status?: __experimental_CommerceSubscriptionStatus; -}>; +export type __experimental_GetSubscriptionsParams = WithOptionalOrgType; +export type __experimental_CancelSubscriptionParams = WithOptionalOrgType; export interface __experimental_CommerceSubscriptionResource extends ClerkResource { id: string; @@ -117,7 +122,7 @@ export interface __experimental_CommerceSubscriptionResource extends ClerkResour plan: __experimental_CommercePlanResource; planPeriod: __experimental_CommerceSubscriptionPlanPeriod; status: __experimental_CommerceSubscriptionStatus; - cancel: () => Promise; + cancel: (params: __experimental_CancelSubscriptionParams) => Promise; } export interface __experimental_CommerceMoney { @@ -134,15 +139,14 @@ export interface __experimental_CommerceTotals { totalDueNow?: __experimental_CommerceMoney; } -export interface __experimental_CreateCheckoutParams { +export type __experimental_CreateCheckoutParams = WithOptionalOrgType<{ planId: string; planPeriod: __experimental_CommerceSubscriptionPlanPeriod; - orgId?: string; -} +}>; -export interface __experimental_ConfirmCheckoutParams { +export type __experimental_ConfirmCheckoutParams = WithOptionalOrgType<{ paymentSourceId?: string; -} +}>; export interface __experimental_CommerceCheckoutResource extends ClerkResource { id: string; @@ -155,5 +159,5 @@ export interface __experimental_CommerceCheckoutResource extends ClerkResource { status: string; totals: __experimental_CommerceTotals; subscription?: __experimental_CommerceSubscriptionResource; - confirm: (params?: __experimental_ConfirmCheckoutParams) => Promise<__experimental_CommerceCheckoutResource>; + confirm: (params: __experimental_ConfirmCheckoutParams) => Promise<__experimental_CommerceCheckoutResource>; } diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 2e49afde77c..daee8d69ebd 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -627,7 +627,9 @@ export interface __experimental_CommercePaymentSourceJSON extends ClerkResourceJ last4: string; payment_method: string; card_type: string; + is_default: boolean; status: __experimental_CommercePaymentSourceStatus; + wallet_type: string | null; } export interface __experimental_CommerceInitializedPaymentSourceJSON extends ClerkResourceJSON {