Skip to content

Commit a6f85ac

Browse files
committed
Replace react-otp-input with shadcn input-otp (#4889)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces a new OTP input component using the `input-otp` library, along with related animations and configuration updates. It replaces the existing `react-otp-input` dependency and enhances the onboarding email confirmation process. ### Detailed summary - Added `input-otp` dependency to `package.json`. - Removed `react-otp-input` from `package.json`. - Updated `tailwind.config.js` to include `caret-blink` animation. - Created new components: `InputOTP`, `InputOTPGroup`, `InputOTPSlot`, `InputOTPSeparator`. - Refactored `ConfirmEmail.tsx` to utilize the new OTP input components. - Improved styling and functionality for OTP input slots. > The following files were skipped due to too many changes: `pnpm-lock.yaml` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 726bbde commit a6f85ac

File tree

5 files changed

+481
-1020
lines changed

5 files changed

+481
-1020
lines changed

apps/dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"flat": "^6.0.1",
6464
"framer-motion": "11.9.0",
6565
"fuse.js": "7.0.0",
66+
"input-otp": "^1.2.4",
6667
"ioredis": "^5.4.1",
6768
"ipaddr.js": "^2.2.0",
6869
"lottie-react": "^2.4.0",
@@ -87,7 +88,6 @@
8788
"react-icons": "^5.2.1",
8889
"react-intersection-observer": "^9.10.3",
8990
"react-markdown": "^9.0.1",
90-
"react-otp-input": "^3.1.1",
9191
"react-responsive-carousel": "^3.2.23",
9292
"react-table": "^7.8.0",
9393
"recharts": "^2.12.7",
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client";
2+
3+
import { OTPInput, OTPInputContext } from "input-otp";
4+
import { Dot } from "lucide-react";
5+
import * as React from "react";
6+
7+
import { cn } from "@/lib/utils";
8+
9+
const InputOTP = React.forwardRef<
10+
React.ElementRef<typeof OTPInput>,
11+
React.ComponentPropsWithoutRef<typeof OTPInput>
12+
>(({ className, containerClassName, ...props }, ref) => (
13+
<OTPInput
14+
ref={ref}
15+
containerClassName={cn(
16+
"flex items-center gap-2 has-[:disabled]:opacity-50",
17+
containerClassName,
18+
)}
19+
className={cn("disabled:cursor-not-allowed", className)}
20+
{...props}
21+
/>
22+
));
23+
InputOTP.displayName = "InputOTP";
24+
25+
const InputOTPGroup = React.forwardRef<
26+
React.ElementRef<"div">,
27+
React.ComponentPropsWithoutRef<"div">
28+
>(({ className, ...props }, ref) => (
29+
<div ref={ref} className={cn("flex items-center", className)} {...props} />
30+
));
31+
InputOTPGroup.displayName = "InputOTPGroup";
32+
33+
const InputOTPSlot = React.forwardRef<
34+
React.ElementRef<"div">,
35+
React.ComponentPropsWithoutRef<"div"> & { index: number }
36+
>(({ index, className, ...props }, ref) => {
37+
const inputOTPContext = React.useContext(OTPInputContext);
38+
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
39+
40+
return (
41+
<div
42+
ref={ref}
43+
className={cn(
44+
"relative flex h-10 w-10 items-center justify-center border-input border-y border-r text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
45+
isActive && "z-10 ring-2 ring-ring ring-offset-background",
46+
className,
47+
)}
48+
{...props}
49+
>
50+
{char}
51+
{hasFakeCaret && (
52+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
53+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
54+
</div>
55+
)}
56+
</div>
57+
);
58+
});
59+
InputOTPSlot.displayName = "InputOTPSlot";
60+
61+
const InputOTPSeparator = React.forwardRef<
62+
React.ElementRef<"div">,
63+
React.ComponentPropsWithoutRef<"div">
64+
>(({ ...props }, ref) => (
65+
// biome-ignore lint/a11y/useFocusableInteractive: <explanation>
66+
<div ref={ref} role="separator" {...props}>
67+
<Dot />
68+
</div>
69+
));
70+
InputOTPSeparator.displayName = "InputOTPSeparator";
71+
72+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

apps/dashboard/src/components/onboarding/ConfirmEmail.tsx

Lines changed: 30 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import {
2+
InputOTP,
3+
InputOTPGroup,
4+
InputOTPSlot,
5+
} from "@/components/ui/input-otp";
6+
import { cn } from "@/lib/utils";
17
import {
28
useConfirmEmail,
39
useResendEmailConfirmation,
410
} from "@3rdweb-sdk/react/hooks/useApi";
511
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
6-
import { Flex, Input } from "@chakra-ui/react";
12+
import { Flex } from "@chakra-ui/react";
713
import { zodResolver } from "@hookform/resolvers/zod";
814
import {
915
type EmailConfirmationValidationSchema,
@@ -12,9 +18,9 @@ import {
1218
import { useErrorHandler } from "contexts/error-handler";
1319
import { useTrack } from "hooks/analytics/useTrack";
1420
import { useTxNotifications } from "hooks/useTxNotifications";
15-
import { type ClipboardEvent, useState } from "react";
21+
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
22+
import { useState } from "react";
1623
import { useForm } from "react-hook-form";
17-
import OtpInput from "react-otp-input";
1824
import { Button, Text } from "tw-components";
1925
import { shortenString } from "utils/usedapp-external";
2026
import { TitleAndDescription } from "./Title";
@@ -148,14 +154,6 @@ const OnboardingConfirmEmail: React.FC<OnboardingConfirmEmailProps> = ({
148154
});
149155
};
150156

151-
const handlePaste = (e: ClipboardEvent<HTMLDivElement>) => {
152-
const data = e.clipboardData.getData("text");
153-
if (data?.match(/^[A-Z]{6}$/)) {
154-
form.setValue("confirmationToken", data);
155-
handleSubmit();
156-
}
157-
};
158-
159157
return (
160158
<>
161159
<TitleAndDescription
@@ -197,36 +195,29 @@ const OnboardingConfirmEmail: React.FC<OnboardingConfirmEmailProps> = ({
197195
{!completed && (
198196
<form onSubmit={handleSubmit}>
199197
<Flex gap={8} flexDir="column" w="full">
200-
<OtpInput
201-
shouldAutoFocus
198+
<InputOTP
199+
maxLength={6}
200+
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
202201
value={token}
203202
onChange={handleChange}
204-
onPaste={handlePaste}
205-
skipDefaultStyles
206-
numInputs={6}
207-
containerStyle={{
208-
display: "flex",
209-
flexDirection: "row",
210-
gap: "12px",
211-
}}
212-
renderInput={(props) => (
213-
<Input
214-
{...props}
215-
w={20}
216-
h={16}
217-
rounded="md"
218-
textAlign="center"
219-
fontSize="larger"
220-
isDisabled={saving}
221-
borderColor={
222-
form.getFieldState("confirmationToken", form.formState)
223-
.error
224-
? "red.500"
225-
: "borderColor"
226-
}
227-
/>
228-
)}
229-
/>
203+
disabled={saving}
204+
>
205+
<InputOTPGroup className="w-full">
206+
{new Array(6).fill(0).map((_, idx) => (
207+
<InputOTPSlot
208+
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
209+
key={idx}
210+
index={idx}
211+
className={cn("h-12 grow text-lg", {
212+
"border-red-500": form.getFieldState(
213+
"confirmationToken",
214+
form.formState,
215+
).error,
216+
})}
217+
/>
218+
))}
219+
</InputOTPGroup>
220+
</InputOTP>
230221

231222
<Flex flexDir="column" gap={3}>
232223
<Button

apps/dashboard/tailwind.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,16 @@ module.exports = {
8585
from: { height: "var(--radix-accordion-content-height)" },
8686
to: { height: "0" },
8787
},
88+
"caret-blink": {
89+
"0%,70%,100%": { opacity: "1" },
90+
"20%,50%": { opacity: "0" },
91+
},
8892
},
8993
animation: {
9094
"accordion-down": "accordion-down 0.2s ease-out",
9195
"accordion-up": "accordion-up 0.2s ease-out",
9296
skeleton: "skeleton 2s cubic-bezier(0.4, 0, 0.6, 1) infinite",
97+
"caret-blink": "caret-blink 1.25s ease-out infinite",
9398
},
9499
fontFamily: {
95100
sans: ["var(--font-sans)", ...fontFamily.sans],

0 commit comments

Comments
 (0)