Skip to content

feat(clerk-js): Add payment sources to OrgProfile, org-specific commerce endpoints #5554

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/great-moons-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/types': patch
---

Add Payment Sources to `<OrgProfile />`, hook up all org-related payment source and checkout methods to the org-specific endpoints
6 changes: 3 additions & 3 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -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" },
Expand All @@ -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" }
Expand Down
22 changes: 15 additions & 7 deletions packages/clerk-js/src/core/modules/commerce/Commerce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
ClerkPaginatedResponse,
} from '@clerk/types';

import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
import {
__experimental_CommerceInitializedPaymentSource,
__experimental_CommercePaymentSource,
Expand All @@ -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)),
Expand Down
16 changes: 12 additions & 4 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,10 +31,15 @@ export class __experimental_CommerceBilling implements __experimental_CommerceBi
return defaultProduct?.plans.map(plan => new __experimental_CommercePlan(plan)) || [];
};

getSubscriptions = async (): Promise<ClerkPaginatedResponse<__experimental_CommerceSubscriptionResource>> => {
getSubscriptions = async (
params: __experimental_GetSubscriptionsParams,
): Promise<ClerkPaginatedResponse<__experimental_CommerceSubscriptionResource>> => {
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>;
Expand All @@ -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;

Expand Down
11 changes: 6 additions & 5 deletions packages/clerk-js/src/core/resources/CommerceCheckout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,10 +53,13 @@ export class __experimental_CommerceCheckout extends BaseResource implements __e
return this;
}

confirm = (params?: __experimental_ConfirmCheckoutParams): Promise<this> => {
confirm = (params: __experimental_ConfirmCheckoutParams): Promise<this> => {
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,
});
};
}
27 changes: 24 additions & 3 deletions packages/clerk-js/src/core/resources/CommercePaymentSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type {
__experimental_CommercePaymentSourceJSON,
__experimental_CommercePaymentSourceResource,
__experimental_CommercePaymentSourceStatus,
__experimental_MakeDefaultPaymentSourceParams,
__experimental_RemovePaymentSourceParams,
DeletedObjectJSON,
} from '@clerk/types';

Expand All @@ -19,6 +21,7 @@ export class __experimental_CommercePaymentSource
cardType!: string;
isDefault!: boolean;
status!: __experimental_CommercePaymentSourceStatus;
walletType: string | undefined;

constructor(data: __experimental_CommercePaymentSourceJSON) {
super();
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
__experimental_CancelSubscriptionParams,
__experimental_CommerceSubscriptionJSON,
__experimental_CommerceSubscriptionPlanPeriod,
__experimental_CommerceSubscriptionResource,
Expand Down Expand Up @@ -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;
Expand Down
15 changes: 5 additions & 10 deletions packages/clerk-js/src/core/resources/Organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,16 +240,11 @@ export class Organization extends BaseResource implements OrganizationResource {
__experimental_getSubscriptions = async (
getSubscriptionsParams?: __experimental_GetSubscriptionsParams,
): Promise<ClerkPaginatedResponse<__experimental_CommerceSubscriptionResource>> => {
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>;

Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ const Components = (props: ComponentsProps) => {
<Checkout
planId={checkoutDrawer.props.planId}
planPeriod={checkoutDrawer.props.planPeriod}
orgId={checkoutDrawer.props.orgId}
subscriberType={checkoutDrawer.props.subscriberType}
onSubscriptionComplete={checkoutDrawer.props.onSubscriptionComplete}
/>
</LazyDrawerRenderer>
Expand Down
1 change: 1 addition & 0 deletions packages/clerk-js/src/ui/components/Checkout/Checkout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const __experimental_Checkout = (props: __experimental_CheckoutProps) =>
<__experimental_CheckoutContext.Provider
value={{
componentName: 'Checkout',
...props,
}}
>
<Drawer.Content>
Expand Down
41 changes: 28 additions & 13 deletions packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useClerk } from '@clerk/shared/react';
import { useClerk, useOrganization } from '@clerk/shared/react';
import type {
__experimental_CommerceCheckoutResource,
__experimental_CommerceMoney,
Expand All @@ -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';
Expand Down Expand Up @@ -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<ClerkRuntimeError | ClerkAPIError | string | undefined>();

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);
})
Expand Down Expand Up @@ -176,16 +189,18 @@ const CheckoutFormElements = ({
{/* TODO(@Commerce): needs localization */}
<Disclosure.Trigger text='Add a New Payment Source' />
<Disclosure.Content>
<AddPaymentSource
checkout={checkout}
onSuccess={onAddPaymentSourceSuccess}
submitLabel={localizationKeys(
'userProfile.__experimental_billingPage.paymentSourcesSection.formButtonPrimary__pay',
{
amount: `${(checkout.totals.totalDueNow || checkout.totals.grandTotal).currencySymbol}${(checkout.totals.totalDueNow || checkout.totals.grandTotal).amountFormatted}`,
},
)}
/>
<__experimental_PaymentSourcesContext.Provider value={{ componentName: 'PaymentSources', subscriberType }}>
<AddPaymentSource
checkout={checkout}
onSuccess={onAddPaymentSourceSuccess}
submitLabel={localizationKeys(
'userProfile.__experimental_billingPage.paymentSourcesSection.formButtonPrimary__pay',
{
amount: `${(checkout.totals.totalDueNow || checkout.totals.grandTotal).currencySymbol}${(checkout.totals.totalDueNow || checkout.totals.grandTotal).amountFormatted}`,
},
)}
/>
</__experimental_PaymentSourcesContext.Provider>
</Disclosure.Content>
</Disclosure.Root>
</Col>
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { __experimental_PricingTableContext } from '../../contexts';
import { __experimental_PaymentSourcesContext, __experimental_PricingTableContext } from '../../contexts';
import { Col, descriptors, localizationKeys } from '../../customizables';
import {
Card,
Expand All @@ -11,6 +11,7 @@ import {
useCardState,
withCardStateProvider,
} from '../../elements';
import { __experimental_PaymentSources } from '../PaymentSources/PaymentSources';
import { __experimental_PricingTable } from '../PricingTable';

export const OrganizationBillingPage = withCardStateProvider(() => {
Expand Down Expand Up @@ -58,7 +59,13 @@ export const OrganizationBillingPage = withCardStateProvider(() => {
</__experimental_PricingTableContext.Provider>
</TabPanel>
<TabPanel sx={{ width: '100%' }}>Invoices</TabPanel>
<TabPanel sx={{ width: '100%' }}>Payment Sources</TabPanel>
<TabPanel sx={{ width: '100%' }}>
<__experimental_PaymentSourcesContext.Provider
value={{ componentName: 'PaymentSources', subscriberType: 'org' }}
>
<__experimental_PaymentSources />
</__experimental_PaymentSourcesContext.Provider>
</TabPanel>
</TabPanels>
</Tabs>
</Col>
Expand Down
Loading