diff --git a/.changeset/soft-toys-flow.md b/.changeset/soft-toys-flow.md new file mode 100644 index 00000000000..376ce55a195 --- /dev/null +++ b/.changeset/soft-toys-flow.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': patch +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Add localizations for some commerce strings, general cleanups diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 28817f657c7..365ebe317b3 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -4,7 +4,7 @@ { "path": "./dist/clerk.browser.js", "maxSize": "69.3KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "113KB" }, { "path": "./dist/clerk.headless*.js", "maxSize": "53KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "106.3KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "107.5KB" }, { "path": "./dist/vendors*.js", "maxSize": "40.2KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, @@ -22,7 +22,7 @@ { "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" }, { "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" }, { "path": "./dist/checkout*.js", "maxSize": "7.25KB" }, - { "path": "./dist/paymentSources*.js", "maxSize": "9.15KB" }, + { "path": "./dist/paymentSources*.js", "maxSize": "9.17KB" }, { "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" }, { "path": "./dist/op-billing-page*.js", "maxSize": "3.0KB" }, { "path": "./dist/sessionTasks*.js", "maxSize": "1KB" } diff --git a/packages/clerk-js/src/core/resources/CommercePayment.ts b/packages/clerk-js/src/core/resources/CommercePayment.ts index 91fa656dfa1..d299130799a 100644 --- a/packages/clerk-js/src/core/resources/CommercePayment.ts +++ b/packages/clerk-js/src/core/resources/CommercePayment.ts @@ -7,14 +7,15 @@ import type { } from '@clerk/types'; import { commerceMoneyFromJSON } from '../../utils'; +import { unixEpochToDate } from '../../utils/date'; import { BaseResource, CommercePaymentSource, CommerceSubscription } from './internal'; export class CommercePayment extends BaseResource implements CommercePaymentResource { id!: string; amount!: CommerceMoney; - failedAt?: number; - paidAt?: number; - updatedAt!: number; + failedAt?: Date; + paidAt?: Date; + updatedAt!: Date; paymentSource!: CommercePaymentSource; subscription!: CommerceSubscription; subscriptionItem!: CommerceSubscription; @@ -33,9 +34,9 @@ export class CommercePayment extends BaseResource implements CommercePaymentReso this.id = data.id; this.amount = commerceMoneyFromJSON(data.amount); - this.paidAt = data.paid_at; - this.failedAt = data.failed_at; - this.updatedAt = data.updated_at; + this.paidAt = data.paid_at ? unixEpochToDate(data.paid_at) : undefined; + this.failedAt = data.failed_at ? unixEpochToDate(data.failed_at) : undefined; + this.updatedAt = unixEpochToDate(data.updated_at); this.paymentSource = new CommercePaymentSource(data.payment_source); this.subscription = new CommerceSubscription(data.subscription); this.subscriptionItem = new CommerceSubscription(data.subscription_item); diff --git a/packages/clerk-js/src/core/resources/CommercePaymentSource.ts b/packages/clerk-js/src/core/resources/CommercePaymentSource.ts index 22dfa9aed1f..8a2583ac66d 100644 --- a/packages/clerk-js/src/core/resources/CommercePaymentSource.ts +++ b/packages/clerk-js/src/core/resources/CommercePaymentSource.ts @@ -88,7 +88,7 @@ export class CommerceInitializedPaymentSource extends BaseResource implements Co this.externalClientSecret = data.external_client_secret; this.externalGatewayId = data.external_gateway_id; - this.paymentMethodOrder = data.payment_method_order; + this.paymentMethodOrder = data.payment_method_order ?? ['card']; return this; } } diff --git a/packages/clerk-js/src/core/resources/CommerceSettings.ts b/packages/clerk-js/src/core/resources/CommerceSettings.ts index bce56538fb1..104ce856c43 100644 --- a/packages/clerk-js/src/core/resources/CommerceSettings.ts +++ b/packages/clerk-js/src/core/resources/CommerceSettings.ts @@ -23,11 +23,10 @@ export class CommerceSettings extends BaseResource implements CommerceSettingsRe return this; } - // TODO(@commerce): Remove `?.` once we launch. - this.billing.stripePublishableKey = data?.billing?.stripe_publishable_key || ''; - this.billing.enabled = data?.billing?.enabled || false; - this.billing.hasPaidUserPlans = data?.billing?.has_paid_user_plans || false; - this.billing.hasPaidOrgPlans = data?.billing?.has_paid_org_plans || false; + this.billing.stripePublishableKey = data.billing.stripe_publishable_key || ''; + this.billing.enabled = data.billing.enabled || false; + this.billing.hasPaidUserPlans = data.billing.has_paid_user_plans || false; + this.billing.hasPaidOrgPlans = data.billing.has_paid_org_plans || false; return this; } diff --git a/packages/clerk-js/src/core/resources/CommerceStatement.ts b/packages/clerk-js/src/core/resources/CommerceStatement.ts index b51f6b71957..0ae065dbd63 100644 --- a/packages/clerk-js/src/core/resources/CommerceStatement.ts +++ b/packages/clerk-js/src/core/resources/CommerceStatement.ts @@ -7,12 +7,13 @@ import type { } from '@clerk/types'; import { commerceTotalsFromJSON } from '../../utils'; +import { unixEpochToDate } from '../../utils/date'; import { BaseResource, CommercePayment } from './internal'; export class CommerceStatement extends BaseResource implements CommerceStatementResource { id!: string; status!: CommerceStatementStatus; - timestamp!: number; + timestamp!: Date; totals!: CommerceStatementTotals; groups!: CommerceStatementGroup[]; @@ -28,7 +29,7 @@ export class CommerceStatement extends BaseResource implements CommerceStatement this.id = data.id; this.status = data.status; - this.timestamp = data.timestamp; + this.timestamp = unixEpochToDate(data.timestamp); this.totals = commerceTotalsFromJSON(data.totals); this.groups = data.groups.map(group => new CommerceStatementGroup(group)); return this; @@ -37,7 +38,7 @@ export class CommerceStatement extends BaseResource implements CommerceStatement export class CommerceStatementGroup { id!: string; - timestamp!: number; + timestamp!: Date; items!: CommercePayment[]; constructor(data: CommerceStatementGroupJSON) { @@ -50,7 +51,7 @@ export class CommerceStatementGroup { } this.id = data.id; - this.timestamp = data.timestamp; + this.timestamp = unixEpochToDate(data.timestamp); this.items = data.items.map(item => new CommercePayment(item)); return this; } diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationPlansPage.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationPlansPage.tsx index d675adc27ef..93b8acefdc4 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationPlansPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationPlansPage.tsx @@ -22,7 +22,9 @@ const OrganizationPlansPageInternal = () => { paddingBlockEnd: t.space.$4, })} > - void navigate('../', { searchParams: new URLSearchParams('tab=plans') })}> + void navigate('../', { searchParams: new URLSearchParams('tab=subscriptions') })} + > { const { params, navigate } = useRouter(); const { isLoading } = useStatements(); const { getPaymentAttemptById } = usePaymentAttemptsContext(); + const localizationRoot = useSubscriberTypeLocalizationRoot(); const paymentAttempt = params.paymentAttemptId ? getPaymentAttemptById(params.paymentAttemptId) : null; const subscriptionItem = paymentAttempt?.subscriptionItem; @@ -39,10 +41,6 @@ export const PaymentAttemptPage = () => { ); } - if (!paymentAttempt) { - return Payment attempt not found; - } - return ( <> { > void navigate('../../', { searchParams: new URLSearchParams('tab=payments') })}> - - ({ - borderWidth: t.borderWidths.$normal, - borderStyle: t.borderStyles.$solid, - borderColor: t.colors.$neutralAlpha100, - borderRadius: t.radii.$lg, - overflow: 'clip', - })} - > + {!paymentAttempt ? ( + + ) : ( ({ - padding: t.space.$4, - background: t.colors.$neutralAlpha25, - display: 'flex', - justifyContent: 'space-between', - alignItems: 'flex-start', + borderWidth: t.borderWidths.$normal, + borderStyle: t.borderStyles.$solid, + borderColor: t.colors.$neutralAlpha100, + borderRadius: t.radii.$lg, + overflow: 'clip', })} > - - - ({ - display: 'flex', - alignItems: 'center', - gap: t.space.$0x25, - color: t.colors.$colorTextSecondary, - })} - > - ({ + 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(paymentAttempt.id)} - + + + {truncateWithEndVisible(paymentAttempt.id)} + + - - + {paymentAttempt.status} + + + ({ + padding: t.space.$4, + })} > - {paymentAttempt.status} - - - ({ - padding: t.space.$4, - })} - > - {subscriptionItem && ( - - - - - - - - - - {subscriptionItem.credit && subscriptionItem.credit.amount.amount > 0 && ( - - + {subscriptionItem && ( + + + - )} - - )} - - ({ - 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', - })} - > - - + + + + {subscriptionItem.credit && subscriptionItem.credit.amount.amount > 0 && ( + + + + + )} + + )} + + ({ + 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', - alignItems: 'center', - gap: t.space.$2x5, + justifyContent: 'space-between', })} > - - USD - + ({ + display: 'flex', + alignItems: 'center', + gap: t.space.$2x5, + })} > - {paymentAttempt.amount.currencySymbol} - {paymentAttempt.amount.amountFormatted} - - + + {paymentAttempt.amount.currency} + + + {paymentAttempt.amount.currencySymbol} + {paymentAttempt.amount.amountFormatted} + + + - + )} ); }; diff --git a/packages/clerk-js/src/ui/components/PaymentAttempts/PaymentAttemptsList.tsx b/packages/clerk-js/src/ui/components/PaymentAttempts/PaymentAttemptsList.tsx index 76ec97c9413..ffa3bea2d7a 100644 --- a/packages/clerk-js/src/ui/components/PaymentAttempts/PaymentAttemptsList.tsx +++ b/packages/clerk-js/src/ui/components/PaymentAttempts/PaymentAttemptsList.tsx @@ -1,14 +1,11 @@ import type { CommercePaymentResource } from '@clerk/types'; -import React from 'react'; -import { Pagination } from '@/ui/elements/Pagination'; +import { DataTable, DataTableRow } from '@/ui/elements/DataTable'; -import { usePaymentAttempts } from '../../../ui/contexts'; -import type { LocalizationKey } from '../../customizables'; -import { Badge, Col, descriptors, Flex, Spinner, Table, Tbody, Td, Text, Th, Thead, Tr } from '../../customizables'; +import { usePaymentAttempts, useSubscriberTypeLocalizationRoot } from '../../contexts'; +import { Badge, localizationKeys, Td, Text } from '../../customizables'; import { useRouter } from '../../router'; -import type { PropsOfComponent } from '../../styledSystem'; -import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible'; +import { formatDate, truncateWithEndVisible } from '../../utils'; /* ------------------------------------------------------------------------------------------------- * PaymentAttemptsList @@ -16,6 +13,7 @@ import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible'; export const PaymentAttemptsList = () => { const { data: paymentAttempts, isLoading } = usePaymentAttempts(); + const localizationRoot = useSubscriberTypeLocalizationRoot(); return ( { pageCount={1} itemsPerPage={10} isLoading={isLoading} - emptyStateLocalizationKey='No payment history' - headers={['Date', 'Amount', 'Status']} + emptyStateLocalizationKey={localizationKeys(`${localizationRoot}.billingPage.paymentHistorySection.empty`)} + headers={[ + localizationKeys(`${localizationRoot}.billingPage.paymentHistorySection.tableHeader__date`), + localizationKeys(`${localizationRoot}.billingPage.paymentHistorySection.tableHeader__amount`), + localizationKeys(`${localizationRoot}.billingPage.paymentHistorySection.tableHeader__status`), + ]} rows={(paymentAttempts?.data || []).map(i => ( - - {new Date(paidAt || failedAt || updatedAt).toLocaleString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - })} - + {formatDate(paidAt || failedAt || updatedAt, 'long')} ); }; - -/* ------------------------------------------------------------------------------------------------- - * DataTable - * -----------------------------------------------------------------------------------------------*/ - -type DataTableProps = { - headers: (LocalizationKey | string)[]; - rows: React.ReactNode[]; - isLoading?: boolean; - page: number; - onPageChange: (page: number) => void; - itemCount: number; - emptyStateLocalizationKey: LocalizationKey | string; - pageCount: number; - itemsPerPage: number; -}; - -const DataTable = (props: DataTableProps) => { - const { - headers, - page, - onPageChange, - rows, - isLoading, - itemCount, - itemsPerPage, - pageCount, - emptyStateLocalizationKey, - } = props; - - const startingRow = itemCount > 0 ? Math.max(0, (page - 1) * itemsPerPage) + 1 : 0; - const endingRow = Math.min(page * itemsPerPage, itemCount); - - return ( - - ({ overflowX: 'auto', padding: t.space.$1 })}> - - - - {headers.map((h, index) => ( - - - - {isLoading ? ( - - - - ) : !rows.length ? ( - - ) : ( - rows - )} - -
- ))} -
- -
-
- {pageCount > 1 && ( - - )} - - ); -}; - -const DataTableEmptyRow = (props: { localizationKey: LocalizationKey | string }) => { - return ( - - - - - - ); -}; - -const DataTableRow = (props: PropsOfComponent) => { - return ( - ({ ':hover': { backgroundColor: t.colors.$neutralAlpha50 } })} - /> - ); -}; diff --git a/packages/clerk-js/src/ui/components/PaymentSources/AddPaymentSource.tsx b/packages/clerk-js/src/ui/components/PaymentSources/AddPaymentSource.tsx index 524425d5cec..ff21cd07e2e 100644 --- a/packages/clerk-js/src/ui/components/PaymentSources/AddPaymentSource.tsx +++ b/packages/clerk-js/src/ui/components/PaymentSources/AddPaymentSource.tsx @@ -15,7 +15,7 @@ import { FormButtons } from '@/ui/elements/FormButtons'; import { FormContainer } from '@/ui/elements/FormContainer'; import { clerkUnsupportedEnvironmentWarning } from '../../../core/errors'; -import { useEnvironment, useSubscriberTypeContext } from '../../contexts'; +import { useEnvironment, useSubscriberTypeContext, useSubscriberTypeLocalizationRoot } from '../../contexts'; import { descriptors, Flex, localizationKeys, Spinner, useAppearance, useLocalizations } from '../../customizables'; import type { LocalizationKey } from '../../localization'; import { handleError, normalizeColorString } from '../../utils'; @@ -236,6 +236,7 @@ const AddPaymentSourceForm = ({ children }: PropsWithChildren) => { const elements = useElements(); const { displayConfig } = useEnvironment(); const { t } = useLocalizations(); + const localizationRoot = useSubscriberTypeLocalizationRoot(); const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -260,10 +261,8 @@ const AddPaymentSourceForm = ({ children }: PropsWithChildren) => { try { await onSuccess({ stripeSetupIntent: setupIntent }); } catch (error) { - console.log('catch', error); void handleError(error, [], card.setError); } finally { - console.log('finally'); card.setIdle(); initializePaymentSource(); // resets the payment intent } @@ -290,8 +289,7 @@ const AddPaymentSourceForm = ({ children }: PropsWithChildren) => { type: 'tabs', defaultCollapsed: false, }, - // TODO(@COMMERCE): Should this be fetched from the fapi? - paymentMethodOrder: paymentMethodOrder || ['card'], + paymentMethodOrder, applePay: checkout ? { recurringPaymentRequest: { @@ -311,7 +309,8 @@ const AddPaymentSourceForm = ({ children }: PropsWithChildren) => { void const { close } = useActionContext(); const clerk = useClerk(); const subscriberType = useSubscriberTypeContext(); + const localizationRoot = useSubscriberTypeLocalizationRoot(); const onAddPaymentSourceSuccess = async (context: { stripeSetupIntent?: SetupIntent }) => { const resource = subscriberType === 'org' ? clerk?.organization : clerk.user; @@ -40,9 +41,11 @@ const AddScreen = withCardStateProvider(({ onSuccess }: { onSuccess: () => void onSuccess={onAddPaymentSourceSuccess} cancelAction={close} > - + @@ -62,6 +65,7 @@ const RemoveScreen = ({ const card = useCardState(); const subscriberType = useSubscriberTypeContext(); const { organization } = useOrganization(); + const localizationRoot = useSubscriberTypeLocalizationRoot(); const ref = useRef( `${paymentSource.paymentMethod === 'card' ? paymentSource.cardType : paymentSource.paymentMethod} ${paymentSource.paymentMethod === 'card' ? `⋯ ${paymentSource.last4}` : '-'}`, ); @@ -81,14 +85,22 @@ const RemoveScreen = ({ return ( { const clerk = useClerk(); const subscriberType = useSubscriberTypeContext(); - + const localizationRoot = useSubscriberTypeLocalizationRoot(); const resource = subscriberType === 'org' ? clerk?.organization : clerk.user; const { data, isLoading, mutate: mutatePaymentSources } = usePaymentSources(); @@ -119,7 +131,7 @@ export const PaymentSources = withCardStateProvider(() => { return ( ({ @@ -161,7 +173,7 @@ export const PaymentSources = withCardStateProvider(() => { @@ -188,10 +200,11 @@ const PaymentSourceMenu = ({ const card = useCardState(); const { organization } = useOrganization(); const subscriberType = useSubscriberTypeContext(); + const localizationRoot = useSubscriberTypeLocalizationRoot(); const actions = [ { - label: localizationKeys('userProfile.billingPage.paymentSourcesSection.actionLabel__remove'), + label: localizationKeys(`${localizationRoot}.billingPage.paymentSourcesSection.actionLabel__remove`), isDestructive: true, onClick: () => open(`remove-${paymentSource.id}`), isDisabled: !paymentSource.isRemovable, @@ -200,7 +213,7 @@ const PaymentSourceMenu = ({ if (!paymentSource.isDefault) { actions.unshift({ - label: localizationKeys('userProfile.billingPage.paymentSourcesSection.actionLabel__default'), + label: localizationKeys(`${localizationRoot}.billingPage.paymentSourcesSection.actionLabel__default`), isDestructive: false, onClick: () => { paymentSource diff --git a/packages/clerk-js/src/ui/components/Statements/Statement.tsx b/packages/clerk-js/src/ui/components/Statements/Statement.tsx index ae10b73ff7a..1192e5328ab 100644 --- a/packages/clerk-js/src/ui/components/Statements/Statement.tsx +++ b/packages/clerk-js/src/ui/components/Statements/Statement.tsx @@ -175,8 +175,8 @@ function SectionContentDetailsHeader({ }: { title: string | LocalizationKey; description: string | LocalizationKey; - secondaryTitle: string | LocalizationKey; - secondaryDescription: string | LocalizationKey; + secondaryTitle?: string | LocalizationKey; + secondaryDescription?: string | LocalizationKey; }) { return ( - - + {secondaryTitle && ( + + )} + {secondaryDescription && ( + + )} ); diff --git a/packages/clerk-js/src/ui/components/Statements/StatementPage.tsx b/packages/clerk-js/src/ui/components/Statements/StatementPage.tsx index 67ac76301f6..e473576376d 100644 --- a/packages/clerk-js/src/ui/components/Statements/StatementPage.tsx +++ b/packages/clerk-js/src/ui/components/Statements/StatementPage.tsx @@ -1,7 +1,8 @@ import { Header } from '@/ui/elements/Header'; +import { formatDate } from '@/ui/utils/formatDate'; -import { useStatements, useStatementsContext } from '../../contexts'; -import { Box, descriptors, Spinner, Text } from '../../customizables'; +import { useStatements, useStatementsContext, useSubscriberTypeLocalizationRoot } from '../../contexts'; +import { Box, descriptors, localizationKeys, Spinner, Text, useLocalizations } from '../../customizables'; import { Plus, RotateLeftRight } from '../../icons'; import { useRouter } from '../../router'; import { Statement } from './Statement'; @@ -10,7 +11,8 @@ export const StatementPage = () => { const { params, navigate } = useRouter(); const { isLoading } = useStatements(); const { getStatementById } = useStatementsContext(); - + const localizationRoot = useSubscriberTypeLocalizationRoot(); + const { t } = useLocalizations(); const statement = params.statementId ? getStatementById(params.statementId) : null; if (isLoading) { @@ -25,10 +27,6 @@ export const StatementPage = () => { ); } - if (!statement) { - return Statement not found; - } - return ( <> { onClick={() => void navigate('../../', { searchParams: new URLSearchParams('tab=statements') })} >
- - - - {statement.groups.map(group => ( - - - - {group.items.map(item => ( - - - - + + + {statement.groups.map(group => ( + + + + {group.items.map(item => ( + + - {item.subscription.credit && item.subscription.credit.amount.amount > 0 ? ( + - ) : null} - - - ))} - - - ))} - - - + {item.subscription.credit && item.subscription.credit.amount.amount > 0 ? ( + + ) : null} + + + ))} + + + ))} + + + + )} ); }; diff --git a/packages/clerk-js/src/ui/components/Statements/StatementsList.tsx b/packages/clerk-js/src/ui/components/Statements/StatementsList.tsx index 836f85de03a..0d8e3a855c5 100644 --- a/packages/clerk-js/src/ui/components/Statements/StatementsList.tsx +++ b/packages/clerk-js/src/ui/components/Statements/StatementsList.tsx @@ -1,13 +1,11 @@ import type { CommerceStatementResource } from '@clerk/types'; -import React from 'react'; -import { Pagination } from '@/ui/elements/Pagination'; +import { DataTable, DataTableRow } from '@/ui/elements/DataTable'; +import { formatDate } from '@/ui/utils/formatDate'; -import { useStatements } from '../../../ui/contexts'; -import type { LocalizationKey } from '../../customizables'; -import { Col, descriptors, Flex, Spinner, Table, Tbody, Td, Text, Th, Thead, Tr } from '../../customizables'; +import { useStatements, useSubscriberTypeLocalizationRoot } from '../../../ui/contexts'; +import { localizationKeys, Td, Text } from '../../customizables'; import { useRouter } from '../../router'; -import type { PropsOfComponent } from '../../styledSystem'; import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible'; /* ------------------------------------------------------------------------------------------------- @@ -16,6 +14,7 @@ import { truncateWithEndVisible } from '../../utils/truncateTextWithEndVisible'; export const StatementsList = () => { const { data: statements, isLoading } = useStatements(); + const localizationRoot = useSubscriberTypeLocalizationRoot(); return ( { pageCount={1} itemsPerPage={10} isLoading={isLoading} - emptyStateLocalizationKey='No statements to display' - headers={['Date', 'Amount']} + emptyStateLocalizationKey={localizationKeys(`${localizationRoot}.billingPage.statementsSection.empty`)} + headers={[ + localizationKeys(`${localizationRoot}.billingPage.statementsSection.tableHeader__date`), + localizationKeys(`${localizationRoot}.billingPage.statementsSection.tableHeader__amount`), + ]} rows={(statements?.data || []).map(i => ( - - {new Date(timestamp).toLocaleString('en-US', { month: 'long', year: 'numeric' })} - + {formatDate(timestamp, 'monthyear')} ); }; - -/* ------------------------------------------------------------------------------------------------- - * DataTable - * -----------------------------------------------------------------------------------------------*/ - -type DataTableProps = { - headers: (LocalizationKey | string)[]; - rows: React.ReactNode[]; - isLoading?: boolean; - page: number; - onPageChange: (page: number) => void; - itemCount: number; - emptyStateLocalizationKey: LocalizationKey | string; - pageCount: number; - itemsPerPage: number; -}; - -const DataTable = (props: DataTableProps) => { - const { - headers, - page, - onPageChange, - rows, - isLoading, - itemCount, - itemsPerPage, - pageCount, - emptyStateLocalizationKey, - } = props; - - const startingRow = itemCount > 0 ? Math.max(0, (page - 1) * itemsPerPage) + 1 : 0; - const endingRow = Math.min(page * itemsPerPage, itemCount); - - return ( - - ({ overflowX: 'auto', padding: t.space.$1 })}> - - - - {headers.map((h, index) => ( - - - - {isLoading ? ( - - - - ) : !rows.length ? ( - - ) : ( - rows - )} - -
- ))} -
- -
-
- {pageCount > 1 && ( - - )} - - ); -}; - -const DataTableEmptyRow = (props: { localizationKey: LocalizationKey | string }) => { - return ( - - - - - - ); -}; - -const DataTableRow = (props: PropsOfComponent) => { - return ( - ({ ':hover': { backgroundColor: t.colors.$neutralAlpha50 } })} - /> - ); -}; diff --git a/packages/clerk-js/src/ui/components/Subscriptions/SubscriptionsList.tsx b/packages/clerk-js/src/ui/components/Subscriptions/SubscriptionsList.tsx index c5cf4d702c1..4c2645deab3 100644 --- a/packages/clerk-js/src/ui/components/Subscriptions/SubscriptionsList.tsx +++ b/packages/clerk-js/src/ui/components/Subscriptions/SubscriptionsList.tsx @@ -3,7 +3,12 @@ import type { CommerceSubscriptionResource } from '@clerk/types'; import { ProfileSection } from '@/ui/elements/Section'; import { useProtect } from '../../common'; -import { usePlansContext, useSubscriberTypeContext, useSubscriptions } from '../../contexts'; +import { + usePlansContext, + useSubscriberTypeContext, + useSubscriberTypeLocalizationRoot, + useSubscriptions, +} from '../../contexts'; import type { LocalizationKey } from '../../customizables'; import { Badge, @@ -34,6 +39,7 @@ export function SubscriptionsList({ arrowButtonEmptyText: LocalizationKey; }) { const { handleSelectPlan, captionForSubscription, canManageSubscription } = usePlansContext(); + const localizationRoot = useSubscriberTypeLocalizationRoot(); const subscriberType = useSubscriberTypeContext(); const { data: subscriptions } = useSubscriptions(); const canManageBilling = useProtect( @@ -79,9 +85,21 @@ export function SubscriptionsList({ - - - + diff --git a/packages/clerk-js/src/ui/components/UserProfile/PlansPage.tsx b/packages/clerk-js/src/ui/components/UserProfile/PlansPage.tsx index 4acf49cb1c8..cf2ea151153 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/PlansPage.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/PlansPage.tsx @@ -19,7 +19,9 @@ const PlansPageInternal = () => { paddingBlockEnd: t.space.$4, })} > - void navigate('../', { searchParams: new URLSearchParams('tab=plans') })}> + void navigate('../', { searchParams: new URLSearchParams('tab=subscriptions') })} + > (DEFAUlT); export const useSubscriberTypeContext = () => useContext(SubscriberTypeContext) || DEFAUlT; + +export const useSubscriberTypeLocalizationRoot = () => { + const subscriberType = useSubscriberTypeContext(); + return subscriberType === 'user' ? 'userProfile' : 'organizationProfile'; +}; diff --git a/packages/clerk-js/src/ui/elements/DataTable.tsx b/packages/clerk-js/src/ui/elements/DataTable.tsx new file mode 100644 index 00000000000..3cd5a35060f --- /dev/null +++ b/packages/clerk-js/src/ui/elements/DataTable.tsx @@ -0,0 +1,122 @@ +import React from 'react'; + +import type { LocalizationKey } from '../customizables'; +import { Col, descriptors, Flex, Spinner, Table, Tbody, Td, Text, Th, Thead, Tr } from '../customizables'; +import type { PropsOfComponent } from '../styledSystem'; +import { Pagination } from './Pagination'; + +/* ------------------------------------------------------------------------------------------------- + * DataTable + * -----------------------------------------------------------------------------------------------*/ + +export type DataTableProps = { + headers: (LocalizationKey | string)[]; + rows: React.ReactNode[]; + isLoading?: boolean; + page: number; + onPageChange: (page: number) => void; + itemCount: number; + emptyStateLocalizationKey: LocalizationKey | string; + pageCount: number; + itemsPerPage: number; +}; + +export const DataTable = (props: DataTableProps) => { + const { + headers, + page, + onPageChange, + rows, + isLoading, + itemCount, + itemsPerPage, + pageCount, + emptyStateLocalizationKey, + } = props; + + const startingRow = itemCount > 0 ? Math.max(0, (page - 1) * itemsPerPage) + 1 : 0; + const endingRow = Math.min(page * itemsPerPage, itemCount); + + return ( + + ({ overflowX: 'auto', padding: t.space.$1 })}> +
PlanStart dateEdit + +
+ + + {headers.map((h, index) => ( + + + + {isLoading ? ( + + + + ) : !rows.length ? ( + + ) : ( + rows + )} + +
+ ))} +
+ +
+ + {pageCount > 1 && ( + + )} + + ); +}; + +export const DataTableEmptyRow = (props: { localizationKey: LocalizationKey | string }) => { + return ( + + + + + + ); +}; + +export const DataTableRow = (props: PropsOfComponent) => { + return ( + ({ ':hover': { backgroundColor: t.colors.$neutralAlpha50 } })} + /> + ); +}; diff --git a/packages/clerk-js/src/ui/utils/formatDate.ts b/packages/clerk-js/src/ui/utils/formatDate.ts index ff0da612847..3c227f12e91 100644 --- a/packages/clerk-js/src/ui/utils/formatDate.ts +++ b/packages/clerk-js/src/ui/utils/formatDate.ts @@ -1,11 +1,13 @@ -export function formatDate(date: Date, format: 'short' | 'long' = 'long', locale: string = 'en-US'): string { +export function formatDate( + date: Date, + format: 'monthyear' | 'short' | 'long' = 'long', + locale: string = 'en-US', +): string { const options: Intl.DateTimeFormatOptions = { month: format === 'short' ? 'short' : 'long', - day: 'numeric', + ...(format !== 'monthyear' && { day: 'numeric' }), + ...(format !== 'short' && { year: 'numeric' }), }; - if (format === 'long') { - options.year = 'numeric'; - } return new Intl.DateTimeFormat(locale, options).format(date); } diff --git a/packages/clerk-js/src/ui/utils/index.ts b/packages/clerk-js/src/ui/utils/index.ts index 55f1d9cb815..3d801b95b71 100644 --- a/packages/clerk-js/src/ui/utils/index.ts +++ b/packages/clerk-js/src/ui/utils/index.ts @@ -29,3 +29,4 @@ export * from './web3CallbackErrorHandler'; export * from './originPrefersPopup'; export * from './normalizeColorString'; export * from './formatDate'; +export * from './truncateTextWithEndVisible'; diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index e8e829d98e4..40036228dc4 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -93,6 +93,7 @@ export const enUS: LocalizationResource = { switchPlan: 'Switch to this plan', switchToAnnual: 'Switch to annual', switchToMonthly: 'Switch to monthly', + totalDue: 'Total due', totalDueToday: 'Total Due Today', viewFeatures: 'View features', year: 'Year', @@ -190,6 +191,13 @@ export const enUS: LocalizationResource = { badge__manualInvitation: 'No automatic enrollment', badge__unverified: 'Unverified', billingPage: { + paymentHistorySection: { + empty: 'No payment history', + tableHeader__date: 'Date', + tableHeader__amount: 'Amount', + tableHeader__status: 'Status', + notFound: 'Payment attempt not found', + }, paymentSourcesSection: { actionLabel__default: 'Make default', actionLabel__remove: 'Remove', @@ -212,11 +220,25 @@ export const enUS: LocalizationResource = { headerTitle__payments: 'Payments', headerTitle__plans: 'Plans', headerTitle__statements: 'Statements', - headerTitle__subscriptions: 'Subscriptions', + headerTitle__subscriptions: 'Subscription', + }, + statementsSection: { + empty: 'No statements to display', + itemCaption__paidForPlan: 'Paid for {{plan}} {{period}} plan', + itemCaption__proratedCredit: 'Prorated credit for partial usage of previous subscription', + itemCaption__subscribedAndPaidForPlan: 'Subscribed and paid for {{plan}} {{period}} plan', + notFound: 'Statement not found', + tableHeader__date: 'Date', + tableHeader__amount: 'Amount', + title: 'Statements', + totalPaid: 'Total paid', }, subscriptionsListSection: { actionLabel__newSubscription: 'Subscribe to a plan', actionLabel__switchPlan: 'Switch plans', + tableHeader__plan: 'Plan', + tableHeader__startDate: 'Start date', + tableHeader__edit: 'Edit', title: 'Subscription', }, subscriptionsSection: { @@ -818,6 +840,13 @@ export const enUS: LocalizationResource = { title__codelist: 'Backup codes', }, billingPage: { + paymentHistorySection: { + empty: 'No payment history', + tableHeader__date: 'Date', + tableHeader__amount: 'Amount', + tableHeader__status: 'Status', + notFound: 'Payment attempt not found', + }, paymentSourcesSection: { actionLabel__default: 'Make default', actionLabel__remove: 'Remove', @@ -842,9 +871,23 @@ export const enUS: LocalizationResource = { headerTitle__statements: 'Statements', headerTitle__subscriptions: 'Subscription', }, + statementsSection: { + empty: 'No statements to display', + itemCaption__paidForPlan: 'Paid for {{plan}} {{period}} plan', + itemCaption__proratedCredit: 'Prorated credit for partial usage of previous subscription', + itemCaption__subscribedAndPaidForPlan: 'Subscribed and paid for {{plan}} {{period}} plan', + notFound: 'Statement not found', + tableHeader__date: 'Date', + tableHeader__amount: 'Amount', + title: 'Statements', + totalPaid: 'Total paid', + }, subscriptionsListSection: { actionLabel__newSubscription: 'Subscribe to a plan', actionLabel__switchPlan: 'Switch plans', + tableHeader__plan: 'Plan', + tableHeader__startDate: 'Start date', + tableHeader__edit: 'Edit', title: 'Subscription', }, subscriptionsSection: { diff --git a/packages/types/src/commerce.ts b/packages/types/src/commerce.ts index 7100a4d4b2c..8b27f61d081 100644 --- a/packages/types/src/commerce.ts +++ b/packages/types/src/commerce.ts @@ -116,9 +116,9 @@ export type CommercePaymentStatus = 'pending' | 'paid' | 'failed'; export interface CommercePaymentResource extends ClerkResource { id: string; amount: CommerceMoney; - paidAt?: number; - failedAt?: number; - updatedAt: number; + paidAt?: Date; + failedAt?: Date; + updatedAt: Date; paymentSource: CommercePaymentSourceResource; subscription: CommerceSubscriptionResource; subscriptionItem: CommerceSubscriptionResource; @@ -136,12 +136,12 @@ export interface CommerceStatementResource extends ClerkResource { id: string; totals: CommerceStatementTotals; status: CommerceStatementStatus; - timestamp: number; + timestamp: Date; groups: CommerceStatementGroup[]; } export interface CommerceStatementGroup { - timestamp: number; + timestamp: Date; items: CommercePaymentResource[]; } diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts index 4208d30e653..150b8a32241 100644 --- a/packages/types/src/localization.ts +++ b/packages/types/src/localization.ts @@ -131,6 +131,7 @@ type _LocalizationResource = { subtotal: LocalizationValue; credit: LocalizationValue; creditRemainder: LocalizationValue; + totalDue: LocalizationValue; totalDueToday: LocalizationValue; pastDue: LocalizationValue; paymentMethods: LocalizationValue; @@ -762,14 +763,35 @@ type _LocalizationResource = { headerTitle__subscriptions: LocalizationValue; headerTitle__statements: LocalizationValue; }; + statementsSection: { + empty: LocalizationValue; + itemCaption__paidForPlan: LocalizationValue; + itemCaption__proratedCredit: LocalizationValue; + itemCaption__subscribedAndPaidForPlan: LocalizationValue; + notFound: LocalizationValue; + tableHeader__date: LocalizationValue; + tableHeader__amount: LocalizationValue; + title: LocalizationValue; + totalPaid: LocalizationValue; + }; switchPlansSection: { title: LocalizationValue; }; subscriptionsListSection: { + tableHeader__plan: LocalizationValue; + tableHeader__startDate: LocalizationValue; + tableHeader__edit: LocalizationValue; title: LocalizationValue; actionLabel__newSubscription: LocalizationValue; actionLabel__switchPlan: LocalizationValue; }; + paymentHistorySection: { + empty: LocalizationValue; + notFound: LocalizationValue; + tableHeader__date: LocalizationValue; + tableHeader__amount: LocalizationValue; + tableHeader__status: LocalizationValue; + }; paymentSourcesSection: { title: LocalizationValue; add: LocalizationValue; @@ -967,14 +989,35 @@ type _LocalizationResource = { headerTitle__subscriptions: LocalizationValue; headerTitle__statements: LocalizationValue; }; + statementsSection: { + empty: LocalizationValue; + itemCaption__paidForPlan: LocalizationValue; + itemCaption__proratedCredit: LocalizationValue; + itemCaption__subscribedAndPaidForPlan: LocalizationValue; + notFound: LocalizationValue; + tableHeader__date: LocalizationValue; + tableHeader__amount: LocalizationValue; + title: LocalizationValue; + totalPaid: LocalizationValue; + }; switchPlansSection: { title: LocalizationValue; }; subscriptionsListSection: { + tableHeader__plan: LocalizationValue; + tableHeader__startDate: LocalizationValue; + tableHeader__edit: LocalizationValue; title: LocalizationValue; actionLabel__newSubscription: LocalizationValue; actionLabel__switchPlan: LocalizationValue; }; + paymentHistorySection: { + empty: LocalizationValue; + notFound: LocalizationValue; + tableHeader__date: LocalizationValue; + tableHeader__amount: LocalizationValue; + tableHeader__status: LocalizationValue; + }; paymentSourcesSection: { title: LocalizationValue; add: LocalizationValue;