Skip to content

Commit 0f439b6

Browse files
simeng-liCopilot
andauthored
feat(console): add billing history page (#8129)
* feat(console): add billing history page add billing history page * fix(cloud): fix index key fix index key Co-authored-by: Copilot <[email protected]> * refactor(console): add useMemo wrapper add useMemo wrapper --------- Co-authored-by: Copilot <[email protected]>
1 parent 9ec3f7b commit 0f439b6

File tree

6 files changed

+119
-22
lines changed

6 files changed

+119
-22
lines changed

packages/connectors/connector-logto-email/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"access": "public"
5353
},
5454
"devDependencies": {
55-
"@logto/cloud": "0.2.5-2553f41",
55+
"@logto/cloud": "0.2.5-acaf5b8",
5656
"@silverhand/eslint-config": "6.0.1",
5757
"@silverhand/ts-config": "6.0.0",
5858
"@types/node": "^22.14.0",

packages/console/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@fontsource/roboto-mono": "^5.0.0",
2929
"@inkeep/cxkit-react": "^0.5.66",
3030
"@jest/types": "^29.5.0",
31-
"@logto/cloud": "0.2.5-2553f41",
31+
"@logto/cloud": "0.2.5-acaf5b8",
3232
"@logto/connector-kit": "workspace:^",
3333
"@logto/core-kit": "workspace:^",
3434
"@logto/language-kit": "workspace:^",

packages/console/src/cloud/types/router.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,7 @@ export type LogtoEnterpriseResponse = GetArrayElementType<
7373
export type LogtoEnterpriseSubscriptionResponse = GuardedResponse<
7474
GetRoutes['/api/me/logto-enterprises/:id']
7575
>;
76+
77+
export type LogtoEnterpriseSubscriptionInvoiceResponse = GetArrayElementType<
78+
GuardedResponse<GetRoutes['/api/me/logto-enterprises/:id/invoices']>['invoices']
79+
>;
Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,98 @@
1+
import { type ResponseError } from '@withtyped/client';
2+
import dayjs from 'dayjs';
3+
import { useCallback, useMemo } from 'react';
4+
import { useParams } from 'react-router-dom';
5+
import useSWR from 'swr';
6+
7+
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
8+
import { type LogtoEnterpriseSubscriptionInvoiceResponse } from '@/cloud/types/router';
9+
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
10+
import ItemPreview from '@/components/ItemPreview';
11+
import PageMeta from '@/components/PageMeta';
12+
import DynamicT from '@/ds-components/DynamicT';
13+
import Table from '@/ds-components/Table';
14+
import InvoiceStatusTag from '@/pages/TenantSettings/BillingHistory/InvoiceStatusTag';
15+
import { formatPeriod } from '@/utils/subscription';
16+
117
function BillingHistory() {
2-
return <div>Enterprise Billing History Page</div>;
18+
const { logtoEnterpriseId = '' } = useParams();
19+
const cloudApi = useCloudApi();
20+
21+
const { data, isLoading } = useSWR<
22+
{ invoices: LogtoEnterpriseSubscriptionInvoiceResponse[] },
23+
ResponseError
24+
>(logtoEnterpriseId && `/api/me/logto-enterprises/${logtoEnterpriseId}/invoices`, async () =>
25+
cloudApi.get(`/api/me/logto-enterprises/:id/invoices`, {
26+
params: { id: logtoEnterpriseId },
27+
})
28+
);
29+
30+
// Filter out draft invoices
31+
const invoices = useMemo(
32+
() => (data?.invoices ?? []).filter(({ status }) => status !== 'draft'),
33+
[data?.invoices]
34+
);
35+
36+
const openStripeHostedInvoicePage = useCallback(
37+
async (invoiceId: string) => {
38+
const { hostedInvoiceUrl } = await cloudApi.get(
39+
'/api/me/logto-enterprises/:enterpriseId/invoices/:invoiceId/hosted-invoice-url',
40+
{
41+
params: { enterpriseId: logtoEnterpriseId, invoiceId },
42+
}
43+
);
44+
45+
window.open(hostedInvoiceUrl, '_blank');
46+
},
47+
[cloudApi, logtoEnterpriseId]
48+
);
49+
50+
return (
51+
<div>
52+
<PageMeta titleKey={['tenants.tabs.billing_history', 'tenants.title']} />
53+
<Table
54+
rowGroups={[{ key: 'invoices', data: invoices }]}
55+
rowIndexKey="id"
56+
columns={[
57+
{
58+
title: <DynamicT forKey="subscription.billing_history.invoice_column" />,
59+
dataIndex: 'basicSkuId',
60+
render: ({ periodStart, periodEnd }) => {
61+
return (
62+
<ItemPreview title={formatPeriod({ periodStart, periodEnd, displayYear: true })} />
63+
);
64+
},
65+
},
66+
{
67+
title: <DynamicT forKey="subscription.billing_history.status_column" />,
68+
dataIndex: 'status',
69+
render: ({ status }) => {
70+
return <InvoiceStatusTag status={status} />;
71+
},
72+
},
73+
{
74+
title: <DynamicT forKey="subscription.billing_history.amount_column" />,
75+
dataIndex: 'amountPaid',
76+
render: ({ amountPaid }) => {
77+
return `$${(amountPaid / 100).toFixed(2)}`;
78+
},
79+
},
80+
{
81+
title: <DynamicT forKey="subscription.billing_history.invoice_created_date_column" />,
82+
dataIndex: 'createdAt',
83+
render: ({ createdAt }) => {
84+
return dayjs(createdAt).format('MMMM DD, YYYY');
85+
},
86+
},
87+
]}
88+
isLoading={isLoading}
89+
placeholder={<EmptyDataPlaceholder />}
90+
rowClickHandler={({ id }) => {
91+
void openStripeHostedInvoicePage(id);
92+
}}
93+
/>
94+
</div>
95+
);
396
}
497

598
export default BillingHistory;

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
"zod": "3.24.3"
102102
},
103103
"devDependencies": {
104-
"@logto/cloud": "0.2.5-2553f41",
104+
"@logto/cloud": "0.2.5-acaf5b8",
105105
"@silverhand/eslint-config": "6.0.1",
106106
"@silverhand/ts-config": "6.0.0",
107107
"@types/adm-zip": "^0.5.5",

pnpm-lock.yaml

Lines changed: 18 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)