react-native-otp-input-box
Version:
A customizable, lightweight OTP/PIN input for React Native with paste, SMS autofill, secure entry, ref methods, and more.
157 lines (156 loc) • 7.05 kB
JavaScript
import React, { useRef, useState, useEffect, useImperativeHandle, forwardRef } from "react";
import { View, TextInput, StyleSheet, Platform, } from "react-native";
const OtpInput = forwardRef(({ length = 6, onOtpChange, onOtpComplete, value, defaultValue = "", keyboardType = "number-pad", autoFocus = true, secureTextEntry = false, disabled = false, error = false, placeholderCharacter = "", blurOnComplete = false, inputStyle, focusedInputStyle, errorInputStyle, disabledInputStyle, containerStyle, gap = 5, autoComplete = "off", accessibilityLabel = "OTP input", testID = "otp-input", }, ref) => {
const isControlled = value !== undefined;
const [internalOtp, setInternalOtp] = useState(() => {
const initial = isControlled ? value : defaultValue;
return initial
? initial.slice(0, length).split("").concat(Array(length - Math.min(initial.length, length)).fill(""))
: Array(length).fill("");
});
const [focusedIndex, setFocusedIndex] = useState(0);
const inputs = useRef([]);
const otp = isControlled
? value.slice(0, length).split("").concat(Array(Math.max(0, length - value.length)).fill(""))
: internalOtp;
// Sync controlled value to internal state
useEffect(() => {
if (isControlled && value) {
const arr = value.slice(0, length).split("");
const padded = arr.concat(Array(length - arr.length).fill(""));
setInternalOtp(padded);
}
}, [isControlled, value, length]);
const updateOtp = (newOtp, notifyChange = true) => {
var _a;
if (!isControlled)
setInternalOtp(newOtp);
const joined = newOtp.join("");
if (notifyChange)
onOtpChange === null || onOtpChange === void 0 ? void 0 : onOtpChange(joined);
if (joined.length === length) {
onOtpComplete === null || onOtpComplete === void 0 ? void 0 : onOtpComplete(joined);
if (blurOnComplete)
(_a = inputs.current[length - 1]) === null || _a === void 0 ? void 0 : _a.blur();
}
};
const handleChange = (text, index) => {
var _a, _b;
if (disabled)
return;
// Handle paste (multiple characters)
if (text.length > 1) {
const digits = text.replace(/\D/g, "").slice(0, length);
const newOtp = [...otp];
let i = 0;
for (; i < digits.length && index + i < length; i++) {
newOtp[index + i] = digits[i];
}
updateOtp(newOtp);
const nextFocus = Math.min(index + i, length - 1);
(_a = inputs.current[nextFocus]) === null || _a === void 0 ? void 0 : _a.focus();
return;
}
const newOtp = [...otp];
newOtp[index] = text;
updateOtp(newOtp);
if (text && index < length - 1) {
(_b = inputs.current[index + 1]) === null || _b === void 0 ? void 0 : _b.focus();
}
};
const handleKeyPress = (e, index) => {
var _a;
if (disabled)
return;
if (e.nativeEvent.key === "Backspace" && otp[index] === "" && index > 0) {
(_a = inputs.current[index - 1]) === null || _a === void 0 ? void 0 : _a.focus();
}
else if (e.nativeEvent.key === "Backspace") {
const newOtp = [...otp];
newOtp[index] = "";
updateOtp(newOtp);
}
};
const handleFocus = (index) => {
setFocusedIndex(index);
};
const handleBlur = () => {
setFocusedIndex(-1);
};
useImperativeHandle(ref, () => ({
clear: () => {
var _a;
const empty = Array(length).fill("");
if (!isControlled)
setInternalOtp(empty);
onOtpChange === null || onOtpChange === void 0 ? void 0 : onOtpChange("");
(_a = inputs.current[0]) === null || _a === void 0 ? void 0 : _a.focus();
},
focus: (index = 0) => {
var _a;
(_a = inputs.current[Math.min(index, length - 1)]) === null || _a === void 0 ? void 0 : _a.focus();
},
blur: () => {
inputs.current.forEach((inp) => inp === null || inp === void 0 ? void 0 : inp.blur());
},
setValue: (val) => {
const arr = val.slice(0, length).split("");
const padded = arr.concat(Array(length - arr.length).fill(""));
if (!isControlled)
setInternalOtp(padded);
onOtpChange === null || onOtpChange === void 0 ? void 0 : onOtpChange(padded.join(""));
if (padded.join("").length === length)
onOtpComplete === null || onOtpComplete === void 0 ? void 0 : onOtpComplete(padded.join(""));
},
getValue: () => otp.join(""),
}));
const textContentType = autoComplete === "sms-otp" || autoComplete === "one-time-code"
? "oneTimeCode"
: "none";
const getInputStyle = (index) => [
styles.input,
{ marginHorizontal: gap / 2 },
inputStyle,
focusedIndex === index && (focusedInputStyle || styles.inputFocused),
error && (errorInputStyle || styles.inputError),
disabled && (disabledInputStyle || styles.inputDisabled),
];
return (<View style={[styles.container, containerStyle]} accessibilityLabel={accessibilityLabel} accessibilityRole="none">
{otp.map((digit, index) => (<TextInput key={index} ref={(r) => {
inputs.current[index] = r;
}} value={digit} placeholder={placeholderCharacter} placeholderTextColor="#999" keyboardType={keyboardType} maxLength={length} // Allows paste; single-char enforced in handleChange
onChangeText={(text) => handleChange(text, index)} onKeyPress={(e) => handleKeyPress(e, index)} onFocus={() => handleFocus(index)} onBlur={handleBlur} style={getInputStyle(index)} autoFocus={autoFocus && index === 0} secureTextEntry={secureTextEntry} editable={!disabled} selectTextOnFocus textContentType={Platform.OS === "ios" ? textContentType : undefined} autoComplete={Platform.OS === "android" && autoComplete !== "off" ? "sms-otp" : undefined} testID={`${testID}-${index}`} accessibilityLabel={`${accessibilityLabel} digit ${index + 1} of ${length}`}/>))}
</View>);
});
const styles = StyleSheet.create({
container: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
marginVertical: 20,
},
input: {
width: 45,
height: 50,
borderWidth: 1,
borderColor: "#999",
textAlign: "center",
fontSize: 18,
borderRadius: 5,
backgroundColor: "#fff",
},
inputFocused: {
borderColor: "#007AFF",
borderWidth: 2,
},
inputError: {
borderColor: "#FF3B30",
borderWidth: 2,
},
inputDisabled: {
backgroundColor: "#f0f0f0",
color: "#999",
},
});
OtpInput.displayName = "OtpInput";
export default OtpInput;