UNPKG

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
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;