forked from cerc-io/snowballtools-base
Refactor: VerifyCodeInput
Component and Modify Access Code Validation (#212)
### TL;DR Implemented the new `VerifyCodeInput` component and updated the access code validation logic. ### What changed? 1. Added a new reusable `VerifyCodeInput` component for verifying codes in a user-friendly way. This component handles paste events, input changes, and keyboard navigation. 2. Updated the backend route `/accesscode` to accept an arbitrary code for now. 3. Incorporated the `VerifyCodeInput` component into the `AccessCode` page, replacing the generic `Input` component. 4. Updated the access code validation logic to check for a trimmed length of 5 characters instead of 6. 5. Added a slight pause for UX purposes when validating the access code on the frontend. ### How to test? 1. Go to the Access Code page. 2. Try entering an access code with various inputs (keyboard, paste, etc.) to see if it works seamlessly. 3. Verify that only a 5-digit code is considered valid. 4. Check the backend logs to ensure the validation endpoint is working as expected. ### Why make this change? This change improves the user experience by providing a custom input component for access code verification and ensures that the access code validation meets the new requirements. ---
This commit is contained in:
parent
b261e7e436
commit
f2e59c11fd
@ -5,10 +5,13 @@ import { authenticateUser, createUser } from '../turnkey-backend';
|
|||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Access Code
|
||||||
|
//
|
||||||
router.post('/accesscode', async (req, res) => {
|
router.post('/accesscode', async (req, res) => {
|
||||||
console.log('Access Code', req.body);
|
console.log('Access Code', req.body);
|
||||||
const { accesscode } = req.body;
|
const { accesscode } = req.body;
|
||||||
if (accesscode === '444444') {
|
if (accesscode === '44444') {
|
||||||
return res.send({ isValid: true });
|
return res.send({ isValid: true });
|
||||||
} else {
|
} else {
|
||||||
return res.sendStatus(204);
|
return res.sendStatus(204);
|
||||||
|
@ -0,0 +1,117 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { Input } from '../Input';
|
||||||
|
|
||||||
|
export interface VerifyCodeInputProps {
|
||||||
|
code: string;
|
||||||
|
setCode: (code: string) => void;
|
||||||
|
submitCode: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VerifyCodeInput = ({
|
||||||
|
code,
|
||||||
|
setCode,
|
||||||
|
submitCode,
|
||||||
|
loading,
|
||||||
|
}: VerifyCodeInputProps) => {
|
||||||
|
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||||
|
|
||||||
|
const handlePaste = (
|
||||||
|
e: React.ClipboardEvent<HTMLInputElement>,
|
||||||
|
i: number,
|
||||||
|
) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const pasteData = e.clipboardData.getData('text').replace(/\D/g, ''); // Only digits
|
||||||
|
if (pasteData.length > 0) {
|
||||||
|
let newCodeArray = code.split('');
|
||||||
|
for (let j = 0; j < pasteData.length && i + j < 6; j++) {
|
||||||
|
newCodeArray[i + j] = pasteData[j];
|
||||||
|
}
|
||||||
|
const newCode = newCodeArray.join('');
|
||||||
|
setCode(newCode);
|
||||||
|
const nextIndex = Math.min(i + pasteData.length, 5);
|
||||||
|
const nextInput = inputRefs.current[nextIndex];
|
||||||
|
if (nextInput) nextInput.focus();
|
||||||
|
if (!newCode.includes(' ')) {
|
||||||
|
submitCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLInputElement>,
|
||||||
|
i: number,
|
||||||
|
) => {
|
||||||
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
||||||
|
|
||||||
|
if (e.key === 'Backspace') {
|
||||||
|
e.preventDefault();
|
||||||
|
const isEmpty = code[i] === ' ';
|
||||||
|
const newCode = !isEmpty
|
||||||
|
? code.slice(0, i) + ' ' + code.slice(i + 1, 6)
|
||||||
|
: code.slice(0, i - 1) + ' ' + code.slice(i, 6);
|
||||||
|
|
||||||
|
setCode(newCode.slice(0, 6));
|
||||||
|
|
||||||
|
if (i === 0 || !isEmpty) return;
|
||||||
|
const prev = inputRefs.current[i - 1];
|
||||||
|
if (prev) prev.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!e.key.match(/[0-9]/)) return;
|
||||||
|
|
||||||
|
e.preventDefault(); // Prevent the default event to avoid duplicate input
|
||||||
|
|
||||||
|
const newCode = code.slice(0, i) + e.key + code.slice(i + 1, 6);
|
||||||
|
setCode(newCode);
|
||||||
|
|
||||||
|
if (i === 5) {
|
||||||
|
submitCode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = inputRefs.current[i + 1];
|
||||||
|
if (next) next.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, i: number) => {
|
||||||
|
const value = e.target.value.slice(-1);
|
||||||
|
if (!value.match(/[0-9]/)) return;
|
||||||
|
|
||||||
|
const newCode = code.slice(0, i) + value + code.slice(i + 1, 6);
|
||||||
|
setCode(newCode);
|
||||||
|
|
||||||
|
if (i < 5) {
|
||||||
|
const next = inputRefs.current[i + 1];
|
||||||
|
if (next) next.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newCode.includes(' ')) {
|
||||||
|
submitCode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRefs.current[0]) {
|
||||||
|
inputRefs.current[0].focus();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{code.split('').map((char, i) => (
|
||||||
|
<Input
|
||||||
|
key={i}
|
||||||
|
value={char === ' ' ? '' : char}
|
||||||
|
ref={(el) => (inputRefs.current[i] = el)}
|
||||||
|
onChange={(e) => handleChange(e, i)}
|
||||||
|
onPaste={(e) => handlePaste(e, i)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, i)}
|
||||||
|
disabled={!!loading}
|
||||||
|
style={{ textAlign: 'center' }} // Add this line to center text
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export { VerifyCodeInput } from './VerifyCodeInput';
|
@ -6,7 +6,7 @@ import {
|
|||||||
LoaderIcon,
|
LoaderIcon,
|
||||||
} from 'components/shared/CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { WavyBorder } from 'components/shared/WavyBorder';
|
import { WavyBorder } from 'components/shared/WavyBorder';
|
||||||
import { Input } from 'components/shared/Input';
|
import { VerifyCodeInput } from 'components/shared/VerifyCodeInput';
|
||||||
import { verifyAccessCode } from 'utils/accessCode';
|
import { verifyAccessCode } from 'utils/accessCode';
|
||||||
|
|
||||||
type AccessMethod = 'accesscode' | 'passkey';
|
type AccessMethod = 'accesscode' | 'passkey';
|
||||||
@ -28,6 +28,9 @@ export const AccessCode: React.FC<AccessCodeProps> = ({
|
|||||||
setAccessMethod('accesscode');
|
setAccessMethod('accesscode');
|
||||||
try {
|
try {
|
||||||
const isValidAccessCode = await verifyAccessCode(accessCode);
|
const isValidAccessCode = await verifyAccessCode(accessCode);
|
||||||
|
|
||||||
|
// add a pause for ux
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
if (isValidAccessCode) {
|
if (isValidAccessCode) {
|
||||||
localStorage.setItem('accessCode', accessCode);
|
localStorage.setItem('accessCode', accessCode);
|
||||||
onCorrectAccessCode();
|
onCorrectAccessCode();
|
||||||
@ -43,7 +46,7 @@ export const AccessCode: React.FC<AccessCodeProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loading = accessMethod;
|
const loading = accessMethod;
|
||||||
const isValidAccessCodeLength = accessCode.length === 6;
|
const isValidAccessCodeLength = accessCode.trim().length === 5;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -56,10 +59,11 @@ export const AccessCode: React.FC<AccessCodeProps> = ({
|
|||||||
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
<div className="self-stretch p-4 xs:p-6 flex-col justify-center items-center gap-8 flex">
|
||||||
<div className="self-stretch flex-col gap-8 flex">
|
<div className="self-stretch flex-col gap-8 flex">
|
||||||
<div className="flex-col justify-start items-start gap-2 inline-flex">
|
<div className="flex-col justify-start items-start gap-2 inline-flex">
|
||||||
<Input
|
<VerifyCodeInput
|
||||||
value={accessCode}
|
loading={!!loading}
|
||||||
onChange={(e) => setAccessCode(e.target.value)}
|
code={accessCode}
|
||||||
disabled={!!loading}
|
setCode={setAccessCode}
|
||||||
|
submitCode={validateAccessCode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@ -70,9 +74,7 @@ export const AccessCode: React.FC<AccessCodeProps> = ({
|
|||||||
<ArrowRightCircleFilledIcon height="16" />
|
<ArrowRightCircleFilledIcon height="16" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={validateAccessCode}
|
||||||
validateAccessCode();
|
|
||||||
}}
|
|
||||||
variant={'secondary'}
|
variant={'secondary'}
|
||||||
disabled={!accessCode || !isValidAccessCodeLength || !!loading}
|
disabled={!accessCode || !isValidAccessCodeLength || !!loading}
|
||||||
>
|
>
|
||||||
@ -82,7 +84,10 @@ export const AccessCode: React.FC<AccessCodeProps> = ({
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="justify-center items-center gap-2 inline-flex">
|
<div className="justify-center items-center gap-2 inline-flex">
|
||||||
<div className="text-red-500 text-sm">
|
<div className="text-red-500 text-sm">
|
||||||
Error: {error.message}
|
Error: {error.message}.{' '}
|
||||||
|
<a href="/signup" className="underline">
|
||||||
|
Try again?
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user