Remove admin functionality related code
The admin functionality related code has been removed which includes various user and organization functionalities like delete, update, ban etc. This includes action logic, UI components and supportive utility functions. Notable deletions include the server action files, dialog components for actions like banning and deleting, and related utility functions. This massive cleanup is aimed at simplifying the codebase and the commit reflects adherence to project restructuring.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { PageHeader } from '@kit/ui/page';
|
||||
|
||||
import { MobileAppNavigation } from '~/(dashboard)/home/[account]/(components)/mobile-app-navigation';
|
||||
import { MobileAppNavigation } from '~/(dashboard)/home/[account]/_components/mobile-app-navigation';
|
||||
|
||||
export function AppHeader({
|
||||
children,
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { Session } from '@supabase/supabase-js';
|
||||
|
||||
import { ArrowLeftCircleIcon, ArrowRightCircleIcon } from 'lucide-react';
|
||||
|
||||
import { AccountSelector } from '@kit/accounts/account-selector';
|
||||
@@ -15,7 +17,7 @@ import {
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { ProfileDropdownContainer } from '~/(dashboard)/home/components/personal-account-dropdown';
|
||||
import { ProfileDropdownContainer } from '~/(dashboard)/home/_components/personal-account-dropdown';
|
||||
import featureFlagsConfig from '~/config/feature-flags.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
@@ -36,6 +38,7 @@ export function AppSidebar(props: {
|
||||
account: string;
|
||||
accounts: AccountModel[];
|
||||
collapsed: boolean;
|
||||
session: Session | null;
|
||||
}) {
|
||||
return (
|
||||
<Sidebar collapsed={props.collapsed}>
|
||||
@@ -45,6 +48,7 @@ export function AppSidebar(props: {
|
||||
setCollapsed={setCollapsed}
|
||||
account={props.account}
|
||||
accounts={props.accounts}
|
||||
session={props.session}
|
||||
/>
|
||||
)}
|
||||
</Sidebar>
|
||||
@@ -54,6 +58,7 @@ export function AppSidebar(props: {
|
||||
function SidebarContainer(props: {
|
||||
account: string;
|
||||
accounts: AccountModel[];
|
||||
session: Session | null;
|
||||
collapsed: boolean;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
}) {
|
||||
@@ -84,7 +89,10 @@ function SidebarContainer(props: {
|
||||
|
||||
<div className={'absolute bottom-4 left-0 w-full'}>
|
||||
<SidebarContent>
|
||||
<ProfileDropdownContainer collapsed={props.collapsed} />
|
||||
<ProfileDropdownContainer
|
||||
session={props.session}
|
||||
collapsed={props.collapsed}
|
||||
/>
|
||||
|
||||
<AppSidebarFooterMenu
|
||||
collapsed={props.collapsed}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
|
||||
import { createBillingPortalSession } from '../server-actions';
|
||||
|
||||
export function BillingPortalForm(props: { accountId: string }) {
|
||||
return (
|
||||
<div className={'mx-auto w-full max-w-2xl'}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Manage your Team Plan</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
You can change your plan at any time.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<form action={createBillingPortalSession}>
|
||||
<input type={'hidden'} name={'accountId'} value={props.accountId} />
|
||||
|
||||
<Button>Manage your Billing Settings</Button>
|
||||
|
||||
<span>
|
||||
Visit the billing portal to manage your subscription (update
|
||||
payment method, cancel subscription, etc.)
|
||||
</span>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { EmbeddedCheckout, PlanPicker } from '@kit/billing-gateway/components';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@kit/ui/card';
|
||||
|
||||
import billingConfig from '~/config/billing.config';
|
||||
|
||||
import { createTeamAccountCheckoutSession } from '../server-actions';
|
||||
|
||||
export function TeamAccountCheckoutForm(params: { accountId: string }) {
|
||||
const [pending, startTransition] = useTransition();
|
||||
const [checkoutToken, setCheckoutToken] = useState<string | null>(null);
|
||||
|
||||
// If the checkout token is set, render the embedded checkout component
|
||||
if (checkoutToken) {
|
||||
return (
|
||||
<EmbeddedCheckout
|
||||
checkoutToken={checkoutToken}
|
||||
provider={billingConfig.provider}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, render the plan picker component
|
||||
return (
|
||||
<div className={'mx-auto w-full max-w-2xl'}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Manage your Team Plan</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
You can change your plan at any time.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<PlanPicker
|
||||
pending={pending}
|
||||
config={billingConfig}
|
||||
onSubmit={({ planId }) => {
|
||||
startTransition(async () => {
|
||||
const { checkoutToken } =
|
||||
await createTeamAccountCheckoutSession({
|
||||
planId,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
|
||||
setCheckoutToken(checkoutToken);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,25 @@
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/_lib/load-workspace';
|
||||
import { BillingPortalForm } from '~/(dashboard)/home/[account]/billing/_components/billing-portal-form';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
function OrganizationAccountBillingPage() {
|
||||
import { TeamAccountCheckoutForm } from './_components/team-account-checkout-form';
|
||||
|
||||
interface Params {
|
||||
params: {
|
||||
account: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function OrganizationAccountBillingPage({ params }: Params) {
|
||||
const workspace = await loadOrganizationWorkspace(params.account);
|
||||
const accountId = workspace.account.id;
|
||||
const customerId = await loadCustomerIdFromAccount(accountId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
@@ -11,9 +27,31 @@ function OrganizationAccountBillingPage() {
|
||||
description={<Trans i18nKey={'common:billingTabDescription'} />}
|
||||
/>
|
||||
|
||||
<PageBody></PageBody>
|
||||
<PageBody>
|
||||
<TeamAccountCheckoutForm accountId={accountId} />
|
||||
|
||||
<If condition={customerId}>
|
||||
<BillingPortalForm accountId={accountId} />
|
||||
</If>
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(OrganizationAccountBillingPage);
|
||||
|
||||
async function loadCustomerIdFromAccount(accountId: string) {
|
||||
const client = getSupabaseServerComponentClient();
|
||||
|
||||
const { data, error } = await client
|
||||
.from('billing_customers')
|
||||
.select('customer_id')
|
||||
.eq('account_id', accountId)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data?.customer_id;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
||||
import { BillingSessionStatus } from '@kit/billing-gateway/components';
|
||||
import { requireAuth } from '@kit/supabase/require-auth';
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
|
||||
import billingConfig from '~/config/billing.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
interface SessionPageProps {
|
||||
searchParams: {
|
||||
session_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
const LazyEmbeddedCheckout = dynamic(async () => {
|
||||
const { EmbeddedCheckout } = await import('@kit/billing-gateway/components');
|
||||
|
||||
return EmbeddedCheckout;
|
||||
});
|
||||
|
||||
async function ReturnStripeSessionPage({ searchParams }: SessionPageProps) {
|
||||
const { customerEmail, checkoutToken } = await loadCheckoutSession(
|
||||
searchParams.session_id,
|
||||
);
|
||||
|
||||
if (checkoutToken) {
|
||||
return (
|
||||
<LazyEmbeddedCheckout
|
||||
checkoutToken={checkoutToken}
|
||||
provider={billingConfig.provider}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'fixed left-0 top-48 z-50 mx-auto w-full'}>
|
||||
<BillingSessionStatus
|
||||
customerEmail={customerEmail ?? ''}
|
||||
redirectPath={pathsConfig.app.home}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
'fixed left-0 top-0 w-full bg-background/30 backdrop-blur-sm' +
|
||||
' !m-0 h-full'
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(ReturnStripeSessionPage);
|
||||
|
||||
export async function loadCheckoutSession(sessionId: string) {
|
||||
const client = getSupabaseServerComponentClient();
|
||||
|
||||
await requireAuth(client);
|
||||
|
||||
const gateway = await getBillingGatewayProvider(client);
|
||||
|
||||
const session = await gateway.retrieveCheckoutSession({
|
||||
sessionId,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const checkoutToken = session.isSessionOpen ? session.checkoutToken : null;
|
||||
|
||||
// otherwise - we show the user the return page
|
||||
// and display the details of the session
|
||||
return {
|
||||
status: session.status,
|
||||
customerEmail: session.customer.email,
|
||||
checkoutToken,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
'use server';
|
||||
|
||||
import { headers } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getProductPlanPairFromId } from '@kit/billing';
|
||||
import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
||||
import { requireAuth } from '@kit/supabase/require-auth';
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import billingConfig from '~/config/billing.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
/**
|
||||
* Creates a checkout session for a team account.
|
||||
*
|
||||
* @param {object} params - The parameters for creating the checkout session.
|
||||
* @param {string} params.planId - The ID of the plan to be associated with the account.
|
||||
*/
|
||||
export async function createTeamAccountCheckoutSession(params: {
|
||||
planId: string;
|
||||
accountId: string;
|
||||
}) {
|
||||
const client = getSupabaseServerActionClient();
|
||||
|
||||
// we parse the plan ID from the parameters
|
||||
// no need in continuing if the plan ID is not valid
|
||||
const planId = z.string().min(1).parse(params.planId);
|
||||
|
||||
// we require the user to be authenticated
|
||||
const { data: session } = await requireAuth(client);
|
||||
|
||||
if (!session) {
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
|
||||
const userId = session.user.id;
|
||||
const accountId = params.accountId;
|
||||
|
||||
const hasPermission = await getPermissionsForAccountId(userId, accountId);
|
||||
|
||||
// if the user does not have permission to manage billing for the account
|
||||
// then we should not proceed
|
||||
if (!hasPermission) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
|
||||
// here we have confirmed that the user has permission to manage billing for the account
|
||||
// so we go on and create a checkout session
|
||||
const service = await getBillingGatewayProvider(client);
|
||||
const productPlanPairFromId = getProductPlanPairFromId(billingConfig, planId);
|
||||
|
||||
if (!productPlanPairFromId) {
|
||||
throw new Error('Product not found');
|
||||
}
|
||||
|
||||
// the return URL for the checkout session
|
||||
const returnUrl = getCheckoutSessionReturnUrl();
|
||||
|
||||
// find the customer ID for the account if it exists
|
||||
// (eg. if the account has been billed before)
|
||||
const customerId = await getCustomerIdFromAccountId(client, accountId);
|
||||
const customerEmail = session.user.email;
|
||||
|
||||
// retrieve the product and plan from the billing configuration
|
||||
const { product, plan } = productPlanPairFromId;
|
||||
|
||||
// call the payment gateway to create the checkout session
|
||||
const { checkoutToken } = await service.createCheckoutSession({
|
||||
accountId,
|
||||
returnUrl,
|
||||
planId,
|
||||
customerEmail,
|
||||
customerId,
|
||||
paymentType: product.paymentType,
|
||||
trialPeriodDays: plan.trialPeriodDays,
|
||||
});
|
||||
|
||||
// return the checkout token to the client
|
||||
// so we can call the payment gateway to complete the checkout
|
||||
return {
|
||||
checkoutToken,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createBillingPortalSession(data: FormData) {
|
||||
const client = getSupabaseServerActionClient();
|
||||
|
||||
const accountId = z
|
||||
.object({
|
||||
accountId: z.string().min(1),
|
||||
})
|
||||
.parse(Object.fromEntries(data)).accountId;
|
||||
|
||||
const { data: session, error } = await requireAuth(client);
|
||||
|
||||
if (error ?? !session) {
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
|
||||
const userId = session.user.id;
|
||||
|
||||
// we require the user to have permissions to manage billing for the account
|
||||
const hasPermission = await getPermissionsForAccountId(userId, accountId);
|
||||
|
||||
// if the user does not have permission to manage billing for the account
|
||||
// then we should not proceed
|
||||
if (!hasPermission) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
|
||||
const service = await getBillingGatewayProvider(client);
|
||||
const customerId = await getCustomerIdFromAccountId(client, accountId);
|
||||
const returnUrl = getBillingPortalReturnUrl();
|
||||
|
||||
if (!customerId) {
|
||||
throw new Error('Customer not found');
|
||||
}
|
||||
|
||||
const { url } = await service.createBillingPortalSession({
|
||||
customerId,
|
||||
returnUrl,
|
||||
});
|
||||
|
||||
// redirect the user to the billing portal
|
||||
return redirect(url);
|
||||
}
|
||||
|
||||
function getCheckoutSessionReturnUrl() {
|
||||
const origin = headers().get('origin')!;
|
||||
|
||||
return new URL(pathsConfig.app.accountBillingReturn, origin).toString();
|
||||
}
|
||||
|
||||
function getBillingPortalReturnUrl() {
|
||||
const origin = headers().get('origin')!;
|
||||
|
||||
return new URL(pathsConfig.app.accountBilling, origin).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the permissions for a user on an account for managing billing.
|
||||
* @param userId
|
||||
* @param accountId
|
||||
*/
|
||||
async function getPermissionsForAccountId(userId: string, accountId: string) {
|
||||
const client = getSupabaseServerActionClient();
|
||||
|
||||
const { data, error } = await client.rpc('has_permission', {
|
||||
account_id: accountId,
|
||||
user_id: userId,
|
||||
permission_name: 'billing.manage',
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function getCustomerIdFromAccountId(
|
||||
client: ReturnType<typeof getSupabaseServerActionClient>,
|
||||
accountId: string,
|
||||
) {
|
||||
const { data, error } = await client
|
||||
.from('billing_customers')
|
||||
.select('customer_id')
|
||||
.eq('account_id', accountId)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data?.customer_id;
|
||||
}
|
||||
@@ -1,37 +1,45 @@
|
||||
import { use } from 'react';
|
||||
|
||||
import { parseSidebarStateCookie } from '@kit/shared/cookies/sidebar-state.cookie';
|
||||
import { parseThemeCookie } from '@kit/shared/cookies/theme.cookie';
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
import { Page } from '@kit/ui/page';
|
||||
|
||||
import { AppSidebar } from '~/(dashboard)/home/[account]/_components/app-sidebar';
|
||||
import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/_lib/load-workspace';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { AppSidebar } from './(components)/app-sidebar';
|
||||
import { loadOrganizationWorkspace } from './(lib)/load-workspace';
|
||||
|
||||
interface Params {
|
||||
account: string;
|
||||
}
|
||||
|
||||
async function OrganizationWorkspaceLayout({
|
||||
function OrganizationWorkspaceLayout({
|
||||
children,
|
||||
params,
|
||||
}: React.PropsWithChildren<{
|
||||
params: Params;
|
||||
}>) {
|
||||
const data = await loadOrganizationWorkspace(params.account);
|
||||
const [data, session] = use(
|
||||
Promise.all([loadOrganizationWorkspace(params.account), loadSession()]),
|
||||
);
|
||||
|
||||
const ui = getUIStateCookies();
|
||||
const sidebarCollapsed = ui.sidebarState === 'collapsed';
|
||||
|
||||
const accounts = data.accounts.map(({ name, slug, picture_url }) => ({
|
||||
label: name,
|
||||
value: slug,
|
||||
image: picture_url,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Page
|
||||
sidebar={
|
||||
<AppSidebar
|
||||
collapsed={sidebarCollapsed}
|
||||
account={params.account}
|
||||
accounts={data.accounts.map(({ name, slug, picture_url }) => ({
|
||||
label: name,
|
||||
value: slug,
|
||||
image: picture_url,
|
||||
}))}
|
||||
session={session}
|
||||
accounts={accounts}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -48,3 +56,18 @@ function getUIStateCookies() {
|
||||
sidebarState: parseSidebarStateCookie(),
|
||||
};
|
||||
}
|
||||
|
||||
async function loadSession() {
|
||||
const client = getSupabaseServerComponentClient();
|
||||
|
||||
const {
|
||||
data: { session },
|
||||
error,
|
||||
} = await client.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PlusCircledIcon } from '@radix-ui/react-icons';
|
||||
import { PlusCircleIcon } from 'lucide-react';
|
||||
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
import {
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/(lib)/load-workspace';
|
||||
import { loadOrganizationWorkspace } from '~/(dashboard)/home/[account]/_lib/load-workspace';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
interface Params {
|
||||
@@ -99,7 +99,7 @@ async function OrganizationAccountMembersPage({ params }: Params) {
|
||||
|
||||
<InviteMembersDialogContainer account={account.slug}>
|
||||
<Button size={'sm'}>
|
||||
<PlusCircledIcon className={'mr-2 w-4'} />
|
||||
<PlusCircleIcon className={'mr-2 w-4'} />
|
||||
<span>Add Member</span>
|
||||
</Button>
|
||||
</InviteMembersDialogContainer>
|
||||
|
||||
@@ -7,11 +7,11 @@ 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 { 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'),
|
||||
() => import('~/(dashboard)/home/[account]/_components/dashboard-demo'),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { CheckIcon, ChevronRightIcon } from 'lucide-react';
|
||||
import type { Stripe } from 'stripe';
|
||||
|
||||
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
|
||||
* for the `paid` status.
|
||||
*
|
||||
* @param {Stripe.Checkout.Session['status']} status - The status of the Stripe checkout session.
|
||||
* @param {string} customerEmail - The email address of the customer associated with the session.
|
||||
*
|
||||
* @returns {ReactElement} - The component to render based on the session status.
|
||||
*/
|
||||
export function BillingSessionStatus({
|
||||
customerEmail,
|
||||
}: React.PropsWithChildren<{
|
||||
status: Stripe.Checkout.Session['status'];
|
||||
customerEmail: string;
|
||||
}>) {
|
||||
return <SuccessSessionStatus customerEmail={customerEmail} />;
|
||||
}
|
||||
|
||||
function SuccessSessionStatus({
|
||||
customerEmail,
|
||||
}: React.PropsWithChildren<{
|
||||
customerEmail: string;
|
||||
}>) {
|
||||
return (
|
||||
<section
|
||||
data-test={'payment-return-success'}
|
||||
className={
|
||||
'mx-auto max-w-xl rounded-xl border p-16 fade-in xl:drop-shadow-sm' +
|
||||
' dark:border-dark-800 border-gray-100' +
|
||||
' bg-background ease-out animate-in slide-in-from-bottom-8' +
|
||||
' duration-1000 zoom-in-50 dark:shadow-2xl dark:shadow-primary/40'
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex flex-col items-center justify-center space-y-4 text-center'
|
||||
}
|
||||
>
|
||||
<CheckIcon
|
||||
className={
|
||||
'w-16 rounded-full bg-green-500 p-1 text-white ring-8' +
|
||||
' ring-green-500/30 dark:ring-green-500/50'
|
||||
}
|
||||
/>
|
||||
|
||||
<Heading level={3}>
|
||||
<span className={'mr-4 font-semibold'}>
|
||||
<Trans i18nKey={'subscription:checkoutSuccessTitle'} />
|
||||
</span>
|
||||
🎉
|
||||
</Heading>
|
||||
|
||||
<div
|
||||
className={'flex flex-col space-y-4 text-gray-500 dark:text-gray-400'}
|
||||
>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey={'subscription:checkoutSuccessDescription'}
|
||||
values={{ customerEmail }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button data-test={'checkout-success-back-button'} variant={'outline'}>
|
||||
<Link href={pathsConfig.app.home}>
|
||||
<span className={'flex items-center space-x-2.5'}>
|
||||
<span>
|
||||
<Trans i18nKey={'subscription:checkoutSuccessBackButton'} />
|
||||
</span>
|
||||
|
||||
<ChevronRightIcon className={'h-4'} />
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const EmbeddedStripeCheckout = dynamic(
|
||||
() => {
|
||||
return import('../../components/embedded-stripe-checkout');
|
||||
},
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
function RecoverCheckout({ clientSecret }: { clientSecret: string }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<EmbeddedStripeCheckout
|
||||
clientSecret={clientSecret}
|
||||
onClose={() => {
|
||||
return router.replace('/settings/subscription');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecoverCheckout;
|
||||
@@ -1,81 +0,0 @@
|
||||
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 { BillingSessionStatus } from './components/billing-session-status';
|
||||
import RecoverCheckout from './components/recover-checkout';
|
||||
|
||||
interface SessionPageProps {
|
||||
searchParams: {
|
||||
session_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function ReturnStripeSessionPage({ searchParams }: SessionPageProps) {
|
||||
const { status, customerEmail, clientSecret } = await loadStripeSession(
|
||||
searchParams.session_id,
|
||||
);
|
||||
|
||||
if (clientSecret) {
|
||||
return <RecoverCheckout clientSecret={clientSecret} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'fixed left-0 top-48 z-50 mx-auto w-full'}>
|
||||
<BillingSessionStatus
|
||||
status={status}
|
||||
customerEmail={customerEmail ?? ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
'fixed left-0 top-0 w-full bg-background/30 backdrop-blur-sm' +
|
||||
' !m-0 h-full'
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(ReturnStripeSessionPage);
|
||||
|
||||
export async function loadStripeSession(sessionId: string) {
|
||||
await requireSession(getSupabaseServerComponentClient());
|
||||
|
||||
// now we fetch the session from Stripe
|
||||
// and check if it's still open
|
||||
const stripe = await createStripeClient();
|
||||
|
||||
const session = await stripe.checkout.sessions
|
||||
.retrieve(sessionId)
|
||||
.catch(() => undefined);
|
||||
|
||||
if (!session) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const isSessionOpen = session.status === 'open';
|
||||
const clientSecret = isSessionOpen ? session.client_secret : null;
|
||||
const isEmbeddedMode = session.ui_mode === 'embedded';
|
||||
|
||||
// if the session is still open, we redirect the user to the checkout page
|
||||
// in Stripe self hosted mode
|
||||
if (isSessionOpen && !isEmbeddedMode && session.url) {
|
||||
redirect(session.url);
|
||||
}
|
||||
|
||||
// otherwise - we show the user the return page
|
||||
// and display the details of the session
|
||||
return {
|
||||
status: session.status,
|
||||
customerEmail: session.customer_details?.email,
|
||||
clientSecret,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user