Skip to content

Commit f16a682

Browse files
committed
feat: email otp input
1 parent fec7f1d commit f16a682

4 files changed

Lines changed: 36 additions & 21 deletions

File tree

src/components/OtpInput.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,28 @@ import styles from '@/styles/otpInput.module.css';
44
interface Props {
55
length?: number;
66
value: string;
7+
inputMode?: 'numeric' | 'text';
78
onChange: (value: string) => void;
89
}
910

10-
const OtpInput: React.FC<Props> = ({ length = 6, value, onChange }) => {
11+
const OtpInput: React.FC<Props> = ({
12+
length = 6,
13+
value,
14+
inputMode = 'numeric',
15+
onChange,
16+
}) => {
1117
const inputs = useRef<Array<HTMLInputElement | null>>([]);
1218

1319
const values = value.split('').concat(Array(length).fill('')).slice(0, length);
1420

1521
const handleChange = (index: number, char: string) => {
16-
if (!/^\d?$/.test(char)) return;
22+
if (inputMode === 'numeric') {
23+
if (!/^\d?$/.test(char)) return;
24+
}
25+
26+
if (inputMode === 'text') {
27+
if (!/[a-z]/i.test(char)) return;
28+
}
1729

1830
const newValue = value.substring(0, index) + char + value.substring(index + 1);
1931

@@ -48,7 +60,7 @@ const OtpInput: React.FC<Props> = ({ length = 6, value, onChange }) => {
4860
key={i}
4961
ref={el => (inputs.current[i] = el)}
5062
type="text"
51-
inputMode="numeric"
63+
inputMode={inputMode}
5264
maxLength={1}
5365
value={digit || ''}
5466
className={styles.otpInput}

src/views/EmailRegistration.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import styles from '@/styles/verifyOTP.module.css';
66
import { createFetchWithAuth } from '@/fetchWithAuth';
77
import { isPasskeySupported } from '@/utils';
88
import { useInternalAuth } from '@/context/InternalAuthContext';
9+
import OtpInput from '@/components/OtpInput';
910

1011
const EmailRegistration: React.FC = () => {
1112
const navigate = useNavigate();
@@ -129,16 +130,11 @@ const EmailRegistration: React.FC = () => {
129130
— Code expires in {formatTime(emailTimeLeft)}
130131
</span>
131132
</label>
132-
133-
<input
134-
id="emailCode"
135-
type="text"
136-
maxLength={6}
133+
<OtpInput
134+
length={6}
137135
value={emailOtp}
138-
autoComplete="off"
139-
onChange={e => setEmailOtp(e.target.value)}
140-
className={styles.input}
141-
required
136+
onChange={setEmailOtp}
137+
inputMode="text"
142138
/>
143139

144140
<button type="button" onClick={onResendEmail} className={styles.resend}>

tests/EmailRegistration.test.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ jest.mock('react-router-dom', () => ({
1717
useNavigate: jest.fn(),
1818
}));
1919

20+
jest.mock('@/components/OtpInput', () => (props: any) => (
21+
<input
22+
data-testid="otp-input"
23+
value={props.value}
24+
onChange={e => props.onChange(e.target.value)}
25+
/>
26+
));
27+
2028
describe('EmailRegistration', () => {
2129
const navigate = jest.fn();
2230
const validateToken = jest.fn();
@@ -57,12 +65,11 @@ describe('EmailRegistration', () => {
5765
test('shows validation error if OTP is not 6 digits', async () => {
5866
render(<EmailRegistration />);
5967

60-
fireEvent.change(screen.getByLabelText(/email verification code/i), {
61-
target: { value: '123' },
68+
fireEvent.change(screen.getByTestId('otp-input'), {
69+
target: { value: 'ABC' },
6270
});
6371

6472
fireEvent.click(screen.getByRole('button', { name: /verify & continue/i }));
65-
6673
expect(screen.getByText(/please enter a valid code/i)).toBeInTheDocument();
6774
});
6875

@@ -71,8 +78,8 @@ describe('EmailRegistration', () => {
7178

7279
render(<EmailRegistration />);
7380

74-
fireEvent.change(screen.getByLabelText(/email verification code/i), {
75-
target: { value: '123456' },
81+
fireEvent.change(screen.getByTestId('otp-input'), {
82+
target: { value: 'ABCDEF' },
7683
});
7784

7885
await act(async () => {
@@ -124,8 +131,8 @@ describe('EmailRegistration', () => {
124131
expect(isPasskeySupported).toHaveBeenCalled();
125132
});
126133

127-
fireEvent.change(screen.getByLabelText(/email verification code/i), {
128-
target: { value: '123456' },
134+
fireEvent.change(screen.getByTestId('otp-input'), {
135+
target: { value: 'ABCDEF' },
129136
});
130137

131138
await act(async () => {
@@ -145,8 +152,8 @@ describe('EmailRegistration', () => {
145152

146153
render(<EmailRegistration />);
147154

148-
fireEvent.change(screen.getByLabelText(/email verification code/i), {
149-
target: { value: '123456' },
155+
fireEvent.change(screen.getByTestId('otp-input'), {
156+
target: { value: 'ABCDEF' },
150157
});
151158

152159
await act(async () => {

0 commit comments

Comments
 (0)