Update billing schema and enhance configuration display
Updated the billing schema to include a more descriptive line item and an optional tiers element. Also, billing configuration was refactored and displayed more prominently in the UI. The plan feature listing now utilizes checkmarks to denote each feature and the product details are more clearly displayed.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { Plus, PlusCircle } from 'lucide-react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { LineItemSchema } from '@kit/billing';
|
||||
@@ -5,6 +6,9 @@ import { formatCurrency } from '@kit/shared/utils';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
const className =
|
||||
'flex text-secondary-foreground items-center justify-between text-sm';
|
||||
|
||||
export function LineItemDetails(
|
||||
props: React.PropsWithChildren<{
|
||||
lineItems: z.infer<typeof LineItemSchema>[];
|
||||
@@ -12,22 +16,42 @@ export function LineItemDetails(
|
||||
selectedInterval?: string | undefined;
|
||||
}>,
|
||||
) {
|
||||
const className =
|
||||
'flex items-center justify-between text-sm border-b border-dashed pb-2 last:border-transparent';
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div className={'flex flex-col space-y-1 px-1'}>
|
||||
{props.lineItems.map((item) => {
|
||||
// If the item has a description, we render it as a simple text
|
||||
// and pass the item as values to the translation so we can use
|
||||
// the item properties in the translation.
|
||||
if (item.description) {
|
||||
return (
|
||||
<div key={item.id} className={className}>
|
||||
<span className={'flex items-center space-x-1.5'}>
|
||||
<Plus className={'w-4'} />
|
||||
|
||||
<Trans
|
||||
i18nKey={item.description}
|
||||
values={item}
|
||||
defaults={item.description}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case 'base':
|
||||
return (
|
||||
<div key={item.id} className={className}>
|
||||
<span className={'flex space-x-2'}>
|
||||
<span>
|
||||
<Trans i18nKey={'billing:basePlan'} />
|
||||
<span className={'flex items-center space-x-1'}>
|
||||
<span className={'flex items-center space-x-1.5'}>
|
||||
<PlusCircle className={'w-4'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'billing:basePlan'} />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span>/</span>
|
||||
<span>-</span>
|
||||
|
||||
<span>
|
||||
<If
|
||||
@@ -50,8 +74,12 @@ export function LineItemDetails(
|
||||
case 'per-seat':
|
||||
return (
|
||||
<div key={item.id} className={className}>
|
||||
<span>
|
||||
<Trans i18nKey={'billing:perTeamMember'} />
|
||||
<span className={'flex items-center space-x-1.5'}>
|
||||
<PlusCircle className={'w-4'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'billing:perTeamMember'} />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className={'font-semibold'}>
|
||||
@@ -63,14 +91,18 @@ export function LineItemDetails(
|
||||
case 'metered':
|
||||
return (
|
||||
<div key={item.id} className={className}>
|
||||
<span className={'flex items-center space-x-2'}>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'billing:perUnit'}
|
||||
values={{
|
||||
unit: item.unit,
|
||||
}}
|
||||
/>
|
||||
<span className={'flex items-center space-x-1'}>
|
||||
<span className={'flex items-center space-x-1.5'}>
|
||||
<PlusCircle className={'w-4'} />
|
||||
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'billing:perUnit'}
|
||||
values={{
|
||||
unit: item.unit,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{item.included ? (
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
RadioGroupItem,
|
||||
RadioGroupItemLabel,
|
||||
} from '@kit/ui/radio-group';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
@@ -410,8 +411,10 @@ function PlanDetails({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<span className={'font-semibold'}>
|
||||
<Separator />
|
||||
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<span className={'text-sm font-semibold'}>
|
||||
<Trans i18nKey={'billing:detailsLabel'} />
|
||||
</span>
|
||||
|
||||
@@ -423,16 +426,16 @@ function PlanDetails({
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<span className={'font-semibold'}>
|
||||
<span className={'text-sm font-semibold'}>
|
||||
<Trans i18nKey={'billing:featuresLabel'} />
|
||||
</span>
|
||||
|
||||
{selectedProduct.features.map((item) => {
|
||||
return (
|
||||
<div key={item} className={'flex items-center space-x-2 text-sm'}>
|
||||
<div key={item} className={'flex items-center space-x-1 text-sm'}>
|
||||
<CheckCircle className={'h-4 text-green-500'} />
|
||||
|
||||
<span className={'text-muted-foreground'}>
|
||||
<span className={'text-secondary-foreground'}>
|
||||
<Trans i18nKey={`billing:features.${item}`} defaults={item} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,14 @@ import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowRight, CheckCircle, Sparkles } from 'lucide-react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BillingConfig, getBaseLineItem, getPlanIntervals } from '@kit/billing';
|
||||
import {
|
||||
BillingConfig,
|
||||
LineItemSchema,
|
||||
getBaseLineItem,
|
||||
getPlanIntervals,
|
||||
} from '@kit/billing';
|
||||
import { formatCurrency } from '@kit/shared/utils';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -15,6 +21,8 @@ import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { LineItemDetails } from './line-item-details';
|
||||
|
||||
interface Paths {
|
||||
signUp: string;
|
||||
}
|
||||
@@ -23,9 +31,11 @@ export function PricingTable({
|
||||
config,
|
||||
paths,
|
||||
CheckoutButtonRenderer,
|
||||
displayPlanDetails = true,
|
||||
}: {
|
||||
config: BillingConfig;
|
||||
paths: Paths;
|
||||
displayPlanDetails?: boolean;
|
||||
|
||||
CheckoutButtonRenderer?: React.ComponentType<{
|
||||
planId: string;
|
||||
@@ -83,6 +93,7 @@ export function PricingTable({
|
||||
baseLineItem={basePlan}
|
||||
product={product}
|
||||
paths={paths}
|
||||
displayPlanDetails={displayPlanDetails}
|
||||
CheckoutButton={CheckoutButtonRenderer}
|
||||
/>
|
||||
);
|
||||
@@ -95,6 +106,7 @@ export function PricingTable({
|
||||
function PricingItem(
|
||||
props: React.PropsWithChildren<{
|
||||
className?: string;
|
||||
displayPlanDetails: boolean;
|
||||
|
||||
paths: {
|
||||
signUp: string;
|
||||
@@ -109,6 +121,7 @@ function PricingItem(
|
||||
|
||||
plan: {
|
||||
id: string;
|
||||
lineItems: z.infer<typeof LineItemSchema>[];
|
||||
interval?: string;
|
||||
name?: string;
|
||||
href?: string;
|
||||
@@ -132,13 +145,19 @@ function PricingItem(
|
||||
) {
|
||||
const highlighted = props.product.highlighted ?? false;
|
||||
|
||||
// we want to exclude the base plan from the list of line items
|
||||
// since we are displaying the base plan separately as the main price
|
||||
const lineItemsToDisplay = props.plan.lineItems.filter((item) => {
|
||||
return item.type !== 'base';
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
data-cy={'subscription-plan'}
|
||||
className={cn(
|
||||
props.className,
|
||||
`s-full flex flex-1 grow flex-col items-stretch
|
||||
justify-between space-y-8 self-stretch p-8 lg:w-4/12 xl:max-w-[22rem] xl:p-10`,
|
||||
justify-between space-y-8 self-stretch p-6 lg:w-4/12 xl:max-w-[22rem] xl:p-8`,
|
||||
{
|
||||
['bg-primary text-primary-foreground border-primary']: highlighted,
|
||||
},
|
||||
@@ -212,12 +231,30 @@ function PricingItem(
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<h6 className={'text-sm font-semibold'}>
|
||||
<Trans i18nKey={'billing:featuresLabel'} />
|
||||
</h6>
|
||||
|
||||
<FeaturesList
|
||||
highlighted={highlighted}
|
||||
features={props.product.features}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<If condition={props.displayPlanDetails && lineItemsToDisplay.length}>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<h6 className={'text-sm font-semibold'}>
|
||||
<Trans i18nKey={'billing:detailsLabel'} />
|
||||
</h6>
|
||||
|
||||
<LineItemDetails
|
||||
selectedInterval={props.plan.interval}
|
||||
currency={props.product.currency}
|
||||
lineItems={lineItemsToDisplay}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<If condition={props.selectable}>
|
||||
@@ -286,18 +323,16 @@ function ListItem({
|
||||
}>) {
|
||||
return (
|
||||
<li className={'flex items-center space-x-1.5'}>
|
||||
<div>
|
||||
<CheckCircle
|
||||
className={cn('h-4', {
|
||||
['text-primary-foreground']: highlighted,
|
||||
['text-green-600']: !highlighted,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<CheckCircle
|
||||
className={cn('h-4', {
|
||||
['text-primary-foreground']: highlighted,
|
||||
['text-green-600']: !highlighted,
|
||||
})}
|
||||
/>
|
||||
|
||||
<span
|
||||
className={cn('text-sm', {
|
||||
['text-muted-foreground']: !highlighted,
|
||||
['text-secondary-foreground']: !highlighted,
|
||||
['text-primary-foreground']: highlighted,
|
||||
})}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user