mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 10:42:01 +10:00
feat: migrate nextjs to rr7
This commit is contained in:
@ -0,0 +1,198 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export const ZDisable2FAForm = z.object({
|
||||
totpCode: z.string().trim().optional(),
|
||||
backupCode: z.string().trim().optional(),
|
||||
});
|
||||
|
||||
export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>;
|
||||
|
||||
export const DisableAuthenticatorAppDialog = () => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp');
|
||||
|
||||
const { mutateAsync: disable2FA } = trpc.twoFactorAuthentication.disable.useMutation();
|
||||
|
||||
const disable2FAForm = useForm<TDisable2FAForm>({
|
||||
defaultValues: {
|
||||
totpCode: '',
|
||||
backupCode: '',
|
||||
},
|
||||
resolver: zodResolver(ZDisable2FAForm),
|
||||
});
|
||||
|
||||
const onCloseTwoFactorDisableDialog = () => {
|
||||
disable2FAForm.reset();
|
||||
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const onToggleTwoFactorDisableMethodClick = () => {
|
||||
const method = twoFactorDisableMethod === 'totp' ? 'backup' : 'totp';
|
||||
|
||||
if (method === 'totp') {
|
||||
disable2FAForm.setValue('backupCode', '');
|
||||
}
|
||||
|
||||
if (method === 'backup') {
|
||||
disable2FAForm.setValue('totpCode', '');
|
||||
}
|
||||
|
||||
setTwoFactorDisableMethod(method);
|
||||
};
|
||||
|
||||
const { isSubmitting: isDisable2FASubmitting } = disable2FAForm.formState;
|
||||
|
||||
const onDisable2FAFormSubmit = async ({ totpCode, backupCode }: TDisable2FAForm) => {
|
||||
try {
|
||||
await disable2FA({ totpCode, backupCode });
|
||||
|
||||
toast({
|
||||
title: _(msg`Two-factor authentication disabled`),
|
||||
description: _(
|
||||
msg`Two-factor authentication has been disabled for your account. You will no longer be required to enter a code from your authenticator app when signing in.`,
|
||||
),
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
onCloseTwoFactorDisableDialog();
|
||||
});
|
||||
|
||||
await revalidate();
|
||||
} catch (_err) {
|
||||
toast({
|
||||
title: _(msg`Unable to disable two-factor authentication`),
|
||||
description: _(
|
||||
msg`We were unable to disable two-factor authentication for your account. Please ensure that you have entered your password and backup code correctly and try again.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onCloseTwoFactorDisableDialog}>
|
||||
<DialogTrigger asChild={true}>
|
||||
<Button className="flex-shrink-0" variant="destructive">
|
||||
<Trans>Disable 2FA</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Disable 2FA</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Please provide a token from the authenticator, or a backup code. If you do not have a
|
||||
backup code available, please contact support.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...disable2FAForm}>
|
||||
<form onSubmit={disable2FAForm.handleSubmit(onDisable2FAFormSubmit)}>
|
||||
<fieldset className="flex flex-col gap-y-4" disabled={isDisable2FASubmitting}>
|
||||
{twoFactorDisableMethod === 'totp' && (
|
||||
<FormField
|
||||
name="totpCode"
|
||||
control={disable2FAForm.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<PinInput {...field} value={field.value ?? ''} maxLength={6}>
|
||||
{Array(6)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<PinInputGroup key={i}>
|
||||
<PinInputSlot index={i} />
|
||||
</PinInputGroup>
|
||||
))}
|
||||
</PinInput>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{twoFactorDisableMethod === 'backup' && (
|
||||
<FormField
|
||||
control={disable2FAForm.control}
|
||||
name="backupCode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>Backup Code</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onToggleTwoFactorDisableMethodClick}
|
||||
>
|
||||
{twoFactorDisableMethod === 'totp' ? (
|
||||
<Trans>Use Backup Code</Trans>
|
||||
) : (
|
||||
<Trans>Use Authenticator</Trans>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button type="submit" variant="destructive" loading={isDisable2FASubmitting}>
|
||||
<Trans>Disable 2FA</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,271 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRevalidator } from 'react-router';
|
||||
import { renderSVG } from 'uqr';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { RecoveryCodeList } from './recovery-code-list';
|
||||
|
||||
export const ZEnable2FAForm = z.object({
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
export type TEnable2FAForm = z.infer<typeof ZEnable2FAForm>;
|
||||
|
||||
export type EnableAuthenticatorAppDialogProps = {
|
||||
onSuccess?: () => void;
|
||||
};
|
||||
|
||||
export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorAppDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const { revalidate } = useRevalidator();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
|
||||
|
||||
const { mutateAsync: enable2FA } = trpc.twoFactorAuthentication.enable.useMutation();
|
||||
|
||||
const {
|
||||
mutateAsync: setup2FA,
|
||||
data: setup2FAData,
|
||||
isPending: isSettingUp2FA,
|
||||
} = trpc.twoFactorAuthentication.setup.useMutation({
|
||||
onError: () => {
|
||||
toast({
|
||||
title: _(msg`Unable to setup two-factor authentication`),
|
||||
description: _(
|
||||
msg`We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const enable2FAForm = useForm<TEnable2FAForm>({
|
||||
defaultValues: {
|
||||
token: '',
|
||||
},
|
||||
resolver: zodResolver(ZEnable2FAForm),
|
||||
});
|
||||
|
||||
const { isSubmitting: isEnabling2FA } = enable2FAForm.formState;
|
||||
|
||||
const onEnable2FAFormSubmit = async ({ token }: TEnable2FAForm) => {
|
||||
try {
|
||||
const data = await enable2FA({ code: token });
|
||||
|
||||
setRecoveryCodes(data.recoveryCodes);
|
||||
onSuccess?.();
|
||||
|
||||
toast({
|
||||
title: _(msg`Two-factor authentication enabled`),
|
||||
description: _(
|
||||
msg`You will now be required to enter a code from your authenticator app when signing in.`,
|
||||
),
|
||||
});
|
||||
} catch (_err) {
|
||||
toast({
|
||||
title: _(msg`Unable to setup two-factor authentication`),
|
||||
description: _(
|
||||
msg`We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const downloadRecoveryCodes = () => {
|
||||
if (recoveryCodes) {
|
||||
const blob = new Blob([recoveryCodes.join('\n')], {
|
||||
type: 'text/plain',
|
||||
});
|
||||
|
||||
downloadFile({
|
||||
filename: 'documenso-2FA-recovery-codes.txt',
|
||||
data: blob,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnable2FA = async () => {
|
||||
if (!setup2FAData) {
|
||||
await setup2FA();
|
||||
}
|
||||
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
enable2FAForm.reset();
|
||||
|
||||
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
|
||||
setRecoveryCodes(null);
|
||||
void revalidate();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild={true}>
|
||||
<Button
|
||||
className="flex-shrink-0"
|
||||
loading={isSettingUp2FA}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void handleEnable2FA();
|
||||
}}
|
||||
>
|
||||
<Trans>Enable 2FA</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent position="center">
|
||||
{setup2FAData && (
|
||||
<>
|
||||
{recoveryCodes ? (
|
||||
<div>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Backup codes</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Your recovery codes are listed below. Please store them in a safe place.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4">
|
||||
<RecoveryCodeList recoveryCodes={recoveryCodes} />
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">
|
||||
<Trans>Close</Trans>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button onClick={downloadRecoveryCodes}>
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
) : (
|
||||
<Form {...enable2FAForm}>
|
||||
<form onSubmit={enable2FAForm.handleSubmit(onEnable2FAFormSubmit)}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Enable Authenticator App</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
To enable two-factor authentication, scan the following QR code using your
|
||||
authenticator app.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<fieldset disabled={isEnabling2FA} className="mt-4 flex flex-col gap-y-4">
|
||||
<div
|
||||
className="flex h-36 justify-center"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: renderSVG(setup2FAData?.uri ?? ''),
|
||||
}}
|
||||
/>
|
||||
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans>
|
||||
If your authenticator app does not support QR codes, you can use the
|
||||
following code instead:
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p className="bg-muted/60 text-muted-foreground rounded-lg p-2 text-center font-mono tracking-widest">
|
||||
{setup2FAData?.secret}
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans>
|
||||
Once you have scanned the QR code or entered the code manually, enter the
|
||||
code provided by your authenticator app below.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<FormField
|
||||
name="token"
|
||||
control={enable2FAForm.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-muted-foreground">
|
||||
<Trans>Token</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PinInput {...field} value={field.value ?? ''} maxLength={6}>
|
||||
{Array(6)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<PinInputGroup key={i}>
|
||||
<PinInputSlot index={i} />
|
||||
</PinInputGroup>
|
||||
))}
|
||||
</PinInput>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button type="submit" loading={isEnabling2FA}>
|
||||
<Trans>Enable 2FA</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
61
apps/remix/app/components/forms/2fa/recovery-code-list.tsx
Normal file
61
apps/remix/app/components/forms/2fa/recovery-code-list.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Copy } from 'lucide-react';
|
||||
|
||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type RecoveryCodeListProps = {
|
||||
recoveryCodes: string[];
|
||||
};
|
||||
|
||||
export const RecoveryCodeList = ({ recoveryCodes }: RecoveryCodeListProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const onCopyRecoveryCodeClick = async (code: string) => {
|
||||
try {
|
||||
const result = await copyToClipboard(code);
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Unable to copy recovery code');
|
||||
}
|
||||
|
||||
toast({
|
||||
title: _(msg`Recovery code copied`),
|
||||
description: _(msg`Your recovery code has been copied to your clipboard.`),
|
||||
});
|
||||
} catch (_err) {
|
||||
toast({
|
||||
title: _(msg`Unable to copy recovery code`),
|
||||
description: _(
|
||||
msg`We were unable to copy your recovery code to your clipboard. Please try again.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{recoveryCodes.map((code) => (
|
||||
<div
|
||||
key={code}
|
||||
className="bg-muted text-muted-foreground relative rounded-lg p-4 font-mono md:text-center"
|
||||
>
|
||||
<span>{code}</span>
|
||||
|
||||
<div className="absolute inset-y-0 right-4 flex items-center justify-center">
|
||||
<button
|
||||
className="opacity-60 hover:opacity-80"
|
||||
onClick={() => void onCopyRecoveryCodeClick(code)}
|
||||
>
|
||||
<Copy className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,175 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input';
|
||||
|
||||
import { RecoveryCodeList } from './recovery-code-list';
|
||||
|
||||
export const ZViewRecoveryCodesForm = z.object({
|
||||
token: z.string().min(1, { message: 'Token is required' }),
|
||||
});
|
||||
|
||||
export type TViewRecoveryCodesForm = z.infer<typeof ZViewRecoveryCodesForm>;
|
||||
|
||||
export const ViewRecoveryCodesDialog = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const {
|
||||
data: recoveryCodes,
|
||||
mutate,
|
||||
isPending,
|
||||
error,
|
||||
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
|
||||
|
||||
const viewRecoveryCodesForm = useForm<TViewRecoveryCodesForm>({
|
||||
defaultValues: {
|
||||
token: '',
|
||||
},
|
||||
resolver: zodResolver(ZViewRecoveryCodesForm),
|
||||
});
|
||||
|
||||
const downloadRecoveryCodes = () => {
|
||||
if (recoveryCodes) {
|
||||
const blob = new Blob([recoveryCodes.join('\n')], {
|
||||
type: 'text/plain',
|
||||
});
|
||||
|
||||
downloadFile({
|
||||
filename: 'documenso-2FA-recovery-codes.txt',
|
||||
data: blob,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex-shrink-0">
|
||||
<Trans>View Codes</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="w-full max-w-xl md:max-w-xl lg:max-w-xl">
|
||||
{recoveryCodes ? (
|
||||
<div>
|
||||
<DialogHeader className="mb-4">
|
||||
<DialogTitle>
|
||||
<Trans>View Recovery Codes</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Your recovery codes are listed below. Please store them in a safe place.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<RecoveryCodeList recoveryCodes={recoveryCodes} />
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">
|
||||
<Trans>Close</Trans>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button onClick={downloadRecoveryCodes}>
|
||||
<Trans>Download</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
) : (
|
||||
<Form {...viewRecoveryCodesForm}>
|
||||
<form onSubmit={viewRecoveryCodesForm.handleSubmit((value) => mutate(value))}>
|
||||
<DialogHeader className="mb-4">
|
||||
<DialogTitle>
|
||||
<Trans>View Recovery Codes</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>Please provide a token from your authenticator, or a backup code.</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<fieldset className="flex flex-col space-y-4" disabled={isPending}>
|
||||
<FormField
|
||||
name="token"
|
||||
control={viewRecoveryCodesForm.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<PinInput {...field} value={field.value ?? ''} maxLength={6}>
|
||||
{Array(6)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<PinInputGroup key={i}>
|
||||
<PinInputSlot index={i} />
|
||||
</PinInputGroup>
|
||||
))}
|
||||
</PinInput>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
{match(AppError.parseError(error).message)
|
||||
.with('INCORRECT_TWO_FACTOR_CODE', () => (
|
||||
<Trans>Invalid code. Please try again.</Trans>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<Trans>Something went wrong. Please try again or contact support.</Trans>
|
||||
))}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="secondary">
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button type="submit" loading={isPending}>
|
||||
<Trans>View</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user