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:
@@ -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 UserSettingsLayout(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PersonalAccountSettingsContainer } from '@kit/accounts/personal-account-settings';
|
||||
|
||||
import featureFlagsConfig from '~/config/feature-flags.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { PersonalAccountSettingsContainer } from '@kit/accounts/personal-account-settings';
|
||||
|
||||
function PersonalAccountSettingsPage() {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
'use server';
|
||||
|
||||
import { URL } from 'next/dist/compiled/@edge-runtime/primitives';
|
||||
import { headers } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getProductPlanPairFromId } from '@kit/billing';
|
||||
import { getGatewayProvider } from '@kit/billing-gateway';
|
||||
import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import billingConfig from '~/config/billing.config';
|
||||
@@ -30,7 +29,7 @@ export async function createPersonalAccountCheckoutSession(params: {
|
||||
}
|
||||
|
||||
const planId = z.string().min(1).parse(params.planId);
|
||||
const service = await getGatewayProvider(client);
|
||||
const service = await getBillingGatewayProvider(client);
|
||||
const productPlanPairFromId = getProductPlanPairFromId(billingConfig, planId);
|
||||
|
||||
if (!productPlanPairFromId) {
|
||||
@@ -77,7 +76,7 @@ export async function createBillingPortalSession() {
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
|
||||
const service = await getGatewayProvider(client);
|
||||
const service = await getBillingGatewayProvider(client);
|
||||
|
||||
const accountId = data.user.id;
|
||||
const customerId = await getCustomerIdFromAccountId(accountId);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Page } from '@kit/ui/page';
|
||||
|
||||
import { HomeSidebar } from '~/(dashboard)/home/components/home-sidebar';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { Page } from '@kit/ui/page';
|
||||
|
||||
function UserHomeLayout({ children }: React.PropsWithChildren) {
|
||||
return <Page sidebar={<HomeSidebar />}>{children}</Page>;
|
||||
}
|
||||
|
||||
@@ -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 UserHomePage() {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'),
|
||||
{
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { GlobalLoader } from '@kit/ui/global-loader';
|
||||
|
||||
export default GlobalLoader;
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { AccountSelector } from '@kit/accounts/account-selector';
|
||||
|
||||
import featureFlagsConfig from '~/config/feature-flags.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
import { AccountSelector } from '@kit/accounts/account-selector';
|
||||
|
||||
const features = {
|
||||
enableOrganizationAccounts: featureFlagsConfig.enableOrganizationAccounts,
|
||||
enableOrganizationCreation: featureFlagsConfig.enableOrganizationCreation,
|
||||
|
||||
@@ -2,13 +2,13 @@ import { use } from 'react';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar';
|
||||
|
||||
import { HomeSidebarAccountSelector } from '~/(dashboard)/home/components/home-sidebar-account-selector';
|
||||
import { ProfileDropdownContainer } from '~/(dashboard)/home/components/personal-account-dropdown';
|
||||
import { personalAccountSidebarConfig } from '~/config/personal-account-sidebar.config';
|
||||
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
import { Sidebar, SidebarContent, SidebarNavigation } from '@kit/ui/sidebar';
|
||||
|
||||
export function HomeSidebar() {
|
||||
const collapsed = getSidebarCollapsed();
|
||||
const accounts = use(loadUserAccounts());
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||
import { useUserSession } from '@kit/supabase/hooks/use-user-session';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
export function ProfileDropdownContainer(props: { collapsed: boolean }) {
|
||||
const userSession = useUserSession();
|
||||
const signOut = useSignOut();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { notFound } from 'next/navigation';
|
||||
import Script from 'next/script';
|
||||
|
||||
import { allPosts } from 'contentlayer/generated';
|
||||
|
||||
import appConfig from '~/config/app.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Post } from 'contentlayer/generated';
|
||||
import { CoverImage } from '~/(marketing)/blog/components/cover-image';
|
||||
import { DateFormatter } from '~/(marketing)/blog/components/date-formatter';
|
||||
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { If } from '@kit/ui/if';
|
||||
|
||||
import { CoverImage } from '~/(marketing)/blog/components/cover-image';
|
||||
import { DateFormatter } from '~/(marketing)/blog/components/date-formatter';
|
||||
|
||||
const PostHeader: React.FC<{
|
||||
post: Post;
|
||||
}> = ({ post }) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { Post } from 'contentlayer/generated';
|
||||
import { CoverImage } from '~/(marketing)/blog/components/cover-image';
|
||||
import { DateFormatter } from '~/(marketing)/blog/components/date-formatter';
|
||||
|
||||
import { If } from '@kit/ui/if';
|
||||
|
||||
import { CoverImage } from '~/(marketing)/blog/components/cover-image';
|
||||
import { DateFormatter } from '~/(marketing)/blog/components/date-formatter';
|
||||
|
||||
type Props = {
|
||||
post: Post;
|
||||
preloadImage?: boolean;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { allPosts } from 'contentlayer/generated';
|
||||
|
||||
import PostPreview from '~/(marketing)/blog/components/post-preview';
|
||||
import { SitePageHeader } from '~/(marketing)/components/site-page-header';
|
||||
import appConfig from '~/config/app.config';
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||
import { useUserSession } from '@kit/supabase/hooks/use-user-session';
|
||||
import { Button } from '@kit/ui/button';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
export function SiteHeaderAccountSection() {
|
||||
const signOut = useSignOut();
|
||||
const userSession = useUserSession();
|
||||
|
||||
@@ -4,15 +4,16 @@ import { notFound } from 'next/navigation';
|
||||
|
||||
import { allDocumentationPages } from 'contentlayer/generated';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
|
||||
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Mdx } from '@kit/ui/mdx';
|
||||
|
||||
import { SitePageHeader } from '~/(marketing)/components/site-page-header';
|
||||
import { DocsCards } from '~/(marketing)/docs/components/docs-cards';
|
||||
import { DocumentationPageLink } from '~/(marketing)/docs/components/documentation-page-link';
|
||||
import { getDocumentationPageTree } from '~/(marketing)/docs/utils/get-documentation-page-tree';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Mdx } from '@kit/ui/mdx';
|
||||
|
||||
const getPageBySlug = cache((slug: string) => {
|
||||
return allDocumentationPages.find((post) => post.resolvedPath === slug);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { allDocumentationPages } from 'contentlayer/generated';
|
||||
|
||||
import appConfig from '~/config/app.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { SitePageHeader } from '../components/site-page-header';
|
||||
|
||||
@@ -2,11 +2,12 @@ import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<div className={'flex flex-col space-y-16'}>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PricingTable } from '@kit/billing/components/pricing-table';
|
||||
|
||||
import billingConfig from '~/config/billing.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { PricingTable } from '@kit/billing/components/pricing-table';
|
||||
|
||||
import { SitePageHeader } from '../components/site-page-header';
|
||||
|
||||
export const metadata = {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import isUserSuperAdmin from '~/admin/utils/is-user-super-admin';
|
||||
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import isUserSuperAdmin from '~/admin/utils/is-user-super-admin';
|
||||
|
||||
export function withAdminSession<Args extends unknown[], Response>(
|
||||
fn: (...params: Args) => Response,
|
||||
) {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { withAdminSession } from '~/admin/lib/actions-utils';
|
||||
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import { withAdminSession } from '~/admin/lib/actions-utils';
|
||||
|
||||
const getClient = () => getSupabaseServerActionClient({ admin: true });
|
||||
|
||||
export const deleteOrganizationAction = withAdminSession(
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useState, useTransition } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import type Organization from '@/lib/organizations/types/organization';
|
||||
|
||||
import useCsrfToken from '@kit/hooks/use-csrf-token';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
@@ -15,8 +17,6 @@ import {
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
|
||||
import type Organization from '@/lib/organizations/types/organization';
|
||||
|
||||
import { deleteOrganizationAction } from '../actions.server';
|
||||
|
||||
function DeleteOrganizationModal({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
|
||||
|
||||
import { getOrganizationByUid } from '@/lib/organizations/database/queries';
|
||||
|
||||
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
|
||||
|
||||
import AdminGuard from '../../../../../../packages/admin/components/AdminGuard';
|
||||
import DeleteOrganizationModal from '../components/DeleteOrganizationModal';
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
import { DataTable } from '@/components/app/DataTable';
|
||||
import type Membership from '@/lib/organizations/types/membership';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { EllipsisVerticalIcon } from 'lucide-react';
|
||||
|
||||
@@ -15,10 +17,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
|
||||
import type Membership from '@/lib/organizations/types/membership';
|
||||
|
||||
import { DataTable } from '@/components/app/DataTable';
|
||||
|
||||
import RoleBadge from '../../../../../(app)/[account]/account/organization/components/RoleBadge';
|
||||
|
||||
type Data = {
|
||||
|
||||
@@ -2,15 +2,13 @@ import { use } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
import appConfig from '@/config/app.config';
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
|
||||
import AdminHeader from '@packages/admin/components/AdminHeader';
|
||||
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
|
||||
|
||||
import appConfig from '@/config/app.config';
|
||||
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
|
||||
import getPageFromQueryParams from '../../../utils/get-page-from-query-param';
|
||||
import { getMembershipsByOrganizationUid } from '../../queries';
|
||||
import OrganizationsMembersTable from './components/OrganizationsMembersTable';
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { EllipsisIcon } from 'lucide-react';
|
||||
import { getI18n } from 'react-i18next';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
|
||||
import pricingConfig from '@/config/pricing.config';
|
||||
|
||||
import { DataTable } from '@/components/app/DataTable';
|
||||
|
||||
import SubscriptionStatusBadge from '../../../(app)/[account]/components/organizations/SubscriptionStatusBadge';
|
||||
import type { getOrganizations } from '../queries';
|
||||
|
||||
type Response = Awaited<ReturnType<typeof getOrganizations>>;
|
||||
type Organizations = Response['organizations'];
|
||||
|
||||
const columns: ColumnDef<Organizations[0]>[] = [
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'id',
|
||||
id: 'id',
|
||||
size: 10,
|
||||
},
|
||||
{
|
||||
header: 'UUID',
|
||||
accessorKey: 'uuid',
|
||||
id: 'uuid',
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'name',
|
||||
id: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Subscription',
|
||||
id: 'subscription',
|
||||
cell: ({ row }) => {
|
||||
const priceId = row.original?.subscription?.data?.priceId;
|
||||
|
||||
const plan = pricingConfig.products.find((product) => {
|
||||
return product.plans.some((plan) => plan.stripePriceId === priceId);
|
||||
});
|
||||
|
||||
if (plan) {
|
||||
const price = plan.plans.find((plan) => plan.stripePriceId === priceId);
|
||||
|
||||
if (!price) {
|
||||
return 'Unknown Price';
|
||||
}
|
||||
|
||||
return `${plan.name} - ${price.name}`;
|
||||
}
|
||||
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Subscription Status',
|
||||
id: 'subscription-status',
|
||||
cell: ({ row }) => {
|
||||
const subscription = row.original?.subscription?.data;
|
||||
|
||||
if (!subscription) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return <SubscriptionStatusBadge subscription={subscription} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Subscription Period',
|
||||
id: 'subscription-period',
|
||||
cell: ({ row }) => {
|
||||
const subscription = row.original?.subscription?.data;
|
||||
const i18n = getI18n();
|
||||
const language = i18n.language ?? 'en';
|
||||
|
||||
if (!subscription) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const canceled = subscription.cancelAtPeriodEnd;
|
||||
const date = subscription.periodEndsAt;
|
||||
const formattedDate = new Date(date).toLocaleDateString(language);
|
||||
|
||||
return canceled ? (
|
||||
<span className={'text-orange-500'}>Stops on {formattedDate}</span>
|
||||
) : (
|
||||
<span className={'text-green-500'}>Renews on {formattedDate}</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Members',
|
||||
id: 'members',
|
||||
cell: ({ row }) => {
|
||||
const memberships = row.original.memberships.filter((item) => !item.code);
|
||||
const invites = row.original.memberships.length - memberships.length;
|
||||
const uid = row.original.uuid;
|
||||
const length = memberships.length;
|
||||
|
||||
return (
|
||||
<Link
|
||||
data-test={'organization-members-link'}
|
||||
href={`organizations/${uid}/members`}
|
||||
className={'cursor-pointer hover:underline'}
|
||||
>
|
||||
{length} member{length === 1 ? '' : 's'}{' '}
|
||||
{invites ? `(${invites} invites)` : ''}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
const organization = row.original;
|
||||
const uid = organization.uuid;
|
||||
|
||||
return (
|
||||
<div className={'flex justify-end'}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size={'icon'}>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<EllipsisIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigator.clipboard.writeText(uid)}
|
||||
>
|
||||
Copy UUID
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/admin/organizations/${uid}/members`}>
|
||||
View Members
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
className={'text-red-500'}
|
||||
href={`/admin/organizations/${uid}/delete`}
|
||||
>
|
||||
Delete
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function OrganizationsTable({
|
||||
organizations,
|
||||
pageCount,
|
||||
perPage,
|
||||
page,
|
||||
}: React.PropsWithChildren<{
|
||||
organizations: Organizations;
|
||||
pageCount: number;
|
||||
perPage: number;
|
||||
page: number;
|
||||
}>) {
|
||||
return (
|
||||
<DataTable
|
||||
tableProps={{
|
||||
'data-test': 'admin-organizations-table',
|
||||
}}
|
||||
pageSize={perPage}
|
||||
pageIndex={page - 1}
|
||||
pageCount={pageCount}
|
||||
columns={columns}
|
||||
data={organizations}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrganizationsTable;
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
|
||||
function OrganizationsAdminPageError() {
|
||||
return (
|
||||
<PageBody>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
import appConfig from '@/config/app.config';
|
||||
import AdminGuard from '@/packages/admin/components/AdminGuard';
|
||||
import AdminHeader from '@/packages/admin/components/AdminHeader';
|
||||
|
||||
@@ -5,10 +7,6 @@ import getSupabaseServerComponentClient from '@packages/supabase/server-componen
|
||||
|
||||
import { Input } from '@kit/ui/input';
|
||||
|
||||
import appConfig from '@/config/app.config';
|
||||
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
|
||||
import OrganizationsTable from './components/OrganizationsTable';
|
||||
import { getOrganizations } from './queries';
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import type { Database } from '@/database.types';
|
||||
|
||||
import { MEMBERSHIPS_TABLE, ORGANIZATIONS_TABLE } from '@/lib/db-tables';
|
||||
import type { UserOrganizationData } from '@/lib/organizations/database/queries';
|
||||
import type MembershipRole from '@/lib/organizations/types/membership-role';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
|
||||
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
import appConfig from '@/config/app.config';
|
||||
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
|
||||
|
||||
import AdminDashboard from '../../packages/admin/components/AdminDashboard';
|
||||
import AdminGuard from '../../packages/admin/components/AdminGuard';
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { withAdminSession } from '~/admin/lib/actions-utils';
|
||||
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import { withAdminSession } from '~/admin/lib/actions-utils';
|
||||
|
||||
const getClient = () => getSupabaseServerActionClient({ admin: true });
|
||||
|
||||
export const banUser = withAdminSession(async ({ userId }) => {
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { User } from '@supabase/gotrue-js';
|
||||
|
||||
import ErrorBoundary from '@/components/app/ErrorBoundary';
|
||||
|
||||
import useCsrfToken from '@kit/hooks/use-csrf-token';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -20,8 +22,6 @@ import {
|
||||
import { Input } from '@kit/ui/input';
|
||||
import { Label } from '@kit/ui/label';
|
||||
|
||||
import ErrorBoundary from '@/components/app/ErrorBoundary';
|
||||
|
||||
import { banUser } from '../actions.server';
|
||||
|
||||
function BanUserModal({
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useEffect } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import useSupabase from '@kit/hooks/use-supabase';
|
||||
|
||||
import Spinner from '@/components/app/Spinner';
|
||||
|
||||
import useSupabase from '@kit/hooks/use-supabase';
|
||||
|
||||
function ImpersonateUserAuthSetter({
|
||||
tokens,
|
||||
}: React.PropsWithChildren<{
|
||||
|
||||
@@ -6,6 +6,9 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { User } from '@supabase/gotrue-js';
|
||||
|
||||
import If from '@/components/app/If';
|
||||
import LoadingOverlay from '@/components/app/LoadingOverlay';
|
||||
|
||||
import useCsrfToken from '@kit/hooks/use-csrf-token';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -16,9 +19,6 @@ import {
|
||||
DialogTitle,
|
||||
} from '@kit/ui/dialog';
|
||||
|
||||
import If from '@/components/app/If';
|
||||
import LoadingOverlay from '@/components/app/LoadingOverlay';
|
||||
|
||||
import { impersonateUser } from '../actions.server';
|
||||
import ImpersonateUserAuthSetter from '../components/ImpersonateUserAuthSetter';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import If from '@/components/app/If';
|
||||
import { EllipsisVerticalIcon } from 'lucide-react';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -12,8 +13,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
|
||||
import If from '@/components/app/If';
|
||||
|
||||
function UserActionsDropdown({
|
||||
uid,
|
||||
isBanned,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
import configuration from '@/config/app.config';
|
||||
import type MembershipRole from '@/lib/organizations/types/membership-role';
|
||||
import { ChevronRightIcon } from 'lucide-react';
|
||||
|
||||
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
|
||||
@@ -17,12 +20,6 @@ import {
|
||||
TableRow,
|
||||
} from '@kit/ui/table';
|
||||
|
||||
import configuration from '@/config/app.config';
|
||||
|
||||
import type MembershipRole from '@/lib/organizations/types/membership-role';
|
||||
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
|
||||
import RoleBadge from '../../../(app)/[account]/account/organization/components/RoleBadge';
|
||||
import AdminGuard from '../../../../packages/admin/components/AdminGuard';
|
||||
import AdminHeader from '../../../../packages/admin/components/AdminHeader';
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { EllipsisIcon } from 'lucide-react';
|
||||
import { getI18n } from 'react-i18next';
|
||||
|
||||
import type UserData from '@kit/session/types/user-data';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@kit/ui/tooltip';
|
||||
|
||||
import { DataTable } from '@/components/app/DataTable';
|
||||
import If from '@/components/app/If';
|
||||
|
||||
type UserRow = {
|
||||
id: string;
|
||||
email: string | undefined;
|
||||
phone: string | undefined;
|
||||
createdAt: string;
|
||||
updatedAt: string | undefined;
|
||||
lastSignInAt: string | undefined;
|
||||
banDuration: string | undefined;
|
||||
data: UserData;
|
||||
};
|
||||
|
||||
const columns: ColumnDef<UserRow>[] = [
|
||||
{
|
||||
header: '',
|
||||
id: 'avatar',
|
||||
size: 10,
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
const data = user.data;
|
||||
const displayName = data?.displayName;
|
||||
const photoUrl = data?.photoUrl;
|
||||
const displayText = displayName ?? user.email ?? user.phone ?? '';
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Avatar>
|
||||
{photoUrl ? <AvatarImage src={photoUrl} /> : null}
|
||||
<AvatarFallback>{displayText[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>{displayText}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'ID',
|
||||
id: 'id',
|
||||
size: 30,
|
||||
cell: ({ row }) => {
|
||||
const id = row.original.id;
|
||||
|
||||
return (
|
||||
<Link className={'hover:underline'} href={`/admin/users/${id}`}>
|
||||
{id}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Email',
|
||||
id: 'email',
|
||||
cell: ({ row }) => {
|
||||
const email = row.original.email;
|
||||
|
||||
return (
|
||||
<span title={email} className={'block max-w-full truncate'}>
|
||||
{email}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Name',
|
||||
size: 50,
|
||||
id: 'displayName',
|
||||
cell: ({ row }) => {
|
||||
return row.original.data?.displayName ?? '';
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Created at',
|
||||
id: 'createdAt',
|
||||
cell: ({ row }) => {
|
||||
const date = new Date(row.original.createdAt);
|
||||
const i18n = getI18n();
|
||||
const language = i18n.language ?? 'en';
|
||||
const createdAtLabel = date.toLocaleDateString(language);
|
||||
|
||||
return <span>{createdAtLabel}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Last sign in',
|
||||
id: 'lastSignInAt',
|
||||
cell: ({ row }) => {
|
||||
const lastSignInAt = row.original.lastSignInAt;
|
||||
|
||||
if (!lastSignInAt) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
const date = new Date(lastSignInAt);
|
||||
return <span suppressHydrationWarning>{date.toLocaleString()}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
id: 'status',
|
||||
cell: ({ row }) => {
|
||||
const banDuration = row.original.banDuration;
|
||||
|
||||
if (!banDuration || banDuration === 'none') {
|
||||
return (
|
||||
<Badge className={'inline-flex'} color={'success'}>
|
||||
Active
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge className={'inline-flex'} color={'error'}>
|
||||
Banned
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
const user = row.original;
|
||||
const banDuration = row.original.banDuration;
|
||||
const isBanned = banDuration && banDuration !== 'none';
|
||||
|
||||
return (
|
||||
<div className={'flex justify-end'}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size={'icon'}>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<EllipsisIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigator.clipboard.writeText(user.id)}
|
||||
>
|
||||
Copy user ID
|
||||
</DropdownMenuItem>
|
||||
|
||||
<If condition={!isBanned}>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/admin/users/${user.id}/impersonate`}>
|
||||
Impersonate User
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
className={
|
||||
'text-orange-500 hover:bg-orange-50 dark:hover:bg-orange-500/5'
|
||||
}
|
||||
href={`/admin/users/${user.id}/ban`}
|
||||
>
|
||||
Ban User
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
className={
|
||||
'text-red-500 hover:bg-red-50 dark:hover:bg-red-500/5'
|
||||
}
|
||||
href={`/admin/users/${user.id}/delete`}
|
||||
>
|
||||
Delete User
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
|
||||
<If condition={isBanned}>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/admin/users/${user.id}/reactivate`}>
|
||||
Reactivate User
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</If>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function UsersTable({
|
||||
users,
|
||||
page,
|
||||
pageCount,
|
||||
perPage,
|
||||
}: React.PropsWithChildren<{
|
||||
users: UserRow[];
|
||||
pageCount: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
}>) {
|
||||
return (
|
||||
<DataTable
|
||||
tableProps={{
|
||||
'data-test': 'admin-users-table',
|
||||
}}
|
||||
pageIndex={page - 1}
|
||||
pageSize={perPage}
|
||||
pageCount={pageCount}
|
||||
data={users}
|
||||
columns={columns}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default UsersTable;
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
|
||||
function UsersAdminPageError() {
|
||||
return (
|
||||
<PageBody>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
import appConfig from '@/config/app.config';
|
||||
|
||||
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
|
||||
|
||||
import type UserData from '@kit/session/types/user-data';
|
||||
|
||||
import appConfig from '@/config/app.config';
|
||||
|
||||
import { PageBody } from '@/components/app/Page';
|
||||
|
||||
import AdminGuard from '../../../packages/admin/components/AdminGuard';
|
||||
import AdminHeader from '../../../packages/admin/components/AdminHeader';
|
||||
import getPageFromQueryParams from '../utils/get-page-from-query-param';
|
||||
import UsersTable from './components/UsersTable';
|
||||
import { getUsers } from './queries';
|
||||
|
||||
interface UsersAdminPageProps {
|
||||
@@ -32,14 +30,7 @@ async function UsersAdminPage({ searchParams }: UsersAdminPageProps) {
|
||||
<div className={'flex flex-1 flex-col'}>
|
||||
<AdminHeader>Users</AdminHeader>
|
||||
|
||||
<PageBody>
|
||||
<UsersTable
|
||||
users={users}
|
||||
page={page}
|
||||
pageCount={pageCount}
|
||||
perPage={perPage}
|
||||
/>
|
||||
</PageBody>
|
||||
<PageBody></PageBody>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
|
||||
|
||||
import { USERS_TABLE } from '@/lib/db-tables';
|
||||
|
||||
import getSupabaseServerComponentClient from '@packages/supabase/server-component-client';
|
||||
|
||||
export async function getUsers(ids: string[]) {
|
||||
const client = getSupabaseServerComponentClient({ admin: true });
|
||||
|
||||
|
||||
50
apps/web/app/api/billing/webhook/route.ts
Normal file
50
apps/web/app/api/billing/webhook/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { getBillingEventHandlerService } from '@kit/billing-gateway';
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-client';
|
||||
|
||||
import billingConfig from '~/config/billing.config';
|
||||
|
||||
/**
|
||||
* @description Handle the webhooks from Stripe related to checkouts
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const client = getSupabaseRouteHandlerClient();
|
||||
|
||||
// we can infer the provider from the billing config or the request
|
||||
// for simplicity, we'll use the billing config for now
|
||||
// TODO: use dynamic provider from request?
|
||||
const provider = billingConfig.provider;
|
||||
|
||||
Logger.info(
|
||||
{
|
||||
name: 'billing',
|
||||
provider,
|
||||
},
|
||||
`Received billing webhook. Processing...`,
|
||||
);
|
||||
|
||||
const service = await getBillingEventHandlerService(client, provider);
|
||||
|
||||
try {
|
||||
await service.handleWebhookEvent(request);
|
||||
|
||||
Logger.info(
|
||||
{
|
||||
name: 'billing',
|
||||
},
|
||||
`Successfully processed billing webhook`,
|
||||
);
|
||||
|
||||
return new Response('OK', { status: 200 });
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
{
|
||||
name: 'billing',
|
||||
error: e,
|
||||
},
|
||||
`Failed to process billing webhook`,
|
||||
);
|
||||
|
||||
return new Response('Error', { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { headers } from 'next/headers';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import type { Stripe } from 'stripe';
|
||||
|
||||
import getSupabaseRouteHandlerClient from '@packages/supabase/route-handler-client';
|
||||
|
||||
import { Logger } from '@kit/logger';
|
||||
import createStripeClient from '@kit/stripe/get-stripe';
|
||||
import StripeWebhooks from '@kit/stripe/stripe-webhooks.enum';
|
||||
|
||||
import { setOrganizationSubscriptionData } from '@/lib/organizations/database/mutations';
|
||||
import {
|
||||
addSubscription,
|
||||
deleteSubscription,
|
||||
updateSubscriptionById,
|
||||
} from '@/lib/subscriptions/mutations';
|
||||
|
||||
const STRIPE_SIGNATURE_HEADER = 'stripe-signature';
|
||||
|
||||
const webhookSecretKey = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||
|
||||
const logName = 'stripe-webhook';
|
||||
|
||||
/**
|
||||
* @description Handle the webhooks from Stripe related to checkouts
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const signature = headers().get(STRIPE_SIGNATURE_HEADER);
|
||||
|
||||
Logger.info(`[Stripe] Received Stripe Webhook`);
|
||||
|
||||
if (!webhookSecretKey) {
|
||||
Logger.error(
|
||||
{
|
||||
name: logName,
|
||||
},
|
||||
`The variable STRIPE_WEBHOOK_SECRET is unset. Please add the STRIPE_WEBHOOK_SECRET environment variable`,
|
||||
);
|
||||
|
||||
return new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
|
||||
// verify signature header is not missing
|
||||
if (!signature) {
|
||||
return new Response('Invalid signature', {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const rawBody = await request.text();
|
||||
const stripe = await createStripeClient();
|
||||
|
||||
// create an Admin client to write to the subscriptions table
|
||||
const client = getSupabaseRouteHandlerClient({
|
||||
admin: true,
|
||||
});
|
||||
|
||||
try {
|
||||
// build the event from the raw body and signature using Stripe
|
||||
const event = stripe.webhooks.constructEvent(
|
||||
rawBody,
|
||||
signature,
|
||||
webhookSecretKey,
|
||||
);
|
||||
|
||||
Logger.info(
|
||||
{
|
||||
name: logName,
|
||||
type: event.type,
|
||||
},
|
||||
`Processing Stripe Webhook...`,
|
||||
);
|
||||
|
||||
switch (event.type) {
|
||||
case StripeWebhooks.Completed: {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
const subscriptionId = session.subscription as string;
|
||||
|
||||
const subscription =
|
||||
await stripe.subscriptions.retrieve(subscriptionId);
|
||||
|
||||
await onCheckoutCompleted(client, session, subscription);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case StripeWebhooks.SubscriptionDeleted: {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await deleteSubscription(client, subscription.id);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case StripeWebhooks.SubscriptionUpdated: {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
|
||||
await updateSubscriptionById(client, subscription);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
{
|
||||
error,
|
||||
name: logName,
|
||||
},
|
||||
`Webhook handling failed`,
|
||||
);
|
||||
|
||||
return new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description When the checkout is completed, we store the order. The
|
||||
* subscription is only activated if the order was paid successfully.
|
||||
* Otherwise, we have to wait for a further webhook
|
||||
*/
|
||||
async function onCheckoutCompleted(
|
||||
client: SupabaseClient,
|
||||
session: Stripe.Checkout.Session,
|
||||
subscription: Stripe.Subscription,
|
||||
) {
|
||||
const organizationUid = getOrganizationUidFromClientReference(session);
|
||||
const customerId = session.customer as string;
|
||||
|
||||
// build organization subscription and set on the organization document
|
||||
// we add just enough data in the DB, so we do not query
|
||||
// Stripe for every bit of data
|
||||
// if you need your DB record to contain further data
|
||||
// add it to {@link buildOrganizationSubscription}
|
||||
const { error, data } = await addSubscription(client, subscription);
|
||||
|
||||
if (error) {
|
||||
return Promise.reject(
|
||||
`Failed to add subscription to the database: ${error}`,
|
||||
);
|
||||
}
|
||||
|
||||
return setOrganizationSubscriptionData(client, {
|
||||
organizationUid,
|
||||
customerId,
|
||||
subscriptionId: data.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name getOrganizationUidFromClientReference
|
||||
* @description Get the organization UUID from the client reference ID
|
||||
* @param session
|
||||
*/
|
||||
function getOrganizationUidFromClientReference(
|
||||
session: Stripe.Checkout.Session,
|
||||
) {
|
||||
return session.client_reference_id!;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
import { Logger } from '@kit/shared/logger';
|
||||
import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-client';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestUrl = new URL(request.url);
|
||||
const searchParams = requestUrl.searchParams;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
|
||||
function AuthLayout({ children }: React.PropsWithChildren) {
|
||||
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { PasswordResetRequestContainer } from '@kit/auth/password-reset';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import authConfig from '~/config/auth.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { SignInMethodsContainer } from '@kit/auth/sign-in';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import authConfig from '~/config/auth.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import authConfig from '~/config/auth.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { SignUpMethodsContainer } from '@kit/auth/sign-up';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import authConfig from '~/config/auth.config';
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { MultiFactorChallengeContainer } from '@kit/auth/mfa';
|
||||
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeftIcon } from 'lucide-react';
|
||||
import { SiteHeader } from '~/(marketing)/components/site-header';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { SiteHeader } from '~/(marketing)/components/site-header';
|
||||
|
||||
const ErrorPage = () => {
|
||||
return (
|
||||
<div className={'flex h-screen flex-1 flex-col'}>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import { useCallback, useState, useTransition } from 'react';
|
||||
|
||||
import authConfig from '~/config/auth.config';
|
||||
|
||||
import { EmailOtpContainer } from '@kit/auth/src/components/email-otp-container';
|
||||
import { OauthProviders } from '@kit/auth/src/components/oauth-providers';
|
||||
import { PasswordSignInContainer } from '@kit/auth/src/components/password-sign-in-container';
|
||||
@@ -14,6 +12,8 @@ import { If } from '@kit/ui/if';
|
||||
import { LoadingOverlay } from '@kit/ui/loading-overlay';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import authConfig from '~/config/auth.config';
|
||||
|
||||
enum Mode {
|
||||
SignUp,
|
||||
SignIn,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
|
||||
function InvitePageLayout({ children }: React.PropsWithChildren) {
|
||||
return <AuthLayoutShell Logo={AppLogo}>{children}</AuthLayoutShell>;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Inter as SansFont } from 'next/font/google';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
import { Toaster } from '@kit/ui/sonner';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { RootProviders } from '~/components/root-providers';
|
||||
import appConfig from '~/config/app.config';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
|
||||
import { Toaster } from '@kit/ui/sonner';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import '../styles/globals.css';
|
||||
|
||||
const sans = SansFont({
|
||||
@@ -51,7 +51,7 @@ function getClassName() {
|
||||
export const metadata = {
|
||||
title: appConfig.name,
|
||||
description: appConfig.description,
|
||||
metadataBase: new URL(appConfig.url!),
|
||||
metadataBase: new URL(appConfig.url),
|
||||
openGraph: {
|
||||
url: appConfig.url,
|
||||
siteName: appConfig.name,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ArrowLeftIcon } from '@radix-ui/react-icons';
|
||||
import { SiteHeader } from '~/(marketing)/components/site-header';
|
||||
import appConfig from '~/config/app.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { SiteHeader } from '~/(marketing)/components/site-header';
|
||||
import appConfig from '~/config/app.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
export const metadata = {
|
||||
title: `Page not found - ${appConfig.name}`,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import appConfig from '@/config/app.config';
|
||||
import { invariant } from '@epic-web/invariant';
|
||||
import { allDocumentationPages, allPosts } from 'contentlayer/generated';
|
||||
import { getServerSideSitemap } from 'next-sitemap';
|
||||
import { join } from 'path';
|
||||
|
||||
import appConfig from '@/config/app.config';
|
||||
|
||||
const siteUrl = appConfig.url;
|
||||
|
||||
export async function GET() {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { AuthLayoutShell } from '@kit/auth/shared';
|
||||
import PasswordResetForm from '@kit/auth/src/components/password-reset-form';
|
||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||
|
||||
import pathsConfig from '~/config/paths.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
async function PasswordResetPage() {
|
||||
const client = getSupabaseServerComponentClient();
|
||||
const user = await client.auth.getUser();
|
||||
|
||||
Reference in New Issue
Block a user