Skip to content

feat(clerk-js,types): Implement billing invoices #5627

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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3de5121
fix(clerk-js): Reduce line truncation maxLength
alexcarpenter Apr 14, 2025
007bd1d
add changeset
alexcarpenter Apr 14, 2025
d08dcac
InvoicesList
alexcarpenter Apr 14, 2025
82f133e
Create InvoicePage.tsx
alexcarpenter Apr 14, 2025
3723195
invoice page
alexcarpenter Apr 14, 2025
48441a4
Merge branch 'main' into alexcarpenter/com-182-build-out-invoices-tab…
alexcarpenter Apr 14, 2025
8daba80
Delete .changeset/silly-bats-open.md
alexcarpenter Apr 14, 2025
953dcfd
add changeset
alexcarpenter Apr 14, 2025
16974f7
Merge branch 'alexcarpenter/com-182-build-out-invoices-tab-within-use…
alexcarpenter Apr 14, 2025
dd2f683
Update InvoicesList.tsx
alexcarpenter Apr 14, 2025
9b4cb44
added preliminary invoice fetching / context\
aeliox Apr 15, 2025
e5d59f8
wip
alexcarpenter Apr 15, 2025
e283187
remove next billing attempt
alexcarpenter Apr 15, 2025
337c685
wip
alexcarpenter Apr 15, 2025
a62aa39
fix routing
alexcarpenter Apr 15, 2025
8d4bf97
truncate
alexcarpenter Apr 15, 2025
fc64f4d
title
alexcarpenter Apr 15, 2025
ad2bec8
lazy load
alexcarpenter Apr 15, 2025
b4535ac
move to components folder
alexcarpenter Apr 15, 2025
e217423
add to org profile
alexcarpenter Apr 15, 2025
8a0005e
lazy load
alexcarpenter Apr 15, 2025
22f5787
pass subscriber type
alexcarpenter Apr 15, 2025
a55cbcc
Update bundlewatch.config.json
alexcarpenter Apr 15, 2025
6e81112
add descriptors
alexcarpenter Apr 15, 2025
58d0d25
add back link
alexcarpenter Apr 15, 2025
e8314fe
Merge branch 'main' into alexcarpenter/com-182-build-out-invoices-tab…
alexcarpenter Apr 15, 2025
e740773
fix
alexcarpenter Apr 15, 2025
ff7b0e5
Update bundlewatch.config.json
alexcarpenter Apr 15, 2025
f5dd98b
remove lazy loading
alexcarpenter Apr 15, 2025
00dfecd
Update .changeset/silent-beds-invite.md
alexcarpenter Apr 15, 2025
6c08dbb
Merge branch 'main' into alexcarpenter/com-182-build-out-invoices-tab…
alexcarpenter Apr 16, 2025
992406c
Merge branch 'alexcarpenter/com-182-build-out-invoices-tab-within-use…
alexcarpenter Apr 16, 2025
2831b26
Update bundlewatch.config.json
alexcarpenter Apr 16, 2025
967e481
Merge branch 'main' into alexcarpenter/com-182-build-out-invoices-tab…
alexcarpenter Apr 16, 2025
0f9de4d
Update bundlewatch.config.json
alexcarpenter Apr 16, 2025
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
5 changes: 5 additions & 0 deletions .changeset/silent-beds-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Add invoices data fetching and invoice UI to org and user profile.
12 changes: 6 additions & 6 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": "73.88KB" },
{ "path": "./dist/clerk.js", "maxSize": "592.5kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "74KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "99.2KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "100KB" },
{ "path": "./dist/vendors*.js", "maxSize": "36KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
Expand All @@ -14,16 +14,16 @@
{ "path": "./dist/signin*.js", "maxSize": "12.5KB" },
{ "path": "./dist/signup*.js", "maxSize": "6.75KB" },
{ "path": "./dist/userbutton*.js", "maxSize": "5KB" },
{ "path": "./dist/userprofile*.js", "maxSize": "15KB" },
{ "path": "./dist/userprofile*.js", "maxSize": "16KB" },
{ "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/pricingTable*.js", "maxSize": "5.28KB" },
{ "path": "./dist/checkout*.js", "maxSize": "3.05KB" },
{ "path": "./dist/paymentSources*.js", "maxSize": "8.2KB" },
{ "path": "./dist/up-billing-page*.js", "maxSize": "1KB" },
{ "path": "./dist/op-billing-page*.js", "maxSize": "1KB" },
{ "path": "./dist/up-billing-page*.js", "maxSize": "2.5KB" },
{ "path": "./dist/op-billing-page*.js", "maxSize": "2.5KB" },
{ "path": "./dist/sessionTasks*.js", "maxSize": "1KB" }
]
}
24 changes: 24 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type {
__experimental_CommerceBillingNamespace,
__experimental_CommerceCheckoutJSON,
__experimental_CommerceInvoiceJSON,
__experimental_CommerceInvoiceResource,
__experimental_CommercePlanResource,
__experimental_CommerceProductJSON,
__experimental_CommerceSubscriptionJSON,
__experimental_CommerceSubscriptionResource,
__experimental_CreateCheckoutParams,
__experimental_GetInvoicesParams,
__experimental_GetPlansParams,
__experimental_GetSubscriptionsParams,
ClerkPaginatedResponse,
Expand All @@ -14,6 +17,7 @@ import type {
import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOffsetSearchParams';
import {
__experimental_CommerceCheckout,
__experimental_CommerceInvoice,
__experimental_CommercePlan,
__experimental_CommerceSubscription,
BaseResource,
Expand Down Expand Up @@ -51,6 +55,26 @@ export class __experimental_CommerceBilling implements __experimental_CommerceBi
});
};

