Update Billing Provider and Refactor Pricing UI

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.
This commit is contained in:
giancarlo
2024-04-07 18:12:45 +08:00
parent 1e23ee2783
commit ab1e90f093
5 changed files with 161 additions and 172 deletions

View File

@@ -15,7 +15,7 @@ NEXT_PUBLIC_AUTH_PASSWORD=true
NEXT_PUBLIC_AUTH_MAGIC_LINK=false
# BILLING
NEXT_PUBLIC_BILLING_PROVIDER=lemon-squeezy
NEXT_PUBLIC_BILLING_PROVIDER=stripe
# SUPABASE
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321

View File

@@ -16,28 +16,6 @@ export default createBillingSchema({
provider,
// products configuration
products: [
{
id: 'lifetime',
name: 'Lifetime',
description: 'The perfect plan for a lifetime',
currency: 'USD',
features: ['Feature 1', 'Feature 2', 'Feature 3'],
plans: [
{
name: 'Lifetime',
id: 'lifetime',
paymentType: 'one-time',
lineItems: [
{
id: '324643',
name: 'Base',
cost: 999.99,
type: 'flat',
},
],
},
],
},
{
id: 'starter',
name: 'Starter',
@@ -56,42 +34,7 @@ export default createBillingSchema({
id: '324646',
name: 'Addon 2',
cost: 9.99,
type: 'metered',
unit: 'GBs',
tiers: [
{
upTo: 5,
cost: 0,
},
{
upTo: 10,
cost: 6.99,
},
{
upTo: 'unlimited',
cost: 0.49,
},
],
},
{
id: '324645',
name: 'Addon 2',
cost: 9.99,
type: 'per-seat',
tiers: [
{
upTo: 5,
cost: 0,
},
{
upTo: 10,
cost: 6.99,
},
{
upTo: 'unlimited',
cost: 0.49,
},
],
type: 'flat',
},
],
},

View File

@@ -38,114 +38,154 @@ export function LineItemDetails(
);
}
const BaseFee = () => (
<div key={item.id} className={className}>
<span className={'flex items-center space-x-1'}>
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:basePlan'} />
<Trans i18nKey={'billing:perTeamMember'} />
</span>
</span>
<span>-</span>
<If condition={!item.tiers?.length}>
<span className={'font-semibold'}>
{formatCurrency(props.currency.toLowerCase(), item.cost)}
</span>
</If>
</div>
<span>
<If
condition={props.selectedInterval}
fallback={<Trans i18nKey={'billing:lifetime'} />}
>
<Trans
i18nKey={`billing:billingInterval.${props.selectedInterval}`}
/>
</If>
<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>
</span>
<span className={'font-semibold'}>
{formatCurrency(props?.currency.toLowerCase(), item.cost)}
</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 'base':
return <BaseFee />;
case 'flat':
return <FlatFee />;
case 'per-seat':
return (
<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>
<If condition={item.tiers?.length}>
<Tiers item={item} currency={props.currency} />
</If>
</div>
);
return <PerSeat />;
case 'metered': {
return (
<div className={'flex flex-col'}>
<div key={item.id} 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>
<If condition={item.setupFee}>
{(fee) => (
<span>
<Trans
i18nKey={'billing:setupFee'}
values={{
setupFee: formatCurrency(props.currency, fee),
}}
/>
</span>
)}
</If>
</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>
{/* If there are tiers, we render them as a list */}
<If condition={item.tiers?.length}>
<Tiers item={item} currency={props.currency} />
</If>
</div>
);
return <Metered />;
}
}
})}
@@ -160,6 +200,8 @@ function Tiers({
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';
@@ -173,7 +215,6 @@ function Tiers({
const upTo = tier.upTo;
const isIncluded = tier.cost === 0;
const unit = item.unit;
return (
<span

View File

@@ -214,12 +214,12 @@ export function PlanPicker(
return null;
}
const baseLineItem = getPrimaryLineItem(
const primaryLineItem = getPrimaryLineItem(
props.config,
plan.id,
);
if (!baseLineItem) {
if (!primaryLineItem) {
throw new Error(`Base line item was not found`);
}
@@ -295,7 +295,7 @@ export function PlanPicker(
<span>
{formatCurrency(
product.currency.toLowerCase(),
baseLineItem.cost,
primaryLineItem.cost,
)}
</span>
</Price>
@@ -420,19 +420,21 @@ function PlanDetails({
</p>
</div>
<Separator />
<If condition={selectedPlan.lineItems.length > 0}>
<Separator />
<div className={'flex flex-col space-y-2'}>
<span className={'text-sm font-semibold'}>
<Trans i18nKey={'billing:detailsLabel'} />
</span>
<div className={'flex flex-col space-y-2'}>
<span className={'text-sm font-semibold'}>
<Trans i18nKey={'billing:detailsLabel'} />
</span>
<LineItemDetails
lineItems={selectedPlan.lineItems ?? []}
selectedInterval={isRecurring ? selectedInterval : undefined}
currency={selectedProduct.currency}
/>
</div>
<LineItemDetails
lineItems={selectedPlan.lineItems ?? []}
selectedInterval={isRecurring ? selectedInterval : undefined}
currency={selectedProduct.currency}
/>
</div>
</If>
<Separator />

View File

@@ -386,13 +386,16 @@ function PlanIntervalSwitcher(
{props.intervals.map((plan, index) => {
const selected = plan === props.interval;
const className = cn('focus:!ring-0 !outline-none', {
'rounded-r-none border-r-transparent': index === 0,
'rounded-l-none': index === props.intervals.length - 1,
['hover:bg-muted']: !selected,
['font-semibold cursor-default bg-muted hover:bg-muted hover:text-initial']:
selected,
});
const className = cn(
'focus:!ring-0 !outline-none animate-in transition-all fade-in',
{
'rounded-r-none border-r-transparent': index === 0,
'rounded-l-none': index === props.intervals.length - 1,
['hover:text-current hover:bg-muted']: !selected,
['font-semibold cursor-default hover:text-initial hover:bg-background border-primary']:
selected,
},
);
return (
<Button
@@ -403,7 +406,7 @@ function PlanIntervalSwitcher(
>
<span className={'flex items-center space-x-1'}>
<If condition={selected}>
<CheckCircle className={'animate-in fade-in h-4'} />
<CheckCircle className={'animate-in fade-in zoom-in-90 h-4'} />
</If>
<span className={'capitalize'}>