From 29bf8b9cb9135ff20616aa270deacd7aa899d8ba Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Tue, 14 Mar 2023 17:17:07 +0000 Subject: [PATCH 01/25] feat: magic code input component --- src/CONST.js | 2 + src/components/MagicCodeInput.js | 214 ++++++++++++++++++ .../ValidateCodeForm/BaseValidateCodeForm.js | 5 +- src/stories/MagicCodeInput.stories.js | 30 +++ src/styles/utilities/sizing.js | 4 + 5 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 src/components/MagicCodeInput.js create mode 100644 src/stories/MagicCodeInput.stories.js diff --git a/src/CONST.js b/src/CONST.js index 418d5bc1ad6b..f0e9f85b5409 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -537,6 +537,8 @@ const CONST = { EMAIL: 'email', }, + MAGIC_CODE_NUMBERS: 6, + KEYBOARD_TYPE: { PHONE_PAD: 'phone-pad', NUMBER_PAD: 'number-pad', diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js new file mode 100644 index 000000000000..769cfb64e9fa --- /dev/null +++ b/src/components/MagicCodeInput.js @@ -0,0 +1,214 @@ +import React from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import styles from '../styles/styles'; +import CONST from '../CONST'; +import TextInput from './TextInput'; + +const propTypes = { + /** Name attribute for the input */ + name: PropTypes.string, + + /** Input value */ + value: PropTypes.string, + + /** Should the input auto focus? */ + autoFocus: PropTypes.bool, + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus: PropTypes.bool, + + /** A ref to forward the current input */ + forwardedRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({current: PropTypes.instanceOf(React.Component)}), + ]), + + /** Function to call when the input is changed */ + onChange: PropTypes.func, + + /** Function to call when the input is submitted or fully complete */ + onSubmit: PropTypes.func, +}; + +const defaultProps = { + value: undefined, + name: '', + autoFocus: true, + forwardedRef: undefined, + shouldDelayFocus: false, + onChange: () => {}, + onSubmit: () => {}, +}; + +class MagicCodeInput extends React.PureComponent { + constructor(props) { + super(props); + + this.inputRefs = {}; + this.state = { + focusedIndex: 0, + numbers: props.value ? this._decomposeString(props.value) : [] + }; + } + + componentDidMount() { + if (!this.props.autoFocus) { + return; + } + + let focusElIndex = _.findIndex(this.state.numbers, e => e === undefined); + focusElIndex = focusElIndex === -1 ? 0 : focusElIndex; + + if (this.props.shouldDelayFocus) { + this.focusTimeout = setTimeout(() => this.updateFocus(focusElIndex), CONST.ANIMATED_TRANSITION); + return; + } + this.updateFocus(focusElIndex); + } + + _decomposeString(value) { + return value.trim().split(''); + } + + _composeToString(value) { + return _.filter(value, v => v !== undefined).join(''); + } + + /** + * Focuses on the input at the given index and updates the + * forwarded reference. + * + * @param {number} index The index of the input to focus on. + * @returns {void} + */ + updateFocus(index) { + if (index >= Object.keys(this.inputRefs).length) { + return; + } + if (!this.inputRefs[index]) { + return; + } + + this.inputRefs[index].focus(); + + // Update the ref with the currently focused input + if (!this.props.forwardedRef) { + return; + } + this.props.forwardedRef(this.inputRefs[index]); + } + + /** + * Updates the input value on the given index. + * + * @param {string} value The value written in the input. + * @param {*} index The index of the input changed. + * @param {*} callback Function to be executed after the state changes. + */ + updateValue(value, index, callback) { + this.setState(prevState => { + const numbers = [...prevState.numbers]; + numbers[index] = value; + return { ...prevState, numbers, focusedIndex: index }; + }, callback); + } + + /** + * Updates the value of the input and focus on the next one, + * taking into consideration if the value added is bigger + * than one character (quick typing or pasting content). + * + * @param {string} value The value written in the input + * @param {number} index The index of the input changed + * @returns {void} + */ + onChange(value, index) { + if (_.isUndefined(value) || _.isEmpty(value)) { + return; + } + + // If there's a bigger value written, for example by pasting the input, + // it spreads the string content through all the inputs and focus on the last + // input with a value or the next empty input, if it exists + if (value.length > 1) { + const numbersArr = this._decomposeString(value); + this.setState(prevState => { + const numbers = [...prevState.numbers.slice(0, index), ...numbersArr.slice(0, CONST.MAGIC_CODE_NUMBERS - index)]; + return { numbers }; + }, () => { + this.updateFocus(Math.min(index + numbersArr.length, Object.keys(this.inputRefs).length - 1)); + this.props.onChange(this._composeToString(this.state.numbers)); + }); + + } else { + this.updateValue(value, index, () => { + this.updateFocus(index + 1); + this.props.onChange(this._composeToString(this.state.numbers)); + }); + } + } + + /** + * Handles logic related to certain key presses (Backspace, Enter, + * ArrowLeft, ArrowRight) that cause value and focus change. + * + * @param {*} event The event passed by the key press + * @param {*} index The index of the input where the event occurred + */ + onKeyPress({nativeEvent: {key: keyValue}}, index) { + // Deletes the existing number on the focused input, + // if there is no number, it will delete the previous input + if (keyValue === 'Backspace') { + if (!_.isUndefined(this.state.numbers[index])) { + this.updateValue(undefined, index); + } else { + this.updateValue(undefined, index - 1, () => this.updateFocus(this.state.focusedIndex)); + } + } else if (keyValue === 'Enter') { + this.props.onSubmit(this.state.numbers.join('')); + } else if (keyValue === 'ArrowLeft') { + this.setState(prevState => ({ ...prevState, focusedIndex: index - 1}), () => this.updateFocus(this.state.focusedIndex)); + } else if (keyValue === 'ArrowRight') { + this.setState(prevState => ({ ...prevState, focusedIndex: index + 1}), () => this.updateFocus(this.state.focusedIndex)); + } + } + + render() { + return ( + + {_.map(Array.from(Array(CONST.MAGIC_CODE_NUMBERS).keys()), i => ( + { this.inputRefs[i] = ref; }} + name={this.props.name} + + // Only allows one character to be entered on the last input + // so that there's no flickering when more than one number + // is added + maxLength={i === CONST.MAGIC_CODE_NUMBERS - 1 ? 1 : 1} + blurOnSubmit={false} + keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + value={this.state.numbers[i] === undefined ? '' : this.state.numbers[i].toString()} + inputStyle={[styles.iouAmountTextInput, styles.p1, styles.textAlignCenter, styles.w15]} + onFocus={() => this.updateFocus(i)} + onChangeText={value => this.onChange(value, i)} + onKeyPress={ev => this.onKeyPress(ev, i)} + /> + ))} + + ) + } +} + +MagicCodeInput.propTypes = propTypes; +MagicCodeInput.defaultProps = defaultProps; +MagicCodeInput.displayName = 'MagicCodeInput'; + +export default React.forwardRef((props, ref) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + +)); diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 2e9b81d02189..d1955db1476a 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -25,6 +25,7 @@ import {withNetwork} from '../../../components/OnyxProvider'; import networkPropTypes from '../../../components/networkPropTypes'; import * as User from '../../../libs/actions/User'; import FormHelpMessage from '../../../components/FormHelpMessage'; +import MagicCodeInput from '../../../components/MagicCodeInput'; import Terms from '../Terms'; const propTypes = { @@ -206,7 +207,7 @@ class BaseValidateCodeForm extends React.Component { ) : ( - this.inputValidateCode = el} @@ -216,8 +217,6 @@ class BaseValidateCodeForm extends React.Component { value={this.state.validateCode} onChangeText={text => this.onTextInput(text, 'validateCode')} onSubmitEditing={this.validateAndSubmitForm} - blurOnSubmit={false} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} errorText={this.state.formError.validateCode ? this.props.translate(this.state.formError.validateCode) : ''} autoFocus /> diff --git a/src/stories/MagicCodeInput.stories.js b/src/stories/MagicCodeInput.stories.js new file mode 100644 index 000000000000..d424f685c428 --- /dev/null +++ b/src/stories/MagicCodeInput.stories.js @@ -0,0 +1,30 @@ +import React from 'react'; +import MagicCodeInput from '../components/MagicCodeInput'; + +/** + * We use the Component Story Format for writing stories. Follow the docs here: + * + * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format + */ +const story = { + title: 'Components/MagicCodeInput', + component: MagicCodeInput, +}; + +// eslint-disable-next-line react/jsx-props-no-spreading +const Template = args => ; + +// Arguments can be passed to the component by binding +// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args + +const AutoFocus = Template.bind({}); +AutoFocus.args = { + label: 'Auto-focused text input', + name: 'AutoFocus', + autoFocus: true, +}; + +export default story; +export { + AutoFocus, +}; diff --git a/src/styles/utilities/sizing.js b/src/styles/utilities/sizing.js index a55774756075..7ddc2f8f7141 100644 --- a/src/styles/utilities/sizing.js +++ b/src/styles/utilities/sizing.js @@ -8,6 +8,10 @@ export default { height: '100%', }, + w15: { + width: '15%', + }, + w20: { width: '20%', }, From 0bafd04f4b42aeeb031b7a332ef950ca4eb0625a Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Thu, 16 Mar 2023 14:00:48 +0000 Subject: [PATCH 02/25] feat: improve logic and performance for android --- src/components/MagicCodeInput.js | 264 +++++++++++------- src/components/TextInput/BaseTextInput.js | 6 +- .../TextInput/baseTextInputPropTypes.js | 4 + .../ValidateCodeForm/BaseValidateCodeForm.js | 4 +- src/stories/MagicCodeInput.stories.js | 11 +- 5 files changed, 182 insertions(+), 107 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 769cfb64e9fa..a67e7dc84875 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -1,10 +1,12 @@ import React from 'react'; -import {View} from 'react-native'; +import {Pressable, Text, StyleSheet, View, Platform} from 'react-native'; +import getPlatform from '../libs/getPlatform'; import PropTypes from 'prop-types'; import _ from 'underscore'; import styles from '../styles/styles'; import CONST from '../CONST'; import TextInput from './TextInput'; +import FormHelpMessage from './FormHelpMessage'; const propTypes = { /** Name attribute for the input */ @@ -13,7 +15,7 @@ const propTypes = { /** Input value */ value: PropTypes.string, - /** Should the input auto focus? */ + /** Should the input auto focus */ autoFocus: PropTypes.bool, /** Whether we should wait before focusing the TextInput, useful when using transitions */ @@ -25,6 +27,12 @@ const propTypes = { PropTypes.shape({current: PropTypes.instanceOf(React.Component)}), ]), + /** Error text to display */ + errorText: PropTypes.string, + + /* Should submit when the input is complete */ + submitOnComplete: PropTypes.bool, + /** Function to call when the input is changed */ onChange: PropTypes.func, @@ -38,168 +46,218 @@ const defaultProps = { autoFocus: true, forwardedRef: undefined, shouldDelayFocus: false, + submitOnComplete: false, onChange: () => {}, onSubmit: () => {}, }; +/** + * Verifies if a string is a number. + * + * @param {string} value The string to check if it's numeric. + * @returns {boolean} True if the string is numeric, false otherwise. + */ +function isNumeric(value) { + if (typeof value !== "string") return false; + return !Number.isNaN(value) && !Number.isNaN(parseFloat(value)); +} + class MagicCodeInput extends React.PureComponent { constructor(props) { super(props); - this.inputRefs = {}; + this.inputNrArray = Array.from(Array(CONST.MAGIC_CODE_NUMBERS).keys()); + this.inputRef = React.createRef(null); + this.state = { + input: '', focusedIndex: 0, - numbers: props.value ? this._decomposeString(props.value) : [] + editIndex: 0, + numbers: props.value ? this._decomposeString(props.value) : Array(CONST.MAGIC_CODE_NUMBERS).fill(''), }; + + this.onChangeText = this.onChangeText.bind(this); + this.onKeyPress = this.onKeyPress.bind(this); } componentDidMount() { - if (!this.props.autoFocus) { + if (!this.props.forwardedRef) { return; } + this.props.forwardedRef(this.inputRef); - let focusElIndex = _.findIndex(this.state.numbers, e => e === undefined); - focusElIndex = focusElIndex === -1 ? 0 : focusElIndex; + if (!this.props.autoFocus) { + return; + } if (this.props.shouldDelayFocus) { - this.focusTimeout = setTimeout(() => this.updateFocus(focusElIndex), CONST.ANIMATED_TRANSITION); + this.focusTimeout = setTimeout(() => this.inputRef.focus(), CONST.ANIMATED_TRANSITION); return; } - this.updateFocus(focusElIndex); + this.inputRef.focus(); } - _decomposeString(value) { - return value.trim().split(''); - } + componentDidUpdate(prevProps) { + if (prevProps.value === this.props.value || this.props.value === this._composeToString(this.state.numbers)) { + return; + } - _composeToString(value) { - return _.filter(value, v => v !== undefined).join(''); + this.setState({ + numbers: this._decomposeString(this.props.value) + }) } /** - * Focuses on the input at the given index and updates the - * forwarded reference. + * Converts a given string into an array of numbers that must have the same + * number of elements as the number of inputs. * - * @param {number} index The index of the input to focus on. - * @returns {void} + * @param {string} value The string to be converted into an array. + * @returns {array} The array of numbers. */ - updateFocus(index) { - if (index >= Object.keys(this.inputRefs).length) { - return; - } - if (!this.inputRefs[index]) { - return; + _decomposeString(value) { + let arr = value.trim().split('').slice(0, CONST.MAGIC_CODE_NUMBERS).map(v => isNumeric(v) ? v : ''); + if (arr.length < CONST.MAGIC_CODE_NUMBERS) { + arr = arr.concat(Array(CONST.MAGIC_CODE_NUMBERS - arr.length).fill('')); } + return arr; + } - this.inputRefs[index].focus(); - - // Update the ref with the currently focused input - if (!this.props.forwardedRef) { - return; - } - this.props.forwardedRef(this.inputRefs[index]); + _composeToString(value) { + return _.filter(value, v => v !== undefined).join(''); } /** - * Updates the input value on the given index. + * Focuses on the input when it is pressed. * - * @param {string} value The value written in the input. - * @param {*} index The index of the input changed. - * @param {*} callback Function to be executed after the state changes. + * @param {number} index The index of the input. */ - updateValue(value, index, callback) { - this.setState(prevState => { - const numbers = [...prevState.numbers]; - numbers[index] = value; - return { ...prevState, numbers, focusedIndex: index }; - }, callback); + onFocus(event, index) { + event.preventDefault(); + this.setState(prevState => ({...prevState, input: '', focusedIndex: index, editIndex: index})); + this.inputRef.focus(); } /** - * Updates the value of the input and focus on the next one, - * taking into consideration if the value added is bigger - * than one character (quick typing or pasting content). + * Updates the magic inputs with the contents written in the + * input. It spreads each number into each input and updates + * the focused input on the next empty one, if exists. + * It handles both fast typing and only one digit at a time + * in a specific position. * - * @param {string} value The value written in the input - * @param {number} index The index of the input changed - * @returns {void} + * @param {string} value The contents of the input text. */ - onChange(value, index) { - if (_.isUndefined(value) || _.isEmpty(value)) { + onChangeText(value) { + if (_.isUndefined(value) || _.isEmpty(value) || !isNumeric(value)) { return; } - // If there's a bigger value written, for example by pasting the input, - // it spreads the string content through all the inputs and focus on the last - // input with a value or the next empty input, if it exists - if (value.length > 1) { - const numbersArr = this._decomposeString(value); - this.setState(prevState => { - const numbers = [...prevState.numbers.slice(0, index), ...numbersArr.slice(0, CONST.MAGIC_CODE_NUMBERS - index)]; - return { numbers }; - }, () => { - this.updateFocus(Math.min(index + numbersArr.length, Object.keys(this.inputRefs).length - 1)); - this.props.onChange(this._composeToString(this.state.numbers)); - }); - - } else { - this.updateValue(value, index, () => { - this.updateFocus(index + 1); - this.props.onChange(this._composeToString(this.state.numbers)); - }); - } + const numbersArr = value.trim().split(''); + this.setState(prevState => { + let numbers = [ + ...prevState.numbers.slice(0, prevState.editIndex), + ...numbersArr.slice(0, CONST.MAGIC_CODE_NUMBERS - prevState.editIndex) + ]; + + // Updates the focused input taking into consideration the last input + // edited and the number of digits added by the user. + const focusedIndex = Math.min(prevState.editIndex + numbersArr.length - 1, CONST.MAGIC_CODE_NUMBERS - 1) + + return { numbers, focusedIndex, input: value, editIndex: prevState.editIndex }; + }, () => { + const finalInput = this._composeToString(this.state.numbers); + this.props.onChange(finalInput); + + if (this.props.submitOnComplete && finalInput.length === CONST.MAGIC_CODE_NUMBERS) { + this.props.onSubmit(finalInput); + } + }); } /** - * Handles logic related to certain key presses (Backspace, Enter, - * ArrowLeft, ArrowRight) that cause value and focus change. + * Handles logic related to certain key presses. + * + * NOTE: when using Android Emulator, this can only be tested using + * hardware keyboard inputs. * - * @param {*} event The event passed by the key press - * @param {*} index The index of the input where the event occurred + * @param {object} event The event passed by the key press. */ - onKeyPress({nativeEvent: {key: keyValue}}, index) { - // Deletes the existing number on the focused input, - // if there is no number, it will delete the previous input - if (keyValue === 'Backspace') { - if (!_.isUndefined(this.state.numbers[index])) { - this.updateValue(undefined, index); - } else { - this.updateValue(undefined, index - 1, () => this.updateFocus(this.state.focusedIndex)); - } - } else if (keyValue === 'Enter') { - this.props.onSubmit(this.state.numbers.join('')); + onKeyPress({nativeEvent: {key: keyValue}}) { + // Handles the delete character logic if the current input is less than 2 characters, + // meaning that it's the last character to be deleted or it's a character being + // deleted in the middle of the input, which should delete all the characters after it. + if (keyValue === 'Backspace' && this.state.input.length < 2) { + this.setState(prevState => { + const numbers = [...prevState.numbers]; + numbers[prevState.focusedIndex] = ''; + + return { + input: '', + numbers: numbers.slice(0, prevState.focusedIndex), + focusedIndex: prevState.focusedIndex === 0 ? 0 : prevState.focusedIndex - 1, + editIndex: prevState.editIndex === 0 ? 0 : prevState.editIndex - 1 + } + }); } else if (keyValue === 'ArrowLeft') { - this.setState(prevState => ({ ...prevState, focusedIndex: index - 1}), () => this.updateFocus(this.state.focusedIndex)); + this.setState(prevState => ({ + ...prevState, + input: '', + focusedIndex: prevState.focusedIndex - 1, + editIndex: prevState.focusedIndex - 1 + })); } else if (keyValue === 'ArrowRight') { - this.setState(prevState => ({ ...prevState, focusedIndex: index + 1}), () => this.updateFocus(this.state.focusedIndex)); + this.setState(prevState => ({ + ...prevState, + input: '', + focusedIndex: prevState.focusedIndex + 1, + editIndex: prevState.focusedIndex + 1 + })); + } else if (keyValue === 'Enter') { + this.setState(prevState => ({...prevState, input: '' })); + this.props.onSubmit(this._composeToString(this.state.numbers)); } } render() { return ( - - {_.map(Array.from(Array(CONST.MAGIC_CODE_NUMBERS).keys()), i => ( + <> + + {_.map(this.inputNrArray, index => ( + + this.onFocus(event, index)} + inputStyle={[styles.iouAmountTextInput, styles.textAlignCenter]} + /> + + ))} + + this.inputRef = el} + autoFocus={this.props.autoFocus} inputMode="numeric" - ref={(ref) => { this.inputRefs[i] = ref; }} + textContentType="oneTimeCode" name={this.props.name} - - // Only allows one character to be entered on the last input - // so that there's no flickering when more than one number - // is added - maxLength={i === CONST.MAGIC_CODE_NUMBERS - 1 ? 1 : 1} - blurOnSubmit={false} + maxLength={CONST.MAGIC_CODE_NUMBERS} + value={this.state.input} + label={this.props.label} + autoComplete={this.props.autoComplete} + nativeID={this.props.nativeID} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - value={this.state.numbers[i] === undefined ? '' : this.state.numbers[i].toString()} - inputStyle={[styles.iouAmountTextInput, styles.p1, styles.textAlignCenter, styles.w15]} - onFocus={() => this.updateFocus(i)} - onChangeText={value => this.onChange(value, i)} - onKeyPress={ev => this.onKeyPress(ev, i)} + onPress={(event) => this.onFocus(event, 0)} + onChangeText={this.onChangeText} + onKeyPress={this.onKeyPress} + onBlur={() => this.setState(prevState => ({...prevState, focusedIndex: undefined}))} /> - ))} - + + {!_.isEmpty(this.props.errorText) && ( + + )} + ) } } diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index 74f7700ec7a6..370e7f5517f8 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -28,7 +28,7 @@ class BaseTextInput extends Component { const activeLabel = props.forceActiveLabel || value.length > 0 || props.prefixCharacter; this.state = { - isFocused: false, + isFocused: props.focused, labelTranslateY: new Animated.Value(activeLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y), labelScale: new Animated.Value(activeLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE), passwordHidden: props.secureTextEntry, @@ -73,6 +73,10 @@ class BaseTextInput extends Component { } componentDidUpdate(prevProps) { + if (prevProps.focused !== this.props.focused) { + this.setState({isFocused: this.props.focused}); + } + // Activate or deactivate the label when value is changed programmatically from outside const inputValue = _.isUndefined(this.props.value) ? this.input.value : this.props.value; if ((_.isUndefined(inputValue) || this.state.value === inputValue) && _.isEqual(prevProps.selection, this.props.selection)) { diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index 5c52d1e17b8f..a8ead0e7a0b4 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -34,6 +34,9 @@ const propTypes = { /** Should the input auto focus? */ autoFocus: PropTypes.bool, + /** Is the input focused */ + focused: PropTypes.bool, + /** Disable the virtual keyboard */ disableKeyboard: PropTypes.bool, @@ -86,6 +89,7 @@ const defaultProps = { inputStyle: [], autoFocus: false, autoCorrect: true, + focused: false, /** * To be able to function as either controlled or uncontrolled component we should not diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index d1955db1476a..15392a3acce9 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -215,8 +215,8 @@ class BaseValidateCodeForm extends React.Component { nativeID="validateCode" name="validateCode" value={this.state.validateCode} - onChangeText={text => this.onTextInput(text, 'validateCode')} - onSubmitEditing={this.validateAndSubmitForm} + onChange={text => this.onTextInput(text, 'validateCode')} + onSubmit={this.validateAndSubmitForm} errorText={this.state.formError.validateCode ? this.props.translate(this.state.formError.validateCode) : ''} autoFocus /> diff --git a/src/stories/MagicCodeInput.stories.js b/src/stories/MagicCodeInput.stories.js index d424f685c428..741a3bb7a90c 100644 --- a/src/stories/MagicCodeInput.stories.js +++ b/src/stories/MagicCodeInput.stories.js @@ -19,12 +19,21 @@ const Template = args => ; const AutoFocus = Template.bind({}); AutoFocus.args = { - label: 'Auto-focused text input', + label: 'Auto-focused magic code input', name: 'AutoFocus', autoFocus: true, }; +const SubmitOnComplete = Template.bind({}); +SubmitOnComplete.args = { + label: 'Submits when the magic code input is complete', + name: 'SubmitOnComplete', + submitOnComplete: true, + onSubmit: () => console.log('Submitted!') +}; + export default story; export { AutoFocus, + SubmitOnComplete }; From 20fe2530a22b09804a8437a9ba13e317c47ec006 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Fri, 17 Mar 2023 11:37:01 +0000 Subject: [PATCH 03/25] fix: linting errors --- src/components/MagicCodeInput.js | 118 ++++++++++++++------------ src/stories/MagicCodeInput.stories.js | 4 +- 2 files changed, 66 insertions(+), 56 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index a67e7dc84875..d63b10065256 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -1,6 +1,5 @@ import React from 'react'; -import {Pressable, Text, StyleSheet, View, Platform} from 'react-native'; -import getPlatform from '../libs/getPlatform'; +import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import styles from '../styles/styles'; @@ -30,9 +29,15 @@ const propTypes = { /** Error text to display */ errorText: PropTypes.string, + /** Specifies autocomplete hints for the system, so it can provide autofill */ + autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code']).isRequired, + /* Should submit when the input is complete */ submitOnComplete: PropTypes.bool, + /** Id to use for this button */ + nativeID: PropTypes.string, + /** Function to call when the input is changed */ onChange: PropTypes.func, @@ -44,21 +49,23 @@ const defaultProps = { value: undefined, name: '', autoFocus: true, - forwardedRef: undefined, shouldDelayFocus: false, + forwardedRef: undefined, + errorText: '', submitOnComplete: false, + nativeID: '', onChange: () => {}, onSubmit: () => {}, }; /** * Verifies if a string is a number. - * - * @param {string} value The string to check if it's numeric. + * + * @param {string} value The string to check if it's numeric. * @returns {boolean} True if the string is numeric, false otherwise. */ function isNumeric(value) { - if (typeof value !== "string") return false; + if (typeof value !== 'string') { return false; } return !Number.isNaN(value) && !Number.isNaN(parseFloat(value)); } @@ -73,7 +80,7 @@ class MagicCodeInput extends React.PureComponent { input: '', focusedIndex: 0, editIndex: 0, - numbers: props.value ? this._decomposeString(props.value) : Array(CONST.MAGIC_CODE_NUMBERS).fill(''), + numbers: props.value ? this.decomposeString(props.value) : Array(CONST.MAGIC_CODE_NUMBERS).fill(''), }; this.onChangeText = this.onChangeText.bind(this); @@ -98,42 +105,26 @@ class MagicCodeInput extends React.PureComponent { } componentDidUpdate(prevProps) { - if (prevProps.value === this.props.value || this.props.value === this._composeToString(this.state.numbers)) { + if (prevProps.value === this.props.value || this.props.value === this.composeToString(this.state.numbers)) { return; } this.setState({ - numbers: this._decomposeString(this.props.value) - }) - } - - /** - * Converts a given string into an array of numbers that must have the same - * number of elements as the number of inputs. - * - * @param {string} value The string to be converted into an array. - * @returns {array} The array of numbers. - */ - _decomposeString(value) { - let arr = value.trim().split('').slice(0, CONST.MAGIC_CODE_NUMBERS).map(v => isNumeric(v) ? v : ''); - if (arr.length < CONST.MAGIC_CODE_NUMBERS) { - arr = arr.concat(Array(CONST.MAGIC_CODE_NUMBERS - arr.length).fill('')); - } - return arr; - } - - _composeToString(value) { - return _.filter(value, v => v !== undefined).join(''); + numbers: this.decomposeString(this.props.value), + }); } /** * Focuses on the input when it is pressed. - * - * @param {number} index The index of the input. + * + * @param {object} event The event passed by the input. + * @param {number} index The index of the input. */ onFocus(event, index) { event.preventDefault(); - this.setState(prevState => ({...prevState, input: '', focusedIndex: index, editIndex: index})); + this.setState(prevState => ({ + ...prevState, input: '', focusedIndex: index, editIndex: index, + })); this.inputRef.focus(); } @@ -143,7 +134,7 @@ class MagicCodeInput extends React.PureComponent { * the focused input on the next empty one, if exists. * It handles both fast typing and only one digit at a time * in a specific position. - * + * * @param {string} value The contents of the input text. */ onChangeText(value) { @@ -152,19 +143,21 @@ class MagicCodeInput extends React.PureComponent { } const numbersArr = value.trim().split(''); - this.setState(prevState => { - let numbers = [ + this.setState((prevState) => { + const numbers = [ ...prevState.numbers.slice(0, prevState.editIndex), - ...numbersArr.slice(0, CONST.MAGIC_CODE_NUMBERS - prevState.editIndex) + ...numbersArr.slice(0, CONST.MAGIC_CODE_NUMBERS - prevState.editIndex), ]; // Updates the focused input taking into consideration the last input // edited and the number of digits added by the user. - const focusedIndex = Math.min(prevState.editIndex + numbersArr.length - 1, CONST.MAGIC_CODE_NUMBERS - 1) + const focusedIndex = Math.min(prevState.editIndex + (numbersArr.length - 1), CONST.MAGIC_CODE_NUMBERS - 1); - return { numbers, focusedIndex, input: value, editIndex: prevState.editIndex }; + return { + numbers, focusedIndex, input: value, editIndex: prevState.editIndex, + }; }, () => { - const finalInput = this._composeToString(this.state.numbers); + const finalInput = this.composeToString(this.state.numbers); this.props.onChange(finalInput); if (this.props.submitOnComplete && finalInput.length === CONST.MAGIC_CODE_NUMBERS) { @@ -175,10 +168,10 @@ class MagicCodeInput extends React.PureComponent { /** * Handles logic related to certain key presses. - * + * * NOTE: when using Android Emulator, this can only be tested using * hardware keyboard inputs. - * + * * @param {object} event The event passed by the key press. */ onKeyPress({nativeEvent: {key: keyValue}}) { @@ -186,37 +179,56 @@ class MagicCodeInput extends React.PureComponent { // meaning that it's the last character to be deleted or it's a character being // deleted in the middle of the input, which should delete all the characters after it. if (keyValue === 'Backspace' && this.state.input.length < 2) { - this.setState(prevState => { + this.setState((prevState) => { const numbers = [...prevState.numbers]; numbers[prevState.focusedIndex] = ''; - + return { input: '', numbers: numbers.slice(0, prevState.focusedIndex), focusedIndex: prevState.focusedIndex === 0 ? 0 : prevState.focusedIndex - 1, - editIndex: prevState.editIndex === 0 ? 0 : prevState.editIndex - 1 - } + editIndex: prevState.editIndex === 0 ? 0 : prevState.editIndex - 1, + }; }); } else if (keyValue === 'ArrowLeft') { this.setState(prevState => ({ ...prevState, input: '', focusedIndex: prevState.focusedIndex - 1, - editIndex: prevState.focusedIndex - 1 + editIndex: prevState.focusedIndex - 1, })); } else if (keyValue === 'ArrowRight') { this.setState(prevState => ({ ...prevState, input: '', focusedIndex: prevState.focusedIndex + 1, - editIndex: prevState.focusedIndex + 1 + editIndex: prevState.focusedIndex + 1, })); } else if (keyValue === 'Enter') { - this.setState(prevState => ({...prevState, input: '' })); - this.props.onSubmit(this._composeToString(this.state.numbers)); + this.setState(prevState => ({...prevState, input: ''})); + this.props.onSubmit(this.composeToString(this.state.numbers)); } } + /** + * Converts a given string into an array of numbers that must have the same + * number of elements as the number of inputs. + * + * @param {string} value The string to be converted into an array. + * @returns {array} The array of numbers. + */ + decomposeString(value) { + let arr = _.map(value.trim().split('').slice(0, CONST.MAGIC_CODE_NUMBERS), v => (isNumeric(v) ? v : '')); + if (arr.length < CONST.MAGIC_CODE_NUMBERS) { + arr = arr.concat(Array(CONST.MAGIC_CODE_NUMBERS - arr.length).fill('')); + } + return arr; + } + + composeToString(value) { + return _.filter(value, v => v !== undefined).join(''); + } + render() { return ( <> @@ -229,7 +241,7 @@ class MagicCodeInput extends React.PureComponent { value={this.state.numbers[index] || ''} maxLength={1} blurOnSubmit={false} - onPress={(event) => this.onFocus(event, index)} + onPress={event => this.onFocus(event, index)} inputStyle={[styles.iouAmountTextInput, styles.textAlignCenter]} /> @@ -244,11 +256,10 @@ class MagicCodeInput extends React.PureComponent { name={this.props.name} maxLength={CONST.MAGIC_CODE_NUMBERS} value={this.state.input} - label={this.props.label} autoComplete={this.props.autoComplete} nativeID={this.props.nativeID} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - onPress={(event) => this.onFocus(event, 0)} + onPress={event => this.onFocus(event, 0)} onChangeText={this.onChangeText} onKeyPress={this.onKeyPress} onBlur={() => this.setState(prevState => ({...prevState, focusedIndex: undefined}))} @@ -258,13 +269,12 @@ class MagicCodeInput extends React.PureComponent { )} - ) + ); } } MagicCodeInput.propTypes = propTypes; MagicCodeInput.defaultProps = defaultProps; -MagicCodeInput.displayName = 'MagicCodeInput'; export default React.forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/stories/MagicCodeInput.stories.js b/src/stories/MagicCodeInput.stories.js index 741a3bb7a90c..2dbbf85cc6a5 100644 --- a/src/stories/MagicCodeInput.stories.js +++ b/src/stories/MagicCodeInput.stories.js @@ -29,11 +29,11 @@ SubmitOnComplete.args = { label: 'Submits when the magic code input is complete', name: 'SubmitOnComplete', submitOnComplete: true, - onSubmit: () => console.log('Submitted!') + onSubmit: () => console.debug('Submitted!'), }; export default story; export { AutoFocus, - SubmitOnComplete + SubmitOnComplete, }; From 80483a267597439493f94384e331ee77e749b07d Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Fri, 17 Mar 2023 16:22:51 +0000 Subject: [PATCH 04/25] fix: remove unnecessary code and apply review fixes --- src/components/MagicCodeInput.js | 46 ++++++++++++++++---------------- src/libs/ValidationUtils.js | 12 +++++++++ 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index d63b10065256..bed7406dd7ad 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import styles from '../styles/styles'; +import * as ValidationUtils from '../libs/ValidationUtils'; import CONST from '../CONST'; import TextInput from './TextInput'; import FormHelpMessage from './FormHelpMessage'; @@ -58,23 +59,12 @@ const defaultProps = { onSubmit: () => {}, }; -/** - * Verifies if a string is a number. - * - * @param {string} value The string to check if it's numeric. - * @returns {boolean} True if the string is numeric, false otherwise. - */ -function isNumeric(value) { - if (typeof value !== 'string') { return false; } - return !Number.isNaN(value) && !Number.isNaN(parseFloat(value)); -} - class MagicCodeInput extends React.PureComponent { constructor(props) { super(props); this.inputNrArray = Array.from(Array(CONST.MAGIC_CODE_NUMBERS).keys()); - this.inputRef = React.createRef(null); + this.inputRef = null; this.state = { input: '', @@ -105,7 +95,7 @@ class MagicCodeInput extends React.PureComponent { } componentDidUpdate(prevProps) { - if (prevProps.value === this.props.value || this.props.value === this.composeToString(this.state.numbers)) { + if (prevProps.value === this.props.value) { return; } @@ -114,6 +104,13 @@ class MagicCodeInput extends React.PureComponent { }); } + componentWillUnmount() { + if (!this.focusTimeout) { + return; + } + clearTimeout(this.focusTimeout); + } + /** * Focuses on the input when it is pressed. * @@ -122,9 +119,11 @@ class MagicCodeInput extends React.PureComponent { */ onFocus(event, index) { event.preventDefault(); - this.setState(prevState => ({ - ...prevState, input: '', focusedIndex: index, editIndex: index, - })); + this.setState({ + input: '', + focusedIndex: index, + editIndex: index, + }); this.inputRef.focus(); } @@ -138,7 +137,7 @@ class MagicCodeInput extends React.PureComponent { * @param {string} value The contents of the input text. */ onChangeText(value) { - if (_.isUndefined(value) || _.isEmpty(value) || !isNumeric(value)) { + if (_.isUndefined(value) || _.isEmpty(value) || !ValidationUtils.isNumeric(value)) { return; } @@ -154,7 +153,10 @@ class MagicCodeInput extends React.PureComponent { const focusedIndex = Math.min(prevState.editIndex + (numbersArr.length - 1), CONST.MAGIC_CODE_NUMBERS - 1); return { - numbers, focusedIndex, input: value, editIndex: prevState.editIndex, + numbers, + focusedIndex, + input: value, + editIndex: prevState.editIndex, }; }, () => { const finalInput = this.composeToString(this.state.numbers); @@ -192,20 +194,18 @@ class MagicCodeInput extends React.PureComponent { }); } else if (keyValue === 'ArrowLeft') { this.setState(prevState => ({ - ...prevState, input: '', focusedIndex: prevState.focusedIndex - 1, editIndex: prevState.focusedIndex - 1, })); } else if (keyValue === 'ArrowRight') { this.setState(prevState => ({ - ...prevState, input: '', focusedIndex: prevState.focusedIndex + 1, editIndex: prevState.focusedIndex + 1, })); } else if (keyValue === 'Enter') { - this.setState(prevState => ({...prevState, input: ''})); + this.setState({input: ''}); this.props.onSubmit(this.composeToString(this.state.numbers)); } } @@ -218,7 +218,7 @@ class MagicCodeInput extends React.PureComponent { * @returns {array} The array of numbers. */ decomposeString(value) { - let arr = _.map(value.trim().split('').slice(0, CONST.MAGIC_CODE_NUMBERS), v => (isNumeric(v) ? v : '')); + let arr = _.map(value.trim().split('').slice(0, CONST.MAGIC_CODE_NUMBERS), v => (ValidationUtils.isNumeric(v) ? v : '')); if (arr.length < CONST.MAGIC_CODE_NUMBERS) { arr = arr.concat(Array(CONST.MAGIC_CODE_NUMBERS - arr.length).fill('')); } @@ -262,7 +262,7 @@ class MagicCodeInput extends React.PureComponent { onPress={event => this.onFocus(event, 0)} onChangeText={this.onChangeText} onKeyPress={this.onKeyPress} - onBlur={() => this.setState(prevState => ({...prevState, focusedIndex: undefined}))} + onBlur={() => this.setState({focusedIndex: undefined})} /> {!_.isEmpty(this.props.errorText) && ( diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index f2e35941edc4..da7b32c6aca3 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -420,6 +420,17 @@ function isValidTaxID(taxID) { return taxID && CONST.REGEX.TAX_ID.test(taxID.replace(CONST.REGEX.NON_NUMERIC, '')); } +/** + * Verifies if a string is a number. + * + * @param {string} value The string to check if it's numeric. + * @returns {boolean} True if the string is numeric, false otherwise. + */ +function isNumeric(value) { + if (typeof value !== 'string') { return false; } + return !Number.isNaN(value) && !Number.isNaN(parseFloat(value)); +} + export { meetsAgeRequirements, getAgeRequirementError, @@ -450,4 +461,5 @@ export { isValidValidateCode, isValidDisplayName, doesContainReservedWord, + isNumeric, }; From 94ce197117608a1b68092727ba8f15087f104157 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Mon, 20 Mar 2023 08:51:01 +0000 Subject: [PATCH 05/25] refactor: simplify backspace logic --- src/components/MagicCodeInput.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index bed7406dd7ad..dc9e3fe4408c 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -181,17 +181,12 @@ class MagicCodeInput extends React.PureComponent { // meaning that it's the last character to be deleted or it's a character being // deleted in the middle of the input, which should delete all the characters after it. if (keyValue === 'Backspace' && this.state.input.length < 2) { - this.setState((prevState) => { - const numbers = [...prevState.numbers]; - numbers[prevState.focusedIndex] = ''; - - return { - input: '', - numbers: numbers.slice(0, prevState.focusedIndex), - focusedIndex: prevState.focusedIndex === 0 ? 0 : prevState.focusedIndex - 1, - editIndex: prevState.editIndex === 0 ? 0 : prevState.editIndex - 1, - }; - }); + this.setState(({numbers, focusedIndex, editIndex}) => ({ + input: '', + numbers: focusedIndex === 0 ? [] : [...numbers.slice(0, focusedIndex), ''], + focusedIndex: Math.max(0, focusedIndex - 1), + editIndex: Math.max(0, editIndex - 1), + })); } else if (keyValue === 'ArrowLeft') { this.setState(prevState => ({ input: '', From ef0592786f401a9f7888072aad3b15ab29d218b8 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Tue, 21 Mar 2023 17:03:49 +0000 Subject: [PATCH 06/25] feat: set submit on complete to true by default --- src/components/MagicCodeInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index dc9e3fe4408c..fe03efc718b3 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -53,7 +53,7 @@ const defaultProps = { shouldDelayFocus: false, forwardedRef: undefined, errorText: '', - submitOnComplete: false, + submitOnComplete: true, nativeID: '', onChange: () => {}, onSubmit: () => {}, From 4911091a3ab97d0766956cad4bb4bba7b7a9dab6 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Wed, 22 Mar 2023 09:02:42 +0000 Subject: [PATCH 07/25] fix: rename const and fix function documentation --- src/CONST.js | 2 +- src/components/MagicCodeInput.js | 30 +++++++++++++++--------------- src/libs/ValidationUtils.js | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index f0e9f85b5409..167a58e6aa9c 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -537,7 +537,7 @@ const CONST = { EMAIL: 'email', }, - MAGIC_CODE_NUMBERS: 6, + MAGIC_CODE_LENGTH: 6, KEYBOARD_TYPE: { PHONE_PAD: 'phone-pad', diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index fe03efc718b3..692d70d86cef 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -63,14 +63,14 @@ class MagicCodeInput extends React.PureComponent { constructor(props) { super(props); - this.inputNrArray = Array.from(Array(CONST.MAGIC_CODE_NUMBERS).keys()); + this.inputNrArray = Array.from(Array(CONST.MAGIC_CODE_LENGTH).keys()); this.inputRef = null; this.state = { input: '', focusedIndex: 0, editIndex: 0, - numbers: props.value ? this.decomposeString(props.value) : Array(CONST.MAGIC_CODE_NUMBERS).fill(''), + numbers: props.value ? this.decomposeString(props.value) : Array(CONST.MAGIC_CODE_LENGTH).fill(''), }; this.onChangeText = this.onChangeText.bind(this); @@ -114,8 +114,8 @@ class MagicCodeInput extends React.PureComponent { /** * Focuses on the input when it is pressed. * - * @param {object} event The event passed by the input. - * @param {number} index The index of the input. + * @param {Object} event + * @param {Number} index */ onFocus(event, index) { event.preventDefault(); @@ -134,7 +134,7 @@ class MagicCodeInput extends React.PureComponent { * It handles both fast typing and only one digit at a time * in a specific position. * - * @param {string} value The contents of the input text. + * @param {String} value */ onChangeText(value) { if (_.isUndefined(value) || _.isEmpty(value) || !ValidationUtils.isNumeric(value)) { @@ -145,12 +145,12 @@ class MagicCodeInput extends React.PureComponent { this.setState((prevState) => { const numbers = [ ...prevState.numbers.slice(0, prevState.editIndex), - ...numbersArr.slice(0, CONST.MAGIC_CODE_NUMBERS - prevState.editIndex), + ...numbersArr.slice(0, CONST.MAGIC_CODE_LENGTH - prevState.editIndex), ]; // Updates the focused input taking into consideration the last input // edited and the number of digits added by the user. - const focusedIndex = Math.min(prevState.editIndex + (numbersArr.length - 1), CONST.MAGIC_CODE_NUMBERS - 1); + const focusedIndex = Math.min(prevState.editIndex + (numbersArr.length - 1), CONST.MAGIC_CODE_LENGTH - 1); return { numbers, @@ -162,7 +162,7 @@ class MagicCodeInput extends React.PureComponent { const finalInput = this.composeToString(this.state.numbers); this.props.onChange(finalInput); - if (this.props.submitOnComplete && finalInput.length === CONST.MAGIC_CODE_NUMBERS) { + if (this.props.submitOnComplete && finalInput.length === CONST.MAGIC_CODE_LENGTH) { this.props.onSubmit(finalInput); } }); @@ -174,7 +174,7 @@ class MagicCodeInput extends React.PureComponent { * NOTE: when using Android Emulator, this can only be tested using * hardware keyboard inputs. * - * @param {object} event The event passed by the key press. + * @param {Object} event */ onKeyPress({nativeEvent: {key: keyValue}}) { // Handles the delete character logic if the current input is less than 2 characters, @@ -209,13 +209,13 @@ class MagicCodeInput extends React.PureComponent { * Converts a given string into an array of numbers that must have the same * number of elements as the number of inputs. * - * @param {string} value The string to be converted into an array. - * @returns {array} The array of numbers. + * @param {String} value + * @returns {Array} */ decomposeString(value) { - let arr = _.map(value.trim().split('').slice(0, CONST.MAGIC_CODE_NUMBERS), v => (ValidationUtils.isNumeric(v) ? v : '')); - if (arr.length < CONST.MAGIC_CODE_NUMBERS) { - arr = arr.concat(Array(CONST.MAGIC_CODE_NUMBERS - arr.length).fill('')); + let arr = _.map(value.trim().split('').slice(0, CONST.MAGIC_CODE_LENGTH), v => (ValidationUtils.isNumeric(v) ? v : '')); + if (arr.length < CONST.MAGIC_CODE_LENGTH) { + arr = arr.concat(Array(CONST.MAGIC_CODE_LENGTH - arr.length).fill('')); } return arr; } @@ -249,7 +249,7 @@ class MagicCodeInput extends React.PureComponent { inputMode="numeric" textContentType="oneTimeCode" name={this.props.name} - maxLength={CONST.MAGIC_CODE_NUMBERS} + maxLength={CONST.MAGIC_CODE_LENGTH} value={this.state.input} autoComplete={this.props.autoComplete} nativeID={this.props.nativeID} diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index da7b32c6aca3..2e4538a2274c 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -421,10 +421,10 @@ function isValidTaxID(taxID) { } /** - * Verifies if a string is a number. + * Checks if a value is a number. * - * @param {string} value The string to check if it's numeric. - * @returns {boolean} True if the string is numeric, false otherwise. + * @param {String} value + * @returns {Boolean} */ function isNumeric(value) { if (typeof value !== 'string') { return false; } From 968f54b245936db136fceae49e337c8fa6e737fd Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Thu, 23 Mar 2023 08:57:05 +0000 Subject: [PATCH 08/25] refactor: rename submitOnComplete in magic code input --- src/components/MagicCodeInput.js | 6 +++--- src/stories/MagicCodeInput.stories.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 692d70d86cef..e602672766b8 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -34,7 +34,7 @@ const propTypes = { autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code']).isRequired, /* Should submit when the input is complete */ - submitOnComplete: PropTypes.bool, + shouldSubmitOnComplete: PropTypes.bool, /** Id to use for this button */ nativeID: PropTypes.string, @@ -53,7 +53,7 @@ const defaultProps = { shouldDelayFocus: false, forwardedRef: undefined, errorText: '', - submitOnComplete: true, + shouldSubmitOnComplete: true, nativeID: '', onChange: () => {}, onSubmit: () => {}, @@ -162,7 +162,7 @@ class MagicCodeInput extends React.PureComponent { const finalInput = this.composeToString(this.state.numbers); this.props.onChange(finalInput); - if (this.props.submitOnComplete && finalInput.length === CONST.MAGIC_CODE_LENGTH) { + if (this.props.shouldSubmitOnComplete && finalInput.length === CONST.MAGIC_CODE_LENGTH) { this.props.onSubmit(finalInput); } }); diff --git a/src/stories/MagicCodeInput.stories.js b/src/stories/MagicCodeInput.stories.js index 2dbbf85cc6a5..0939e3177266 100644 --- a/src/stories/MagicCodeInput.stories.js +++ b/src/stories/MagicCodeInput.stories.js @@ -28,7 +28,7 @@ const SubmitOnComplete = Template.bind({}); SubmitOnComplete.args = { label: 'Submits when the magic code input is complete', name: 'SubmitOnComplete', - submitOnComplete: true, + shouldSubmitOnComplete: true, onSubmit: () => console.debug('Submitted!'), }; From 387cd96f9aea2339dfadde5dda6d3a08d96d8fc7 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Thu, 30 Mar 2023 12:49:09 +0100 Subject: [PATCH 09/25] fix: edit on focused index and submit on complete timeout fix --- src/components/MagicCodeInput.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index e602672766b8..c37e011b155e 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -63,7 +63,7 @@ class MagicCodeInput extends React.PureComponent { constructor(props) { super(props); - this.inputNrArray = Array.from(Array(CONST.MAGIC_CODE_LENGTH).keys()); + this.inputPlaceholderSlots = Array.from(Array(CONST.MAGIC_CODE_LENGTH).keys()); this.inputRef = null; this.state = { @@ -162,8 +162,10 @@ class MagicCodeInput extends React.PureComponent { const finalInput = this.composeToString(this.state.numbers); this.props.onChange(finalInput); + // If the input is complete and submit on complete is enabled, waits for a possible state + // update and then calls the onSubmit callback. if (this.props.shouldSubmitOnComplete && finalInput.length === CONST.MAGIC_CODE_LENGTH) { - this.props.onSubmit(finalInput); + setTimeout(() => this.props.onSubmit(finalInput), 0); } }); } @@ -220,15 +222,22 @@ class MagicCodeInput extends React.PureComponent { return arr; } + /** + * Converts an array of strings into a single string. If there are undefined or + * empty values, it will replace them with a space. + * + * @param {Array} value + * @returns {String} + */ composeToString(value) { - return _.filter(value, v => v !== undefined).join(''); + return _.map(value, v => ((v === undefined || v === '') ? ' ' : v)).join(''); } render() { return ( <> - {_.map(this.inputNrArray, index => ( + {_.map(this.inputPlaceholderSlots, index => ( Date: Thu, 30 Mar 2023 14:13:01 +0100 Subject: [PATCH 10/25] fix: submit when not complete and refactor/rename variables --- src/CONST.js | 1 + src/components/MagicCodeInput.js | 22 ++++++++++--------- .../ValidateCodeForm/BaseValidateCodeForm.js | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 12e2eeb1b46b..c54ff2eb9a7d 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -612,6 +612,7 @@ const CONST = { }, MAGIC_CODE_LENGTH: 6, + MAGIC_CODE_EMPTY_CHAR: ' ', KEYBOARD_TYPE: { PHONE_PAD: 'phone-pad', diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index c37e011b155e..5347c3a6a129 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -43,7 +43,7 @@ const propTypes = { onChange: PropTypes.func, /** Function to call when the input is submitted or fully complete */ - onSubmit: PropTypes.func, + onFulfill: PropTypes.func, }; const defaultProps = { @@ -56,7 +56,7 @@ const defaultProps = { shouldSubmitOnComplete: true, nativeID: '', onChange: () => {}, - onSubmit: () => {}, + onFulfill: () => {}, }; class MagicCodeInput extends React.PureComponent { @@ -70,7 +70,7 @@ class MagicCodeInput extends React.PureComponent { input: '', focusedIndex: 0, editIndex: 0, - numbers: props.value ? this.decomposeString(props.value) : Array(CONST.MAGIC_CODE_LENGTH).fill(''), + numbers: props.value ? this.decomposeString(props.value) : Array(CONST.MAGIC_CODE_LENGTH).fill(CONST.MAGIC_CODE_EMPTY_CHAR), }; this.onChangeText = this.onChangeText.bind(this); @@ -164,8 +164,8 @@ class MagicCodeInput extends React.PureComponent { // If the input is complete and submit on complete is enabled, waits for a possible state // update and then calls the onSubmit callback. - if (this.props.shouldSubmitOnComplete && finalInput.length === CONST.MAGIC_CODE_LENGTH) { - setTimeout(() => this.props.onSubmit(finalInput), 0); + if (this.props.shouldSubmitOnComplete && _.filter(this.state.numbers, n => ValidationUtils.isNumeric(n)).length === CONST.MAGIC_CODE_LENGTH) { + setTimeout(() => this.props.onFulfill(finalInput), 0); } }); } @@ -185,7 +185,9 @@ class MagicCodeInput extends React.PureComponent { if (keyValue === 'Backspace' && this.state.input.length < 2) { this.setState(({numbers, focusedIndex, editIndex}) => ({ input: '', - numbers: focusedIndex === 0 ? [] : [...numbers.slice(0, focusedIndex), ''], + numbers: focusedIndex === 0 + ? Array(CONST.MAGIC_CODE_LENGTH).fill(CONST.MAGIC_CODE_EMPTY_CHAR) + : [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR], focusedIndex: Math.max(0, focusedIndex - 1), editIndex: Math.max(0, editIndex - 1), })); @@ -203,7 +205,7 @@ class MagicCodeInput extends React.PureComponent { })); } else if (keyValue === 'Enter') { this.setState({input: ''}); - this.props.onSubmit(this.composeToString(this.state.numbers)); + this.props.onFulfill(this.composeToString(this.state.numbers)); } } @@ -215,9 +217,9 @@ class MagicCodeInput extends React.PureComponent { * @returns {Array} */ decomposeString(value) { - let arr = _.map(value.trim().split('').slice(0, CONST.MAGIC_CODE_LENGTH), v => (ValidationUtils.isNumeric(v) ? v : '')); + let arr = _.map(value.split('').slice(0, CONST.MAGIC_CODE_LENGTH), v => (ValidationUtils.isNumeric(v) ? v : CONST.MAGIC_CODE_EMPTY_CHAR)); if (arr.length < CONST.MAGIC_CODE_LENGTH) { - arr = arr.concat(Array(CONST.MAGIC_CODE_LENGTH - arr.length).fill('')); + arr = arr.concat(Array(CONST.MAGIC_CODE_LENGTH - arr.length).fill(CONST.MAGIC_CODE_EMPTY_CHAR)); } return arr; } @@ -230,7 +232,7 @@ class MagicCodeInput extends React.PureComponent { * @returns {String} */ composeToString(value) { - return _.map(value, v => ((v === undefined || v === '') ? ' ' : v)).join(''); + return _.map(value, v => ((v === undefined || v === '') ? CONST.MAGIC_CODE_EMPTY_CHAR : v)).join(''); } render() { diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 92430a3bff73..827520e3dbf0 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -220,7 +220,7 @@ class BaseValidateCodeForm extends React.Component { name="validateCode" value={this.state.validateCode} onChange={text => this.onTextInput(text, 'validateCode')} - onSubmit={this.validateAndSubmitForm} + onFulfill={this.validateAndSubmitForm} errorText={this.state.formError.validateCode ? this.props.translate(this.state.formError.validateCode) : ''} autoFocus /> From f0f749c2153c4a82452f09dccee5e470b149accd Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Thu, 30 Mar 2023 14:14:46 +0100 Subject: [PATCH 11/25] refactor: update story prop after rename --- src/stories/MagicCodeInput.stories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stories/MagicCodeInput.stories.js b/src/stories/MagicCodeInput.stories.js index 0939e3177266..de22993b2217 100644 --- a/src/stories/MagicCodeInput.stories.js +++ b/src/stories/MagicCodeInput.stories.js @@ -29,7 +29,7 @@ SubmitOnComplete.args = { label: 'Submits when the magic code input is complete', name: 'SubmitOnComplete', shouldSubmitOnComplete: true, - onSubmit: () => console.debug('Submitted!'), + onFulfill: () => console.debug('Submitted!'), }; export default story; From 5ccde1cf5cb8d913c9320cc2c3905114cd01d4c5 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Fri, 31 Mar 2023 18:11:52 +0100 Subject: [PATCH 12/25] fix: make logic similar to Slack and Github examples --- src/components/MagicCodeInput.js | 126 ++++++++++-------- src/components/TextInput/BaseTextInput.js | 6 +- .../TextInput/baseTextInputPropTypes.js | 4 - src/styles/styles.js | 6 + 4 files changed, 79 insertions(+), 63 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 5347c3a6a129..f951282f1da3 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -5,6 +5,7 @@ import _ from 'underscore'; import styles from '../styles/styles'; import * as ValidationUtils from '../libs/ValidationUtils'; import CONST from '../CONST'; +import Text from './Text'; import TextInput from './TextInput'; import FormHelpMessage from './FormHelpMessage'; @@ -36,9 +37,6 @@ const propTypes = { /* Should submit when the input is complete */ shouldSubmitOnComplete: PropTypes.bool, - /** Id to use for this button */ - nativeID: PropTypes.string, - /** Function to call when the input is changed */ onChange: PropTypes.func, @@ -54,7 +52,6 @@ const defaultProps = { forwardedRef: undefined, errorText: '', shouldSubmitOnComplete: true, - nativeID: '', onChange: () => {}, onFulfill: () => {}, }; @@ -64,7 +61,7 @@ class MagicCodeInput extends React.PureComponent { super(props); this.inputPlaceholderSlots = Array.from(Array(CONST.MAGIC_CODE_LENGTH).keys()); - this.inputRef = null; + this.inputRefs = {}; this.state = { input: '', @@ -88,10 +85,10 @@ class MagicCodeInput extends React.PureComponent { } if (this.props.shouldDelayFocus) { - this.focusTimeout = setTimeout(() => this.inputRef.focus(), CONST.ANIMATED_TRANSITION); - return; + this.focusTimeout = setTimeout(() => this.inputRefs[0].focus(), CONST.ANIMATED_TRANSITION); } - this.inputRef.focus(); + + this.inputRefs[0].focus(); } componentDidUpdate(prevProps) { @@ -124,7 +121,6 @@ class MagicCodeInput extends React.PureComponent { focusedIndex: index, editIndex: index, }); - this.inputRef.focus(); } /** @@ -141,16 +137,17 @@ class MagicCodeInput extends React.PureComponent { return; } - const numbersArr = value.trim().split(''); this.setState((prevState) => { + const numbersArr = value.trim().split('').slice(0, CONST.MAGIC_CODE_LENGTH - prevState.editIndex); const numbers = [ ...prevState.numbers.slice(0, prevState.editIndex), - ...numbersArr.slice(0, CONST.MAGIC_CODE_LENGTH - prevState.editIndex), + ...numbersArr, + ...prevState.numbers.slice(numbersArr.length + prevState.editIndex, CONST.MAGIC_CODE_LENGTH), ]; // Updates the focused input taking into consideration the last input // edited and the number of digits added by the user. - const focusedIndex = Math.min(prevState.editIndex + (numbersArr.length - 1), CONST.MAGIC_CODE_LENGTH - 1); + const focusedIndex = Math.min(prevState.editIndex + (numbersArr.length - 1) + 1, CONST.MAGIC_CODE_LENGTH - 1); return { numbers, @@ -162,10 +159,11 @@ class MagicCodeInput extends React.PureComponent { const finalInput = this.composeToString(this.state.numbers); this.props.onChange(finalInput); - // If the input is complete and submit on complete is enabled, waits for a possible state - // update and then calls the onSubmit callback. + // Blurs the input and removes focus from the last input and, if it should submit + // on complete, it will call the onFulfill callback. if (this.props.shouldSubmitOnComplete && _.filter(this.state.numbers, n => ValidationUtils.isNumeric(n)).length === CONST.MAGIC_CODE_LENGTH) { - setTimeout(() => this.props.onFulfill(finalInput), 0); + this.inputRefs[this.state.focusedIndex].blur(); + this.setState({focusedIndex: undefined}, () => this.props.onFulfill(finalInput)); } }); } @@ -179,18 +177,36 @@ class MagicCodeInput extends React.PureComponent { * @param {Object} event */ onKeyPress({nativeEvent: {key: keyValue}}) { - // Handles the delete character logic if the current input is less than 2 characters, - // meaning that it's the last character to be deleted or it's a character being - // deleted in the middle of the input, which should delete all the characters after it. - if (keyValue === 'Backspace' && this.state.input.length < 2) { - this.setState(({numbers, focusedIndex, editIndex}) => ({ - input: '', - numbers: focusedIndex === 0 - ? Array(CONST.MAGIC_CODE_LENGTH).fill(CONST.MAGIC_CODE_EMPTY_CHAR) - : [...numbers.slice(0, focusedIndex), CONST.MAGIC_CODE_EMPTY_CHAR], - focusedIndex: Math.max(0, focusedIndex - 1), - editIndex: Math.max(0, editIndex - 1), - })); + if (keyValue === 'Backspace') { + this.setState(({numbers, focusedIndex}) => { + // If the currently focused index already has a value, it will delete + // that value but maintain the focus on the same input. + if (numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { + return { + input: '', + numbers: [ + ...numbers.slice(0, focusedIndex), + CONST.MAGIC_CODE_EMPTY_CHAR, + ...numbers.slice(focusedIndex + 1, CONST.MAGIC_CODE_LENGTH), + ], + }; + } + + // Deletes the value of the previous input and focuses on it. + const hasInputs = _.filter(numbers, n => ValidationUtils.isNumeric(n)).length !== 0; + return { + input: '', + numbers: focusedIndex === 0 && !hasInputs + ? Array(CONST.MAGIC_CODE_LENGTH).fill(CONST.MAGIC_CODE_EMPTY_CHAR) + : [ + ...numbers.slice(0, focusedIndex - 1), + CONST.MAGIC_CODE_EMPTY_CHAR, + ...numbers.slice(focusedIndex, CONST.MAGIC_CODE_LENGTH), + ], + focusedIndex: Math.max(0, focusedIndex - 1), + editIndex: Math.max(0, focusedIndex - 1), + }; + }); } else if (keyValue === 'ArrowLeft') { this.setState(prevState => ({ input: '', @@ -241,36 +257,38 @@ class MagicCodeInput extends React.PureComponent { {_.map(this.inputPlaceholderSlots, index => ( - this.onFocus(event, index)} - inputStyle={[styles.iouAmountTextInput, styles.textAlignCenter]} - /> + + + {this.state.numbers[index] || ''} + + + + this.inputRefs[index] = ref} + autoFocus={this.props.autoFocus} + inputMode="numeric" + textContentType="oneTimeCode" + name={this.props.name} + caretHidden + maxLength={CONST.MAGIC_CODE_LENGTH} + value={this.state.input} + autoComplete={this.props.autoComplete} + keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + onChangeText={this.onChangeText} + onKeyPress={this.onKeyPress} + onPress={event => this.onFocus(event, index)} + onFocus={event => this.onFocus(event, index)} + onBlur={() => this.setState({focusedIndex: undefined})} + /> + ))} - - this.inputRef = el} - autoFocus={this.props.autoFocus} - inputMode="numeric" - textContentType="oneTimeCode" - name={this.props.name} - maxLength={CONST.MAGIC_CODE_LENGTH} - value={this.state.input} - autoComplete={this.props.autoComplete} - nativeID={this.props.nativeID} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - onPress={event => this.onFocus(event, 0)} - onChangeText={this.onChangeText} - onKeyPress={this.onKeyPress} - onBlur={() => this.setState({focusedIndex: undefined})} - /> - {!_.isEmpty(this.props.errorText) && ( )} diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js index e4ec70bb52fa..d85aea8a71a0 100644 --- a/src/components/TextInput/BaseTextInput.js +++ b/src/components/TextInput/BaseTextInput.js @@ -28,7 +28,7 @@ class BaseTextInput extends Component { const activeLabel = props.forceActiveLabel || value.length > 0 || props.prefixCharacter; this.state = { - isFocused: props.focused, + isFocused: false, labelTranslateY: new Animated.Value(activeLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y), labelScale: new Animated.Value(activeLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE), passwordHidden: props.secureTextEntry, @@ -73,10 +73,6 @@ class BaseTextInput extends Component { } componentDidUpdate(prevProps) { - if (prevProps.focused !== this.props.focused) { - this.setState({isFocused: this.props.focused}); - } - // Activate or deactivate the label when value is changed programmatically from outside const inputValue = _.isUndefined(this.props.value) ? this.input.value : this.props.value; if ((_.isUndefined(inputValue) || this.state.value === inputValue) && _.isEqual(prevProps.selection, this.props.selection)) { diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index c7eb303ac554..6de484dec74d 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -37,9 +37,6 @@ const propTypes = { /** Should the input auto focus? */ autoFocus: PropTypes.bool, - /** Is the input focused */ - focused: PropTypes.bool, - /** Disable the virtual keyboard */ disableKeyboard: PropTypes.bool, @@ -95,7 +92,6 @@ const defaultProps = { inputStyle: [], autoFocus: false, autoCorrect: true, - focused: false, /** * To be able to function as either controlled or uncontrolled component we should not diff --git a/src/styles/styles.js b/src/styles/styles.js index bf37f87b74d8..612f23ac700e 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -2259,6 +2259,12 @@ const styles = { backgroundColor: themeColors.checkBox, }, + magicCodeInput: { + fontSize: variables.fontSizeXLarge, + color: themeColors.heading, + lineHeight: variables.inputHeight, + }, + iouAmountText: { ...headlineFont, fontSize: variables.iouAmountTextSize, From 423393a689a6684679130edcd873d7795e6b877b Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Fri, 31 Mar 2023 18:21:14 +0100 Subject: [PATCH 13/25] fix: small fix for Android --- src/components/MagicCodeInput.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index f951282f1da3..c775d8f99141 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -274,7 +274,6 @@ class MagicCodeInput extends React.PureComponent { inputMode="numeric" textContentType="oneTimeCode" name={this.props.name} - caretHidden maxLength={CONST.MAGIC_CODE_LENGTH} value={this.state.input} autoComplete={this.props.autoComplete} From 592e1eb301e26894751dc628958f0dcc47f8f354 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Mon, 3 Apr 2023 08:57:51 +0100 Subject: [PATCH 14/25] fix: rename prop and fix autofocus --- src/components/MagicCodeInput.js | 9 +++++---- .../signin/ValidateCodeForm/BaseValidateCodeForm.js | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index c775d8f99141..932ba9b8a13d 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -38,7 +38,7 @@ const propTypes = { shouldSubmitOnComplete: PropTypes.bool, /** Function to call when the input is changed */ - onChange: PropTypes.func, + onChangeText: PropTypes.func, /** Function to call when the input is submitted or fully complete */ onFulfill: PropTypes.func, @@ -52,7 +52,7 @@ const defaultProps = { forwardedRef: undefined, errorText: '', shouldSubmitOnComplete: true, - onChange: () => {}, + onChangeText: () => {}, onFulfill: () => {}, }; @@ -157,7 +157,7 @@ class MagicCodeInput extends React.PureComponent { }; }, () => { const finalInput = this.composeToString(this.state.numbers); - this.props.onChange(finalInput); + this.props.onChangeText(finalInput); // Blurs the input and removes focus from the last input and, if it should submit // on complete, it will call the onFulfill callback. @@ -270,12 +270,13 @@ class MagicCodeInput extends React.PureComponent { this.inputRefs[index] = ref} - autoFocus={this.props.autoFocus} + autoFocus={index === 0 && this.props.autoFocus} inputMode="numeric" textContentType="oneTimeCode" name={this.props.name} maxLength={CONST.MAGIC_CODE_LENGTH} value={this.state.input} + hideFocusedState autoComplete={this.props.autoComplete} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} onChangeText={this.onChangeText} diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 827520e3dbf0..552edd59a389 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -219,7 +219,7 @@ class BaseValidateCodeForm extends React.Component { nativeID="validateCode" name="validateCode" value={this.state.validateCode} - onChange={text => this.onTextInput(text, 'validateCode')} + onChangeText={text => this.onTextInput(text, 'validateCode')} onFulfill={this.validateAndSubmitForm} errorText={this.state.formError.validateCode ? this.props.translate(this.state.formError.validateCode) : ''} autoFocus From 469f5e4e0f9613ce34a8c65a94e0505e43350c7a Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Mon, 3 Apr 2023 09:00:56 +0100 Subject: [PATCH 15/25] fix: autoComplete only for first input --- src/components/MagicCodeInput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 932ba9b8a13d..d1fc0cdea17b 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -277,7 +277,7 @@ class MagicCodeInput extends React.PureComponent { maxLength={CONST.MAGIC_CODE_LENGTH} value={this.state.input} hideFocusedState - autoComplete={this.props.autoComplete} + autoComplete={index === 0 ? this.props.autoComplete : 'off'} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} onChangeText={this.onChangeText} onKeyPress={this.onKeyPress} From 7c061756219000188ebded5e77778bad4af9074f Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Thu, 20 Apr 2023 13:27:05 +0100 Subject: [PATCH 16/25] fix: delete on first input when not empty --- src/components/MagicCodeInput.js | 38 +++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index d1fc0cdea17b..15197aa25fc3 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -182,27 +182,39 @@ class MagicCodeInput extends React.PureComponent { // If the currently focused index already has a value, it will delete // that value but maintain the focus on the same input. if (numbers[focusedIndex] !== CONST.MAGIC_CODE_EMPTY_CHAR) { + const newNumbers = [ + ...numbers.slice(0, focusedIndex), + CONST.MAGIC_CODE_EMPTY_CHAR, + ...numbers.slice(focusedIndex + 1, CONST.MAGIC_CODE_LENGTH), + ]; return { input: '', - numbers: [ - ...numbers.slice(0, focusedIndex), - CONST.MAGIC_CODE_EMPTY_CHAR, - ...numbers.slice(focusedIndex + 1, CONST.MAGIC_CODE_LENGTH), - ], + numbers: newNumbers, }; } - // Deletes the value of the previous input and focuses on it. const hasInputs = _.filter(numbers, n => ValidationUtils.isNumeric(n)).length !== 0; + let newNumbers = numbers; + + // Fill the array with empty characters if there are no inputs. + if (focusedIndex === 0 && !hasInputs) { + newNumbers = Array(CONST.MAGIC_CODE_LENGTH).fill(CONST.MAGIC_CODE_EMPTY_CHAR); + + // Deletes the value of the previous input and focuses on it. + } else if (focusedIndex !== 0) { + newNumbers = [ + ...numbers.slice(0, Math.max(0, focusedIndex - 1)), + CONST.MAGIC_CODE_EMPTY_CHAR, + ...numbers.slice(focusedIndex, CONST.MAGIC_CODE_LENGTH), + ]; + } + + // Saves the input string so that it can compare to the change text + // event that will be triggered, this is a workaround for mobile that + // triggers the change text on the event after the key press. return { input: '', - numbers: focusedIndex === 0 && !hasInputs - ? Array(CONST.MAGIC_CODE_LENGTH).fill(CONST.MAGIC_CODE_EMPTY_CHAR) - : [ - ...numbers.slice(0, focusedIndex - 1), - CONST.MAGIC_CODE_EMPTY_CHAR, - ...numbers.slice(focusedIndex, CONST.MAGIC_CODE_LENGTH), - ], + numbers: newNumbers, focusedIndex: Math.max(0, focusedIndex - 1), editIndex: Math.max(0, focusedIndex - 1), }; From b24e9e9c4084f76cb51ed037fb86ee39d695a939 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Thu, 20 Apr 2023 16:03:43 +0100 Subject: [PATCH 17/25] fix: workaround for callback on input change on mobile --- src/components/MagicCodeInput.js | 43 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 15197aa25fc3..2a7350015503 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import _ from 'underscore'; import styles from '../styles/styles'; import * as ValidationUtils from '../libs/ValidationUtils'; +import getPlatform from '../libs/getPlatform'; import CONST from '../CONST'; import Text from './Text'; import TextInput from './TextInput'; @@ -153,12 +154,17 @@ class MagicCodeInput extends React.PureComponent { numbers, focusedIndex, input: value, - editIndex: prevState.editIndex, }; }, () => { const finalInput = this.composeToString(this.state.numbers); this.props.onChangeText(finalInput); + // Only focus on web, since focusing on mobile it will cause flickering when + // fast typing. + if (getPlatform() === CONST.PLATFORM.WEB) { + this.inputRefs[this.state.focusedIndex].focus(); + } + // Blurs the input and removes focus from the last input and, if it should submit // on complete, it will call the onFulfill callback. if (this.props.shouldSubmitOnComplete && _.filter(this.state.numbers, n => ValidationUtils.isNumeric(n)).length === CONST.MAGIC_CODE_LENGTH) { @@ -190,6 +196,7 @@ class MagicCodeInput extends React.PureComponent { return { input: '', numbers: newNumbers, + editIndex: focusedIndex, }; } @@ -218,19 +225,18 @@ class MagicCodeInput extends React.PureComponent { focusedIndex: Math.max(0, focusedIndex - 1), editIndex: Math.max(0, focusedIndex - 1), }; + }, () => { + if (_.isUndefined(this.state.focusedIndex)) { + return; + } + this.inputRefs[this.state.focusedIndex].focus(); }); - } else if (keyValue === 'ArrowLeft') { - this.setState(prevState => ({ - input: '', - focusedIndex: prevState.focusedIndex - 1, - editIndex: prevState.focusedIndex - 1, - })); - } else if (keyValue === 'ArrowRight') { - this.setState(prevState => ({ - input: '', - focusedIndex: prevState.focusedIndex + 1, - editIndex: prevState.focusedIndex + 1, - })); + } else if (keyValue === 'ArrowLeft' && !_.isUndefined(this.state.focusedIndex)) { + this.inputRefs[Math.max(0, this.state.focusedIndex - 1)].focus(); + this.setState({input: ''}); + } else if (keyValue === 'ArrowRight' && !_.isUndefined(this.state.focusedIndex)) { + this.inputRefs[Math.min(this.state.focusedIndex + 1, CONST.MAGIC_CODE_LENGTH - 1)].focus(); + this.setState({input: ''}); } else if (keyValue === 'Enter') { this.setState({input: ''}); this.props.onFulfill(this.composeToString(this.state.numbers)); @@ -291,7 +297,16 @@ class MagicCodeInput extends React.PureComponent { hideFocusedState autoComplete={index === 0 ? this.props.autoComplete : 'off'} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - onChangeText={this.onChangeText} + onChangeText={(value) => { + // Do not run when the event comes from an input that + // is not currently being responsible for the input, + // this is necessary to avoid calls when the input + // changes due to delete of characters. + if (index !== this.state.editIndex) { + return; + } + this.onChangeText(value); + }} onKeyPress={this.onKeyPress} onPress={event => this.onFocus(event, index)} onFocus={event => this.onFocus(event, index)} From f1e58c248e2d620f7ee3755a0274e6c007fd4cd6 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Thu, 20 Apr 2023 16:03:58 +0100 Subject: [PATCH 18/25] fix: magic code input story console error --- src/stories/MagicCodeInput.stories.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/stories/MagicCodeInput.stories.js b/src/stories/MagicCodeInput.stories.js index de22993b2217..eac52347d9c0 100644 --- a/src/stories/MagicCodeInput.stories.js +++ b/src/stories/MagicCodeInput.stories.js @@ -22,12 +22,14 @@ AutoFocus.args = { label: 'Auto-focused magic code input', name: 'AutoFocus', autoFocus: true, + autoComplete: 'one-time-code', }; const SubmitOnComplete = Template.bind({}); SubmitOnComplete.args = { label: 'Submits when the magic code input is complete', name: 'SubmitOnComplete', + autoComplete: 'one-time-code', shouldSubmitOnComplete: true, onFulfill: () => console.debug('Submitted!'), }; From 7d353c3150d96554daaa393c807220bd32e9f118 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Fri, 21 Apr 2023 08:35:33 +0100 Subject: [PATCH 19/25] fix: another approach to fix keyboard navigation --- src/components/MagicCodeInput.js | 51 ++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 2a7350015503..84134fed0927 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import _ from 'underscore'; import styles from '../styles/styles'; import * as ValidationUtils from '../libs/ValidationUtils'; -import getPlatform from '../libs/getPlatform'; import CONST from '../CONST'; import Text from './Text'; import TextInput from './TextInput'; @@ -71,6 +70,7 @@ class MagicCodeInput extends React.PureComponent { numbers: props.value ? this.decomposeString(props.value) : Array(CONST.MAGIC_CODE_LENGTH).fill(CONST.MAGIC_CODE_EMPTY_CHAR), }; + this.onFocus = this.onFocus.bind(this); this.onChangeText = this.onChangeText.bind(this); this.onKeyPress = this.onKeyPress.bind(this); } @@ -115,7 +115,21 @@ class MagicCodeInput extends React.PureComponent { * @param {Object} event * @param {Number} index */ - onFocus(event, index) { + onFocus(event) { + event.preventDefault(); + this.setState({ + input: '', + }); + } + + /** + * Callback for the onPress event, updates the indexes + * of the currently focused input. + * + * @param {Object} event + * @param {Number} index + */ + onPress(event, index) { event.preventDefault(); this.setState({ input: '', @@ -159,12 +173,6 @@ class MagicCodeInput extends React.PureComponent { const finalInput = this.composeToString(this.state.numbers); this.props.onChangeText(finalInput); - // Only focus on web, since focusing on mobile it will cause flickering when - // fast typing. - if (getPlatform() === CONST.PLATFORM.WEB) { - this.inputRefs[this.state.focusedIndex].focus(); - } - // Blurs the input and removes focus from the last input and, if it should submit // on complete, it will call the onFulfill callback. if (this.props.shouldSubmitOnComplete && _.filter(this.state.numbers, n => ValidationUtils.isNumeric(n)).length === CONST.MAGIC_CODE_LENGTH) { @@ -232,11 +240,17 @@ class MagicCodeInput extends React.PureComponent { this.inputRefs[this.state.focusedIndex].focus(); }); } else if (keyValue === 'ArrowLeft' && !_.isUndefined(this.state.focusedIndex)) { - this.inputRefs[Math.max(0, this.state.focusedIndex - 1)].focus(); - this.setState({input: ''}); + this.setState(prevState => ({ + input: '', + focusedIndex: Math.max(0, prevState.focusedIndex - 1), + editIndex: Math.max(0, prevState.focusedIndex - 1), + }), () => this.inputRefs[this.state.focusedIndex].focus()); } else if (keyValue === 'ArrowRight' && !_.isUndefined(this.state.focusedIndex)) { - this.inputRefs[Math.min(this.state.focusedIndex + 1, CONST.MAGIC_CODE_LENGTH - 1)].focus(); - this.setState({input: ''}); + this.setState(prevState => ({ + input: '', + focusedIndex: Math.min(prevState.focusedIndex + 1, CONST.MAGIC_CODE_LENGTH - 1), + editIndex: Math.min(prevState.focusedIndex + 1, CONST.MAGIC_CODE_LENGTH - 1), + }), () => this.inputRefs[this.state.focusedIndex].focus()); } else if (keyValue === 'Enter') { this.setState({input: ''}); this.props.onFulfill(this.composeToString(this.state.numbers)); @@ -298,19 +312,18 @@ class MagicCodeInput extends React.PureComponent { autoComplete={index === 0 ? this.props.autoComplete : 'off'} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} onChangeText={(value) => { - // Do not run when the event comes from an input that - // is not currently being responsible for the input, - // this is necessary to avoid calls when the input - // changes due to delete of characters. + // Do not run when the event comes from an input that is + // not currently being responsible for the input, this is + // necessary to avoid calls when the input changes due to + // deleted characters. Only happens in mobile. if (index !== this.state.editIndex) { return; } this.onChangeText(value); }} onKeyPress={this.onKeyPress} - onPress={event => this.onFocus(event, index)} - onFocus={event => this.onFocus(event, index)} - onBlur={() => this.setState({focusedIndex: undefined})} + onPress={event => this.onPress(event, index)} + onFocus={this.onFocus} /> From d8e6314e034cafdf1b5ffb9c0c3230182935ad36 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Tue, 25 Apr 2023 08:36:34 +0100 Subject: [PATCH 20/25] fix: close mobile keyboard on input complete and fix number validation --- src/components/MagicCodeInput.js | 2 +- src/libs/ValidationUtils.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 84134fed0927..384e9aa2dcc8 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -176,7 +176,7 @@ class MagicCodeInput extends React.PureComponent { // Blurs the input and removes focus from the last input and, if it should submit // on complete, it will call the onFulfill callback. if (this.props.shouldSubmitOnComplete && _.filter(this.state.numbers, n => ValidationUtils.isNumeric(n)).length === CONST.MAGIC_CODE_LENGTH) { - this.inputRefs[this.state.focusedIndex].blur(); + this.inputRefs[this.state.editIndex].blur(); this.setState({focusedIndex: undefined}, () => this.props.onFulfill(finalInput)); } }); diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index 5ec3607c292f..6d27cc0d4385 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -431,14 +431,14 @@ function isValidTaxID(taxID) { } /** - * Checks if a value is a number. + * Checks if a string value is a number. * * @param {String} value * @returns {Boolean} */ function isNumeric(value) { if (typeof value !== 'string') { return false; } - return !Number.isNaN(value) && !Number.isNaN(parseFloat(value)); + return /^\d*$/.test(value); } export { From 83971d2d9948029533dbdbfc6f4642e37d4abfc5 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Tue, 25 Apr 2023 08:36:49 +0100 Subject: [PATCH 21/25] fix: reset validate code when resending new code --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index d9015ae64d54..854e8ed2c84f 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -140,7 +140,7 @@ class BaseValidateCodeForm extends React.Component { User.resendValidateCode(this.props.credentials.login, true); // Give feedback to the user to let them know the email was sent so they don't spam the button. - this.setState({linkSent: true}); + this.setState({validateCode: '', linkSent: true}); } /** From 9c0749aa5d1ee7675bb303c60c05570f2a3217ed Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Tue, 25 Apr 2023 14:36:27 +0100 Subject: [PATCH 22/25] feat: reset input after resending code --- src/components/MagicCodeInput.js | 33 ++++++++++--------- .../ValidateCodeForm/BaseValidateCodeForm.js | 7 ++-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 384e9aa2dcc8..d24d5cc49c91 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -22,12 +22,6 @@ const propTypes = { /** Whether we should wait before focusing the TextInput, useful when using transitions */ shouldDelayFocus: PropTypes.bool, - /** A ref to forward the current input */ - forwardedRef: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.shape({current: PropTypes.instanceOf(React.Component)}), - ]), - /** Error text to display */ errorText: PropTypes.string, @@ -49,7 +43,6 @@ const defaultProps = { name: '', autoFocus: true, shouldDelayFocus: false, - forwardedRef: undefined, errorText: '', shouldSubmitOnComplete: true, onChangeText: () => {}, @@ -76,11 +69,6 @@ class MagicCodeInput extends React.PureComponent { } componentDidMount() { - if (!this.props.forwardedRef) { - return; - } - this.props.forwardedRef(this.inputRef); - if (!this.props.autoFocus) { return; } @@ -257,6 +245,21 @@ class MagicCodeInput extends React.PureComponent { } } + focus() { + this.setState({focusedIndex: 0}); + this.inputRefs[0].focus(); + } + + clear() { + this.setState({ + input: '', + focusedIndex: 0, + editIndex: 0, + numbers: Array(CONST.MAGIC_CODE_LENGTH).fill(CONST.MAGIC_CODE_EMPTY_CHAR), + }); + this.inputRefs[0].focus(); + } + /** * Converts a given string into an array of numbers that must have the same * number of elements as the number of inputs. @@ -340,7 +343,5 @@ class MagicCodeInput extends React.PureComponent { MagicCodeInput.propTypes = propTypes; MagicCodeInput.defaultProps = defaultProps; -export default React.forwardRef((props, ref) => ( - // eslint-disable-next-line react/jsx-props-no-spreading - -)); +export default MagicCodeInput; + diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 854e8ed2c84f..97abbb6e3f8f 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -94,6 +94,9 @@ class BaseValidateCodeForm extends React.Component { if (prevProps.isVisible && !this.props.isVisible && this.state.validateCode) { this.clearValidateCode(); } + if (prevProps.isVisible && this.state.linkSent && this.props.account.message && this.state.validateCode) { + this.clearValidateCode(); + } if (!prevProps.credentials.validateCode && this.props.credentials.validateCode) { this.setState({validateCode: this.props.credentials.validateCode}); } @@ -126,7 +129,7 @@ class BaseValidateCodeForm extends React.Component { * Clear Validate Code from the state */ clearValidateCode() { - this.setState({validateCode: ''}, this.inputValidateCode.clear); + this.setState({validateCode: ''}, () => this.inputValidateCode.clear()); } /** @@ -140,7 +143,7 @@ class BaseValidateCodeForm extends React.Component { User.resendValidateCode(this.props.credentials.login, true); // Give feedback to the user to let them know the email was sent so they don't spam the button. - this.setState({validateCode: '', linkSent: true}); + this.setState({linkSent: true}); } /** From 4e4e220c3938d498f11b33b261d0f03b79a065c2 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Wed, 26 Apr 2023 10:17:17 +0100 Subject: [PATCH 23/25] fix: reset link sent on input --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 97abbb6e3f8f..b8c9dc0a5bd9 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -94,7 +94,9 @@ class BaseValidateCodeForm extends React.Component { if (prevProps.isVisible && !this.props.isVisible && this.state.validateCode) { this.clearValidateCode(); } - if (prevProps.isVisible && this.state.linkSent && this.props.account.message && this.state.validateCode) { + + // Clear the code input if a new magic code was requested + if (this.props.isVisible && this.state.linkSent && this.props.account.message && this.state.validateCode) { this.clearValidateCode(); } if (!prevProps.credentials.validateCode && this.props.credentials.validateCode) { @@ -118,6 +120,7 @@ class BaseValidateCodeForm extends React.Component { this.setState({ [key]: text, formError: {[key]: ''}, + linkSent: false, }); if (this.props.account.errors) { From b1a39eb58309af7bbf360589bb6abc74c237df5c Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Fri, 28 Apr 2023 14:42:00 +0100 Subject: [PATCH 24/25] fix: minHeight for iPad fix --- src/components/MagicCodeInput.js | 2 +- src/styles/styles.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index d24d5cc49c91..10c54e513f55 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -289,7 +289,7 @@ class MagicCodeInput extends React.PureComponent { render() { return ( <> - + {_.map(this.inputPlaceholderSlots, index => ( Date: Fri, 28 Apr 2023 15:33:46 +0100 Subject: [PATCH 25/25] fix: use same variable for input minHeight --- src/styles/styles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/styles.js b/src/styles/styles.js index 67a8493cb638..21c807ef04ba 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -2312,7 +2312,7 @@ const styles = { magicCodeInputContainer: { flexDirection: 'row', justifyContent: 'space-between', - minHeight: 50, + minHeight: variables.inputHeight, }, magicCodeInput: {