diff --git a/apps/web/package.json b/apps/web/package.json index 71b480000..71eea5555 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,6 +28,7 @@ "cookie-es": "^1.0.0", "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", diff --git a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx index 98bcacf10..5af9ced18 100644 --- a/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/document-action-auth-2fa.tsx @@ -18,7 +18,7 @@ import { 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 { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog'; @@ -138,7 +138,15 @@ export const DocumentActionAuth2FA = ({ 2FA token - + + {Array(6) + .fill(null) + .map((_, i) => ( + + + + ))} + diff --git a/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx b/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx index c5745a499..5949d14bb 100644 --- a/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx +++ b/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx @@ -28,7 +28,7 @@ import { FormItem, 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({ @@ -107,7 +107,15 @@ export const DisableAuthenticatorAppDialog = () => { render={({ field }) => ( - + + {Array(6) + .fill(null) + .map((_, i) => ( + + + + ))} + diff --git a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx index ce0b66ba4..96cbf6d1f 100644 --- a/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx +++ b/apps/web/src/components/forms/2fa/enable-authenticator-app-dialog.tsx @@ -30,7 +30,7 @@ import { 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'; import { RecoveryCodeList } from './recovery-code-list'; @@ -212,7 +212,15 @@ export const EnableAuthenticatorAppDialog = ({ onSuccess }: EnableAuthenticatorA Token - + + {Array(6) + .fill(null) + .map((_, i) => ( + + + + ))} + diff --git a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx index 8a6177b5b..3c5c4e5ca 100644 --- a/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx +++ b/apps/web/src/components/forms/2fa/view-recovery-codes-dialog.tsx @@ -30,7 +30,7 @@ import { FormItem, 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 { RecoveryCodeList } from './recovery-code-list'; @@ -115,7 +115,15 @@ export const ViewRecoveryCodesDialog = () => { render={({ field }) => ( - + + {Array(6) + .fill(null) + .map((_, i) => ( + + + + ))} + diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index 8d4dd7cd0..59b8af6c7 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -38,6 +38,7 @@ import { } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { PasswordInput } from '@documenso/ui/primitives/password-input'; +import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input'; import { useToast } from '@documenso/ui/primitives/use-toast'; const ERROR_MESSAGES: Partial> = { @@ -372,9 +373,17 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign name="totpCode" render={({ field }) => ( - Authentication Token + Token - + + {Array(6) + .fill(null) + .map((_, i) => ( + + + + ))} + diff --git a/package-lock.json b/package-lock.json index c47cd67d1..685181df1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,6 +109,7 @@ "cookie-es": "^1.0.0", "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", @@ -13767,6 +13768,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", diff --git a/packages/tailwind-config/index.cjs b/packages/tailwind-config/index.cjs index 01e7296d3..ae36f7fcf 100644 --- a/packages/tailwind-config/index.cjs +++ b/packages/tailwind-config/index.cjs @@ -124,10 +124,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', diff --git a/packages/ui/primitives/pin-input.tsx b/packages/ui/primitives/pin-input.tsx new file mode 100644 index 000000000..1d80dd3ad --- /dev/null +++ b/packages/ui/primitives/pin-input.tsx @@ -0,0 +1,76 @@ +'use client'; + +import * as React from 'react'; + +import { OTPInput, OTPInputContext } from 'input-otp'; +import { Minus } from 'lucide-react'; + +import { cn } from '../lib/utils'; + +const PinInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, containerClassName, ...props }, ref) => ( + +)); + +PinInput.displayName = 'PinInput'; + +const PinInputGroup = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> +>(({ className, ...props }, ref) => ( +
+)); + +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 ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ); +}); + +PinInputSlot.displayName = 'PinInputSlot'; + +const PinInputSeparator = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> +>(({ ...props }, ref) => ( +
+ +
+)); + +PinInputSeparator.displayName = 'PinInputSeparator'; + +export { PinInput, PinInputGroup, PinInputSlot, PinInputSeparator };