diff --git a/.changeset/pretty-chairs-spend.md b/.changeset/pretty-chairs-spend.md new file mode 100644 index 00000000000..b5de0230d75 --- /dev/null +++ b/.changeset/pretty-chairs-spend.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': patch +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Add new Billing Statements UI to User and Org Profile diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index c6bfe6f055e..abdac25a556 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,7 +1,7 @@ { "files": [ { "path": "./dist/clerk.js", "maxSize": "594kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "68KB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "68.2KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "110KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "52KB" }, { "path": "./dist/ui-common*.js", "maxSize": "104KB" }, diff --git a/packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts b/packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts index 21bccaace12..d583693450c 100644 --- a/packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts +++ b/packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts @@ -2,15 +2,15 @@ import type { ClerkPaginatedResponse, CommerceBillingNamespace, CommerceCheckoutJSON, - CommerceInvoiceJSON, - CommerceInvoiceResource, CommercePlanResource, CommerceProductJSON, + CommerceStatementJSON, + CommerceStatementResource, CommerceSubscriptionJSON, CommerceSubscriptionResource, CreateCheckoutParams, - GetInvoicesParams, GetPlansParams, + GetStatementsParams, GetSubscriptionsParams, } from '@clerk/types'; @@ -18,8 +18,8 @@ import { convertPageToOffsetSearchParams } from '../../../utils/convertPageToOff import { BaseResource, CommerceCheckout, - CommerceInvoice, CommercePlan, + CommerceStatement, CommerceSubscription, } from '../../resources/internal'; @@ -55,19 +55,20 @@ export class CommerceBilling implements CommerceBillingNamespace { }); }; - getInvoices = async (params: GetInvoicesParams): Promise> => { + getStatements = async (params: GetStatementsParams): Promise> => { const { orgId, ...rest } = params; return await BaseResource._fetch({ - path: orgId ? `/organizations/${orgId}/commerce/invoices` : `/me/commerce/invoices`, + path: orgId ? `/organizations/${orgId}/commerce/statements` : `/me/commerce/statements`, method: 'GET', search: convertPageToOffsetSearchParams(rest), }).then(res => { - const { data: invoices, total_count } = res?.response as unknown as ClerkPaginatedResponse; + const { data: statements, total_count } = + res?.response as unknown as ClerkPaginatedResponse; return { total_count, - data: invoices.map(invoice => new CommerceInvoice(invoice)), + data: statements.map(statement => new CommerceStatement(statement)), }; }); }; diff --git a/packages/clerk-js/src/core/resources/CommerceCheckout.ts b/packages/clerk-js/src/core/resources/CommerceCheckout.ts index 835f1276c46..9d46ab81753 100644 --- a/packages/clerk-js/src/core/resources/CommerceCheckout.ts +++ b/packages/clerk-js/src/core/resources/CommerceCheckout.ts @@ -20,7 +20,7 @@ export class CommerceCheckout extends BaseResource implements CommerceCheckoutRe id!: string; externalClientSecret!: string; externalGatewayId!: string; - invoice_id!: string; + statement_id!: string; paymentSource?: CommercePaymentSource; plan!: CommercePlan; planPeriod!: CommerceSubscriptionPlanPeriod; @@ -43,7 +43,7 @@ export class CommerceCheckout extends BaseResource implements CommerceCheckoutRe this.id = data.id; this.externalClientSecret = data.external_client_secret; this.externalGatewayId = data.external_gateway_id; - this.invoice_id = data.invoice_id; + this.statement_id = data.statement_id; this.paymentSource = data.payment_source ? new CommercePaymentSource(data.payment_source) : undefined; this.plan = new CommercePlan(data.plan); this.planPeriod = data.plan_period; diff --git a/packages/clerk-js/src/core/resources/CommerceInvoice.ts b/packages/clerk-js/src/core/resources/CommerceInvoice.ts deleted file mode 100644 index 8f47d6b818d..00000000000 --- a/packages/clerk-js/src/core/resources/CommerceInvoice.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { - CommerceInvoiceJSON, - CommerceInvoiceResource, - CommerceInvoiceStatus, - CommerceInvoiceTotals, -} from '@clerk/types'; - -import { commerceTotalsFromJSON } from '../../utils'; -import { BaseResource } from './internal'; - -export class CommerceInvoice extends BaseResource implements CommerceInvoiceResource { - id!: string; - paymentDueOn!: number; - paidOn!: number; - status!: CommerceInvoiceStatus; - totals!: CommerceInvoiceTotals; - - constructor(data: CommerceInvoiceJSON) { - super(); - this.fromJSON(data); - } - - protected fromJSON(data: CommerceInvoiceJSON | null): this { - if (!data) { - return this; - } - - this.id = data.id; - this.paymentDueOn = data.payment_due_on; - this.paidOn = data.paid_on; - this.status = data.status; - this.totals = commerceTotalsFromJSON(data.totals); - - return this; - } -} diff --git a/packages/clerk-js/src/core/resources/CommercePlan.ts b/packages/clerk-js/src/core/resources/CommercePlan.ts index 12ae0d0d04a..59b8d095d81 100644 --- a/packages/clerk-js/src/core/resources/CommercePlan.ts +++ b/packages/clerk-js/src/core/resources/CommercePlan.ts @@ -47,7 +47,7 @@ export class CommercePlan extends BaseResource implements CommercePlanResource { this.publiclyVisible = data.publicly_visible; this.slug = data.slug; this.avatarUrl = data.avatar_url; - this.features = data.features.map(feature => new CommerceFeature(feature)); + this.features = (data.features || []).map(feature => new CommerceFeature(feature)); return this; } diff --git a/packages/clerk-js/src/core/resources/CommerceStatement.ts b/packages/clerk-js/src/core/resources/CommerceStatement.ts new file mode 100644 index 00000000000..4c91723ef92 --- /dev/null +++ b/packages/clerk-js/src/core/resources/CommerceStatement.ts @@ -0,0 +1,88 @@ +import type { + CommerceMoney, + CommercePaymentChargeType, + CommercePaymentJSON, + CommercePaymentStatus, + CommerceStatementGroupJSON, + CommerceStatementJSON, + CommerceStatementResource, + CommerceStatementStatus, + CommerceStatementTotals, +} from '@clerk/types'; + +import { commerceMoneyFromJSON, commerceTotalsFromJSON } from '../../utils'; +import { BaseResource, CommercePaymentSource, CommerceSubscription } from './internal'; + +export class CommerceStatement extends BaseResource implements CommerceStatementResource { + id!: string; + status!: CommerceStatementStatus; + timestamp!: number; + totals!: CommerceStatementTotals; + groups!: CommerceStatementGroup[]; + + constructor(data: CommerceStatementJSON) { + super(); + this.fromJSON(data); + } + + protected fromJSON(data: CommerceStatementJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.status = data.status; + this.timestamp = data.timestamp; + this.totals = commerceTotalsFromJSON(data.totals); + this.groups = data.groups.map(group => new CommerceStatementGroup(group)); + return this; + } +} + +export class CommerceStatementGroup { + id!: string; + timestamp!: number; + items!: CommercePayment[]; + + constructor(data: CommerceStatementGroupJSON) { + this.fromJSON(data); + } + + protected fromJSON(data: CommerceStatementGroupJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.timestamp = data.timestamp; + this.items = data.items.map(item => new CommercePayment(item)); + return this; + } +} + +export class CommercePayment { + id!: string; + amount!: CommerceMoney; + paymentSource!: CommercePaymentSource; + subscription!: CommerceSubscription; + chargeType!: CommercePaymentChargeType; + status!: CommercePaymentStatus; + + constructor(data: CommercePaymentJSON) { + this.fromJSON(data); + } + + protected fromJSON(data: CommercePaymentJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.amount = commerceMoneyFromJSON(data.amount); + this.paymentSource = new CommercePaymentSource(data.payment_source); + this.subscription = new CommerceSubscription(data.subscription); + this.chargeType = data.charge_type; + this.status = data.status; + return this; + } +} diff --git a/packages/clerk-js/src/core/resources/CommerceSubscription.ts b/packages/clerk-js/src/core/resources/CommerceSubscription.ts index 1a4261162a8..fe63c472629 100644 --- a/packages/clerk-js/src/core/resources/CommerceSubscription.ts +++ b/packages/clerk-js/src/core/resources/CommerceSubscription.ts @@ -1,5 +1,6 @@ import type { CancelSubscriptionParams, + CommerceMoney, CommerceSubscriptionJSON, CommerceSubscriptionPlanPeriod, CommerceSubscriptionResource, @@ -7,6 +8,7 @@ import type { DeletedObjectJSON, } from '@clerk/types'; +import { commerceMoneyFromJSON } from '../../utils'; import { BaseResource, CommercePlan, DeletedObject } from './internal'; export class CommerceSubscription extends BaseResource implements CommerceSubscriptionResource { @@ -18,6 +20,10 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr periodStart!: number; periodEnd!: number; canceledAt!: number | null; + amount?: CommerceMoney; + credit?: { + amount: CommerceMoney; + }; constructor(data: CommerceSubscriptionJSON) { super(); this.fromJSON(data); @@ -36,6 +42,8 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr this.periodStart = data.period_start; this.periodEnd = data.period_end; this.canceledAt = data.canceled_at; + this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined; + this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined; return this; } diff --git a/packages/clerk-js/src/core/resources/internal.ts b/packages/clerk-js/src/core/resources/internal.ts index 5516809ee9d..6ae1cb0c007 100644 --- a/packages/clerk-js/src/core/resources/internal.ts +++ b/packages/clerk-js/src/core/resources/internal.ts @@ -6,7 +6,7 @@ export * from './AuthConfig'; export * from './Client'; export * from './CommerceCheckout'; export * from './CommerceFeature'; -export * from './CommerceInvoice'; +export * from './CommerceStatement'; export * from './CommercePaymentSource'; export * from './CommercePlan'; export * from './CommerceProduct'; diff --git a/packages/clerk-js/src/ui/components/Invoices/InvoicePage.tsx b/packages/clerk-js/src/ui/components/Invoices/InvoicePage.tsx deleted file mode 100644 index f6ef92e00b9..00000000000 --- a/packages/clerk-js/src/ui/components/Invoices/InvoicePage.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { InvoicesContextProvider, useInvoicesContext } from '../../contexts'; -import { Badge, Box, Button, Dd, descriptors, Dl, Dt, Heading, Icon, Span, Spinner, Text } from '../../customizables'; -import { Header, LineItems } from '../../elements'; -import { useClipboard } from '../../hooks'; -import { Check, Copy } from '../../icons'; -import { useRouter } from '../../router'; -import { common } from '../../styledSystem'; -import { colors } from '../../utils'; -import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible'; - -const InvoicePageInternal = () => { - const { params, navigate } = useRouter(); - const { getInvoiceById, isLoading } = useInvoicesContext(); - const invoice = params.invoiceId ? getInvoiceById(params.invoiceId) : null; - - if (isLoading) { - return ( - - - - ); - } - - return ( - <> - - void navigate('../../', { searchParams: new URLSearchParams('tab=invoices') })}> - - - - ({ - 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 ? ( - - Invoice not found - - ) : ( - ({ - borderWidth: t.borderWidths.$normal, - borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$neutralAlpha100, - borderRadius: t.radii.$lg, - overflow: 'hidden', - })} - > - ({ - 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, - })} - > - - - - Invoice ID - - ({ - display: 'flex', - alignItems: 'center', - gap: t.space.$0x25, - color: t.colors.$colorTextSecondary, - })} - > - - - {truncateWithEndVisible(invoice.id)} - - - - - - {invoice.status} - - -
({ - display: 'flex', - justifyContent: 'space-between', - marginBlockStart: t.space.$3, - })} - > - -
- - Created on - -
-
- - {new Date(invoice.paymentDueOn).toLocaleDateString()} - -
-
- -
- - Due on - -
-
- - {new Date(invoice.paymentDueOn).toLocaleDateString()} - -
-
-
-
- ({ - padding: t.space.$4, - })} - > - - - - - - - - - - - - - - - - - - - -
- )} -
- - ); -}; - -function CopyButton({ text, copyLabel = 'Copy' }: { text: string; copyLabel?: string }) { - const { onCopy, hasCopied } = useClipboard(text); - - return ( - - ); -} - -export const InvoicePage = () => { - return ( - - - - ); -}; diff --git a/packages/clerk-js/src/ui/components/Invoices/index.ts b/packages/clerk-js/src/ui/components/Invoices/index.ts deleted file mode 100644 index cfa41fe6c43..00000000000 --- a/packages/clerk-js/src/ui/components/Invoices/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './InvoicesList'; -export * from './InvoicePage'; diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx index 3e5d8a0d5f0..5bd8c586ad7 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationBillingPage.tsx @@ -1,8 +1,8 @@ import { Protect } from '../../common'; import { - InvoicesContextProvider, PlansContextProvider, PricingTableContext, + StatementsContextProvider, SubscriberTypeContext, useSubscriptions, } from '../../contexts'; @@ -23,14 +23,14 @@ import { import { useTabState } from '../../hooks/useTabState'; import { ArrowsUpDown } from '../../icons'; import { useRouter } from '../../router'; -import { InvoicesList } from '../Invoices'; import { PaymentSources } from '../PaymentSources'; import { PricingTable } from '../PricingTable'; +import { StatementsList } from '../Statements'; import { SubscriptionsList } from '../Subscriptions'; const orgTabMap = { 0: 'plans', - 1: 'invoices', + 1: 'statements', 2: 'payment-methods', } as const; @@ -74,7 +74,7 @@ const OrganizationBillingPageInternal = withCardStateProvider(() => { : localizationKeys('organizationProfile.billingPage.start.headerTitle__plans') } /> - + @@ -136,9 +136,9 @@ const OrganizationBillingPageInternal = withCardStateProvider(() => { )} - - - + + + diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx index ef2f19ac5e4..1b51625a650 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx @@ -5,9 +5,9 @@ import { CustomPageContentContainer } from '../../common/CustomPageContentContai import { useEnvironment, useOrganizationProfileContext } from '../../contexts'; import { Route, Switch } from '../../router'; import { OrganizationGeneralPage } from './OrganizationGeneralPage'; -import { OrganizationInvoicePage } from './OrganizationInvoicePage'; import { OrganizationMembers } from './OrganizationMembers'; import { OrganizationPlansPage } from './OrganizationPlansPage'; +import { OrganizationStatementPage } from './OrganizationStatementPage'; const OrganizationBillingPage = lazy(() => import(/* webpackChunkName: "op-billing-page"*/ './OrganizationBillingPage').then(module => ({ @@ -79,10 +79,10 @@ export const OrganizationProfileRoutes = () => { - + {/* TODO(@commerce): Should this be lazy loaded ? */} - + diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationInvoicePage.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationStatementPage.tsx similarity index 57% rename from packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationInvoicePage.tsx rename to packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationStatementPage.tsx index d1f50574e4b..7c85e1a3b2e 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationInvoicePage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationStatementPage.tsx @@ -1,10 +1,10 @@ import { SubscriberTypeContext } from '../../contexts'; -import { InvoicePage } from '../Invoices'; +import { StatementPage } from '../Statements'; -export const OrganizationInvoicePage = () => { +export const OrganizationStatementPage = () => { return ( - + ); }; diff --git a/packages/clerk-js/src/ui/components/Statements/Statement.tsx b/packages/clerk-js/src/ui/components/Statements/Statement.tsx new file mode 100644 index 00000000000..e6ade79c8fc --- /dev/null +++ b/packages/clerk-js/src/ui/components/Statements/Statement.tsx @@ -0,0 +1,453 @@ +import type { LocalizationKey } from '../../customizables'; +import { Badge, Box, Button, descriptors, Heading, Icon, Span, Text } from '../../customizables'; +import { useClipboard } from '../../hooks'; +import { Check, Copy, Plans } from '../../icons'; +import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible'; + +/* ------------------------------------------------------------------------------------------------- + * Statement.Root + * -----------------------------------------------------------------------------------------------*/ + +function Root({ children }: { children: React.ReactNode }) { + return ( + ({ + borderWidth: t.borderWidths.$normal, + borderStyle: t.borderStyles.$solid, + borderColor: t.colors.$neutralAlpha100, + borderRadius: t.radii.$lg, + overflow: 'hidden', + })} + > + {children} + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Statement.Header + * -----------------------------------------------------------------------------------------------*/ + +function Header({ title, id, status }: { title: string | LocalizationKey; id: string; status: string }) { + return ( + ({ + padding: t.space.$4, + background: t.colors.$neutralAlpha25, + display: 'flex', + justifyContent: 'space-between', + alignItems: 'flex-start', + })} + > + + + ({ + display: 'flex', + alignItems: 'center', + gap: t.space.$0x25, + color: t.colors.$colorTextSecondary, + })} + > + + + {truncateWithEndVisible(id)} + + + + + {status} + + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Statement.Body + * -----------------------------------------------------------------------------------------------*/ + +function Body({ children }: { children: React.ReactNode }) { + return {children}; +} +/* ------------------------------------------------------------------------------------------------- + * Statement.Section + * -----------------------------------------------------------------------------------------------*/ + +function Section({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Statement.SectionHeader + * -----------------------------------------------------------------------------------------------*/ + +function SectionHeader({ text }: { text: string | LocalizationKey }) { + return ( + ({ + paddingInline: t.space.$4, + paddingBlock: t.space.$1, + background: t.colors.$neutralAlpha50, + borderBlockWidth: t.borderWidths.$normal, + borderBlockStyle: t.borderStyles.$solid, + borderBlockColor: t.colors.$neutralAlpha100, + })} + > + ({ + fontWeight: t.fontWeights.$medium, + })} + /> + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Statement.SectionContent + * -----------------------------------------------------------------------------------------------*/ + +function SectionContent({ children }: { children: React.ReactNode }) { + return {children}; +} + +/* ------------------------------------------------------------------------------------------------- + * Statement.SectionContentItem + * -----------------------------------------------------------------------------------------------*/ + +function SectionContentItem({ children }: { children: React.ReactNode }) { + return ( + ({ + paddingInline: t.space.$4, + paddingBlock: t.space.$3, + '&:not(:first-child)': { + borderBlockStartWidth: t.borderWidths.$normal, + borderBlockStartStyle: t.borderStyles.$solid, + borderBlockStartColor: t.colors.$neutralAlpha100, + }, + })} + > + {children} + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Statement.SectionContentDetailsHeader + * -----------------------------------------------------------------------------------------------*/ + +function SectionContentDetailsHeader({ + title, + description, + secondaryTitle, + secondaryDescription, +}: { + title: string | LocalizationKey; + description: string | LocalizationKey; + secondaryTitle: string | LocalizationKey; + secondaryDescription: string | LocalizationKey; +}) { + return ( + ({ + marginBlockEnd: t.space.$2, + display: 'flex', + justifyContent: 'space-between', + })} + > + + ({ + display: 'flex', + alignItems: 'center', + gap: t.space.$1, + })} + > + + + + + + + + + + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Statement.SectionContentDetailsList + * -----------------------------------------------------------------------------------------------*/ + +function SectionContentDetailsList({ children }: { children: React.ReactNode }) { + return ( + ({ + margin: 0, + padding: 0, + borderWidth: t.borderWidths.$normal, + borderStyle: t.borderStyles.$solid, + borderColor: t.colors.$neutralAlpha100, + borderRadius: t.radii.$md, + overflow: 'hidden', + })} + > + {children} + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Statement.SectionContentDetailsListItem + * -----------------------------------------------------------------------------------------------*/ + +function SectionContentDetailsListItem({ + labelIcon, + label, + valueCopyable = false, + value, + valueTruncated = false, +}: { + icon?: React.ReactNode; + label: string | LocalizationKey; + labelIcon?: React.ComponentType; + value: string | LocalizationKey; + valueTruncated?: boolean; + valueCopyable?: boolean; +}) { + return ( + ({ + margin: 0, + paddingInline: t.space.$2, + paddingBlock: t.space.$1x5, + display: 'flex', + justifyContent: 'space-between', + flexWrap: 'wrap', + columnGap: t.space.$2, + rowGap: t.space.$0x5, + '&:not(:first-child)': { + borderBlockStartWidth: t.borderWidths.$normal, + borderBlockStartStyle: t.borderStyles.$solid, + borderBlockStartColor: t.colors.$neutralAlpha100, + }, + })} + > + ({ + display: 'flex', + alignItems: 'center', + gap: t.space.$1x5, + })} + > + {labelIcon ? ( + + ) : null} + + + ({ + display: 'flex', + alignItems: 'center', + gap: t.space.$0x25, + color: t.colors.$colorTextSecondary, + })} + > + {typeof value === 'string' ? ( + <> + {valueCopyable ? ( + + ) : null} + + {valueTruncated ? truncateWithEndVisible(value) : value} + + + ) : ( + + )} + + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Statement.Footer + * -----------------------------------------------------------------------------------------------*/ + +function Footer({ label, value }: { label: string | LocalizationKey; value: string }) { + return ( + ({ + paddingInline: t.space.$4, + paddingBlock: t.space.$3, + background: t.colors.$neutralAlpha25, + borderBlockStartWidth: t.borderWidths.$normal, + borderBlockStartStyle: t.borderStyles.$solid, + borderBlockStartColor: t.colors.$neutralAlpha100, + display: 'flex', + justifyContent: 'space-between', + })} + > + + ({ + display: 'flex', + alignItems: 'center', + gap: t.space.$2x5, + })} + > + + USD + + + {value} + + + + ); +} + +function CopyButton({ text, copyLabel = 'Copy' }: { text: string; copyLabel?: string }) { + const { onCopy, hasCopied } = useClipboard(text); + + return ( + + ); +} + +export const Statement = { + Root, + Header, + Body, + Section, + SectionHeader, + SectionContent, + SectionContentItem, + SectionContentDetailsHeader, + SectionContentDetailsList, + SectionContentDetailsListItem, + Footer, +}; diff --git a/packages/clerk-js/src/ui/components/Statements/StatementPage.tsx b/packages/clerk-js/src/ui/components/Statements/StatementPage.tsx new file mode 100644 index 00000000000..7a3a1de67c4 --- /dev/null +++ b/packages/clerk-js/src/ui/components/Statements/StatementPage.tsx @@ -0,0 +1,115 @@ +import { StatementsContextProvider, useStatementsContext } from '../../contexts'; +import { Box, descriptors, Spinner, Text } from '../../customizables'; +import { Header } from '../../elements'; +import { Plus, RotateLeftRight } from '../../icons'; +import { useRouter } from '../../router'; +import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible'; +import { Statement } from './Statement'; + +const StatementPageInternal = () => { + const { params, navigate } = useRouter(); + const { getStatementById, isLoading } = useStatementsContext(); + const statement = params.statementId ? getStatementById(params.statementId) : null; + + if (isLoading) { + return ( + + + + ); + } + + if (!statement) { + return Statement not found; + } + + return ( + <> + ({ + borderBlockEndWidth: t.borderWidths.$normal, + borderBlockEndStyle: t.borderStyles.$solid, + borderBlockEndColor: t.colors.$neutralAlpha100, + marginBlockEnd: t.space.$4, + paddingBlockEnd: t.space.$4, + })} + > + void navigate('../../', { searchParams: new URLSearchParams('tab=statements') })} + > + + + + + + + {statement.groups.map(group => ( + + + + {group.items.map(item => ( + + + + + {item.subscription.credit && item.subscription.credit.amount.amount > 0 ? ( + + ) : null} + + + ))} + + + ))} + + + + + ); +}; + +export const StatementPage = () => { + return ( + + + + ); +}; diff --git a/packages/clerk-js/src/ui/components/Invoices/InvoicesList.tsx b/packages/clerk-js/src/ui/components/Statements/StatementsList.tsx similarity index 75% rename from packages/clerk-js/src/ui/components/Invoices/InvoicesList.tsx rename to packages/clerk-js/src/ui/components/Statements/StatementsList.tsx index 76c2600fcfd..51f4815b0a8 100644 --- a/packages/clerk-js/src/ui/components/Invoices/InvoicesList.tsx +++ b/packages/clerk-js/src/ui/components/Statements/StatementsList.tsx @@ -1,34 +1,20 @@ -import type { CommerceInvoiceResource, CommerceInvoiceStatus } from '@clerk/types'; +import type { CommerceStatementResource } from '@clerk/types'; import React from 'react'; -import { useInvoicesContext } from '../../contexts'; +import { useStatementsContext } from '../../contexts'; import type { LocalizationKey } from '../../customizables'; -import { - Badge, - Col, - descriptors, - Flex, - Link, - Spinner, - Table, - Tbody, - Td, - Text, - Th, - Thead, - Tr, -} from '../../customizables'; +import { Col, descriptors, Flex, Spinner, Table, Tbody, Td, Text, Th, Thead, Tr } from '../../customizables'; import { Pagination } from '../../elements'; import { useRouter } from '../../router'; import type { PropsOfComponent } from '../../styledSystem'; import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible'; /* ------------------------------------------------------------------------------------------------- - * InvoicesList + * StatementsList * -----------------------------------------------------------------------------------------------*/ -export const InvoicesList = () => { - const { invoices, isLoading, totalCount } = useInvoicesContext(); +export const StatementsList = () => { + const { statements, isLoading, totalCount } = useStatementsContext(); return ( { pageCount={1} itemsPerPage={10} isLoading={isLoading} - emptyStateLocalizationKey='No invoices to display' - headers={['Date/Invoice', 'Status', 'Total']} - rows={invoices.map(i => ( - ( + ))} /> ); }; -const InvoicesListRow = ({ invoice }: { invoice: CommerceInvoiceResource }) => { +const StatementsListRow = ({ statement }: { statement: CommerceStatementResource }) => { const { - paymentDueOn, + timestamp, id, - status, totals: { grandTotal }, - } = invoice; + } = statement; const { navigate } = useRouter(); - const badgeColorSchemeMap: Record = { - paid: 'success', - unpaid: 'warning', - past_due: 'danger', - }; const handleClick = () => { - void navigate(`invoice/${id}`); + void navigate(`statement/${id}`); }; return ( @@ -74,30 +54,17 @@ const InvoicesListRow = ({ invoice }: { invoice: CommerceInvoiceResource }) => { }} > - {new Date(paymentDueOn).toLocaleDateString()} + {new Date(timestamp).toLocaleString('en-US', { month: 'long', year: 'numeric' })} ({ marginTop: t.space.$0x5, textTransform: 'uppercase' })} + sx={t => ({ marginTop: t.space.$0x5 })} > {truncateWithEndVisible(id)} - - - {status} - - { elementDescriptor={descriptors.tableHead} key={index} localizationKey={h} + sx={{ width: index === 0 ? 'auto' : '25%' }} /> ))} diff --git a/packages/clerk-js/src/ui/components/Statements/index.ts b/packages/clerk-js/src/ui/components/Statements/index.ts new file mode 100644 index 00000000000..f88c8bd10bb --- /dev/null +++ b/packages/clerk-js/src/ui/components/Statements/index.ts @@ -0,0 +1,2 @@ +export * from './StatementsList'; +export * from './StatementPage'; diff --git a/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx index 16d8a3b6f8d..29aa7841dc3 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/BillingPage.tsx @@ -1,7 +1,7 @@ import { - InvoicesContextProvider, PlansContextProvider, PricingTableContext, + StatementsContextProvider, SubscriberTypeContext, useSubscriptions, } from '../../contexts'; @@ -21,14 +21,14 @@ import { import { useTabState } from '../../hooks/useTabState'; import { ArrowsUpDown } from '../../icons'; import { useRouter } from '../../router'; -import { InvoicesList } from '../Invoices'; import { PaymentSources } from '../PaymentSources'; import { PricingTable } from '../PricingTable'; +import { StatementsList } from '../Statements'; import { SubscriptionsList } from '../Subscriptions'; const tabMap = { 0: 'plans', - 1: 'invoices', + 1: 'statements', 2: 'payment-methods', } as const; @@ -74,7 +74,7 @@ const BillingPageInternal = withCardStateProvider(() => { : localizationKeys('userProfile.billingPage.start.headerTitle__plans') } /> - + ({ width: '100%', flexDirection: 'column' })}> @@ -118,9 +118,9 @@ const BillingPageInternal = withCardStateProvider(() => { )} - - - + + + diff --git a/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx b/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx index 7d7cc93154f..6252af0e006 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx @@ -4,7 +4,7 @@ import { CustomPageContentContainer } from '../../common/CustomPageContentContai import { USER_PROFILE_NAVBAR_ROUTE_ID } from '../../constants'; import { useEnvironment, useUserProfileContext } from '../../contexts'; import { Route, Switch } from '../../router'; -import { InvoicePage } from '../Invoices/InvoicePage'; +import { StatementPage } from '../Statements'; import { AccountPage } from './AccountPage'; import { PlansPage } from './PlansPage'; import { SecurityPage } from './SecurityPage'; @@ -71,10 +71,10 @@ export const UserProfileRoutes = () => { - + {/* TODO(@commerce): Should this be lazy loaded ? */} - + diff --git a/packages/clerk-js/src/ui/contexts/components/Invoices.tsx b/packages/clerk-js/src/ui/contexts/components/Invoices.tsx deleted file mode 100644 index bc11ef8bf45..00000000000 --- a/packages/clerk-js/src/ui/contexts/components/Invoices.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useClerk, useOrganization, useUser } from '@clerk/shared/react'; -import type { PropsWithChildren } from 'react'; -import { createContext, useContext } from 'react'; - -import { useFetch } from '../../hooks'; -import type { InvoicesCtx } from '../../types'; -import { useSubscriberTypeContext } from './SubscriberType'; - -const InvoicesContext = createContext(null); - -export const InvoicesContextProvider = ({ children }: PropsWithChildren) => { - const { billing } = useClerk(); - const { organization } = useOrganization(); - const subscriberType = useSubscriberTypeContext(); - const { user } = useUser(); - - const resource = subscriberType === 'org' ? organization : user; - - const { data, isLoading, revalidate } = useFetch( - billing.getInvoices, - { ...(subscriberType === 'org' ? { orgId: organization?.id } : {}) }, - undefined, - `commerce-invoices-${resource?.id}`, - ); - const { data: invoices, total_count: totalCount } = data || { data: [], totalCount: 0 }; - - const getInvoiceById = (invoiceId: string) => { - return invoices.find(invoice => invoice.id === invoiceId); - }; - - return ( - - {children} - - ); -}; - -export const useInvoicesContext = () => { - const context = useContext(InvoicesContext); - - if (!context || context.componentName !== 'Invoices') { - throw new Error('Clerk: useInvoicesContext called outside Invoices.'); - } - - const { componentName, ...ctx } = context; - - return { - ...ctx, - componentName, - }; -}; diff --git a/packages/clerk-js/src/ui/contexts/components/Plans.tsx b/packages/clerk-js/src/ui/contexts/components/Plans.tsx index 2045543bb16..abac46240ba 100644 --- a/packages/clerk-js/src/ui/contexts/components/Plans.tsx +++ b/packages/clerk-js/src/ui/contexts/components/Plans.tsx @@ -51,21 +51,21 @@ export const PlansContextProvider = ({ children }: PropsWithChildren) => { } = useFetch(billing.getPlans, { subscriberType }, undefined, 'commerce-plans'); // Revalidates the next time the hooks gets mounted - const { revalidate: revalidateInvoices } = useFetch( + const { revalidate: revalidateStatements } = useFetch( undefined, { ...(subscriberType === 'org' ? { orgId: organization?.id } : {}), }, undefined, - `commerce-invoices-${resource?.id}`, + `commerce-statements-${resource?.id}`, ); const revalidate = useCallback(() => { // Revalidate the plans and subscriptions revalidateSubscriptions(); revalidatePlans(); - revalidateInvoices(); - }, [revalidateInvoices, revalidatePlans, revalidateSubscriptions]); + revalidateStatements(); + }, [revalidateStatements, revalidatePlans, revalidateSubscriptions]); const isLoaded = useMemo(() => { if (isSignedIn) { diff --git a/packages/clerk-js/src/ui/contexts/components/Statements.tsx b/packages/clerk-js/src/ui/contexts/components/Statements.tsx new file mode 100644 index 00000000000..062b1ebc6eb --- /dev/null +++ b/packages/clerk-js/src/ui/contexts/components/Statements.tsx @@ -0,0 +1,58 @@ +import { useClerk, useOrganization, useUser } from '@clerk/shared/react'; +import type { PropsWithChildren } from 'react'; +import { createContext, useContext } from 'react'; + +import { useFetch } from '../../hooks'; +import type { StatementsCtx } from '../../types'; +import { useSubscriberTypeContext } from './SubscriberType'; + +const StatementsContext = createContext(null); + +export const StatementsContextProvider = ({ children }: PropsWithChildren) => { + const { billing } = useClerk(); + const { organization } = useOrganization(); + const subscriberType = useSubscriberTypeContext(); + const { user } = useUser(); + + const { data, isLoading, revalidate } = useFetch( + billing.getStatements, + { ...(subscriberType === 'org' ? { orgId: organization?.id } : {}) }, + undefined, + `commerce-statements-${user?.id}`, + ); + const { data: statements, total_count: totalCount } = data || { data: [], totalCount: 0 }; + + const getStatementById = (statementId: string) => { + return statements.find(statement => statement.id === statementId); + }; + + return ( + + {children} + + ); +}; + +export const useStatementsContext = () => { + const context = useContext(StatementsContext); + + if (!context || context.componentName !== 'Statements') { + throw new Error('Clerk: useStatementsContext called outside Statements.'); + } + + const { componentName, ...ctx } = context; + + return { + ...ctx, + componentName, + }; +}; diff --git a/packages/clerk-js/src/ui/contexts/components/index.ts b/packages/clerk-js/src/ui/contexts/components/index.ts index cb0b95373a0..5bb1e062d6f 100644 --- a/packages/clerk-js/src/ui/contexts/components/index.ts +++ b/packages/clerk-js/src/ui/contexts/components/index.ts @@ -13,5 +13,5 @@ export * from './GoogleOneTap'; export * from './Waitlist'; export * from './PricingTable'; export * from './Checkout'; -export * from './Invoices'; +export * from './Statements'; export * from './Plans'; diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts index f5d56d3ae25..17fa1ca0b50 100644 --- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts +++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts @@ -346,25 +346,35 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([ 'paymentSourceRowValue', 'paymentSourceRowBadge', - 'invoiceRoot', - 'invoiceCard', - 'invoiceHeader', - 'invoiceHeaderContent', - 'invoiceTitle', - 'invoiceHeaderTitleBadgeContainer', - 'invoiceId', - 'invoiceIdContainer', - 'invoiceTitleIdContainer', - 'invoiceBadge', - 'invoiceDetails', - 'invoiceDetailsItem', - 'invoiceDetailsItemTitle', - 'invoiceDetailsItemTitleText', - 'invoiceDetailsItemValue', - 'invoiceDetailsItemValueText', - 'invoiceCopyButton', - 'invoiceContent', - + 'statementRoot', + 'statementHeader', + 'statementHeaderTitleContainer', + 'statementHeaderTitle', + 'statementHeaderBadge', + 'statementBody', + 'statementSection', + 'statementSectionHeader', + 'statementSectionHeaderTitle', + 'statementSectionContent', + 'statementSectionContentItem', + 'statementSectionContentDetailsList', + 'statementSectionContentDetailsListItem', + 'statementSectionContentDetailsListItemLabelContainer', + 'statementSectionContentDetailsListItemLabel', + 'statementSectionContentDetailsListItemValue', + 'statementSectionContentDetailsHeader', + 'statementSectionContentDetailsHeaderItem', + 'statementSectionContentDetailsHeaderItemIcon', + 'statementSectionContentDetailsHeaderTitle', + 'statementSectionContentDetailsHeaderDescription', + 'statementSectionContentDetailsHeaderSecondaryTitle', + 'statementSectionContentDetailsHeaderSecondaryDescription', + 'statementFooter', + 'statementFooterLabel', + 'statementFooterValueContainer', + 'statementFooterCurrency', + 'statementFooterValue', + 'statementCopyButton', 'menuButton', 'menuButtonEllipsis', 'menuList', diff --git a/packages/clerk-js/src/ui/foundations/shadows.ts b/packages/clerk-js/src/ui/foundations/shadows.ts index 490215ff011..96f46a467c2 100644 --- a/packages/clerk-js/src/ui/foundations/shadows.ts +++ b/packages/clerk-js/src/ui/foundations/shadows.ts @@ -10,7 +10,8 @@ export const shadows = Object.freeze({ input: '0px 0px 1px 0px {{color}}', focusRing: '0px 0px 0px 4px {{color}}', badge: '0px 2px 0px -1px rgba(0, 0, 0, 0.04)', - tableBodyShadow: '0px 0px 1px 0px rgba(0, 0, 0, 0.08), 0px 1px 2px 0px rgba(0, 0, 0, 0.12)', + tableBodyShadow: + '0px 0px 2px 0px rgba(0, 0, 0, 0.08), 0px 1px 2px 0px rgba(25, 28, 33, 0.12), 0px 0px 0px 1px rgba(0, 0, 0, 0.06)', segmentedControl: '0px 1px 2px 0px rgba(0, 0, 0, 0.08)', switchControl: '0px 2px 2px -1px rgba(0, 0, 0, 0.06), 0px 0px 0px 1px rgba(0, 0, 0, 0.06), 0px 4px 4px -2px rgba(0, 0, 0, 0.06)', diff --git a/packages/clerk-js/src/ui/icons/block.svg b/packages/clerk-js/src/ui/icons/block.svg index 782391fe05c..45d5f127750 100644 --- a/packages/clerk-js/src/ui/icons/block.svg +++ b/packages/clerk-js/src/ui/icons/block.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/packages/clerk-js/src/ui/icons/index.ts b/packages/clerk-js/src/ui/icons/index.ts index 763d6c83a69..f17e1b81f88 100644 --- a/packages/clerk-js/src/ui/icons/index.ts +++ b/packages/clerk-js/src/ui/icons/index.ts @@ -55,6 +55,7 @@ export { default as Plus } from './plus.svg'; export { default as Print } from './print.svg'; export { default as QuestionMark } from './question-mark.svg'; export { default as RequestAuthIcon } from './request-auth.svg'; +export { default as RotateLeftRight } from './rotate-left-right.svg'; export { default as Selector } from './selector.svg'; export { default as SignOut } from './signout.svg'; export { default as SignOutDouble } from './signout-double.svg'; diff --git a/packages/clerk-js/src/ui/icons/rotate-left-right.svg b/packages/clerk-js/src/ui/icons/rotate-left-right.svg new file mode 100644 index 00000000000..b9262da6fbb --- /dev/null +++ b/packages/clerk-js/src/ui/icons/rotate-left-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/clerk-js/src/ui/primitives/Heading.tsx b/packages/clerk-js/src/ui/primitives/Heading.tsx index bf9b8c74fb1..3be852f4bce 100644 --- a/packages/clerk-js/src/ui/primitives/Heading.tsx +++ b/packages/clerk-js/src/ui/primitives/Heading.tsx @@ -17,7 +17,7 @@ const { applyVariants, filterProps } = createVariants(theme => ({ })); // @ts-ignore -export type HeadingProps = PrimitiveProps<'div'> & StyleVariants & { as?: 'h1' | 'h2' }; +export type HeadingProps = PrimitiveProps<'div'> & StyleVariants & { as?: 'h1' | 'h2' | 'h3' }; export const Heading = (props: HeadingProps) => { const { as: As = 'h1', ...rest } = props; diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts index 1d4fa7f48b3..3cd7393c5fb 100644 --- a/packages/clerk-js/src/ui/types.ts +++ b/packages/clerk-js/src/ui/types.ts @@ -2,8 +2,8 @@ import type { __internal_CheckoutProps, __internal_PlanDetailsProps, __internal_UserVerificationProps, - CommerceInvoiceResource, CommercePlanResource, + CommerceStatementResource, CommerceSubscriptionResource, CreateOrganizationProps, GoogleOneTapProps, @@ -125,13 +125,13 @@ export type PaymentSourcesCtx = { componentName: 'PaymentSources'; }; -export type InvoicesCtx = { - componentName: 'Invoices'; - invoices: CommerceInvoiceResource[]; +export type StatementsCtx = { + componentName: 'Statements'; + statements: CommerceStatementResource[]; totalCount: number; isLoading: boolean; revalidate: () => void; - getInvoiceById: (invoiceId: string) => CommerceInvoiceResource | undefined; + getStatementById: (statementId: string) => CommerceStatementResource | undefined; }; export type PlansCtx = { diff --git a/packages/clerk-js/src/utils/commerce.ts b/packages/clerk-js/src/utils/commerce.ts index a794bba19e3..ac39f166150 100644 --- a/packages/clerk-js/src/utils/commerce.ts +++ b/packages/clerk-js/src/utils/commerce.ts @@ -1,10 +1,10 @@ import type { CommerceCheckoutTotals, CommerceCheckoutTotalsJSON, - CommerceInvoiceTotals, - CommerceInvoiceTotalsJSON, CommerceMoney, CommerceMoneyJSON, + CommerceStatementTotals, + CommerceStatementTotalsJSON, } from '@clerk/types'; export const commerceMoneyFromJSON = (data: CommerceMoneyJSON): CommerceMoney => { @@ -16,7 +16,7 @@ export const commerceMoneyFromJSON = (data: CommerceMoneyJSON): CommerceMoney => }; }; -export const commerceTotalsFromJSON = (data: T) => { +export const commerceTotalsFromJSON = (data: T) => { const totals = { grandTotal: commerceMoneyFromJSON(data.grand_total), subtotal: commerceMoneyFromJSON(data.subtotal), @@ -31,5 +31,5 @@ export const commerceTotalsFromJSON = ; - invoiceRoot: WithOptions; - invoiceCard: WithOptions; - invoiceHeader: WithOptions; - invoiceHeaderContent: WithOptions; - invoiceTitle: WithOptions; - invoiceHeaderTitleBadgeContainer: WithOptions; - invoiceTitleIdContainer: WithOptions; - invoiceId: WithOptions; - invoiceIdContainer: WithOptions; - invoiceBadge: WithOptions; - invoiceDetails: WithOptions; - invoiceDetailsItem: WithOptions; - invoiceDetailsItemTitle: WithOptions; - invoiceDetailsItemTitleText: WithOptions; - invoiceDetailsItemValue: WithOptions; - invoiceDetailsItemValueText: WithOptions; - invoiceCopyButton: WithOptions; - invoiceContent: WithOptions; - + statementRoot: WithOptions; + statementHeader: WithOptions; + statementHeaderTitle: WithOptions; + statementHeaderBadge: WithOptions; + statementBody: WithOptions; + statementSection: WithOptions; + statementSectionHeader: WithOptions; + statementHeaderTitleContainer: WithOptions; + statementSectionHeaderTitle: WithOptions; + statementSectionContent: WithOptions; + statementSectionContentItem: WithOptions; + statementSectionContentDetailsList: WithOptions; + statementSectionContentDetailsListItem: WithOptions; + statementSectionContentDetailsListItemLabelContainer: WithOptions; + statementSectionContentDetailsListItemLabel: WithOptions; + statementSectionContentDetailsListItemValue: WithOptions; + statementSectionContentDetailsHeader: WithOptions; + statementSectionContentDetailsHeaderItem: WithOptions; + statementSectionContentDetailsHeaderItemIcon: WithOptions; + statementSectionContentDetailsHeaderTitle: WithOptions; + statementSectionContentDetailsHeaderDescription: WithOptions; + statementSectionContentDetailsHeaderSecondaryTitle: WithOptions; + statementSectionContentDetailsHeaderSecondaryDescription: WithOptions; + statementFooter: WithOptions; + statementFooterLabel: WithOptions; + statementFooterValueContainer: WithOptions; + statementFooterCurrency: WithOptions; + statementFooterValue: WithOptions; + statementCopyButton: WithOptions; menuButton: WithOptions; menuButtonEllipsis: WithOptions; menuList: WithOptions; diff --git a/packages/types/src/commerce.ts b/packages/types/src/commerce.ts index e8c6eea2483..2027e45540e 100644 --- a/packages/types/src/commerce.ts +++ b/packages/types/src/commerce.ts @@ -9,7 +9,7 @@ type WithOptionalOrgType = T & { export interface CommerceBillingNamespace { getPlans: () => Promise; getSubscriptions: (params: GetSubscriptionsParams) => Promise>; - getInvoices: (params: GetInvoicesParams) => Promise>; + getStatements: (params: GetStatementsParams) => Promise>; startCheckout: (params: CreateCheckoutParams) => Promise; } @@ -102,16 +102,33 @@ export interface CommerceInitializedPaymentSourceResource extends ClerkResource externalGatewayId: string; } -export type GetInvoicesParams = WithOptionalOrgType; +export type GetStatementsParams = WithOptionalOrgType; -export type CommerceInvoiceStatus = 'paid' | 'unpaid' | 'past_due'; +export type CommerceStatementStatus = 'open' | 'closed'; -export interface CommerceInvoiceResource extends ClerkResource { +export interface CommerceStatementResource extends ClerkResource { id: string; - totals: CommerceInvoiceTotals; - paymentDueOn: number; - paidOn: number; - status: CommerceInvoiceStatus; + totals: CommerceStatementTotals; + status: CommerceStatementStatus; + timestamp: number; + groups: CommerceStatementGroup[]; +} + +export interface CommerceStatementGroup { + timestamp: number; + items: CommercePayment[]; +} + +export type CommercePaymentChargeType = 'checkout' | 'recurring'; +export type CommercePaymentStatus = 'pending' | 'paid' | 'failed'; + +export interface CommercePayment { + id: string; + amount: CommerceMoney; + paymentSource: CommercePaymentSourceResource; + subscription: CommerceSubscriptionResource; + chargeType: CommercePaymentChargeType; + status: CommercePaymentStatus; } export type GetSubscriptionsParams = WithOptionalOrgType; @@ -126,6 +143,10 @@ export interface CommerceSubscriptionResource extends ClerkResource { periodStart: number; periodEnd: number; canceledAt: number | null; + amount?: CommerceMoney; + credit?: { + amount: CommerceMoney; + }; cancel: (params: CancelSubscriptionParams) => Promise; } @@ -145,7 +166,7 @@ export interface CommerceCheckoutTotals { } // eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface CommerceInvoiceTotals extends Omit {} +export interface CommerceStatementTotals extends Omit {} export type CreateCheckoutParams = WithOptionalOrgType<{ planId: string; @@ -169,7 +190,7 @@ export interface CommerceCheckoutResource extends ClerkResource { id: string; externalClientSecret: string; externalGatewayId: string; - invoice_id: string; + statement_id: string; paymentSource?: CommercePaymentSourceResource; plan: CommercePlanResource; planPeriod: CommerceSubscriptionPlanPeriod; diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index a217d277c37..6041b523525 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -3,8 +3,10 @@ */ import type { - CommerceInvoiceStatus, + CommercePaymentChargeType, CommercePaymentSourceStatus, + CommercePaymentStatus, + CommerceStatementStatus, CommerceSubscriptionPlanPeriod, CommerceSubscriptionStatus, } from './commerce'; @@ -640,18 +642,38 @@ export interface CommerceInitializedPaymentSourceJSON extends ClerkResourceJSON external_gateway_id: string; } -export interface CommerceInvoiceJSON extends ClerkResourceJSON { - object: 'commerce_invoice'; +export interface CommerceStatementJSON extends ClerkResourceJSON { + object: 'commerce_statement'; id: string; - paid_on: number; - payment_due_on: number; - status: CommerceInvoiceStatus; - totals: CommerceInvoiceTotalsJSON; + status: CommerceStatementStatus; + timestamp: number; + groups: CommerceStatementGroupJSON[]; + totals: CommerceStatementTotalsJSON; +} + +export interface CommerceStatementGroupJSON extends ClerkResourceJSON { + object: 'commerce_statement_group'; + timestamp: number; + items: CommercePaymentJSON[]; +} + +export interface CommercePaymentJSON extends ClerkResourceJSON { + object: 'commerce_payment'; + id: string; + amount: CommerceMoneyJSON; + payment_source: CommercePaymentSourceJSON; + subscription: CommerceSubscriptionJSON; + charge_type: CommercePaymentChargeType; + status: CommercePaymentStatus; } export interface CommerceSubscriptionJSON extends ClerkResourceJSON { object: 'commerce_subscription'; id: string; + amount: CommerceMoneyJSON; + credit: { + amount: CommerceMoneyJSON; + }; payment_source_id: string; plan: CommercePlanJSON; plan_period: CommerceSubscriptionPlanPeriod; @@ -677,14 +699,14 @@ export interface CommerceCheckoutTotalsJSON { } // eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface CommerceInvoiceTotalsJSON extends Omit {} +export interface CommerceStatementTotalsJSON extends Omit {} export interface CommerceCheckoutJSON extends ClerkResourceJSON { object: 'commerce_checkout'; id: string; external_client_secret: string; external_gateway_id: string; - invoice_id: string; + statement_id: string; payment_source?: CommercePaymentSourceJSON; plan: CommercePlanJSON; plan_period: CommerceSubscriptionPlanPeriod; diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts index dc6fa8a24dc..23915bb7b7b 100644 --- a/packages/types/src/localization.ts +++ b/packages/types/src/localization.ts @@ -133,7 +133,7 @@ type _LocalizationResource = { lineItems: { title__totalPaid: LocalizationValue; title__paymentMethod: LocalizationValue; - title__invoiceId: LocalizationValue; + title__statementId: LocalizationValue; title__subscriptionBegins: LocalizationValue; }; emailForm: { @@ -696,7 +696,7 @@ type _LocalizationResource = { start: { headerTitle__plans: LocalizationValue; headerTitle__subscriptions: LocalizationValue; - headerTitle__invoices: LocalizationValue; + headerTitle__statements: LocalizationValue; }; subscriptionsListSection: { title: LocalizationValue; @@ -890,7 +890,7 @@ type _LocalizationResource = { start: { headerTitle__plans: LocalizationValue; headerTitle__subscriptions: LocalizationValue; - headerTitle__invoices: LocalizationValue; + headerTitle__statements: LocalizationValue; }; subscriptionsListSection: { title: LocalizationValue;