Update billing system to support single and recurring payments
This update modifies the billing system to properly handle both single and recurring payment plans. Logic is introduced to determine whether the selected plan is recurring or a one-time payment and adjust the interface accordingly. The naming of some components and variables has been changed to more accurately reflect their purpose. Additionally, a
This commit is contained in:
@@ -25,7 +25,10 @@ export function PersonalAccountCheckoutForm(props: {
|
||||
}) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [error, setError] = useState(false);
|
||||
const [checkoutToken, setCheckoutToken] = useState<string>();
|
||||
|
||||
const [checkoutToken, setCheckoutToken] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// only allow trial if the user is not already a customer
|
||||
const canStartTrial = !props.customerId;
|
||||
@@ -36,6 +39,7 @@ export function PersonalAccountCheckoutForm(props: {
|
||||
<EmbeddedCheckout
|
||||
checkoutToken={checkoutToken}
|
||||
provider={billingConfig.provider}
|
||||
onClose={() => setCheckoutToken(undefined)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import {
|
||||
BillingPortalCard,
|
||||
CurrentPlanCard,
|
||||
CurrentSubscriptionCard,
|
||||
} from '@kit/billing-gateway/components';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
@@ -51,9 +51,9 @@ async function PersonalAccountBillingPage() {
|
||||
<If condition={subscription}>
|
||||
{(subscription) => (
|
||||
<div
|
||||
className={'mx-auto flex w-full max-w-2xl flex-col space-y-4'}
|
||||
className={'mx-auto flex w-full max-w-2xl flex-col space-y-6'}
|
||||
>
|
||||
<CurrentPlanCard
|
||||
<CurrentSubscriptionCard
|
||||
subscription={subscription}
|
||||
config={billingConfig}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,10 @@ export function TeamAccountCheckoutForm(params: {
|
||||
}) {
|
||||
const routeParams = useParams();
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [checkoutToken, setCheckoutToken] = useState<string | null>(null);
|
||||
|
||||
const [checkoutToken, setCheckoutToken] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// If the checkout token is set, render the embedded checkout component
|
||||
if (checkoutToken) {
|
||||
@@ -32,6 +35,7 @@ export function TeamAccountCheckoutForm(params: {
|
||||
<EmbeddedCheckout
|
||||
checkoutToken={checkoutToken}
|
||||
provider={billingConfig.provider}
|
||||
onClose={() => setCheckoutToken(undefined)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
BillingPortalCard,
|
||||
CurrentPlanCard,
|
||||
CurrentSubscriptionCard,
|
||||
} from '@kit/billing-gateway/components';
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
@@ -53,7 +53,7 @@ async function TeamAccountBillingPage({ params }: Params) {
|
||||
</If>
|
||||
|
||||
<div>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div className={'flex flex-col space-y-6'}>
|
||||
<If
|
||||
condition={subscription}
|
||||
fallback={
|
||||
@@ -66,7 +66,10 @@ async function TeamAccountBillingPage({ params }: Params) {
|
||||
}
|
||||
>
|
||||
{(data) => (
|
||||
<CurrentPlanCard subscription={data} config={billingConfig} />
|
||||
<CurrentSubscriptionCard
|
||||
subscription={data}
|
||||
config={billingConfig}
|
||||
/>
|
||||
)}
|
||||
</If>
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ async function TeamAccountMembersPage({ params }: Params) {
|
||||
|
||||
<PageBody>
|
||||
<div
|
||||
className={'mx-auto flex w-full max-w-3xl flex-col space-y-4 pb-32'}
|
||||
className={'mx-auto flex w-full max-w-3xl flex-col space-y-6 pb-32'}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader className={'flex flex-row justify-between'}>
|
||||
|
||||
@@ -7,6 +7,29 @@ const provider = BillingProviderSchema.parse(
|
||||
export default createBillingSchema({
|
||||
provider,
|
||||
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: 'price_1P0jgcI1i3VnbZTqXVXaZkMP',
|
||||
name: 'Base',
|
||||
description: 'Base plan',
|
||||
cost: 999.99,
|
||||
type: 'base',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'starter',
|
||||
name: 'Starter',
|
||||
|
||||
@@ -20,12 +20,13 @@
|
||||
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your organization owner.",
|
||||
"manageTeamPlan": "Manage your Team Plan",
|
||||
"manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.",
|
||||
"flatSubscription": "Flat Subscription",
|
||||
"basePlan": "Base Plan",
|
||||
"billingInterval": {
|
||||
"label": "Choose your billing interval",
|
||||
"month": "Billed monthly",
|
||||
"year": "Billed yearly"
|
||||
},
|
||||
"lifetime": "Lifetime",
|
||||
"trialPeriod": "{{period}} day trial",
|
||||
"perPeriod": "per {{period}}",
|
||||
"processing": "Processing...",
|
||||
@@ -85,6 +86,21 @@
|
||||
"badge": "Paused",
|
||||
"heading": "Your subscription is paused",
|
||||
"description": "Your subscription is paused. You can resume it at any time."
|
||||
},
|
||||
"succeeded": {
|
||||
"badge": "Succeeded",
|
||||
"heading": "Your payment was successful",
|
||||
"description": "Your payment was successful. Thank you for subscribing!"
|
||||
},
|
||||
"pending": {
|
||||
"badge": "Pending",
|
||||
"heading": "Your payment is pending",
|
||||
"description": "Your payment is pending. Please bear with us."
|
||||
},
|
||||
"failed": {
|
||||
"badge": "Failed",
|
||||
"heading": "Your payment failed",
|
||||
"description": "Your payment failed. Please update your payment method."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { BadgeCheck } from 'lucide-react';
|
||||
|
||||
import { BillingConfig, getProductPlanPairByVariantId } from '@kit/billing';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { CurrentPlanBadge } from './current-plan-badge';
|
||||
import { LineItemDetails } from './line-item-details';
|
||||
|
||||
type Order = Database['public']['Tables']['orders']['Row'];
|
||||
type LineItem = Database['public']['Tables']['order_items']['Row'];
|
||||
|
||||
interface Props {
|
||||
order: Order & {
|
||||
items: LineItem[];
|
||||
};
|
||||
|
||||
config: BillingConfig;
|
||||
}
|
||||
|
||||
export function CurrentLifetimeOrderCard({
|
||||
order,
|
||||
config,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const lineItems = order.items;
|
||||
const firstLineItem = lineItems[0];
|
||||
|
||||
if (!firstLineItem) {
|
||||
throw new Error('No line items found in subscription');
|
||||
}
|
||||
|
||||
const { product, plan } = getProductPlanPairByVariantId(
|
||||
config,
|
||||
firstLineItem.variant_id,
|
||||
);
|
||||
|
||||
if (!product || !plan) {
|
||||
throw new Error(
|
||||
'Product or plan not found. Did you forget to add it to the billing config?',
|
||||
);
|
||||
}
|
||||
|
||||
const productLineItems = plan.lineItems;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<Trans i18nKey="billing:planCardTitle" />
|
||||
</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
<Trans i18nKey="billing:planCardDescription" />
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className={'space-y-3 text-sm'}>
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<div className={'flex items-center space-x-2 text-lg font-semibold'}>
|
||||
<BadgeCheck
|
||||
className={
|
||||
's-6 fill-green-500 text-white dark:fill-white dark:text-black'
|
||||
}
|
||||
/>
|
||||
|
||||
<span>{product.name}</span>
|
||||
|
||||
<CurrentPlanBadge status={order.status} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
<span className="font-semibold">
|
||||
<Trans i18nKey="billing:detailsLabel" />
|
||||
</span>
|
||||
|
||||
<LineItemDetails
|
||||
lineItems={productLineItems}
|
||||
currency={order.currency}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,13 @@ import { Database } from '@kit/supabase/database';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
type Status =
|
||||
| Database['public']['Enums']['subscription_status']
|
||||
| Database['public']['Enums']['payment_status'];
|
||||
|
||||
export function CurrentPlanBadge(
|
||||
props: React.PropsWithoutRef<{
|
||||
status: Database['public']['Enums']['subscription_status'];
|
||||
status: Status;
|
||||
}>,
|
||||
) {
|
||||
let variant: 'success' | 'warning' | 'destructive';
|
||||
@@ -12,12 +16,14 @@ export function CurrentPlanBadge(
|
||||
|
||||
switch (props.status) {
|
||||
case 'active':
|
||||
case 'succeeded':
|
||||
variant = 'success';
|
||||
break;
|
||||
case 'trialing':
|
||||
variant = 'success';
|
||||
break;
|
||||
case 'past_due':
|
||||
case 'failed':
|
||||
variant = 'destructive';
|
||||
break;
|
||||
case 'canceled':
|
||||
@@ -27,6 +33,7 @@ export function CurrentPlanBadge(
|
||||
variant = 'destructive';
|
||||
break;
|
||||
case 'incomplete':
|
||||
case 'pending':
|
||||
variant = 'warning';
|
||||
break;
|
||||
case 'incomplete_expired':
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { formatDate } from 'date-fns';
|
||||
import { BadgeCheck, CheckCircle2 } from 'lucide-react';
|
||||
import { BadgeCheck } from 'lucide-react';
|
||||
|
||||
import { BillingConfig, getProductPlanPairByVariantId } from '@kit/billing';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@kit/ui/accordion';
|
||||
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -34,7 +30,7 @@ interface Props {
|
||||
config: BillingConfig;
|
||||
}
|
||||
|
||||
export function CurrentPlanCard({
|
||||
export function CurrentSubscriptionCard({
|
||||
subscription,
|
||||
config,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './plan-picker';
|
||||
export * from './current-plan-card';
|
||||
export * from './current-subscription-card';
|
||||
export * from './current-lifetime-order-card';
|
||||
export * from './embedded-checkout';
|
||||
export * from './billing-session-status';
|
||||
export * from './billing-portal-card';
|
||||
|
||||
@@ -2,13 +2,14 @@ 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';
|
||||
|
||||
export function LineItemDetails(
|
||||
props: React.PropsWithChildren<{
|
||||
lineItems: z.infer<typeof LineItemSchema>[];
|
||||
currency: string;
|
||||
selectedInterval: string;
|
||||
selectedInterval?: string | undefined;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
@@ -23,15 +24,20 @@ export function LineItemDetails(
|
||||
>
|
||||
<span className={'flex space-x-2'}>
|
||||
<span>
|
||||
<Trans i18nKey={'billing:flatSubscription'} />
|
||||
<Trans i18nKey={'billing:basePlan'} />
|
||||
</span>
|
||||
|
||||
<span>/</span>
|
||||
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={`billing:billingInterval.${props.selectedInterval}`}
|
||||
/>
|
||||
<If
|
||||
condition={props.selectedInterval}
|
||||
fallback={<Trans i18nKey={'billing:lifetime'} />}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={`billing:billingInterval.${props.selectedInterval}`}
|
||||
/>
|
||||
</If>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -101,6 +101,10 @@ export function PlanPicker(
|
||||
|
||||
const { t } = useTranslation(`billing`);
|
||||
|
||||
// display the period picker if the selected plan is recurring or if no plan is selected
|
||||
const isRecurringPlan =
|
||||
selectedPlan?.paymentType === 'recurring' || !selectedPlan;
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<div
|
||||
@@ -112,67 +116,74 @@ export function PlanPicker(
|
||||
className={'flex w-full max-w-xl flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit(props.onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
name={'interval'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'rounded-md border p-4'}>
|
||||
<FormLabel htmlFor={'plan-picker-id'}>
|
||||
<Trans i18nKey={'common:billingInterval.label'} />
|
||||
</FormLabel>
|
||||
<div
|
||||
className={cn('transition-all', {
|
||||
['pointer-events-none opacity-50']: !isRecurringPlan,
|
||||
})}
|
||||
>
|
||||
<FormField
|
||||
name={'interval'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'rounded-md border p-4'}>
|
||||
<FormLabel htmlFor={'plan-picker-id'}>
|
||||
<Trans i18nKey={'common:billingInterval.label'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl id={'plan-picker-id'}>
|
||||
<RadioGroup name={field.name} value={field.value}>
|
||||
<div className={'flex space-x-2.5'}>
|
||||
{intervals.map((interval) => {
|
||||
const selected = field.value === interval;
|
||||
<FormControl id={'plan-picker-id'}>
|
||||
<RadioGroup name={field.name} value={field.value}>
|
||||
<div className={'flex space-x-2.5'}>
|
||||
{intervals.map((interval) => {
|
||||
const selected = field.value === interval;
|
||||
|
||||
return (
|
||||
<label
|
||||
htmlFor={interval}
|
||||
key={interval}
|
||||
className={cn(
|
||||
'hover:bg-muted flex items-center space-x-2 rounded-md border border-transparent px-4 py-2',
|
||||
{
|
||||
['border-border']: selected,
|
||||
['hover:bg-muted']: !selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={interval}
|
||||
value={interval}
|
||||
onClick={() => {
|
||||
form.setValue('planId', '', {
|
||||
shouldValidate: true,
|
||||
});
|
||||
return (
|
||||
<label
|
||||
htmlFor={interval}
|
||||
key={interval}
|
||||
className={cn(
|
||||
'hover:bg-muted flex items-center space-x-2 rounded-md border border-transparent px-4 py-2',
|
||||
{
|
||||
['border-border']: selected,
|
||||
['hover:bg-muted']: !selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
id={interval}
|
||||
value={interval}
|
||||
onClick={() => {
|
||||
form.setValue('planId', '', {
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
form.setValue('productId', '', {
|
||||
shouldValidate: true,
|
||||
});
|
||||
form.setValue('productId', '', {
|
||||
shouldValidate: true,
|
||||
});
|
||||
|
||||
form.setValue('interval', interval, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<span className={'text-sm font-bold'}>
|
||||
<Trans
|
||||
i18nKey={`billing:billingInterval.${interval}`}
|
||||
form.setValue('interval', interval, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<span className={'text-sm font-bold'}>
|
||||
<Trans
|
||||
i18nKey={`billing:billingInterval.${interval}`}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
name={'planId'}
|
||||
@@ -185,9 +196,13 @@ export function PlanPicker(
|
||||
<FormControl>
|
||||
<RadioGroup name={field.name}>
|
||||
{props.config.products.map((product) => {
|
||||
const plan = product.plans.find(
|
||||
(item) => item.interval === selectedInterval,
|
||||
);
|
||||
const plan = product.plans.find((item) => {
|
||||
if (item.paymentType === 'one-time') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.interval === selectedInterval;
|
||||
});
|
||||
|
||||
if (!plan) {
|
||||
return null;
|
||||
@@ -277,12 +292,21 @@ export function PlanPicker(
|
||||
|
||||
<div>
|
||||
<span className={'text-muted-foreground'}>
|
||||
<Trans
|
||||
i18nKey={`billing:perPeriod`}
|
||||
values={{
|
||||
period: selectedInterval,
|
||||
}}
|
||||
/>
|
||||
<If
|
||||
condition={
|
||||
plan.paymentType === 'recurring'
|
||||
}
|
||||
fallback={
|
||||
<Trans i18nKey={`billing:lifetime`} />
|
||||
}
|
||||
>
|
||||
<Trans
|
||||
i18nKey={`billing:perPeriod`}
|
||||
values={{
|
||||
period: selectedInterval,
|
||||
}}
|
||||
/>
|
||||
</If>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -348,8 +372,11 @@ function PlanDetails({
|
||||
|
||||
selectedPlan: {
|
||||
lineItems: z.infer<typeof LineItemSchema>[];
|
||||
paymentType: string;
|
||||
};
|
||||
}) {
|
||||
const isRecurring = selectedPlan.paymentType === 'recurring';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
@@ -364,7 +391,9 @@ function PlanDetails({
|
||||
defaults={selectedProduct.name}
|
||||
/>
|
||||
</b>{' '}
|
||||
/ <Trans i18nKey={`billing:billingInterval.${selectedInterval}`} />
|
||||
<If condition={isRecurring}>
|
||||
/ <Trans i18nKey={`billing:billingInterval.${selectedInterval}`} />
|
||||
</If>
|
||||
</Heading>
|
||||
|
||||
<p>
|
||||
@@ -384,7 +413,7 @@ function PlanDetails({
|
||||
|
||||
<LineItemDetails
|
||||
lineItems={selectedPlan.lineItems ?? []}
|
||||
selectedInterval={selectedInterval}
|
||||
selectedInterval={isRecurring ? selectedInterval : undefined}
|
||||
currency={selectedProduct.currency}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -81,20 +81,20 @@ export class BillingEventHandlerService {
|
||||
|
||||
Logger.info(ctx, 'Successfully updated subscription');
|
||||
},
|
||||
onCheckoutSessionCompleted: async (payload, customerId) => {
|
||||
onCheckoutSessionCompleted: async (payload) => {
|
||||
// Handle the checkout session completed event
|
||||
// here we add the subscription to the database
|
||||
const client = this.clientProvider();
|
||||
|
||||
// Check if the payload contains an order_id
|
||||
// if it does, we add an order, otherwise we add a subscription
|
||||
if ('order_id' in payload) {
|
||||
if ('target_order_id' in payload) {
|
||||
const ctx = {
|
||||
namespace: this.namespace,
|
||||
orderId: payload.order_id,
|
||||
orderId: payload.target_order_id,
|
||||
provider: payload.billing_provider,
|
||||
accountId: payload.target_account_id,
|
||||
customerId,
|
||||
customerId: payload.target_customer_id,
|
||||
};
|
||||
|
||||
Logger.info(ctx, 'Processing order completed event...');
|
||||
@@ -114,7 +114,7 @@ export class BillingEventHandlerService {
|
||||
subscriptionId: payload.target_subscription_id,
|
||||
provider: payload.billing_provider,
|
||||
accountId: payload.target_account_id,
|
||||
customerId,
|
||||
customerId: payload.target_customer_id,
|
||||
};
|
||||
|
||||
Logger.info(ctx, 'Processing checkout session completed event...');
|
||||
|
||||
@@ -186,9 +186,9 @@ export type BillingConfig = z.infer<typeof BillingSchema>;
|
||||
export type ProductSchema = z.infer<typeof ProductSchema>;
|
||||
|
||||
export function getPlanIntervals(config: z.infer<typeof BillingSchema>) {
|
||||
const intervals = config.products.flatMap((product) =>
|
||||
product.plans.map((plan) => plan.interval),
|
||||
);
|
||||
const intervals = config.products
|
||||
.flatMap((product) => product.plans.map((plan) => plan.interval))
|
||||
.filter(Boolean);
|
||||
|
||||
return Array.from(new Set(intervals));
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function PersonalAccountSettingsContainer(
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className={'flex w-full flex-col space-y-8 pb-32'}>
|
||||
<div className={'flex w-full flex-col space-y-6 pb-32'}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function TeamAccountSettingsContainer(props: {
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<div className={'flex w-full flex-col space-y-8'}>
|
||||
<div className={'flex w-full flex-col space-y-6'}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
|
||||
@@ -63,10 +63,6 @@ function EmbeddedCheckoutPopup({
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Complete your purchase</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogContent
|
||||
className={className}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
|
||||
@@ -73,6 +73,7 @@ export async function createStripeCheckout(
|
||||
line_items: lineItems,
|
||||
client_reference_id: clientReferenceId,
|
||||
subscription_data: subscriptionData,
|
||||
customer_creation: 'always',
|
||||
...customerData,
|
||||
...urls,
|
||||
});
|
||||
|
||||
@@ -171,7 +171,7 @@ export class StripeWebhookHandlerService
|
||||
const payload: UpsertOrderParams = {
|
||||
target_account_id: accountId,
|
||||
target_customer_id: customerId,
|
||||
order_id: sessionId,
|
||||
target_order_id: sessionId,
|
||||
billing_provider: this.provider,
|
||||
status: status,
|
||||
currency: currency,
|
||||
|
||||
@@ -410,11 +410,9 @@ export type Database = {
|
||||
created_at: string
|
||||
currency: string
|
||||
id: string
|
||||
product_id: string
|
||||
status: Database["public"]["Enums"]["payment_status"]
|
||||
total_amount: number
|
||||
updated_at: string
|
||||
variant_id: string
|
||||
}
|
||||
Insert: {
|
||||
account_id: string
|
||||
@@ -423,11 +421,9 @@ export type Database = {
|
||||
created_at?: string
|
||||
currency: string
|
||||
id: string
|
||||
product_id: string
|
||||
status: Database["public"]["Enums"]["payment_status"]
|
||||
total_amount: number
|
||||
updated_at?: string
|
||||
variant_id: string
|
||||
}
|
||||
Update: {
|
||||
account_id?: string
|
||||
@@ -436,11 +432,9 @@ export type Database = {
|
||||
created_at?: string
|
||||
currency?: string
|
||||
id?: string
|
||||
product_id?: string
|
||||
status?: Database["public"]["Enums"]["payment_status"]
|
||||
total_amount?: number
|
||||
updated_at?: string
|
||||
variant_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
@@ -891,7 +885,7 @@ export type Database = {
|
||||
Args: {
|
||||
target_account_id: string
|
||||
target_customer_id: string
|
||||
order_id: string
|
||||
target_order_id: string
|
||||
status: Database["public"]["Enums"]["payment_status"]
|
||||
billing_provider: Database["public"]["Enums"]["billing_provider"]
|
||||
total_amount: number
|
||||
@@ -905,11 +899,9 @@ export type Database = {
|
||||
created_at: string
|
||||
currency: string
|
||||
id: string
|
||||
product_id: string
|
||||
status: Database["public"]["Enums"]["payment_status"]
|
||||
total_amount: number
|
||||
updated_at: string
|
||||
variant_id: string
|
||||
}
|
||||
}
|
||||
upsert_subscription: {
|
||||
|
||||
@@ -1249,7 +1249,7 @@ select
|
||||
create or replace function public.upsert_order(
|
||||
target_account_id uuid,
|
||||
target_customer_id varchar(255),
|
||||
order_id text,
|
||||
target_order_id text,
|
||||
status public.payment_status,
|
||||
billing_provider public.billing_provider,
|
||||
total_amount numeric,
|
||||
@@ -1261,7 +1261,7 @@ declare
|
||||
new_billing_customer_id int;
|
||||
begin
|
||||
insert into public.billing_customers(account_id, provider, customer_id)
|
||||
values (target_account_id, target_billing_provider, target_customer_id)
|
||||
values (target_account_id, billing_provider, target_customer_id)
|
||||
on conflict (account_id, provider, customer_id) do update
|
||||
set provider = excluded.provider
|
||||
returning id into new_billing_customer_id;
|
||||
@@ -1277,7 +1277,7 @@ begin
|
||||
values (
|
||||
target_account_id,
|
||||
new_billing_customer_id,
|
||||
order_id,
|
||||
target_order_id,
|
||||
status,
|
||||
billing_provider,
|
||||
total_amount,
|
||||
|
||||
Reference in New Issue
Block a user