Skip to content

Instantly share code, notes, and snippets.

@adeleke5140
Last active July 25, 2023 20:09
Show Gist options
  • Save adeleke5140/5ab19340eca3ab59a0a4ca8044b10f73 to your computer and use it in GitHub Desktop.
Save adeleke5140/5ab19340eca3ab59a0a4ca8044b10f73 to your computer and use it in GitHub Desktop.
2fa-input-react

2fa with React

This is an attempt at creating a set of input element that accept 2fa code.

There is a bug where pressing the backspace key messes with the focused input which I don't have the time to fix right now.

Mine is written in TS but this is the original version in JS: Original

import { ChangeEvent, FormEvent, ClipboardEvent, KeyboardEvent, FocusEvent } from "react"
import { useEffect, useState, useRef, useReducer, Reducer } from "react"
function submitValues(values: number[]) {
return new Promise(resolve => {
setTimeout(() => {
resolve(values)
}, 1000)
})
}
function clampIndex(index: number) {
if (index > 3) {
return 3
} else if (index < 0) {
return 0
} else {
return index
}
}
enum STATUS {
IDLE,
PENDING,
}
type ActionType =
| { type: "INPUT", payload: { index: number, value: number | string } }
| { type: "BACK" }
| { type: "FOCUS", payload: { focusedIndex: number } }
| { type: "PASTE", payload: { pastedValue: number[] } }
| { type: "VERIFY", }
| { type: "VERIFY_SUCCESS" }
interface InitialState {
inputValues: (number | '')[]
status: STATUS,
focusedIndex: number
}
function reducer(state: InitialState, action: ActionType) {
switch (action.type) {
case "INPUT":
return {
...state,
inputValues: [
...state.inputValues.slice(0, action.payload.index),
action.payload.value,
...state.inputValues.slice(action.payload.index + 1)
] as number[],
focusedIndex: clampIndex(action.payload.index + 1)
}
case "BACK":
return {
...state,
focusedIndex: clampIndex(state.focusedIndex - 1)
}
case "PASTE":
return {
...state,
inputValues: state.inputValues.map((_, index) => action.payload.pastedValue[index] || '')
}
case "FOCUS":
return {
...state,
focusedIndex: action.payload.focusedIndex
}
case "VERIFY":
return {
...state,
status: STATUS.PENDING
}
case "VERIFY_SUCCESS":
return {
...state,
status: STATUS.IDLE
}
default:
throw new Error("unknown action")
}
}
const initialState = {
inputValues: Array(3).fill(''),
focusedIndex: 0,
status: STATUS.IDLE
}
const TwoFactorInput = () => {
const [{ inputValues, focusedIndex, status }, dispatch] = useReducer<Reducer<InitialState, ActionType>>(reducer, initialState)
function handleInput(index: number, value: number | string) {
dispatch({ type: 'INPUT', payload: { index, value } })
}
function handleBack() {
dispatch({ type: 'BACK' })
}
function handlePaste(pastedValue: number[]) {
dispatch({ type: 'PASTE', payload: { pastedValue } })
if (pastedValue.length === 3) {
dispatch({ type: 'VERIFY' })
submitValues(pastedValue).then(() => {
dispatch({ type: 'VERIFY_SUCCESS' })
})
}
}
function handleFocus(focusedIndex: number) {
dispatch({ type: 'FOCUS', payload: { focusedIndex } })
}
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault()
dispatch({ type: 'VERIFY' })
submitValues(inputValues as number[]).then(() => {
dispatch({ type: 'VERIFY_SUCCESS' })
})
}
return (
<form className="flex gap-4" onSubmit={handleSubmit}>
{inputValues.map((value, index) => (
<Input
key={index}
value={value}
onChange={handleInput}
onPaste={handlePaste}
onFocus={handleFocus}
onBackspace={handleBack}
isFocused={focusedIndex === index}
index={index}
isDisabled={status === STATUS.PENDING}
/>
))}
</form>
)
}
interface InputProps {
index: number,
value: number | '',
onChange: (index: number, value: number | string) => void,
onPaste: (pastedValue: number[]) => void
onBackspace: () => void
isFocused: boolean
onFocus: (index: number) => void
isDisabled: boolean
}
const Input = (props: InputProps) => {
const { index, value, onChange, onPaste, onBackspace, isFocused, onFocus, isDisabled, ...rest } = props
const inputRef = useRef<HTMLInputElement | null>(null)
useEffect(() => {
requestAnimationFrame(() => {
if (inputRef.current !== document.activeElement && isFocused) {
inputRef.current?.focus()
}
})
}, [inputRef, isFocused])
function handleChange(e: ChangeEvent<HTMLInputElement>) {
onChange(index, e.target.value)
}
function handlePaste(e: ClipboardEvent<HTMLInputElement>) {
onPaste(e.clipboardData.getData('text').split('').map(Number))
}
function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Backspace') {
onBackspace()
}
}
function handleFocus(e: FocusEvent<HTMLInputElement>) {
e.target.setSelectionRange(0, 1)
onFocus(index)
}
return (
<input
ref={inputRef}
type="text"
value={value}
onChange={handleChange}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
maxLength={1}
disabled={isDisabled}
{...rest}
className="w-12 h-12 text-s-grey-17 text-lg text-center border-2 border-s-grey-16 rounded focus:outline-none focus:border-blue-500"
/>
)
}
export { TwoFactorInput }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment