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:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Emails - @kit/emails
|
||||
# Emails - @kit/email-templates
|
||||
|
||||
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,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
@@ -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",
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ export function UpdateAccountDetailsForm({
|
||||
>
|
||||
<FormField
|
||||
name={'displayName'}
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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": {
|
||||
"@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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user