Skip to content

Commit 857110d

Browse files
alexcarpenteraeliox
authored andcommitted
feat(clerk-js,types): Implement billing invoices (#5627)
Co-authored-by: Keiran Flanigan <[email protected]>
1 parent ec55df7 commit 857110d

File tree

21 files changed

+698
-18
lines changed

21 files changed

+698
-18
lines changed

.changeset/silent-beds-invite.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Add invoices data fetching and invoice UI to org and user profile.
+6-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"files": [
3-
{ "path": "./dist/clerk.js", "maxSize": "590kB" },
4-
{ "path": "./dist/clerk.browser.js", "maxSize": "73.88KB" },
3+
{ "path": "./dist/clerk.js", "maxSize": "592.5kB" },
4+
{ "path": "./dist/clerk.browser.js", "maxSize": "74KB" },
55
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
6-
{ "path": "./dist/ui-common*.js", "maxSize": "99.2KB" },
6+
{ "path": "./dist/ui-common*.js", "maxSize": "100KB" },
77
{ "path": "./dist/vendors*.js", "maxSize": "36KB" },
88
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
99
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
@@ -14,16 +14,16 @@
1414
{ "path": "./dist/signin*.js", "maxSize": "12.5KB" },
1515
{ "path": "./dist/signup*.js", "maxSize": "6.75KB" },
1616
{ "path": "./dist/userbutton*.js", "maxSize": "5KB" },
17-
{ "path": "./dist/userprofile*.js", "maxSize": "15KB" },
17+
{ "path": "./dist/userprofile*.js", "maxSize": "16KB" },
1818
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
1919
{ "path": "./dist/onetap*.js", "maxSize": "1KB" },
2020
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
2121
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
2222
{ "path": "./dist/pricingTable*.js", "maxSize": "5.28KB" },
2323
{ "path": "./dist/checkout*.js", "maxSize": "3.05KB" },
2424
{ "path": "./dist/paymentSources*.js", "maxSize": "8.2KB" },
25-
{ "path": "./dist/up-billing-page*.js", "maxSize": "1KB" },
26-
{ "path": "./dist/op-billing-page*.js", "maxSize": "1KB" },
25+
{ "path": "./dist/up-billing-page*.js", "maxSize": "2.5KB" },
26+
{ "path": "./dist/op-billing-page*.js", "maxSize": "2.5KB" },
2727
{ "path": "./dist/sessionTasks*.js", "maxSize": "1KB" }
2828
]
2929
}

packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts

+24
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import type {
22
__experimental_CommerceBillingNamespace,
33
__experimental_CommerceCheckoutJSON,
4+
__experimental_CommerceInvoiceJSON,
5+
__experimental_CommerceInvoiceResource,
46
__experimental_CommercePlanResource,
57
__experimental_CommerceProductJSON,
68
__experimental_CommerceSubscriptionJSON,
79
__experimental_CommerceSubscriptionResource,
810
__experimental_CreateCheckoutParams,
11+
__experimental_GetInvoicesParams,
912
__experimental_GetPlansParams,
1013
__experimental_GetSubscriptionsParams,
1114
ClerkPaginatedResponse,
@@ -14,6 +17,7 @@ import type {
1417
import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
1518
import {
1619
__experimental_CommerceCheckout,
20+
__experimental_CommerceInvoice,
1721
__experimental_CommercePlan,
1822
__experimental_CommerceSubscription,
1923
BaseResource,
@@ -51,6 +55,26 @@ export class __experimental_CommerceBilling implements __experimental_CommerceBi
5155
});
5256
};
5357

