Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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/slow-foxes-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/types': patch
---

Refactor billing statement page and payment attempt page data loading
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{ "path": "./dist/clerk.js", "maxSize": "618KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "74KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "115.08KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "55.2KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand All @@ -23,7 +23,7 @@
{ "path": "./dist/waitlist*.js", "maxSize": "1.5KB" },
{ "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" },
{ "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.45KB" },
{ "path": "./dist/checkout*.js", "maxSize": "8.5KB" },
{ "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" },
{ "path": "./dist/op-billing-page*.js", "maxSize": "3.0KB" },
{ "path": "./dist/up-plans-page*.js", "maxSize": "1.0KB" },
Expand Down
22 changes: 22 additions & 0 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ export class CommerceBilling implements CommerceBillingNamespace {
});
};

getStatement = async (params: { id: string; orgId?: string }): Promise<CommerceStatementResource> => {
const statement = (
await BaseResource._fetch({
path: params.orgId
? `/organizations/${params.orgId}/commerce/statements/${params.id}`
: `/me/commerce/statements/${params.id}`,
method: 'GET',
})
)?.response as unknown as CommerceStatementJSON;
return new CommerceStatement(statement);
};

getPaymentAttempts = async (
params: GetPaymentAttemptsParams,
): Promise<ClerkPaginatedResponse<CommercePaymentResource>> => {
Expand All @@ -122,6 +134,16 @@ export class CommerceBilling implements CommerceBillingNamespace {
});
};

getPaymentAttempt = async (params: { id: string; orgId?: string }): Promise<CommercePaymentResource> => {
const paymentAttempt = (await BaseResource._fetch({
path: params.orgId
? `/organizations/${params.orgId}/commerce/payment_attempts/${params.id}`
: `/me/commerce/payment_attempts/${params.id}`,
method: 'GET',
})) as unknown as CommercePaymentJSON;
return new CommercePayment(paymentAttempt);
};

