feat: 2fa pin input component

This commit is contained in:
Ephraim Atta-Duncan
2024-02-15 14:21:40 +00:00
parent fe2093fe7c
commit 897f0dabde
3 changed files with 70 additions and 8 deletions

View File

@ -35,6 +35,7 @@
"perfect-freehand": "^1.2.0",
"posthog-js": "^1.75.3",
"posthog-node": "^3.1.1",
"rci": "^0.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
@ -47,6 +48,7 @@
"typescript": "5.2.2",
"ua-parser-js": "^1.0.37",
"uqr": "^0.1.2",
"use-is-focused": "^0.0.1",
"zod": "^3.22.4"
},
"devDependencies": {

View File

@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
@ -27,8 +27,8 @@ import {
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { PinInput, type PinInputState } from '@documenso/ui/primitives/pin-input';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { RecoveryCodeList } from './recovery-code-list';
@ -54,6 +54,7 @@ export const EnableAuthenticatorAppDialog = ({
open,
onOpenChange,
}: EnableAuthenticatorAppDialogProps) => {
const [state, setState] = useState<PinInputState>('input');
const router = useRouter();
const { toast } = useToast();
@ -119,13 +120,15 @@ export const EnableAuthenticatorAppDialog = ({
token,
}: TEnableTwoFactorAuthenticationForm) => {
try {
await enableTwoFactorAuthentication({ code: token });
const enabled2fa = await enableTwoFactorAuthentication({ code: token });
toast({
title: 'Two-factor authentication enabled',
description:
'Two-factor authentication has been enabled for your account. You will now be required to enter a code from your authenticator app when signing in.',
});
return enabled2fa;
} catch (_err) {
toast({
title: 'Unable to setup two-factor authentication',
@ -136,6 +139,31 @@ export const EnableAuthenticatorAppDialog = ({
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onPinInputChange = ({ currentTarget: input }: any) => {
input.value = input.value.replace(/\D+/g, '');
if (input.value.length === 6) {
setState('loading');
void onEnableTwoFactorAuthenticationFormSubmit({ token: input.value }).then((success) => {
if (success) {
setState('success');
return;
}
setState('error');
setTimeout(() => {
setState('input');
input.value = '';
input.dispatchEvent(new Event('input'));
input.focus();
}, 500);
});
}
};
const onCompleteClick = () => {
flushSync(() => {
onOpenChange(false);
@ -146,7 +174,7 @@ export const EnableAuthenticatorAppDialog = ({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full max-w-xl md:max-w-xl lg:max-w-xl">
<DialogContent className="w-full max-w-md md:max-w-md lg:max-w-md">
<DialogHeader>
<DialogTitle>Enable Authenticator App</DialogTitle>
@ -241,18 +269,18 @@ export const EnableAuthenticatorAppDialog = ({
<FormField
name="token"
control={enableTwoFactorAuthenticationForm.control}
render={({ field }) => (
render={({ field: _field }) => (
<FormItem>
<FormLabel className="text-muted-foreground">Token</FormLabel>
<FormControl>
<Input {...field} type="text" value={field.value ?? ''} />
<PinInput id="remix" state={state} onChange={onPinInputChange} autoFocus />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
{/* <DialogFooter>
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
@ -260,7 +288,7 @@ export const EnableAuthenticatorAppDialog = ({
<Button type="submit" loading={isEnableTwoFactorAuthenticationSubmitting}>
Enable 2FA
</Button>
</DialogFooter>
</DialogFooter> */}
</form>
</Form>
))

32
package-lock.json generated
View File

@ -151,6 +151,7 @@
"perfect-freehand": "^1.2.0",
"posthog-js": "^1.75.3",
"posthog-node": "^3.1.1",
"rci": "^0.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-dropzone": "^14.2.3",
@ -163,6 +164,7 @@
"typescript": "5.2.2",
"ua-parser-js": "^1.0.37",
"uqr": "^0.1.2",
"use-is-focused": "^0.0.1",
"zod": "^3.22.4"
},
"devDependencies": {
@ -15726,6 +15728,18 @@
"node": ">= 0.8"
}
},
"node_modules/rci": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/rci/-/rci-0.1.0.tgz",
"integrity": "sha512-o/elFrXXRLdYDAq/qQUFE175TqzJ5nU3MYwIwa6WOZfljNJ4akQSy1n7zA79swB696MNIFDWJs+Do0q2FBTy+Q==",
"dependencies": {
"use-code-input": "0.0.2"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/re-resizable": {
"version": "6.9.6",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz",
@ -18860,6 +18874,24 @@
}
}
},
"node_modules/use-code-input": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/use-code-input/-/use-code-input-0.0.2.tgz",
"integrity": "sha512-lDIUiRca0K8sF+c/KZ9cz5g6oPqlFiTmaDgwGzg0wlNSnFAvROtweKy0XpihEWJwo2tjETtgAxIh82RVGaBFHQ==",
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/use-is-focused": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/use-is-focused/-/use-is-focused-0.0.1.tgz",
"integrity": "sha512-EXVmfDqdzUJOYukC9rBCs4TYd93lDVAL6TxegnV0+3U4cBxWxhbyt1bOm5u1ox+0MZZjamBFU/NSTLTtex2uwQ==",
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/use-sidecar": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",