@@ -114,44 +114,52 @@ function MagicCodeInput(
114
114
) {
115
115
const styles = useThemeStyles ( ) ;
116
116
const StyleUtils = useStyleUtils ( ) ;
117
- const inputRefs = useRef < BaseTextInputRef | null > ( ) ;
117
+ const inputRef = useRef < BaseTextInputRef | null > ( ) ;
118
118
const [ input , setInput ] = useState ( TEXT_INPUT_EMPTY_STATE ) ;
119
119
const [ focusedIndex , setFocusedIndex ] = useState < number | undefined > ( 0 ) ;
120
- const [ editIndex , setEditIndex ] = useState ( 0 ) ;
120
+ const editIndex = useRef ( 0 ) ;
121
121
const [ wasSubmitted , setWasSubmitted ] = useState ( false ) ;
122
122
const shouldFocusLast = useRef ( false ) ;
123
123
const inputWidth = useRef ( 0 ) ;
124
124
const lastFocusedIndex = useRef ( 0 ) ;
125
125
const lastValue = useRef < string | number > ( TEXT_INPUT_EMPTY_STATE ) ;
126
+ const valueRef = useRef ( value ) ;
126
127
127
128
useEffect ( ( ) => {
128
129
lastValue . current = input . length ;
129
130
} , [ input ] ) ;
130
131
132
+ useEffect ( ( ) => {
133
+ // Note: there are circumstances where the value state isn't updated yet
134
+ // when e.g. onChangeText gets called the next time. In those cases its safer to access the value from a ref
135
+ // to not have outdated values.
136
+ valueRef . current = value ;
137
+ } , [ value ] ) ;
138
+
131
139
const blurMagicCodeInput = ( ) => {
132
- inputRefs . current ?. blur ( ) ;
140
+ inputRef . current ?. blur ( ) ;
133
141
setFocusedIndex ( undefined ) ;
134
142
} ;
135
143
136
144
const focusMagicCodeInput = ( ) => {
137
145
setFocusedIndex ( 0 ) ;
138
146
lastFocusedIndex . current = 0 ;
139
- setEditIndex ( 0 ) ;
140
- inputRefs . current ?. focus ( ) ;
147
+ editIndex . current = 0 ;
148
+ inputRef . current ?. focus ( ) ;
141
149
} ;
142
150
143
151
const setInputAndIndex = ( index : number ) => {
144
152
setInput ( TEXT_INPUT_EMPTY_STATE ) ;
145
153
setFocusedIndex ( index ) ;
146
- setEditIndex ( index ) ;
154
+ editIndex . current = index ;
147
155
} ;
148
156
149
157
useImperativeHandle ( ref , ( ) => ( {
150
158
focus ( ) {
151
159
focusMagicCodeInput ( ) ;
152
160
} ,
153
161
focusLastSelected ( ) {
154
- inputRefs . current ?. focus ( ) ;
162
+ inputRef . current ?. focus ( ) ;
155
163
} ,
156
164
resetFocus ( ) {
157
165
setInput ( TEXT_INPUT_EMPTY_STATE ) ;
@@ -160,7 +168,7 @@ function MagicCodeInput(
160
168
clear ( ) {
161
169
lastFocusedIndex . current = 0 ;
162
170
setInputAndIndex ( 0 ) ;
163
- inputRefs . current ?. focus ( ) ;
171
+ inputRef . current ?. focus ( ) ;
164
172
onChangeTextProp ( '' ) ;
165
173
} ,
166
174
blur ( ) {
@@ -219,7 +227,7 @@ function MagicCodeInput(
219
227
// TapGestureHandler works differently on mobile web and native app
220
228
// On web gesture handler doesn't block interactions with textInput below so there is no need to run `focus()` manually
221
229
if ( ! Browser . isMobileChrome ( ) && ! Browser . isMobileSafari ( ) ) {
222
- inputRefs . current ?. focus ( ) ;
230
+ inputRef . current ?. focus ( ) ;
223
231
}
224
232
setInputAndIndex ( index ) ;
225
233
lastFocusedIndex . current = index ;
@@ -231,6 +239,12 @@ function MagicCodeInput(
231
239
* the focused input on the next empty one, if exists.
232
240
* It handles both fast typing and only one digit at a time
233
241
* in a specific position.
242
+ *
243
+ * Note: this works under the assumption that the backing text input will always have a cleared text,
244
+ * and entering text will exactly call onChangeText with one new character/digit.
245
+ * When the OS is inserting one time passwords for example it will call this method successively with one more digit each time.
246
+ * Thus, this method relies on an internal value ref to make sure to always use the latest value (as state updates are async, and
247
+ * might happen later than the next call to onChangeText).
234
248
*/
235
249
const onChangeText = ( textValue ?: string ) => {
236
250
if ( ! textValue ?. length || ! ValidationUtils . isNumeric ( textValue ) ) {
@@ -249,16 +263,17 @@ function MagicCodeInput(
249
263
const numbersArr = addedValue
250
264
. trim ( )
251
265
. split ( '' )
252
- . slice ( 0 , maxLength - editIndex ) ;
253
- const updatedFocusedIndex = Math . min ( editIndex + ( numbersArr . length - 1 ) + 1 , maxLength - 1 ) ;
266
+ . slice ( 0 , maxLength - editIndex . current ) ;
267
+ const updatedFocusedIndex = Math . min ( editIndex . current + ( numbersArr . length - 1 ) + 1 , maxLength - 1 ) ;
254
268
255
- let numbers = decomposeString ( value , maxLength ) ;
256
- numbers = [ ...numbers . slice ( 0 , editIndex ) , ...numbersArr , ...numbers . slice ( numbersArr . length + editIndex , maxLength ) ] ;
269
+ let numbers = decomposeString ( valueRef . current , maxLength ) ;
270
+ numbers = [ ...numbers . slice ( 0 , editIndex . current ) , ...numbersArr , ...numbers . slice ( numbersArr . length + editIndex . current , maxLength ) ] ;
257
271
258
272
setInputAndIndex ( updatedFocusedIndex ) ;
259
273
260
274
const finalInput = composeToString ( numbers ) ;
261
275
onChangeTextProp ( finalInput ) ;
276
+ valueRef . current = finalInput ;
262
277
} ;
263
278
264
279
/**
@@ -275,12 +290,13 @@ function MagicCodeInput(
275
290
// If keyboard is disabled and no input is focused we need to remove
276
291
// the last entered digit and focus on the correct input
277
292
if ( isDisableKeyboard && focusedIndex === undefined ) {
278
- const indexBeforeLastEditIndex = editIndex === 0 ? editIndex : editIndex - 1 ;
293
+ const curEditIndex = editIndex . current ;
294
+ const indexBeforeLastEditIndex = curEditIndex === 0 ? curEditIndex : curEditIndex - 1 ;
279
295
280
- const indexToFocus = numbers . at ( editIndex ) === CONST . MAGIC_CODE_EMPTY_CHAR ? indexBeforeLastEditIndex : editIndex ;
296
+ const indexToFocus = numbers . at ( curEditIndex ) === CONST . MAGIC_CODE_EMPTY_CHAR ? indexBeforeLastEditIndex : curEditIndex ;
281
297
if ( indexToFocus !== undefined ) {
282
298
lastFocusedIndex . current = indexToFocus ;
283
- inputRefs . current ?. focus ( ) ;
299
+ inputRef . current ?. focus ( ) ;
284
300
}
285
301
onChangeTextProp ( value . substring ( 0 , indexToFocus ) ) ;
286
302
@@ -292,7 +308,7 @@ function MagicCodeInput(
292
308
if ( focusedIndex !== undefined && numbers ?. at ( focusedIndex ) !== CONST . MAGIC_CODE_EMPTY_CHAR ) {
293
309
setInput ( TEXT_INPUT_EMPTY_STATE ) ;
294
310
numbers = [ ...numbers . slice ( 0 , focusedIndex ) , CONST . MAGIC_CODE_EMPTY_CHAR , ...numbers . slice ( focusedIndex + 1 , maxLength ) ] ;
295
- setEditIndex ( focusedIndex ) ;
311
+ editIndex . current = focusedIndex ;
296
312
onChangeTextProp ( composeToString ( numbers ) ) ;
297
313
return ;
298
314
}
@@ -318,17 +334,17 @@ function MagicCodeInput(
318
334
319
335
if ( newFocusedIndex !== undefined ) {
320
336
lastFocusedIndex . current = newFocusedIndex ;
321
- inputRefs . current ?. focus ( ) ;
337
+ inputRef . current ?. focus ( ) ;
322
338
}
323
339
}
324
340
if ( keyValue === 'ArrowLeft' && focusedIndex !== undefined ) {
325
341
const newFocusedIndex = Math . max ( 0 , focusedIndex - 1 ) ;
326
342
setInputAndIndex ( newFocusedIndex ) ;
327
- inputRefs . current ?. focus ( ) ;
343
+ inputRef . current ?. focus ( ) ;
328
344
} else if ( keyValue === 'ArrowRight' && focusedIndex !== undefined ) {
329
345
const newFocusedIndex = Math . min ( focusedIndex + 1 , maxLength - 1 ) ;
330
346
setInputAndIndex ( newFocusedIndex ) ;
331
- inputRefs . current ?. focus ( ) ;
347
+ inputRef . current ?. focus ( ) ;
332
348
} else if ( keyValue === 'Enter' ) {
333
349
// We should prevent users from submitting when it's offline.
334
350
if ( isOffline ) {
@@ -340,7 +356,7 @@ function MagicCodeInput(
340
356
const newFocusedIndex = ( event as unknown as KeyboardEvent ) . shiftKey ? focusedIndex - 1 : focusedIndex + 1 ;
341
357
if ( newFocusedIndex >= 0 && newFocusedIndex < maxLength ) {
342
358
setInputAndIndex ( newFocusedIndex ) ;
343
- inputRefs . current ?. focus ( ) ;
359
+ inputRef . current ?. focus ( ) ;
344
360
if ( event ?. preventDefault ) {
345
361
event . preventDefault ( ) ;
346
362
}
@@ -383,7 +399,7 @@ function MagicCodeInput(
383
399
onLayout = { ( e ) => {
384
400
inputWidth . current = e . nativeEvent . layout . width ;
385
401
} }
386
- ref = { ( inputRef ) => ( inputRefs . current = inputRef ) }
402
+ ref = { ( newRef ) => ( inputRef . current = newRef ) }
387
403
autoFocus = { autoFocus }
388
404
inputMode = "numeric"
389
405
textContentType = "oneTimeCode"
0 commit comments