import React, { FC, forwardRef, useContext, useEffect, useImperativeHandle, useRef, useState, } from 'react'; import { Animated, PixelRatio, TextInput, TextInputFocusEvent, TouchableOpacity, View, } from 'react-native'; import { useComponentId } from '../Application'; import { Spacing, Styles } from '../Consts'; import { useScaleSize, Text } from '../Text'; import { ErrorView, getBorderColor } from './common'; import { CaretProps, InputOTPProps } from './index'; import styles from './styles'; import { Icon } from '../Icon'; import SystemTextInput from './SystemTextInput'; import { ApplicationContext, ComponentContext, MiniAppContext, } from '../Context'; const OTPCaret: FC = ({ index, length }) => { const DURATION = 300; const { theme } = useContext(ApplicationContext); const opacity = useRef(new Animated.Value(0)).current; useEffect(() => { Animated.loop( Animated.sequence([ Animated.timing(opacity, { toValue: 1, duration: DURATION, useNativeDriver: true, }), Animated.delay(DURATION * 2), Animated.timing(opacity, { toValue: 0, duration: DURATION, useNativeDriver: true, }), ]), ).start(); }, [opacity]); const spacingStyle = !isNaN(Number(length)) && index !== Number(length) - 1 && { marginRight: Spacing.L }; return ( - ); }; const InputOTP = forwardRef( ( { length, errorMessage, errorSpacing, floatingValue = 'Label', placeholder, onChangeText, onBlur, onFocus, dataType = 'number', params, style, hintText, ...props }: InputOTPProps, ref, ) => { const MAX_LENGTH = 10; const [value, setValue] = useState(''); const [focused, setFocused] = useState(false); const inputRef = useRef(null); const { theme } = useContext(ApplicationContext); const context = useContext(MiniAppContext); const componentName = 'InputOTP'; const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false; const { componentId } = useComponentId( `${componentName}/${placeholder}`, props.accessibilityLabel, ); useImperativeHandle(ref, () => ({ onChangeText: (text: string) => { _onChangeText(text); }, focus: () => inputRef.current?.focus(), blur: () => inputRef.current?.blur(), setText: (text: string) => _onChangeText(text), })); const onFocusInput = (e: TextInputFocusEvent) => { setFocused(true); onFocus?.(e); }; const onBlurInput = (e: TextInputFocusEvent) => { setFocused(false); onBlur?.(e); }; const _onChangeText = (text: string) => { if (text.length > MAX_LENGTH) return; if (dataType === 'number' && isNaN(Number(text))) return; if ( !isNaN(Number(text)) && value.length >= Number(length) && text.length >= value.length ) return; setValue(text); onChangeText?.(text); }; const onClearText = () => { _onChangeText(''); }; const fontScale = PixelRatio.getFontScale(); const renderInputs = (inputLength: number) => { const TextInputs: React.ReactNode[] = []; for (let i = 0; i < inputLength; i++) { TextInputs.push( {focused && value.length === i ? ( ) : ( {value[i] || '-'} )} , ); } return TextInputs; }; const renderUnidentifiedInputs = () => { const splitValue = value.split(''); if (!focused && value.length === 0 && !!placeholder) { return ( {placeholder} ); } return ( {splitValue.map((letter, index) => { return ( 1 && value.length > 6 ? Spacing.M : Spacing.L, } } typography={'header_m_bold'} > {letter} ); })} {focused && value.length !== MAX_LENGTH && } ); }; const renderInputView = () => { return ( inputRef.current?.focus()} style={[ styles.otpInput, { backgroundColor: theme.colors.background.surface }, getBorderColor(theme, focused, errorMessage), ]} > {!!floatingValue && ( {floatingValue} )} {value.length > 0 && focused && ( )} {length ? renderInputs(length) : renderUnidentifiedInputs()} ); }; return ( {renderInputView()} ); }, ); export default InputOTP;