startCheckout = async (params: CreateCheckoutParams) => {
const { orgId, ...rest } = params;
const json = (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useClerk, useOrganization } from '@clerk/shared/react';
import useSWR from 'swr';

import { Alert } from '@/ui/elements/Alert';
import { Header } from '@/ui/elements/Header';
import { LineItems } from '@/ui/elements/LineItems';
import { formatDate } from '@/ui/utils/formatDate';
import { truncateWithEndVisible } from '@/ui/utils/truncateTextWithEndVisible';

import { usePaymentAttemptsContext, useStatements } from '../../contexts';
import { useSubscriberTypeLocalizationRoot } from '../../contexts/components';
import { useSubscriberTypeContext, useSubscriberTypeLocalizationRoot } from '../../contexts/components';
import {
Badge,
Box,
Expand All @@ -16,18 +19,39 @@ import {
Span,
Spinner,
Text,
useLocalizations,
} from '../../customizables';
import { useClipboard } from '../../hooks';
import { Check, Copy } from '../../icons';
import { useRouter } from '../../router';

export const PaymentAttemptPage = () => {
const { params, navigate } = useRouter();
const { isLoading } = useStatements();
const { getPaymentAttemptById } = usePaymentAttemptsContext();
Comment thread
aeliox marked this conversation as resolved.
const subscriberType = useSubscriberTypeContext();
const { organization } = useOrganization();
const localizationRoot = useSubscriberTypeLocalizationRoot();
const { t, translateError } = useLocalizations();
const clerk = useClerk();

const {
data: paymentAttempt,
isLoading,
error,
} = useSWR(
params.paymentAttemptId
? {
type: 'payment-attempt',
id: params.paymentAttemptId,
orgId: subscriberType === 'organization' ? organization?.id : undefined,
}
: null,
() =>
clerk.billing.getPaymentAttempt({
id: params.paymentAttemptId,
orgId: subscriberType === 'organization' ? organization?.id : undefined,
}),
);

const paymentAttempt = params.paymentAttemptId ? getPaymentAttemptById(params.paymentAttemptId) : null;
const subscriptionItem = paymentAttempt?.subscriptionItem;

if (isLoading) {
Expand Down Expand Up @@ -61,10 +85,15 @@ export const PaymentAttemptPage = () => {
</Header.BackLink>
</Header.Root>
{!paymentAttempt ? (
<Text
localizationKey={localizationKeys(`${localizationRoot}.billingPage.paymentHistorySection.notFound`)}
sx={{ textAlign: 'center' }}
/>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
<Alert
variant='danger'
colorScheme='danger'
>
{translateError(error.errors[0]) ||
t(localizationKeys(`${localizationRoot}.billingPage.paymentHistorySection.notFound`))}
</Alert>
</Box>
) : (
<Box
elementDescriptor={descriptors.paymentAttemptRoot}
Expand Down
8 changes: 6 additions & 2 deletions packages/clerk-js/src/ui/components/Statements/Statement.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from 'react';

import type { LocalizationKey } from '../../customizables';
import { Badge, Box, Button, descriptors, Heading, Icon, Span, Text } from '../../customizables';
import { useClipboard } from '../../hooks';
Expand Down Expand Up @@ -279,7 +281,7 @@ function SectionContentDetailsListItem({
icon?: React.ReactNode;
label: string | LocalizationKey;
labelIcon?: React.ComponentType;
value: string | LocalizationKey;
value: string | LocalizationKey | React.ReactElement;
valueTruncated?: boolean;
valueCopyable?: boolean;
}) {
Expand Down Expand Up @@ -348,12 +350,14 @@ function SectionContentDetailsListItem({
{valueTruncated ? truncateWithEndVisible(value) : value}
</Text>
</>
) : React.isValidElement(value) ? (
value
) : (
<Text
elementDescriptor={descriptors.statementSectionContentDetailsListItemValue}
colorScheme='secondary'
variant='caption'
localizationKey={value}
localizationKey={value as LocalizationKey}
/>
)}
</Span>
Expand Down
80 changes: 66 additions & 14 deletions packages/clerk-js/src/ui/components/Statements/StatementPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,51 @@
import { useClerk, useOrganization } from '@clerk/shared/react';
import useSWR from 'swr';

import { Alert } from '@/ui/elements/Alert';
import { Header } from '@/ui/elements/Header';
import { formatDate } from '@/ui/utils/formatDate';

import { useStatements, useStatementsContext, useSubscriberTypeLocalizationRoot } from '../../contexts';
import { Box, descriptors, localizationKeys, Spinner, Text, useLocalizations } from '../../customizables';
import { Plus, RotateLeftRight } from '../../icons';
import { useSubscriberTypeContext, useSubscriberTypeLocalizationRoot } from '../../contexts/components';
import {
Box,
descriptors,
Icon,
localizationKeys,
SimpleButton,
Span,
Spinner,
useLocalizations,
} from '../../customizables';
import { ArrowRightIcon, Plus, RotateLeftRight } from '../../icons';
import { useRouter } from '../../router';
import { Statement } from './Statement';

export const StatementPage = () => {
const { params, navigate } = useRouter();
const { isLoading } = useStatements();
const { getStatementById } = useStatementsContext();
const subscriberType = useSubscriberTypeContext();
const { organization } = useOrganization();
const localizationRoot = useSubscriberTypeLocalizationRoot();
const { t } = useLocalizations();
const statement = params.statementId ? getStatementById(params.statementId) : null;
const { t, translateError } = useLocalizations();
const clerk = useClerk();

const {
data: statement,
isLoading,
error,
} = useSWR(
params.statementId
? {
type: 'statement',
id: params.statementId,
orgId: subscriberType === 'organization' ? organization?.id : undefined,
}
: null,
() =>
clerk.billing.getStatement({
id: params.statementId,
orgId: subscriberType === 'organization' ? organization?.id : undefined,
}),
);

if (isLoading) {
return (
Expand Down Expand Up @@ -48,10 +80,15 @@ export const StatementPage = () => {
</Header.BackLink>
</Header.Root>
{!statement ? (
<Text
localizationKey={localizationKeys(`${localizationRoot}.billingPage.statementsSection.notFound`)}
sx={{ textAlign: 'center' }}
/>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
<Alert
variant='danger'
colorScheme='danger'
>
{translateError(error.errors[0]) ||
t(localizationKeys(`${localizationRoot}.billingPage.statementsSection.notFound`))}
</Alert>
</Box>
) : (
<Statement.Root>
<Statement.Header
Expand Down Expand Up @@ -91,9 +128,24 @@ export const StatementPage = () => {
)
}
labelIcon={item.chargeType === 'recurring' ? RotateLeftRight : Plus}
value={item.id}
valueTruncated
valueCopyable
value={
<SimpleButton
onClick={() => void navigate(`../../payment-attempt/${item.id}`)}
variant='link'
colorScheme='primary'
textVariant='buttonSmall'
sx={t => ({
gap: t.space.$1,
})}
>
<Span localizationKey={localizationKeys('commerce.viewPayment')} />
<Icon
icon={ArrowRightIcon}
size='sm'
aria-hidden
/>
</SimpleButton>
}
/>
{item.subscriptionItem.credit && item.subscriptionItem.credit.amount.amount > 0 ? (
<Statement.SectionContentDetailsListItem
Expand Down
17 changes: 0 additions & 17 deletions packages/clerk-js/src/ui/contexts/components/PaymentAttempts.tsx

This file was deleted.

17 changes: 0 additions & 17 deletions packages/clerk-js/src/ui/contexts/components/Statements.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions packages/clerk-js/src/ui/contexts/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ export * from './GoogleOneTap';
export * from './Waitlist';
export * from './PricingTable';
export * from './Checkout';
export * from './Statements';
export * from './PaymentAttempts';
export * from './Plans';
export * from './ApiKeys';
export * from './OAuthConsent';
1 change: 1 addition & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export const enUS: LocalizationResource = {
totalDue: 'Total due',
totalDueToday: 'Total Due Today',
viewFeatures: 'View features',
viewPayment: 'View payment',
Comment thread
aeliox marked this conversation as resolved.
year: 'Year',
},
createOrganization: {
Expand Down
20 changes: 20 additions & 0 deletions packages/types/src/commerce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export interface CommerceBillingNamespace {
*/
getPaymentAttempts: (params: GetPaymentAttemptsParams) => Promise<ClerkPaginatedResponse<CommercePaymentResource>>;

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
* @example
* ```tsx
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
getPaymentAttempt: (params: { id: string; orgId?: string }) => Promise<CommercePaymentResource>;

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
Expand Down Expand Up @@ -79,6 +89,16 @@ export interface CommerceBillingNamespace {
*/
getStatements: (params: GetStatementsParams) => Promise<ClerkPaginatedResponse<CommerceStatementResource>>;

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
* @example
* ```tsx
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
getStatement: (params: { id: string; orgId?: string }) => Promise<CommerceStatementResource>;

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export type __internal_LocalizationResource = {
defaultFreePlanActive: LocalizationValue;
viewFeatures: LocalizationValue;
seeAllFeatures: LocalizationValue;
viewPayment: LocalizationValue;
availableFeatures: LocalizationValue;
subtotal: LocalizationValue;
credit: LocalizationValue;
Expand Down
Loading