58+
getInvoices = async (
59+
params: __experimental_GetInvoicesParams,
60+
): Promise<ClerkPaginatedResponse<__experimental_CommerceInvoiceResource>> => {
61+
const { orgId, ...rest } = params;
62+
63+
return await BaseResource._fetch({
64+
path: orgId ? `/organizations/${orgId}/invoices` : `/me/commerce/invoices`,
65+
method: 'GET',
66+
search: convertPageToOffsetSearchParams(rest),
67+
}).then(res => {
68+
const { data: invoices, total_count } =
69+
res?.response as unknown as ClerkPaginatedResponse<__experimental_CommerceInvoiceJSON>;
70+
71+
return {
72+
total_count,
73+
data: invoices.map(invoice => new __experimental_CommerceInvoice(invoice)),
74+
};
75+
});
76+
};
77+
5478
startCheckout = async (params: __experimental_CreateCheckoutParams) => {
5579
const { orgId, ...rest } = params;
5680
const json = (

packages/clerk-js/src/core/resources/CommerceInvoice.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
__experimental_CommerceInvoiceJSON,
33
__experimental_CommerceInvoiceResource,
4+
__experimental_CommerceInvoiceStatus,
45
__experimental_CommerceTotals,
56
} from '@clerk/types';
67

@@ -13,7 +14,7 @@ export class __experimental_CommerceInvoice extends BaseResource implements __ex
1314
planId!: string;
1415
paymentDueOn!: number;
1516
paidOn!: number;
16-
status!: string;
17+
status!: __experimental_CommerceInvoiceStatus;
1718
totals!: __experimental_CommerceTotals;
1819

1920
constructor(data: __experimental_CommerceInvoiceJSON) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { useInvoicesContext } from '../../contexts';
2+
import { Badge, Box, Dd, descriptors, Dl, Dt, Heading, Spinner, Text } from '../../customizables';
3+
import { Header, LineItems } from '../../elements';
4+
import { useRouter } from '../../router';
5+
import { common } from '../../styledSystem';
6+
import { colors } from '../../utils';
7+
import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible';
8+
9+
export const InvoicePage = () => {
10+
const { params, navigate } = useRouter();
11+
const { getInvoiceById, isLoading } = useInvoicesContext();
12+
const invoice = params.invoiceId ? getInvoiceById(params.invoiceId) : null;
13+
14+
if (isLoading) {
15+
return (
16+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
17+
<Spinner
18+
colorScheme='primary'
19+
sx={{ margin: 'auto', display: 'block' }}
20+
elementDescriptor={descriptors.spinner}
21+
/>
22+
</Box>
23+
);
24+
}
25+
26+
return (
27+
<>
28+
<Header.Root>
29+
<Header.BackLink onClick={() => void navigate('../../', { searchParams: new URLSearchParams('tab=invoices') })}>
30+
<Header.Title
31+
localizationKey='Invoices'
32+
textVariant='h2'
33+
/>
34+
</Header.BackLink>
35+
</Header.Root>
36+
<Box
37+
elementDescriptor={descriptors.invoiceRoot}
38+
sx={t => ({
39+
display: 'flex',
40+
flexDirection: 'column',
41+
gap: t.space.$4,
42+
borderTopWidth: t.borderWidths.$normal,
43+
borderTopStyle: t.borderStyles.$solid,
44+
borderTopColor: t.colors.$neutralAlpha100,
45+
marginBlockStart: t.space.$4,
46+
paddingBlockStart: t.space.$4,
47+
})}
48+
>
49+
{!invoice ? (
50+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
51+
<Text>Invoice not found</Text>
52+
</Box>
53+
) : (
54+
<Box
55+
elementDescriptor={descriptors.invoiceCard}
56+
sx={t => ({
57+
borderWidth: t.borderWidths.$normal,
58+
borderStyle: t.borderStyles.$solid,
59+
borderColor: t.colors.$neutralAlpha100,
60+
borderRadius: t.radii.$lg,
61+
overflow: 'hidden',
62+
})}
63+
>
64+
<Box
65+
elementDescriptor={descriptors.invoiceHeader}
66+
as='header'
67+
sx={t => ({
68+
padding: t.space.$4,
69+
background: common.mergedColorsBackground(
70+
colors.setAlpha(t.colors.$colorBackground, 1),
71+
t.colors.$neutralAlpha50,
72+
),
73+
borderBlockEndWidth: t.borderWidths.$normal,
74+
borderBlockEndStyle: t.borderStyles.$solid,
75+
borderBlockEndColor: t.colors.$neutralAlpha100,
76+
})}
77+
>
78+
<Box
79+
sx={{
80+
display: 'flex',
81+
justifyContent: 'space-between',
82+
alignItems: 'center',
83+
}}
84+
>
85+
<Heading
86+
textVariant='h2'
87+
elementDescriptor={descriptors.invoiceTitle}
88+
>
89+
{truncateWithEndVisible(invoice.id)}
90+
</Heading>
91+
<Badge
92+
elementDescriptor={descriptors.invoiceBadge}
93+
colorScheme={
94+
invoice.status === 'paid' ? 'success' : invoice.status === 'unpaid' ? 'warning' : 'danger'
95+
}
96+
sx={{ textTransform: 'capitalize' }}
97+
>
98+
{invoice.status}
99+
</Badge>
100+
</Box>
101+
<Dl
102+
elementDescriptor={descriptors.invoiceDetails}
103+
sx={t => ({
104+
display: 'flex',
105+
justifyContent: 'space-between',
106+
marginBlockStart: t.space.$3,
107+
})}
108+
>
109+
<Box elementDescriptor={descriptors.invoiceDetailsItem}>
110+
<Dt elementDescriptor={descriptors.invoiceDetailsItemTitle}>
111+
<Text
112+
colorScheme='secondary'
113+
variant='body'
114+
>
115+
Created on
116+
</Text>
117+
</Dt>
118+
<Dd elementDescriptor={descriptors.invoiceDetailsItemValue}>
119+
<Text variant='subtitle'>{new Date(invoice.paymentDueOn).toLocaleDateString()}</Text>
120+
</Dd>
121+
</Box>
122+
<Box
123+
elementDescriptor={descriptors.invoiceDetailsItem}
124+
sx={{
125+
textAlign: 'right',
126+
}}
127+
>
128+
<Dt elementDescriptor={descriptors.invoiceDetailsItemTitle}>
129+
<Text
130+
colorScheme='secondary'
131+
variant='body'
132+
>
133+
Due on
134+
</Text>
135+
</Dt>
136+
<Dd elementDescriptor={descriptors.invoiceDetailsItemValue}>
137+
<Text variant='subtitle'>{new Date(invoice.paymentDueOn).toLocaleDateString()}</Text>
138+
</Dd>
139+
</Box>
140+
</Dl>
141+
</Box>
142+
<Box
143+
elementDescriptor={descriptors.invoiceContent}
144+
sx={t => ({
145+
padding: t.space.$4,
146+
})}
147+
>
148+
<LineItems.Root>
149+
<LineItems.Group>
150+
<LineItems.Title title='Plan' />
151+
<LineItems.Description
152+
text={`${invoice.totals.grandTotal.currencySymbol}${invoice.totals.grandTotal.amountFormatted}`}
153+
suffix='per month'
154+
/>
155+
</LineItems.Group>
156+
<LineItems.Group
157+
variant='secondary'
158+
borderTop
159+
>
160+
<LineItems.Title title='Subtotal' />
161+
<LineItems.Description
162+
text={`${invoice.totals.grandTotal.currencySymbol}${invoice.totals.grandTotal.amountFormatted}`}
163+
/>
164+
</LineItems.Group>
165+
<LineItems.Group variant='secondary'>
166+
<LineItems.Title title='Tax' />
167+
<LineItems.Description
168+
text={`${invoice.totals.grandTotal.currencySymbol}${invoice.totals.grandTotal.amountFormatted}`}
169+
/>
170+
</LineItems.Group>
171+
<LineItems.Group borderTop>
172+
<LineItems.Title title='Total due' />
173+
<LineItems.Description
174+
text={`${invoice.totals.grandTotal.currencySymbol}${invoice.totals.grandTotal.amountFormatted}`}
175+
prefix='USD'
176+
/>
177+
</LineItems.Group>
178+
</LineItems.Root>
179+
</Box>
180+
</Box>
181+
)}
182+
</Box>
183+
</>
184+
);
185+
};

0 commit comments

Comments
 (0)