Updated account deletion process and refactor packages
The primary update was on the process of account deletion where email notifications are now sent to users. The @kit/emails was also renamed to @kit/email-templates and adjustments were accordingly made on the relevant code and configuration files. In addition, package interaction was refactored to enhance readability and ease of maintenance. Some minor alterations were made on the User Interface, and code comments were updated.
This commit is contained in:
@@ -81,7 +81,7 @@ Below are the reusable packages that can be shared across multiple applications
|
|||||||
- **`@kit/billing-gateway`**: Billing gateway package that defines the schema and logic for managing payment gateways
|
- **`@kit/billing-gateway`**: Billing gateway package that defines the schema and logic for managing payment gateways
|
||||||
- **`@kit/stripe`**: Stripe package that defines the schema and logic for managing Stripe. This is used by the `@kit/billing-gateway` package and abstracts the Stripe API.
|
- **`@kit/stripe`**: Stripe package that defines the schema and logic for managing Stripe. This is used by the `@kit/billing-gateway` package and abstracts the Stripe API.
|
||||||
- **`@kit/lemon-squeezy`**: Lemon Squeezy package that defines the schema and logic for managing Lemon Squeezy. This is used by the `@kit/billing-gateway` package and abstracts the Lemon Squeezy API.
|
- **`@kit/lemon-squeezy`**: Lemon Squeezy package that defines the schema and logic for managing Lemon Squeezy. This is used by the `@kit/billing-gateway` package and abstracts the Lemon Squeezy API.
|
||||||
- **`@kit/emails`**: Here we define the email templates using the `react.email` package.
|
- **`@kit/email-templates`**: Here we define the email templates using the `react.email` package.
|
||||||
- **`@kit/mailers`**: Mailer package that abstracts the email service provider (e.g., Resend, Cloudflare, SendGrid, Mailgun, etc.)
|
- **`@kit/mailers`**: Mailer package that abstracts the email service provider (e.g., Resend, Cloudflare, SendGrid, Mailgun, etc.)
|
||||||
|
|
||||||
And features that can be added to the application:
|
And features that can be added to the application:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const AppConfigSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const appConfig = AppConfigSchema.parse({
|
const appConfig = AppConfigSchema.parse({
|
||||||
name: 'Awesomely',
|
name: process.env.NEXT_PUBLIC_PRODUCT_NAME,
|
||||||
title: 'Awesomely - Your SaaS Title',
|
title: 'Awesomely - Your SaaS Title',
|
||||||
description: 'Your SaaS Description',
|
description: 'Your SaaS Description',
|
||||||
url: process.env.NEXT_PUBLIC_SITE_URL,
|
url: process.env.NEXT_PUBLIC_SITE_URL,
|
||||||
|
|||||||
@@ -128,9 +128,12 @@ function getPatterns() {
|
|||||||
handler: async (req: NextRequest, res: NextResponse) => {
|
handler: async (req: NextRequest, res: NextResponse) => {
|
||||||
const supabase = createMiddlewareClient(req, res);
|
const supabase = createMiddlewareClient(req, res);
|
||||||
const { data } = await supabase.auth.getSession();
|
const { data } = await supabase.auth.getSession();
|
||||||
|
|
||||||
|
// check if we need to verify MFA (user is authenticated but needs to verify MFA)
|
||||||
const isVerifyMfa = req.nextUrl.pathname === pathsConfig.auth.verifyMfa;
|
const isVerifyMfa = req.nextUrl.pathname === pathsConfig.auth.verifyMfa;
|
||||||
|
|
||||||
// If user is logged in, redirect to home page.
|
// If user is logged in and does not need to verify MFA,
|
||||||
|
// redirect to home page.
|
||||||
if (data.session && !isVerifyMfa) {
|
if (data.session && !isVerifyMfa) {
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL(pathsConfig.app.home, req.nextUrl.origin).href,
|
new URL(pathsConfig.app.home, req.nextUrl.origin).href,
|
||||||
@@ -157,10 +160,6 @@ function getPatterns() {
|
|||||||
const requiresMultiFactorAuthentication =
|
const requiresMultiFactorAuthentication =
|
||||||
await checkRequiresMultiFactorAuthentication(supabase);
|
await checkRequiresMultiFactorAuthentication(supabase);
|
||||||
|
|
||||||
console.log({
|
|
||||||
requiresMultiFactorAuthentication,
|
|
||||||
});
|
|
||||||
|
|
||||||
// If user requires multi-factor authentication, redirect to MFA page.
|
// If user requires multi-factor authentication, redirect to MFA page.
|
||||||
if (requiresMultiFactorAuthentication) {
|
if (requiresMultiFactorAuthentication) {
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const INTERNAL_PACKAGES = [
|
|||||||
'@kit/billing',
|
'@kit/billing',
|
||||||
'@kit/billing-gateway',
|
'@kit/billing-gateway',
|
||||||
'@kit/stripe',
|
'@kit/stripe',
|
||||||
|
'@kit/email-templates',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"@kit/auth": "^0.1.0",
|
"@kit/auth": "^0.1.0",
|
||||||
"@kit/billing": "^0.1.0",
|
"@kit/billing": "^0.1.0",
|
||||||
"@kit/billing-gateway": "^0.1.0",
|
"@kit/billing-gateway": "^0.1.0",
|
||||||
"@kit/emails": "^0.1.0",
|
"@kit/email-templates": "^0.1.0",
|
||||||
"@kit/i18n": "^0.1.0",
|
"@kit/i18n": "^0.1.0",
|
||||||
"@kit/mailers": "^0.1.0",
|
"@kit/mailers": "^0.1.0",
|
||||||
"@kit/shared": "^0.1.0",
|
"@kit/shared": "^0.1.0",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"@next/mdx": "^14.1.0",
|
"@next/mdx": "^14.1.0",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@supabase/ssr": "^0.1.0",
|
"@supabase/ssr": "^0.1.0",
|
||||||
"@supabase/supabase-js": "^2.39.8",
|
"@supabase/supabase-js": "^2.40.0",
|
||||||
"@tanstack/react-query": "5.28.6",
|
"@tanstack/react-query": "5.28.6",
|
||||||
"@tanstack/react-query-next-experimental": "^5.28.6",
|
"@tanstack/react-query-next-experimental": "^5.28.6",
|
||||||
"@tanstack/react-table": "^8.11.3",
|
"@tanstack/react-table": "^8.11.3",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.51.1",
|
"react-hook-form": "^7.51.2",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"recharts": "^2.10.3",
|
"recharts": "^2.10.3",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"currentPassword": "Current Password",
|
"currentPassword": "Current Password",
|
||||||
"newPassword": "New Password",
|
"newPassword": "New Password",
|
||||||
"repeatPassword": "Repeat New Password",
|
"repeatPassword": "Repeat New Password",
|
||||||
|
"repeatPasswordDescription": "Please repeat your new password to confirm it",
|
||||||
"yourPassword": "Your Password",
|
"yourPassword": "Your Password",
|
||||||
"updatePasswordSubmitLabel": "Update Password",
|
"updatePasswordSubmitLabel": "Update Password",
|
||||||
"updateEmailCardTitle": "Update your Email",
|
"updateEmailCardTitle": "Update your Email",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"@kit/stripe": "0.1.0",
|
"@kit/stripe": "0.1.0",
|
||||||
"@kit/supabase": "^0.1.0",
|
"@kit/supabase": "^0.1.0",
|
||||||
"@kit/ui": "0.1.0",
|
"@kit/ui": "0.1.0",
|
||||||
"@supabase/supabase-js": "^2.39.8",
|
"@supabase/supabase-js": "^2.40.0",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "0.1.0",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "0.1.0",
|
||||||
"@kit/ui": "*",
|
"@kit/ui": "*",
|
||||||
"@supabase/supabase-js": "^2.39.8",
|
"@supabase/supabase-js": "^2.40.0",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Emails - @kit/emails
|
# Emails - @kit/email-templates
|
||||||
|
|
||||||
This package is responsible for managing email templates using the react.email library.
|
This package is responsible for managing email templates using the react.email library.
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@kit/emails",
|
"name": "@kit/email-templates",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/billing-gateway": "*",
|
"@kit/billing-gateway": "*",
|
||||||
|
"@kit/email-templates": "*",
|
||||||
|
"@kit/mailers": "*",
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "0.2.0",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "0.1.0",
|
||||||
"@kit/shared": "*",
|
"@kit/shared": "*",
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "0.1.0",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "0.1.0",
|
||||||
"@kit/ui": "*",
|
"@kit/ui": "*",
|
||||||
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"react-hook-form": "^7.51.2",
|
"react-hook-form": "^7.51.2",
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useFormStatus } from 'react-dom';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
AlertDialog,
|
||||||
DialogContent,
|
AlertDialogCancel,
|
||||||
DialogHeader,
|
AlertDialogContent,
|
||||||
DialogTitle,
|
AlertDialogFooter,
|
||||||
DialogTrigger,
|
AlertDialogHeader,
|
||||||
} from '@kit/ui/dialog';
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@kit/ui/alert-dialog';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
import { ErrorBoundary } from '@kit/ui/error-boundary';
|
import { ErrorBoundary } from '@kit/ui/error-boundary';
|
||||||
import { Form, FormControl, FormItem, FormLabel } from '@kit/ui/form';
|
import { Form, FormControl, FormItem, FormLabel } from '@kit/ui/form';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
@@ -20,10 +26,6 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { deletePersonalAccountAction } from '../../server/personal-accounts-server-actions';
|
import { deletePersonalAccountAction } from '../../server/personal-accounts-server-actions';
|
||||||
|
|
||||||
export function AccountDangerZone() {
|
export function AccountDangerZone() {
|
||||||
return <DeleteAccountContainer />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeleteAccountContainer() {
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<div className={'flex flex-col space-y-1'}>
|
<div className={'flex flex-col space-y-1'}>
|
||||||
@@ -45,30 +47,39 @@ function DeleteAccountContainer() {
|
|||||||
|
|
||||||
function DeleteAccountModal() {
|
function DeleteAccountModal() {
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<AlertDialog>
|
||||||
<DialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button data-test={'delete-account-button'} variant={'destructive'}>
|
<Button data-test={'delete-account-button'} variant={'destructive'}>
|
||||||
<Trans i18nKey={'account:deleteAccount'} />
|
<Trans i18nKey={'account:deleteAccount'} />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</AlertDialogTrigger>
|
||||||
|
|
||||||
<DialogContent>
|
<AlertDialogContent onEscapeKeyDown={(e) => e.preventDefault()}>
|
||||||
<DialogHeader>
|
<AlertDialogHeader>
|
||||||
<DialogTitle>
|
<AlertDialogTitle>
|
||||||
<Trans i18nKey={'account:deleteAccount'} />
|
<Trans i18nKey={'account:deleteAccount'} />
|
||||||
</DialogTitle>
|
</AlertDialogTitle>
|
||||||
</DialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<ErrorBoundary fallback={<DeleteAccountErrorAlert />}>
|
<ErrorBoundary fallback={<DeleteAccountErrorAlert />}>
|
||||||
<DeleteAccountForm />
|
<DeleteAccountForm />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</DialogContent>
|
</AlertDialogContent>
|
||||||
</Dialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeleteAccountForm() {
|
function DeleteAccountForm() {
|
||||||
const form = useForm();
|
const form = useForm({
|
||||||
|
resolver: zodResolver(
|
||||||
|
z.object({
|
||||||
|
confirmation: z.string().refine((value) => value === 'DELETE'),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
defaultValues: {
|
||||||
|
confirmation: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -110,17 +121,25 @@ function DeleteAccountForm() {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex justify-end space-x-2.5'}>
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
<Trans i18nKey={'common:cancel'} />
|
||||||
|
</AlertDialogCancel>
|
||||||
|
|
||||||
<DeleteAccountSubmitButton />
|
<DeleteAccountSubmitButton />
|
||||||
</div>
|
</AlertDialogFooter>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeleteAccountSubmitButton() {
|
function DeleteAccountSubmitButton() {
|
||||||
|
const { pending } = useFormStatus();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
type={'submit'}
|
||||||
|
disabled={pending}
|
||||||
data-test={'confirm-delete-account-button'}
|
data-test={'confirm-delete-account-button'}
|
||||||
name={'action'}
|
name={'action'}
|
||||||
value={'delete'}
|
value={'delete'}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Factor } from '@supabase/gotrue-js';
|
|||||||
|
|
||||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { X } from 'lucide-react';
|
import { ShieldCheck, X } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
|||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
@@ -84,6 +85,8 @@ export function MultiFactorAuthFactorsList() {
|
|||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<Alert variant={'info'}>
|
<Alert variant={'info'}>
|
||||||
|
<ShieldCheck className={'h-4'} />
|
||||||
|
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'account:multiFactorAuthHeading'} />
|
<Trans i18nKey={'account:multiFactorAuthHeading'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
@@ -171,6 +174,10 @@ function ConfirmUnenrollFactorModal(
|
|||||||
>
|
>
|
||||||
<Trans i18nKey={'account:unenrollFactorModalButtonLabel'} />
|
<Trans i18nKey={'account:unenrollFactorModalButtonLabel'} />
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
|
|
||||||
|
<AlertDialogCancel>
|
||||||
|
<Trans i18nKey={'common:cancel'} />
|
||||||
|
</AlertDialogCancel>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { useState } from 'react';
|
|||||||
import type { User } from '@supabase/gotrue-js';
|
import type { User } from '@supabase/gotrue-js';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -15,6 +17,7 @@ import { Button } from '@kit/ui/button';
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -38,16 +41,6 @@ export const UpdatePasswordForm = ({
|
|||||||
const updateUserMutation = useUpdateUser();
|
const updateUserMutation = useUpdateUser();
|
||||||
const [needsReauthentication, setNeedsReauthentication] = useState(false);
|
const [needsReauthentication, setNeedsReauthentication] = useState(false);
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
resolver: zodResolver(
|
|
||||||
PasswordUpdateSchema.withTranslation(t('passwordNotMatching')),
|
|
||||||
),
|
|
||||||
defaultValues: {
|
|
||||||
newPassword: '',
|
|
||||||
repeatPassword: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatePasswordFromCredential = (password: string) => {
|
const updatePasswordFromCredential = (password: string) => {
|
||||||
const redirectTo = [window.location.origin, callbackPath].join('');
|
const redirectTo = [window.location.origin, callbackPath].join('');
|
||||||
|
|
||||||
@@ -87,6 +80,16 @@ export const UpdatePasswordForm = ({
|
|||||||
updatePasswordFromCredential(newPassword);
|
updatePasswordFromCredential(newPassword);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(
|
||||||
|
PasswordUpdateSchema.withTranslation(t('passwordNotMatching')),
|
||||||
|
),
|
||||||
|
defaultValues: {
|
||||||
|
newPassword: '',
|
||||||
|
repeatPassword: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -95,27 +98,11 @@ export const UpdatePasswordForm = ({
|
|||||||
>
|
>
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<If condition={updateUserMutation.data}>
|
<If condition={updateUserMutation.data}>
|
||||||
<Alert variant={'success'}>
|
<SuccessAlert />
|
||||||
<AlertTitle>
|
|
||||||
<Trans i18nKey={'account:updatePasswordSuccess'} />
|
|
||||||
</AlertTitle>
|
|
||||||
|
|
||||||
<AlertDescription>
|
|
||||||
<Trans i18nKey={'account:updatePasswordSuccessMessage'} />
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={needsReauthentication}>
|
<If condition={needsReauthentication}>
|
||||||
<Alert variant={'warning'}>
|
<NeedsReauthenticationAlert />
|
||||||
<AlertTitle>
|
|
||||||
<Trans i18nKey={'account:needsReauthentication'} />
|
|
||||||
</AlertTitle>
|
|
||||||
|
|
||||||
<AlertDescription>
|
|
||||||
<Trans i18nKey={'account:needsReauthenticationDescription'} />
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
@@ -164,6 +151,10 @@ export const UpdatePasswordForm = ({
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
<FormDescription>
|
||||||
|
<Trans i18nKey={'account:repeatPasswordDescription'} />
|
||||||
|
</FormDescription>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
@@ -180,3 +171,35 @@ export const UpdatePasswordForm = ({
|
|||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function SuccessAlert() {
|
||||||
|
return (
|
||||||
|
<Alert variant={'success'}>
|
||||||
|
<Check className={'h-4'} />
|
||||||
|
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans i18nKey={'account:updatePasswordSuccess'} />
|
||||||
|
</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans i18nKey={'account:updatePasswordSuccessMessage'} />
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NeedsReauthenticationAlert() {
|
||||||
|
return (
|
||||||
|
<Alert variant={'warning'}>
|
||||||
|
<ExclamationTriangleIcon className={'h-4'} />
|
||||||
|
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans i18nKey={'account:needsReauthentication'} />
|
||||||
|
</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans i18nKey={'account:needsReauthenticationDescription'} />
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export function UpdateAccountDetailsForm({
|
|||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
name={'displayName'}
|
name={'displayName'}
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
import { RedirectType, redirect } from 'next/navigation';
|
import { RedirectType, redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Logger } from '@kit/shared/logger';
|
import { Logger } from '@kit/shared/logger';
|
||||||
import { requireAuth } from '@kit/supabase/require-auth';
|
import { requireAuth } from '@kit/supabase/require-auth';
|
||||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||||
|
|
||||||
import { PersonalAccountsService } from './services/personal-accounts.service';
|
import { PersonalAccountsService } from './services/personal-accounts.service';
|
||||||
|
|
||||||
|
const emailSettings = getEmailSettingsFromEnvironment();
|
||||||
|
|
||||||
export async function refreshAuthSession() {
|
export async function refreshAuthSession() {
|
||||||
const client = getSupabaseServerActionClient();
|
const client = getSupabaseServerActionClient();
|
||||||
|
|
||||||
@@ -23,40 +27,45 @@ export async function deletePersonalAccountAction(formData: FormData) {
|
|||||||
throw new Error('Confirmation required to delete account');
|
throw new Error('Confirmation required to delete account');
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await requireAuth(getSupabaseServerActionClient());
|
const client = getSupabaseServerActionClient();
|
||||||
|
const session = await requireAuth(client);
|
||||||
|
|
||||||
if (session.error) {
|
if (session.error) {
|
||||||
|
Logger.error(`User is not authenticated. Redirecting to login page`);
|
||||||
|
|
||||||
redirect(session.redirectTo);
|
redirect(session.redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = getSupabaseServerActionClient();
|
// retrieve user ID and email
|
||||||
const service = new PersonalAccountsService(client);
|
|
||||||
const userId = session.data.user.id;
|
const userId = session.data.user.id;
|
||||||
|
const userEmail = session.data.user.email ?? null;
|
||||||
|
|
||||||
Logger.info(
|
// create a new instance of the personal accounts service
|
||||||
{
|
const service = new PersonalAccountsService(client);
|
||||||
|
|
||||||
|
// delete the user's account and cancel all subscriptions
|
||||||
|
await service.deletePersonalAccount({
|
||||||
|
adminClient: getSupabaseServerActionClient({ admin: true }),
|
||||||
userId,
|
userId,
|
||||||
name: 'accounts',
|
userEmail,
|
||||||
},
|
emailSettings,
|
||||||
`Deleting personal account...`,
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await service.deletePersonalAccount(
|
|
||||||
getSupabaseServerActionClient({ admin: true }),
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Logger.info(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
name: 'accounts',
|
|
||||||
},
|
|
||||||
`Personal account deleted successfully.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// sign out the user after deleting their account
|
||||||
await client.auth.signOut();
|
await client.auth.signOut();
|
||||||
|
|
||||||
|
// redirect to the home page
|
||||||
redirect('/', RedirectType.replace);
|
redirect('/', RedirectType.replace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEmailSettingsFromEnvironment() {
|
||||||
|
return z
|
||||||
|
.object({
|
||||||
|
fromEmail: z.string().email(),
|
||||||
|
productName: z.string().min(1),
|
||||||
|
})
|
||||||
|
.parse({
|
||||||
|
fromEmail: process.env.EMAIL_SENDER,
|
||||||
|
productName: process.env.NEXT_PUBLIC_PRODUCT_NAME,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
import { BillingGatewayService } from '@kit/billing-gateway';
|
import { BillingGatewayService } from '@kit/billing-gateway';
|
||||||
|
import { Mailer } from '@kit/mailers';
|
||||||
import { Logger } from '@kit/shared/logger';
|
import { Logger } from '@kit/shared/logger';
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
@@ -22,17 +23,25 @@ export class PersonalAccountsService {
|
|||||||
* Delete personal account of a user.
|
* Delete personal account of a user.
|
||||||
* This will delete the user from the authentication provider and cancel all subscriptions.
|
* This will delete the user from the authentication provider and cancel all subscriptions.
|
||||||
*/
|
*/
|
||||||
async deletePersonalAccount(
|
async deletePersonalAccount(params: {
|
||||||
adminClient: SupabaseClient<Database>,
|
adminClient: SupabaseClient<Database>;
|
||||||
params: { userId: string },
|
|
||||||
) {
|
userId: string;
|
||||||
|
userEmail: string | null;
|
||||||
|
|
||||||
|
emailSettings: {
|
||||||
|
fromEmail: string;
|
||||||
|
productName: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
{ userId: params.userId, name: this.namespace },
|
{ userId: params.userId, name: this.namespace },
|
||||||
'User requested deletion. Processing...',
|
'User requested deletion. Processing...',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// execute the deletion of the user
|
||||||
try {
|
try {
|
||||||
await adminClient.auth.admin.deleteUser(params.userId);
|
await params.adminClient.auth.admin.deleteUser(params.userId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(
|
Logger.error(
|
||||||
{
|
{
|
||||||
@@ -46,6 +55,7 @@ export class PersonalAccountsService {
|
|||||||
throw new Error('Error deleting user');
|
throw new Error('Error deleting user');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel all user subscriptions
|
||||||
try {
|
try {
|
||||||
await this.cancelAllUserSubscriptions(params.userId);
|
await this.cancelAllUserSubscriptions(params.userId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -55,6 +65,57 @@ export class PersonalAccountsService {
|
|||||||
name: this.namespace,
|
name: this.namespace,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send account deletion email
|
||||||
|
if (params.userEmail) {
|
||||||
|
try {
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
userId: params.userId,
|
||||||
|
name: this.namespace,
|
||||||
|
},
|
||||||
|
`Sending account deletion email...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.sendAccountDeletionEmail({
|
||||||
|
fromEmail: params.emailSettings.fromEmail,
|
||||||
|
productName: params.emailSettings.productName,
|
||||||
|
userDisplayName: params.userEmail,
|
||||||
|
userEmail: params.userEmail,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
{
|
||||||
|
userId: params.userId,
|
||||||
|
name: this.namespace,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
`Error sending account deletion email`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendAccountDeletionEmail(params: {
|
||||||
|
fromEmail: string;
|
||||||
|
userEmail: string;
|
||||||
|
userDisplayName: string;
|
||||||
|
productName: string;
|
||||||
|
}) {
|
||||||
|
const { renderAccountDeleteEmail } = await import('@kit/email-templates');
|
||||||
|
const mailer = new Mailer();
|
||||||
|
|
||||||
|
const html = await renderAccountDeleteEmail({
|
||||||
|
userDisplayName: params.userDisplayName,
|
||||||
|
productName: params.productName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await mailer.sendEmail({
|
||||||
|
to: params.userEmail,
|
||||||
|
from: params.fromEmail,
|
||||||
|
subject: 'Account Deletion Request',
|
||||||
|
html,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async cancelAllUserSubscriptions(userId: string) {
|
private async cancelAllUserSubscriptions(userId: string) {
|
||||||
@@ -93,6 +154,7 @@ export class PersonalAccountsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// execute all cancellation requests
|
||||||
await Promise.all(cancellationRequests);
|
await Promise.all(cancellationRequests);
|
||||||
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { useSignInWithOtp } from '@kit/supabase/hooks/use-sign-in-with-otp';
|
|
||||||
import { useVerifyOtp } from '@kit/supabase/hooks/use-verify-otp';
|
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import { If } from '@kit/ui/if';
|
|
||||||
import { Input } from '@kit/ui/input';
|
|
||||||
import { Label } from '@kit/ui/label';
|
|
||||||
import { OtpInput } from '@kit/ui/otp-input';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
|
|
||||||
export function EmailOtpContainer({
|
|
||||||
shouldCreateUser,
|
|
||||||
onSignIn,
|
|
||||||
inviteCode,
|
|
||||||
redirectUrl,
|
|
||||||
}: React.PropsWithChildren<{
|
|
||||||
inviteCode?: string;
|
|
||||||
redirectUrl: string;
|
|
||||||
shouldCreateUser: boolean;
|
|
||||||
onSignIn?: () => void;
|
|
||||||
}>) {
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
|
|
||||||
if (email) {
|
|
||||||
return (
|
|
||||||
<VerifyOtpForm
|
|
||||||
redirectUrl={redirectUrl}
|
|
||||||
inviteCode={inviteCode}
|
|
||||||
onSuccess={onSignIn}
|
|
||||||
email={email}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EmailOtpForm onSuccess={setEmail} shouldCreateUser={shouldCreateUser} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function VerifyOtpForm({
|
|
||||||
email,
|
|
||||||
inviteCode,
|
|
||||||
onSuccess,
|
|
||||||
redirectUrl,
|
|
||||||
}: {
|
|
||||||
email: string;
|
|
||||||
redirectUrl: string;
|
|
||||||
onSuccess?: () => void;
|
|
||||||
inviteCode?: string;
|
|
||||||
}) {
|
|
||||||
const verifyOtpMutation = useVerifyOtp();
|
|
||||||
const [verifyCode, setVerifyCode] = useState('');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className={'w-full'}
|
|
||||||
onSubmit={async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const queryParams = inviteCode ? `?inviteCode=${inviteCode}` : '';
|
|
||||||
const redirectTo = [redirectUrl, queryParams].join('');
|
|
||||||
|
|
||||||
await verifyOtpMutation.mutateAsync({
|
|
||||||
email,
|
|
||||||
token: verifyCode,
|
|
||||||
type: 'email',
|
|
||||||
options: {
|
|
||||||
redirectTo,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
onSuccess && onSuccess();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={'flex flex-col space-y-4'}>
|
|
||||||
<OtpInput onValid={setVerifyCode} onInvalid={() => setVerifyCode('')} />
|
|
||||||
|
|
||||||
<Button disabled={verifyOtpMutation.isPending || !verifyCode}>
|
|
||||||
{verifyOtpMutation.isPending ? (
|
|
||||||
<Trans i18nKey={'account:verifyingCode'} />
|
|
||||||
) : (
|
|
||||||
<Trans i18nKey={'account:submitVerificationCode'} />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmailOtpForm({
|
|
||||||
shouldCreateUser,
|
|
||||||
onSuccess,
|
|
||||||
}: React.PropsWithChildren<{
|
|
||||||
shouldCreateUser: boolean;
|
|
||||||
onSuccess: (email: string) => void;
|
|
||||||
}>) {
|
|
||||||
const signInWithOtpMutation = useSignInWithOtp();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className={'w-full'}
|
|
||||||
onSubmit={async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const email = event.currentTarget.email.value;
|
|
||||||
|
|
||||||
await signInWithOtpMutation.mutateAsync({
|
|
||||||
email,
|
|
||||||
options: {
|
|
||||||
shouldCreateUser,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
onSuccess(email);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={'flex flex-col space-y-4'}>
|
|
||||||
<Label>
|
|
||||||
<Trans i18nKey={'auth:emailAddress'} />
|
|
||||||
<Input name={'email'} type={'email'} placeholder={''} />
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Button disabled={signInWithOtpMutation.isPending}>
|
|
||||||
<If
|
|
||||||
condition={signInWithOtpMutation.isPending}
|
|
||||||
fallback={<Trans i18nKey={'auth:sendEmailCode'} />}
|
|
||||||
>
|
|
||||||
<Trans i18nKey={'auth:sendingEmailCode'} />
|
|
||||||
</If>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/accounts": "*",
|
"@kit/accounts": "*",
|
||||||
"@kit/emails": "*",
|
"@kit/email-templates": "*",
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "0.2.0",
|
||||||
"@kit/mailers": "*",
|
"@kit/mailers": "*",
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "0.1.0",
|
||||||
@@ -22,12 +22,12 @@
|
|||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "0.1.0",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "0.1.0",
|
||||||
"@kit/ui": "*",
|
"@kit/ui": "*",
|
||||||
"@hookform/resolvers/zod": "1.0.0",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"lucide-react": "^0.363.0"
|
"lucide-react": "^0.363.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@kit/accounts": "0.1.0",
|
"@kit/accounts": "0.1.0",
|
||||||
"@kit/emails": "0.1.0",
|
"@kit/email-templates": "0.1.0",
|
||||||
"@kit/mailers": "0.1.0",
|
"@kit/mailers": "0.1.0",
|
||||||
"@kit/shared": "0.1.0",
|
"@kit/shared": "0.1.0",
|
||||||
"@kit/supabase": "0.1.0",
|
"@kit/supabase": "0.1.0",
|
||||||
|
|||||||
@@ -149,7 +149,9 @@ export class AccountInvitationsService {
|
|||||||
for (const invitation of responseInvitations) {
|
for (const invitation of responseInvitations) {
|
||||||
const promise = async () => {
|
const promise = async () => {
|
||||||
try {
|
try {
|
||||||
const { renderInviteEmail } = await import('@kit/emails');
|
const { renderInviteEmail } = await import(
|
||||||
|
'../../../../email-templates'
|
||||||
|
);
|
||||||
|
|
||||||
const html = renderInviteEmail({
|
const html = renderInviteEmail({
|
||||||
link: this.getInvitationLink(invitation.invite_token),
|
link: this.getInvitationLink(invitation.invite_token),
|
||||||
|
|||||||
@@ -29,13 +29,13 @@
|
|||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "0.1.0",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "0.1.0",
|
||||||
"@supabase/ssr": "^0.1.0",
|
"@supabase/ssr": "^0.1.0",
|
||||||
"@supabase/supabase-js": "^2.39.8",
|
"@supabase/supabase-js": "^2.40.0",
|
||||||
"@tanstack/react-query": "5.28.6"
|
"@tanstack/react-query": "5.28.6"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@epic-web/invariant": "^1.0.0",
|
"@epic-web/invariant": "^1.0.0",
|
||||||
"@supabase/ssr": "^0.1.0",
|
"@supabase/ssr": "^0.1.0",
|
||||||
"@supabase/supabase-js": "^2.39.8",
|
"@supabase/supabase-js": "^2.40.0",
|
||||||
"@tanstack/react-query": "^5.28.6"
|
"@tanstack/react-query": "^5.28.6"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@radix-ui/react-accordion": "1.1.2",
|
"@radix-ui/react-accordion": "1.1.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"date-fns": "^3.2.0",
|
"date-fns": "^3.2.0",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"react-hook-form": "^7.49.2",
|
"react-hook-form": "^7.51.2",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"sonner": "^1.4.41",
|
"sonner": "^1.4.41",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
@@ -58,7 +59,7 @@
|
|||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
"react-day-picker": "^8.10.0",
|
"react-day-picker": "^8.10.0",
|
||||||
"react-hook-form": "^7.51.1",
|
"react-hook-form": "^7.51.2",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"sonner": "^1.4.41",
|
"sonner": "^1.4.41",
|
||||||
"tailwindcss": "3.4.1",
|
"tailwindcss": "3.4.1",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { MDXComponents } from 'mdx/types';
|
|||||||
import { getMDXComponent } from 'next-contentlayer/hooks';
|
import { getMDXComponent } from 'next-contentlayer/hooks';
|
||||||
|
|
||||||
import Components from './mdx-components';
|
import Components from './mdx-components';
|
||||||
// @ts-expect-error
|
// @ts-ignore
|
||||||
import styles from './mdx-renderer.module.css';
|
import styles from './mdx-renderer.module.css';
|
||||||
|
|
||||||
export function Mdx({
|
export function Mdx({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Slot } from '@radix-ui/react-slot';
|
|||||||
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
|
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
|
||||||
import { Controller, FormProvider, useFormContext } from 'react-hook-form';
|
import { Controller, FormProvider, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { cn } from '../utils/cn';
|
import { cn } from '../utils';
|
||||||
import { Label } from './label';
|
import { Label } from './label';
|
||||||
|
|
||||||
const Form = FormProvider;
|
const Form = FormProvider;
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ const InputOTPSlot = React.forwardRef<
|
|||||||
React.ComponentPropsWithoutRef<'div'> & { index: number }
|
React.ComponentPropsWithoutRef<'div'> & { index: number }
|
||||||
>(({ index, className, ...props }, ref) => {
|
>(({ index, className, ...props }, ref) => {
|
||||||
const inputOTPContext = React.useContext(OTPInputContext);
|
const inputOTPContext = React.useContext(OTPInputContext);
|
||||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
const slot = inputOTPContext.slots[index];
|
||||||
|
|
||||||
|
if (!slot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { char, isActive, hasFakeCaret } = slot;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -34,7 +34,7 @@ importers:
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^3.3.4
|
specifier: ^3.3.4
|
||||||
version: 3.3.4(react-hook-form@7.51.1)
|
version: 3.3.4(react-hook-form@7.51.2)
|
||||||
'@kit/accounts':
|
'@kit/accounts':
|
||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: link:../../packages/features/accounts
|
version: link:../../packages/features/accounts
|
||||||
@@ -50,9 +50,9 @@ importers:
|
|||||||
'@kit/billing-gateway':
|
'@kit/billing-gateway':
|
||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: link:../../packages/billing-gateway
|
version: link:../../packages/billing-gateway
|
||||||
'@kit/emails':
|
'@kit/email-templates':
|
||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: link:../../packages/emails
|
version: link:../../packages/email-templates
|
||||||
'@kit/i18n':
|
'@kit/i18n':
|
||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: link:../../packages/i18n
|
version: link:../../packages/i18n
|
||||||
@@ -81,7 +81,7 @@ importers:
|
|||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: 0.1.0(@supabase/supabase-js@2.40.0)
|
version: 0.1.0(@supabase/supabase-js@2.40.0)
|
||||||
'@supabase/supabase-js':
|
'@supabase/supabase-js':
|
||||||
specifier: ^2.39.8
|
specifier: ^2.40.0
|
||||||
version: 2.40.0
|
version: 2.40.0
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: 5.28.6
|
specifier: 5.28.6
|
||||||
@@ -126,8 +126,8 @@ importers:
|
|||||||
specifier: 18.2.0
|
specifier: 18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.51.1
|
specifier: ^7.51.2
|
||||||
version: 7.51.1(react@18.2.0)
|
version: 7.51.2(react@18.2.0)
|
||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0)
|
version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -253,7 +253,7 @@ importers:
|
|||||||
specifier: '*'
|
specifier: '*'
|
||||||
version: link:../ui
|
version: link:../ui
|
||||||
'@supabase/supabase-js':
|
'@supabase/supabase-js':
|
||||||
specifier: ^2.39.8
|
specifier: ^2.40.0
|
||||||
version: 2.40.0
|
version: 2.40.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.363.0
|
specifier: ^0.363.0
|
||||||
@@ -262,7 +262,7 @@ importers:
|
|||||||
specifier: ^3.22.4
|
specifier: ^3.22.4
|
||||||
version: 3.22.4
|
version: 3.22.4
|
||||||
|
|
||||||
packages/emails:
|
packages/email-templates:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@react-email/components':
|
'@react-email/components':
|
||||||
specifier: 0.0.15
|
specifier: 0.0.15
|
||||||
@@ -283,12 +283,21 @@ importers:
|
|||||||
|
|
||||||
packages/features/accounts:
|
packages/features/accounts:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@hookform/resolvers':
|
||||||
|
specifier: ^3.3.4
|
||||||
|
version: 3.3.4(react-hook-form@7.51.2)
|
||||||
'@kit/billing-gateway':
|
'@kit/billing-gateway':
|
||||||
specifier: '*'
|
specifier: '*'
|
||||||
version: link:../../billing-gateway
|
version: link:../../billing-gateway
|
||||||
|
'@kit/email-templates':
|
||||||
|
specifier: '*'
|
||||||
|
version: link:../../email-templates
|
||||||
'@kit/eslint-config':
|
'@kit/eslint-config':
|
||||||
specifier: 0.2.0
|
specifier: 0.2.0
|
||||||
version: link:../../../tooling/eslint
|
version: link:../../../tooling/eslint
|
||||||
|
'@kit/mailers':
|
||||||
|
specifier: '*'
|
||||||
|
version: link:../../mailers
|
||||||
'@kit/prettier-config':
|
'@kit/prettier-config':
|
||||||
specifier: 0.1.0
|
specifier: 0.1.0
|
||||||
version: link:../../../tooling/prettier
|
version: link:../../../tooling/prettier
|
||||||
@@ -313,6 +322,12 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.363.0
|
specifier: ^0.363.0
|
||||||
version: 0.363.0(react@18.2.0)
|
version: 0.363.0(react@18.2.0)
|
||||||
|
react-hook-form:
|
||||||
|
specifier: ^7.51.2
|
||||||
|
version: 7.51.2(react@18.2.0)
|
||||||
|
zod:
|
||||||
|
specifier: ^3.22.4
|
||||||
|
version: 3.22.4
|
||||||
|
|
||||||
packages/features/admin:
|
packages/features/admin:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -382,12 +397,15 @@ importers:
|
|||||||
|
|
||||||
packages/features/team-accounts:
|
packages/features/team-accounts:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@hookform/resolvers':
|
||||||
|
specifier: ^3.3.4
|
||||||
|
version: 3.3.4(react-hook-form@7.51.2)
|
||||||
'@kit/accounts':
|
'@kit/accounts':
|
||||||
specifier: '*'
|
specifier: '*'
|
||||||
version: link:../accounts
|
version: link:../accounts
|
||||||
'@kit/emails':
|
'@kit/email-templates':
|
||||||
specifier: '*'
|
specifier: '*'
|
||||||
version: link:../../emails
|
version: link:../../email-templates
|
||||||
'@kit/eslint-config':
|
'@kit/eslint-config':
|
||||||
specifier: 0.2.0
|
specifier: 0.2.0
|
||||||
version: link:../../../tooling/eslint
|
version: link:../../../tooling/eslint
|
||||||
@@ -545,7 +563,7 @@ importers:
|
|||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: 0.1.0(@supabase/supabase-js@2.40.0)
|
version: 0.1.0(@supabase/supabase-js@2.40.0)
|
||||||
'@supabase/supabase-js':
|
'@supabase/supabase-js':
|
||||||
specifier: ^2.39.8
|
specifier: ^2.40.0
|
||||||
version: 2.40.0
|
version: 2.40.0
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: 5.28.6
|
specifier: 5.28.6
|
||||||
@@ -553,6 +571,9 @@ importers:
|
|||||||
|
|
||||||
packages/ui:
|
packages/ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@hookform/resolvers':
|
||||||
|
specifier: ^3.3.4
|
||||||
|
version: 3.3.4(react-hook-form@7.51.2)
|
||||||
'@radix-ui/react-accordion':
|
'@radix-ui/react-accordion':
|
||||||
specifier: 1.1.2
|
specifier: 1.1.2
|
||||||
version: 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.71)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -663,8 +684,8 @@ importers:
|
|||||||
specifier: ^8.10.0
|
specifier: ^8.10.0
|
||||||
version: 8.10.0(date-fns@3.6.0)(react@18.2.0)
|
version: 8.10.0(date-fns@3.6.0)(react@18.2.0)
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: ^7.51.1
|
specifier: ^7.51.2
|
||||||
version: 7.51.1(react@18.2.0)
|
version: 7.51.2(react@18.2.0)
|
||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0)
|
version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -1770,13 +1791,12 @@ packages:
|
|||||||
yargs: 17.7.2
|
yargs: 17.7.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@hookform/resolvers@3.3.4(react-hook-form@7.51.1):
|
/@hookform/resolvers@3.3.4(react-hook-form@7.51.2):
|
||||||
resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==}
|
resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react-hook-form: ^7.0.0
|
react-hook-form: ^7.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
react-hook-form: 7.51.1(react@18.2.0)
|
react-hook-form: 7.51.2(react@18.2.0)
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@humanwhocodes/config-array@0.11.14:
|
/@humanwhocodes/config-array@0.11.14:
|
||||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||||
@@ -9911,8 +9931,8 @@ packages:
|
|||||||
- webpack-cli
|
- webpack-cli
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/react-hook-form@7.51.1(react@18.2.0):
|
/react-hook-form@7.51.2(react@18.2.0):
|
||||||
resolution: {integrity: sha512-ifnBjl+kW0ksINHd+8C/Gp6a4eZOdWyvRv0UBaByShwU8JbVx5hTcTWEcd5VdybvmPTATkVVXk9npXArHmo56w==}
|
resolution: {integrity: sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==}
|
||||||
engines: {node: '>=12.22.0'}
|
engines: {node: '>=12.22.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18
|
react: ^16.8.0 || ^17 || ^18
|
||||||
|
|||||||
13
turbo.json
13
turbo.json
@@ -55,6 +55,17 @@
|
|||||||
"globalEnv": [
|
"globalEnv": [
|
||||||
"SKIP_ENV_VALIDATION",
|
"SKIP_ENV_VALIDATION",
|
||||||
"STRIPE_SECRET_KEY",
|
"STRIPE_SECRET_KEY",
|
||||||
"STRIPE_WEBHOOK_SECRET"
|
"STRIPE_WEBHOOK_SECRET",
|
||||||
|
"NEXT_PUBLIC_PRODUCT_NAME",
|
||||||
|
"EMAIL_SENDER",
|
||||||
|
"EMAIL_PORT",
|
||||||
|
"EMAIL_HOST",
|
||||||
|
"EMAIL_TLS",
|
||||||
|
"EMAIL_USER",
|
||||||
|
"EMAIL_PASSWORD",
|
||||||
|
"SIGN_IN_PATH",
|
||||||
|
"SIGN_UP_PATH",
|
||||||
|
"TEAM_ACCOUNTS_HOME_PATH",
|
||||||
|
"INVITATION_PAGE_PATH"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user