Remove billing and checkout redirect buttons and related services

Deleted the billing-redirect-button, checkout-redirect-button, and embedded-stripe-checkout components. Additionally, removed the shadcn directory, which encompassed billing-related icons. This change streamlines the subscription settings interface and organizes the system's payment management. This update is a stepping stone towards improving the billing system's overall architecture.
This commit is contained in:
giancarlo
2024-03-25 11:39:41 +08:00
parent 78c704e54d
commit cb8b23e8c0
123 changed files with 1674 additions and 3071 deletions

View File

@@ -1,7 +1,7 @@
import { MobileAppNavigation } from '~/(dashboard)/home/[account]/(components)/mobile-app-navigation';
import { PageHeader } from '@kit/ui/page';
import { MobileAppNavigation } from '~/(dashboard)/home/[account]/(components)/mobile-app-navigation';
export function AppHeader({
children,
title,

View File

@@ -1,10 +1,10 @@
'use client';
import { getOrganizationAccountSidebarConfig } from '~/config/organization-account-sidebar.config';
import { SidebarDivider, SidebarGroup, SidebarItem } from '@kit/ui/sidebar';
import { Trans } from '@kit/ui/trans';
import { getOrganizationAccountSidebarConfig } from '~/config/organization-account-sidebar.config';
export function AppSidebarNavigation({
account,
}: React.PropsWithChildren<{

View File

@@ -3,9 +3,6 @@
import { useRouter } from 'next/navigation';
import { ArrowLeftCircleIcon, ArrowRightCircleIcon } from 'lucide-react';
import { ProfileDropdownContainer } from '~/(dashboard)/home/components/personal-account-dropdown';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
import { AccountSelector } from '@kit/accounts/account-selector';
import { Sidebar, SidebarContent } from '@kit/ui/sidebar';
@@ -18,6 +15,10 @@ import {
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import { ProfileDropdownContainer } from '~/(dashboard)/home/components/personal-account-dropdown';
import featureFlagsConfig from '~/config/feature-flags.config';
import pathsConfig from '~/config/paths.config';
import { AppSidebarNavigation } from './app-sidebar-navigation';
type AccountModel = {

View File

@@ -4,9 +4,6 @@ import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { HomeIcon, LogOutIcon, MenuIcon } from 'lucide-react';
import featureFlagsConfig from '~/config/feature-flags.config';
import { getOrganizationAccountSidebarConfig } from '~/config/organization-account-sidebar.config';
import pathsConfig from '~/config/paths.config';
import { AccountSelector } from '@kit/accounts/account-selector';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
@@ -26,6 +23,10 @@ import {
} from '@kit/ui/dropdown-menu';
import { Trans } from '@kit/ui/trans';
import featureFlagsConfig from '~/config/feature-flags.config';
import { getOrganizationAccountSidebarConfig } from '~/config/organization-account-sidebar.config';
import pathsConfig from '~/config/paths.config';
export const MobileAppNavigation = (
props: React.PropsWithChildren<{
slug: string;

View File

@@ -3,10 +3,11 @@ import { cache } from 'react';
import { redirect } from 'next/navigation';
import 'server-only';
import pathsConfig from '~/config/paths.config';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import pathsConfig from '~/config/paths.config';
/**
* Load the organization workspace data.
* We place this function into a separate file so it can be reused in multiple places across the server components.

View File

@@ -1,8 +1,8 @@
import { withI18n } from '~/lib/i18n/with-i18n';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
function OrganizationAccountBillingPage() {
return (
<>

View File

@@ -1,9 +1,9 @@
import { withI18n } from '~/lib/i18n/with-i18n';
import { parseSidebarStateCookie } from '@kit/shared/cookies/sidebar-state.cookie';
import { parseThemeCookie } from '@kit/shared/cookies/theme.cookie';
import { Page } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
import { AppSidebar } from './(components)/app-sidebar';
import { loadOrganizationWorkspace } from './(lib)/load-workspace';

View File

@@ -1,6 +1,4 @@
import { PlusCircledIcon } from '@radix-ui/react-icons';
import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/(lib)/load-workspace';
import { withI18n } from '~/lib/i18n/with-i18n';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import {
@@ -19,6 +17,9 @@ import {
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/(lib)/load-workspace';
import { withI18n } from '~/lib/i18n/with-i18n';
interface Params {
params: {
account: string;

View File

@@ -1,14 +1,15 @@
import loadDynamic from 'next/dynamic';
import { PlusIcon } from 'lucide-react';
import { AppHeader } from '~/(dashboard)/home/[account]/(components)/app-header';
import { withI18n } from '~/lib/i18n/with-i18n';
import { Button } from '@kit/ui/button';
import { PageBody } from '@kit/ui/page';
import Spinner from '@kit/ui/spinner';
import { Trans } from '@kit/ui/trans';
import { AppHeader } from '~/(dashboard)/home/[account]/(components)/app-header';
import { withI18n } from '~/lib/i18n/with-i18n';
const DashboardDemo = loadDynamic(
() => import('~/(dashboard)/home/[account]/(components)/dashboard-demo'),
{

View File

@@ -1,3 +0,0 @@
import { GlobalLoader } from '@kit/ui/global-loader';
export default GlobalLoader;

View File

@@ -1,87 +0,0 @@
import { use } from 'react';
import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/(lib)/load-workspace';
import featureFlagsConfig from '~/config/feature-flags.config';
import { withI18n } from '~/lib/i18n/with-i18n';
import {
TeamAccountDangerZone,
UpdateOrganizationForm,
} from '@kit/team-accounts/components';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { If } from '@kit/ui/if';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
export const metadata = {
title: 'Organization Settings',
};
const allowOrganizationDelete = featureFlagsConfig.enableOrganizationDeletion;
interface Params {
params: {
account: string;
};
}
function OrganizationSettingsPage({ params }: Params) {
const { account, user } = use(loadOrganizationWorkspace(params.account));
return (
<>
<PageHeader
title={<Trans i18nKey={'organization:settingsPageTitle'} />}
description={<Trans i18nKey={'organization:settingsPageDescription'} />}
/>
<PageBody>
<div className={'mx-auto flex max-w-5xl flex-col space-y-4'}>
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey={'organization:generalTabLabel'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'organization:generalTabLabelSubheading'} />
</CardDescription>
</CardHeader>
<CardContent>
<UpdateOrganizationForm
accountId={account.id}
accountName={account.name}
/>
</CardContent>
</Card>
<If condition={allowOrganizationDelete}>
<Card className={'border-2 border-destructive'}>
<CardHeader>
<CardTitle>
<Trans i18nKey={'organization:dangerZone'} />
</CardTitle>
<CardDescription>
<Trans i18nKey={'organization:dangerZoneSubheading'} />
</CardDescription>
</CardHeader>
<CardContent>
<TeamAccountDangerZone userId={user.id} account={account} />
</CardContent>
</Card>
</If>
</div>
</PageBody>
</>
);
}
export default withI18n(OrganizationSettingsPage);

View File

@@ -1,32 +0,0 @@
'use client';
import { ArrowUpRightIcon } from 'lucide-react';
import { Button } from '@kit/ui/button';
export function BillingPortalRedirectButton({
children,
customerId,
className,
}: React.PropsWithChildren<{
customerId: string;
className?: string;
}>) {
return (
<form action={createBillingPortalSessionAction}>
<input type={'hidden'} name={'customerId'} value={customerId} />
<Button
data-test={'manage-billing-redirect-button'}
variant={'outline'}
className={className}
>
<span className={'flex items-center space-x-2'}>
<span>{children}</span>
<ArrowUpRightIcon className={'h-3'} />
</span>
</Button>
</form>
);
}

View File

@@ -1,101 +0,0 @@
'use client';
import { useEffect } from 'react';
import { useFormState, useFormStatus } from 'react-dom';
import { ChevronRightIcon } from 'lucide-react';
import { isBrowser } from '@kit/shared/utils';
import { Button } from '@kit/ui/button';
import { cn } from '@kit/ui/utils';
export function CheckoutRedirectButton({
children,
onCheckoutCreated,
...props
}): React.PropsWithChildren<{
disabled?: boolean;
stripePriceId?: string;
recommended?: boolean;
organizationUid: string;
onCheckoutCreated?: (clientSecret: string) => void;
}> {
const [state, formAction] = useFormState(createCheckoutAction, {
clientSecret: '',
});
useEffect(() => {
if (state.clientSecret && onCheckoutCreated) {
onCheckoutCreated(state.clientSecret);
}
}, [state.clientSecret, onCheckoutCreated]);
return (
<form data-test={'checkout-form'} action={formAction}>
<CheckoutFormData
organizationUid={props.organizationUid}
priceId={props.stripePriceId}
/>
<SubmitCheckoutButton
disabled={props.disabled}
recommended={props.recommended}
>
{children}
</SubmitCheckoutButton>
</form>
);
}
function SubmitCheckoutButton(
props: React.PropsWithChildren<{
recommended?: boolean;
disabled?: boolean;
}>,
) {
const { pending } = useFormStatus();
return (
<Button
className={cn({
'bg-primary text-primary-foreground dark:bg-white dark:text-gray-900':
props.recommended,
})}
variant={props.recommended ? 'custom' : 'outline'}
disabled={props.disabled ?? pending}
>
<span className={'flex items-center space-x-2'}>
<span>{props.children}</span>
<ChevronRightIcon className={'h-4'} />
</span>
</Button>
);
}
function CheckoutFormData(
props: React.PropsWithChildren<{
organizationUid: string | undefined;
priceId: string | undefined;
}>,
) {
return (
<>
<input
type="hidden"
name={'organizationUid'}
defaultValue={props.organizationUid}
/>
<input type="hidden" name={'returnUrl'} defaultValue={getReturnUrl()} />
<input type="hidden" name={'priceId'} defaultValue={props.priceId} />
</>
);
}
function getReturnUrl() {
return isBrowser()
? [window.location.origin, window.location.pathname].join('')
: undefined;
}

View File

@@ -1,137 +0,0 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { Close as DialogPrimitiveClose } from '@radix-ui/react-dialog';
import {
EmbeddedCheckout,
EmbeddedCheckoutProvider,
} from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
import { XIcon } from 'lucide-react';
import pricingConfig, {
StripeCheckoutDisplayMode,
} from '@/config/pricing.config';
import { cn } from '@/lib/utils';
import If from '@/components/app/If';
import LogoImage from '@/components/app/Logo/LogoImage';
import Trans from '@/components/app/Trans';
const STRIPE_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
if (!STRIPE_PUBLISHABLE_KEY) {
throw new Error(
'Missing NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY environment variable. Did you forget to add it to your .env file?',
);
}
const stripePromise = loadStripe(STRIPE_PUBLISHABLE_KEY);
export default function EmbeddedStripeCheckout({
clientSecret,
onClose,
}: React.PropsWithChildren<{
clientSecret: string;
onClose?: () => void;
}>) {
return (
<EmbeddedCheckoutPopup key={clientSecret} onClose={onClose}>
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={{ clientSecret }}
>
<EmbeddedCheckout className={'EmbeddedCheckoutClassName'} />
</EmbeddedCheckoutProvider>
</EmbeddedCheckoutPopup>
);
}
function EmbeddedCheckoutPopup({
onClose,
children,
}: React.PropsWithChildren<{
onClose?: () => void;
}>) {
const [open, setOpen] = useState(true);
const displayMode = pricingConfig.displayMode;
const isPopup = displayMode === StripeCheckoutDisplayMode.Popup;
const isOverlay = displayMode === StripeCheckoutDisplayMode.Overlay;
const className = cn({
[`bg-white p-4 max-h-[98vh] overflow-y-auto shadow-transparent border border-gray-200 dark:border-dark-700`]:
isPopup,
[`bg-background !flex flex-col flex-1 fixed top-0 !max-h-full !max-w-full left-0 w-screen h-screen border-transparent shadow-transparent py-4 px-8`]:
isOverlay,
});
const close = () => {
setOpen(false);
if (onClose) {
onClose();
}
};
return (
<Dialog
defaultOpen
open={open}
onOpenChange={(open) => {
if (!open && onClose) {
onClose();
}
setOpen(open);
}}
>
<DialogContent
className={className}
onOpenAutoFocus={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<If condition={isOverlay}>
<div className={'mb-8'}>
<div className={'flex items-center justify-between'}>
<LogoImage />
<Button onClick={close} variant={'outline'}>
<Trans i18nKey={'common:cancel'} />
</Button>
</div>
</div>
</If>
<If condition={isPopup}>
<DialogPrimitiveClose asChild>
<Button
size={'icon'}
className={'absolute right-4 top-2 flex items-center'}
aria-label={'Close Checkout'}
onClick={close}
>
<XIcon className={'h-6 text-gray-900'} />
<span className="sr-only">
<Trans i18nKey={'common:cancel'} />
</span>
</Button>
</DialogPrimitiveClose>
</If>
<div
className={cn({
[`flex-1 rounded-xl bg-white p-8`]: isOverlay,
})}
>
{children}
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,112 +0,0 @@
'use client';
import React, { useState } from 'react';
import dynamic from 'next/dynamic';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import type Organization from '@/lib/organizations/types/organization';
import ErrorBoundary from '@/components/app/ErrorBoundary';
import If from '@/components/app/If';
import PricingTable from '@/components/app/PricingTable';
import Trans from '@/components/app/Trans';
import BillingPortalRedirectButton from './billing-redirect-button';
import CheckoutRedirectButton from './checkout-redirect-button';
const EmbeddedStripeCheckout = dynamic(
() => import('./embedded-stripe-checkout'),
{
ssr: false,
},
);
const PlanSelectionForm: React.FC<{
organization: WithId<Organization>;
customerId: Maybe<string>;
}> = ({ organization, customerId }) => {
const [clientSecret, setClientSecret] = useState<string>();
const [retry, setRetry] = useState(0);
return (
<div className={'flex flex-col space-y-6'}>
<If condition={clientSecret}>
<EmbeddedStripeCheckout clientSecret={clientSecret!} />
</If>
<div className={'flex w-full flex-col justify-center space-y-8'}>
<PricingTable
CheckoutButton={(props) => {
return (
<ErrorBoundary
key={retry}
fallback={
<CheckoutErrorMessage
onRetry={() => setRetry((retry) => retry + 1)}
/>
}
>
<CheckoutRedirectButton
organizationUid={organization.uuid}
stripePriceId={props.stripePriceId}
recommended={props.recommended}
onCheckoutCreated={setClientSecret}
>
<Trans
i18nKey={'subscription:checkout'}
defaults={'Checkout'}
/>
</CheckoutRedirectButton>
</ErrorBoundary>
);
}}
/>
<If condition={customerId}>
<div className={'flex flex-col space-y-2'}>
<BillingPortalRedirectButton customerId={customerId as string}>
<Trans i18nKey={'subscription:manageBilling'} />
</BillingPortalRedirectButton>
<span className={'text-xs text-gray-500 dark:text-gray-400'}>
<Trans i18nKey={'subscription:manageBillingDescription'} />
</span>
</div>
</If>
</div>
</div>
);
};
export default PlanSelectionForm;
function NoPermissionsAlert() {
return (
<Alert variant={'warning'}>
<AlertTitle>
<Trans i18nKey={'subscription:noPermissionsAlertHeading'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'subscription:noPermissionsAlertBody'} />
</AlertDescription>
</Alert>
);
}
function CheckoutErrorMessage({ onRetry }: { onRetry: () => void }) {
return (
<div className={'flex flex-col space-y-2'}>
<span className={'text-sm font-medium text-red-500'}>
<Trans i18nKey={'subscription:unknownErrorAlertHeading'} />
</span>
<Button onClick={onRetry} variant={'ghost'}>
<Trans i18nKey={'common:retry'} />
</Button>
</div>
);
}

View File

@@ -1,99 +0,0 @@
'use client';
import React from 'react';
import type { ReadonlyURLSearchParams } from 'next/navigation';
import { useSearchParams } from 'next/navigation';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import Trans from '@/components/app/Trans';
enum SubscriptionStatusQueryParams {
Success = 'success',
Cancel = 'cancel',
Error = 'error',
}
function PlansStatusAlertContainer() {
const status = useSubscriptionStatus();
if (status === undefined) {
return null;
}
return <PlansStatusAlert status={status as SubscriptionStatusQueryParams} />;
}
export default PlansStatusAlertContainer;
function PlansStatusAlert({
status,
}: {
status: SubscriptionStatusQueryParams;
}) {
switch (status) {
case SubscriptionStatusQueryParams.Cancel:
return (
<Alert variant={'warning'}>
<AlertTitle>
<Trans i18nKey={'subscription:checkOutCanceledAlertHeading'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'subscription:checkOutCanceledAlert'} />
</AlertDescription>
</Alert>
);
case SubscriptionStatusQueryParams.Error:
return (
<Alert variant={'destructive'}>
<AlertTitle>
<Trans i18nKey={'subscription:unknownErrorAlertHeading'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'subscription:unknownErrorAlert'} />
</AlertDescription>
</Alert>
);
case SubscriptionStatusQueryParams.Success:
return (
<Alert variant={'success'}>
<AlertTitle>
<Trans i18nKey={'subscription:checkOutCompletedAlertHeading'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'subscription:checkOutCompletedAlert'} />
</AlertDescription>
</Alert>
);
}
}
function useSubscriptionStatus() {
const params = useSearchParams();
return getStatus(params);
}
function getStatus(params: ReadonlyURLSearchParams | null) {
if (!params) {
return;
}
const error = params.has(SubscriptionStatusQueryParams.Error);
const canceled = params.has(SubscriptionStatusQueryParams.Cancel);
const success = params.has(SubscriptionStatusQueryParams.Success);
if (canceled) {
return SubscriptionStatusQueryParams.Cancel;
} else if (success) {
return SubscriptionStatusQueryParams.Success;
} else if (error) {
return SubscriptionStatusQueryParams.Error;
}
}

View File

@@ -1,57 +0,0 @@
'use client';
import useCurrentOrganization from '@/lib/organizations/hooks/use-current-organization';
import If from '@/components/app/If';
import Trans from '@/components/app/Trans';
import BillingPortalRedirectButton from './billing-redirect-button';
import PlanSelectionForm from './plan-selection-form';
import SubscriptionCard from './subscription-card';
const PlansContainer: React.FC = () => {
const organization = useCurrentOrganization();
if (!organization) {
return null;
}
const customerId = organization.subscription?.customerId;
const subscription = organization.subscription?.data;
if (!subscription) {
return (
<PlanSelectionForm customerId={customerId} organization={organization} />
);
}
return (
<div className={'flex flex-col space-y-4'}>
<div>
<div
className={'w-full divide-y rounded-xl border lg:w-9/12 xl:w-6/12'}
>
<div className={'p-6'}>
<SubscriptionCard subscription={subscription} />
</div>
<If condition={customerId}>
<div className={'flex justify-end p-6'}>
<div className={'flex flex-col items-end space-y-2'}>
<BillingPortalRedirectButton customerId={customerId as string}>
<Trans i18nKey={'subscription:manageBilling'} />
</BillingPortalRedirectButton>
<span className={'text-xs text-gray-500 dark:text-gray-400'}>
<Trans i18nKey={'subscription:manageBillingDescription'} />
</span>
</div>
</div>
</If>
</div>
</div>
</div>
);
};
export default PlansContainer;

View File

@@ -1,141 +0,0 @@
import React, { useMemo } from 'react';
import Heading from '@/components/ui/heading';
import { CheckCircleIcon, XCircleIcon } from 'lucide-react';
import { getI18n } from 'react-i18next';
import SubscriptionStatusBadge from '~/(dashboard)/home/[account]/(components)/organizations/SubscriptionStatusBadge';
import pricingConfig from '@/config/pricing.config';
import type { OrganizationSubscription } from '@/lib/organizations/types/organization-subscription';
import If from '@/components/app/If';
import PricingTable from '@/components/app/PricingTable';
import Trans from '@/components/app/Trans';
import SubscriptionStatusAlert from './subscription-status-alert';
const SubscriptionCard: React.FC<{
subscription: OrganizationSubscription;
}> = ({ subscription }) => {
const details = useSubscriptionDetails(subscription.priceId);
const cancelAtPeriodEnd = subscription.cancelAtPeriodEnd;
const isActive = subscription.status === 'active';
const language = getI18n().language;
const dates = useMemo(() => {
const endDate = new Date(subscription.periodEndsAt);
const trialEndDate =
subscription.trialEndsAt && new Date(subscription.trialEndsAt);
return {
endDate: endDate.toLocaleDateString(language),
trialEndDate: trialEndDate
? trialEndDate.toLocaleDateString(language)
: null,
};
}, [language, subscription]);
if (!details) {
return null;
}
return (
<div
className={'flex space-x-2'}
data-test={'subscription-card'}
data-test-status={subscription.status}
>
<div className={'flex w-9/12 flex-col space-y-4'}>
<div className={'flex flex-col space-y-1'}>
<div className={'flex items-center space-x-4'}>
<Heading level={4}>
<span data-test={'subscription-name'}>
{details.product.name}
</span>
</Heading>
<div>
<SubscriptionStatusBadge subscription={subscription} />
</div>
</div>
<span className={'text-sm text-gray-500 dark:text-gray-400'}>
{details.product.description}
</span>
</div>
<If condition={isActive}>
<RenewStatusDescription
dates={dates}
cancelAtPeriodEnd={cancelAtPeriodEnd}
/>
</If>
<SubscriptionStatusAlert subscription={subscription} values={dates} />
</div>
<div className={'w-3/12'}>
<span className={'flex items-center justify-end space-x-1'}>
<PricingTable.Price>{details.plan.price}</PricingTable.Price>
<span className={'lowercase text-gray-500 dark:text-gray-400'}>
/{details.plan.name}
</span>
</span>
</div>
</div>
);
};
function RenewStatusDescription(
props: React.PropsWithChildren<{
cancelAtPeriodEnd: boolean;
dates: {
endDate: string;
trialEndDate: string | null;
};
}>,
) {
return (
<span className={'flex items-center space-x-1.5 text-sm'}>
<If condition={props.cancelAtPeriodEnd}>
<XCircleIcon className={'h-5 text-yellow-700'} />
<span>
<Trans
i18nKey={'subscription:cancelAtPeriodEndDescription'}
values={props.dates}
/>
</span>
</If>
<If condition={!props.cancelAtPeriodEnd}>
<CheckCircleIcon className={'h-5 text-green-700'} />
<span>
<Trans
i18nKey={'subscription:renewAtPeriodEndDescription'}
values={props.dates}
/>
</span>
</If>
</span>
);
}
function useSubscriptionDetails(priceId: string) {
const products = pricingConfig.products;
return useMemo(() => {
for (const product of products) {
for (const plan of product.plans) {
if (plan.stripePriceId === priceId) {
return { plan, product };
}
}
}
}, [products, priceId]);
}
export default SubscriptionCard;

View File

@@ -1,68 +0,0 @@
import classNames from 'clsx';
import type { OrganizationSubscription } from '@/lib/organizations/types/organization-subscription';
import Trans from '@/components/app/Trans';
function SubscriptionStatusAlert(
props: React.PropsWithChildren<{
subscription: OrganizationSubscription;
values: {
endDate: string;
trialEndDate: string | null;
};
}>,
) {
const status = props.subscription.status;
let message = '';
let type: 'success' | 'error' | 'warn';
switch (status) {
case 'active':
message = 'subscription:status.active.description';
type = 'success';
break;
case 'trialing':
message = 'subscription:status.trialing.description';
type = 'success';
break;
case 'canceled':
message = 'subscription:status.canceled.description';
type = 'warn';
break;
case 'incomplete':
message = 'subscription:status.incomplete.description';
type = 'warn';
break;
case 'incomplete_expired':
message = 'subscription:status.incomplete_expired.description';
type = 'error';
break;
case 'unpaid':
message = 'subscription:status.unpaid.description';
type = 'error';
break;
case 'past_due':
message = 'subscription:status.past_due.description';
type = 'error';
break;
default:
return null;
}
return (
<span
className={classNames('text-sm', {
'text-orange-700 dark:text-gray-400': type === 'warn',
'text-red-700 dark:text-red-400': type === 'error',
'text-green-700 dark:text-green-400': type === 'success',
})}
>
<Trans i18nKey={message} values={props.values} />
</span>
);
}
export default SubscriptionStatusAlert;

View File

@@ -1,34 +0,0 @@
import Heading from '@/components/ui/heading';
import { withI18n } from '@packages/i18n/with-i18n';
import Trans from '@/components/app/Trans';
import PlansStatusAlertContainer from './components/plan-status-alert-container';
import PlansContainer from './components/plans-container';
export const metadata = {
title: 'Subscription',
};
const SubscriptionSettingsPage = () => {
return (
<div className={'flex w-full flex-col space-y-4'}>
<div className={'flex flex-col space-y-1 px-2'}>
<Heading level={4}>
<Trans i18nKey={'common:subscriptionSettingsTabLabel'} />
</Heading>
<span className={'text-gray-500 dark:text-gray-400'}>
<Trans i18nKey={'subscription:subscriptionTabSubheading'} />
</span>
</div>
<PlansStatusAlertContainer />
<PlansContainer />
</div>
);
};
export default withI18n(SubscriptionSettingsPage);

View File

@@ -4,12 +4,13 @@ import Link from 'next/link';
import { CheckIcon, ChevronRightIcon } from 'lucide-react';
import type { Stripe } from 'stripe';
import pathsConfig from '~/config/paths.config';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
import { Trans } from '@kit/ui/trans';
import pathsConfig from '~/config/paths.config';
/**
* Retrieves the session status for a Stripe checkout session.
* Since we should only arrive here for a successful checkout, we only check

View File

@@ -1,12 +1,12 @@
import { notFound, redirect } from 'next/navigation';
import requireSession from '@/lib/user/require-session';
import { withI18n } from '@packages/i18n/with-i18n';
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
import createStripeClient from '@kit/stripe/get-stripe';
import requireSession from '@/lib/user/require-session';
import { BillingSessionStatus } from './components/billing-session-status';
import RecoverCheckout from './components/recover-checkout';