diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 95d14949135..80403e9e330 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -161,6 +161,7 @@ export const enUS: LocalizationResource = { }, }, reSubscribe: 'Resubscribe', + seats: 'Seats', seeAllFeatures: 'See all features', startFreeTrial: 'Start free trial', startFreeTrial__days: 'Start {{days}}-day free trial', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 4596b067216..0c471cf14ee 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -187,6 +187,7 @@ export type __internal_LocalizationResource = { cancelSubscription: LocalizationValue; keepSubscription: LocalizationValue; reSubscribe: LocalizationValue; + seats: LocalizationValue; subscribe: LocalizationValue; startFreeTrial: LocalizationValue; startFreeTrial__days: LocalizationValue<'days'>; diff --git a/packages/ui/src/components/Subscriptions/SubscriptionsList.tsx b/packages/ui/src/components/Subscriptions/SubscriptionsList.tsx index 43dc1b4a052..aa40bc8ced8 100644 --- a/packages/ui/src/components/Subscriptions/SubscriptionsList.tsx +++ b/packages/ui/src/components/Subscriptions/SubscriptionsList.tsx @@ -1,8 +1,9 @@ import type { BillingPlanResource, BillingSubscriptionItemResource } from '@clerk/shared/types'; -import { useMemo } from 'react'; +import { Fragment, useMemo } from 'react'; import { useProtect } from '@/ui/common/Gate'; import { ProfileSection } from '@/ui/elements/Section'; +import { common } from '@/ui/styledSystem'; import { normalizeFormatted, @@ -14,7 +15,7 @@ import { } from '../../contexts'; import type { LocalizationKey } from '../../customizables'; import { Col, Flex, Icon, localizationKeys, Span, Table, Tbody, Td, Text, Th, Thead, Tr } from '../../customizables'; -import { ArrowsUpDown, CogFilled, Plans, Plus } from '../../icons'; +import { ArrowsUpDown, CogFilled, Plans, Plus, Users } from '../../icons'; import { useRouter } from '../../router'; import { SubscriptionBadge } from './badge'; @@ -50,7 +51,7 @@ export function SubscriptionsList({ const isManageButtonVisible = canManageBilling && !hasActiveFreePlan && subscriptionItems.length > 0; - const sortedSubscriptions = useMemo( + const sortedSubscriptionItems = useMemo( () => subscriptionItems.sort((a, b) => { // always put active subscriptions first @@ -78,7 +79,18 @@ export function SubscriptionsList({ })} > {subscriptionItems.length > 0 && ( - +
({ + overflow: 'hidden', + 'tr > td': { + paddingTop: t.space.$3, + paddingBottom: t.space.$3, + paddingInlineStart: t.space.$3, + paddingInlineEnd: t.space.$3, + }, + })} + tableHeadVisuallyHidden + > - {sortedSubscriptions.map(subscription => ( - ( + ))} @@ -152,78 +164,153 @@ export function SubscriptionsList({ ); } -function SubscriptionRow({ subscription, length }: { subscription: BillingSubscriptionItemResource; length: number }) { +function SubscriptionItemRow({ + subscriptionItem, + length, +}: { + subscriptionItem: BillingSubscriptionItemResource; + length: number; +}) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const fee = subscription.planPeriod === 'annual' ? subscription.plan.annualFee! : subscription.plan.fee; + const fee = subscriptionItem.planPeriod === 'annual' ? subscriptionItem.plan.annualFee! : subscriptionItem.plan.fee!; const { captionForSubscription } = usePlansContext(); const feeFormatted = useMemo(() => { return normalizeFormatted(fee.amountFormatted); }, [fee.amountFormatted]); + + const subItemSeatsQty = subscriptionItem.seats?.quantity; + return ( - - - + { + if (subscriptionItem.status === 'upcoming') { + return { + background: common.mutedBackground(t), + }; + } + + return {}; + }} + > + + + ({ + width: t.sizes.$4, + height: t.sizes.$4, + opacity: t.opacity.$inactive, + color: t.colors.$colorMutedForeground, + })} + /> + ({ marginInlineEnd: t.sizes.$1 })} + > + {subscriptionItem.plan.name} + + {subscriptionItem.isFreeTrial || length > 1 || !!subscriptionItem.canceledAt ? ( + + ) : null} + + + {(!subscriptionItem.plan.isDefault || subscriptionItem.status === 'upcoming') && ( + // here + + )} + + + + + {typeof subItemSeatsQty !== 'undefined' ? ( + { + if (subscriptionItem.status === 'upcoming') { + return { + background: common.mutedBackground(t), + }; + } + + return {}; + }} + > + + + ({ + width: t.sizes.$4, + height: t.sizes.$4, + opacity: t.opacity.$inactive, + color: t.colors.$colorMutedForeground, + })} + /> + ({ marginInlineEnd: t.sizes.$1 })} + localizationKey={localizationKeys('billing.seats')} + /> + + + + - - + + + ) : null} + ); }
-
+
({ + textAlign: 'end', + })} + > + + {fee.currencySymbol} + {feeFormatted} + {fee.amount > 0 && ( + ({ + color: t.colors.$colorMutedForeground, + textTransform: 'lowercase', + ':before': { + content: '"/"', + marginInline: t.space.$1, + }, + })} + localizationKey={ + subscriptionItem.planPeriod === 'annual' + ? localizationKeys('billing.year') + : localizationKeys('billing.month') + } + /> + )} + +
+
({ + textAlign: 'end', + })} > - ({ - width: t.sizes.$4, - height: t.sizes.$4, - opacity: t.opacity.$inactive, - })} - /> ({ marginInlineEnd: t.sizes.$1 })} - > - {subscription.plan.name} - - {subscription.isFreeTrial || length > 1 || !!subscription.canceledAt ? ( - - ) : null} - - - {(!subscription.plan.isDefault || subscription.status === 'upcoming') && ( - // here - - )} - - ({ - textAlign: 'end', - })} - > - - {fee.currencySymbol} - {feeFormatted} - {fee.amount > 0 && ( - ({ - color: t.colors.$colorMutedForeground, - textTransform: 'lowercase', - ':before': { - content: '"/"', - marginInline: t.space.$1, - }, - })} localizationKey={ - subscription.planPeriod === 'annual' - ? localizationKeys('billing.year') - : localizationKeys('billing.month') + subItemSeatsQty === null + ? localizationKeys('billing.pricingTable.seatCost.unlimitedSeats') + : localizationKeys('billing.pricingTable.seatCost.upToSeats', { endsAfterBlock: subItemSeatsQty }) } /> - )} - -