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:
@@ -15,7 +15,7 @@ NEXT_PUBLIC_AUTH_PASSWORD=true
|
|||||||
NEXT_PUBLIC_AUTH_MAGIC_LINK=false
|
NEXT_PUBLIC_AUTH_MAGIC_LINK=false
|
||||||
|
|
||||||
# BILLING
|
# BILLING
|
||||||
NEXT_PUBLIC_BILLING_PROVIDER=lemon-squeezy
|
NEXT_PUBLIC_BILLING_PROVIDER=stripe
|
||||||
|
|
||||||
# SUPABASE
|
# SUPABASE
|
||||||
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
|
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
|
||||||
|
|||||||
@@ -16,28 +16,6 @@ export default createBillingSchema({
|
|||||||
provider,
|
provider,
|
||||||
// products configuration
|
// products configuration
|
||||||
products: [
|
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',
|
id: 'starter',
|
||||||
name: 'Starter',
|
name: 'Starter',
|
||||||
@@ -56,42 +34,7 @@ export default createBillingSchema({
|
|||||||
id: '324646',
|
id: '324646',
|
||||||
name: 'Addon 2',
|
name: 'Addon 2',
|
||||||
cost: 9.99,
|
cost: 9.99,
|
||||||
type: 'metered',
|
type: 'flat',
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,8 +38,31 @@ export function LineItemDetails(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const BaseFee = () => (
|
const SetupFee = () => (
|
||||||
<div key={item.id} className={className}>
|
<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'}>
|
||||||
<span className={'flex items-center space-x-1.5'}>
|
<span className={'flex items-center space-x-1.5'}>
|
||||||
<PlusSquare className={'w-4'} />
|
<PlusSquare className={'w-4'} />
|
||||||
@@ -67,14 +90,31 @@ export function LineItemDetails(
|
|||||||
{formatCurrency(props?.currency.toLowerCase(), item.cost)}
|
{formatCurrency(props?.currency.toLowerCase(), item.cost)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (item.type) {
|
const PerSeat = () => (
|
||||||
case 'base':
|
|
||||||
return <BaseFee />;
|
|
||||||
|
|
||||||
case 'per-seat':
|
|
||||||
return (
|
|
||||||
<div className={'flex flex-col'}>
|
<div className={'flex flex-col'}>
|
||||||
<div key={item.id} className={className}>
|
<div key={item.id} className={className}>
|
||||||
<span className={'flex items-center space-x-1.5'}>
|
<span className={'flex items-center space-x-1.5'}>
|
||||||
@@ -92,16 +132,17 @@ export function LineItemDetails(
|
|||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SetupFee />
|
||||||
|
|
||||||
<If condition={item.tiers?.length}>
|
<If condition={item.tiers?.length}>
|
||||||
<Tiers item={item} currency={props.currency} />
|
<Tiers item={item} currency={props.currency} />
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'metered': {
|
const Metered = () => (
|
||||||
return (
|
<div key={item.id} className={'flex flex-col'}>
|
||||||
<div className={'flex flex-col'}>
|
<div className={className}>
|
||||||
<div key={item.id} className={className}>
|
|
||||||
<span className={'flex items-center space-x-1'}>
|
<span className={'flex items-center space-x-1'}>
|
||||||
<span className={'flex items-center space-x-1.5'}>
|
<span className={'flex items-center space-x-1.5'}>
|
||||||
<PlusSquare className={'w-4'} />
|
<PlusSquare className={'w-4'} />
|
||||||
@@ -115,19 +156,6 @@ export function LineItemDetails(
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<If condition={item.setupFee}>
|
|
||||||
{(fee) => (
|
|
||||||
<span>
|
|
||||||
<Trans
|
|
||||||
i18nKey={'billing:setupFee'}
|
|
||||||
values={{
|
|
||||||
setupFee: formatCurrency(props.currency, fee),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</If>
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -140,12 +168,24 @@ export function LineItemDetails(
|
|||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SetupFee />
|
||||||
|
|
||||||
{/* If there are tiers, we render them as a list */}
|
{/* If there are tiers, we render them as a list */}
|
||||||
<If condition={item.tiers?.length}>
|
<If condition={item.tiers?.length}>
|
||||||
<Tiers item={item} currency={props.currency} />
|
<Tiers item={item} currency={props.currency} />
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
switch (item.type) {
|
||||||
|
case 'flat':
|
||||||
|
return <FlatFee />;
|
||||||
|
|
||||||
|
case 'per-seat':
|
||||||
|
return <PerSeat />;
|
||||||
|
|
||||||
|
case 'metered': {
|
||||||
|
return <Metered />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
@@ -160,6 +200,8 @@ function Tiers({
|
|||||||
currency: string;
|
currency: string;
|
||||||
item: z.infer<typeof LineItemSchema>;
|
item: z.infer<typeof LineItemSchema>;
|
||||||
}) {
|
}) {
|
||||||
|
const unit = item.unit;
|
||||||
|
|
||||||
const tiers = item.tiers?.map((tier, index) => {
|
const tiers = item.tiers?.map((tier, index) => {
|
||||||
const previousTier = item.tiers?.[index - 1];
|
const previousTier = item.tiers?.[index - 1];
|
||||||
const isNoLimit = tier.upTo === 'unlimited';
|
const isNoLimit = tier.upTo === 'unlimited';
|
||||||
@@ -173,7 +215,6 @@ function Tiers({
|
|||||||
|
|
||||||
const upTo = tier.upTo;
|
const upTo = tier.upTo;
|
||||||
const isIncluded = tier.cost === 0;
|
const isIncluded = tier.cost === 0;
|
||||||
const unit = item.unit;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -214,12 +214,12 @@ export function PlanPicker(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseLineItem = getPrimaryLineItem(
|
const primaryLineItem = getPrimaryLineItem(
|
||||||
props.config,
|
props.config,
|
||||||
plan.id,
|
plan.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!baseLineItem) {
|
if (!primaryLineItem) {
|
||||||
throw new Error(`Base line item was not found`);
|
throw new Error(`Base line item was not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ export function PlanPicker(
|
|||||||
<span>
|
<span>
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
product.currency.toLowerCase(),
|
product.currency.toLowerCase(),
|
||||||
baseLineItem.cost,
|
primaryLineItem.cost,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Price>
|
</Price>
|
||||||
@@ -420,6 +420,7 @@ function PlanDetails({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<If condition={selectedPlan.lineItems.length > 0}>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div className={'flex flex-col space-y-2'}>
|
<div className={'flex flex-col space-y-2'}>
|
||||||
@@ -433,6 +434,7 @@ function PlanDetails({
|
|||||||
currency={selectedProduct.currency}
|
currency={selectedProduct.currency}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</If>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
|||||||
@@ -386,13 +386,16 @@ function PlanIntervalSwitcher(
|
|||||||
{props.intervals.map((plan, index) => {
|
{props.intervals.map((plan, index) => {
|
||||||
const selected = plan === props.interval;
|
const selected = plan === props.interval;
|
||||||
|
|
||||||
const className = cn('focus:!ring-0 !outline-none', {
|
const className = cn(
|
||||||
|
'focus:!ring-0 !outline-none animate-in transition-all fade-in',
|
||||||
|
{
|
||||||
'rounded-r-none border-r-transparent': index === 0,
|
'rounded-r-none border-r-transparent': index === 0,
|
||||||
'rounded-l-none': index === props.intervals.length - 1,
|
'rounded-l-none': index === props.intervals.length - 1,
|
||||||
['hover:bg-muted']: !selected,
|
['hover:text-current hover:bg-muted']: !selected,
|
||||||
['font-semibold cursor-default bg-muted hover:bg-muted hover:text-initial']:
|
['font-semibold cursor-default hover:text-initial hover:bg-background border-primary']:
|
||||||
selected,
|
selected,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -403,7 +406,7 @@ function PlanIntervalSwitcher(
|
|||||||
>
|
>
|
||||||
<span className={'flex items-center space-x-1'}>
|
<span className={'flex items-center space-x-1'}>
|
||||||
<If condition={selected}>
|
<If condition={selected}>
|
||||||
<CheckCircle className={'animate-in fade-in h-4'} />
|
<CheckCircle className={'animate-in fade-in zoom-in-90 h-4'} />
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<span className={'capitalize'}>
|
<span className={'capitalize'}>
|
||||||
|
|||||||
Reference in New Issue
Block a user