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:
giancarlo
2024-03-28 11:20:12 +08:00
parent 6048cc4759
commit 3ac4d3b00d
30 changed files with 290 additions and 264 deletions

View File

@@ -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/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/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.)
And features that can be added to the application:

View File

@@ -36,7 +36,7 @@ const AppConfigSchema = z.object({
});
const appConfig = AppConfigSchema.parse({
name: 'Awesomely',
name: process.env.NEXT_PUBLIC_PRODUCT_NAME,
title: 'Awesomely - Your SaaS Title',
description: 'Your SaaS Description',
url: process.env.NEXT_PUBLIC_SITE_URL,

View File

@@ -128,9 +128,12 @@ function getPatterns() {
handler: async (req: NextRequest, res: NextResponse) => {
const supabase = createMiddlewareClient(req, res);
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;
// 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) {
return NextResponse.redirect(
new URL(pathsConfig.app.home, req.nextUrl.origin).href,
@@ -157,10 +160,6 @@ function getPatterns() {
const requiresMultiFactorAuthentication =
await checkRequiresMultiFactorAuthentication(supabase);
console.log({
requiresMultiFactorAuthentication,
});
// If user requires multi-factor authentication, redirect to MFA page.
if (requiresMultiFactorAuthentication) {
return NextResponse.redirect(

View File

@@ -15,6 +15,7 @@ const INTERNAL_PACKAGES = [
'@kit/billing',
'@kit/billing-gateway',
'@kit/stripe',
'@kit/email-templates',
];
/** @type {import('next').NextConfig} */

View File

@@ -22,7 +22,7 @@
"@kit/auth": "^0.1.0",
"@kit/billing": "^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/mailers": "^0.1.0",
"@kit/shared": "^0.1.0",
@@ -32,7 +32,7 @@
"@next/mdx": "^14.1.0",
"@radix-ui/react-icons": "^1.3.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-next-experimental": "^5.28.6",
"@tanstack/react-table": "^8.11.3",
@@ -47,7 +47,7 @@
"next-themes": "^0.2.1",
"react": "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",
"recharts": "^2.10.3",
"rehype-autolink-headings": "^7.1.0",

View File

@@ -34,6 +34,7 @@
"currentPassword": "Current Password",
"newPassword": "New Password",
"repeatPassword": "Repeat New Password",
"repeatPasswordDescription": "Please repeat your new password to confirm it",
"yourPassword": "Your Password",
"updatePasswordSubmitLabel": "Update Password",
"updateEmailCardTitle": "Update your Email",

View File

@@ -19,7 +19,7 @@
"@kit/stripe": "0.1.0",
"@kit/supabase": "^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",
"zod": "^3.22.4"
},
@@ -33,7 +33,7 @@
"@kit/tailwind-config": "0.1.0",
"@kit/tsconfig": "0.1.0",
"@kit/ui": "*",
"@supabase/supabase-js": "^2.39.8",
"@supabase/supabase-js": "^2.40.0",
"lucide-react": "^0.363.0",
"zod": "^3.22.4"
},

View File

@@ -1,4 +1,4 @@
# Emails - @kit/emails
# Emails - @kit/email-templates
This package is responsible for managing email templates using the react.email library.

View File

@@ -1,5 +1,5 @@
{
"name": "@kit/emails",
"name": "@kit/email-templates",
"private": true,
"version": "0.1.0",
"scripts": {

View File

@@ -16,6 +16,8 @@
},
"devDependencies": {
"@kit/billing-gateway": "*",
"@kit/email-templates": "*",
"@kit/mailers": "*",
"@kit/eslint-config": "0.2.0",
"@kit/prettier-config": "0.1.0",
"@kit/shared": "*",
@@ -23,6 +25,7 @@
"@kit/tailwind-config": "0.1.0",
"@kit/tsconfig": "0.1.0",
"@kit/ui": "*",
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-icons": "^1.3.0",
"lucide-react": "^0.363.0",
"react-hook-form": "^7.51.2",

View File

@@ -1,17 +1,23 @@
'use client';
import { useFormStatus } from 'react-dom';
import { zodResolver } from '@hookform/resolvers/zod';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@kit/ui/dialog';
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@kit/ui/alert-dialog';
import { Button } from '@kit/ui/button';
import { ErrorBoundary } from '@kit/ui/error-boundary';
import { Form, FormControl, FormItem, FormLabel } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
@@ -20,10 +26,6 @@ import { Trans } from '@kit/ui/trans';
import { deletePersonalAccountAction } from '../../server/personal-accounts-server-actions';
export function AccountDangerZone() {
return <DeleteAccountContainer />;
}
function DeleteAccountContainer() {
return (
<div className={'flex flex-col space-y-4'}>
<div className={'flex flex-col space-y-1'}>
@@ -45,30 +47,39 @@ function DeleteAccountContainer() {
function DeleteAccountModal() {
return (
<Dialog>
<DialogTrigger asChild>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button data-test={'delete-account-button'} variant={'destructive'}>
<Trans i18nKey={'account:deleteAccount'} />
</Button>
</DialogTrigger>
</AlertDialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
<AlertDialogContent onEscapeKeyDown={(e) => e.preventDefault()}>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans i18nKey={'account:deleteAccount'} />
</DialogTitle>
</DialogHeader>
</AlertDialogTitle>
</AlertDialogHeader>
<ErrorBoundary fallback={<DeleteAccountErrorAlert />}>
<DeleteAccountForm />
</ErrorBoundary>
</DialogContent>
</Dialog>
</AlertDialogContent>
</AlertDialog>
);
}
function DeleteAccountForm() {
const form = useForm();
const form = useForm({
resolver: zodResolver(
z.object({
confirmation: z.string().refine((value) => value === 'DELETE'),
}),
),
defaultValues: {
confirmation: '',
},
});
return (
<Form {...form}>
@@ -110,17 +121,25 @@ function DeleteAccountForm() {
</FormItem>
</div>
<div className={'flex justify-end space-x-2.5'}>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans i18nKey={'common:cancel'} />
</AlertDialogCancel>
<DeleteAccountSubmitButton />
</div>
</AlertDialogFooter>
</form>
</Form>
);
}
function DeleteAccountSubmitButton() {
const { pending } = useFormStatus();
return (
<Button
type={'submit'}
disabled={pending}
data-test={'confirm-delete-account-button'}
name={'action'}
value={'delete'}

View File

@@ -4,7 +4,7 @@ import type { Factor } from '@supabase/gotrue-js';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { X } from 'lucide-react';
import { ShieldCheck, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
@@ -15,6 +15,7 @@ import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
@@ -84,6 +85,8 @@ export function MultiFactorAuthFactorsList() {
return (
<div className={'flex flex-col space-y-4'}>
<Alert variant={'info'}>
<ShieldCheck className={'h-4'} />
<AlertTitle>
<Trans i18nKey={'account:multiFactorAuthHeading'} />
</AlertTitle>
@@ -171,6 +174,10 @@ function ConfirmUnenrollFactorModal(
>
<Trans i18nKey={'account:unenrollFactorModalButtonLabel'} />
</AlertDialogAction>
<AlertDialogCancel>
<Trans i18nKey={'common:cancel'} />
</AlertDialogCancel>
</AlertDialogContent>
</AlertDialog>
);

View File

@@ -5,6 +5,8 @@ import { useState } from 'react';
import type { User } from '@supabase/gotrue-js';
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 { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
@@ -15,6 +17,7 @@ import { Button } from '@kit/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -38,16 +41,6 @@ export const UpdatePasswordForm = ({
const updateUserMutation = useUpdateUser();
const [needsReauthentication, setNeedsReauthentication] = useState(false);
const form = useForm({
resolver: zodResolver(
PasswordUpdateSchema.withTranslation(t('passwordNotMatching')),
),
defaultValues: {
newPassword: '',
repeatPassword: '',
},
});
const updatePasswordFromCredential = (password: string) => {
const redirectTo = [window.location.origin, callbackPath].join('');
@@ -87,6 +80,16 @@ export const UpdatePasswordForm = ({
updatePasswordFromCredential(newPassword);
};
const form = useForm({
resolver: zodResolver(
PasswordUpdateSchema.withTranslation(t('passwordNotMatching')),
),
defaultValues: {
newPassword: '',
repeatPassword: '',
},
});
return (
<Form {...form}>
<form
@@ -95,27 +98,11 @@ export const UpdatePasswordForm = ({
>
<div className={'flex flex-col space-y-4'}>
<If condition={updateUserMutation.data}>
<Alert variant={'success'}>
<AlertTitle>
<Trans i18nKey={'account:updatePasswordSuccess'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'account:updatePasswordSuccessMessage'} />
</AlertDescription>
</Alert>
<SuccessAlert />
</If>
<If condition={needsReauthentication}>
<Alert variant={'warning'}>
<AlertTitle>
<Trans i18nKey={'account:needsReauthentication'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'account:needsReauthenticationDescription'} />
</AlertDescription>
</Alert>
<NeedsReauthenticationAlert />
</If>
<FormField
@@ -164,6 +151,10 @@ export const UpdatePasswordForm = ({
/>
</FormControl>
<FormDescription>
<Trans i18nKey={'account:repeatPasswordDescription'} />
</FormDescription>
<FormMessage />
</FormItem>
);
@@ -180,3 +171,35 @@ export const UpdatePasswordForm = ({
</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>
);
}

View File

@@ -64,7 +64,6 @@ export function UpdateAccountDetailsForm({
>
<FormField
name={'displayName'}
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>

View File

@@ -2,12 +2,16 @@
import { RedirectType, redirect } from 'next/navigation';
import { z } from 'zod';
import { Logger } from '@kit/shared/logger';
import { requireAuth } from '@kit/supabase/require-auth';
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
import { PersonalAccountsService } from './services/personal-accounts.service';
const emailSettings = getEmailSettingsFromEnvironment();
export async function refreshAuthSession() {
const client = getSupabaseServerActionClient();
@@ -23,40 +27,45 @@ export async function deletePersonalAccountAction(formData: FormData) {
throw new Error('Confirmation required to delete account');
}
const session = await requireAuth(getSupabaseServerActionClient());
const client = getSupabaseServerActionClient();
const session = await requireAuth(client);
if (session.error) {
Logger.error(`User is not authenticated. Redirecting to login page`);
redirect(session.redirectTo);
}
const client = getSupabaseServerActionClient();
const service = new PersonalAccountsService(client);
// retrieve user ID and email
const userId = session.data.user.id;
const userEmail = session.data.user.email ?? null;
Logger.info(
{
userId,
name: 'accounts',
},
`Deleting personal account...`,
);
// create a new instance of the personal accounts service
const service = new PersonalAccountsService(client);
await service.deletePersonalAccount(
getSupabaseServerActionClient({ admin: true }),
{
userId,
},
);
Logger.info(
{
userId,
name: 'accounts',
},
`Personal account deleted successfully.`,
);
// delete the user's account and cancel all subscriptions
await service.deletePersonalAccount({
adminClient: getSupabaseServerActionClient({ admin: true }),
userId,
userEmail,
emailSettings,
});
// sign out the user after deleting their account
await client.auth.signOut();
// redirect to the home page
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,
});
}

View File

@@ -1,6 +1,7 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { BillingGatewayService } from '@kit/billing-gateway';
import { Mailer } from '@kit/mailers';
import { Logger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database';
@@ -22,17 +23,25 @@ export class PersonalAccountsService {
* Delete personal account of a user.
* This will delete the user from the authentication provider and cancel all subscriptions.
*/
async deletePersonalAccount(
adminClient: SupabaseClient<Database>,
params: { userId: string },
) {
async deletePersonalAccount(params: {
adminClient: SupabaseClient<Database>;
userId: string;
userEmail: string | null;
emailSettings: {
fromEmail: string;
productName: string;
};
}) {
Logger.info(
{ userId: params.userId, name: this.namespace },
'User requested deletion. Processing...',
);
// execute the deletion of the user
try {
await adminClient.auth.admin.deleteUser(params.userId);
await params.adminClient.auth.admin.deleteUser(params.userId);
} catch (error) {
Logger.error(
{
@@ -46,6 +55,7 @@ export class PersonalAccountsService {
throw new Error('Error deleting user');
}
// Cancel all user subscriptions
try {
await this.cancelAllUserSubscriptions(params.userId);
} catch (error) {
@@ -55,6 +65,57 @@ export class PersonalAccountsService {
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) {
@@ -93,6 +154,7 @@ export class PersonalAccountsService {
);
}
// execute all cancellation requests
await Promise.all(cancellationRequests);
Logger.info(

View File

@@ -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>
);
}

View File

@@ -13,7 +13,7 @@
},
"devDependencies": {
"@kit/accounts": "*",
"@kit/emails": "*",
"@kit/email-templates": "*",
"@kit/eslint-config": "0.2.0",
"@kit/mailers": "*",
"@kit/prettier-config": "0.1.0",
@@ -22,12 +22,12 @@
"@kit/tailwind-config": "0.1.0",
"@kit/tsconfig": "0.1.0",
"@kit/ui": "*",
"@hookform/resolvers/zod": "1.0.0",
"@hookform/resolvers": "^3.3.4",
"lucide-react": "^0.363.0"
},
"peerDependencies": {
"@kit/accounts": "0.1.0",
"@kit/emails": "0.1.0",
"@kit/email-templates": "0.1.0",
"@kit/mailers": "0.1.0",
"@kit/shared": "0.1.0",
"@kit/supabase": "0.1.0",

View File

@@ -149,7 +149,9 @@ export class AccountInvitationsService {
for (const invitation of responseInvitations) {
const promise = async () => {
try {
const { renderInviteEmail } = await import('@kit/emails');
const { renderInviteEmail } = await import(
'../../../../email-templates'
);
const html = renderInviteEmail({
link: this.getInvitationLink(invitation.invite_token),

View File

@@ -29,13 +29,13 @@
"@kit/tailwind-config": "0.1.0",
"@kit/tsconfig": "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"
},
"peerDependencies": {
"@epic-web/invariant": "^1.0.0",
"@supabase/ssr": "^0.1.0",
"@supabase/supabase-js": "^2.39.8",
"@supabase/supabase-js": "^2.40.0",
"@tanstack/react-query": "^5.28.6"
},
"eslintConfig": {

View File

@@ -9,6 +9,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-accordion": "1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
@@ -38,7 +39,7 @@
"class-variance-authority": "^0.7.0",
"date-fns": "^3.2.0",
"lucide-react": "^0.363.0",
"react-hook-form": "^7.49.2",
"react-hook-form": "^7.51.2",
"react-i18next": "^14.1.0",
"sonner": "^1.4.41",
"zod": "^3.22.4"
@@ -58,7 +59,7 @@
"lucide-react": "^0.363.0",
"prettier": "^3.2.4",
"react-day-picker": "^8.10.0",
"react-hook-form": "^7.51.1",
"react-hook-form": "^7.51.2",
"react-i18next": "^14.1.0",
"sonner": "^1.4.41",
"tailwindcss": "3.4.1",

View File

@@ -2,7 +2,7 @@ import type { MDXComponents } from 'mdx/types';
import { getMDXComponent } from 'next-contentlayer/hooks';
import Components from './mdx-components';
// @ts-expect-error
// @ts-ignore
import styles from './mdx-renderer.module.css';
export function Mdx({

View File

@@ -5,7 +5,7 @@ import { Slot } from '@radix-ui/react-slot';
import type { ControllerProps, FieldPath, FieldValues } 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';
const Form = FormProvider;

View File

@@ -36,7 +36,13 @@ const InputOTPSlot = React.forwardRef<
React.ComponentPropsWithoutRef<'div'> & { index: number }
>(({ index, className, ...props }, ref) => {
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 (
<div

56
pnpm-lock.yaml generated
View File

@@ -34,7 +34,7 @@ importers:
version: 1.0.0
'@hookform/resolvers':
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':
specifier: ^0.1.0
version: link:../../packages/features/accounts
@@ -50,9 +50,9 @@ importers:
'@kit/billing-gateway':
specifier: ^0.1.0
version: link:../../packages/billing-gateway
'@kit/emails':
'@kit/email-templates':
specifier: ^0.1.0
version: link:../../packages/emails
version: link:../../packages/email-templates
'@kit/i18n':
specifier: ^0.1.0
version: link:../../packages/i18n
@@ -81,7 +81,7 @@ importers:
specifier: ^0.1.0
version: 0.1.0(@supabase/supabase-js@2.40.0)
'@supabase/supabase-js':
specifier: ^2.39.8
specifier: ^2.40.0
version: 2.40.0
'@tanstack/react-query':
specifier: 5.28.6
@@ -126,8 +126,8 @@ importers:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
react-hook-form:
specifier: ^7.51.1
version: 7.51.1(react@18.2.0)
specifier: ^7.51.2
version: 7.51.2(react@18.2.0)
react-i18next:
specifier: ^14.1.0
version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0)
@@ -253,7 +253,7 @@ importers:
specifier: '*'
version: link:../ui
'@supabase/supabase-js':
specifier: ^2.39.8
specifier: ^2.40.0
version: 2.40.0
lucide-react:
specifier: ^0.363.0
@@ -262,7 +262,7 @@ importers:
specifier: ^3.22.4
version: 3.22.4
packages/emails:
packages/email-templates:
dependencies:
'@react-email/components':
specifier: 0.0.15
@@ -283,12 +283,21 @@ importers:
packages/features/accounts:
devDependencies:
'@hookform/resolvers':
specifier: ^3.3.4
version: 3.3.4(react-hook-form@7.51.2)
'@kit/billing-gateway':
specifier: '*'
version: link:../../billing-gateway
'@kit/email-templates':
specifier: '*'
version: link:../../email-templates
'@kit/eslint-config':
specifier: 0.2.0
version: link:../../../tooling/eslint
'@kit/mailers':
specifier: '*'
version: link:../../mailers
'@kit/prettier-config':
specifier: 0.1.0
version: link:../../../tooling/prettier
@@ -313,6 +322,12 @@ importers:
lucide-react:
specifier: ^0.363.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:
devDependencies:
@@ -382,12 +397,15 @@ importers:
packages/features/team-accounts:
devDependencies:
'@hookform/resolvers':
specifier: ^3.3.4
version: 3.3.4(react-hook-form@7.51.2)
'@kit/accounts':
specifier: '*'
version: link:../accounts
'@kit/emails':
'@kit/email-templates':
specifier: '*'
version: link:../../emails
version: link:../../email-templates
'@kit/eslint-config':
specifier: 0.2.0
version: link:../../../tooling/eslint
@@ -545,7 +563,7 @@ importers:
specifier: ^0.1.0
version: 0.1.0(@supabase/supabase-js@2.40.0)
'@supabase/supabase-js':
specifier: ^2.39.8
specifier: ^2.40.0
version: 2.40.0
'@tanstack/react-query':
specifier: 5.28.6
@@ -553,6 +571,9 @@ importers:
packages/ui:
dependencies:
'@hookform/resolvers':
specifier: ^3.3.4
version: 3.3.4(react-hook-form@7.51.2)
'@radix-ui/react-accordion':
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)
@@ -663,8 +684,8 @@ importers:
specifier: ^8.10.0
version: 8.10.0(date-fns@3.6.0)(react@18.2.0)
react-hook-form:
specifier: ^7.51.1
version: 7.51.1(react@18.2.0)
specifier: ^7.51.2
version: 7.51.2(react@18.2.0)
react-i18next:
specifier: ^14.1.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
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==}
peerDependencies:
react-hook-form: ^7.0.0
dependencies:
react-hook-form: 7.51.1(react@18.2.0)
dev: false
react-hook-form: 7.51.2(react@18.2.0)
/@humanwhocodes/config-array@0.11.14:
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
@@ -9911,8 +9931,8 @@ packages:
- webpack-cli
dev: false
/react-hook-form@7.51.1(react@18.2.0):
resolution: {integrity: sha512-ifnBjl+kW0ksINHd+8C/Gp6a4eZOdWyvRv0UBaByShwU8JbVx5hTcTWEcd5VdybvmPTATkVVXk9npXArHmo56w==}
/react-hook-form@7.51.2(react@18.2.0):
resolution: {integrity: sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==}
engines: {node: '>=12.22.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18

View File

@@ -55,6 +55,17 @@
"globalEnv": [
"SKIP_ENV_VALIDATION",
"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"
]
}