From 29bf8b9cb9135ff20616aa270deacd7aa899d8ba Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Tue, 14 Mar 2023 17:17:07 +0000 Subject: [PATCH 001/108] 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 002/108] 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 003/108] 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 004/108] 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 005/108] 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 006/108] 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 007/108] 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 008/108] 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 8958fede56bc0847e5980748dba1a87c9b06670c Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Fri, 24 Mar 2023 20:10:04 +0530 Subject: [PATCH 009/108] Implementing Invite message page --- src/ONYXKEYS.js | 1 + src/ROUTES.js | 2 + src/components/Form.js | 5 + src/components/MenuItem.js | 6 +- src/components/MultipleAvatars.js | 66 +++++- src/components/menuItemPropTypes.js | 3 + src/languages/en.js | 9 +- src/languages/es.js | 9 +- .../AppNavigator/ModalStackNavigators.js | 7 + src/libs/Navigation/linkingConfig.js | 3 + src/libs/actions/Policy.js | 5 + src/pages/settings/InitialSettingsPage.js | 2 + .../workspace/WorkspaceInviteMessagePage.js | 211 ++++++++++++++++++ src/pages/workspace/WorkspaceInvitePage.js | 64 +----- src/styles/StyleUtils.js | 32 +++ src/styles/styles.js | 38 ---- 16 files changed, 353 insertions(+), 110 deletions(-) create mode 100644 src/pages/workspace/WorkspaceInviteMessagePage.js diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 42a4a2d651be..dc58aa6543c8 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -110,6 +110,7 @@ export default { POLICY: 'policy_', REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_', POLICY_MEMBER_LIST: 'policyMemberList_', + WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', DOWNLOAD: 'download_', }, diff --git a/src/ROUTES.js b/src/ROUTES.js index 2cbe87a4361e..989f9420d694 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -114,6 +114,7 @@ export default { WORKSPACE_NEW: 'workspace/new', WORKSPACE_INITIAL: 'workspace/:policyID', WORKSPACE_INVITE: 'workspace/:policyID/invite', + WORKSPACE_INVITE_MESSAGE: 'workspace/:policyID/invite-message', WORKSPACE_SETTINGS: 'workspace/:policyID/settings', WORKSPACE_CARD: 'workspace/:policyID/card', WORKSPACE_REIMBURSE: 'workspace/:policyID/reimburse', @@ -124,6 +125,7 @@ export default { WORKSPACE_NEW_ROOM: 'workspace/new-room', getWorkspaceInitialRoute: policyID => `workspace/${policyID}`, getWorkspaceInviteRoute: policyID => `workspace/${policyID}/invite`, + getWorkspaceInviteMessageRoute: policyID => `workspace/${policyID}/invite-message`, getWorkspaceSettingsRoute: policyID => `workspace/${policyID}/settings`, getWorkspaceCardRoute: policyID => `workspace/${policyID}/card`, getWorkspaceReimburseRoute: policyID => `workspace/${policyID}/reimburse`, diff --git a/src/components/Form.js b/src/components/Form.js index 116179b46d4a..63889f9b203d 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -77,6 +77,9 @@ const propTypes = { /** Container styles */ style: stylePropTypes, + /** Custom content to display in the footer after submit button */ + footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + ...withLocalizePropTypes, }; @@ -91,6 +94,7 @@ const defaultProps = { isSubmitActionDangerous: false, scrollToOverflowEnabled: false, scrollContextEnabled: false, + footerContent: null, style: [], }; @@ -335,6 +339,7 @@ class Form extends React.Component { disablePressOnEnter /> )} + {this.props.footerContent} ); diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 2942f250ce83..19063a9e52ba 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -67,6 +67,8 @@ const MenuItem = (props) => { styles.lineHeightNormal, ], props.style); + const fallbackAvatarSize = (props.viewMode === CONST.OPTION_MODE.COMPACT) ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; + return ( { @@ -175,12 +177,12 @@ const MenuItem = (props) => { )} {!_.isEmpty(props.floatRightAvatars) && ( - + diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js index 6e30a2579a25..735525461675 100644 --- a/src/components/MultipleAvatars.js +++ b/src/components/MultipleAvatars.js @@ -55,13 +55,12 @@ const defaultProps = { }; const MultipleAvatars = (props) => { - const avatarContainerStyles = props.size === CONST.AVATAR_SIZE.SMALL ? styles.emptyAvatarSmall : styles.emptyAvatar; + let avatarContainerStyles = props.size === CONST.AVATAR_SIZE.SMALL ? styles.emptyAvatarSmall : styles.emptyAvatar; const singleAvatarStyles = props.size === CONST.AVATAR_SIZE.SMALL ? styles.singleAvatarSmall : styles.singleAvatar; const secondAvatarStyles = [ props.size === CONST.AVATAR_SIZE.SMALL ? styles.secondAvatarSmall : styles.secondAvatar, ...props.secondAvatarStyle, ]; - const horizontalStyles = [styles.horizontalStackedAvatar4, styles.horizontalStackedAvatar3, styles.horizontalStackedAvatar2, styles.horizontalStackedAvatar1]; if (!props.icons.length) { return null; @@ -83,6 +82,32 @@ const MultipleAvatars = (props) => { ); } + const oneAvatarSize = StyleUtils.getAvatarStyle(props.size); + const oneAvatarBorderWidth = StyleUtils.getAvatarBorderSize(props.size); + const overlapSize = oneAvatarSize.width / 3; + + if (props.shouldStackHorizontally) { + let width; + + // Height of one avatar + border space + const height = oneAvatarSize.height + (2 * oneAvatarBorderWidth); + if (props.icons.length > 4) { + // Width of overlapping avatars + border space + width = (oneAvatarSize.width * 3) + (oneAvatarBorderWidth * 8); + } else { + // one avatar width + overlaping avatar sizes + border space + width = oneAvatarSize.width + (overlapSize * 2 * (props.icons.length - 1)) + (oneAvatarBorderWidth * (props.icons.length * 2)); + } + avatarContainerStyles = StyleUtils.combineStyles([ + styles.alignItemsCenter, + styles.flexRow, + { + width, + height, + }, + ]); + } + return ( {props.shouldStackHorizontally ? ( @@ -91,13 +116,19 @@ const MultipleAvatars = (props) => { _.map([...props.icons].splice(0, 4).reverse(), (icon, index) => ( @@ -114,12 +145,29 @@ const MultipleAvatars = (props) => { // Set overlay background color with RGBA value so that the text will not inherit opacity StyleUtils.getBackgroundColorWithOpacityStyle(themeColors.overlay, variables.overlayOpacity), styles.horizontalStackedAvatar4Overlay, - StyleUtils.getAvatarBorderRadius(props.size, props.icons[3].type), + { + borderWidth: oneAvatarBorderWidth, borderRadius: oneAvatarSize.width, left: -((oneAvatarSize.width * 2) + (oneAvatarBorderWidth * 2)), zIndex: 6, + }, + (props.icons[3].type === CONST.ICON_TYPE_WORKSPACE ? StyleUtils.getAvatarBorderRadius(props.size, props.icons[3].type) : {}), ]} > - - {`+${props.icons.length - 4}`} - + + + {`+${props.icons.length - 4}`} + + )} diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js index 9f3b40a55b43..a66e031dd5b4 100644 --- a/src/components/menuItemPropTypes.js +++ b/src/components/menuItemPropTypes.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import _ from 'underscore'; import CONST from '../CONST'; import stylePropTypes from '../styles/stylePropTypes'; import avatarPropTypes from './avatarPropTypes'; @@ -85,6 +86,8 @@ const propTypes = { /** Prop to identify if we should load avatars vertically instead of diagonally */ shouldStackHorizontally: PropTypes.bool, + + avatarSize: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), }; export default propTypes; diff --git a/src/languages/en.js b/src/languages/en.js index 765be98ed4dc..042f36239a9a 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1013,11 +1013,16 @@ export default { }, invite: { invitePeople: 'Invite new members', - personalMessagePrompt: 'Add a personal message (optional)', genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.', - welcomeNote: ({workspaceName}) => `You have been invited to ${workspaceName || 'a workspace'}! Download the Expensify mobile app at use.expensify.com/download to start tracking your expenses.`, pleaseEnterValidLogin: `Please ensure the email or phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, }, + inviteMessage: { + inviteMessageTitle: 'Add message', + inviteMessagePrompt: 'Make your invitation extra special by adding a message below', + personalMessagePrompt: 'Message', + genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.', + welcomeNote: ({workspaceName}) => `You have been invited to ${workspaceName || 'a workspace'}! Download the Expensify mobile app at use.expensify.com/download to start tracking your expenses.`, + }, editor: { nameInputLabel: 'Name', nameInputHelpText: 'This is the name you will see on your workspace.', diff --git a/src/languages/es.js b/src/languages/es.js index f7012c3d6654..dc9b3c703591 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -1014,11 +1014,16 @@ export default { }, invite: { invitePeople: 'Invitar nuevos miembros', - personalMessagePrompt: 'Agregar un mensaje personal (Opcional)', genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', - welcomeNote: ({workspaceName}) => `¡Has sido invitado a ${workspaceName}! Descargue la aplicación móvil Expensify en use.expensify.com/download para comenzar a rastrear sus gastos.`, pleaseEnterValidLogin: `Asegúrese de que el correo electrónico o el número de teléfono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`, }, + inviteMessage: { + inviteMessageTitle: 'Añadir un mensaje', + inviteMessagePrompt: 'Añadir un mensaje para hacer tu invitación destacar', + personalMessagePrompt: 'Mensaje', + genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', + welcomeNote: ({workspaceName}) => `¡Has sido invitado a ${workspaceName}! Descargue la aplicación móvil Expensify en use.expensify.com/download para comenzar a rastrear sus gastos.`, + }, editor: { nameInputLabel: 'Nombre', nameInputHelpText: 'Este es el nombre que verás en tu espacio de trabajo.', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 405ea6c6c332..aa5ad473a659 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -455,6 +455,13 @@ const SettingsModalStackNavigator = createModalStackNavigator([ }, name: 'Workspace_Invite', }, + { + getComponent: () => { + const WorkspaceInviteMessagePage = require('../../../pages/workspace/WorkspaceInviteMessagePage').default; + return WorkspaceInviteMessagePage; + }, + name: 'Workspace_Invite_Message', + }, { getComponent: () => { const WorkspaceNewRoomPage = require('../../../pages/workspace/WorkspaceNewRoomPage').default; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 080b8d148cff..abfd7c71cf3e 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -170,6 +170,9 @@ export default { Workspace_Invite: { path: ROUTES.WORKSPACE_INVITE, }, + Workspace_Invite_Message: { + path: ROUTES.WORKSPACE_INVITE_MESSAGE, + }, Workspace_NewRoom: { path: ROUTES.WORKSPACE_NEW_ROOM, }, diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index f1cbb66cbdeb..d82381eeccaf 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1011,6 +1011,10 @@ function openWorkspaceInvitePage(policyID, clientMemberEmails) { }); } +function setWorkspaceInviteMembersDraft(policyID, memberEmails) { + Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${policyID}`, memberEmails); +} + /** * * @param {String} reportID @@ -1070,6 +1074,7 @@ export { openWorkspaceMembersPage, openWorkspaceInvitePage, removeWorkspace, + setWorkspaceInviteMembersDraft, isPolicyOwner, leaveRoom, }; diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 1fe9a18a8260..7c1184434eba 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -160,6 +160,7 @@ class InitialSettingsPage extends React.Component { action: () => { Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); }, floatRightAvatars: policiesAvatars, shouldStackHorizontally: true, + avatarSize: CONST.AVATAR_SIZE.SMALLER, brickRoadIndicator: policyBrickRoadIndicator, }, { @@ -224,6 +225,7 @@ class InitialSettingsPage extends React.Component { brickRoadIndicator={item.brickRoadIndicator} floatRightAvatars={item.floatRightAvatars} shouldStackHorizontally={item.shouldStackHorizontally} + avatarSize={item.avatarSize} /> ); } diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js new file mode 100644 index 000000000000..ca1a6cf852db --- /dev/null +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -0,0 +1,211 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {Pressable, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import Str from 'expensify-common/lib/str'; +import lodashGet from 'lodash/get'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; +import ScreenWrapper from '../../components/ScreenWrapper'; +import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import Navigation from '../../libs/Navigation/Navigation'; +import styles from '../../styles/styles'; +import compose from '../../libs/compose'; +import ONYXKEYS from '../../ONYXKEYS'; +import * as Policy from '../../libs/actions/Policy'; +import TextInput from '../../components/TextInput'; +import MultipleAvatars from '../../components/MultipleAvatars'; +import CONST from '../../CONST'; +import * as Link from '../../libs/actions/Link'; +import Text from '../../components/Text'; +import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy'; +import * as ReportUtils from '../../libs/ReportUtils'; +import ROUTES from '../../ROUTES'; +import * as Localize from '../../libs/Localize'; +import Form from '../../components/Form'; + +const personalDetailsPropTypes = PropTypes.shape({ + /** The login of the person (either email or phone number) */ + login: PropTypes.string.isRequired, + + /** The URL of the person's avatar (there should already be a default avatar if + the person doesn't have their own avatar uploaded yet) */ + avatar: PropTypes.string.isRequired, + + /** This is either the user's full name, or their login if full name is an empty string */ + displayName: PropTypes.string.isRequired, +}); + +const propTypes = { + + /** All of the personal details for everyone */ + personalDetails: PropTypes.objectOf(personalDetailsPropTypes).isRequired, + + invitedMembersDraft: PropTypes.arrayOf(PropTypes.string).isRequired, + + /** URL Route params */ + route: PropTypes.shape({ + /** Params from the URL path */ + params: PropTypes.shape({ + /** policyID passed via route: /workspace/:policyID/invite-message */ + policyID: PropTypes.string, + }), + }).isRequired, + + ...policyPropTypes, + ...withLocalizePropTypes, +}; + +const defaultProps = policyDefaultProps; + +class WorkspaceInviteMessagePage extends React.Component { + constructor(props) { + super(props); + + this.onSubmit = this.onSubmit.bind(this); + this.validate = this.validate.bind(this); + this.openPrivacyURL = this.openPrivacyURL.bind(this); + this.state = { + welcomeNote: this.getWelcomeNote(), + }; + } + + componentDidUpdate(prevProps) { + if ( + !(prevProps.preferredLocale !== this.props.preferredLocale + && this.state.welcomeNote === Localize.translate(prevProps.preferredLocale, 'workspace.inviteMessage.welcomeNote', {workspaceName: this.props.policy.name}) + ) + ) { + return; + } + this.setState({welcomeNote: this.getWelcomeNote()}); + } + + onSubmit() { + const filteredLogins = _.uniq(_.compact(_.map(this.props.invitedMembersDraft, login => login.toLowerCase().trim()))); + Policy.addMembersToWorkspace(filteredLogins, this.state.welcomeNote || this.getWelcomeNote(), this.props.route.params.policyID); + Navigation.navigate(ROUTES.getWorkspaceMembersRoute(this.props.route.params.policyID)); + } + + getAvatars() { + const filteredPersonalDetails = _.pick(this.props.personalDetails, this.props.invitedMembersDraft); + return _.map(filteredPersonalDetails, personalDetail => ({ + source: ReportUtils.getAvatar(personalDetail.avatar, personalDetail.login), + type: CONST.ICON_TYPE_AVATAR, + name: personalDetail.login, + })); + } + + getWelcomeNote() { + return this.props.translate('workspace.inviteMessage.welcomeNote', { + workspaceName: this.props.policy.name, + }); + } + + getAvatarTooltips() { + const filteredPersonalDetails = _.pick(this.props.personalDetails, this.props.invitedMembersDraft); + return _.map(filteredPersonalDetails, personalDetail => Str.removeSMSDomain(personalDetail.login)); + } + + openPrivacyURL(e) { + e.preventDefault(); + Link.openExternalLink(CONST.PRIVACY_URL); + } + + /** + * @returns {Boolean} + */ + validate() { + // No validation required as the invite message is optional + return {}; + } + + render() { + const policyName = lodashGet(this.props.policy, 'name'); + + return ( + + Navigation.dismissModal()} + onBackButtonPress={() => Navigation.goBack()} + /> +
+ + + {this.props.translate('common.privacy')} + + + + ) + } + > + + + + + + {this.props.translate('workspace.inviteMessage.inviteMessagePrompt')} + + + + this.setState({welcomeNote: text})} + /> + +
+
+ ); + } +} + +WorkspaceInviteMessagePage.propTypes = propTypes; +WorkspaceInviteMessagePage.defaultProps = defaultProps; + +export default compose( + withLocalize, + withPolicy, + withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS, + }, + invitedMembersDraft: { + key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, + }, + }), +)(WorkspaceInviteMessagePage); diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index e1fbdbeda817..8c61b3952371 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {Pressable, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import lodashGet from 'lodash/get'; @@ -12,7 +12,6 @@ import styles from '../../styles/styles'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import * as Policy from '../../libs/actions/Policy'; -import TextInput from '../../components/TextInput'; import FormAlertWithSubmitButton from '../../components/FormAlertWithSubmitButton'; import FormSubmit from '../../components/FormSubmit'; import OptionsSelector from '../../components/OptionsSelector'; @@ -20,13 +19,11 @@ import * as OptionsListUtils from '../../libs/OptionsListUtils'; import CONST from '../../CONST'; import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator'; import * as Link from '../../libs/actions/Link'; -import Text from '../../components/Text'; import withPolicy, {policyPropTypes, policyDefaultProps} from './withPolicy'; import {withNetwork} from '../../components/OnyxProvider'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; import networkPropTypes from '../../components/networkPropTypes'; import ROUTES from '../../ROUTES'; -import * as Localize from '../../libs/Localize'; const personalDetailsPropTypes = PropTypes.shape({ /** The login of the person (either email or phone number) */ @@ -92,7 +89,6 @@ class WorkspaceInvitePage extends React.Component { personalDetails, selectedOptions: [], userToInvite, - welcomeNote: this.getWelcomeNote(), shouldDisableButton: false, }; } @@ -105,13 +101,6 @@ class WorkspaceInvitePage extends React.Component { } componentDidUpdate(prevProps) { - if ( - prevProps.preferredLocale !== this.props.preferredLocale - && this.state.welcomeNote === Localize.translate(prevProps.preferredLocale, 'workspace.invite.welcomeNote', {workspaceName: this.props.policy.name}) - ) { - this.setState({welcomeNote: this.getWelcomeNote()}); - } - const isReconnecting = prevProps.network.isOffline && !this.props.network.isOffline; if (!isReconnecting) { return; @@ -131,17 +120,6 @@ class WorkspaceInvitePage extends React.Component { return [...CONST.EXPENSIFY_EMAILS, ...usersToExclude]; } - /** - * Gets the welcome note default text - * - * @returns {Object} - */ - getWelcomeNote() { - return this.props.translate('workspace.invite.welcomeNote', { - workspaceName: this.props.policy.name, - }); - } - /** * @returns {Boolean} */ @@ -268,13 +246,10 @@ class WorkspaceInvitePage extends React.Component { if (!this.validate()) { return; } - - this.setState({shouldDisableButton: true}, () => { - const logins = _.map(this.state.selectedOptions, option => option.login); - const filteredLogins = _.uniq(_.compact(_.map(logins, login => login.toLowerCase().trim()))); - Policy.addMembersToWorkspace(filteredLogins, this.state.welcomeNote, this.props.route.params.policyID); - Navigation.goBack(); - }); + const logins = _.map(this.state.selectedOptions, option => option.login); + const filteredLogins = _.uniq(_.compact(_.map(logins, login => login.toLowerCase().trim()))); + Policy.setWorkspaceInviteMembersDraft(this.props.route.params.policyID, filteredLogins); + Navigation.navigate(ROUTES.getWorkspaceInviteMessageRoute(this.props.route.params.policyID)); } /** @@ -338,41 +313,16 @@ class WorkspaceInvitePage extends React.Component { )}
- - this.setState({welcomeNote: text})} - /> - - - - - {this.props.translate('common.privacy')} - - - diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index 17893cb94c85..49897c4a11c4 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -80,6 +80,36 @@ function getAvatarStyle(size) { }; } +function getAvatarExtraFontSize(size) { + const AVATAR_SIZES = { + [CONST.AVATAR_SIZE.DEFAULT]: variables.fontSizeNormal, + [CONST.AVATAR_SIZE.SMALL_SUBSCRIPT]: variables.fontSizeExtraSmall, + [CONST.AVATAR_SIZE.MID_SUBSCRIPT]: variables.fontSizeExtraSmall, + [CONST.AVATAR_SIZE.SUBSCRIPT]: variables.fontSizeExtraSmall, + [CONST.AVATAR_SIZE.SMALL]: variables.fontSizeSmall, + [CONST.AVATAR_SIZE.SMALLER]: variables.fontSizeExtraSmall, + [CONST.AVATAR_SIZE.LARGE]: variables.fontSizeXLarge, + [CONST.AVATAR_SIZE.MEDIUM]: variables.fontSizeMedium, + [CONST.AVATAR_SIZE.LARGE_BORDERED]: variables.fontSizeXLarge, + }; + return AVATAR_SIZES[size]; +} + +function getAvatarBorderSize(size) { + const AVATAR_SIZES = { + [CONST.AVATAR_SIZE.DEFAULT]: 3, + [CONST.AVATAR_SIZE.SMALL_SUBSCRIPT]: 2, + [CONST.AVATAR_SIZE.MID_SUBSCRIPT]: 2, + [CONST.AVATAR_SIZE.SUBSCRIPT]: 2, + [CONST.AVATAR_SIZE.SMALL]: 3, + [CONST.AVATAR_SIZE.SMALLER]: 2, + [CONST.AVATAR_SIZE.LARGE]: 4, + [CONST.AVATAR_SIZE.MEDIUM]: 3, + [CONST.AVATAR_SIZE.LARGE_BORDERED]: 4, + }; + return AVATAR_SIZES[size]; +} + /** * Return the border radius for an avatar * @@ -930,6 +960,8 @@ export { getAvatarSize, getAvatarStyle, getAvatarBorderStyle, + getAvatarExtraFontSize, + getAvatarBorderSize, getErrorPageContainerStyle, getSafeAreaPadding, getSafeAreaMargins, diff --git a/src/styles/styles.js b/src/styles/styles.js index 14a2b992eba0..3baf34f7dbee 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1673,14 +1673,6 @@ const styles = { borderRadius: 24, }, - horizontalStackedAvatar: { - height: 28, - width: 28, - backgroundColor: themeColors.appBG, - paddingTop: 2, - alignItems: 'center', - }, - singleSubscript: { height: variables.iconSizeNormal, width: variables.iconSizeNormal, @@ -1812,38 +1804,8 @@ const styles = { width: variables.avatarSizeSmall, }, - horizontalStackedAvatar1: { - left: -19, - top: -79, - zIndex: 2, - }, - - horizontalStackedAvatar2: { - left: 1, - top: -51, - zIndex: 3, - }, - - horizontalStackedAvatar3: { - left: 21, - top: -23, - zIndex: 4, - }, - - horizontalStackedAvatar4: { - top: 5, - left: 41, - zIndex: 5, - }, - horizontalStackedAvatar4Overlay: { - top: -107, - left: 41, - height: 28, - width: 28, - borderWidth: 2, borderStyle: 'solid', - zIndex: 6, }, modalViewContainer: { From 8455cf4d60fe9817f2a647a6371665ea5958eda0 Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Fri, 24 Mar 2023 21:28:38 +0530 Subject: [PATCH 010/108] Merge changes --- src/components/MultipleAvatars.js | 2 +- src/styles/StyleUtils.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js index 735525461675..afff5b3f8a2e 100644 --- a/src/components/MultipleAvatars.js +++ b/src/components/MultipleAvatars.js @@ -83,7 +83,7 @@ const MultipleAvatars = (props) => { } const oneAvatarSize = StyleUtils.getAvatarStyle(props.size); - const oneAvatarBorderWidth = StyleUtils.getAvatarBorderSize(props.size); + const oneAvatarBorderWidth = StyleUtils.getAvatarBorderWidth(props.size); const overlapSize = oneAvatarSize.width / 3; if (props.shouldStackHorizontally) { diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index 49897c4a11c4..6b7d3f23d9f5 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -95,7 +95,7 @@ function getAvatarExtraFontSize(size) { return AVATAR_SIZES[size]; } -function getAvatarBorderSize(size) { +function getAvatarBorderWidth(size) { const AVATAR_SIZES = { [CONST.AVATAR_SIZE.DEFAULT]: 3, [CONST.AVATAR_SIZE.SMALL_SUBSCRIPT]: 2, @@ -959,6 +959,8 @@ function getDirectionStyle(direction) { export { getAvatarSize, getAvatarStyle, + getAvatarExtraFontSize, + getAvatarBorderWidth, getAvatarBorderStyle, getAvatarExtraFontSize, getAvatarBorderSize, From b3f2a1f0cd157328bd4b29f2e9d17fb1665f6002 Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Fri, 24 Mar 2023 21:35:38 +0530 Subject: [PATCH 011/108] Fixing issues --- src/styles/StyleUtils.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index 6b7d3f23d9f5..7823f2849df4 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -962,8 +962,6 @@ export { getAvatarExtraFontSize, getAvatarBorderWidth, getAvatarBorderStyle, - getAvatarExtraFontSize, - getAvatarBorderSize, getErrorPageContainerStyle, getSafeAreaPadding, getSafeAreaMargins, From 8eb8cf9f458d590a35a865a3143eb1fdc9dfe916 Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Fri, 24 Mar 2023 22:31:43 +0530 Subject: [PATCH 012/108] Fixing lint failures --- src/pages/workspace/WorkspaceInviteMessagePage.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index ca1a6cf852db..2bb16c5278e8 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -39,9 +39,9 @@ const personalDetailsPropTypes = PropTypes.shape({ const propTypes = { /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropTypes).isRequired, + personalDetails: PropTypes.objectOf(personalDetailsPropTypes), - invitedMembersDraft: PropTypes.arrayOf(PropTypes.string).isRequired, + invitedMembersDraft: PropTypes.arrayOf(PropTypes.string), /** URL Route params */ route: PropTypes.shape({ @@ -56,7 +56,11 @@ const propTypes = { ...withLocalizePropTypes, }; -const defaultProps = policyDefaultProps; +const defaultProps = { + ...policyDefaultProps, + personalDetails: {}, + invitedMembersDraft: [], +}; class WorkspaceInviteMessagePage extends React.Component { constructor(props) { From c6926f1d0e07248f89d362f829f4a9dc6a302e5f Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Sat, 25 Mar 2023 12:31:11 +0530 Subject: [PATCH 013/108] Fixing regression --- src/components/MenuItem.js | 2 +- src/pages/workspace/WorkspaceInviteMessagePage.js | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 19063a9e52ba..fbaa4c71d90b 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -182,7 +182,7 @@ const MenuItem = (props) => { isHovered={hovered} isPressed={pressed} icons={props.floatRightAvatars} - size={props.avatarSize ? props.avatarSize : fallbackAvatarSize} + size={props.avatarSize || fallbackAvatarSize} fallbackIcon={defaultWorkspaceAvatars.WorkspaceBuilding} shouldStackHorizontally={props.shouldStackHorizontally} /> diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index 2bb16c5278e8..4202ca792f29 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -92,12 +92,14 @@ class WorkspaceInviteMessagePage extends React.Component { } getAvatars() { - const filteredPersonalDetails = _.pick(this.props.personalDetails, this.props.invitedMembersDraft); - return _.map(filteredPersonalDetails, personalDetail => ({ - source: ReportUtils.getAvatar(personalDetail.avatar, personalDetail.login), - type: CONST.ICON_TYPE_AVATAR, - name: personalDetail.login, - })); + return _.map(this.props.invitedMembersDraft, (memberlogin) => { + const userPersonalDetail = lodashGet(this.props.personalDetails, memberlogin, {login: memberlogin, avatar: ''}); + return { + source: ReportUtils.getAvatar(userPersonalDetail.avatar, userPersonalDetail.login), + type: CONST.ICON_TYPE_AVATAR, + name: userPersonalDetail.login, + }; + }); } getWelcomeNote() { From 4f8fc62d4bfeef5b082ccfc8a74de943ca80473e Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Sat, 25 Mar 2023 13:08:27 +0530 Subject: [PATCH 014/108] Resolving calculation comment --- src/components/MultipleAvatars.js | 8 ++------ src/styles/StyleUtils.js | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js index afff5b3f8a2e..23b5e499d3ce 100644 --- a/src/components/MultipleAvatars.js +++ b/src/components/MultipleAvatars.js @@ -119,9 +119,7 @@ const MultipleAvatars = (props) => { style={[styles.justifyContentCenter, styles.alignItemsCenter, StyleUtils.getHorizontalStackedAvatarBorderStyle(props.isHovered, props.isPressed), - { - left: -(overlapSize * index), borderRadius: oneAvatarSize.width, borderWidth: oneAvatarBorderWidth, zIndex: index + 2, - }, + StyleUtils.getHorizontalStackedAvatarStyle(index, overlapSize, oneAvatarBorderWidth, oneAvatarSize.width), (icon.type === CONST.ICON_TYPE_WORKSPACE ? StyleUtils.getAvatarBorderRadius(props.size, icon.type) : {}), ]} > @@ -145,9 +143,7 @@ const MultipleAvatars = (props) => { // Set overlay background color with RGBA value so that the text will not inherit opacity StyleUtils.getBackgroundColorWithOpacityStyle(themeColors.overlay, variables.overlayOpacity), styles.horizontalStackedAvatar4Overlay, - { - borderWidth: oneAvatarBorderWidth, borderRadius: oneAvatarSize.width, left: -((oneAvatarSize.width * 2) + (oneAvatarBorderWidth * 2)), zIndex: 6, - }, + StyleUtils.getHorizontalStackedOverlayAvatarStyle(oneAvatarSize, oneAvatarBorderWidth), (props.icons[3].type === CONST.ICON_TYPE_WORKSPACE ? StyleUtils.getAvatarBorderRadius(props.size, props.icons[3].type) : {}), ]} > diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index 7823f2849df4..7f774145a6e2 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -768,6 +768,24 @@ function getHorizontalStackedAvatarBorderStyle(isHovered, isPressed) { }; } +function getHorizontalStackedAvatarStyle(index, overlapSize, borderWidth, borderRadius) { + return { + left: -(overlapSize * index), + borderRadius, + borderWidth, + zIndex: index + 2, + }; +} + +function getHorizontalStackedOverlayAvatarStyle(oneAvatarSize, oneAvatarBorderWidth) { + return { + borderWidth: oneAvatarBorderWidth, + borderRadius: oneAvatarSize.width, + left: -((oneAvatarSize.width * 2) + (oneAvatarBorderWidth * 2)), + zIndex: 6, + }; +} + /** * @param {Number} safeAreaPaddingBottom * @returns {Object} @@ -996,6 +1014,8 @@ export { getMinimumHeight, fade, getHorizontalStackedAvatarBorderStyle, + getHorizontalStackedAvatarStyle, + getHorizontalStackedOverlayAvatarStyle, getReportWelcomeBackgroundImageStyle, getReportWelcomeTopMarginStyle, getReportWelcomeContainerStyle, From cd795e10ec0a0ed811ad05c6a6f280def406a8be Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Sat, 25 Mar 2023 13:26:27 +0530 Subject: [PATCH 015/108] Fixing future comments --- src/ONYXKEYS.js | 1 + src/pages/workspace/WorkspaceInviteMessagePage.js | 15 +++------------ src/pages/workspace/WorkspaceInvitePage.js | 6 +++++- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index dc58aa6543c8..63fb45033de0 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -183,6 +183,7 @@ export default { PROFILE_SETTINGS_FORM: 'profileSettingsForm', DISPLAY_NAME_FORM: 'displayNameForm', LEGAL_NAME_FORM: 'legalNameForm', + WORKSPACE_INVITE_MESSAGE_FORM: 'workspaceInviteMessageForm', DATE_OF_BIRTH_FORM: 'dateOfBirthForm', HOME_ADDRESS_FORM: 'homeAddressForm', NEW_ROOM_FORM: 'newRoomForm', diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index 4202ca792f29..094a220fe018 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -86,8 +86,7 @@ class WorkspaceInviteMessagePage extends React.Component { } onSubmit() { - const filteredLogins = _.uniq(_.compact(_.map(this.props.invitedMembersDraft, login => login.toLowerCase().trim()))); - Policy.addMembersToWorkspace(filteredLogins, this.state.welcomeNote || this.getWelcomeNote(), this.props.route.params.policyID); + Policy.addMembersToWorkspace(this.props.invitedMembersDraft, this.state.welcomeNote || this.getWelcomeNote(), this.props.route.params.policyID); Navigation.navigate(ROUTES.getWorkspaceMembersRoute(this.props.route.params.policyID)); } @@ -118,14 +117,6 @@ class WorkspaceInviteMessagePage extends React.Component { Link.openExternalLink(CONST.PRIVACY_URL); } - /** - * @returns {Boolean} - */ - validate() { - // No validation required as the invite message is optional - return {}; - } - render() { const policyName = lodashGet(this.props.policy, 'name'); @@ -142,8 +133,8 @@ class WorkspaceInviteMessagePage extends React.Component { />
{}} onSubmit={this.onSubmit} submitButtonText={this.props.translate('common.save')} enabledWhenOffline diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 8c61b3952371..ae33789fff0b 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -247,7 +247,11 @@ class WorkspaceInvitePage extends React.Component { return; } const logins = _.map(this.state.selectedOptions, option => option.login); - const filteredLogins = _.uniq(_.compact(_.map(logins, login => login.toLowerCase().trim()))); + const filteredLogins = _.chain(logins) + .map(login => login.toLowerCase().trim()) + .compact() + .uniq() + .value(); Policy.setWorkspaceInviteMembersDraft(this.props.route.params.policyID, filteredLogins); Navigation.navigate(ROUTES.getWorkspaceInviteMessageRoute(this.props.route.params.policyID)); } From b9157482d78b12cb70c74f646cb623bfd5f500f8 Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Sat, 25 Mar 2023 14:31:12 +0530 Subject: [PATCH 016/108] adding JSDocs --- .../workspace/WorkspaceInviteMessagePage.js | 8 +++++-- src/styles/StyleUtils.js | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index 094a220fe018..afbdc05a08bb 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -112,8 +112,12 @@ class WorkspaceInviteMessagePage extends React.Component { return _.map(filteredPersonalDetails, personalDetail => Str.removeSMSDomain(personalDetail.login)); } - openPrivacyURL(e) { - e.preventDefault(); + /** + * Opens privacy url as an external link + * @param {Object} event + */ + openPrivacyURL(event) { + event.preventDefault(); Link.openExternalLink(CONST.PRIVACY_URL); } diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index 7f774145a6e2..055a2339bbed 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -80,6 +80,11 @@ function getAvatarStyle(size) { }; } +/** + * Get Font size of '+1' text on avatar overlay + * @param {String} size + * @returns {Number} + */ function getAvatarExtraFontSize(size) { const AVATAR_SIZES = { [CONST.AVATAR_SIZE.DEFAULT]: variables.fontSizeNormal, @@ -95,6 +100,11 @@ function getAvatarExtraFontSize(size) { return AVATAR_SIZES[size]; } +/** + * Get Bordersize of Avatar based on avatar size + * @param {String} size + * @returns {Number} + */ function getAvatarBorderWidth(size) { const AVATAR_SIZES = { [CONST.AVATAR_SIZE.DEFAULT]: 3, @@ -768,6 +778,14 @@ function getHorizontalStackedAvatarBorderStyle(isHovered, isPressed) { }; } +/** + * Get computed avatar styles based on position and border size + * @param {Number} index + * @param {Number} overlapSize + * @param {Number} borderWidth + * @param {Number} borderRadius + * @returns {Object} + */ function getHorizontalStackedAvatarStyle(index, overlapSize, borderWidth, borderRadius) { return { left: -(overlapSize * index), @@ -777,6 +795,12 @@ function getHorizontalStackedAvatarStyle(index, overlapSize, borderWidth, border }; } +/** + * Get computed avatar styles of '+1' overlay based on size + * @param {Object} oneAvatarSize + * @param {Numer} oneAvatarBorderWidth + * @returns {Object} + */ function getHorizontalStackedOverlayAvatarStyle(oneAvatarSize, oneAvatarBorderWidth) { return { borderWidth: oneAvatarBorderWidth, From da2352ed669a0da92a5bdebc6f26af49cedccd3f Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Sat, 25 Mar 2023 14:46:26 +0530 Subject: [PATCH 017/108] Fixing errors --- src/pages/workspace/WorkspaceInviteMessagePage.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index afbdc05a08bb..58e9597c5547 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -67,7 +67,6 @@ class WorkspaceInviteMessagePage extends React.Component { super(props); this.onSubmit = this.onSubmit.bind(this); - this.validate = this.validate.bind(this); this.openPrivacyURL = this.openPrivacyURL.bind(this); this.state = { welcomeNote: this.getWelcomeNote(), @@ -138,7 +137,7 @@ class WorkspaceInviteMessagePage extends React.Component { {}} + validate={() => ({})} onSubmit={this.onSubmit} submitButtonText={this.props.translate('common.save')} enabledWhenOffline From df8e7c0621c6acfc1268cbb705b9695f24c7fb76 Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Tue, 28 Mar 2023 20:27:56 +0530 Subject: [PATCH 018/108] Fixing PR style comments --- src/components/MenuItem.js | 1 + src/components/MultipleAvatars.js | 2 +- src/pages/workspace/WorkspaceInviteMessagePage.js | 6 ++++-- src/pages/workspace/WorkspaceInvitePage.js | 2 +- src/styles/StyleUtils.js | 1 + src/styles/styles.js | 4 ---- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index fbaa4c71d90b..c05c9fd294eb 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -50,6 +50,7 @@ const defaultProps = { brickRoadIndicator: '', floatRightAvatars: [], shouldStackHorizontally: false, + avatarSize: undefined, }; const MenuItem = (props) => { diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js index 23b5e499d3ce..af02ba397fbd 100644 --- a/src/components/MultipleAvatars.js +++ b/src/components/MultipleAvatars.js @@ -113,7 +113,7 @@ const MultipleAvatars = (props) => { {props.shouldStackHorizontally ? ( <> { - _.map([...props.icons].splice(0, 4).reverse(), (icon, index) => ( + _.map([...props.icons].splice(0, 4), (icon, index) => ( ({})} onSubmit={this.onSubmit} - submitButtonText={this.props.translate('common.save')} + submitButtonText={this.props.translate('common.invite')} enabledWhenOffline footerContent={ ( @@ -147,7 +149,7 @@ class WorkspaceInviteMessagePage extends React.Component { onPress={this.openPrivacyURL} accessibilityRole="link" href={CONST.PRIVACY_URL} - style={[styles.mh5, styles.mb3, styles.alignSelfStart]} + style={[styles.mb5, styles.alignSelfStart]} > diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index ae33789fff0b..10519571740c 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -323,7 +323,7 @@ class WorkspaceInvitePage extends React.Component { buttonText={this.props.translate('common.next')} onSubmit={this.inviteUser} message={this.props.policy.alertMessage} - containerStyles={[styles.flexReset, styles.mb0, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb3]} + containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter /> diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js index 055a2339bbed..bf270a50d9c8 100644 --- a/src/styles/StyleUtils.js +++ b/src/styles/StyleUtils.js @@ -807,6 +807,7 @@ function getHorizontalStackedOverlayAvatarStyle(oneAvatarSize, oneAvatarBorderWi borderRadius: oneAvatarSize.width, left: -((oneAvatarSize.width * 2) + (oneAvatarBorderWidth * 2)), zIndex: 6, + borderStyle: 'solid', }; } diff --git a/src/styles/styles.js b/src/styles/styles.js index 3baf34f7dbee..9538aed8a2ba 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1804,10 +1804,6 @@ const styles = { width: variables.avatarSizeSmall, }, - horizontalStackedAvatar4Overlay: { - borderStyle: 'solid', - }, - modalViewContainer: { alignItems: 'center', flex: 1, From 387cd96f9aea2339dfadde5dda6d3a08d96d8fc7 Mon Sep 17 00:00:00 2001 From: Ana Margarida Silva Date: Thu, 30 Mar 2023 12:49:09 +0100 Subject: [PATCH 019/108] 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 020/108] 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 021/108] 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 022/108] 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 023/108] 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 024/108] 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 025/108] 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 fabd9af9c1ab788cbfb8a548d85cff48dbe21737 Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Tue, 4 Apr 2023 11:29:05 +0530 Subject: [PATCH 026/108] Reseting draft after submit --- src/pages/workspace/WorkspaceInviteMessagePage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index 5774af4480e1..107ec9db81b9 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -86,6 +86,7 @@ class WorkspaceInviteMessagePage extends React.Component { onSubmit() { Policy.addMembersToWorkspace(this.props.invitedMembersDraft, this.state.welcomeNote || this.getWelcomeNote(), this.props.route.params.policyID); + Policy.setWorkspaceInviteMembersDraft(this.props.route.params.policyID, []); Navigation.navigate(ROUTES.getWorkspaceMembersRoute(this.props.route.params.policyID)); } From f3eb8ace3270981b32bc056416f95c0cada6d507 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Thu, 30 Mar 2023 13:12:07 +0200 Subject: [PATCH 027/108] optimistically create policy expense chats for new members of workspaces --- src/libs/actions/Policy.js | 80 +++++++++++++++++++++- src/pages/workspace/WorkspaceInvitePage.js | 2 +- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index e04d1719c700..6582690c1911 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -15,6 +15,7 @@ import DateUtils from '../DateUtils'; import * as ReportUtils from '../ReportUtils'; import Log from '../Log'; import * as Report from './Report'; +import Permissions from '../Permissions'; const allPolicies = {}; Onyx.connect({ @@ -222,17 +223,91 @@ function removeMembers(members, policyID) { }, {optimisticData, failureData}); } +/** +* Optimistically create a chat for each member of the workspace, creates both optimistic and success data for onyx. +* +* @param {String} policyID +* @param {Array} members +* @param {Array} betas +* @returns {Object} - object with onyxSuccessData, onyxOptimisticData, and optimisticReportIDs (map login to reportID) +*/ +function createPolicyExpenseChats(policyID, members, betas) { + const workspaceMembersChats = { + onyxSuccessData: [], + onyxOptimisticData: [], + optimisticReportIDs: {}, + }; + + // If the user is not in the beta, we don't want to create any chats + if (!Permissions.canUsePolicyExpenseChat(betas)) { + return workspaceMembersChats; + } + + _.each(members, (login) => { + const optimisticReport = ReportUtils.buildOptimisticChatReport( + [login], + undefined, + CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + policyID, + login, + ); + const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(optimisticReport.ownerEmail); + + workspaceMembersChats.optimisticReportIDs[login] = optimisticReport.reportID; + workspaceMembersChats.onyxOptimisticData.push({ + onyxMethod: CONST.ONYX.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReport.reportID}`, + value: { + ...optimisticReport, + pendingFields: { + createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + isOptimisticReport: true, + }, + }); + workspaceMembersChats.onyxOptimisticData.push({ + onyxMethod: CONST.ONYX.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticReport.reportID}`, + value: {[optimisticCreatedAction.reportActionID]: optimisticCreatedAction}, + }); + workspaceMembersChats.onyxSuccessData.push({ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReport.reportID}`, + value: { + isLoadingReportActions: false, + pendingFields: { + createChat: null, + }, + errorFields: { + createChat: null, + }, + isOptimisticReport: false, + }, + }); + workspaceMembersChats.onyxSuccessData.push({ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticReport.reportID}`, + value: {[optimisticCreatedAction.reportActionID]: {pendingAction: null}}, + }); + }); + return workspaceMembersChats; +} + /** * Adds members to the specified workspace/policyID * * @param {Array} memberLogins * @param {String} welcomeNote * @param {String} policyID + * @param {Array} betas */ -function addMembersToWorkspace(memberLogins, welcomeNote, policyID) { +function addMembersToWorkspace(memberLogins, welcomeNote, policyID, betas) { const membersListKey = `${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`; const logins = _.map(memberLogins, memberLogin => OptionsListUtils.addSMSDomainIfPhoneNumber(memberLogin)); + // create onyx data for policy expense chats for each new member + const membersChats = createPolicyExpenseChats(policyID, logins, betas); + const optimisticData = [ { onyxMethod: CONST.ONYX.METHOD.MERGE, @@ -241,6 +316,7 @@ function addMembersToWorkspace(memberLogins, welcomeNote, policyID) { // Convert to object with each key containing {pendingAction: ‘add’} value: _.object(logins, Array(logins.length).fill({pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD})), }, + ...membersChats.onyxOptimisticData, ]; const successData = [ @@ -252,6 +328,7 @@ function addMembersToWorkspace(memberLogins, welcomeNote, policyID) { // need to remove the members since that will be handled by onClose of OfflineWithFeedback. value: _.object(logins, Array(logins.length).fill({pendingAction: null, errors: null})), }, + ...membersChats.onyxSuccessData, ]; const failureData = [ @@ -275,6 +352,7 @@ function addMembersToWorkspace(memberLogins, welcomeNote, policyID) { // Escape HTML special chars to enable them to appear in the invite email welcomeNote: _.escape(welcomeNote), policyID, + optimisticReportIDs: JSON.stringify(membersChats.optimisticReportIDs), }, {optimisticData, successData, failureData}); } diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index e1fbdbeda817..b2bba213122d 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -272,7 +272,7 @@ class WorkspaceInvitePage extends React.Component { this.setState({shouldDisableButton: true}, () => { const logins = _.map(this.state.selectedOptions, option => option.login); const filteredLogins = _.uniq(_.compact(_.map(logins, login => login.toLowerCase().trim()))); - Policy.addMembersToWorkspace(filteredLogins, this.state.welcomeNote, this.props.route.params.policyID); + Policy.addMembersToWorkspace(filteredLogins, this.state.welcomeNote, this.props.route.params.policyID, this.props.betas); Navigation.goBack(); }); } From e928f1dc0e469b0dd723db622c6122da88e4be3e Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Fri, 7 Apr 2023 15:37:12 +0200 Subject: [PATCH 028/108] add onyx failure data for creating policy members chats --- src/libs/actions/Policy.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 6582690c1911..2c73566673c0 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -235,6 +235,7 @@ function createPolicyExpenseChats(policyID, members, betas) { const workspaceMembersChats = { onyxSuccessData: [], onyxOptimisticData: [], + onyxFailureData: [], optimisticReportIDs: {}, }; @@ -289,6 +290,13 @@ function createPolicyExpenseChats(policyID, members, betas) { key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticReport.reportID}`, value: {[optimisticCreatedAction.reportActionID]: {pendingAction: null}}, }); + workspaceMembersChats.onyxFailureData.push({ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReport.reportID}`, + value: { + isLoadingReportActions: false, + }, + }); }); return workspaceMembersChats; } @@ -344,6 +352,7 @@ function addMembersToWorkspace(memberLogins, welcomeNote, policyID, betas) { }, })), }, + ...membersChats.onyxFailureData, ]; API.write('AddMembersToWorkspace', { From c6142bfc54cb90f926d6b43cb6d76b321b5729b6 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Fri, 7 Apr 2023 18:27:19 +0200 Subject: [PATCH 029/108] remove isLoadingReportActions key from onyx success data --- src/libs/actions/Policy.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 2c73566673c0..6d4a6124f114 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -275,7 +275,6 @@ function createPolicyExpenseChats(policyID, members, betas) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReport.reportID}`, value: { - isLoadingReportActions: false, pendingFields: { createChat: null, }, From cfdcc1234b83af4b488cbed6cf535a0e7da4d292 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Fri, 7 Apr 2023 15:40:53 +0200 Subject: [PATCH 030/108] stop creating new policy member chat when user already had one --- src/libs/ReportUtils.js | 20 ++++++++++++++++++++ src/libs/actions/Policy.js | 15 +++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index d1dc784ae2ee..4589fe1a8903 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1515,6 +1515,25 @@ function getChatByParticipants(newParticipantList) { }); } +/** + * Attempts to find a report in onyx with the provided list of participants in given policy + * @param {Array} newParticipantList + * @param {String} policyID + * @returns {object|undefined} + */ +function getChatByParticipantsAndPolicy(newParticipantList, policyID) { + newParticipantList.sort(); + return _.find(allReports, (report) => { + // If the report has been deleted, or there are no participants (like an empty #admins room) then skip it + if (!report || !report.participants) { + return false; + } + + // Only return the room if it has all the participants and is not a policy room + return report.policyID === policyID && _.isEqual(newParticipantList, _.sortBy(report.participants)); + }); +} + /** * @param {String} policyID * @returns {Array} @@ -1733,6 +1752,7 @@ export { buildOptimisticAddCommentReportAction, shouldReportBeInOptionList, getChatByParticipants, + getChatByParticipantsAndPolicy, getAllPolicyReports, getIOUReportActionMessage, getDisplayNameForParticipant, diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 6d4a6124f114..4d5c57fcc5db 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -245,6 +245,21 @@ function createPolicyExpenseChats(policyID, members, betas) { } _.each(members, (login) => { + const oldChat = ReportUtils.getChatByParticipantsAndPolicy([login, sessionEmail], policyID); + + // If the chat already exists, we don't want to create a new one - just make sure it's not archived + if (oldChat) { + workspaceMembersChats.optimisticReportIDs[login] = oldChat.reportID; + workspaceMembersChats.onyxOptimisticData.push({ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${oldChat.reportID}`, + value: { + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS.OPEN, + }, + }); + return; + } const optimisticReport = ReportUtils.buildOptimisticChatReport( [login], undefined, From a6d67491d9563929a3d50a7dc9ec78500ed202d7 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Mon, 10 Apr 2023 21:57:39 +0200 Subject: [PATCH 031/108] pass additional report action ID on creating worspace member chat --- src/libs/actions/Policy.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 4d5c57fcc5db..1ccccc65001a 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -236,7 +236,7 @@ function createPolicyExpenseChats(policyID, members, betas) { onyxSuccessData: [], onyxOptimisticData: [], onyxFailureData: [], - optimisticReportIDs: {}, + reportIDs: {}, }; // If the user is not in the beta, we don't want to create any chats @@ -269,7 +269,11 @@ function createPolicyExpenseChats(policyID, members, betas) { ); const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(optimisticReport.ownerEmail); - workspaceMembersChats.optimisticReportIDs[login] = optimisticReport.reportID; + workspaceMembersChats.reportIDs[login] = { + reportID: optimisticReport.reportID, + reportActionID: optimisticCreatedAction.reportActionID, + }; + workspaceMembersChats.onyxOptimisticData.push({ onyxMethod: CONST.ONYX.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReport.reportID}`, @@ -286,6 +290,7 @@ function createPolicyExpenseChats(policyID, members, betas) { key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticReport.reportID}`, value: {[optimisticCreatedAction.reportActionID]: optimisticCreatedAction}, }); + workspaceMembersChats.onyxSuccessData.push({ onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReport.reportID}`, @@ -304,6 +309,7 @@ function createPolicyExpenseChats(policyID, members, betas) { key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticReport.reportID}`, value: {[optimisticCreatedAction.reportActionID]: {pendingAction: null}}, }); + workspaceMembersChats.onyxFailureData.push({ onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReport.reportID}`, @@ -375,7 +381,7 @@ function addMembersToWorkspace(memberLogins, welcomeNote, policyID, betas) { // Escape HTML special chars to enable them to appear in the invite email welcomeNote: _.escape(welcomeNote), policyID, - optimisticReportIDs: JSON.stringify(membersChats.optimisticReportIDs), + reportIDs: JSON.stringify(membersChats.reportIDs), }, {optimisticData, successData, failureData}); } From 315b6a7b578adc6cb2df5c5024a17cac2b10a7cc Mon Sep 17 00:00:00 2001 From: Aswin S Date: Mon, 17 Apr 2023 12:25:08 +0530 Subject: [PATCH 032/108] fix: use block styling for parent HTML node --- src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js | 2 +- src/styles/utilities/display.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js index fbda8b4941dd..25784aad6122 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js @@ -50,7 +50,7 @@ const customHTMLElementModels = { }), }; -const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText]}; +const defaultViewProps = {style: [styles.dBlock, styles.userSelectText]}; // We are using the explicit composite architecture for performance gains. // Configuration for RenderHTML is handled in a top-level component providing diff --git a/src/styles/utilities/display.js b/src/styles/utilities/display.js index a16a62694af8..9e7e4107a937 100644 --- a/src/styles/utilities/display.js +++ b/src/styles/utilities/display.js @@ -20,4 +20,7 @@ export default { dInline: { display: 'inline', }, + dBlock: { + display: 'block', + }, }; From 8aaa49fac9d73cd077fa7a974f90da11aecb3de4 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Mon, 17 Apr 2023 12:26:33 +0530 Subject: [PATCH 033/108] fix: image styling with block parent --- src/components/ThumbnailImage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ThumbnailImage.js b/src/components/ThumbnailImage.js index dedd320d1c91..9ad20873089f 100644 --- a/src/components/ThumbnailImage.js +++ b/src/components/ThumbnailImage.js @@ -87,7 +87,7 @@ class ThumbnailImage extends PureComponent { render() { return ( - + Date: Mon, 17 Apr 2023 15:07:32 +0530 Subject: [PATCH 034/108] fix: use block display only for web & desktop platforms --- .../HTMLEngineProvider/BaseHTMLEngineProvider.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js index 25784aad6122..76c3b6eccd24 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js @@ -10,6 +10,8 @@ import htmlRenderers from './HTMLRenderers'; import * as HTMLEngineUtils from './htmlEngineUtils'; import styles from '../../styles/styles'; import fontFamily from '../../styles/fontFamily'; +import getPlatform from '../../libs/getPlatform/index'; +import CONST from '../../CONST'; const propTypes = { /** Whether text elements should be selectable */ @@ -50,7 +52,13 @@ const customHTMLElementModels = { }), }; -const defaultViewProps = {style: [styles.dBlock, styles.userSelectText]}; +// For web platform defaultViewProps should use block display, otherwise immediate +// children will inherit display:block even when they have display:inline set in CSS. +const defaultViewProps = { + style: [CONST.PLATFORM.WEB, CONST.PLATFORM.DESKTOP].includes(getPlatform()) + ? [styles.dBlock, styles.userSelectText] + : [styles.dFlex, styles.userSelectText], +}; // We are using the explicit composite architecture for performance gains. // Configuration for RenderHTML is handled in a top-level component providing From 5249dc0385249f343830ecaa6edf6244d41c89a4 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Tue, 18 Apr 2023 15:06:55 +0200 Subject: [PATCH 035/108] Rename reportIDs to reportCreationData Co-authored-by: David Bondy --- src/libs/actions/Policy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 1ccccc65001a..ccd7376aa380 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -236,7 +236,7 @@ function createPolicyExpenseChats(policyID, members, betas) { onyxSuccessData: [], onyxOptimisticData: [], onyxFailureData: [], - reportIDs: {}, + reportCreationData: {}, }; // If the user is not in the beta, we don't want to create any chats From 624e39c9d94c417eb8f8420cc25ae66fa0bdfa31 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Tue, 18 Apr 2023 15:07:05 +0200 Subject: [PATCH 036/108] Rename reportIDs to reportCreationData Co-authored-by: David Bondy --- src/libs/actions/Policy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index ccd7376aa380..62437ae2921b 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -381,7 +381,7 @@ function addMembersToWorkspace(memberLogins, welcomeNote, policyID, betas) { // Escape HTML special chars to enable them to appear in the invite email welcomeNote: _.escape(welcomeNote), policyID, - reportIDs: JSON.stringify(membersChats.reportIDs), + reportCreationData: JSON.stringify(membersChats.reportCreationData), }, {optimisticData, successData, failureData}); } From f998868162f23a2e04649c5bdafb4216847ae7b0 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Wed, 19 Apr 2023 07:44:04 +0200 Subject: [PATCH 037/108] Change reportIDs key to reportCreationData in src/libs/actions/Policy.js Co-authored-by: David Bondy --- src/libs/actions/Policy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 62437ae2921b..891a63dc7003 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -269,7 +269,7 @@ function createPolicyExpenseChats(policyID, members, betas) { ); const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(optimisticReport.ownerEmail); - workspaceMembersChats.reportIDs[login] = { + workspaceMembersChats.reportCreationData[login] = { reportID: optimisticReport.reportID, reportActionID: optimisticCreatedAction.reportActionID, }; From e9992b73d386e9ca366c18cd0a265616878bce6f Mon Sep 17 00:00:00 2001 From: Abdul Rahuman Date: Wed, 19 Apr 2023 14:43:34 +0530 Subject: [PATCH 038/108] Fixing privacy link to move along with the submit button --- src/components/Form.js | 2 +- src/components/FormAlertWithSubmitButton.js | 48 +++++++++++-------- .../workspace/WorkspaceInviteMessagePage.js | 2 +- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/components/Form.js b/src/components/Form.js index 15f16aa6e696..f808bde1f0f5 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -343,6 +343,7 @@ class Form extends React.Component { isLoading={this.props.formState.isLoading} message={_.isEmpty(this.props.formState.errorFields) ? this.getErrorMessage() : null} onSubmit={this.submit} + footerContent={this.props.footerContent} onFixTheErrorsLinkPressed={() => { const errors = !_.isEmpty(this.state.errors) ? this.state.errors : this.props.formState.errorFields; const focusKey = _.find(_.keys(this.inputRefs), key => _.keys(errors).includes(key)); @@ -372,7 +373,6 @@ class Form extends React.Component { disablePressOnEnter /> )} - {this.props.footerContent} ); diff --git a/src/components/FormAlertWithSubmitButton.js b/src/components/FormAlertWithSubmitButton.js index 2ff4f0b94b55..6f8c91617de8 100644 --- a/src/components/FormAlertWithSubmitButton.js +++ b/src/components/FormAlertWithSubmitButton.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import styles from '../styles/styles'; import Button from './Button'; import FormAlertWrapper from './FormAlertWrapper'; +import {View} from 'react-native'; const propTypes = { /** Text for the button */ @@ -41,6 +42,9 @@ const propTypes = { /** Whether the form submit action is dangerous */ isSubmitActionDangerous: PropTypes.bool, + + /** Custom content to display in the footer after submit button */ + footerContent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), }; const defaultProps = { @@ -53,6 +57,7 @@ const defaultProps = { enabledWhenOffline: false, disablePressOnEnter: false, isSubmitActionDangerous: false, + footerContent: null, }; const FormAlertWithSubmitButton = props => ( @@ -63,25 +68,30 @@ const FormAlertWithSubmitButton = props => ( message={props.message} onFixTheErrorsLinkPressed={props.onFixTheErrorsLinkPressed} > - {isOffline => ((isOffline && !props.enabledWhenOffline) ? ( -