Refactor authentication flow and improve code organization
The update implemented a redirect functionality in the multi-factor authentication flow for a better user experience. It also involved a refactoring of some parts of the code, substituting direct routing paths with path configs for easier future modifications. Import statements were adjusted for better code organization and readability.
This commit is contained in:
@@ -1,326 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { CheckCircle, Sparkles } from 'lucide-react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { BillingSchema, getPlanIntervals } from '../create-billing-schema';
|
||||
|
||||
type Config = z.infer<typeof BillingSchema>;
|
||||
|
||||
interface Paths {
|
||||
signUp: string;
|
||||
}
|
||||
|
||||
export function PricingTable({
|
||||
config,
|
||||
paths,
|
||||
CheckoutButtonRenderer,
|
||||
}: {
|
||||
config: Config;
|
||||
paths: Paths;
|
||||
|
||||
CheckoutButtonRenderer?: React.ComponentType<{
|
||||
planId: string;
|
||||
highlighted?: boolean;
|
||||
}>;
|
||||
}) {
|
||||
const intervals = getPlanIntervals(config);
|
||||
|
||||
const [planVariant, setPlanVariant] = useState<string>(
|
||||
intervals[0] as string,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-12'}>
|
||||
<div className={'flex justify-center'}>
|
||||
<PlanIntervalSwitcher
|
||||
intervals={intervals}
|
||||
interval={planVariant}
|
||||
setInterval={setPlanVariant}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
'flex flex-col items-start space-y-6 lg:space-y-0' +
|
||||
' justify-center space-x-2 lg:flex-row'
|
||||
}
|
||||
>
|
||||
{config.products.map((product) => {
|
||||
const plan = product.plans.find(
|
||||
(item) => item.interval === planVariant,
|
||||
);
|
||||
|
||||
if (!plan || product.hidden) {
|
||||
console.warn(`No plan found for ${product.name}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<PricingItem
|
||||
selectable
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
product={product}
|
||||
paths={paths}
|
||||
CheckoutButton={CheckoutButtonRenderer}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PricingItem(
|
||||
props: React.PropsWithChildren<{
|
||||
paths: {
|
||||
signUp: string;
|
||||
};
|
||||
|
||||
selectable: boolean;
|
||||
|
||||
plan: {
|
||||
id: string;
|
||||
price: string;
|
||||
interval: string;
|
||||
name?: string;
|
||||
href?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
CheckoutButton?: React.ComponentType<{
|
||||
planId: string;
|
||||
highlighted?: boolean;
|
||||
}>;
|
||||
|
||||
product: {
|
||||
name: string;
|
||||
currency: string;
|
||||
description: string;
|
||||
badge?: string;
|
||||
highlighted?: boolean;
|
||||
features: string[];
|
||||
};
|
||||
}>,
|
||||
) {
|
||||
const highlighted = props.product.highlighted ?? false;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-cy={'subscription-plan'}
|
||||
className={cn(
|
||||
`
|
||||
relative flex w-full flex-col justify-between space-y-6 rounded-lg
|
||||
border p-6 lg:w-4/12 xl:max-w-xs xl:p-8 2xl:w-3/12
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<div className={'flex flex-col space-y-2.5'}>
|
||||
<div className={'flex items-center space-x-2.5'}>
|
||||
<Heading level={4}>
|
||||
<b className={'font-semibold'}>{props.product.name}</b>
|
||||
</Heading>
|
||||
|
||||
<If condition={props.product.badge}>
|
||||
<div
|
||||
className={cn(
|
||||
`flex space-x-1 rounded-md px-2 py-1 text-xs font-medium`,
|
||||
{
|
||||
['text-primary-foreground bg-primary']: highlighted,
|
||||
['text-muted-foreground bg-gray-50']: !highlighted,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<If condition={highlighted}>
|
||||
<Sparkles className={'mr-1 h-4 w-4'} />
|
||||
</If>
|
||||
|
||||
<span>{props.product.badge}</span>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<span className={'text-sm text-gray-500 dark:text-gray-400'}>
|
||||
{props.product.description}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center space-x-1'}>
|
||||
<Price>
|
||||
<span className={'text-base'}>{props.product.currency}</span>
|
||||
{props.plan.price}
|
||||
</Price>
|
||||
|
||||
<If condition={props.plan.name}>
|
||||
<span className={cn(`text-muted-foreground text-base lowercase`)}>
|
||||
<span>/</span>
|
||||
<span>{props.plan.interval}</span>
|
||||
</span>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div className={'text-current'}>
|
||||
<FeaturesList features={props.product.features} />
|
||||
</div>
|
||||
|
||||
<If condition={props.selectable}>
|
||||
<If
|
||||
condition={props.plan.id && props.CheckoutButton}
|
||||
fallback={
|
||||
<DefaultCheckoutButton
|
||||
signUpPath={props.paths.signUp}
|
||||
highlighted={highlighted}
|
||||
plan={props.plan}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(CheckoutButton) => (
|
||||
<CheckoutButton highlighted={highlighted} planId={props.plan.id} />
|
||||
)}
|
||||
</If>
|
||||
</If>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeaturesList(
|
||||
props: React.PropsWithChildren<{
|
||||
features: string[];
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<ul className={'flex flex-col space-y-2'}>
|
||||
{props.features.map((feature) => {
|
||||
return (
|
||||
<ListItem key={feature}>
|
||||
<Trans
|
||||
i18nKey={`common:plans.features.${feature}`}
|
||||
defaults={feature}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function Price({ children }: React.PropsWithChildren) {
|
||||
// little trick to re-animate the price when switching plans
|
||||
const key = Math.random();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={`animate-in slide-in-from-left-4 fade-in items-center duration-500`}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
'flex items-center text-2xl font-bold lg:text-3xl xl:text-4xl 2xl:text-5xl'
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ListItem({ children }: React.PropsWithChildren) {
|
||||
return (
|
||||
<li className={'flex items-center space-x-3 font-medium'}>
|
||||
<div>
|
||||
<CheckCircle className={'h-5 text-green-500'} />
|
||||
</div>
|
||||
|
||||
<span className={'text-muted-foreground text-sm'}>{children}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function PlanIntervalSwitcher(
|
||||
props: React.PropsWithChildren<{
|
||||
intervals: string[];
|
||||
interval: string;
|
||||
setInterval: (interval: string) => void;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className={'flex'}>
|
||||
{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-gray-50 dark:hover:bg-background/80']: !selected,
|
||||
['text-primary-800 dark:text-primary-500 font-semibold' +
|
||||
' hover:bg-background hover:text-initial']: selected,
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={plan}
|
||||
variant={'outline'}
|
||||
className={className}
|
||||
onClick={() => props.setInterval(plan)}
|
||||
>
|
||||
<span className={'flex items-center space-x-1'}>
|
||||
<If condition={selected}>
|
||||
<CheckCircle className={'h-4 text-green-500'} />
|
||||
</If>
|
||||
|
||||
<span className={'capitalize'}>
|
||||
<Trans
|
||||
i18nKey={`common:plans.interval.${plan}`}
|
||||
defaults={plan}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DefaultCheckoutButton(
|
||||
props: React.PropsWithChildren<{
|
||||
plan: {
|
||||
id: string;
|
||||
href?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
signUpPath: string;
|
||||
highlighted?: boolean;
|
||||
}>,
|
||||
) {
|
||||
const linkHref =
|
||||
props.plan.href ?? `${props.signUpPath}?utm_source=${props.plan.id}` ?? '';
|
||||
|
||||
const label = props.plan.label ?? 'common:getStarted';
|
||||
|
||||
return (
|
||||
<div className={'bottom-0 left-0 w-full p-0'}>
|
||||
<Link className={'w-full'} href={linkHref}>
|
||||
<Button
|
||||
className={'w-full'}
|
||||
variant={props.highlighted ? 'default' : 'outline'}
|
||||
>
|
||||
<Trans i18nKey={label} defaults={label} />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user