getInvoices = async (
params: __experimental_GetInvoicesParams,
): Promise<ClerkPaginatedResponse<__experimental_CommerceInvoiceResource>> => {
const { orgId, ...rest } = params;

return await BaseResource._fetch({
path: orgId ? `/organizations/${orgId}/invoices` : `/me/commerce/invoices`,
method: 'GET',
search: convertPageToOffsetSearchParams(rest),
}).then(res => {
const { data: invoices, total_count } =
res?.response as unknown as ClerkPaginatedResponse<__experimental_CommerceInvoiceJSON>;

return {
total_count,
data: invoices.map(invoice => new __experimental_CommerceInvoice(invoice)),
};
});
};

startCheckout = async (params: __experimental_CreateCheckoutParams) => {
const { orgId, ...rest } = params;
const json = (
Expand Down
3 changes: 2 additions & 1 deletion packages/clerk-js/src/core/resources/CommerceInvoice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
__experimental_CommerceInvoiceJSON,
__experimental_CommerceInvoiceResource,
__experimental_CommerceInvoiceStatus,
__experimental_CommerceTotals,
} from '@clerk/types';

Expand All @@ -13,7 +14,7 @@ export class __experimental_CommerceInvoice extends BaseResource implements __ex
planId!: string;
paymentDueOn!: number;
paidOn!: number;
status!: string;
status!: __experimental_CommerceInvoiceStatus;
totals!: __experimental_CommerceTotals;

constructor(data: __experimental_CommerceInvoiceJSON) {
Expand Down
185 changes: 185 additions & 0 deletions packages/clerk-js/src/ui/components/Invoices/InvoicePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { useInvoicesContext } from '../../contexts';
import { Badge, Box, Dd, descriptors, Dl, Dt, Heading, Spinner, Text } from '../../customizables';
import { Header, LineItems } from '../../elements';
import { useRouter } from '../../router';
import { common } from '../../styledSystem';
import { colors } from '../../utils';
import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible';

export const InvoicePage = () => {
const { params, navigate } = useRouter();
const { getInvoiceById, isLoading } = useInvoicesContext();
const invoice = params.invoiceId ? getInvoiceById(params.invoiceId) : null;

if (isLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
<Spinner
colorScheme='primary'
sx={{ margin: 'auto', display: 'block' }}
elementDescriptor={descriptors.spinner}
/>
</Box>
);
}

return (
<>
<Header.Root>
<Header.BackLink onClick={() => void navigate('../../', { searchParams: new URLSearchParams('tab=invoices') })}>
<Header.Title
localizationKey='Invoices'
textVariant='h2'
/>
</Header.BackLink>
</Header.Root>
<Box
elementDescriptor={descriptors.invoiceRoot}
sx={t => ({
display: 'flex',
flexDirection: 'column',
gap: t.space.$4,
borderTopWidth: t.borderWidths.$normal,
borderTopStyle: t.borderStyles.$solid,
borderTopColor: t.colors.$neutralAlpha100,
marginBlockStart: t.space.$4,
paddingBlockStart: t.space.$4,
})}
>
{!invoice ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
<Text>Invoice not found</Text>
</Box>
) : (
<Box
elementDescriptor={descriptors.invoiceCard}
sx={t => ({
borderWidth: t.borderWidths.$normal,
borderStyle: t.borderStyles.$solid,
borderColor: t.colors.$neutralAlpha100,
borderRadius: t.radii.$lg,
overflow: 'hidden',
})}
>
<Box
elementDescriptor={descriptors.invoiceHeader}
as='header'
sx={t => ({
padding: t.space.$4,
background: common.mergedColorsBackground(
colors.setAlpha(t.colors.$colorBackground, 1),
t.colors.$neutralAlpha50,
),
borderBlockEndWidth: t.borderWidths.$normal,
borderBlockEndStyle: t.borderStyles.$solid,
borderBlockEndColor: t.colors.$neutralAlpha100,
})}
>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Heading
textVariant='h2'
elementDescriptor={descriptors.invoiceTitle}
>
{truncateWithEndVisible(invoice.id)}
</Heading>
<Badge
elementDescriptor={descriptors.invoiceBadge}
colorScheme={
invoice.status === 'paid' ? 'success' : invoice.status === 'unpaid' ? 'warning' : 'danger'
}
sx={{ textTransform: 'capitalize' }}
>
{invoice.status}
</Badge>
</Box>
<Dl
elementDescriptor={descriptors.invoiceDetails}
sx={t => ({
display: 'flex',
justifyContent: 'space-between',
marginBlockStart: t.space.$3,
})}
>
<Box elementDescriptor={descriptors.invoiceDetailsItem}>
<Dt elementDescriptor={descriptors.invoiceDetailsItemTitle}>
<Text
colorScheme='secondary'
variant='body'
>
Created on
</Text>
</Dt>
<Dd elementDescriptor={descriptors.invoiceDetailsItemValue}>
<Text variant='subtitle'>{new Date(invoice.paymentDueOn).toLocaleDateString()}</Text>
</Dd>
</Box>
<Box
elementDescriptor={descriptors.invoiceDetailsItem}
sx={{
textAlign: 'right',
}}
>
<Dt elementDescriptor={descriptors.invoiceDetailsItemTitle}>
<Text
colorScheme='secondary'
variant='body'
>
Due on
</Text>
</Dt>
<Dd elementDescriptor={descriptors.invoiceDetailsItemValue}>
<Text variant='subtitle'>{new Date(invoice.paymentDueOn).toLocaleDateString()}</Text>
</Dd>
</Box>
</Dl>
</Box>
<Box
elementDescriptor={descriptors.invoiceContent}
sx={t => ({
padding: t.space.$4,
})}
>
<LineItems.Root>
<LineItems.Group>
<LineItems.Title title='Plan' />
<LineItems.Description
text={`${invoice.totals.grandTotal.currencySymbol}${invoice.totals.grandTotal.amountFormatted}`}
suffix='per month'
/>
</LineItems.Group>
<LineItems.Group
variant='secondary'
borderTop
>
<LineItems.Title title='Subtotal' />
<LineItems.Description
text={`${invoice.totals.grandTotal.currencySymbol}${invoice.totals.grandTotal.amountFormatted}`}
/>
</LineItems.Group>
<LineItems.Group variant='secondary'>
<LineItems.Title title='Tax' />
<LineItems.Description
text={`${invoice.totals.grandTotal.currencySymbol}${invoice.totals.grandTotal.amountFormatted}`}
/>
</LineItems.Group>
<LineItems.Group borderTop>
<LineItems.Title title='Total due' />
<LineItems.Description
text={`${invoice.totals.grandTotal.currencySymbol}${invoice.totals.grandTotal.amountFormatted}`}
prefix='USD'
/>
</LineItems.Group>
</LineItems.Root>
</Box>
</Box>
)}
</Box>
</>
);
};
Loading