Updated the billing provider in the environment configuration to use 'stripe' instead of 'lemon-squeezy'. Multiple changes were also made to UI components related to pricing, including better data handling for different billing tiers and enhanced visualization of selected options. These revisions aim to both enhance the user experience and ensure compatibility with the new billing provider.
265 lines
7.5 KiB
TypeScript
265 lines
7.5 KiB
TypeScript
import { PlusSquare } from 'lucide-react';
|
|
import { z } from 'zod';
|
|
|
|
import { LineItemSchema } from '@kit/billing';
|
|
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>[];
|
|
currency: string;
|
|
selectedInterval?: string | undefined;
|
|
}>,
|
|
) {
|
|
return (
|
|
<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'}>
|
|
<PlusSquare className={'w-4'} />
|
|
|
|
<Trans
|
|
i18nKey={item.description}
|
|
values={item}
|
|
defaults={item.description}
|
|
/>
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const SetupFee = () => (
|
|
<If condition={item.setupFee}>
|
|
<div className={className}>
|
|
<span className={'flex items-center space-x-1'}>
|
|
<PlusSquare className={'w-4'} />
|
|
|
|
<span>
|
|
<Trans
|
|
i18nKey={'billing:setupFee'}
|
|
values={{
|
|
setupFee: formatCurrency(
|
|
props?.currency.toLowerCase(),
|
|
item.setupFee as number,
|
|
),
|
|
}}
|
|
/>
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</If>
|
|
);
|
|
|
|
const FlatFee = () => (
|
|
<div key={item.id} className={'flex flex-col'}>
|
|
<div className={className}>
|
|
<span className={'flex items-center space-x-1'}>
|
|
<span className={'flex items-center space-x-1.5'}>
|
|
<PlusSquare className={'w-4'} />
|
|
|
|
<span>
|
|
<Trans i18nKey={'billing:basePlan'} />
|
|
</span>
|
|
</span>
|
|
|
|
<span>-</span>
|
|
|
|
<span>
|
|
<If
|
|
condition={props.selectedInterval}
|
|
fallback={<Trans i18nKey={'billing:lifetime'} />}
|
|
>
|
|
<Trans
|
|
i18nKey={`billing:billingInterval.${props.selectedInterval}`}
|
|
/>
|
|
</If>
|
|
</span>
|
|
</span>
|
|
|
|
<span className={'font-semibold'}>
|
|
{formatCurrency(props?.currency.toLowerCase(), item.cost)}
|
|
</span>
|
|
</div>
|
|
|
|
<SetupFee />
|
|
|
|
<If condition={item.tiers?.length}>
|
|
<span className={'flex items-center space-x-1.5'}>
|
|
<PlusSquare className={'w-4'} />
|
|
|
|
<span className={'flex space-x-1 text-sm'}>
|
|
<span>
|
|
<Trans
|
|
i18nKey={'billing:perUnit'}
|
|
values={{
|
|
unit: item.unit,
|
|
}}
|
|
/>
|
|
</span>
|
|
</span>
|
|
</span>
|
|
|
|
<Tiers item={item} currency={props.currency} />
|
|
</If>
|
|
</div>
|
|
);
|
|
|
|
const PerSeat = () => (
|
|
<div className={'flex flex-col'}>
|
|
<div key={item.id} className={className}>
|
|
<span className={'flex items-center space-x-1.5'}>
|
|
<PlusSquare className={'w-4'} />
|
|
|
|
<span>
|
|
<Trans i18nKey={'billing:perTeamMember'} />
|
|
</span>
|
|
</span>
|
|
|
|
<If condition={!item.tiers?.length}>
|
|
<span className={'font-semibold'}>
|
|
{formatCurrency(props.currency.toLowerCase(), item.cost)}
|
|
</span>
|
|
</If>
|
|
</div>
|
|
|
|
<SetupFee />
|
|
|
|
<If condition={item.tiers?.length}>
|
|
<Tiers item={item} currency={props.currency} />
|
|
</If>
|
|
</div>
|
|
);
|
|
|
|
const Metered = () => (
|
|
<div key={item.id} className={'flex flex-col'}>
|
|
<div className={className}>
|
|
<span className={'flex items-center space-x-1'}>
|
|
<span className={'flex items-center space-x-1.5'}>
|
|
<PlusSquare className={'w-4'} />
|
|
|
|
<span className={'flex space-x-1'}>
|
|
<span>
|
|
<Trans
|
|
i18nKey={'billing:perUnit'}
|
|
values={{
|
|
unit: item.unit,
|
|
}}
|
|
/>
|
|
</span>
|
|
</span>
|
|
</span>
|
|
</span>
|
|
|
|
{/* If there are no tiers, there is a flat cost for usage */}
|
|
<If condition={!item.tiers?.length}>
|
|
<span className={'font-semibold'}>
|
|
{formatCurrency(props?.currency.toLowerCase(), item.cost)}
|
|
</span>
|
|
</If>
|
|
</div>
|
|
|
|
<SetupFee />
|
|
|
|
{/* If there are tiers, we render them as a list */}
|
|
<If condition={item.tiers?.length}>
|
|
<Tiers item={item} currency={props.currency} />
|
|
</If>
|
|
</div>
|
|
);
|
|
|
|
switch (item.type) {
|
|
case 'flat':
|
|
return <FlatFee />;
|
|
|
|
case 'per-seat':
|
|
return <PerSeat />;
|
|
|
|
case 'metered': {
|
|
return <Metered />;
|
|
}
|
|
}
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Tiers({
|
|
currency,
|
|
item,
|
|
}: {
|
|
currency: string;
|
|
item: z.infer<typeof LineItemSchema>;
|
|
}) {
|
|
const unit = item.unit;
|
|
|
|
const tiers = item.tiers?.map((tier, index) => {
|
|
const previousTier = item.tiers?.[index - 1];
|
|
const isNoLimit = tier.upTo === 'unlimited';
|
|
|
|
const previousTierFrom =
|
|
previousTier?.upTo === 'unlimited'
|
|
? 'unlimited'
|
|
: previousTier === undefined
|
|
? 0
|
|
: previousTier.upTo + 1 || 0;
|
|
|
|
const upTo = tier.upTo;
|
|
const isIncluded = tier.cost === 0;
|
|
|
|
return (
|
|
<span
|
|
className={'text-secondary-foreground flex space-x-1 text-xs'}
|
|
key={tier.upTo}
|
|
>
|
|
<span>-</span>
|
|
|
|
<If condition={isNoLimit}>
|
|
<span className={'font-bold'}>
|
|
{formatCurrency(currency.toLowerCase(), tier.cost)}
|
|
</span>
|
|
|
|
<span>
|
|
<Trans
|
|
i18nKey={'billing:andAbove'}
|
|
values={{ unit, previousTier: (previousTierFrom as number) - 1 }}
|
|
/>
|
|
</span>
|
|
</If>
|
|
|
|
<If condition={!isNoLimit}>
|
|
<If condition={isIncluded}>
|
|
<span>
|
|
<Trans i18nKey={'billing:includedUpTo'} values={{ unit, upTo }} />
|
|
</span>
|
|
</If>
|
|
|
|
<If condition={!isIncluded}>
|
|
<span className={'font-bold'}>
|
|
{formatCurrency(currency.toLowerCase(), tier.cost)}
|
|
</span>
|
|
|
|
<span>
|
|
<Trans
|
|
i18nKey={'billing:fromPreviousTierUpTo'}
|
|
values={{ previousTierFrom, unit, upTo }}
|
|
/>
|
|
</span>
|
|
</If>
|
|
</If>
|
|
</span>
|
|
);
|
|
});
|
|
|
|
return <div className={'my-2.5 flex flex-col space-y-1.5'}>{tiers}</div>;
|
|
}
|