From aadb22cdbfcfddb4da3778e74429abd9c29f2522 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 24 May 2024 02:51:25 +0000 Subject: [PATCH] fix: use shadcn pin input and revert changes --- apps/web/package.json | 3 +- .../2fa/enable-authenticator-app-dialog.tsx | 51 +------ apps/web/src/components/forms/signin.tsx | 45 +----- package-lock.json | 42 ++---- packages/tailwind-config/index.cjs | 5 + packages/ui/primitives/pin-input.tsx | 136 +++++++++--------- packages/ui/styles/theme.css | 22 --- 7 files changed, 93 insertions(+), 211 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 8656d5092..6ff9979bb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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": { 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 41a5ca573..7a181c4cc 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 @@ -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('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 ( - + Enable Authenticator App @@ -248,43 +241,13 @@ export const EnableAuthenticatorAppDialog = ({ ( + render={({ field }) => ( Token - { - 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 - /> + + )} /> diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index b182d5d76..ec690a568 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -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> = { @@ -73,7 +72,6 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState< 'totp' | 'backup' >('totp'); - const [state, setState] = useState('input'); const form = useForm({ 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} > - + Two-Factor Authentication @@ -273,38 +265,13 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign ( + render={({ field }) => ( Authentication Token - { - 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 - /> + + )} /> diff --git a/package-lock.json b/package-lock.json index 803a68344..310f227ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/tailwind-config/index.cjs b/packages/tailwind-config/index.cjs index 1564454d8..f1061b654 100644 --- a/packages/tailwind-config/index.cjs +++ b/packages/tailwind-config/index.cjs @@ -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', diff --git a/packages/ui/primitives/pin-input.tsx b/packages/ui/primitives/pin-input.tsx index 9f579288c..3c51a49c6 100644 --- a/packages/ui/primitives/pin-input.tsx +++ b/packages/ui/primitives/pin-input.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, containerClassName, ...props }, ref) => ( + +)); -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(null); - const focused = useIsFocused(inputRef); +const PinInputGroup = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> +>(({ className, ...props }, ref) => ( +
+)); - 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 ( - { - 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 ( -
-
-
- ); - }} - /> +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
); -}; +}); -export { PinInput }; +PinInputSlot.displayName = 'PinInputSlot'; + +const PinInputSeparator = React.forwardRef< + React.ElementRef<'div'>, + React.ComponentPropsWithoutRef<'div'> +>(({ ...props }, ref) => ( +
+ +
+)); + +PinInputSeparator.displayName = 'PinInputSeparator'; + +export { PinInput, PinInputGroup, PinInputSlot, PinInputSeparator }; diff --git a/packages/ui/styles/theme.css b/packages/ui/styles/theme.css index 15c6a2f8d..fe7bfa087 100644 --- a/packages/ui/styles/theme.css +++ b/packages/ui/styles/theme.css @@ -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; - } -}