1
- import React , { ReactNode , RefObject , Component } from 'react' ;
1
+ import React , {
2
+ RefObject ,
3
+ forwardRef ,
4
+ useCallback ,
5
+ useEffect ,
6
+ useImperativeHandle ,
7
+ useReducer ,
8
+ useRef ,
9
+ } from 'react' ;
2
10
import {
3
11
Clipboard ,
4
12
Keyboard ,
5
13
StyleProp ,
6
14
TextInput ,
15
+ TextInputProps ,
7
16
TextStyle ,
8
17
View ,
9
18
ViewStyle ,
10
- TextInputProps ,
11
19
} from 'react-native' ;
12
20
13
21
import OtpInput from './OtpInput' ;
22
+ import { ActionTypes , OtpInputsRef , Actions } from './types' ;
23
+ import { fillOtpCode } from './helpers' ;
14
24
15
25
type Props = TextInputProps & {
16
26
styles ?: StyleProp < ViewStyle > ;
@@ -23,164 +33,184 @@ type Props = TextInputProps & {
23
33
testIDPrefix : string ;
24
34
} ;
25
35
26
- type State = {
27
- otpCode : Array < string > ;
28
- previousCopiedText ?: string ;
36
+ const ACTION_TYPES : ActionTypes = {
37
+ setOtpTextForIndex : 'setOtpTextForIndex' ,
38
+ setOtpCode : 'setOtpCode' ,
39
+ clearOtp : 'clearOtp' ,
29
40
} ;
30
41
31
- const MINIMAL_INDEX = 0 ;
32
-
33
- class OtpInputs extends Component < Props , State > {
34
- constructor ( props : Props ) {
35
- super ( props ) ;
36
-
37
- const inputs = [ ] ;
38
-
39
- for ( let index = 0 ; index < this . props . numberOfInputs ; index ++ ) {
40
- inputs [ index ] = React . createRef < TextInput > ( ) ;
42
+ const reducer = ( state : { [ key : string ] : string } , action : Actions ) => {
43
+ switch ( action . type ) {
44
+ case ACTION_TYPES . setOtpTextForIndex : {
45
+ return {
46
+ [ `${ action . payload . index } ` ] : action . payload . text ,
47
+ } ;
41
48
}
42
49
43
- this . _interval = undefined ;
44
- this . inputs = inputs as Array < RefObject < TextInput > > ;
45
- this . state = {
46
- previousCopiedText : '' ,
47
- otpCode : [ ] ,
48
- } ;
49
- }
50
-
51
- componentDidMount ( ) : void {
52
- this . _listenOnCopiedText ( ) ;
53
-
54
- this . _interval = setInterval ( ( ) => {
55
- this . _listenOnCopiedText ( ) ;
56
- } , 1000 ) ;
57
- }
58
-
59
- componentWillUnmount ( ) : void {
60
- clearInterval ( this . _interval ) ;
61
- }
62
-
63
- inputs : Array < RefObject < TextInput > > ;
64
- _interval : any ;
65
-
66
- reset = ( ) : void => {
67
- this . setState ( { otpCode : [ ] } ) ;
68
- this . props . handleChange ( '' ) ;
69
- this . inputs . forEach ( i => i . current ! . clear ( ) ) ;
70
- } ;
71
-
72
- _listenOnCopiedText = async ( ) : Promise < void > => {
73
- const { numberOfInputs } = this . props ;
74
- const { otpCode, previousCopiedText } = this . state ;
75
- const copiedText = await Clipboard . getString ( ) ;
76
-
77
- if (
78
- copiedText &&
79
- copiedText . length === numberOfInputs &&
80
- copiedText !== otpCode . join ( '' ) &&
81
- copiedText !== previousCopiedText
82
- ) {
83
- clearInterval ( this . _interval ) ;
84
- this . _handleAfterOtpAction ( copiedText . split ( '' ) , numberOfInputs , true ) ;
85
- }
86
- } ;
87
-
88
- _handleAfterOtpAction = (
89
- otpCode : Array < string > ,
90
- indexToFocus : number ,
91
- fromClipboard ?: boolean ,
92
- ) : void => {
93
- const { handleChange, numberOfInputs } = this . props ;
94
- handleChange ( otpCode . join ( '' ) ) ;
95
-
96
- this . setState ( {
97
- otpCode,
98
- ...( fromClipboard && { previousCopiedText : otpCode . join ( '' ) } ) ,
99
- } ) ;
100
-
101
- if ( indexToFocus === numberOfInputs ) {
102
- return Keyboard . dismiss ( ) ;
103
- }
104
-
105
- if ( indexToFocus >= MINIMAL_INDEX && indexToFocus < numberOfInputs ) {
106
- this . _focusInput ( indexToFocus ) ;
107
- }
108
- } ;
109
-
110
- _handleTextChange = ( text : string , index : number ) : void => {
111
- const { numberOfInputs } = this . props ;
112
-
113
- if ( ! text . length ) {
114
- this . inputs [ index ] . current ! . clear ( ) ;
115
- return this . _focusInput ( index - 1 ) ;
50
+ case ACTION_TYPES . setOtpCode : {
51
+ return fillOtpCode ( action . payload . numberOfInputs , action . payload . code ) ;
116
52
}
117
53
118
- if ( text . length === numberOfInputs ) {
119
- this . _handleAfterOtpAction ( text . split ( '' ) , numberOfInputs , true ) ;
120
- } else if ( text ) {
121
- let otpArray = this . state . otpCode ;
122
- otpArray [ index ] = text [ text . length - 1 ] ;
123
- this . _handleAfterOtpAction ( otpArray , index + 1 ) ;
54
+ case ACTION_TYPES . clearOtp : {
55
+ return fillOtpCode ( action . payload ) ;
124
56
}
125
- } ;
126
57
127
- _focusInput = ( index : number ) : void => {
128
- if ( index >= 0 && index < this . props . numberOfInputs ) {
129
- this . inputs [ index ] . current ! . focus ( ) ;
130
- }
131
- } ;
58
+ default :
59
+ return state ;
60
+ }
61
+ } ;
132
62
133
- _renderInputs = ( ) : Array < JSX . Element > => {
134
- const {
63
+ const OtpInputs = forwardRef < OtpInputsRef , Props > (
64
+ (
65
+ {
135
66
autoCapitalize,
136
67
clearTextOnFocus,
137
68
focusStyles,
138
69
inputContainerStyles,
139
70
inputStyles,
71
+ isRTL,
140
72
keyboardType,
141
73
numberOfInputs,
74
+ placeholder,
142
75
secureTextEntry,
143
76
selectTextOnFocus,
77
+ styles,
144
78
testIDPrefix,
145
- isRTL,
146
- placeholder,
147
- } = this . props ;
148
- const { otpCode } = this . state ;
149
- const iterationArray = Array < number > ( numberOfInputs ) . fill ( 0 ) ;
150
-
151
- return iterationArray . map ( ( _ , index ) => {
152
- let inputIndex = index ;
153
- if ( isRTL ) {
154
- inputIndex = numberOfInputs - 1 - index ;
79
+ } ,
80
+ ref ,
81
+ ) => {
82
+ const [ otpCode , dispatch ] = useReducer ( reducer , numberOfInputs , fillOtpCode ) ;
83
+ const previousCopiedText : { current : string } = useRef ( '' ) ;
84
+ const inputs : { current : Array < RefObject < TextInput > > } = useRef ( [ ] ) ;
85
+
86
+ const handleInputTextChange = ( { text, index } : { text : string ; index : number } ) => {
87
+ dispatch ( {
88
+ type : ACTION_TYPES . setOtpTextForIndex ,
89
+ payload : {
90
+ text,
91
+ index,
92
+ } ,
93
+ } ) ;
94
+ focusInput ( index + 1 ) ;
95
+ } ;
96
+
97
+ const handleTextChange = ( text : string , index : number ) : void => {
98
+ if ( ! text . length ) {
99
+ handleClearInput ( index ) ;
155
100
}
156
101
157
- return (
158
- < OtpInput
159
- autoCapitalize = { autoCapitalize }
160
- clearTextOnFocus = { clearTextOnFocus }
161
- firstInput = { index === 0 }
162
- focusStyles = { focusStyles }
163
- handleTextChange = { ( text : string ) => this . _handleTextChange ( text , inputIndex ) }
164
- inputContainerStyles = { inputContainerStyles }
165
- inputStyles = { inputStyles }
166
- key = { inputIndex }
167
- keyboardType = { keyboardType }
168
- numberOfInputs = { numberOfInputs }
169
- placeholder = { placeholder }
170
- ref = { this . inputs [ inputIndex ] }
171
- secureTextEntry = { secureTextEntry }
172
- selectTextOnFocus = { selectTextOnFocus }
173
- testID = { `${ testIDPrefix } -${ inputIndex } ` }
174
- value = { otpCode [ inputIndex ] }
175
- />
176
- ) ;
177
- } ) ;
178
- } ;
179
-
180
- render ( ) : ReactNode {
181
- return < View style = { this . props . styles } > { this . _renderInputs ( ) } </ View > ;
182
- }
183
- }
102
+ if ( text ) {
103
+ handleInputTextChange ( { text, index } ) ;
104
+ }
105
+
106
+ if ( index === numberOfInputs - 1 && text ) {
107
+ Keyboard . dismiss ( ) ;
108
+ }
109
+ } ;
110
+
111
+ const focusInput = useCallback (
112
+ ( index : number ) : void => {
113
+ if ( index >= 0 && index < numberOfInputs ) {
114
+ const input = inputs . current [ index ] ;
115
+ input && input . current && input . current . focus ( ) ;
116
+ }
117
+ } ,
118
+ [ numberOfInputs ] ,
119
+ ) ;
120
+
121
+ const handleClearInput = useCallback (
122
+ ( inputIndex : number ) => {
123
+ const input = inputs . current [ inputIndex ] ;
124
+ input && input . current && input . current . clear ( ) ;
125
+ focusInput ( inputIndex - 1 ) ;
126
+ } ,
127
+ [ focusInput ] ,
128
+ ) ;
129
+
130
+ const fillInputs = useCallback (
131
+ ( code : string ) => {
132
+ dispatch ( { type : ACTION_TYPES . setOtpCode , payload : { numberOfInputs, code } } ) ;
133
+ } ,
134
+ [ numberOfInputs ] ,
135
+ ) ;
136
+
137
+ useImperativeHandle (
138
+ ref ,
139
+ ( ) => ( {
140
+ reset : ( ) : void => {
141
+ dispatch ( { type : ACTION_TYPES . clearOtp , payload : numberOfInputs } ) ;
142
+ inputs . current . forEach ( input => input && input . current && input . current . clear ( ) ) ;
143
+ previousCopiedText . current = '' ;
144
+ } ,
145
+ } ) ,
146
+ [ numberOfInputs ] ,
147
+ ) ;
148
+
149
+ const listenOnCopiedText = useCallback ( async ( ) : Promise < void > => {
150
+ const copiedText = await Clipboard . getString ( ) ;
151
+ const otpCodeValue = Object . values ( otpCode ) . join ( '' ) ;
152
+
153
+ if (
154
+ copiedText &&
155
+ copiedText . length === numberOfInputs &&
156
+ copiedText !== otpCodeValue &&
157
+ copiedText !== previousCopiedText . current
158
+ ) {
159
+ previousCopiedText . current = otpCodeValue ;
160
+ fillInputs ( copiedText ) ;
161
+ }
162
+ } , [ fillInputs , numberOfInputs , otpCode ] ) ;
163
+
164
+ useEffect ( ( ) => {
165
+ const interval = setInterval ( ( ) => {
166
+ listenOnCopiedText ( ) ;
167
+ } , 1000 ) ;
168
+
169
+ return ( ) => {
170
+ clearInterval ( interval ) ;
171
+ } ;
172
+ } , [ listenOnCopiedText , numberOfInputs ] ) ;
173
+
174
+ const renderInputs = ( ) : Array < JSX . Element > => {
175
+ const iterationArray = Array < number > ( numberOfInputs ) . fill ( 0 ) ;
176
+
177
+ return iterationArray . map ( ( _ , index ) => {
178
+ let inputIndex = index ;
179
+ if ( isRTL ) {
180
+ inputIndex = numberOfInputs - 1 - index ;
181
+ }
182
+ const inputValue = otpCode [ `${ inputIndex } ` ] ;
183
+
184
+ if ( ! inputs . current [ inputIndex ] ) {
185
+ inputs . current [ inputIndex ] = React . createRef < TextInput > ( ) ;
186
+ }
187
+
188
+ return (
189
+ < OtpInput
190
+ autoCapitalize = { autoCapitalize }
191
+ clearTextOnFocus = { clearTextOnFocus }
192
+ firstInput = { index === 0 }
193
+ focusStyles = { focusStyles }
194
+ handleTextChange = { ( text : string ) => handleTextChange ( text , inputIndex ) }
195
+ inputContainerStyles = { inputContainerStyles }
196
+ inputStyles = { inputStyles }
197
+ key = { inputIndex }
198
+ keyboardType = { keyboardType }
199
+ numberOfInputs = { numberOfInputs }
200
+ placeholder = { placeholder }
201
+ ref = { inputs . current [ inputIndex ] }
202
+ secureTextEntry = { secureTextEntry }
203
+ selectTextOnFocus = { selectTextOnFocus }
204
+ testID = { `${ testIDPrefix } -${ inputIndex } ` }
205
+ inputValue = { inputValue }
206
+ />
207
+ ) ;
208
+ } ) ;
209
+ } ;
210
+
211
+ return < View style = { styles } > { renderInputs ( ) } </ View > ;
212
+ } ,
213
+ ) ;
184
214
185
215
export default OtpInputs ;
186
216
0 commit comments