Skip to content

Instantly share code, notes, and snippets.

@princefishthrower
Last active May 13, 2023 21:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save princefishthrower/9d2a7d8403e4b2a36018b87333a837a0 to your computer and use it in GitHub Desktop.
Save princefishthrower/9d2a7d8403e4b2a36018b87333a837a0 to your computer and use it in GitHub Desktop.
Simple OTP TextInput for React Native
import {useIsFocused} from '@react-navigation/native';
import * as React from 'react';
import {useRef} from 'react';
import {TextInput, View} from 'react-native';
import {isIOS} from '../../helpers/utils';
import {useTimeout} from '../../hooks/useTimeout';
export interface IOTPInputProps {
otpCodeChanged: (otpCode: string) => void;
}
const NUMBER_OF_INPUTS = 6;
export function OTPInput(props: IOTPInputProps) {
const {otpCodeChanged} = props;
const isFocused = useIsFocused();
const [values, setValues] = React.useState<string[]>([
'',
'',
'',
'',
'',
'',
]);
const itemsRef = useRef<Array<TextInput | null>>([]);
const applyOTPCodeToInputs = (code: string) => {
// split up code and apply it to all inputs
const codeArray = code.split('');
codeArray.forEach((char, index) => {
const input = itemsRef.current[index];
if (input) {
input.setNativeProps({
text: char,
});
}
});
// focus on last input as a cherry on top
const lastInput = itemsRef.current[itemsRef.current.length - 1];
if (lastInput) {
lastInput.focus();
otpCodeChanged(code);
}
};
useTimeout(
() => {
// focus on the first input
const firstInput = itemsRef.current[0];
if (firstInput) {
firstInput.focus();
}
},
isFocused ? 1000 : null,
);
return (
<View
style={{flex: 1, flexDirection: 'row', justifyContent: 'space-around'}}>
{Array.from({length: NUMBER_OF_INPUTS}, (_, index) => (
<TextInput
style={{
paddingLeft: isIOS() ? 0 : 0,
paddingRight: isIOS() ? 10 : 0,
}}
ref={(el) => (itemsRef.current[index] = el)}
key={index}
keyboardType={'numeric'}
placeholder={'X'}
value={values[index]}
defaultValue=""
// first input can have a length of 6 because they paste their code into it
maxLength={index === 0 ? 6 : 1}
onChange={(event) => {
const {text} = event.nativeEvent;
// only continue one if we see a text of length 1 or 6
if (text.length === 0 || text.length === 1 || text.length === 6) {
if (text.length === 6) {
applyOTPCodeToInputs(text);
return;
}
// going forward, only if text is not empty
if (text.length === 1 && index !== NUMBER_OF_INPUTS - 1) {
const nextInput = itemsRef.current[index + 1];
if (nextInput) {
nextInput.focus();
}
}
}
// determine new value
const newValues = [...values];
newValues[index] = text;
// update state
setValues(newValues);
// also call callback as a flat string
otpCodeChanged(newValues.join(''));
}}
onKeyPress={(event) => {
if (event.nativeEvent.key === 'Backspace') {
// going backward:
if (index !== 0) {
const previousInput = itemsRef.current[index - 1];
if (previousInput) {
previousInput.focus();
return;
}
}
}
}}
textContentType="oneTimeCode"
/>
))}
</View>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment