import * as Clipboard from "expo-clipboard"; import { useCallback, useEffect, useRef, useState } from "react"; import { AppState, Text, TouchableOpacity, View } from "react-native"; import { InputField } from "@/components/input-field"; import { icons } from "@/constants"; import { tr } from "@/lib/i18n"; // `\b` won't match between two digits, so a longer run like an order number // never yields a false positive. const CODE_PATTERN = /\b\d{6}\b/; /** Pulls the 6-digit code out of whatever the user copied from the email. */ export const extractCode = (raw: string | null | undefined): string | null => raw ? (CODE_PATTERN.exec(raw)?.[0] ?? null) : null; type OtpFieldProps = { label?: string; value: string; onChange: (code: string) => void; /** Fired once the field holds a complete 6-digit code. */ onComplete?: (code: string) => void; }; /** * Code entry for the emailed verification/reset codes. * * Three ways in, cheapest first: * 1. iOS surfaces the code above the keyboard once Mail has it — * `textContentType="oneTimeCode"` is what opts the field into that. * 2. Gmail's notification carries a "Copy code" action (Android) and the * code is one long-press away on any platform: coming back to the app * with a code on the clipboard raises the paste chip below. * 3. Typing it. */ export const OtpField = ({ label = tr("components.otp.code"), value, onChange, onComplete, }: OtpFieldProps) => { const [pasteReady, setPasteReady] = useState(false); const completedFor = useRef(null); // `hasStringAsync` inspects the clipboard without reading it, so it never // trips the iOS paste prompt — that only fires on the explicit tap below. const refreshPasteChip = useCallback(async () => { try { setPasteReady(await Clipboard.hasStringAsync()); } catch { setPasteReady(false); } }, []); useEffect(() => { void refreshPasteChip(); // The user leaves for Gmail and comes back with the code copied. const subscription = AppState.addEventListener("change", (state) => { if (state === "active") void refreshPasteChip(); }); return () => subscription.remove(); }, [refreshPasteChip]); const handleChange = useCallback( (next: string) => { // Paste of a whole line ("123456 is your Waseel…") still lands the code. const digits = next.length > 6 ? (extractCode(next) ?? next.replace(/\D/g, "").slice(0, 6)) : next.replace(/\D/g, ""); onChange(digits); }, [onChange], ); const onPastePress = useCallback(async () => { try { const code = extractCode(await Clipboard.getStringAsync()); if (code) { onChange(code); return; } } catch { // Fall through to the hint below. } setPasteReady(false); }, [onChange]); // Auto-submit on a complete code, but only once per distinct code so a // rejected code isn't resubmitted on every re-render. useEffect(() => { if (value.length !== 6 || !onComplete) return; if (completedFor.current === value) return; completedFor.current = value; onComplete(value); }, [value, onComplete]); return ( {pasteReady && value.length < 6 ? ( {tr("components.otp.pasteCode")} ) : null} ); };