mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
fix: use shadcn pin input and revert changes
This commit is contained in:
@ -25,6 +25,7 @@
|
||||
"@tanstack/react-query": "^4.29.5",
|
||||
"formidable": "^2.1.1",
|
||||
"framer-motion": "^10.12.8",
|
||||
"input-otp": "^1.2.4",
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
@ -35,7 +36,6 @@
|
||||
"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",
|
||||
@ -48,7 +48,6 @@
|
||||
"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": {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } 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,7 +54,6 @@ export const EnableAuthenticatorAppDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: EnableAuthenticatorAppDialogProps) => {
|
||||
const [state, setState] = useState<PinInputState>('input');
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
@ -120,20 +119,14 @@ export const EnableAuthenticatorAppDialog = ({
|
||||
token,
|
||||
}: TEnableTwoFactorAuthenticationForm) => {
|
||||
try {
|
||||
const enabled2fa = await enableTwoFactorAuthentication({ code: token });
|
||||
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) {
|
||||
enableTwoFactorAuthenticationForm.setError('token', {
|
||||
message: 'Unable to setup two-factor authentication',
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Unable to setup two-factor authentication',
|
||||
description:
|
||||
@ -153,7 +146,7 @@ export const EnableAuthenticatorAppDialog = ({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-full max-w-md md:max-w-md lg:max-w-md">
|
||||
<DialogContent className="w-full max-w-xl md:max-w-xl lg:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Enable Authenticator App</DialogTitle>
|
||||
|
||||
@ -248,43 +241,13 @@ export const EnableAuthenticatorAppDialog = ({
|
||||
<FormField
|
||||
name="token"
|
||||
control={enableTwoFactorAuthenticationForm.control}
|
||||
render={({ field: _field }) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-muted-foreground">Token</FormLabel>
|
||||
<FormControl>
|
||||
<PinInput
|
||||
id="enable-2fa-pin-input"
|
||||
state={state}
|
||||
onSubmit={async ({ code, input }) => {
|
||||
if (code.length === 6) {
|
||||
setState('loading');
|
||||
enableTwoFactorAuthenticationForm.setValue('token', code);
|
||||
|
||||
await enableTwoFactorAuthenticationForm.handleSubmit(
|
||||
onEnableTwoFactorAuthenticationFormSubmit,
|
||||
)();
|
||||
|
||||
if (
|
||||
enableTwoFactorAuthenticationForm.formState.isSubmitted &&
|
||||
!enableTwoFactorAuthenticationForm.formState.errors.totpCode
|
||||
) {
|
||||
setState('success');
|
||||
return;
|
||||
}
|
||||
|
||||
setState('error');
|
||||
|
||||
setTimeout(() => {
|
||||
setState('input');
|
||||
input.value = '';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
input.focus();
|
||||
}, 500);
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<Input {...field} type="text" value={field.value ?? ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -31,7 +31,6 @@ import {
|
||||
} 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';
|
||||
|
||||
const ERROR_MESSAGES: Partial<Record<keyof typeof ErrorCode, string>> = {
|
||||
@ -73,7 +72,6 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
|
||||
'totp' | 'backup'
|
||||
>('totp');
|
||||
const [state, setState] = useState<PinInputState>('input');
|
||||
|
||||
const form = useForm<TSignInFormSchema>({
|
||||
values: {
|
||||
@ -153,10 +151,9 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
title: 'Unable to sign in',
|
||||
description: errorMessage ?? 'An unknown error occurred',
|
||||
});
|
||||
}
|
||||
|
||||
setState('success');
|
||||
console.log(result);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result?.url) {
|
||||
throw new Error('An unknown error occurred');
|
||||
@ -164,13 +161,8 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
|
||||
window.location.href = result.url;
|
||||
} catch (err) {
|
||||
form.setError('totpCode', {
|
||||
message: 'invalid totp',
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'An unknown error occurred',
|
||||
variant: 'destructive',
|
||||
description:
|
||||
'We encountered an unknown error while attempting to sign you In. Please try again later.',
|
||||
});
|
||||
@ -262,7 +254,7 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
open={isTwoFactorAuthenticationDialogOpen}
|
||||
onOpenChange={onCloseTwoFactorAuthenticationDialog}
|
||||
>
|
||||
<DialogContent className="w-full max-w-md md:max-w-md lg:max-w-md">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Two-Factor Authentication</DialogTitle>
|
||||
</DialogHeader>
|
||||
@ -273,38 +265,13 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="totpCode"
|
||||
render={({ field: _field }) => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Authentication Token</FormLabel>
|
||||
<FormControl>
|
||||
<PinInput
|
||||
id="verify-2fa-signin-pin-input"
|
||||
state={state}
|
||||
onSubmit={async ({ code, input }) => {
|
||||
if (code.length === 6) {
|
||||
setState('loading');
|
||||
form.setValue('totpCode', code);
|
||||
|
||||
await form.handleSubmit(onFormSubmit)();
|
||||
|
||||
if (form.formState.isSubmitted && !form.formState.errors.totpCode) {
|
||||
setState('success');
|
||||
return;
|
||||
}
|
||||
|
||||
setState('error');
|
||||
|
||||
setTimeout(() => {
|
||||
setState('input');
|
||||
input.value = '';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
input.focus();
|
||||
}, 500);
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<Input type="text" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
42
package-lock.json
generated
42
package-lock.json
generated
@ -141,6 +141,7 @@
|
||||
"@tanstack/react-query": "^4.29.5",
|
||||
"formidable": "^2.1.1",
|
||||
"framer-motion": "^10.12.8",
|
||||
"input-otp": "^1.2.4",
|
||||
"lucide-react": "^0.279.0",
|
||||
"luxon": "^3.4.0",
|
||||
"micro": "^10.0.1",
|
||||
@ -151,7 +152,6 @@
|
||||
"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",
|
||||
@ -164,7 +164,6 @@
|
||||
"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": {
|
||||
@ -11750,6 +11749,15 @@
|
||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
|
||||
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
|
||||
},
|
||||
"node_modules/input-otp": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.2.4.tgz",
|
||||
"integrity": "sha512-md6rhmD+zmMnUh5crQNSQxq3keBRYvE3odbr4Qb9g2NWzQv9azi+t1a3X4TBTbh98fsGHgEEJlzbe1q860uGCA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
|
||||
@ -15728,18 +15736,6 @@
|
||||
"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",
|
||||
@ -18874,24 +18870,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
|
||||
@ -110,10 +110,15 @@ module.exports = {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: 0 },
|
||||
},
|
||||
'caret-blink': {
|
||||
'0%,70%,100%': { opacity: '1' },
|
||||
'20%,50%': { opacity: '0' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'caret-blink': 'caret-blink 1.25s ease-out infinite',
|
||||
},
|
||||
screens: {
|
||||
'3xl': '1920px',
|
||||
|
||||
@ -1,83 +1,75 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { CodeInput, getSegmentCssWidth } from 'rci';
|
||||
import { useIsFocused } from 'use-is-focused';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DashIcon } from '@radix-ui/react-icons';
|
||||
import { OTPInput, OTPInputContext } from 'input-otp';
|
||||
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
const PinInput = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
'flex items-center gap-2 has-[:disabled]:opacity-50',
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn('disabled:cursor-not-allowed', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export type PinInputState = 'input' | 'loading' | 'error' | 'success';
|
||||
export type PinInputProps = {
|
||||
id: string;
|
||||
state: PinInputState;
|
||||
autoFocus?: boolean;
|
||||
onSubmit({ code, input }: { code: string; input: EventTarget & HTMLInputElement }): void;
|
||||
};
|
||||
PinInput.displayName = 'PinInput';
|
||||
|
||||
const PinInput = ({ id, autoFocus, state, onSubmit }: PinInputProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const focused = useIsFocused(inputRef);
|
||||
const PinInputGroup = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex items-center', className)} {...props} />
|
||||
));
|
||||
|
||||
const width = getSegmentCssWidth('14px');
|
||||
PinInputGroup.displayName = 'PinInputGroup';
|
||||
|
||||
const PinInputSlot = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const context = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = context.slots[index];
|
||||
|
||||
return (
|
||||
<CodeInput
|
||||
id={id}
|
||||
className={cn({
|
||||
'motion-safe:animate-[shake_0.15s_ease-in-out_0s_2]': state === 'error',
|
||||
})}
|
||||
inputClassName="caret-transparent selection:bg-transparent ring:ring-2"
|
||||
autoFocus={autoFocus}
|
||||
length={6}
|
||||
fontSize="30px"
|
||||
readOnly={state !== 'input'}
|
||||
disabled={state === 'loading'}
|
||||
inputRef={inputRef}
|
||||
padding={'14px'}
|
||||
spacing={'24px'}
|
||||
spellCheck={false}
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
autoComplete="one-time-code"
|
||||
onChange={({ currentTarget: input }) => {
|
||||
input.value = input.value.replace(/\D+/g, '');
|
||||
onSubmit({ code: input.value, input });
|
||||
}}
|
||||
renderSegment={(segment) => {
|
||||
const isCaret = focused && segment.state === 'cursor';
|
||||
const isSelection = focused && segment.state === 'selected';
|
||||
const isLoading = state === 'loading';
|
||||
const isSuccess = state === 'success';
|
||||
const isError = state === 'error';
|
||||
const isActive = isSuccess || isError || isSelection || isCaret;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={segment.index}
|
||||
data-state={state}
|
||||
className={cn(
|
||||
'border-input flex appearance-none rounded-md border [--segment-color:#a2e771] data-[state="error"]:[--segment-color:#dc2626] data-[state="success"]:[--segment-color:#a2e771]',
|
||||
{
|
||||
'shadow-[var(--segment-color)_0_0_0_1px] data-[state]:border-[var(--segment-color)]':
|
||||
isActive,
|
||||
'animate-[pulse-border_1s_ease-in-out_0s_infinite]': isLoading,
|
||||
},
|
||||
)}
|
||||
style={{ width }}
|
||||
>
|
||||
<div
|
||||
className={cn({
|
||||
'm-[5px] flex-1 rounded-sm opacity-[0.15625]': isSelection,
|
||||
'mx-auto my-2 flex-[0_0_2px] animate-[blink-caret_1.2s_step-end_infinite] justify-self-center bg-black':
|
||||
isCaret,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
|
||||
isActive && 'ring-ring z-10 ring-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export { PinInput };
|
||||
PinInputSlot.displayName = 'PinInputSlot';
|
||||
|
||||
const PinInputSeparator = React.forwardRef<
|
||||
React.ElementRef<'div'>,
|
||||
React.ComponentPropsWithoutRef<'div'>
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<DashIcon />
|
||||
</div>
|
||||
));
|
||||
|
||||
PinInputSeparator.displayName = 'PinInputSeparator';
|
||||
|
||||
export { PinInput, PinInputGroup, PinInputSlot, PinInputSeparator };
|
||||
|
||||
@ -114,25 +114,3 @@
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(100 116 139 / 0.5);
|
||||
}
|
||||
|
||||
@keyframes blink-caret {
|
||||
50% {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
25% {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
50% {
|
||||
border-color: var(--segment-color);
|
||||
box-shadow: var(--segment-color) 0 0 0 1px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user