From 2a543a212015993ee58541d51307f803c4d322ff Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Wed, 16 Aug 2023 19:46:34 +1200 Subject: [PATCH 001/446] Updated variables and states (WIP) --- src/components/PDFView/index.native.js | 172 +++++++++++-------------- 1 file changed, 77 insertions(+), 95 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index c240ade664e5..575d78ca955a 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -14,12 +14,10 @@ import withWindowDimensions from '../withWindowDimensions'; import withKeyboardState, {keyboardStatePropTypes} from '../withKeyboardState'; import withLocalize from '../withLocalize'; import CONST from '../../CONST'; - const propTypes = { ...pdfViewPropTypes, ...keyboardStatePropTypes, -}; - +} /** * On the native layer, we use react-native-pdf/PDF to display PDFs. If a PDF is * password-protected we render a PDFPasswordForm to request a password @@ -34,42 +32,24 @@ const propTypes = { * so that PDFPasswordForm doesn't bounce when react-native-pdf/PDF * is (temporarily) rendered. */ -class PDFView extends Component { - constructor(props) { - super(props); - this.state = { - shouldRequestPassword: false, - shouldAttemptPDFLoad: true, - shouldShowLoadingIndicator: true, - isPasswordInvalid: false, - failedToLoadPDF: false, - successToLoadPDF: false, - password: '', - }; - this.initiatePasswordChallenge = this.initiatePasswordChallenge.bind(this); - this.attemptPDFLoadWithPassword = this.attemptPDFLoadWithPassword.bind(this); - this.finishPDFLoad = this.finishPDFLoad.bind(this); - this.handleFailureToLoadPDF = this.handleFailureToLoadPDF.bind(this); - } - componentDidUpdate() { - this.props.onToggleKeyboard(this.props.isKeyboardShown); - } +const [shouldRequestPassword, setShouldRequestPassword] = useState(false); +const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); +const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true), +const [isPasswordInvalid, setIsPasswordInvalid] = useState(false), +const [failedToLoadPDF, setFailedToLoadPDF] = useState(false), +const [successToLoadPDF, setSuccessToLoadPDF] = useState(false), +const [password, setPassword] = useState(''), + + +/* Ignoring this for last.*/ +componentDidUpdate() { + props .onToggleKeyboard(props.isKeyboardShown); +} - handleFailureToLoadPDF(error) { - if (error.message.match(/password/i)) { - this.initiatePasswordChallenge(); - return; - } - this.setState({ - failedToLoadPDF: true, - shouldAttemptPDFLoad: false, - shouldRequestPassword: false, - shouldShowLoadingIndicator: false, - }); - } +function PDFView(props) { /** * Initiate password challenge if message received from react-native-pdf/PDF * indicates that a password is required or invalid. @@ -78,23 +58,29 @@ class PDFView extends Component { * Note that the message doesn't specify whether the password is simply empty or * invalid. */ - initiatePasswordChallenge() { - this.setState({shouldShowLoadingIndicator: false}); - - // Render password form, and don't render PDF and loading indicator. - this.setState({ - shouldRequestPassword: true, - shouldAttemptPDFLoad: false, - }); - + function initiatePasswordChallenge() { + setShouldShowLoadingIndicator(false); + setShouldRequestPassword(true); + setShouldAttemptPDFLoad(false); // The message provided by react-native-pdf doesn't indicate whether this // is an initial password request or if the password is invalid. So we just assume // that if a password was already entered then it's an invalid password error. - if (this.state.password) { - this.setState({isPasswordInvalid: true}); + if (password !== '') { + setIsPasswordInvalid(true); } } + function handleFailureToLoadPDF(error) { + if (error.message.match(/password/i)) { + initiatePasswordChallenge(); + return; + }; + setFailedToLoadPDF(true); + setShouldShowLoadingIndicator(false); + setShouldRequestPassword(false); + setShouldAttemptPDFLoad(false); + } + /** * When the password is submitted via PDFPasswordForm, save the password * in state and attempt to load the PDF. Also show the loading indicator @@ -102,71 +88,69 @@ class PDFView extends Component { * * @param {String} password Password submitted via PDFPasswordForm */ - attemptPDFLoadWithPassword(password) { + function attemptPDFLoadWithPassword(password) { // Render react-native-pdf/PDF so that it can validate the password. // Note that at this point in the password challenge, shouldRequestPassword is true. // Thus react-native-pdf/PDF will be rendered - but not visible. - this.setState({ - password, - shouldAttemptPDFLoad: true, - shouldShowLoadingIndicator: true, - }); - } + setPassword(password), + setShouldAttemptPDFLoad(true); + setShouldShowLoadingIndicator(true); - /** - * After the PDF is successfully loaded hide PDFPasswordForm and the loading - * indicator. - */ - finishPDFLoad() { - this.setState({ - shouldRequestPassword: false, - shouldShowLoadingIndicator: false, - successToLoadPDF: true, - }); - this.props.onLoadComplete(); } + + + /** + * After the PDF is successfully loaded hide PDFPasswordForm and the loading + * indicator. + */ + finishPDFLoad() { + setShouldRequestPassword(false); + setShouldShowLoadingIndicator(false); + setsuccessToLoadPDF(true); + props.onLoadComplete(); + } renderPDFView() { - const pdfStyles = [styles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(this.props.windowWidth, this.props.windowHeight)]; + const pdfStyles = [styles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(props.windowWidth, props.windowHeight)]; // If we haven't yet successfully validated the password and loaded the PDF, // then we need to hide the react-native-pdf/PDF component so that PDFPasswordForm // is positioned nicely. We're specifically hiding it because we still need to render // the PDF component so that it can validate the password. - if (this.state.shouldRequestPassword) { + if (shouldRequestPassword) { pdfStyles.push(styles.invisible); } - const containerStyles = this.state.shouldRequestPassword && this.props.isSmallScreenWidth ? [styles.w100, styles.flex1] : [styles.alignItemsCenter, styles.flex1]; + const containerStyles = shouldRequestPassword && props.isSmallScreenWidth ? [styles.w100, styles.flex1] : [styles.alignItemsCenter, styles.flex1]; return ( - {this.state.failedToLoadPDF && ( + {failedToLoadPDF && ( - {this.props.translate('attachmentView.failedToLoadPDF')} + {props.translate('attachmentView.failedToLoadPDF')} )} - {this.state.shouldAttemptPDFLoad && ( + {shouldAttemptPDFLoad && ( } - source={{uri: this.props.sourceURL}} + source={{uri: props.sourceURL}} style={pdfStyles} - onError={this.handleFailureToLoadPDF} - password={this.state.password} - onLoadComplete={this.finishPDFLoad} - onPageSingleTap={this.props.onPress} - onScaleChanged={this.props.onScaleChanged} + onError={handleFailureToLoadPDF} + password={state.password} + onLoadComplete={finishPDFLoad} + onPageSingleTap={props.onPress} + onScaleChanged={props.onScaleChanged} /> )} {this.state.shouldRequestPassword && ( this.setState({isPasswordInvalid: false})} - isPasswordInvalid={this.state.isPasswordInvalid} - shouldShowLoadingIndicator={this.state.shouldShowLoadingIndicator} + isFocused={props.isFocused} + onSubmit={attemptPDFLoadWithPassword} + onPasswordUpdated={() => setIsPasswordInvalid(false)} + isPasswordInvalid={isPasswordInvalid} + shouldShowLoadingIndicator={shouldShowLoadingIndicator} /> )} @@ -174,20 +158,18 @@ class PDFView extends Component { ); } - render() { - return this.props.onPress && !this.state.successToLoadPDF ? ( - - {this.renderPDFView()} - - ) : ( - this.renderPDFView() - ); - } + return props.onPress && !successToLoadPDF ? ( + + {renderPDFView()} + + ) : ( + renderPDFView() + ); } PDFView.propTypes = propTypes; From 0b36d7cc37bc92b519ba6a415cd6c73dac5a79c3 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Wed, 16 Aug 2023 20:03:21 +1200 Subject: [PATCH 002/446] began updating lifecycle method to useEffect --- src/components/PDFView/index.native.js | 29 +++++++++++--------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 575d78ca955a..22d2aa557b7f 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -41,15 +41,12 @@ const [failedToLoadPDF, setFailedToLoadPDF] = useState(false), const [successToLoadPDF, setSuccessToLoadPDF] = useState(false), const [password, setPassword] = useState(''), +function PDFView(props) { -/* Ignoring this for last.*/ -componentDidUpdate() { - props .onToggleKeyboard(props.isKeyboardShown); -} - - + useEffect(() => { + props.onToggleKeyboard(props.isKeyboardShown); + },); -function PDFView(props) { /** * Initiate password challenge if message received from react-native-pdf/PDF * indicates that a password is required or invalid. @@ -97,20 +94,18 @@ function PDFView(props) { setShouldShowLoadingIndicator(true); } - - /** * After the PDF is successfully loaded hide PDFPasswordForm and the loading * indicator. */ - finishPDFLoad() { - setShouldRequestPassword(false); - setShouldShowLoadingIndicator(false); - setsuccessToLoadPDF(true); - props.onLoadComplete(); - } + function finishPDFLoad() { + setShouldRequestPassword(false); + setShouldShowLoadingIndicator(false); + setsuccessToLoadPDF(true); + props.onLoadComplete(); + } - renderPDFView() { + function renderPDFView() { const pdfStyles = [styles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(props.windowWidth, props.windowHeight)]; // If we haven't yet successfully validated the password and loaded the PDF, @@ -143,7 +138,7 @@ function PDFView(props) { onScaleChanged={props.onScaleChanged} /> )} - {this.state.shouldRequestPassword && ( + {shouldRequestPassword && ( Date: Tue, 29 Aug 2023 20:56:23 +1200 Subject: [PATCH 003/446] latest useEffect updates --- src/components/PDFView/index.native.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 22d2aa557b7f..799c9a6f35d3 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -35,11 +35,11 @@ const propTypes = { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); -const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true), -const [isPasswordInvalid, setIsPasswordInvalid] = useState(false), -const [failedToLoadPDF, setFailedToLoadPDF] = useState(false), -const [successToLoadPDF, setSuccessToLoadPDF] = useState(false), -const [password, setPassword] = useState(''), +const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); +const [isPasswordInvalid, setIsPasswordInvalid] = useState(false); +const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); +const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); +const [password, setPassword] = useState(''); function PDFView(props) { @@ -166,7 +166,6 @@ function PDFView(props) { renderPDFView() ); } - PDFView.propTypes = propTypes; PDFView.defaultProps = defaultProps; From 00efb240c7b2b1a01ee16c63d5d73bb27ae3c09a Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 17 Oct 2023 10:53:46 +1300 Subject: [PATCH 004/446] small errors found during testing --- src/components/PDFView/index.js | 43 ++++++++++---------------- src/components/PDFView/index.native.js | 22 ++++++------- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index bd5fe8162d2e..fd0180c082b8 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -32,31 +32,20 @@ const PAGE_BORDER = 9; */ const LARGE_SCREEN_SIDE_SPACING = 40; -class PDFView extends Component { - constructor(props) { - super(props); - this.state = { - numPages: null, - pageViewports: [], - containerWidth: props.windowWidth, - containerHeight: props.windowHeight, - shouldRequestPassword: false, - isPasswordInvalid: false, - isKeyboardOpen: false, - }; - this.onDocumentLoadSuccess = this.onDocumentLoadSuccess.bind(this); - this.initiatePasswordChallenge = this.initiatePasswordChallenge.bind(this); - this.attemptPDFLoad = this.attemptPDFLoad.bind(this); - this.toggleKeyboardOnSmallScreens = this.toggleKeyboardOnSmallScreens.bind(this); - this.calculatePageHeight = this.calculatePageHeight.bind(this); - this.calculatePageWidth = this.calculatePageWidth.bind(this); - this.renderPage = this.renderPage.bind(this); - this.getDevicePixelRatio = _.memoize(this.getDevicePixelRatio); - this.setListAttributes = this.setListAttributes.bind(this); +const [numPages, setNumPages] = useState(null); +const [pageViewports, setPageViewports] = useState([]); +const [containerWidth, setContainerWidth] = useState(props.windowWidth); +const [containerHeight, setContainerHeight] = useState(props.windowHeight); +const [shouldRequestPassword, setShouldRequestPassword] = useState(false); +const [isPasswordInvalid, setIsPasswordInvalid] = useState(false); +const [isKeyboardOpen, setIsKeyboardOpen] = useState(false); + +function PDFView(props) { + constructor(props) { const workerBlob = new Blob([pdfWorkerSource], {type: 'text/javascript'}); pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob); - this.retrieveCanvasLimits(); + retrieveCanvasLimits(); } componentDidUpdate(prevProps) { @@ -129,9 +118,9 @@ class PDFView extends Component { */ getDevicePixelRatio(width, height) { const nbPixels = width * height; - const ratioHeight = this.props.maxCanvasHeight / height; - const ratioWidth = this.props.maxCanvasWidth / width; - const ratioArea = Math.sqrt(this.props.maxCanvasArea / nbPixels); + const ratioHeight = maxCanvasHeight / height; + const ratioWidth = maxCanvasWidth / width; + const ratioArea = Math.sqrt(maxCanvasArea / nbPixels); const ratio = Math.min(ratioHeight, ratioArea, ratioWidth); return ratio > window.devicePixelRatio ? undefined : ratio; } @@ -144,13 +133,13 @@ class PDFView extends Component { * @returns {Number} */ calculatePageHeight(pageIndex) { - if (this.state.pageViewports.length === 0) { + if (pageViewports.length === 0) { Log.warn('Dev error: calculatePageHeight() in PDFView called too early'); return 0; } - const pageViewport = this.state.pageViewports[pageIndex]; + setPageViewport(pageIndex); const pageWidth = this.calculatePageWidth(); const scale = pageWidth / pageViewport.width; const actualHeight = pageViewport.height * scale + PAGE_BORDER * 2; diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 5e5b3b910e8a..346a3bb409af 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import React, {useState, useEffect} from 'react'; import {View} from 'react-native'; import PDF from 'react-native-pdf'; import KeyboardAvoidingView from '../KeyboardAvoidingView'; @@ -33,16 +33,16 @@ const propTypes = { * is (temporarily) rendered. */ -const [shouldRequestPassword, setShouldRequestPassword] = useState(false); -const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); -const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); -const [isPasswordInvalid, setIsPasswordInvalid] = useState(false); -const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); -const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); -const [password, setPassword] = useState(''); - function PDFView(props) { + const [shouldRequestPassword, setShouldRequestPassword] = useState(false); + const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); + const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); + const [isPasswordInvalid, setIsPasswordInvalid] = useState(false); + const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); + const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); + const [password, setPassword] = useState(''); + useEffect(() => { props.onToggleKeyboard(props.isKeyboardShown); },); @@ -101,7 +101,7 @@ function PDFView(props) { function finishPDFLoad() { setShouldRequestPassword(false); setShouldShowLoadingIndicator(false); - setsuccessToLoadPDF(true); + setSuccessToLoadPDF(true); props.onLoadComplete(); } @@ -133,7 +133,7 @@ function PDFView(props) { source={{uri: props.sourceURL}} style={pdfStyles} onError={handleFailureToLoadPDF} - password={state.password} + password={password} onLoadComplete={finishPDFLoad} onPageSingleTap={props.onPress} onScaleChanged={props.onScaleChanged} From 520b39d3c3445c52a94758c8dec9c28c64ae58ea Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 17 Oct 2023 13:36:43 +1300 Subject: [PATCH 005/446] trying to make lint happy --- src/components/PDFView/index.js | 43 ++++++++++++++++---------- src/components/PDFView/index.native.js | 29 +++++++++-------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index fd0180c082b8..bd5fe8162d2e 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -32,20 +32,31 @@ const PAGE_BORDER = 9; */ const LARGE_SCREEN_SIDE_SPACING = 40; -const [numPages, setNumPages] = useState(null); -const [pageViewports, setPageViewports] = useState([]); -const [containerWidth, setContainerWidth] = useState(props.windowWidth); -const [containerHeight, setContainerHeight] = useState(props.windowHeight); -const [shouldRequestPassword, setShouldRequestPassword] = useState(false); -const [isPasswordInvalid, setIsPasswordInvalid] = useState(false); -const [isKeyboardOpen, setIsKeyboardOpen] = useState(false); - - -function PDFView(props) { +class PDFView extends Component { constructor(props) { + super(props); + this.state = { + numPages: null, + pageViewports: [], + containerWidth: props.windowWidth, + containerHeight: props.windowHeight, + shouldRequestPassword: false, + isPasswordInvalid: false, + isKeyboardOpen: false, + }; + this.onDocumentLoadSuccess = this.onDocumentLoadSuccess.bind(this); + this.initiatePasswordChallenge = this.initiatePasswordChallenge.bind(this); + this.attemptPDFLoad = this.attemptPDFLoad.bind(this); + this.toggleKeyboardOnSmallScreens = this.toggleKeyboardOnSmallScreens.bind(this); + this.calculatePageHeight = this.calculatePageHeight.bind(this); + this.calculatePageWidth = this.calculatePageWidth.bind(this); + this.renderPage = this.renderPage.bind(this); + this.getDevicePixelRatio = _.memoize(this.getDevicePixelRatio); + this.setListAttributes = this.setListAttributes.bind(this); + const workerBlob = new Blob([pdfWorkerSource], {type: 'text/javascript'}); pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob); - retrieveCanvasLimits(); + this.retrieveCanvasLimits(); } componentDidUpdate(prevProps) { @@ -118,9 +129,9 @@ function PDFView(props) { */ getDevicePixelRatio(width, height) { const nbPixels = width * height; - const ratioHeight = maxCanvasHeight / height; - const ratioWidth = maxCanvasWidth / width; - const ratioArea = Math.sqrt(maxCanvasArea / nbPixels); + const ratioHeight = this.props.maxCanvasHeight / height; + const ratioWidth = this.props.maxCanvasWidth / width; + const ratioArea = Math.sqrt(this.props.maxCanvasArea / nbPixels); const ratio = Math.min(ratioHeight, ratioArea, ratioWidth); return ratio > window.devicePixelRatio ? undefined : ratio; } @@ -133,13 +144,13 @@ function PDFView(props) { * @returns {Number} */ calculatePageHeight(pageIndex) { - if (pageViewports.length === 0) { + if (this.state.pageViewports.length === 0) { Log.warn('Dev error: calculatePageHeight() in PDFView called too early'); return 0; } - setPageViewport(pageIndex); + const pageViewport = this.state.pageViewports[pageIndex]; const pageWidth = this.calculatePageWidth(); const scale = pageWidth / pageViewport.width; const actualHeight = pageViewport.height * scale + PAGE_BORDER * 2; diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 346a3bb409af..e4cfe408c2a9 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -14,10 +14,11 @@ import withWindowDimensions from '../withWindowDimensions'; import withKeyboardState, {keyboardStatePropTypes} from '../withKeyboardState'; import withLocalize from '../withLocalize'; import CONST from '../../CONST'; + const propTypes = { ...pdfViewPropTypes, ...keyboardStatePropTypes, -} +}; /** * On the native layer, we use react-native-pdf/PDF to display PDFs. If a PDF is * password-protected we render a PDFPasswordForm to request a password @@ -34,7 +35,6 @@ const propTypes = { */ function PDFView(props) { - const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); @@ -45,7 +45,7 @@ function PDFView(props) { useEffect(() => { props.onToggleKeyboard(props.isKeyboardShown); - },); + }); /** * Initiate password challenge if message received from react-native-pdf/PDF @@ -71,7 +71,7 @@ function PDFView(props) { if (error.message.match(/password/i)) { initiatePasswordChallenge(); return; - }; + } setFailedToLoadPDF(true); setShouldShowLoadingIndicator(false); setShouldRequestPassword(false); @@ -83,21 +83,20 @@ function PDFView(props) { * in state and attempt to load the PDF. Also show the loading indicator * since react-native-pdf/PDF will need to reload the PDF. * - * @param {String} password Password submitted via PDFPasswordForm + * @param {String} pdfPassword Password submitted via PDFPasswordForm */ - function attemptPDFLoadWithPassword(password) { + function attemptPDFLoadWithPassword(pdfPassword) { // Render react-native-pdf/PDF so that it can validate the password. // Note that at this point in the password challenge, shouldRequestPassword is true. // Thus react-native-pdf/PDF will be rendered - but not visible. - setPassword(password), + setPassword(pdfPassword); setShouldAttemptPDFLoad(true); setShouldShowLoadingIndicator(true); - } - /** - * After the PDF is successfully loaded hide PDFPasswordForm and the loading - * indicator. - */ + /** + * After the PDF is successfully loaded hide PDFPasswordForm and the loading + * indicator. + */ function finishPDFLoad() { setShouldRequestPassword(false); setShouldShowLoadingIndicator(false); @@ -132,9 +131,9 @@ function PDFView(props) { renderActivityIndicator={() => } source={{uri: props.sourceURL}} style={pdfStyles} - onError={handleFailureToLoadPDF} + onError={() => handleFailureToLoadPDF} password={password} - onLoadComplete={finishPDFLoad} + onLoadComplete={() => finishPDFLoad} onPageSingleTap={props.onPress} onScaleChanged={props.onScaleChanged} /> @@ -143,7 +142,7 @@ function PDFView(props) { attemptPDFLoadWithPassword} onPasswordUpdated={() => setIsPasswordInvalid(false)} isPasswordInvalid={isPasswordInvalid} shouldShowLoadingIndicator={shouldShowLoadingIndicator} From 72f601da2c7de3bbbb445edf56554d1afaec6332 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 17 Oct 2023 14:51:12 +1300 Subject: [PATCH 006/446] Revert "small errors found during testing" This reverts commit 00efb240c7b2b1a01ee16c63d5d73bb27ae3c09a. --- src/components/PDFView/index.native.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index e4cfe408c2a9..e65b11411d2a 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react'; +import React, {Component} from 'react'; import {View} from 'react-native'; import PDF from 'react-native-pdf'; import KeyboardAvoidingView from '../KeyboardAvoidingView'; @@ -34,14 +34,15 @@ const propTypes = { * is (temporarily) rendered. */ +const [shouldRequestPassword, setShouldRequestPassword] = useState(false); +const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); +const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); +const [isPasswordInvalid, setIsPasswordInvalid] = useState(false); +const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); +const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); +const [password, setPassword] = useState(''); + function PDFView(props) { - const [shouldRequestPassword, setShouldRequestPassword] = useState(false); - const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); - const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); - const [isPasswordInvalid, setIsPasswordInvalid] = useState(false); - const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); - const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); - const [password, setPassword] = useState(''); useEffect(() => { props.onToggleKeyboard(props.isKeyboardShown); @@ -100,7 +101,7 @@ function PDFView(props) { function finishPDFLoad() { setShouldRequestPassword(false); setShouldShowLoadingIndicator(false); - setSuccessToLoadPDF(true); + setsuccessToLoadPDF(true); props.onLoadComplete(); } @@ -131,9 +132,9 @@ function PDFView(props) { renderActivityIndicator={() => } source={{uri: props.sourceURL}} style={pdfStyles} - onError={() => handleFailureToLoadPDF} + onError={handleFailureToLoadPDF} password={password} - onLoadComplete={() => finishPDFLoad} + onLoadComplete={finishPDFLoad} onPageSingleTap={props.onPress} onScaleChanged={props.onScaleChanged} /> From 437ebebe85a22bc08ac5e95e53aa6cb3989bf04f Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 17 Oct 2023 14:53:25 +1300 Subject: [PATCH 007/446] Revert "small errors found during testing" This reverts commit 00efb240c7b2b1a01ee16c63d5d73bb27ae3c09a. --- src/components/PDFView/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index e65b11411d2a..20a0230b9931 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -133,7 +133,7 @@ function PDFView(props) { source={{uri: props.sourceURL}} style={pdfStyles} onError={handleFailureToLoadPDF} - password={password} + password={state.password} onLoadComplete={finishPDFLoad} onPageSingleTap={props.onPress} onScaleChanged={props.onScaleChanged} From 3597be6d5c79abe2a02ba5d3e019140b1230247c Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 17 Oct 2023 15:59:49 +1300 Subject: [PATCH 008/446] fix functions and run lint/prettier --- src/components/PDFView/index.native.js | 27 +++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 20a0230b9931..d0b6af5a6be6 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import React, {useState, useEffect} from 'react'; import {View} from 'react-native'; import PDF from 'react-native-pdf'; import KeyboardAvoidingView from '../KeyboardAvoidingView'; @@ -34,15 +34,14 @@ const propTypes = { * is (temporarily) rendered. */ -const [shouldRequestPassword, setShouldRequestPassword] = useState(false); -const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); -const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); -const [isPasswordInvalid, setIsPasswordInvalid] = useState(false); -const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); -const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); -const [password, setPassword] = useState(''); - function PDFView(props) { + const [shouldRequestPassword, setShouldRequestPassword] = useState(false); + const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); + const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); + const [isPasswordInvalid, setIsPasswordInvalid] = useState(false); + const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); + const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); + const [password, setPassword] = useState(''); useEffect(() => { props.onToggleKeyboard(props.isKeyboardShown); @@ -101,7 +100,7 @@ function PDFView(props) { function finishPDFLoad() { setShouldRequestPassword(false); setShouldShowLoadingIndicator(false); - setsuccessToLoadPDF(true); + setSuccessToLoadPDF(true); props.onLoadComplete(); } @@ -132,9 +131,9 @@ function PDFView(props) { renderActivityIndicator={() => } source={{uri: props.sourceURL}} style={pdfStyles} - onError={handleFailureToLoadPDF} - password={state.password} - onLoadComplete={finishPDFLoad} + onError={(error) => handleFailureToLoadPDF(error)} + password={password} + onLoadComplete={() => finishPDFLoad()} onPageSingleTap={props.onPress} onScaleChanged={props.onScaleChanged} /> @@ -143,7 +142,7 @@ function PDFView(props) { attemptPDFLoadWithPassword} + onSubmit={() => attemptPDFLoadWithPassword()} onPasswordUpdated={() => setIsPasswordInvalid(false)} isPasswordInvalid={isPasswordInvalid} shouldShowLoadingIndicator={shouldShowLoadingIndicator} From 241f5e71ac4533e1e497c8e715eb5c838711cc9c Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 20 Oct 2023 17:35:17 +0700 Subject: [PATCH 009/446] fix: missing translation for server errors --- src/components/Form.js | 5 +---- src/components/Form/FormWrapper.js | 5 +---- src/components/OptionsSelector/BaseOptionsSelector.js | 2 +- src/components/PDFView/PDFPasswordForm.js | 6 +++--- src/libs/ErrorUtils.ts | 6 ++++-- src/pages/ReimbursementAccount/AddressForm.js | 8 ++++---- src/pages/ReimbursementAccount/IdentityForm.js | 8 ++++---- .../Contacts/ValidateCodeForm/BaseValidateCodeForm.js | 2 +- .../TwoFactorAuthForm/BaseTwoFactorAuthForm.js | 2 +- src/pages/settings/Wallet/ActivatePhysicalCardPage.js | 2 +- src/pages/signin/LoginForm/BaseLoginForm.js | 3 +-- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 6 +++--- 12 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/components/Form.js b/src/components/Form.js index b4e639dcf964..e4babf275af9 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -207,10 +207,7 @@ function Form(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to revalidate the form on update if the preferred locale changed on another device so that errors get translated }, [props.preferredLocale]); - const errorMessage = useMemo(() => { - const latestErrorMessage = ErrorUtils.getLatestErrorMessage(props.formState); - return typeof latestErrorMessage === 'string' ? latestErrorMessage : ''; - }, [props.formState]); + const errorMessage = useMemo(() => ErrorUtils.getLatestErrorMessage(props.formState), [props.formState]); /** * @param {String} inputID - The inputID of the input being touched diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js index 3d9fd37d6f22..b2754cd9c0cf 100644 --- a/src/components/Form/FormWrapper.js +++ b/src/components/Form/FormWrapper.js @@ -81,10 +81,7 @@ function FormWrapper(props) { const {onSubmit, children, formState, errors, inputRefs, submitButtonText, footerContent, isSubmitButtonVisible, style, enabledWhenOffline, isSubmitActionDangerous, formID} = props; const formRef = useRef(null); const formContentRef = useRef(null); - const errorMessage = useMemo(() => { - const latestErrorMessage = ErrorUtils.getLatestErrorMessage(formState); - return typeof latestErrorMessage === 'string' ? latestErrorMessage : ''; - }, [formState]); + const errorMessage = useMemo(() => ErrorUtils.getLatestErrorMessage(formState), [formState]); const scrollViewContent = useCallback( (safeAreaPaddingBottomStyle) => ( diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 4ffddd700359..7bf16fdef4f5 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -172,7 +172,7 @@ class BaseOptionsSelector extends Component { updateSearchValue(value) { this.setState({ - errorMessage: value.length > this.props.maxLength ? this.props.translate('common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}) : '', + errorMessage: value.length > this.props.maxLength ? ['common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}] : '', }); this.props.onChangeText(value); diff --git a/src/components/PDFView/PDFPasswordForm.js b/src/components/PDFView/PDFPasswordForm.js index 58a4e64a28a5..e91eacbec71f 100644 --- a/src/components/PDFView/PDFPasswordForm.js +++ b/src/components/PDFView/PDFPasswordForm.js @@ -54,13 +54,13 @@ function PDFPasswordForm({isFocused, isPasswordInvalid, shouldShowLoadingIndicat const errorText = useMemo(() => { if (isPasswordInvalid) { - return translate('attachmentView.passwordIncorrect'); + return 'attachmentView.passwordIncorrect'; } if (!_.isEmpty(validationErrorText)) { - return translate(validationErrorText); + return validationErrorText; } return ''; - }, [isPasswordInvalid, translate, validationErrorText]); + }, [isPasswordInvalid, validationErrorText]); useEffect(() => { if (!isFocused) { diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index bf4fc0d810a4..ce14d2eda58d 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -46,7 +46,9 @@ type OnyxDataWithErrors = { errors?: Errors; }; -function getLatestErrorMessage(onyxData: TOnyxData): string { +type TranslationData = [string, Record]; + +function getLatestErrorMessage(onyxData: TOnyxData): TranslationData | string { const errors = onyxData.errors ?? {}; if (Object.keys(errors).length === 0) { @@ -55,7 +57,7 @@ function getLatestErrorMessage(onyxData: T const key = Object.keys(errors).sort().reverse()[0]; - return errors[key]; + return [errors[key], {isTranslated: true}]; } type OnyxDataWithErrorFields = { diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 5ddea09c6f4e..5089fc8167ce 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -103,7 +103,7 @@ function AddressForm(props) { value={props.values.street} defaultValue={props.defaultValues.street} onInputChange={props.onFieldChange} - errorText={props.errors.street ? props.translate('bankAccount.error.addressStreet') : ''} + errorText={props.errors.street ? 'bankAccount.error.addressStreet' : ''} hint={props.translate('common.noPO')} renamedInputKeys={props.inputKeys} maxInputLength={CONST.FORM_CHARACTER_LIMIT} @@ -118,7 +118,7 @@ function AddressForm(props) { value={props.values.city} defaultValue={props.defaultValues.city} onChangeText={(value) => props.onFieldChange({city: value})} - errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} + errorText={props.errors.city ? 'bankAccount.error.addressCity' : ''} containerStyles={[styles.mt4]} /> @@ -129,7 +129,7 @@ function AddressForm(props) { value={props.values.state} defaultValue={props.defaultValues.state || ''} onInputChange={(value) => props.onFieldChange({state: value})} - errorText={props.errors.state ? props.translate('bankAccount.error.addressState') : ''} + errorText={props.errors.state ? 'bankAccount.error.addressState' : ''} /> props.onFieldChange({zipCode: value})} - errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} + errorText={props.errors.zipCode ? 'bankAccount.error.zipCode' : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} hint={props.translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} containerStyles={[styles.mt2]} diff --git a/src/pages/ReimbursementAccount/IdentityForm.js b/src/pages/ReimbursementAccount/IdentityForm.js index 20c6e10ec64d..b86779b109f9 100644 --- a/src/pages/ReimbursementAccount/IdentityForm.js +++ b/src/pages/ReimbursementAccount/IdentityForm.js @@ -131,7 +131,7 @@ const defaultProps = { function IdentityForm(props) { // dob field has multiple validations/errors, we are handling it temporarily like this. - const dobErrorText = (props.errors.dob ? props.translate('bankAccount.error.dob') : '') || (props.errors.dobAge ? props.translate('bankAccount.error.age') : ''); + const dobErrorText = (props.errors.dob ? 'bankAccount.error.dob' : '') || (props.errors.dobAge ? 'bankAccount.error.age' : ''); const identityFormInputKeys = ['firstName', 'lastName', 'dob', 'ssnLast4']; const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE); @@ -150,7 +150,7 @@ function IdentityForm(props) { value={props.values.firstName} defaultValue={props.defaultValues.firstName} onChangeText={(value) => props.onFieldChange({firstName: value})} - errorText={props.errors.firstName ? props.translate('bankAccount.error.firstName') : ''} + errorText={props.errors.firstName ? 'bankAccount.error.firstName' : ''} /> @@ -163,7 +163,7 @@ function IdentityForm(props) { value={props.values.lastName} defaultValue={props.defaultValues.lastName} onChangeText={(value) => props.onFieldChange({lastName: value})} - errorText={props.errors.lastName ? props.translate('bankAccount.error.lastName') : ''} + errorText={props.errors.lastName ? 'bankAccount.error.lastName' : ''} /> @@ -189,7 +189,7 @@ function IdentityForm(props) { keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} defaultValue={props.defaultValues.ssnLast4} onChangeText={(value) => props.onFieldChange({ssnLast4: value})} - errorText={props.errors.ssnLast4 ? props.translate('bankAccount.error.ssnLast4') : ''} + errorText={props.errors.ssnLast4 ? 'bankAccount.error.ssnLast4' : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.SSN} /> diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js index 0175f2ceac1f..dc139c03000f 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js @@ -117,7 +117,7 @@ function ActivatePhysicalCardPage({ activateCardCodeInputRef.current.blur(); if (lastFourDigits.replace(CONST.MAGIC_CODE_EMPTY_CHAR, '').length !== LAST_FOUR_DIGITS_LENGTH) { - setFormError(translate('activateCardPage.error.thatDidntMatch')); + setFormError('activateCardPage.error.thatDidntMatch'); return; } diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 3576f92be31f..38e428451f2c 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -197,7 +197,6 @@ function LoginForm(props) { }, })); - const formErrorText = useMemo(() => (formError ? translate(formError) : ''), [formError, translate]); const serverErrorText = useMemo(() => ErrorUtils.getLatestErrorMessage(props.account), [props.account]); const hasError = !_.isEmpty(serverErrorText); @@ -222,7 +221,7 @@ function LoginForm(props) { autoCapitalize="none" autoCorrect={false} keyboardType={CONST.KEYBOARD_TYPE.EMAIL_ADDRESS} - errorText={formErrorText} + errorText={formError} hasError={hasError} maxLength={CONST.LOGIN_CHARACTER_LIMIT} /> diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index dc100fffe4f1..43b54454ba0f 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -312,7 +312,7 @@ function BaseValidateCodeForm(props) { onChangeText={(text) => onTextInput(text, 'recoveryCode')} maxLength={CONST.RECOVERY_CODE_LENGTH} label={props.translate('recoveryCodeForm.recoveryCode')} - errorText={formError.recoveryCode ? props.translate(formError.recoveryCode) : ''} + errorText={formError.recoveryCode ? formError.recoveryCode : ''} hasError={hasError} onSubmitEditing={validateAndSubmitForm} autoFocus @@ -328,7 +328,7 @@ function BaseValidateCodeForm(props) { onChangeText={(text) => onTextInput(text, 'twoFactorAuthCode')} onFulfill={validateAndSubmitForm} maxLength={CONST.TFA_CODE_LENGTH} - errorText={formError.twoFactorAuthCode ? props.translate(formError.twoFactorAuthCode) : ''} + errorText={formError.twoFactorAuthCode ? formError.twoFactorAuthCode : ''} hasError={hasError} autoFocus /> @@ -357,7 +357,7 @@ function BaseValidateCodeForm(props) { value={validateCode} onChangeText={(text) => onTextInput(text, 'validateCode')} onFulfill={validateAndSubmitForm} - errorText={formError.validateCode ? props.translate(formError.validateCode) : ''} + errorText={formError.validateCode ? formError.validateCode : ''} hasError={hasError} autoFocus /> From 096ed12e2b91ae5bdc14f0db171d6c7aeefbd9a4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Sat, 21 Oct 2023 22:24:13 +0700 Subject: [PATCH 010/446] remove redundant dependency --- src/pages/settings/Wallet/ActivatePhysicalCardPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js index dc139c03000f..71b147e3c28c 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js @@ -122,7 +122,7 @@ function ActivatePhysicalCardPage({ } CardSettings.activatePhysicalExpensifyCard(Number(lastFourDigits), cardID); - }, [lastFourDigits, cardID, translate]); + }, [lastFourDigits, cardID]); if (_.isEmpty(physicalCard)) { return ; From 0a9a467ac459e36a3e1ad9f059379ee70c4eb778 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 25 Oct 2023 15:37:34 +0700 Subject: [PATCH 011/446] do not translate already translated text in DotIndicatorMessage --- src/components/AvatarWithImagePicker.js | 2 +- src/components/DistanceRequest/index.js | 4 +-- src/components/OfflineWithFeedback.js | 3 +- src/libs/ErrorUtils.ts | 36 +++++++++++++++---- src/pages/SearchPage.js | 4 ++- .../settings/Wallet/ExpensifyCardPage.js | 2 +- src/pages/signin/LoginForm/BaseLoginForm.js | 3 +- src/pages/signin/UnlinkLoginForm.js | 6 ++-- 8 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 3dd23d9051eb..40ee7aa04208 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -365,7 +365,7 @@ class AvatarWithImagePicker extends React.Component { {this.state.validationError && ( )} diff --git a/src/components/DistanceRequest/index.js b/src/components/DistanceRequest/index.js index bd35678273ec..3d9cdb31195e 100644 --- a/src/components/DistanceRequest/index.js +++ b/src/components/DistanceRequest/index.js @@ -152,11 +152,11 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe // Initially, both waypoints will be null, and if we give fallback value as empty string that will result in true condition, that's why different default values. if (_.keys(waypoints).length === 2 && lodashGet(waypoints, 'waypoint0.address', 'address1') === lodashGet(waypoints, 'waypoint1.address', 'address2')) { - return {0: translate('iou.error.duplicateWaypointsErrorMessage')}; + return {0: 'iou.error.duplicateWaypointsErrorMessage'}; } if (_.size(validatedWaypoints) < 2) { - return {0: translate('iou.error.emptyWaypointsErrorMessage')}; + return {0: 'iou.error.emptyWaypointsErrorMessage'}; } }; diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js index 643e7b2f4a2f..a73a41f21810 100644 --- a/src/components/OfflineWithFeedback.js +++ b/src/components/OfflineWithFeedback.js @@ -7,6 +7,7 @@ import stylePropTypes from '../styles/stylePropTypes'; import styles from '../styles/styles'; import Tooltip from './Tooltip'; import Icon from './Icon'; +import * as ErrorUtils from '../libs/ErrorUtils'; import * as Expensicons from './Icon/Expensicons'; import * as StyleUtils from '../styles/StyleUtils'; import DotIndicatorMessage from './DotIndicatorMessage'; @@ -103,7 +104,7 @@ function OfflineWithFeedback(props) { const hasErrors = !_.isEmpty(props.errors); // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. - const errorMessages = _.omit(props.errors, (e) => e === null); + const errorMessages = ErrorUtils.getErrorMessagesWithTranslationData(_.omit(props.errors, (e) => e === null)); const hasErrorMessages = !_.isEmpty(errorMessages); const isOfflinePendingAction = isOffline && props.pendingAction; const isUpdateOrDeleteError = hasErrors && (props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || props.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index ce14d2eda58d..891616669eb3 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -1,3 +1,5 @@ +import mapKeys from 'lodash/mapKeys'; +import isEmpty from 'lodash/isEmpty'; import CONST from '../CONST'; import DateUtils from './DateUtils'; import * as Localize from './Localize'; @@ -46,9 +48,9 @@ type OnyxDataWithErrors = { errors?: Errors; }; -type TranslationData = [string, Record]; +type TranslationData = [string, Record] | string; -function getLatestErrorMessage(onyxData: TOnyxData): TranslationData | string { +function getLatestErrorMessage(onyxData: TOnyxData): TranslationData { const errors = onyxData.errors ?? {}; if (Object.keys(errors).length === 0) { @@ -64,7 +66,7 @@ type OnyxDataWithErrorFields = { errorFields?: ErrorFields; }; -function getLatestErrorField(onyxData: TOnyxData, fieldName: string): Record { +function getLatestErrorField(onyxData: TOnyxData, fieldName: string): Record { const errorsForField = onyxData.errorFields?.[fieldName] ?? {}; if (Object.keys(errorsForField).length === 0) { @@ -73,10 +75,10 @@ function getLatestErrorField(onyxData const key = Object.keys(errorsForField).sort().reverse()[0]; - return {[key]: errorsForField[key]}; + return {[key]: [errorsForField[key], {isTranslated: true}]}; } -function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { +function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { const errorsForField = onyxData.errorFields?.[fieldName] ?? {}; if (Object.keys(errorsForField).length === 0) { @@ -85,10 +87,30 @@ function getEarliestErrorField(onyxDa const key = Object.keys(errorsForField).sort()[0]; - return {[key]: errorsForField[key]}; + return {[key]: [errorsForField[key], {isTranslated: true}]}; } type ErrorsList = Record; +type ErrorsListWithTranslationData = Record; + +/** + * Method used to attach already translated message with isTranslated: true property + * @param errors - An object containing current errors in the form + * @returns Errors in the form of {timestamp: [message, {isTranslated: true}]} + */ +function getErrorMessagesWithTranslationData(errors: TranslationData | ErrorsList): ErrorsListWithTranslationData { + if (isEmpty(errors)) { + return {}; + } + + if (typeof errors === 'string' || Array.isArray(errors)) { + const [message, variables] = Array.isArray(errors) ? errors : [errors]; + // eslint-disable-next-line @typescript-eslint/naming-convention + return {0: [message, {...variables, isTranslated: true}]}; + } + + return mapKeys(errors, (message) => [message, {isTranslated: true}]); +} /** * Method used to generate error message for given inputID @@ -113,4 +135,4 @@ function addErrorMessage(errors: ErrorsList, inputID?: string, message?: string) } } -export {getAuthenticateErrorMessage, getMicroSecondOnyxError, getLatestErrorMessage, getLatestErrorField, getEarliestErrorField, addErrorMessage}; +export {getAuthenticateErrorMessage, getMicroSecondOnyxError, getLatestErrorMessage, getLatestErrorField, getEarliestErrorField, getErrorMessagesWithTranslationData, addErrorMessage}; diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index c671e7b1a096..f0a4eb58916c 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -202,7 +202,9 @@ class SearchPage extends Component { shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady} textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={ - this.props.network.isOffline ? `${this.props.translate('common.youAppearToBeOffline')} ${this.props.translate('search.resultsAreLimited')}` : '' + this.props.network.isOffline + ? [`${this.props.translate('common.youAppearToBeOffline')} ${this.props.translate('search.resultsAreLimited')}`, {isTranslated: true}] + : '' } onLayout={this.searchRendered} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index e198d449d57d..d6096a3e3aac 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -90,7 +90,7 @@ function ExpensifyCardPage({ ) : null} diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 2c595a39c201..0196d5f91c02 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -239,11 +239,10 @@ function LoginForm(props) { {!_.isEmpty(props.account.success) && {props.account.success}} {!_.isEmpty(props.closeAccount.success || props.account.message) && ( - // DotIndicatorMessage mostly expects onyxData errors, so we need to mock an object so that the messages looks similar to prop.account.errors )} { diff --git a/src/pages/signin/UnlinkLoginForm.js b/src/pages/signin/UnlinkLoginForm.js index 6807ba74c6f9..5b26d254bee5 100644 --- a/src/pages/signin/UnlinkLoginForm.js +++ b/src/pages/signin/UnlinkLoginForm.js @@ -7,6 +7,7 @@ import Str from 'expensify-common/lib/str'; import styles from '../../styles/styles'; import Button from '../../components/Button'; import Text from '../../components/Text'; +import * as ErrorUtils from '../../libs/ErrorUtils'; import * as Session from '../../libs/actions/Session'; import ONYXKEYS from '../../ONYXKEYS'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; @@ -63,18 +64,17 @@ function UnlinkLoginForm(props) { {props.translate('unlinkLoginForm.noLongerHaveAccess', {primaryLogin})} {!_.isEmpty(props.account.message) && ( - // DotIndicatorMessage mostly expects onyxData errors so we need to mock an object so that the messages looks similar to prop.account.errors )} {!_.isEmpty(props.account.errors) && ( )} From b04abe52ac9f1f58ce85ea4398f51647c69b8699 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 25 Oct 2023 16:00:13 +0700 Subject: [PATCH 012/446] fix missing translation for FormAlertWrapper --- src/components/FormAlertWithSubmitButton.js | 2 +- src/components/FormAlertWrapper.js | 2 +- src/pages/EnablePayments/OnfidoPrivacy.js | 6 ++++-- src/pages/settings/Wallet/ReportCardLostPage.js | 4 ++-- src/pages/settings/Wallet/TransferBalancePage.js | 3 ++- src/pages/tasks/NewTaskPage.js | 6 +++--- src/pages/workspace/WorkspaceInvitePage.js | 3 ++- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/FormAlertWithSubmitButton.js b/src/components/FormAlertWithSubmitButton.js index 33d188719d11..f078b99ec47c 100644 --- a/src/components/FormAlertWithSubmitButton.js +++ b/src/components/FormAlertWithSubmitButton.js @@ -27,7 +27,7 @@ const propTypes = { isMessageHtml: PropTypes.bool, /** Error message to display above button */ - message: PropTypes.string, + message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** Callback fired when the "fix the errors" link is pressed */ onFixTheErrorsLinkPressed: PropTypes.func, diff --git a/src/components/FormAlertWrapper.js b/src/components/FormAlertWrapper.js index 67e031ce6ab6..757bc1cca2fb 100644 --- a/src/components/FormAlertWrapper.js +++ b/src/components/FormAlertWrapper.js @@ -27,7 +27,7 @@ const propTypes = { isMessageHtml: PropTypes.bool, /** Error message to display above button */ - message: PropTypes.string, + message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** Props to detect online status */ network: networkPropTypes.isRequired, diff --git a/src/pages/EnablePayments/OnfidoPrivacy.js b/src/pages/EnablePayments/OnfidoPrivacy.js index 85ceb03b01d5..5575525890f2 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.js +++ b/src/pages/EnablePayments/OnfidoPrivacy.js @@ -44,9 +44,11 @@ function OnfidoPrivacy({walletOnfidoData, translate, form}) { BankAccounts.openOnfidoFlow(); }; - let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) || ''; + const onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) || ''; const onfidoFixableErrors = lodashGet(walletOnfidoData, 'fixableErrors', []); - onfidoError += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; + if (_.isArray(onfidoError)) { + onfidoError[0] += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; + } return ( diff --git a/src/pages/settings/Wallet/ReportCardLostPage.js b/src/pages/settings/Wallet/ReportCardLostPage.js index 29a588916326..696a162ac6e5 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.js +++ b/src/pages/settings/Wallet/ReportCardLostPage.js @@ -182,7 +182,7 @@ function ReportCardLostPage({ @@ -200,7 +200,7 @@ function ReportCardLostPage({ diff --git a/src/pages/settings/Wallet/TransferBalancePage.js b/src/pages/settings/Wallet/TransferBalancePage.js index ae54dab569f7..34c97f8e5277 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.js +++ b/src/pages/settings/Wallet/TransferBalancePage.js @@ -3,6 +3,7 @@ import React, {useEffect} from 'react'; import {View, ScrollView} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; +import * as ErrorUtils from '../../../libs/ErrorUtils'; import ONYXKEYS from '../../../ONYXKEYS'; import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import ScreenWrapper from '../../../components/ScreenWrapper'; @@ -165,7 +166,7 @@ function TransferBalancePage(props) { const transferAmount = props.userWallet.currentBalance - calculatedFee; const isTransferable = transferAmount > 0; const isButtonDisabled = !isTransferable || !selectedAccount; - const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? _.chain(props.walletTransfer.errors).values().first().value() : ''; + const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? ErrorUtils.getErrorMessagesWithTranslationData(_.chain(props.walletTransfer.errors).values().first().value()) : ''; const shouldShowTransferView = PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, props.bankAccountList) && diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js index f0d2d506c9d8..2e1f42d90b6e 100644 --- a/src/pages/tasks/NewTaskPage.js +++ b/src/pages/tasks/NewTaskPage.js @@ -114,17 +114,17 @@ function NewTaskPage(props) { // the response function onSubmit() { if (!props.task.title && !props.task.shareDestination) { - setErrorMessage(props.translate('newTaskPage.confirmError')); + setErrorMessage('newTaskPage.confirmError'); return; } if (!props.task.title) { - setErrorMessage(props.translate('newTaskPage.pleaseEnterTaskName')); + setErrorMessage('newTaskPage.pleaseEnterTaskName'); return; } if (!props.task.shareDestination) { - setErrorMessage(props.translate('newTaskPage.pleaseEnterTaskDestination')); + setErrorMessage('newTaskPage.pleaseEnterTaskDestination'); return; } diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index a21173dd7d98..33fd3786c490 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -4,6 +4,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import lodashGet from 'lodash/get'; +import * as ErrorUtils from '../../libs/ErrorUtils'; import ScreenWrapper from '../../components/ScreenWrapper'; import HeaderWithBackButton from '../../components/HeaderWithBackButton'; import Navigation from '../../libs/Navigation/Navigation'; @@ -281,7 +282,7 @@ function WorkspaceInvitePage(props) { isAlertVisible={shouldShowAlertPrompt} buttonText={translate('common.next')} onSubmit={inviteUser} - message={props.policy.alertMessage} + message={ErrorUtils.getErrorMessagesWithTranslationData(props.policy.alertMessage)} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter From 95188d6cb14118265db6a76cb6cbe52eadab039e Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 25 Oct 2023 16:44:38 +0700 Subject: [PATCH 013/446] fix missing translation for FormHelpMessage --- src/components/MoneyRequestConfirmationList.js | 2 +- src/pages/NewChatPage.js | 2 +- src/pages/ReimbursementAccount/AddressForm.js | 4 ++-- src/pages/ReimbursementAccount/CompanyStep.js | 2 +- src/pages/iou/steps/MoneyRequestAmountForm.js | 2 +- src/pages/settings/Profile/PersonalDetails/AddressPage.js | 2 +- src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js | 2 +- src/pages/settings/Wallet/AddDebitCardPage.js | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 0b266351a60c..c5f04d52f5f3 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -538,7 +538,7 @@ function MoneyRequestConfirmationList(props) { )} {button} diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index 381564b82600..e45635f82f1d 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -251,7 +251,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, i shouldShowOptions={isOptionsDataReady} shouldShowConfirmButton confirmButtonText={selectedOptions.length > 1 ? translate('newChatPage.createGroup') : translate('newChatPage.createChat')} - textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} + textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} onConfirmSelection={createGroup} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 5089fc8167ce..4eb5009256b1 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -104,7 +104,7 @@ function AddressForm(props) { defaultValue={props.defaultValues.street} onInputChange={props.onFieldChange} errorText={props.errors.street ? 'bankAccount.error.addressStreet' : ''} - hint={props.translate('common.noPO')} + hint="common.noPO" renamedInputKeys={props.inputKeys} maxInputLength={CONST.FORM_CHARACTER_LIMIT} /> @@ -144,7 +144,7 @@ function AddressForm(props) { onChangeText={(value) => props.onFieldChange({zipCode: value})} errorText={props.errors.zipCode ? 'bankAccount.error.zipCode' : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} - hint={props.translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} + hint={['common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}]} containerStyles={[styles.mt2]} /> diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 0ca9b1b7ea92..926eb3f651ac 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -207,7 +207,7 @@ function CompanyStep({reimbursementAccount, reimbursementAccountDraft, getDefaul containerStyles={[styles.mt4]} defaultValue={getDefaultStateForField('website', defaultWebsite)} shouldSaveDraft - hint={translate('common.websiteExample')} + hint="common.websiteExample" keyboardType={CONST.KEYBOARD_TYPE.URL} /> )} )} diff --git a/src/pages/settings/Wallet/AddDebitCardPage.js b/src/pages/settings/Wallet/AddDebitCardPage.js index e75c3b2c517e..1f62e6f68ac9 100644 --- a/src/pages/settings/Wallet/AddDebitCardPage.js +++ b/src/pages/settings/Wallet/AddDebitCardPage.js @@ -178,7 +178,7 @@ function DebitCardPage(props) { accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} - hint={translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} + hint={['common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}]} containerStyles={[styles.mt4]} /> From e2d2551625ca6798da2de0ea59b8a33f911c90d3 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 25 Oct 2023 17:03:38 +0700 Subject: [PATCH 014/446] fix lint --- src/components/MoneyRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index c5f04d52f5f3..91cba3f2d9bb 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -544,7 +544,7 @@ function MoneyRequestConfirmationList(props) { {button} ); - }, [confirm, props.bankAccountRoute, props.iouCurrencyCode, props.iouType, props.isReadOnly, props.policyID, selectedParticipants, splitOrRequestOptions, translate, formError]); + }, [confirm, props.bankAccountRoute, props.iouCurrencyCode, props.iouType, props.isReadOnly, props.policyID, selectedParticipants, splitOrRequestOptions, formError]); const {image: receiptImage, thumbnail: receiptThumbnail} = props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, props.receiptPath, props.receiptFilename) : {}; From b010edee36298596a5fdbed888d60db91a95731f Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 7 Nov 2023 19:51:42 +1300 Subject: [PATCH 015/446] ran prettier --- src/components/PDFView/index.native.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 71d00704f18b..086fae8305fc 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -1,4 +1,5 @@ -import React, {useState, useEffect} from 'react'; +import {PasswordResponses} from 'pdfjs-dist'; +import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import PDF from 'react-native-pdf'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -142,7 +143,7 @@ function PDFView(props) { attemptPDFLoadWithPassword()} + onSubmit={(formPasswordValue) => attemptPDFLoadWithPassword(formPasswordValue)} onPasswordUpdated={() => setIsPasswordInvalid(false)} isPasswordInvalid={isPasswordInvalid} shouldShowLoadingIndicator={shouldShowLoadingIndicator} From c15f3d4d3d4cb4d5be2d151ceeab91619a066f11 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 7 Nov 2023 20:18:28 +1300 Subject: [PATCH 016/446] removed unnecessary PasswordResponses definition --- src/components/PDFView/index.native.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 086fae8305fc..cc360c3adc68 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -1,4 +1,3 @@ -import {PasswordResponses} from 'pdfjs-dist'; import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import PDF from 'react-native-pdf'; From 6803203c19db5f5abb2368c954dcfbc936763f18 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 21 Nov 2023 20:59:36 +0700 Subject: [PATCH 017/446] fix translation for AddressForm --- src/components/AddressForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddressForm.js b/src/components/AddressForm.js index 19ab35f036c1..4684e11dc0bb 100644 --- a/src/components/AddressForm.js +++ b/src/components/AddressForm.js @@ -65,7 +65,7 @@ const defaultProps = { function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldSaveDraft, state, street1, street2, submitButtonText, zip}) { const {translate} = useLocalize(); const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [country, 'samples'], ''); - const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); + const zipFormat = ['common.zipCodeExampleFormat', {zipSampleFormat}]; const isUSAForm = country === CONST.COUNTRY.US; /** From cc5e2de0cc3e5befc7900f35371af40f3b300dd2 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 28 Nov 2023 10:41:30 +1300 Subject: [PATCH 018/446] destructured props --- src/components/PDFView/index.native.js | 42 +++++++++++++++++--------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index cc360c3adc68..b821b7d18e9d 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -34,7 +34,20 @@ const propTypes = { * is (temporarily) rendered. */ -function PDFView(props) { +function PDFView({ + onToggleKeyboard, + isKeyboardShown, + onLoadComplete, + translate, + fileName, + onPress, + isFocused, + onScaleChanged, + sourceURL, + isSmallScreenWidth, + windowWidth, + windowHeight, + errorLabelStyles}) { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); @@ -44,7 +57,7 @@ function PDFView(props) { const [password, setPassword] = useState(''); useEffect(() => { - props.onToggleKeyboard(props.isKeyboardShown); + onToggleKeyboard(isKeyboardShown); }); /** @@ -101,11 +114,11 @@ function PDFView(props) { setShouldRequestPassword(false); setShouldShowLoadingIndicator(false); setSuccessToLoadPDF(true); - props.onLoadComplete(); + onLoadComplete(); } function renderPDFView() { - const pdfStyles = [styles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(props.windowWidth, props.windowHeight)]; + const pdfStyles = [styles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(windowWidth, windowHeight)]; // If we haven't yet successfully validated the password and loaded the PDF, // then we need to hide the react-native-pdf/PDF component so that PDFPasswordForm @@ -115,13 +128,13 @@ function PDFView(props) { pdfStyles.push(styles.invisible); } - const containerStyles = shouldRequestPassword && props.isSmallScreenWidth ? [styles.w100, styles.flex1] : [styles.alignItemsCenter, styles.flex1]; + const containerStyles = shouldRequestPassword && isSmallScreenWidth ? [styles.w100, styles.flex1] : [styles.alignItemsCenter, styles.flex1]; return ( {failedToLoadPDF && ( - {props.translate('attachmentView.failedToLoadPDF')} + {translate('attachmentView.failedToLoadPDF')} )} {shouldAttemptPDFLoad && ( @@ -129,19 +142,19 @@ function PDFView(props) { fitPolicy={0} trustAllCerts={false} renderActivityIndicator={() => } - source={{uri: props.sourceURL}} + source={{uri: sourceURL}} style={pdfStyles} onError={(error) => handleFailureToLoadPDF(error)} password={password} onLoadComplete={() => finishPDFLoad()} - onPageSingleTap={props.onPress} - onScaleChanged={props.onScaleChanged} + onPageSingleTap={onPress} + onScaleChanged={onScaleChanged} /> )} {shouldRequestPassword && ( attemptPDFLoadWithPassword(formPasswordValue)} onPasswordUpdated={() => setIsPasswordInvalid(false)} isPasswordInvalid={isPasswordInvalid} @@ -153,12 +166,12 @@ function PDFView(props) { ); } - return props.onPress && !successToLoadPDF ? ( + return onPress && !successToLoadPDF ? ( {renderPDFView()} @@ -166,6 +179,7 @@ function PDFView(props) { renderPDFView() ); } +PDFView.displayName = 'PDFView'; PDFView.propTypes = propTypes; PDFView.defaultProps = defaultProps; From 8b5081d78f00abb0325cf07b257a803cb476247a Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 28 Nov 2023 10:43:29 +1300 Subject: [PATCH 019/446] destructured props --- src/components/PDFView/index.native.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index b821b7d18e9d..e2522f475db8 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -35,8 +35,8 @@ const propTypes = { */ function PDFView({ - onToggleKeyboard, - isKeyboardShown, + onToggleKeyboard, + isKeyboardShown, onLoadComplete, translate, fileName, @@ -45,9 +45,10 @@ function PDFView({ onScaleChanged, sourceURL, isSmallScreenWidth, - windowWidth, + windowWidth, windowHeight, - errorLabelStyles}) { + errorLabelStyles, +}) { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); From f842efce2995f39a6a008f420f55e68b12659068 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 28 Nov 2023 10:57:55 +1300 Subject: [PATCH 020/446] updated arrow functions per comment --- src/components/PDFView/index.native.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index e2522f475db8..4bdaaedbd4ea 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -81,7 +81,7 @@ function PDFView({ } } - function handleFailureToLoadPDF(error) { + const handleFailureToLoadPDF = (error) => { if (error.message.match(/password/i)) { initiatePasswordChallenge(); return; @@ -90,7 +90,7 @@ function PDFView({ setShouldShowLoadingIndicator(false); setShouldRequestPassword(false); setShouldAttemptPDFLoad(false); - } + }; /** * When the password is submitted via PDFPasswordForm, save the password @@ -111,12 +111,12 @@ function PDFView({ * After the PDF is successfully loaded hide PDFPasswordForm and the loading * indicator. */ - function finishPDFLoad() { + const finishPDFLoad = () => { setShouldRequestPassword(false); setShouldShowLoadingIndicator(false); setSuccessToLoadPDF(true); onLoadComplete(); - } + }; function renderPDFView() { const pdfStyles = [styles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(windowWidth, windowHeight)]; @@ -145,9 +145,9 @@ function PDFView({ renderActivityIndicator={() => } source={{uri: sourceURL}} style={pdfStyles} - onError={(error) => handleFailureToLoadPDF(error)} + onError={handleFailureToLoadPDF} password={password} - onLoadComplete={() => finishPDFLoad()} + onLoadComplete={finishPDFLoad} onPageSingleTap={onPress} onScaleChanged={onScaleChanged} /> @@ -157,7 +157,7 @@ function PDFView({ attemptPDFLoadWithPassword(formPasswordValue)} - onPasswordUpdated={() => setIsPasswordInvalid(false)} + onPasswordUpdated={setIsPasswordInvalid(false)} isPasswordInvalid={isPasswordInvalid} shouldShowLoadingIndicator={shouldShowLoadingIndicator} /> From 76c508e856c93a4db6d435a0db78075bd2f823b3 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 28 Nov 2023 11:03:08 +1300 Subject: [PATCH 021/446] updated arrow function for onSubmit per comment --- src/components/PDFView/index.native.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 4bdaaedbd4ea..eac644606f2e 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -99,14 +99,14 @@ function PDFView({ * * @param {String} pdfPassword Password submitted via PDFPasswordForm */ - function attemptPDFLoadWithPassword(pdfPassword) { + const attemptPDFLoadWithPassword = (pdfPassword) => { // Render react-native-pdf/PDF so that it can validate the password. // Note that at this point in the password challenge, shouldRequestPassword is true. // Thus react-native-pdf/PDF will be rendered - but not visible. setPassword(pdfPassword); setShouldAttemptPDFLoad(true); setShouldShowLoadingIndicator(true); - } + }; /** * After the PDF is successfully loaded hide PDFPasswordForm and the loading * indicator. @@ -156,7 +156,7 @@ function PDFView({ attemptPDFLoadWithPassword(formPasswordValue)} + onSubmit={attemptPDFLoadWithPassword} onPasswordUpdated={setIsPasswordInvalid(false)} isPasswordInvalid={isPasswordInvalid} shouldShowLoadingIndicator={shouldShowLoadingIndicator} From e527f419b287fcf3b72370a1c5ebbb6c4608727a Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 28 Nov 2023 11:11:44 +1300 Subject: [PATCH 022/446] updated arrow function for onPasswordUpdated due to incorrect change --- src/components/PDFView/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index eac644606f2e..c1e45520d49f 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -157,7 +157,7 @@ function PDFView({ setIsPasswordInvalid(false)} isPasswordInvalid={isPasswordInvalid} shouldShowLoadingIndicator={shouldShowLoadingIndicator} /> From b8dad84ebd7d172300b1b8460a1c0d0a71ef0360 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 1 Dec 2023 19:43:35 +0700 Subject: [PATCH 023/446] create prop type for translatable text --- src/components/AddressSearch/index.js | 3 ++- src/components/FormAlertWithSubmitButton.js | 3 ++- src/components/FormAlertWrapper.js | 3 ++- src/components/FormHelpMessage.js | 3 ++- src/components/RoomNameInput/roomNameInputPropTypes.js | 3 ++- .../TextInput/BaseTextInput/baseTextInputPropTypes.js | 3 ++- src/components/translatableTextPropTypes.js | 8 ++++++++ 7 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 src/components/translatableTextPropTypes.js diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 9f16766a22ae..4143b6e8f699 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -9,6 +9,7 @@ import LocationErrorMessage from '@components/LocationErrorMessage'; import networkPropTypes from '@components/networkPropTypes'; import {withNetwork} from '@components/OnyxProvider'; import TextInput from '@components/TextInput'; +import translatableTextPropTypes from '@components/translatableTextPropTypes'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import * as ApiUtils from '@libs/ApiUtils'; import compose from '@libs/compose'; @@ -38,7 +39,7 @@ const propTypes = { onBlur: PropTypes.func, /** Error text to display */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + errorText: translatableTextPropTypes, /** Hint text to display */ hint: PropTypes.string, diff --git a/src/components/FormAlertWithSubmitButton.js b/src/components/FormAlertWithSubmitButton.js index cab71c6a935c..6a7d770d8779 100644 --- a/src/components/FormAlertWithSubmitButton.js +++ b/src/components/FormAlertWithSubmitButton.js @@ -5,6 +5,7 @@ import _ from 'underscore'; import useThemeStyles from '@styles/useThemeStyles'; import Button from './Button'; import FormAlertWrapper from './FormAlertWrapper'; +import translatableTextPropTypes from './translatableTextPropTypes'; const propTypes = { /** Text for the button */ @@ -27,7 +28,7 @@ const propTypes = { isMessageHtml: PropTypes.bool, /** Error message to display above button */ - message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + message: translatableTextPropTypes, /** Callback fired when the "fix the errors" link is pressed */ onFixTheErrorsLinkPressed: PropTypes.func, diff --git a/src/components/FormAlertWrapper.js b/src/components/FormAlertWrapper.js index d3f49728bfec..f26161efa847 100644 --- a/src/components/FormAlertWrapper.js +++ b/src/components/FormAlertWrapper.js @@ -10,6 +10,7 @@ import {withNetwork} from './OnyxProvider'; import RenderHTML from './RenderHTML'; import Text from './Text'; import TextLink from './TextLink'; +import translatableTextPropTypes from './translatableTextPropTypes'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; const propTypes = { @@ -27,7 +28,7 @@ const propTypes = { isMessageHtml: PropTypes.bool, /** Error message to display above button */ - message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + message: translatableTextPropTypes, /** Props to detect online status */ network: networkPropTypes.isRequired, diff --git a/src/components/FormHelpMessage.js b/src/components/FormHelpMessage.js index bec02c3d51f0..6644bbc0ccee 100644 --- a/src/components/FormHelpMessage.js +++ b/src/components/FormHelpMessage.js @@ -9,10 +9,11 @@ import useThemeStyles from '@styles/useThemeStyles'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Text from './Text'; +import translatableTextPropTypes from './translatableTextPropTypes'; const propTypes = { /** Error or hint text. Ignored when children is not empty */ - message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + message: translatableTextPropTypes, /** Children to render next to dot indicator */ children: PropTypes.node, diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index f457e4e2a494..339c15d0c1e1 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import refPropTypes from '@components/refPropTypes'; +import translatableTextPropTypes from '@components/translatableTextPropTypes'; const propTypes = { /** Callback to execute when the text input is modified correctly */ @@ -12,7 +13,7 @@ const propTypes = { disabled: PropTypes.bool, /** Error text to show */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + errorText: translatableTextPropTypes, /** A ref forwarded to the TextInput */ forwardedRef: refPropTypes, diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js index 5387d1ff81d1..48e5556738bd 100644 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import translatableTextPropTypes from '@components/translatableTextPropTypes'; const propTypes = { /** Input label */ @@ -17,7 +18,7 @@ const propTypes = { placeholder: PropTypes.string, /** Error text to display */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + errorText: translatableTextPropTypes, /** Icon to display in right side of text input */ icon: PropTypes.func, diff --git a/src/components/translatableTextPropTypes.js b/src/components/translatableTextPropTypes.js new file mode 100644 index 000000000000..8da65b0ba202 --- /dev/null +++ b/src/components/translatableTextPropTypes.js @@ -0,0 +1,8 @@ +import PropTypes from 'prop-types'; + +/** + * Traslatable text with phrase key and/or variables + * + * E.g. ['common.error.characterLimitExceedCounter', {length: 5, limit: 20}] + */ +export default PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]); From bcd1c1950341f43171243e51c7a88d0177a9ac84 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 1 Dec 2023 19:44:59 +0700 Subject: [PATCH 024/446] use translatable text type for hint --- .../TextInput/BaseTextInput/baseTextInputPropTypes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js index 48e5556738bd..14f1db5ea045 100644 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -65,7 +65,7 @@ const propTypes = { maxLength: PropTypes.number, /** Hint text to display below the TextInput */ - hint: PropTypes.string, + hint: translatableTextPropTypes, /** Prefix character */ prefixCharacter: PropTypes.string, From 7d8fbbe35b9c28fc796ffea863c93a2501226362 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 12 Dec 2023 09:01:12 +1300 Subject: [PATCH 025/446] merge with main and addressed reviewer comments --- src/components/PDFView/index.native.js | 29 ++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 922123f123cc..a00f07e2dd2e 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -1,17 +1,18 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useState, useCallback} from 'react'; import {View} from 'react-native'; import PDF from 'react-native-pdf'; +import CONST from '@src/CONST'; +import compose from '@libs/compose'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; -import withKeyboardState, {keyboardStatePropTypes} from '@components/withKeyboardState'; -import withLocalize from '@components/withLocalize'; +import useKeyboardState, {keyboardStatePropTypes} from '@hooks/useKeyboardState'; +import useLocalize from '@hooks/useLocalize'; import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import withWindowDimensions from '@components/withWindowDimensions'; -import compose from '@libs/compose'; -import CONST from '@src/CONST'; +import useThemeStyles from '@styles/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import {withThemeStylesPropTypes} from '@components/withThemeStyles'; import PDFPasswordForm from './PDFPasswordForm'; import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; @@ -21,6 +22,7 @@ const propTypes = { ...withThemeStylesPropTypes, ...withStyleUtilsPropTypes, }; + /** * On the native layer, we use react-native-pdf/PDF to display PDFs. If a PDF is * password-protected we render a PDFPasswordForm to request a password @@ -72,18 +74,23 @@ function PDFView({ * Note that the message doesn't specify whether the password is simply empty or * invalid. */ - - function initiatePasswordChallenge() { + + const initiatePasswordChallenge = useCallback(() => { setShouldShowLoadingIndicator(false); + + // Render password form, and don't render PDF and loading indicator. + setShouldRequestPassword(true); setShouldAttemptPDFLoad(false); + // The message provided by react-native-pdf doesn't indicate whether this // is an initial password request or if the password is invalid. So we just assume // that if a password was already entered then it's an invalid password error. + if (password) { setIsPasswordInvalid(true); } - } + }, [password]); const handleFailureToLoadPDF = (error) => { if (error.message.match(/password/i)) { @@ -188,4 +195,4 @@ PDFView.displayName = 'PDFView'; PDFView.propTypes = propTypes; PDFView.defaultProps = defaultProps; -export default compose(withWindowDimensions, withKeyboardState, withLocalize, withThemeStyles, withStyleUtils)(PDFView); +export default compose(useWindowDimensions, useKeyboardState, useLocalize, useThemeStyles, withStyleUtils)(PDFView); From ee78ab661b77202031184fe5ab15c84a0b0cf815 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 12 Dec 2023 10:09:15 +1300 Subject: [PATCH 026/446] fix lint issue --- src/components/PDFView/index.native.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index a00f07e2dd2e..96660062d600 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -1,18 +1,19 @@ -import React, {useEffect, useState, useCallback} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import PDF from 'react-native-pdf'; -import CONST from '@src/CONST'; -import compose from '@libs/compose'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; +import {withStyleUtilsPropTypes} from '@components/withStyleUtils'; +import {withThemeStylesPropTypes} from '@components/withThemeStyles'; import useKeyboardState, {keyboardStatePropTypes} from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; -import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; -import useThemeStyles from '@styles/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {withThemeStylesPropTypes} from '@components/withThemeStyles'; +import compose from '@libs/compose'; +import useStyleUtils from '@styles/useStyleUtils'; +import useThemeStyles from '@styles/useThemeStyles'; +import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; @@ -61,6 +62,7 @@ function PDFView({ const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); const [password, setPassword] = useState(''); + const StyleUtils = useStyleUtils(); useEffect(() => { onToggleKeyboard(isKeyboardShown); @@ -86,7 +88,7 @@ function PDFView({ // The message provided by react-native-pdf doesn't indicate whether this // is an initial password request or if the password is invalid. So we just assume // that if a password was already entered then it's an invalid password error. - + if (password) { setIsPasswordInvalid(true); } @@ -195,4 +197,4 @@ PDFView.displayName = 'PDFView'; PDFView.propTypes = propTypes; PDFView.defaultProps = defaultProps; -export default compose(useWindowDimensions, useKeyboardState, useLocalize, useThemeStyles, withStyleUtils)(PDFView); +export default compose(useWindowDimensions, useKeyboardState, useLocalize, useThemeStyles, useStyleUtils)(PDFView); From 371b846fff8780b86fa7e7e633f6ce3fb2557a43 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Tue, 12 Dec 2023 11:21:23 +1300 Subject: [PATCH 027/446] fix hook issue with previous commit --- src/components/PDFView/index.native.js | 46 ++++++++------------------ 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 96660062d600..b2088aba2962 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -5,12 +5,9 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; -import {withStyleUtilsPropTypes} from '@components/withStyleUtils'; -import {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import useKeyboardState, {keyboardStatePropTypes} from '@hooks/useKeyboardState'; +import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; @@ -19,11 +16,7 @@ import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; const propTypes = { ...pdfViewPropTypes, - ...keyboardStatePropTypes, - ...withThemeStylesPropTypes, - ...withStyleUtilsPropTypes, }; - /** * On the native layer, we use react-native-pdf/PDF to display PDFs. If a PDF is * password-protected we render a PDFPasswordForm to request a password @@ -39,22 +32,7 @@ const propTypes = { * is (temporarily) rendered. */ -function PDFView({ - onToggleKeyboard, - isKeyboardShown, - onLoadComplete, - translate, - fileName, - onPress, - isFocused, - onScaleChanged, - sourceURL, - isSmallScreenWidth, - windowWidth, - windowHeight, - errorLabelStyles, - themeStyles, -}) { +function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, errorLabelStyles}) { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); @@ -62,6 +40,10 @@ function PDFView({ const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); const [password, setPassword] = useState(''); + const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const isKeyboardShown = useKeyboardState(); const StyleUtils = useStyleUtils(); useEffect(() => { @@ -81,14 +63,12 @@ function PDFView({ setShouldShowLoadingIndicator(false); // Render password form, and don't render PDF and loading indicator. - setShouldRequestPassword(true); setShouldAttemptPDFLoad(false); // The message provided by react-native-pdf doesn't indicate whether this // is an initial password request or if the password is invalid. So we just assume // that if a password was already entered then it's an invalid password error. - if (password) { setIsPasswordInvalid(true); } @@ -132,22 +112,22 @@ function PDFView({ }; function renderPDFView() { - const pdfStyles = [themeStyles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(windowWidth, windowHeight)]; + const pdfStyles = [styles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(windowWidth, windowHeight)]; // If we haven't yet successfully validated the password and loaded the PDF, // then we need to hide the react-native-pdf/PDF component so that PDFPasswordForm // is positioned nicely. We're specifically hiding it because we still need to render // the PDF component so that it can validate the password. if (shouldRequestPassword) { - pdfStyles.push(themeStyles.invisible); + pdfStyles.push(styles.invisible); } - const containerStyles = shouldRequestPassword && isSmallScreenWidth ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; + const containerStyles = shouldRequestPassword && isSmallScreenWidth ? [styles.w100, styles.flex1] : [styles.alignItemsCenter, styles.flex1]; return ( {failedToLoadPDF && ( - + {translate('attachmentView.failedToLoadPDF')} )} @@ -166,7 +146,7 @@ function PDFView({ /> )} {shouldRequestPassword && ( - + @@ -197,4 +177,4 @@ PDFView.displayName = 'PDFView'; PDFView.propTypes = propTypes; PDFView.defaultProps = defaultProps; -export default compose(useWindowDimensions, useKeyboardState, useLocalize, useThemeStyles, useStyleUtils)(PDFView); +export default PDFView; From 203e2ba67d44ec2c355181e88f4f202b16af07a2 Mon Sep 17 00:00:00 2001 From: kadiealexander <59587260+kadiealexander@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:53:41 +1300 Subject: [PATCH 028/446] Update index.native.js add line break --- src/components/PDFView/index.native.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index b2088aba2962..38ecf1d5eee6 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -17,6 +17,7 @@ import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; const propTypes = { ...pdfViewPropTypes, }; + /** * On the native layer, we use react-native-pdf/PDF to display PDFs. If a PDF is * password-protected we render a PDFPasswordForm to request a password From d20986f9ef855436f2f769fb135d1a000d6da3c1 Mon Sep 17 00:00:00 2001 From: kadiealexander <59587260+kadiealexander@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:54:46 +1300 Subject: [PATCH 029/446] Update index.native.js add line break near end of file --- src/components/PDFView/index.native.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 38ecf1d5eee6..c47ebcaf1093 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -174,6 +174,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused renderPDFView() ); } + PDFView.displayName = 'PDFView'; PDFView.propTypes = propTypes; PDFView.defaultProps = defaultProps; From ebf46e001de722da46bf8aab9fb9558d2145af02 Mon Sep 17 00:00:00 2001 From: dhairyasenjaliya Date: Tue, 26 Dec 2023 21:00:37 +0530 Subject: [PATCH 030/446] Added limit exceed error message with new limit --- src/CONST.ts | 5 ++++- src/pages/EditRequestDescriptionPage.js | 17 ++++++++++++++++ src/pages/ReportWelcomeMessagePage.js | 20 +++++++++++++++++++ src/pages/iou/MoneyRequestDescriptionPage.js | 20 +++++++++++++++++++ .../request/step/IOURequestStepDescription.js | 20 +++++++++++++++++++ src/pages/tasks/NewTaskDescriptionPage.js | 19 ++++++++++++++++++ src/pages/tasks/NewTaskDetailsPage.js | 7 +++++++ src/pages/tasks/NewTaskTitlePage.js | 2 ++ src/pages/tasks/TaskDescriptionPage.js | 16 ++++++++++++++- src/pages/tasks/TaskTitlePage.js | 3 +++ src/pages/workspace/WorkspaceNewRoomPage.js | 2 ++ src/pages/workspace/WorkspaceSettingsPage.js | 6 +++--- 12 files changed, 132 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index edfea995ed9b..bc3cfee9cb3b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1488,7 +1488,10 @@ const CONST = { FORM_CHARACTER_LIMIT: 50, LEGAL_NAMES_CHARACTER_LIMIT: 150, LOGIN_CHARACTER_LIMIT: 254, - WORKSPACE_NAME_CHARACTER_LIMIT: 80, + + TITLE_CHARACTER_LIMIT: 100, + SUPPORTING_CHARACTER_LIMIT: 500, + AVATAR_CROP_MODAL: { // The next two constants control what is min and max value of the image crop scale. // Values define in how many times the image can be bigger than its container. diff --git a/src/pages/EditRequestDescriptionPage.js b/src/pages/EditRequestDescriptionPage.js index 9b2a9e465746..39d1399aad9d 100644 --- a/src/pages/EditRequestDescriptionPage.js +++ b/src/pages/EditRequestDescriptionPage.js @@ -10,6 +10,7 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; +import * as ErrorUtils from '@libs/ErrorUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -28,6 +29,21 @@ function EditRequestDescriptionPage({defaultDescription, onSubmit}) { const descriptionInputRef = useRef(null); const focusTimeoutRef = useRef(null); + /** + * @param {Object} values + * @param {String} values.title + * @returns {Object} - An object containing the errors for each inputID + */ + const validate = useCallback((values) => { + const errors = {}; + + if (values.comment.length > CONST.SUPPORTING_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'comment', ['common.error.characterLimitExceedCounter', {length: values.comment.length, limit: CONST.SUPPORTING_CHARACTER_LIMIT}]); + } + + return errors; + }, []); + useFocusEffect( useCallback(() => { focusTimeoutRef.current = setTimeout(() => { @@ -55,6 +71,7 @@ function EditRequestDescriptionPage({defaultDescription, onSubmit}) { style={[styles.flexGrow1, styles.ph5]} formID={ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM} onSubmit={onSubmit} + validate={validate} submitButtonText={translate('common.save')} enabledWhenOffline > diff --git a/src/pages/ReportWelcomeMessagePage.js b/src/pages/ReportWelcomeMessagePage.js index ae8a4635a98e..218c8523e2b0 100644 --- a/src/pages/ReportWelcomeMessagePage.js +++ b/src/pages/ReportWelcomeMessagePage.js @@ -14,6 +14,7 @@ import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; @@ -56,6 +57,24 @@ function ReportWelcomeMessagePage(props) { setWelcomeMessage(value); }, []); + /** + * @param {Object} values + * @param {String} values.title + * @returns {Object} - An object containing the errors for each inputID + */ + const validate = useCallback((values) => { + const errors = {}; + + if (values.welcomeMessage.length > CONST.SUPPORTING_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'welcomeMessage', [ + 'common.error.characterLimitExceedCounter', + {length: values.welcomeMessage.length, limit: CONST.SUPPORTING_CHARACTER_LIMIT}, + ]); + } + + return errors; + }, []); + const submitForm = useCallback(() => { Report.updateWelcomeMessage(props.report.reportID, props.report.welcomeMessage, welcomeMessage.trim()); }, [props.report.reportID, props.report.welcomeMessage, welcomeMessage]); @@ -91,6 +110,7 @@ function ReportWelcomeMessagePage(props) { style={[styles.flexGrow1, styles.ph5]} formID={ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM} onSubmit={submitForm} + validate={validate} submitButtonText={props.translate('common.save')} enabledWhenOffline > diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index fe3100b8c3bd..643a068efb45 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -14,6 +14,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as IOU from '@libs/actions/IOU'; import * as Browser from '@libs/Browser'; +import * as ErrorUtils from '@libs/ErrorUtils'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; @@ -95,6 +96,24 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); } + /** + * @param {Object} values + * @param {String} values.title + * @returns {Object} - An object containing the errors for each inputID + */ + const validate = useCallback((values) => { + const errors = {}; + + if (values.moneyRequestComment.length > CONST.SUPPORTING_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'moneyRequestComment', [ + 'common.error.characterLimitExceedCounter', + {length: values.moneyRequestComment.length, limit: CONST.SUPPORTING_CHARACTER_LIMIT}, + ]); + } + + return errors; + }, []); + /** * Sets the money request comment by saving it to Onyx. * @@ -121,6 +140,7 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { style={[styles.flexGrow1, styles.ph5]} formID={ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM} onSubmit={(value) => updateComment(value)} + validate={validate} submitButtonText={translate('common.save')} enabledWhenOffline > diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.js index 849f3276667e..addad5af97c9 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.js +++ b/src/pages/iou/request/step/IOURequestStepDescription.js @@ -10,6 +10,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; import compose from '@libs/compose'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as IOU from '@userActions/IOU'; @@ -61,6 +62,24 @@ function IOURequestStepDescription({ }, []), ); + /** + * @param {Object} values + * @param {String} values.title + * @returns {Object} - An object containing the errors for each inputID + */ + const validate = useCallback((values) => { + const errors = {}; + + if (values.moneyRequestComment.length > CONST.SUPPORTING_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'moneyRequestComment', [ + 'common.error.characterLimitExceedCounter', + {length: values.moneyRequestComment.length, limit: CONST.SUPPORTING_CHARACTER_LIMIT}, + ]); + } + + return errors; + }, []); + const navigateBack = () => { Navigation.goBack(backTo || ROUTES.HOME); }; @@ -85,6 +104,7 @@ function IOURequestStepDescription({ style={[styles.flexGrow1, styles.ph5]} formID={ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM} onSubmit={updateComment} + validate={validate} submitButtonText={translate('common.save')} enabledWhenOffline > diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js index b11e7c163755..cf9b00cc308a 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.js +++ b/src/pages/tasks/NewTaskDescriptionPage.js @@ -13,6 +13,7 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; import compose from '@libs/compose'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as Task from '@userActions/Task'; @@ -47,6 +48,23 @@ function NewTaskDescriptionPage(props) { Navigation.goBack(ROUTES.NEW_TASK); }; + /** + * @param {Object} values - form input values passed by the Form component + * @returns {Boolean} + */ + function validate(values) { + const errors = {}; + + if (values.taskDescription.length > CONST.SUPPORTING_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'taskDescription', [ + 'common.error.characterLimitExceedCounter', + {length: values.taskDescription.length, limit: CONST.SUPPORTING_CHARACTER_LIMIT}, + ]); + } + + return errors; + } + return ( validate(values)} onSubmit={(values) => onSubmit(values)} enabledWhenOffline > diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index 3dab58dfad04..f57cca7799ca 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -58,6 +58,13 @@ function NewTaskDetailsPage(props) { if (!values.taskTitle) { // We error if the user doesn't enter a task name ErrorUtils.addErrorMessage(errors, 'taskTitle', 'newTaskPage.pleaseEnterTaskName'); + } else if (values.taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'taskTitle', ['common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); + } else if (values.taskDescription.length > CONST.SUPPORTING_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'taskDescription', [ + 'common.error.characterLimitExceedCounter', + {length: values.taskDescription.length, limit: CONST.SUPPORTING_CHARACTER_LIMIT}, + ]); } return errors; diff --git a/src/pages/tasks/NewTaskTitlePage.js b/src/pages/tasks/NewTaskTitlePage.js index 7bf6065625c0..e6da2a06435d 100644 --- a/src/pages/tasks/NewTaskTitlePage.js +++ b/src/pages/tasks/NewTaskTitlePage.js @@ -48,6 +48,8 @@ function NewTaskTitlePage(props) { if (!values.taskTitle) { // We error if the user doesn't enter a task name ErrorUtils.addErrorMessage(errors, 'taskTitle', 'newTaskPage.pleaseEnterTaskName'); + } else if (values.taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'taskTitle', ['common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } return errors; diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index 3a6999d4408a..45b7f786cb2b 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -17,6 +17,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; import compose from '@libs/compose'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; @@ -49,7 +50,20 @@ const defaultProps = { const parser = new ExpensiMark(); function TaskDescriptionPage(props) { const styles = useThemeStyles(); - const validate = useCallback(() => ({}), []); + + /** + * @param {Object} values - form input values passed by the Form component + * @returns {Boolean} + */ + const validate = useCallback((values) => { + const errors = {}; + + if (values.description.length > CONST.SUPPORTING_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.SUPPORTING_CHARACTER_LIMIT}]); + } + + return errors; + }, []); const submit = useCallback( (values) => { diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.js index 9b393a8a2374..e9856c85e0d2 100644 --- a/src/pages/tasks/TaskTitlePage.js +++ b/src/pages/tasks/TaskTitlePage.js @@ -14,6 +14,7 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; @@ -53,6 +54,8 @@ function TaskTitlePage(props) { if (_.isEmpty(values.title)) { errors.title = 'newTaskPage.pleaseEnterTaskName'; + } else if (values.title.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'title', ['common.error.characterLimitExceedCounter', {length: values.title.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } return errors; diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 6f86e871e8ae..bde8f3b37f8c 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -183,6 +183,8 @@ function WorkspaceNewRoomPage(props) { } else if (ValidationUtils.isExistingRoomName(values.roomName, props.reports, values.policyID)) { // Certain names are reserved for default rooms and should not be used for policy rooms. ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomAlreadyExistsError'); + } else if (values.roomName.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'roomName', ['common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } if (!values.policyID) { diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 9bc4b755fce9..9ec33b834521 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -18,6 +18,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; @@ -81,10 +82,10 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { if (!ValidationUtils.isRequiredFulfilled(name)) { errors.name = 'workspace.editor.nameIsRequiredError'; - } else if ([...name].length > CONST.WORKSPACE_NAME_CHARACTER_LIMIT) { + } else if ([...name].length > CONST.TITLE_CHARACTER_LIMIT) { // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 // code units. - errors.name = 'workspace.editor.nameIsTooLongError'; + ErrorUtils.addErrorMessage(errors, 'name', ['common.error.characterLimitExceedCounter', {length: [...name].length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } return errors; @@ -148,7 +149,6 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { label={translate('workspace.editor.nameInputLabel')} accessibilityLabel={translate('workspace.editor.nameInputLabel')} defaultValue={policy.name} - maxLength={CONST.WORKSPACE_NAME_CHARACTER_LIMIT} containerStyles={[styles.mt4]} spellCheck={false} /> From d791082cff83e311765ee8ea1a25bfe7663e5fbc Mon Sep 17 00:00:00 2001 From: dhairyasenjaliya Date: Tue, 26 Dec 2023 21:06:42 +0530 Subject: [PATCH 031/446] type fixes --- src/languages/en.ts | 1 - src/languages/es.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b2c08ac80974..59f61c434ee7 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1600,7 +1600,6 @@ export default { nameInputLabel: 'Name', nameInputHelpText: 'This is the name you will see on your workspace.', nameIsRequiredError: 'You need to define a name for your workspace.', - nameIsTooLongError: `Your workspace name can be at most ${CONST.WORKSPACE_NAME_CHARACTER_LIMIT} characters long.`, currencyInputLabel: 'Default currency', currencyInputHelpText: 'All expenses on this workspace will be converted to this currency.', currencyInputDisabledText: "The default currency can't be changed because this workspace is linked to a USD bank account.", diff --git a/src/languages/es.ts b/src/languages/es.ts index dfd6dab6c388..1dbd99eea92e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1624,7 +1624,6 @@ export default { nameInputLabel: 'Nombre', nameInputHelpText: 'Este es el nombre que verás en tu espacio de trabajo.', nameIsRequiredError: 'Debes definir un nombre para tu espacio de trabajo.', - nameIsTooLongError: `El nombre de su espacio de trabajo no puede tener más de ${CONST.WORKSPACE_NAME_CHARACTER_LIMIT} caracteres.`, currencyInputLabel: 'Moneda por defecto', currencyInputHelpText: 'Todas los gastos en este espacio de trabajo serán convertidos a esta moneda.', currencyInputDisabledText: 'La moneda predeterminada no se puede cambiar porque este espacio de trabajo está vinculado a una cuenta bancaria en USD.', From 5276da1d4b530f4a584a86cca6c7fda4faab1b0e Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 17:23:25 +0700 Subject: [PATCH 032/446] use MaybePhraseKey type --- src/components/CheckboxWithLabel.tsx | 3 ++- src/components/CountrySelector.js | 3 ++- src/components/FormAlertWithSubmitButton.tsx | 3 ++- src/components/FormAlertWrapper.tsx | 3 ++- src/components/MagicCodeInput.js | 3 ++- src/components/MenuItem.tsx | 3 ++- ...TemporaryForRefactorRequestConfirmationList.js | 4 ++-- src/components/Picker/types.ts | 3 ++- src/components/RadioButtonWithLabel.tsx | 3 ++- src/components/StatePicker/index.js | 3 ++- src/components/TimePicker/TimePicker.js | 2 +- src/components/ValuePicker/index.js | 3 ++- src/components/translatableTextPropTypes.js | 1 + src/libs/ErrorUtils.ts | 15 ++++++--------- 14 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx index 9660c9e1a2e5..e3a6b5fc44c7 100644 --- a/src/components/CheckboxWithLabel.tsx +++ b/src/components/CheckboxWithLabel.tsx @@ -1,6 +1,7 @@ import React, {ComponentType, ForwardedRef, useState} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import variables from '@styles/variables'; import Checkbox from './Checkbox'; import FormHelpMessage from './FormHelpMessage'; @@ -38,7 +39,7 @@ type CheckboxWithLabelProps = RequiredLabelProps & { style?: StyleProp; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Value for checkbox. This prop is intended to be set by Form.js only */ value?: boolean; diff --git a/src/components/CountrySelector.js b/src/components/CountrySelector.js index 68a6486bce48..01d297d35467 100644 --- a/src/components/CountrySelector.js +++ b/src/components/CountrySelector.js @@ -8,10 +8,11 @@ import ROUTES from '@src/ROUTES'; import FormHelpMessage from './FormHelpMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import refPropTypes from './refPropTypes'; +import translatableTextPropTypes from './translatableTextPropTypes'; const propTypes = { /** Form error text. e.g when no country is selected */ - errorText: PropTypes.string, + errorText: translatableTextPropTypes, /** Callback called when the country changes. */ onInputChange: PropTypes.func.isRequired, diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index d8e30b27371d..d9412bf79857 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -1,12 +1,13 @@ import React from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import Button from './Button'; import FormAlertWrapper from './FormAlertWrapper'; type FormAlertWithSubmitButtonProps = { /** Error message to display above button */ - message?: string; + message?: MaybePhraseKey; /** Whether the button is disabled */ isDisabled?: boolean; diff --git a/src/components/FormAlertWrapper.tsx b/src/components/FormAlertWrapper.tsx index a144bf069502..9d366fd72cb0 100644 --- a/src/components/FormAlertWrapper.tsx +++ b/src/components/FormAlertWrapper.tsx @@ -2,6 +2,7 @@ import React, {ReactNode} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import Network from '@src/types/onyx/Network'; import FormHelpMessage from './FormHelpMessage'; import {withNetwork} from './OnyxProvider'; @@ -26,7 +27,7 @@ type FormAlertWrapperProps = { isMessageHtml?: boolean; /** Error message to display above button */ - message?: string; + message?: MaybePhraseKey; /** Props to detect online status */ network: Network; diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 55a65237a691..b075edc9aeca 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -14,6 +14,7 @@ import networkPropTypes from './networkPropTypes'; import {withNetwork} from './OnyxProvider'; import Text from './Text'; import TextInput from './TextInput'; +import translatableTextPropTypes from './translatableTextPropTypes'; const TEXT_INPUT_EMPTY_STATE = ''; @@ -34,7 +35,7 @@ const propTypes = { shouldDelayFocus: PropTypes.bool, /** Error text to display */ - errorText: PropTypes.string, + errorText: translatableTextPropTypes, /** Specifies autocomplete hints for the system, so it can provide autofill */ autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code', 'off']).isRequired, diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index db150d55f0d2..a713e11e5871 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -12,6 +12,7 @@ import ControlSelection from '@libs/ControlSelection'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getButtonState from '@libs/getButtonState'; +import type {MaybePhraseKey} from '@libs/Localize'; import {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; @@ -142,7 +143,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & error?: string; /** Error to display at the bottom of the component */ - errorText?: string; + errorText?: MaybePhraseKey; /** A boolean flag that gives the icon a green fill if true */ success?: boolean; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 20012bc90ef0..32b9100c0803 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -545,13 +545,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ )} {button} ); - }, [confirm, bankAccountRoute, iouCurrencyCode, iouType, isReadOnly, policyID, selectedParticipants, splitOrRequestOptions, translate, formError, styles.ph1, styles.mb2]); + }, [confirm, bankAccountRoute, iouCurrencyCode, iouType, isReadOnly, policyID, selectedParticipants, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); const {image: receiptImage, thumbnail: receiptThumbnail} = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; return ( diff --git a/src/components/Picker/types.ts b/src/components/Picker/types.ts index 58eed0371893..3fada48005f5 100644 --- a/src/components/Picker/types.ts +++ b/src/components/Picker/types.ts @@ -1,5 +1,6 @@ import {ChangeEvent, Component, ReactElement} from 'react'; import {MeasureLayoutOnSuccessCallback, NativeMethods, StyleProp, ViewStyle} from 'react-native'; +import type {MaybePhraseKey} from '@libs/Localize'; type MeasureLayoutOnFailCallback = () => void; @@ -58,7 +59,7 @@ type BasePickerProps = { placeholder?: PickerPlaceholder; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Customize the BasePicker container */ containerStyles?: StyleProp; diff --git a/src/components/RadioButtonWithLabel.tsx b/src/components/RadioButtonWithLabel.tsx index 4c223262ac50..5327b9dbb2d4 100644 --- a/src/components/RadioButtonWithLabel.tsx +++ b/src/components/RadioButtonWithLabel.tsx @@ -1,6 +1,7 @@ import React, {ComponentType} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {MaybePhraseKey} from '@libs/Localize'; import FormHelpMessage from './FormHelpMessage'; import * as Pressables from './Pressable'; import RadioButton from './RadioButton'; @@ -26,7 +27,7 @@ type RadioButtonWithLabelProps = { hasError?: boolean; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; }; const PressableWithFeedback = Pressables.PressableWithFeedback; diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index 6fa60fbba947..e937fb2f76fd 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -6,13 +6,14 @@ import _ from 'underscore'; import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import refPropTypes from '@components/refPropTypes'; +import translatableTextPropTypes from '@components/translatableTextPropTypes'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import StateSelectorModal from './StateSelectorModal'; const propTypes = { /** Error text to display */ - errorText: PropTypes.string, + errorText: translatableTextPropTypes, /** State to display */ value: PropTypes.string, diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 5b49739150cc..f0633415c78b 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -446,7 +446,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { {isError ? ( ) : ( diff --git a/src/components/ValuePicker/index.js b/src/components/ValuePicker/index.js index b5ddaa7dcb73..38e1689f3b10 100644 --- a/src/components/ValuePicker/index.js +++ b/src/components/ValuePicker/index.js @@ -5,6 +5,7 @@ import {View} from 'react-native'; import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import refPropTypes from '@components/refPropTypes'; +import translatableTextPropTypes from '@components/translatableTextPropTypes'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; @@ -12,7 +13,7 @@ import ValueSelectorModal from './ValueSelectorModal'; const propTypes = { /** Form Error description */ - errorText: PropTypes.string, + errorText: translatableTextPropTypes, /** Item to display */ value: PropTypes.string, diff --git a/src/components/translatableTextPropTypes.js b/src/components/translatableTextPropTypes.js index 8da65b0ba202..10130ab2da3e 100644 --- a/src/components/translatableTextPropTypes.js +++ b/src/components/translatableTextPropTypes.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; /** * Traslatable text with phrase key and/or variables + * Use Localize.MaybePhraseKey instead for Typescript * * E.g. ['common.error.characterLimitExceedCounter', {length: 5, limit: 20}] */ diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 4c369d6a8b4f..e3dd952c7ad0 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -56,9 +56,7 @@ type OnyxDataWithErrors = { errors?: Errors; }; -type TranslationData = [string, Record] | string; - -function getLatestErrorMessage(onyxData: TOnyxData): TranslationData { +function getLatestErrorMessage(onyxData: TOnyxData): Localize.MaybePhraseKey { const errors = onyxData.errors ?? {}; if (Object.keys(errors).length === 0) { @@ -74,7 +72,7 @@ type OnyxDataWithErrorFields = { errorFields?: ErrorFields; }; -function getLatestErrorField(onyxData: TOnyxData, fieldName: string): Record { +function getLatestErrorField(onyxData: TOnyxData, fieldName: string): Record { const errorsForField = onyxData.errorFields?.[fieldName] ?? {}; if (Object.keys(errorsForField).length === 0) { @@ -86,7 +84,7 @@ function getLatestErrorField(onyxData return {[key]: [errorsForField[key], {isTranslated: true}]}; } -function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { +function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { const errorsForField = onyxData.errorFields?.[fieldName] ?? {}; if (Object.keys(errorsForField).length === 0) { @@ -98,15 +96,14 @@ function getEarliestErrorField(onyxDa return {[key]: [errorsForField[key], {isTranslated: true}]}; } -type ErrorsList = Record; -type ErrorsListWithTranslationData = Record; +type ErrorsList = Record; /** * Method used to attach already translated message with isTranslated: true property * @param errors - An object containing current errors in the form * @returns Errors in the form of {timestamp: [message, {isTranslated: true}]} */ -function getErrorMessagesWithTranslationData(errors: TranslationData | ErrorsList): ErrorsListWithTranslationData { +function getErrorMessagesWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsList): ErrorsList { if (isEmpty(errors)) { return {}; } @@ -114,7 +111,7 @@ function getErrorMessagesWithTranslationData(errors: TranslationData | ErrorsLis if (typeof errors === 'string' || Array.isArray(errors)) { const [message, variables] = Array.isArray(errors) ? errors : [errors]; // eslint-disable-next-line @typescript-eslint/naming-convention - return {0: [message, {...variables, isTranslated: true}]}; + return {0: [message as string, {...variables, isTranslated: true}]}; } return mapKeys(errors, (message) => [message, {isTranslated: true}]); From 93de9802ff67a2c8ebe1b493efac97988b5a4190 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 17:30:58 +0700 Subject: [PATCH 033/446] fix type --- src/libs/ErrorUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index e3dd952c7ad0..0f97fa8f39cc 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -96,7 +96,7 @@ function getEarliestErrorField(onyxDa return {[key]: [errorsForField[key], {isTranslated: true}]}; } -type ErrorsList = Record; +type ErrorsList = Record; /** * Method used to attach already translated message with isTranslated: true property @@ -111,7 +111,7 @@ function getErrorMessagesWithTranslationData(errors: Localize.MaybePhraseKey | E if (typeof errors === 'string' || Array.isArray(errors)) { const [message, variables] = Array.isArray(errors) ? errors : [errors]; // eslint-disable-next-line @typescript-eslint/naming-convention - return {0: [message as string, {...variables, isTranslated: true}]}; + return {'0': [message as string, {...variables, isTranslated: true}]}; } return mapKeys(errors, (message) => [message, {isTranslated: true}]); From 916e3decb5fb25aa5757645226aa98e1fc8bf640 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 17:42:35 +0700 Subject: [PATCH 034/446] fix type --- src/components/FormAlertWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FormAlertWrapper.tsx b/src/components/FormAlertWrapper.tsx index 9d366fd72cb0..ef7e57758e3e 100644 --- a/src/components/FormAlertWrapper.tsx +++ b/src/components/FormAlertWrapper.tsx @@ -67,7 +67,7 @@ function FormAlertWrapper({ {` ${translate('common.inTheFormBeforeContinuing')}.`} ); - } else if (isMessageHtml) { + } else if (isMessageHtml && typeof message === 'string') { content = ${message}`} />; } From a6a834015904b14dbe2e50bad458584e6f0c499a Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 17:48:44 +0700 Subject: [PATCH 035/446] remove redundant logic --- .../Contacts/ValidateCodeForm/BaseValidateCodeForm.js | 2 +- .../TwoFactorAuthForm/BaseTwoFactorAuthForm.js | 2 +- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js index f7008715f406..8b19c7bdd233 100644 --- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js @@ -188,7 +188,7 @@ function BaseValidateCodeForm(props) { name="validateCode" value={validateCode} onChangeText={onTextInput} - errorText={formError.validateCode ? formError.validateCode : ErrorUtils.getLatestErrorMessage(props.account)} + errorText={formError.validateCode || ErrorUtils.getLatestErrorMessage(props.account)} hasError={!_.isEmpty(validateLoginError)} onFulfill={validateAndSubmitForm} autoFocus={false} diff --git a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js index 77898916c353..f65f7368de76 100644 --- a/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js +++ b/src/pages/settings/Security/TwoFactorAuth/TwoFactorAuthForm/BaseTwoFactorAuthForm.js @@ -93,7 +93,7 @@ function BaseTwoFactorAuthForm(props) { value={twoFactorAuthCode} onChangeText={onTextInput} onFulfill={validateAndSubmitForm} - errorText={formError.twoFactorAuthCode ? formError.twoFactorAuthCode : ErrorUtils.getLatestErrorMessage(props.account)} + errorText={formError.twoFactorAuthCode || ErrorUtils.getLatestErrorMessage(props.account)} ref={inputRef} autoFocus={false} /> diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index eaff916004be..98dc6bc68f99 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -310,7 +310,7 @@ function BaseValidateCodeForm(props) { onChangeText={(text) => onTextInput(text, 'recoveryCode')} maxLength={CONST.RECOVERY_CODE_LENGTH} label={props.translate('recoveryCodeForm.recoveryCode')} - errorText={formError.recoveryCode ? formError.recoveryCode : ''} + errorText={formError.recoveryCode || ''} hasError={hasError} onSubmitEditing={validateAndSubmitForm} autoFocus @@ -326,7 +326,7 @@ function BaseValidateCodeForm(props) { onChangeText={(text) => onTextInput(text, 'twoFactorAuthCode')} onFulfill={validateAndSubmitForm} maxLength={CONST.TFA_CODE_LENGTH} - errorText={formError.twoFactorAuthCode ? formError.twoFactorAuthCode : ''} + errorText={formError.twoFactorAuthCode || ''} hasError={hasError} autoFocus key="twoFactorAuthCode" @@ -356,7 +356,7 @@ function BaseValidateCodeForm(props) { value={validateCode} onChangeText={(text) => onTextInput(text, 'validateCode')} onFulfill={validateAndSubmitForm} - errorText={formError.validateCode ? formError.validateCode : ''} + errorText={formError.validateCode || ''} hasError={hasError} autoFocus key="validateCode" From 809a5f3655ca897b01f169c011c32d45745974e1 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 18:05:30 +0700 Subject: [PATCH 036/446] fix missing translation in OfflineWithFeedback --- src/components/OfflineWithFeedback.tsx | 3 ++- src/pages/workspace/WorkspaceMembersPage.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5fcf1fe7442b..4f86218eab20 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -3,6 +3,7 @@ import {ImageStyle, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import CONST from '@src/CONST'; import * as OnyxCommon from '@src/types/onyx/OnyxCommon'; @@ -82,7 +83,7 @@ function OfflineWithFeedback({ const hasErrors = isNotEmptyObject(errors ?? {}); // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. - const errorMessages = omitBy(errors, (e) => e === null); + const errorMessages = ErrorUtils.getErrorMessagesWithTranslationData(omitBy(errors, (e) => e === null)); const hasErrorMessages = isNotEmptyObject(errorMessages); const isOfflinePendingAction = !!isOffline && !!pendingAction; const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 9834d4e9e1c0..3e2c1a5ca93f 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -407,7 +407,7 @@ function WorkspaceMembersPage(props) { return ( Policy.dismissAddedWithPrimaryLoginMessages(policyID)} /> From 7647500a94655937c3c7c135ee1aead66c143a13 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 18:07:46 +0700 Subject: [PATCH 037/446] fix lint --- src/libs/ErrorUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 0f97fa8f39cc..e1a585139c5f 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -111,7 +111,7 @@ function getErrorMessagesWithTranslationData(errors: Localize.MaybePhraseKey | E if (typeof errors === 'string' || Array.isArray(errors)) { const [message, variables] = Array.isArray(errors) ? errors : [errors]; // eslint-disable-next-line @typescript-eslint/naming-convention - return {'0': [message as string, {...variables, isTranslated: true}]}; + return {'0': [message ?? '', {...variables, isTranslated: true}]}; } return mapKeys(errors, (message) => [message, {isTranslated: true}]); From 5167629906c1d3f411105a9287fca8b70f7798f3 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 18:13:58 +0700 Subject: [PATCH 038/446] fix type --- src/libs/ErrorUtils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index e1a585139c5f..b694234ce69a 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -1,4 +1,3 @@ -import isEmpty from 'lodash/isEmpty'; import mapKeys from 'lodash/mapKeys'; import CONST from '@src/CONST'; import {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; @@ -104,14 +103,14 @@ type ErrorsList = Record; * @returns Errors in the form of {timestamp: [message, {isTranslated: true}]} */ function getErrorMessagesWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsList): ErrorsList { - if (isEmpty(errors)) { + if (!errors || (Array.isArray(errors) && errors.length === 0)) { return {}; } if (typeof errors === 'string' || Array.isArray(errors)) { const [message, variables] = Array.isArray(errors) ? errors : [errors]; // eslint-disable-next-line @typescript-eslint/naming-convention - return {'0': [message ?? '', {...variables, isTranslated: true}]}; + return {'0': [message, {...(variables ?? []), isTranslated: true}]}; } return mapKeys(errors, (message) => [message, {isTranslated: true}]); From 72e81c06a65ed11aac7e2afa08fad478300873b9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 27 Dec 2023 18:23:56 +0700 Subject: [PATCH 039/446] fix missing translations for DotIndicatorMessage --- src/libs/actions/Card.js | 5 ++--- src/pages/iou/request/step/IOURequestStepDistance.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js index 68642bd8fdf1..d0b589f00fea 100644 --- a/src/libs/actions/Card.js +++ b/src/libs/actions/Card.js @@ -1,6 +1,5 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -164,12 +163,12 @@ function revealVirtualCardDetails(cardID) { API.makeRequestWithSideEffects('RevealExpensifyCardDetails', {cardID}) .then((response) => { if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); + reject('cardPage.cardDetailsLoadingFailure'); return; } resolve(response); }) - .catch(() => reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure'))); + .catch(() => reject('cardPage.cardDetailsLoadingFailure')); }); } diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 66cbd7f135a9..5d7acb66374e 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -132,7 +132,7 @@ function IOURequestStepDistance({ } if (_.size(validatedWaypoints) < 2) { - return {0: translate('iou.error.atLeastTwoDifferentWaypoints')}; + return {0: 'iou.error.atLeastTwoDifferentWaypoints'}; } }; From 94fc3e294f0b8b3281ee6ccb24d2b2262ced3517 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 28 Dec 2023 14:27:55 +0700 Subject: [PATCH 040/446] fix lint --- src/libs/actions/Card.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js index d0b589f00fea..1fb0166ccf17 100644 --- a/src/libs/actions/Card.js +++ b/src/libs/actions/Card.js @@ -163,11 +163,13 @@ function revealVirtualCardDetails(cardID) { API.makeRequestWithSideEffects('RevealExpensifyCardDetails', {cardID}) .then((response) => { if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { + // eslint-disable-next-line prefer-promise-reject-errors reject('cardPage.cardDetailsLoadingFailure'); return; } resolve(response); }) + // eslint-disable-next-line prefer-promise-reject-errors .catch(() => reject('cardPage.cardDetailsLoadingFailure')); }); } From 84164558cc37b74a0b762e82d4beb16f10092fb3 Mon Sep 17 00:00:00 2001 From: dhairyasenjaliya Date: Fri, 29 Dec 2023 00:18:35 +0530 Subject: [PATCH 041/446] added for remaning room --- src/pages/settings/Report/RoomNamePage.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js index 5f64faca50fc..ab600161a686 100644 --- a/src/pages/settings/Report/RoomNamePage.js +++ b/src/pages/settings/Report/RoomNamePage.js @@ -69,6 +69,8 @@ function RoomNamePage({policy, report, reports, translate}) { } else if (ValidationUtils.isExistingRoomName(values.roomName, reports, report.policyID)) { // The room name can't be set to one that already exists on the policy ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomAlreadyExistsError'); + } else if (values.roomName.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'roomName', ['common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } return errors; From dea7484a711629fd7d27c0b474c661e1d79ace15 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 2 Jan 2024 17:13:13 +0700 Subject: [PATCH 042/446] handle client error --- src/components/OfflineWithFeedback.tsx | 2 +- src/languages/en.ts | 3 ++ src/languages/es.ts | 3 ++ src/libs/ErrorUtils.ts | 34 +++++++++---------- src/libs/Localize/index.ts | 2 +- src/libs/actions/Report.ts | 2 +- src/libs/actions/Session/index.ts | 2 +- .../settings/Wallet/TransferBalancePage.js | 2 +- src/pages/signin/LoginForm/BaseLoginForm.js | 2 +- src/pages/signin/UnlinkLoginForm.js | 4 +-- src/pages/workspace/WorkspaceInvitePage.js | 2 +- src/types/onyx/OnyxCommon.ts | 3 +- 12 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 0d54431da458..902e20063687 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -83,7 +83,7 @@ function OfflineWithFeedback({ const hasErrors = isNotEmptyObject(errors ?? {}); // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. - const errorMessages = ErrorUtils.getErrorMessagesWithTranslationData(omitBy(errors, (e) => e === null)); + const errorMessages = ErrorUtils.getErrorsWithTranslationData(omitBy(errors, (e) => e === null)); const hasErrorMessages = isNotEmptyObject(errorMessages); const isOfflinePendingAction = !!isOffline && !!pendingAction; const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); diff --git a/src/languages/en.ts b/src/languages/en.ts index c1decfdf1c70..d86cc1b4d421 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -842,6 +842,9 @@ export default { sharedNoteMessage: 'Keep notes about this chat here. Expensify employees and other users on the team.expensify.com domain can view these notes.', composerLabel: 'Notes', myNote: 'My note', + error: { + genericFailureMessage: "Private notes couldn't be saved", + }, }, addDebitCardPage: { addADebitCard: 'Add a debit card', diff --git a/src/languages/es.ts b/src/languages/es.ts index 42461e766b29..b86a1093bd3a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -838,6 +838,9 @@ export default { sharedNoteMessage: 'Guarda notas sobre este chat aquí. Los empleados de Expensify y otros usuarios del dominio team.expensify.com pueden ver estas notas.', composerLabel: 'Notas', myNote: 'Mi nota', + error: { + genericFailureMessage: 'Notas privadas no han podido ser guardados', + }, }, addDebitCardPage: { addADebitCard: 'Añadir una tarjeta de débito', diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index b694234ce69a..2828c492b123 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -1,4 +1,4 @@ -import mapKeys from 'lodash/mapKeys'; +import mapValues from 'lodash/mapValues'; import CONST from '@src/CONST'; import {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; import {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; @@ -39,8 +39,8 @@ function getAuthenticateErrorMessage(response: Response): keyof TranslationFlatO * Method used to get an error object with microsecond as the key. * @param error - error key or message to be saved */ -function getMicroSecondOnyxError(error: string): Record { - return {[DateUtils.getMicroseconds()]: error}; +function getMicroSecondOnyxError(error: string, isTranslated = false): Record { + return {[DateUtils.getMicroseconds()]: error && [error, {isTranslated}]}; } /** @@ -51,6 +51,11 @@ function getMicroSecondOnyxErrorObject(error: Record): Record(onyxData: T } const key = Object.keys(errors).sort().reverse()[0]; - - return [errors[key], {isTranslated: true}]; + return getErrorWithTranslationData(errors[key]); } type OnyxDataWithErrorFields = { @@ -79,8 +83,7 @@ function getLatestErrorField(onyxData } const key = Object.keys(errorsForField).sort().reverse()[0]; - - return {[key]: [errorsForField[key], {isTranslated: true}]}; + return {[key]: getErrorWithTranslationData(errorsForField[key])}; } function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { @@ -91,29 +94,26 @@ function getEarliestErrorField(onyxDa } const key = Object.keys(errorsForField).sort()[0]; - - return {[key]: [errorsForField[key], {isTranslated: true}]}; + return {[key]: getErrorWithTranslationData(errorsForField[key])}; } type ErrorsList = Record; /** - * Method used to attach already translated message with isTranslated: true property + * Method used to attach already translated message with isTranslated property * @param errors - An object containing current errors in the form - * @returns Errors in the form of {timestamp: [message, {isTranslated: true}]} + * @returns Errors in the form of {timestamp: [message, {isTranslated}]} */ -function getErrorMessagesWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsList): ErrorsList { +function getErrorsWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsList): ErrorsList { if (!errors || (Array.isArray(errors) && errors.length === 0)) { return {}; } if (typeof errors === 'string' || Array.isArray(errors)) { - const [message, variables] = Array.isArray(errors) ? errors : [errors]; - // eslint-disable-next-line @typescript-eslint/naming-convention - return {'0': [message, {...(variables ?? []), isTranslated: true}]}; + return {'0': getErrorWithTranslationData(errors)}; } - return mapKeys(errors, (message) => [message, {isTranslated: true}]); + return mapValues(errors, getErrorWithTranslationData); } /** @@ -146,6 +146,6 @@ export { getLatestErrorMessage, getLatestErrorField, getEarliestErrorField, - getErrorMessagesWithTranslationData, + getErrorsWithTranslationData, addErrorMessage, }; diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 77c34ebdc576..82ba8dc418d3 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -97,7 +97,7 @@ function translateLocal(phrase: TKey, ...variable return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables); } -type MaybePhraseKey = string | [string, Record & {isTranslated?: true}] | []; +type MaybePhraseKey = string | [string, Record & {isTranslated?: boolean}] | []; /** * Return translated string for given error. diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 06c0316a40b5..ec917a5eac99 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2379,7 +2379,7 @@ const updatePrivateNotes = (reportID: string, accountID: number, note: string) = value: { privateNotes: { [accountID]: { - errors: ErrorUtils.getMicroSecondOnyxError("Private notes couldn't be saved"), + errors: ErrorUtils.getMicroSecondOnyxError('privateNotes.error.genericFailureMessage'), }, }, }, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index ca38e0dd5902..32adbcf59cfa 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -647,7 +647,7 @@ function clearAccountMessages() { } function setAccountError(error: string) { - Onyx.merge(ONYXKEYS.ACCOUNT, {errors: ErrorUtils.getMicroSecondOnyxError(error)}); + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: ErrorUtils.getMicroSecondOnyxError(error, true)}); } // It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to diff --git a/src/pages/settings/Wallet/TransferBalancePage.js b/src/pages/settings/Wallet/TransferBalancePage.js index 499f50616218..86bf5f9c7a8d 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.js +++ b/src/pages/settings/Wallet/TransferBalancePage.js @@ -167,7 +167,7 @@ function TransferBalancePage(props) { const transferAmount = props.userWallet.currentBalance - calculatedFee; const isTransferable = transferAmount > 0; const isButtonDisabled = !isTransferable || !selectedAccount; - const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? ErrorUtils.getErrorMessagesWithTranslationData(_.chain(props.walletTransfer.errors).values().first().value()) : ''; + const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? ErrorUtils.getErrorsWithTranslationData(_.chain(props.walletTransfer.errors).values().first().value()) : ''; const shouldShowTransferView = PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, props.bankAccountList) && diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 8c2acbb17a68..037b52957574 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -288,7 +288,7 @@ function LoginForm(props) { )} { diff --git a/src/pages/signin/UnlinkLoginForm.js b/src/pages/signin/UnlinkLoginForm.js index 962e17786ce7..1d278760f13c 100644 --- a/src/pages/signin/UnlinkLoginForm.js +++ b/src/pages/signin/UnlinkLoginForm.js @@ -68,14 +68,14 @@ function UnlinkLoginForm(props) { )} {!_.isEmpty(props.account.errors) && ( )} diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index cb2b9ff52670..87b1802511af 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -278,7 +278,7 @@ function WorkspaceInvitePage(props) { isAlertVisible={shouldShowAlertPrompt} buttonText={translate('common.next')} onSubmit={inviteUser} - message={ErrorUtils.getErrorMessagesWithTranslationData(props.policy.alertMessage)} + message={ErrorUtils.getErrorsWithTranslationData(props.policy.alertMessage)} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 956e9ff36b24..93f5e9df2350 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -1,4 +1,5 @@ import {ValueOf} from 'type-fest'; +import * as Localize from '@libs/Localize'; import {AvatarSource} from '@libs/UserUtils'; import CONST from '@src/CONST'; @@ -8,7 +9,7 @@ type PendingFields = Record = Record; -type Errors = Record; +type Errors = Record; type AvatarType = typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; From a92b6901b0de2c4f45e05f50593d7757480ddc01 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 2 Jan 2024 17:22:05 +0700 Subject: [PATCH 043/446] fix type --- src/libs/ErrorUtils.ts | 1 + src/libs/ReportUtils.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 2828c492b123..bba042d02ef3 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -110,6 +110,7 @@ function getErrorsWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsLi } if (typeof errors === 'string' || Array.isArray(errors)) { + // eslint-disable-next-line @typescript-eslint/naming-convention return {'0': getErrorWithTranslationData(errors)}; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6a4914f44121..7e1e5c1c0f9c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -164,7 +164,7 @@ type ReportRouteParams = { type ReportOfflinePendingActionAndErrors = { addWorkspaceRoomOrChatPendingAction: PendingAction | undefined; - addWorkspaceRoomOrChatErrors: Record | null | undefined; + addWorkspaceRoomOrChatErrors: Errors | null | undefined; }; type OptimisticApprovedReportAction = Pick< @@ -3864,7 +3864,7 @@ function isValidReportIDFromPath(reportIDFromPath: string): boolean { /** * Return the errors we have when creating a chat or a workspace room */ -function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Record | null | undefined { +function getAddWorkspaceRoomOrChatReportErrors(report: OnyxEntry): Errors | null | undefined { // We are either adding a workspace room, or we're creating a chat, it isn't possible for both of these to have errors for the same report at the same time, so // simply looking up the first truthy value will get the relevant property if it's set. return report?.errorFields?.addWorkspaceRoom ?? report?.errorFields?.createChat; From a8061efe5d6065b68aa9c1c661a4ba50800271e4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 3 Jan 2024 00:51:10 +0700 Subject: [PATCH 044/446] fix test --- tests/actions/IOUTest.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 4d9ce42a08ce..f08bfdd73ce9 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -683,7 +683,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); expect(transaction.errors).toBeTruthy(); - expect(_.values(transaction.errors)[0]).toBe('iou.error.genericCreateFailureMessage'); + expect(_.values(transaction.errors)[0]).toBe(["iou.error.genericCreateFailureMessage", {isTranslated: false}]); resolve(); }, }); @@ -1631,7 +1631,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); const updatedAction = _.find(allActions, (reportAction) => !_.isEmpty(reportAction)); expect(updatedAction.actionName).toEqual('MODIFIEDEXPENSE'); - expect(_.values(updatedAction.errors)).toEqual(expect.arrayContaining(['iou.error.genericEditFailureMessage'])); + expect(_.values(updatedAction.errors)).toEqual(expect.arrayContaining([["iou.error.genericEditFailureMessage", {isTranslated: false}]])); resolve(); }, }); @@ -1846,7 +1846,7 @@ describe('actions/IOU', () => { callback: (allActions) => { Onyx.disconnect(connectionID); const erroredAction = _.find(_.values(allActions), (action) => !_.isEmpty(action.errors)); - expect(_.values(erroredAction.errors)).toEqual(expect.arrayContaining(['iou.error.other'])); + expect(_.values(erroredAction.errors)).toEqual(expect.arrayContaining([["iou.error.other", {isTranslated: false}]])); resolve(); }, }); From bb7c9cab65081a15e4215da4d92e617c323eb4c1 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 3 Jan 2024 00:58:37 +0700 Subject: [PATCH 045/446] fix lint --- tests/actions/IOUTest.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index f08bfdd73ce9..7c7bf520675a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -683,7 +683,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); expect(transaction.errors).toBeTruthy(); - expect(_.values(transaction.errors)[0]).toBe(["iou.error.genericCreateFailureMessage", {isTranslated: false}]); + expect(_.values(transaction.errors)[0]).toBe(['iou.error.genericCreateFailureMessage', {isTranslated: false}]); resolve(); }, }); @@ -1631,7 +1631,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); const updatedAction = _.find(allActions, (reportAction) => !_.isEmpty(reportAction)); expect(updatedAction.actionName).toEqual('MODIFIEDEXPENSE'); - expect(_.values(updatedAction.errors)).toEqual(expect.arrayContaining([["iou.error.genericEditFailureMessage", {isTranslated: false}]])); + expect(_.values(updatedAction.errors)).toEqual(expect.arrayContaining([['iou.error.genericEditFailureMessage', {isTranslated: false}]])); resolve(); }, }); @@ -1846,7 +1846,7 @@ describe('actions/IOU', () => { callback: (allActions) => { Onyx.disconnect(connectionID); const erroredAction = _.find(_.values(allActions), (action) => !_.isEmpty(action.errors)); - expect(_.values(erroredAction.errors)).toEqual(expect.arrayContaining([["iou.error.other", {isTranslated: false}]])); + expect(_.values(erroredAction.errors)).toEqual(expect.arrayContaining([['iou.error.other', {isTranslated: false}]])); resolve(); }, }); From 49a45b2cf26dc05203a066342c0ae6ccde5fa742 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 3 Jan 2024 01:06:08 +0700 Subject: [PATCH 046/446] fix test --- tests/actions/IOUTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 7c7bf520675a..1fecb79c6908 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -683,7 +683,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); expect(transaction.errors).toBeTruthy(); - expect(_.values(transaction.errors)[0]).toBe(['iou.error.genericCreateFailureMessage', {isTranslated: false}]); + expect(_.values(transaction.errors)[0]).toStrictEqual(['iou.error.genericCreateFailureMessage', {isTranslated: false}]); resolve(); }, }); From 68bc74de6ce6964e01ccb4512f064c60adcb9f92 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 5 Jan 2024 00:21:27 +0700 Subject: [PATCH 047/446] update spanish translation message --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index d9785f2d4a55..83d904d4a98e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -843,7 +843,7 @@ export default { composerLabel: 'Notas', myNote: 'Mi nota', error: { - genericFailureMessage: 'Notas privadas no han podido ser guardados', + genericFailureMessage: 'Las notas privadas no han podido ser guardadas', }, }, addDebitCardPage: { From 20cdd2ef71c92969cc1205cec93ec7995a7e7084 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 8 Jan 2024 18:20:48 +0000 Subject: [PATCH 048/446] chore(typescript): migrate moneyrequestheader to typescript --- ...equestHeader.js => MoneyRequestHeader.tsx} | 128 ++++++++---------- src/libs/HeaderUtils.ts | 2 +- 2 files changed, 58 insertions(+), 72 deletions(-) rename src/components/{MoneyRequestHeader.js => MoneyRequestHeader.tsx} (65%) diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.tsx similarity index 65% rename from src/components/MoneyRequestHeader.js rename to src/components/MoneyRequestHeader.tsx index 488630dd0590..73b4148279ba 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.tsx @@ -1,89 +1,78 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; import * as HeaderUtils from '@libs/HeaderUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import iouReportPropTypes from '@pages/iouReportPropTypes'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {PersonalDetails, Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; +import {IOUMessage, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; -import participantPropTypes from './participantPropTypes'; -import transactionPropTypes from './transactionPropTypes'; -const propTypes = { - /** The report currently being looked at */ - report: iouReportPropTypes.isRequired, - - /** The policy which the report is tied to */ - policy: PropTypes.shape({ - /** Name of the policy */ - name: PropTypes.string, - }), - - /** Personal details so we can get the ones for the report participants */ - personalDetails: PropTypes.objectOf(participantPropTypes).isRequired, - - /* Onyx Props */ +type MoneyRequestHeaderOnyxProps = { /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user email */ - email: PropTypes.string, - }), + session: OnyxEntry; /** The expense report or iou report (only will have a value if this is a transaction thread) */ - parentReport: iouReportPropTypes, - - /** The report action the transaction is tied to from the parent report */ - parentReportAction: PropTypes.shape(reportActionPropTypes), + parentReport: OnyxEntry; /** All the data for the transaction */ - transaction: transactionPropTypes, + transaction: OnyxEntry; + + /** All report actions */ + // eslint-disable-next-line react/no-unused-prop-types + parentReportActions: OnyxEntry; }; -const defaultProps = { - session: { - email: null, - }, - parentReport: {}, - parentReportAction: {}, - transaction: {}, - policy: {}, +type MoneyRequestHeaderProps = MoneyRequestHeaderOnyxProps & { + /** The report currently being looked at */ + report: Report; + + /** The policy which the report is tied to */ + policy: Policy; + + /** The report action the transaction is tied to from the parent report */ + parentReportAction: ReportAction & OriginalMessageIOU; + + /** Personal details so we can get the ones for the report participants */ + personalDetails: OnyxCollection; }; -function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy, personalDetails}) { +function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy, personalDetails}: MoneyRequestHeaderProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const moneyRequestReport = parentReport; - const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); + const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID); const isApproved = ReportUtils.isReportApproved(moneyRequestReport); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); // Only the requestor can take delete the request, admins can only edit it. - const isActionOwner = lodashGet(parentReportAction, 'actorAccountID') === lodashGet(session, 'accountID', null); + const isActionOwner = parentReportAction.actorAccountID === (session?.accountID ?? null); const deleteTransaction = useCallback(() => { - IOU.deleteMoneyRequest(lodashGet(parentReportAction, 'originalMessage.IOUTransactionID'), parentReportAction, true); - setIsDeleteModalVisible(false); + const { + originalMessage: {IOUTransactionID}, + } = parentReportAction; + if (IOUTransactionID) { + IOU.deleteMoneyRequest(IOUTransactionID, parentReportAction, true); + setIsDeleteModalVisible(false); + } }, [parentReportAction, setIsDeleteModalVisible]); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); - const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction); + const isPending = !!transaction && TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction); const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); @@ -94,7 +83,8 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, setIsDeleteModalVisible(false); }, [canModifyRequest]); - const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)]; + const menuItem = HeaderUtils.getPinMenuItem(report); + const threeDotsMenuItems = menuItem ? [menuItem] : []; if (canModifyRequest) { if (!TransactionUtils.hasReceipt(transaction)) { threeDotsMenuItems.push({ @@ -122,7 +112,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} report={{ ...report, - ownerAccountID: lodashGet(parentReport, 'ownerAccountID', null), + ownerAccountID: parentReport?.ownerAccountID, }} policy={policy} personalDetails={personalDetails} @@ -159,29 +149,25 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, } MoneyRequestHeader.displayName = 'MoneyRequestHeader'; -MoneyRequestHeader.propTypes = propTypes; -MoneyRequestHeader.defaultProps = defaultProps; -export default compose( - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - parentReport: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, - canEvict: false, +const MoneyRequestHeaderWithTransaction = withOnyx>({ + transaction: { + key: ({report, parentReportActions}) => { + const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : ({} as ReportAction); + return `${ONYXKEYS.COLLECTION.TRANSACTION}${(parentReportAction.originalMessage as IOUMessage).IOUTransactionID ?? 0}`; }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({report, parentReportActions}) => { - const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]); - return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - }), -)(MoneyRequestHeader); + }, +})(MoneyRequestHeader); + +export default withOnyx, Omit>({ + session: { + key: ONYXKEYS.SESSION, + }, + parentReport: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`, + }, + parentReportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? '0'}`, + canEvict: false, + }, +})(MoneyRequestHeaderWithTransaction); diff --git a/src/libs/HeaderUtils.ts b/src/libs/HeaderUtils.ts index ebf1b1139621..38cd57156af7 100644 --- a/src/libs/HeaderUtils.ts +++ b/src/libs/HeaderUtils.ts @@ -7,7 +7,7 @@ import * as Session from './actions/Session'; import * as Localize from './Localize'; type MenuItem = { - icon: string | IconAsset; + icon: IconAsset; text: string; onSelected: () => void; }; From c37ab71e374b7f853e8ea22bad21fe329cd6fece Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 8 Jan 2024 18:55:27 +0000 Subject: [PATCH 049/446] refactor(typescript): change import to import type for types --- src/components/MoneyRequestHeader.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 73b4148279ba..878f0d3996f2 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; -import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -13,8 +14,8 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {PersonalDetails, Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; -import {IOUMessage, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; +import type {PersonalDetails, Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; +import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; @@ -153,8 +154,8 @@ MoneyRequestHeader.displayName = 'MoneyRequestHeader'; const MoneyRequestHeaderWithTransaction = withOnyx>({ transaction: { key: ({report, parentReportActions}) => { - const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : ({} as ReportAction); - return `${ONYXKEYS.COLLECTION.TRANSACTION}${(parentReportAction.originalMessage as IOUMessage).IOUTransactionID ?? 0}`; + const parentReportAction = (report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : {}) as ReportAction & OriginalMessageIOU; + return `${ONYXKEYS.COLLECTION.TRANSACTION}${parentReportAction.originalMessage.IOUTransactionID ?? 0}`; }, }, })(MoneyRequestHeader); From cc24a973e993f141dba37895b8f745b18ebe218e Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 8 Jan 2024 14:20:41 -0700 Subject: [PATCH 050/446] Add the data to be loaded from onyx --- .../report/withReportAndReportActionOrNotFound.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index fb0a00e2d10d..5d4590ac4746 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -27,6 +27,9 @@ type OnyxProps = { /** Array of report actions for this report */ reportActions: OnyxEntry; + /** The report's parentReportAction */ + parentReportAction: OnyxEntry; + /** The policies which the user has access to */ policies: OnyxCollection; @@ -114,6 +117,17 @@ export default function (WrappedComponent: key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${route.params.reportID}`, canEvict: false, }, + parentReportAction: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, + selector: (parentReportActions, props) => { + const parentReportActionID = lodashGet(props, 'report.parentReportActionID'); + if (!parentReportActionID) { + return {}; + } + return parentReportActions[parentReportActionID]; + }, + canEvict: false, + }, }), withWindowDimensions, )(React.forwardRef(WithReportOrNotFound)); From d318c952d017a1b59d1672bce48fabd3fd483d84 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 8 Jan 2024 14:25:49 -0700 Subject: [PATCH 051/446] Remove the use of lodash. --- src/pages/home/report/withReportAndReportActionOrNotFound.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index 5d4590ac4746..adc9663a4f34 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -120,7 +120,7 @@ export default function (WrappedComponent: parentReportAction: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, selector: (parentReportActions, props) => { - const parentReportActionID = lodashGet(props, 'report.parentReportActionID'); + const parentReportActionID = props?.report?.parentReportActionID; if (!parentReportActionID) { return {}; } From b2b8868c71539cacd95d4f3275bf92cdc3937723 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 8 Jan 2024 14:38:53 -0700 Subject: [PATCH 052/446] Remove deprecated method --- .../home/report/withReportAndReportActionOrNotFound.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index adc9663a4f34..83c62bca5e4a 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -9,7 +9,6 @@ import withWindowDimensions from '@components/withWindowDimensions'; import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import compose from '@libs/compose'; import getComponentDisplayName from '@libs/getComponentDisplayName'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Report from '@userActions/Report'; @@ -52,11 +51,11 @@ export default function (WrappedComponent: // Handle threads if needed if (!reportAction?.reportActionID) { - reportAction = ReportActionsUtils.getParentReportAction(props.report); + reportAction = props?.parentReportAction ?? {}; } return reportAction; - }, [props.report, props.reportActions, props.route.params.reportActionID]); + }, [props.reportActions, props.route.params.reportActionID, props.parentReportAction]); const reportAction = getReportAction(); From cb61222097aebd1a4719e1d177a038d392f967c5 Mon Sep 17 00:00:00 2001 From: dhairyasenjaliya Date: Tue, 9 Jan 2024 21:53:10 +0530 Subject: [PATCH 053/446] limit on legal name --- .../settings/Profile/PersonalDetails/LegalNamePage.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js index 365ea62184ab..c96bc41ec0e1 100644 --- a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js +++ b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js @@ -60,8 +60,8 @@ function LegalNamePage(props) { } else if (_.isEmpty(values.legalFirstName)) { errors.legalFirstName = 'common.error.fieldRequired'; } - if (values.legalFirstName.length > CONST.LEGAL_NAME.MAX_LENGTH) { - ErrorUtils.addErrorMessage(errors, 'legalFirstName', ['common.error.characterLimitExceedCounter', {length: values.legalFirstName.length, limit: CONST.LEGAL_NAME.MAX_LENGTH}]); + if (values.legalFirstName.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'legalFirstName', ['common.error.characterLimitExceedCounter', {length: values.legalFirstName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } if (!ValidationUtils.isValidLegalName(values.legalLastName)) { @@ -69,8 +69,8 @@ function LegalNamePage(props) { } else if (_.isEmpty(values.legalLastName)) { errors.legalLastName = 'common.error.fieldRequired'; } - if (values.legalLastName.length > CONST.LEGAL_NAME.MAX_LENGTH) { - ErrorUtils.addErrorMessage(errors, 'legalLastName', ['common.error.characterLimitExceedCounter', {length: values.legalLastName.length, limit: CONST.LEGAL_NAME.MAX_LENGTH}]); + if (values.legalLastName.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'legalLastName', ['common.error.characterLimitExceedCounter', {length: values.legalLastName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } return errors; @@ -106,7 +106,6 @@ function LegalNamePage(props) { aria-label={props.translate('privatePersonalDetails.legalFirstName')} role={CONST.ROLE.PRESENTATION} defaultValue={legalFirstName} - maxLength={CONST.LEGAL_NAME.MAX_LENGTH + CONST.SEARCH_MAX_LENGTH} spellCheck={false} /> @@ -119,7 +118,6 @@ function LegalNamePage(props) { aria-label={props.translate('privatePersonalDetails.legalLastName')} role={CONST.ROLE.PRESENTATION} defaultValue={legalLastName} - maxLength={CONST.LEGAL_NAME.MAX_LENGTH + CONST.SEARCH_MAX_LENGTH} spellCheck={false} /> From f81c7a1fc924d16ea6043944b3240d2fe8da9ec1 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 9 Jan 2024 23:37:19 +0700 Subject: [PATCH 054/446] fix lint --- src/components/MenuItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 14b721fa3d4f..afb5f0cfa173 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -135,8 +135,8 @@ type MenuItemProps = (IconProps | AvatarProps | NoIcon) & { /** Error to display below the title */ error?: string; - /** Error to display at the bottom of the component */ - errorText?: MaybePhraseKey; + /** Error to display at the bottom of the component */ + errorText?: MaybePhraseKey; /** A boolean flag that gives the icon a green fill if true */ success?: boolean; From a551927b2bf0b97860a5a4efa8a73f9659c6ea24 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Tue, 9 Jan 2024 17:27:41 -0800 Subject: [PATCH 055/446] Add optimistic violations to money request edits Update everywhere that accesses an arg on policy to use {} as default arg for safety, since we can't use optional chaining --- src/libs/actions/IOU.js | 88 +++++++++++++++++++++++++++++------- src/pages/EditRequestPage.js | 30 +++++++----- 2 files changed, 90 insertions(+), 28 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f2584cb8accd..31fee6d2b9d7 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -341,7 +341,7 @@ function buildOnyxDataForMoneyRequest( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, - policy, + policy = {}, policyTags, policyCategories, hasOutstandingChildRequest = false, @@ -918,13 +918,16 @@ function createDistanceRequest(report, participant, comment, created, category, * @param {String} transactionThreadReportID * @param {Object} transactionChanges * @param {String} [transactionChanges.created] Present when updated the date field + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories * @param {Boolean} onlyIncludeChangedFields * When 'true', then the returned params will only include the transaction details for the fields that were changed. * When `false`, then the returned params will include all the transaction details, regardless of which fields were changed. * This setting is necessary while the UpdateDistanceRequest API is refactored to be fully 1:1:1 in https://github.com/Expensify/App/issues/28358 * @returns {object} */ -function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, onlyIncludeChangedFields) { +function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy = {}, policyTags, policyCategories, onlyIncludeChangedFields) { const optimisticData = []; const successData = []; const failureData = []; @@ -1050,6 +1053,13 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t } } + // Add optimistic transaction violations + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories), + }); + // Clear out the error fields and loading states on success successData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -1088,6 +1098,13 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t value: iouReport, }); + // Reset transaction violations to their original state + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`], + }); + return { params, onyxData: {optimisticData, successData, failureData}, @@ -1100,12 +1117,15 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t * @param {String} transactionID * @param {String} transactionThreadReportID * @param {String} val + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { +function updateMoneyRequestDate(transactionID, transactionThreadReportID, val, policy, policyTags, policyCategories) { const transactionChanges = { created: val, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestDate', params, onyxData); } @@ -1115,12 +1135,15 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {String} val + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, val) { +function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, val, policy, policyTags, policyCategories) { const transactionChanges = { merchant: val, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestMerchant', params, onyxData); } @@ -1130,12 +1153,15 @@ function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, va * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {String} tag + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag) { +function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag, policy, policyTags, policyCategories) { const transactionChanges = { tag, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestTag', params, onyxData); } @@ -1149,10 +1175,12 @@ function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag) { * @param {Number} [transactionChanges.amount] * @param {Object} [transactionChanges.comment] * @param {Object} [transactionChanges.waypoints] - * + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateDistanceRequest(transactionID, transactionThreadReportID, transactionChanges) { - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, false); +function updateDistanceRequest(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories) { + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, false); API.write('UpdateDistanceRequest', params, onyxData); } @@ -2170,8 +2198,11 @@ function setDraftSplitTransaction(transactionID, transactionChanges = {}) { * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {Object} transactionChanges + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function editRegularMoneyRequest(transactionID, transactionThreadReportID, transactionChanges) { +function editRegularMoneyRequest(transactionID, transactionThreadReportID, transactionChanges, policy = {}, policyTags, policyCategories) { // STEP 1: Get all collections we're updating const transactionThread = allReports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`]; const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; @@ -2225,6 +2256,13 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // STEP 4: Compose the optimistic data const currentTime = DateUtils.getDBTime(); + const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories); + // TODO + const previousViolationsOnyxData = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`], + }; const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -2256,6 +2294,11 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans lastVisibleActionCreated: currentTime, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: updatedViolationsOnyxData, + }, ...(!isScanning ? [ { @@ -2379,6 +2422,11 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans lastVisibleActionCreated: transactionThread.lastVisibleActionCreated, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: previousViolationsOnyxData, + }, ]; // STEP 6: Call the API endpoint @@ -2405,12 +2453,15 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans * @param {object} transaction * @param {String} transactionThreadReportID * @param {Object} transactionChanges + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function editMoneyRequest(transaction, transactionThreadReportID, transactionChanges) { +function editMoneyRequest(transaction, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories) { if (TransactionUtils.isDistanceRequest(transaction)) { - updateDistanceRequest(transaction.transactionID, transactionThreadReportID, transactionChanges); + updateDistanceRequest(transaction.transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories); } else { - editRegularMoneyRequest(transaction.transactionID, transactionThreadReportID, transactionChanges); + editRegularMoneyRequest(transaction.transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories); } } @@ -2421,13 +2472,16 @@ function editMoneyRequest(transaction, transactionThreadReportID, transactionCha * @param {String} transactionThreadReportID * @param {String} currency * @param {Number} amount + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestAmountAndCurrency(transactionID, transactionThreadReportID, currency, amount) { +function updateMoneyRequestAmountAndCurrency(transactionID, transactionThreadReportID, currency, amount, policy, policyTags, policyCategories) { const transactionChanges = { amount, currency, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestAmountAndCurrency', params, onyxData); } diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index b322f4eb106c..57fe1e7957a7 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -29,6 +29,7 @@ import EditRequestReceiptPage from './EditRequestReceiptPage'; import EditRequestTagPage from './EditRequestTagPage'; import reportActionPropTypes from './home/report/reportActionPropTypes'; import reportPropTypes from './reportPropTypes'; +import {policyPropTypes} from './workspace/withPolicy'; const propTypes = { /** Route from navigation */ @@ -47,6 +48,9 @@ const propTypes = { /** The report object for the thread report */ report: reportPropTypes, + /** The policy of the report */ + policy: policyPropTypes.policy, + /** Collection of categories attached to a policy */ policyCategories: PropTypes.objectOf(categoryPropTypes), @@ -62,13 +66,14 @@ const propTypes = { const defaultProps = { report: {}, + policy: {}, policyCategories: {}, policyTags: {}, parentReportActions: {}, transaction: {}, }; -function EditRequestPage({report, route, policyCategories, policyTags, parentReportActions, transaction}) { +function EditRequestPage({report, route, policy, policyCategories, policyTags, parentReportActions, transaction}) { const parentReportActionID = lodashGet(report, 'parentReportActionID', '0'); const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {}); const { @@ -112,7 +117,7 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep // Update the transaction object and close the modal function editMoneyRequest(transactionChanges) { - IOU.editMoneyRequest(transaction, report.reportID, transactionChanges); + IOU.editMoneyRequest(transaction, report.reportID, transactionChanges, policy, policyTags, policyCategories); Navigation.dismissModal(report.reportID); } @@ -126,10 +131,10 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep return; } - IOU.updateMoneyRequestAmountAndCurrency(transaction.transactionID, report.reportID, newCurrency, newAmount); + IOU.updateMoneyRequestAmountAndCurrency(transaction.transactionID, report.reportID, newCurrency, newAmount, policy, policyTags, policyCategories); Navigation.dismissModal(); }, - [transaction, report], + [transaction, report, policy, policyTags, policyCategories], ); const saveCreated = useCallback( @@ -139,10 +144,10 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep Navigation.dismissModal(); return; } - IOU.updateMoneyRequestDate(transaction.transactionID, report.reportID, newCreated); + IOU.updateMoneyRequestDate(transaction.transactionID, report.reportID, newCreated, policy, policyTags, policyCategories); Navigation.dismissModal(); }, - [transaction, report], + [transaction, report, policy, policyTags, policyCategories], ); const saveMerchant = useCallback( @@ -158,14 +163,14 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep // This is possible only in case of IOU requests. if (newTrimmedMerchant === '') { - IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); + IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, policy, policyTags, policyCategories); return; } - IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, newMerchant); + IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, newMerchant, policy, policyTags, policyCategories); Navigation.dismissModal(); }, - [transactionMerchant, transaction, report], + [transactionMerchant, transaction, report, policy, policyTags, policyCategories], ); const saveTag = useCallback( @@ -175,10 +180,10 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep // In case the same tag has been selected, reset the tag. updatedTag = ''; } - IOU.updateMoneyRequestTag(transaction.transactionID, report.reportID, updatedTag); + IOU.updateMoneyRequestTag(transaction.transactionID, report.reportID, updatedTag, policy, policyTags, policyCategories); Navigation.dismissModal(); }, - [transactionTag, transaction.transactionID, report.reportID], + [transactionTag, transaction.transactionID, report.reportID, policy, policyTags, policyCategories], ); if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DESCRIPTION) { @@ -300,6 +305,9 @@ export default compose( }), // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, policyCategories: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, }, From 56dd74d8898a29fc50a922d16f601ca6733d7662 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Tue, 9 Jan 2024 22:51:29 -0800 Subject: [PATCH 056/446] Lint fix --- src/pages/EditRequestPage.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 8e867c150b90..7e0577fa86b9 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -162,7 +162,14 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p } // An empty newTrimmedMerchant is only possible for the P2P IOU case - IOU.updateMoneyRequestMerchant(transaction.transactionID, report.reportID, newTrimmedMerchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, policy, policyTags, policyCategories); + IOU.updateMoneyRequestMerchant( + transaction.transactionID, + report.reportID, + newTrimmedMerchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + policy, + policyTags, + policyCategories, + ); Navigation.dismissModal(); }, [transactionMerchant, transaction, report, policy, policyTags, policyCategories], From 079b15d7c89055cbbe548553a5416ee699a1f6ea Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 27 Dec 2023 12:38:40 +0100 Subject: [PATCH 057/446] [TS migration] Migrate 'SettingsWalletPhysicalCard' page to TypeScript --- src/ONYXKEYS.ts | 6 +- src/libs/CardUtils.ts | 7 +- src/libs/GetPhysicalCardUtils.ts | 74 +++------ src/libs/Navigation/types.ts | 24 ++- src/libs/UserUtils.ts | 4 +- ...hysicalCard.js => BaseGetPhysicalCard.tsx} | 145 +++++++----------- ...dAddress.js => GetPhysicalCardAddress.tsx} | 57 ++----- ...dConfirm.js => GetPhysicalCardConfirm.tsx} | 74 +++------ ...calCardName.js => GetPhysicalCardName.tsx} | 66 ++++---- ...lCardPhone.js => GetPhysicalCardPhone.tsx} | 55 +++---- src/types/onyx/Card.ts | 4 +- src/types/onyx/Form.ts | 23 ++- src/types/onyx/PrivatePersonalDetails.ts | 1 + src/types/onyx/index.ts | 22 +-- 14 files changed, 234 insertions(+), 328 deletions(-) rename src/pages/settings/Wallet/Card/{BaseGetPhysicalCard.js => BaseGetPhysicalCard.tsx} (59%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardAddress.js => GetPhysicalCardAddress.tsx} (59%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardConfirm.js => GetPhysicalCardConfirm.tsx} (61%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardName.js => GetPhysicalCardName.tsx} (61%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardPhone.js => GetPhysicalCardPhone.tsx} (60%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 98e3856f4544..bb0fdc16188b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -405,7 +405,7 @@ type OnyxValues = { [ONYXKEYS.WALLET_TERMS]: OnyxTypes.WalletTerms; [ONYXKEYS.BANK_ACCOUNT_LIST]: OnyxTypes.BankAccountList; [ONYXKEYS.FUND_LIST]: OnyxTypes.FundList; - [ONYXKEYS.CARD_LIST]: Record; + [ONYXKEYS.CARD_LIST]: OnyxTypes.CardList; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; @@ -525,8 +525,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form | undefined; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.GetPhysicalCardForm; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.GetPhysicalCardForm | undefined; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index d71ad9c2629a..f66ddbab2c7f 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1,5 +1,6 @@ import lodash from 'lodash'; import Onyx from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx/lib/types'; import CONST from '@src/CONST'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -72,11 +73,11 @@ function getYearFromExpirationDateString(expirationDateString: string) { * @param cardList - collection of assigned cards * @returns collection of assigned cards grouped by domain */ -function getDomainCards(cardList: Record) { +function getDomainCards(cardList: OnyxCollection) { // Check for domainName to filter out personal credit cards. // eslint-disable-next-line you-dont-need-lodash-underscore/filter - const activeCards = lodash.filter(cardList, (card) => !!card.domainName && (CONST.EXPENSIFY_CARD.ACTIVE_STATES as ReadonlyArray).includes(card.state)); - return lodash.groupBy(activeCards, (card) => card.domainName); + const activeCards = lodash.filter(cardList, (card) => !!card?.domainName && (CONST.EXPENSIFY_CARD.ACTIVE_STATES as ReadonlyArray).includes(card.state)); + return lodash.groupBy(activeCards, (card) => card?.domainName); } /** diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index eebefd7c1d52..4e6775fa10b3 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -1,30 +1,12 @@ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +import type {OnyxEntry} from 'react-native-onyx'; import ROUTES from '@src/ROUTES'; -import type {Login} from '@src/types/onyx'; +import type {Route} from '@src/ROUTES'; +import type {GetPhysicalCardForm, LoginList, PrivatePersonalDetails} from '@src/types/onyx'; import Navigation from './Navigation/Navigation'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as UserUtils from './UserUtils'; -type DraftValues = { - addressLine1: string; - addressLine2: string; - city: string; - country: string; - legalFirstName: string; - legalLastName: string; - phoneNumber: string; - state: string; - zipPostCode: string; -}; - -type PrivatePersonalDetails = { - address: {street: string; city: string; state: string; country: string; zip: string}; - legalFirstName: string; - legalLastName: string; - phoneNumber: string; -}; - -type LoginList = Record; - /** * * @param domain @@ -32,13 +14,8 @@ type LoginList = Record; * @param loginList * @returns */ -function getCurrentRoute(domain: string, privatePersonalDetails: PrivatePersonalDetails, loginList: LoginList) { - const { - address: {street, city, state, country, zip}, - legalFirstName, - legalLastName, - phoneNumber, - } = privatePersonalDetails; +function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry): Route { + const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; if (!legalFirstName && !legalLastName) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain); @@ -46,7 +23,7 @@ function getCurrentRoute(domain: string, privatePersonalDetails: PrivatePersonal if (!phoneNumber && !UserUtils.getSecondaryPhoneLogin(loginList)) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain); } - if (!(street && city && state && country && zip)) { + if (!(address?.street && address?.city && address?.state && address?.country && address?.zip)) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS.getRoute(domain); } @@ -60,7 +37,7 @@ function getCurrentRoute(domain: string, privatePersonalDetails: PrivatePersonal * @param loginList * @returns */ -function goToNextPhysicalCardRoute(domain: string, privatePersonalDetails: PrivatePersonalDetails, loginList: LoginList) { +function goToNextPhysicalCardRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry) { Navigation.navigate(getCurrentRoute(domain, privatePersonalDetails, loginList)); } @@ -72,7 +49,7 @@ function goToNextPhysicalCardRoute(domain: string, privatePersonalDetails: Priva * @param loginList * @returns */ -function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDetails: PrivatePersonalDetails, loginList: LoginList) { +function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry) { const expectedRoute = getCurrentRoute(domain, privatePersonalDetails, loginList); // If the user is on the current route or the current route is confirmation, then he's allowed to stay on the current step @@ -90,24 +67,19 @@ function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDe * @param privatePersonalDetails * @returns */ -function getUpdatedDraftValues(draftValues: DraftValues, privatePersonalDetails: PrivatePersonalDetails, loginList: LoginList) { - const { - address: {city, country, state, street = '', zip}, - legalFirstName, - legalLastName, - phoneNumber, - } = privatePersonalDetails; +function getUpdatedDraftValues(draftValues: OnyxEntry, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry): GetPhysicalCardForm { + const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; return { - legalFirstName: draftValues.legalFirstName || legalFirstName, - legalLastName: draftValues.legalLastName || legalLastName, - addressLine1: draftValues.addressLine1 || street.split('\n')[0], - addressLine2: draftValues.addressLine2 || street.split('\n')[1] || '', - city: draftValues.city || city, - country: draftValues.country || country, - phoneNumber: draftValues.phoneNumber || (phoneNumber ?? UserUtils.getSecondaryPhoneLogin(loginList) ?? ''), - state: draftValues.state || state, - zipPostCode: draftValues.zipPostCode || zip, + legalFirstName: draftValues?.legalFirstName || legalFirstName, + legalLastName: draftValues?.legalLastName || legalLastName, + addressLine1: draftValues?.addressLine1 || address?.street.split('\n')[0], + addressLine2: draftValues?.addressLine2 || address?.street.split('\n')[1] || '', + city: draftValues?.city || address?.city, + country: draftValues?.country || address?.country, + phoneNumber: draftValues?.phoneNumber || phoneNumber || UserUtils.getSecondaryPhoneLogin(loginList) || '', + state: draftValues?.state || address?.state, + zipPostCode: draftValues?.zipPostCode || address?.zip || '', }; } @@ -116,13 +88,13 @@ function getUpdatedDraftValues(draftValues: DraftValues, privatePersonalDetails: * @param draftValues * @returns */ -function getUpdatedPrivatePersonalDetails(draftValues: DraftValues) { - const {addressLine1, addressLine2, city, country, legalFirstName, legalLastName, phoneNumber, state, zipPostCode} = draftValues; +function getUpdatedPrivatePersonalDetails(draftValues: OnyxEntry): PrivatePersonalDetails { + const {addressLine1, addressLine2, city, country, legalFirstName, legalLastName, phoneNumber, state, zipPostCode} = draftValues ?? {}; return { legalFirstName, legalLastName, phoneNumber, - address: {street: PersonalDetailsUtils.getFormattedStreet(addressLine1, addressLine2), city, country, state, zip: zipPostCode}, + address: {street: PersonalDetailsUtils.getFormattedStreet(addressLine1, addressLine2), city: city || '', country: country || '', state: state || '', zip: zipPostCode || ''}, }; } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 90f5361f11f4..a384fed9fff9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -72,10 +72,26 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: undefined; [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: undefined; [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: undefined; - [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME]: undefined; - [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE]: undefined; - [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS]: undefined; - [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM]: undefined; + [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME]: { + /** domain passed via route /settings/wallet/card/:domain */ + domain: string; + }; + [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE]: { + /** domain passed via route /settings/wallet/card/:domain */ + domain: string; + }; + [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS]: { + /** Currently selected country */ + country: string; + /** domain passed via route /settings/wallet/card/:domain */ + domain: string; + }; + [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM]: { + /** Currently selected country */ + country: string; + /** domain passed via route /settings/wallet/card/:domain */ + domain: string; + }; [SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: undefined; [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: undefined; [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: undefined; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 653acfa36216..fc0c0c655324 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -218,8 +218,8 @@ function getSmallSizeAvatar(avatarSource: AvatarSource, accountID?: number): Ava /** * Gets the secondary phone login number */ -function getSecondaryPhoneLogin(loginList: Record): string | undefined { - const parsedLoginList = Object.keys(loginList).map((login) => Str.removeSMSDomain(login)); +function getSecondaryPhoneLogin(loginList: OnyxEntry): string | undefined { + const parsedLoginList = Object.keys(loginList ?? {}).map((login) => Str.removeSMSDomain(login)); return parsedLoginList.find((login) => Str.isValidPhone(login)); } diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx similarity index 59% rename from src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js rename to src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index cd1f4591a61a..ec5296434505 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -1,8 +1,7 @@ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef} from 'react'; import {Text} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import FormProvider from '@components/Form/FormProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -10,123 +9,83 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as FormActions from '@libs/actions/FormActions'; import * as Wallet from '@libs/actions/Wallet'; import * as CardUtils from '@libs/CardUtils'; -import FormUtils from '@libs/FormUtils'; import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import assignedCardPropTypes from '@pages/settings/Wallet/assignedCardPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {CardList, GetPhysicalCardForm, LoginList, PrivatePersonalDetails, Session} from '@src/types/onyx'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; -const propTypes = { - /* Onyx Props */ +type OnValidate = (values: OnyxEntry) => void; + +type RenderContentProps = ChildrenProps & { + onSubmit: () => void; + submitButtonText: string; + onValidate: OnValidate; +}; + +type BaseGetPhysicalCardOnyxProps = { /** List of available assigned cards */ - cardList: PropTypes.objectOf(assignedCardPropTypes), + cardList: OnyxEntry; /** User's private personal details */ - privatePersonalDetails: PropTypes.shape({ - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - phoneNumber: PropTypes.string, - /** User's home address */ - address: PropTypes.shape({ - street: PropTypes.string, - city: PropTypes.string, - state: PropTypes.string, - zip: PropTypes.string, - country: PropTypes.string, - }), - }), + privatePersonalDetails: OnyxEntry; /** Draft values used by the get physical card form */ - draftValues: PropTypes.shape({ - addressLine1: PropTypes.string, - addressLine2: PropTypes.string, - city: PropTypes.string, - country: PropTypes.string, - phoneNumber: PropTypes.string, - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - state: PropTypes.string, - zipPostCode: PropTypes.string, - }), + draftValues: OnyxEntry; /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - }), + session: OnyxEntry; /** List of available login methods */ - loginList: PropTypes.shape({ - /** The partner creating the account. It depends on the source: website, mobile, integrations, ... */ - partnerName: PropTypes.string, - - /** Phone/Email associated with user */ - partnerUserID: PropTypes.string, - - /** The date when the login was validated, used to show the brickroad status */ - validatedDate: PropTypes.string, - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - - /** Field-specific pending states for offline UI status */ - pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - }), + loginList: OnyxEntry; +}; - /* Base Props */ +type BaseGetPhysicalCardProps = ChildrenProps & BaseGetPhysicalCardOnyxProps & { /** Text displayed below page title */ - headline: PropTypes.string.isRequired, - - /** Children components that will be rendered by renderContent */ - children: PropTypes.node, + headline: string; /** Current route from ROUTES */ - currentRoute: PropTypes.string.isRequired, + currentRoute: string; /** Expensify card domain */ - domain: PropTypes.string, + domain: string; /** Whether or not the current step of the get physical card flow is the confirmation page */ - isConfirmation: PropTypes.bool, + isConfirmation?: boolean; /** Render prop, used to render form content */ - renderContent: PropTypes.func, + renderContent?: (args: RenderContentProps) => React.ReactNode; /** Text displayed on bottom submit button */ - submitButtonText: PropTypes.string.isRequired, + submitButtonText: string; /** Title displayed on top of the page */ - title: PropTypes.string.isRequired, + title: string; /** Callback executed when validating get physical card form data */ - onValidate: PropTypes.func, + onValidate?: OnValidate; }; -const defaultProps = { - cardList: {}, - children: null, - domain: '', - draftValues: null, - privatePersonalDetails: null, - session: {}, - loginList: {}, - isConfirmation: false, - renderContent: (onSubmit, submitButtonText, styles, children = () => {}, onValidate = () => ({})) => ( + +function DefaultRenderContent({onSubmit, submitButtonText, children, onValidate}: RenderContentProps) { + const styles = useThemeStyles(); + + return ( + // @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript. {children} - ), - onValidate: () => ({}), -}; + ) +} function BaseGetPhysicalCard({ cardList, @@ -136,14 +95,14 @@ function BaseGetPhysicalCard({ draftValues, privatePersonalDetails, headline, - isConfirmation, + isConfirmation = false, loginList, - renderContent, - session: {authToken}, + renderContent = DefaultRenderContent, + session, submitButtonText, title, - onValidate, -}) { + onValidate = () => ({}), +}: BaseGetPhysicalCardProps) { const styles = useThemeStyles(); const isRouteSet = useRef(false); @@ -153,7 +112,7 @@ function BaseGetPhysicalCard({ } const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; - const physicalCard = _.find(domainCards, (card) => !card.isVirtual); + const physicalCard = domainCards.find((card) => !card?.isVirtual); // When there are no cards for the specified domain, user is redirected to the wallet page if (domainCards.length === 0) { @@ -169,7 +128,7 @@ function BaseGetPhysicalCard({ } if (!draftValues) { - const updatedDraftValues = GetPhysicalCardUtils.getUpdatedDraftValues({}, privatePersonalDetails, loginList); + const updatedDraftValues = GetPhysicalCardUtils.getUpdatedDraftValues(null, privatePersonalDetails, loginList); // Form draft data needs to be initialized with the private personal details // If no draft data exists FormActions.setDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM, updatedDraftValues); @@ -187,9 +146,9 @@ function BaseGetPhysicalCard({ // If the current step of the get physical card flow is the confirmation page if (isConfirmation) { const domainCards = CardUtils.getDomainCards(cardList)[domain]; - const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; - const cardID = virtualCard.cardID; - Wallet.requestPhysicalExpensifyCard(cardID, authToken, updatedPrivatePersonalDetails); + const virtualCard = domainCards.find((card) => card?.isVirtual); + const cardID = virtualCard?.cardID ?? ''; + Wallet.requestPhysicalExpensifyCard(cardID, session?.authToken, updatedPrivatePersonalDetails); // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); @@ -197,7 +156,7 @@ function BaseGetPhysicalCard({ return; } GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails, loginList); - }, [authToken, cardList, domain, draftValues, isConfirmation, loginList]); + }, [cardList, domain, draftValues, isConfirmation, loginList, session?.authToken]); return ( Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))} /> {headline} - {renderContent(onSubmit, submitButtonText, styles, children, onValidate)} + {renderContent({onSubmit, submitButtonText, children, onValidate})} ); } -BaseGetPhysicalCard.defaultProps = defaultProps; BaseGetPhysicalCard.displayName = 'BaseGetPhysicalCard'; -BaseGetPhysicalCard.propTypes = propTypes; -export default withOnyx({ +export default withOnyx({ cardList: { key: ONYXKEYS.CARD_LIST, }, @@ -232,6 +189,8 @@ export default withOnyx({ key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, }, draftValues: { - key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM), + key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, }, })(BaseGetPhysicalCard); + +export type {RenderContentProps}; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.js b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx similarity index 59% rename from src/pages/settings/Wallet/Card/GetPhysicalCardAddress.js rename to src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx index 21ba85b6c5dd..19e7cbc23d05 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx @@ -1,58 +1,35 @@ -import PropTypes from 'prop-types'; +import {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry} from 'react-native-onyx/lib/types'; import AddressForm from '@components/AddressForm'; import useLocalize from '@hooks/useLocalize'; import * as FormActions from '@libs/actions/FormActions'; -import FormUtils from '@libs/FormUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; +import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; +import type {RenderContentProps} from './BaseGetPhysicalCard'; -const propTypes = { - /* Onyx Props */ +type GetPhysicalCardAddressOnyxProps = { /** Draft values used by the get physical card form */ - draftValues: PropTypes.shape({ - // User home address - addressLine1: PropTypes.string, - addressLine2: PropTypes.string, - city: PropTypes.string, - country: PropTypes.string, - state: PropTypes.string, - zipPostCode: PropTypes.string, - }), - - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** Currently selected country */ - country: PropTypes.string, - /** domain passed via route /settings/wallet/card/:domain */ - domain: PropTypes.string, - }), - }).isRequired, + draftValues: OnyxEntry; }; -const defaultProps = { - draftValues: { - addressLine1: '', - addressLine2: '', - city: '', - country: '', - state: '', - zipPostCode: '', - }, -}; +type GetPhysicalCardAddressProps = GetPhysicalCardAddressOnyxProps & StackScreenProps; function GetPhysicalCardAddress({ - draftValues: {addressLine1, addressLine2, city, state, zipPostCode, country}, + draftValues, route: { params: {country: countryFromUrl, domain}, }, -}) { +}: GetPhysicalCardAddressProps) { const {translate} = useLocalize(); + const {addressLine1, addressLine2, city, state, zipPostCode, country} = draftValues ?? {}; + useEffect(() => { if (!countryFromUrl) { return; @@ -61,7 +38,7 @@ function GetPhysicalCardAddress({ }, [countryFromUrl]); const renderContent = useCallback( - (onSubmit, submitButtonText) => ( + ({onSubmit, submitButtonText}: RenderContentProps) => ( ({ draftValues: { - key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM), + key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, }, })(GetPhysicalCardAddress); diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.js b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx similarity index 61% rename from src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.js rename to src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx index 9f364c32c075..67a77ce6630b 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx @@ -1,79 +1,53 @@ -import PropTypes from 'prop-types'; +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +import {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry} from 'react-native-onyx/lib/types'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import FormUtils from '@libs/FormUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; +import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; -const goToGetPhysicalCardName = (domain) => { +const goToGetPhysicalCardName = (domain: string) => { Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain), CONST.NAVIGATION.ACTION_TYPE.PUSH); }; -const goToGetPhysicalCardPhone = (domain) => { +const goToGetPhysicalCardPhone = (domain: string) => { Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain), CONST.NAVIGATION.ACTION_TYPE.PUSH); }; -const goToGetPhysicalCardAddress = (domain) => { +const goToGetPhysicalCardAddress = (domain: string) => { Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS.getRoute(domain), CONST.NAVIGATION.ACTION_TYPE.PUSH); }; -const propTypes = { - /* Onyx Props */ +type GetPhysicalCardConfirmOnyxProps = { /** Draft values used by the get physical card form */ - draftValues: PropTypes.shape({ - addressLine1: PropTypes.string, - addressLine2: PropTypes.string, - city: PropTypes.string, - state: PropTypes.string, - country: PropTypes.string, - zipPostCode: PropTypes.string, - phoneNumber: PropTypes.string, - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - }), - - /* Navigation Props */ - /** Navigation route context info provided by react navigation */ - route: PropTypes.shape({ - params: PropTypes.shape({ - /** domain passed via route /settings/wallet/card/:domain */ - domain: PropTypes.string, - }), - }).isRequired, + draftValues: OnyxEntry; }; -const defaultProps = { - draftValues: { - addressLine1: '', - addressLine2: '', - city: '', - state: '', - country: '', - zipPostCode: '', - phoneNumber: '', - legalFirstName: '', - legalLastName: '', - }, -}; +type GetPhysicalCardConfirmProps = GetPhysicalCardConfirmOnyxProps & StackScreenProps; function GetPhysicalCardConfirm({ - draftValues: {addressLine1, addressLine2, city, state, country, zipPostCode, legalFirstName, legalLastName, phoneNumber}, + draftValues, route: { params: {domain}, }, -}) { +}: GetPhysicalCardConfirmProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const {addressLine1, addressLine2, city, state, zipPostCode, country, phoneNumber, legalFirstName, legalLastName} = draftValues ?? {}; + return ( - {translate('getPhysicalCard.estimatedDeliveryMessage')} + {translate('getPhysicalCard.estimatedDeliveryMessage')} @@ -117,12 +91,10 @@ function GetPhysicalCardConfirm({ ); } -GetPhysicalCardConfirm.defaultProps = defaultProps; GetPhysicalCardConfirm.displayName = 'GetPhysicalCardConfirm'; -GetPhysicalCardConfirm.propTypes = propTypes; -export default withOnyx({ +export default withOnyx({ draftValues: { - key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM), + key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, }, })(GetPhysicalCardConfirm); diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardName.js b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx similarity index 61% rename from src/pages/settings/Wallet/Card/GetPhysicalCardName.js rename to src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx index 5b954d432cce..2264845e710d 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardName.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx @@ -1,63 +1,55 @@ -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import FormUtils from '@libs/FormUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; -const propTypes = { - /* Onyx Props */ - /** Draft values used by the get physical card form */ - draftValues: PropTypes.shape({ - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - }), - - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** domain passed via route /settings/wallet/card/:domain */ - domain: PropTypes.string, - }), - }).isRequired, +type OnValidateResult = { + legalFirstName?: string; + legalLastName?: string; }; -const defaultProps = { - draftValues: { - legalFirstName: '', - legalLastName: '', - }, +type GetPhysicalCardNameOnyxProps = { + /** Draft values used by the get physical card form */ + draftValues: OnyxEntry; }; +type GetPhysicalCardNameProps = GetPhysicalCardNameOnyxProps & StackScreenProps; + function GetPhysicalCardName({ - draftValues: {legalFirstName, legalLastName}, + draftValues, route: { params: {domain}, }, -}) { +}: GetPhysicalCardNameProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const onValidate = (values) => { - const errors = {}; - if (!ValidationUtils.isValidLegalName(values.legalFirstName)) { + const {legalFirstName, legalLastName} = draftValues ?? {}; + + const onValidate = (values: OnyxEntry): OnValidateResult => { + const errors: OnValidateResult = {}; + + if (values?.legalFirstName && !ValidationUtils.isValidLegalName(values.legalFirstName)) { errors.legalFirstName = 'privatePersonalDetails.error.hasInvalidCharacter'; - } else if (_.isEmpty(values.legalFirstName)) { + } else if (!values?.legalFirstName) { errors.legalFirstName = 'common.error.fieldRequired'; } - if (!ValidationUtils.isValidLegalName(values.legalLastName)) { + if (values?.legalLastName && !ValidationUtils.isValidLegalName(values.legalLastName)) { errors.legalLastName = 'privatePersonalDetails.error.hasInvalidCharacter'; - } else if (_.isEmpty(values.legalLastName)) { + } else if (!values?.legalLastName) { errors.legalLastName = 'common.error.fieldRequired'; } @@ -74,6 +66,7 @@ function GetPhysicalCardName({ onValidate={onValidate} > - ({ draftValues: { - key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM), + key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, }, })(GetPhysicalCardName); diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx similarity index 60% rename from src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js rename to src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx index 5e4feac83d96..ea53fb7ba81e 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx @@ -1,57 +1,49 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import FormUtils from '@libs/FormUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; -const propTypes = { - /* Onyx Props */ - /** Draft values used by the get physical card form */ - draftValues: PropTypes.shape({ - phoneNumber: PropTypes.string, - }), - - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** domain passed via route /settings/wallet/card/:domain */ - domain: PropTypes.string, - }), - }).isRequired, +type OnValidateResult = { + phoneNumber?: string; }; -const defaultProps = { - draftValues: { - phoneNumber: '', - }, +type GetPhysicalCardPhoneOnyxProps = { + /** Draft values used by the get physical card form */ + draftValues: OnyxEntry; }; +type GetPhysicalCardPhoneProps = GetPhysicalCardPhoneOnyxProps & StackScreenProps; + function GetPhysicalCardPhone({ - draftValues: {phoneNumber}, route: { params: {domain}, }, -}) { + draftValues, +}: GetPhysicalCardPhoneProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const onValidate = (values) => { - const errors = {}; + const {phoneNumber} = draftValues ?? {}; + + const onValidate = (values: OnyxEntry): OnValidateResult => { + const errors: OnValidateResult = {}; - if (!(parsePhoneNumber(values.phoneNumber).possible && Str.isValidPhone(values.phoneNumber))) { + if (!(parsePhoneNumber(values?.phoneNumber ?? '').possible && Str.isValidPhone(values?.phoneNumber ?? ''))) { errors.phoneNumber = 'common.error.phoneNumber'; - } else if (_.isEmpty(values.phoneNumber)) { + } else if (!values?.phoneNumber) { errors.phoneNumber = 'common.error.fieldRequired'; } @@ -68,6 +60,7 @@ function GetPhysicalCardPhone({ onValidate={onValidate} > ({ draftValues: { - key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM), + key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, }, })(GetPhysicalCardPhone); diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index e3b025ff5a2f..68acd88aa120 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -33,5 +33,7 @@ type TCardDetails = { }; }; +type CardList = Record; + export default Card; -export type {TCardDetails}; +export type {TCardDetails, CardList}; diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index ca8d6574adf5..fdde34898f84 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -21,6 +21,27 @@ type DateOfBirthForm = Form & { dob?: string; }; +type GetPhysicalCardForm = Form & { + /** Address line 1 for delivery */ + addressLine1?: string; + /** Address line 2 for delivery */ + addressLine2?: string; + /** City for delivery */ + city?: string; + /** Country for delivery */ + country?: string; + /** First name for delivery */ + legalFirstName?: string; + /** Last name for delivery */ + legalLastName?: string; + /** Phone number for delivery */ + phoneNumber?: string; + /** State for delivery */ + state?: string; + /** Zip code for delivery */ + zipPostCode?: string; +}; + export default Form; -export type {AddDebitCardForm, DateOfBirthForm}; +export type {AddDebitCardForm, DateOfBirthForm, GetPhysicalCardForm}; diff --git a/src/types/onyx/PrivatePersonalDetails.ts b/src/types/onyx/PrivatePersonalDetails.ts index 6ef5b75c4a0f..4d0dedf16ea7 100644 --- a/src/types/onyx/PrivatePersonalDetails.ts +++ b/src/types/onyx/PrivatePersonalDetails.ts @@ -10,6 +10,7 @@ type PrivatePersonalDetails = { legalFirstName?: string; legalLastName?: string; dob?: string; + phoneNumber?: string; /** User's home address */ address?: Address; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 7bd9c321be5e..3fb7f47f1137 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -1,37 +1,38 @@ import type Account from './Account'; import type AccountData from './AccountData'; -import type {BankAccountList} from './BankAccount'; +import type { BankAccountList } from './BankAccount'; import type BankAccount from './BankAccount'; import type Beta from './Beta'; import type BlockedFromConcierge from './BlockedFromConcierge'; import type Card from './Card'; +import type { CardList } from './Card'; import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AddDebitCardForm, DateOfBirthForm} from './Form'; +import type {AddDebitCardForm, DateOfBirthForm, GetPhysicalCardForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; -import type {FundList} from './Fund'; +import type { FundList } from './Fund'; import type Fund from './Fund'; import type IOU from './IOU'; import type Locale from './Locale'; -import type {LoginList} from './Login'; +import type { LoginList } from './Login'; import type Login from './Login'; import type MapboxAccessToken from './MapboxAccessToken'; import type Modal from './Modal'; import type Network from './Network'; -import type {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; +import type { OnyxUpdateEvent, OnyxUpdatesFromServer } from './OnyxUpdatesFromServer'; import type PersonalBankAccount from './PersonalBankAccount'; -import type {PersonalDetailsList} from './PersonalDetails'; +import type { PersonalDetailsList } from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; import type PlaidData from './PlaidData'; import type Policy from './Policy'; import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; -import type PolicyReportField from './PolicyReportField'; import type {PolicyTag, PolicyTags} from './PolicyTag'; +import type PolicyReportField from './PolicyReportField'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -40,7 +41,7 @@ import type RecentWaypoint from './RecentWaypoint'; import type ReimbursementAccount from './ReimbursementAccount'; import type ReimbursementAccountDraft from './ReimbursementAccountDraft'; import type Report from './Report'; -import type {ReportActions} from './ReportAction'; +import type { ReportActions } from './ReportAction'; import type ReportAction from './ReportAction'; import type ReportActionReactions from './ReportActionReactions'; import type ReportActionsDraft from './ReportActionsDraft'; @@ -55,7 +56,7 @@ import type SecurityGroup from './SecurityGroup'; import type Session from './Session'; import type Task from './Task'; import type Transaction from './Transaction'; -import type {TransactionViolation, ViolationName} from './TransactionViolation'; +import type { TransactionViolation, ViolationName } from './TransactionViolation'; import type User from './User'; import type UserLocation from './UserLocation'; import type UserWallet from './UserWallet'; @@ -65,6 +66,7 @@ import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; + export type { Account, AccountData, @@ -74,6 +76,7 @@ export type { Beta, BlockedFromConcierge, Card, + CardList, Credentials, Currency, CustomStatusDraft, @@ -83,6 +86,7 @@ export type { FrequentlyUsedEmoji, Fund, FundList, + GetPhysicalCardForm, IOU, Locale, Login, From c97ce23271d67c9562f14153e9e32771d5060711 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 2 Jan 2024 16:53:29 +0100 Subject: [PATCH 058/446] fix comments --- src/libs/CardUtils.ts | 4 ++-- src/libs/GetPhysicalCardUtils.ts | 22 +++++-------------- .../Wallet/Card/BaseGetPhysicalCard.tsx | 6 ++++- .../Wallet/Card/GetPhysicalCardAddress.tsx | 8 +++---- .../Wallet/Card/GetPhysicalCardConfirm.tsx | 17 +++++++------- .../Wallet/Card/GetPhysicalCardName.tsx | 6 ++--- .../Wallet/Card/GetPhysicalCardPhone.tsx | 10 +++++---- src/types/onyx/Form.ts | 8 +++++++ src/types/onyx/index.ts | 17 +++++++------- 9 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index f66ddbab2c7f..11b68244fbe1 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -75,8 +75,8 @@ function getYearFromExpirationDateString(expirationDateString: string) { */ function getDomainCards(cardList: OnyxCollection) { // Check for domainName to filter out personal credit cards. - // eslint-disable-next-line you-dont-need-lodash-underscore/filter - const activeCards = lodash.filter(cardList, (card) => !!card?.domainName && (CONST.EXPENSIFY_CARD.ACTIVE_STATES as ReadonlyArray).includes(card.state)); + const activeCards = Object.values(cardList ?? {}).filter((card) => !!card?.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.some((element) => element === card.state)); + return lodash.groupBy(activeCards, (card) => card?.domainName); } diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index 4e6775fa10b3..6b0e940a053d 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ import type {OnyxEntry} from 'react-native-onyx'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; @@ -7,13 +6,6 @@ import Navigation from './Navigation/Navigation'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as UserUtils from './UserUtils'; -/** - * - * @param domain - * @param privatePersonalDetails - * @param loginList - * @returns - */ function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry): Route { const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; @@ -30,13 +22,6 @@ function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry) { Navigation.navigate(getCurrentRoute(domain, privatePersonalDetails, loginList)); } @@ -71,6 +56,8 @@ function getUpdatedDraftValues(draftValues: OnyxEntry, priv const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; return { + /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ + // we do not need to use nullish coalescing here because we want to allow empty strings legalFirstName: draftValues?.legalFirstName || legalFirstName, legalLastName: draftValues?.legalLastName || legalLastName, addressLine1: draftValues?.addressLine1 || address?.street.split('\n')[0], @@ -80,6 +67,7 @@ function getUpdatedDraftValues(draftValues: OnyxEntry, priv phoneNumber: draftValues?.phoneNumber || phoneNumber || UserUtils.getSecondaryPhoneLogin(loginList) || '', state: draftValues?.state || address?.state, zipPostCode: draftValues?.zipPostCode || address?.zip || '', + /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ }; } @@ -89,12 +77,12 @@ function getUpdatedDraftValues(draftValues: OnyxEntry, priv * @returns */ function getUpdatedPrivatePersonalDetails(draftValues: OnyxEntry): PrivatePersonalDetails { - const {addressLine1, addressLine2, city, country, legalFirstName, legalLastName, phoneNumber, state, zipPostCode} = draftValues ?? {}; + const {addressLine1, addressLine2, city = '', country = '', legalFirstName, legalLastName, phoneNumber, state = '', zipPostCode = ''} = draftValues ?? {}; return { legalFirstName, legalLastName, phoneNumber, - address: {street: PersonalDetailsUtils.getFormattedStreet(addressLine1, addressLine2), city: city || '', country: country || '', state: state || '', zip: zipPostCode || ''}, + address: {street: PersonalDetailsUtils.getFormattedStreet(addressLine1, addressLine2), city, country, state, zip: zipPostCode}, }; } diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index ec5296434505..36bf749125b5 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -1,4 +1,5 @@ import React, {useCallback, useEffect, useRef} from 'react'; +import type {ReactNode} from 'react'; import {Text} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx/lib/types'; @@ -42,10 +43,13 @@ type BaseGetPhysicalCardOnyxProps = { loginList: OnyxEntry; }; -type BaseGetPhysicalCardProps = ChildrenProps & BaseGetPhysicalCardOnyxProps & { +type BaseGetPhysicalCardProps = BaseGetPhysicalCardOnyxProps & { /** Text displayed below page title */ headline: string; + /** Children components that will be rendered by renderContent */ + children?: ReactNode; + /** Current route from ROUTES */ currentRoute: string; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx index 19e7cbc23d05..578a36afc31f 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx @@ -1,14 +1,14 @@ -import {StackScreenProps} from '@react-navigation/stack'; +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; -import {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import AddressForm from '@components/AddressForm'; import useLocalize from '@hooks/useLocalize'; import * as FormActions from '@libs/actions/FormActions'; import type {SettingsNavigatorParamList} from '@navigation/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; +import type SCREENS from '@src/SCREENS'; import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; import type {RenderContentProps} from './BaseGetPhysicalCard'; @@ -28,7 +28,7 @@ function GetPhysicalCardAddress({ }: GetPhysicalCardAddressProps) { const {translate} = useLocalize(); - const {addressLine1, addressLine2, city, state, zipPostCode, country} = draftValues ?? {}; + const {addressLine1 = '', addressLine2 = '', city = '', state = '', zipPostCode = '', country = ''} = draftValues ?? {}; useEffect(() => { if (!countryFromUrl) { diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx index 67a77ce6630b..967d919b239c 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx @@ -1,8 +1,7 @@ -/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ -import {StackScreenProps} from '@react-navigation/stack'; +import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import Text from '@components/Text'; @@ -14,7 +13,7 @@ import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; +import type SCREENS from '@src/SCREENS'; import type {GetPhysicalCardForm} from '@src/types/onyx'; import BaseGetPhysicalCard from './BaseGetPhysicalCard'; @@ -46,7 +45,7 @@ function GetPhysicalCardConfirm({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const {addressLine1, addressLine2, city, state, zipPostCode, country, phoneNumber, legalFirstName, legalLastName} = draftValues ?? {}; + const {addressLine1, addressLine2, city = '', state = '', zipPostCode = '', country = '', phoneNumber = '', legalFirstName = '', legalLastName = ''} = draftValues ?? {}; return ( diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx index 2264845e710d..67eaa0193f0d 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx @@ -36,7 +36,7 @@ function GetPhysicalCardName({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const {legalFirstName, legalLastName} = draftValues ?? {}; + const {legalFirstName = '', legalLastName = ''} = draftValues ?? {}; const onValidate = (values: OnyxEntry): OnValidateResult => { const errors: OnValidateResult = {}; @@ -72,7 +72,7 @@ function GetPhysicalCardName({ name="legalFirstName" label={translate('getPhysicalCard.legalFirstName')} aria-label={translate('getPhysicalCard.legalFirstName')} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} autoCapitalize="words" defaultValue={legalFirstName} containerStyles={[styles.mh5]} @@ -85,7 +85,7 @@ function GetPhysicalCardName({ name="legalLastName" label={translate('getPhysicalCard.legalLastName')} aria-label={translate('getPhysicalCard.legalLastName')} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} autoCapitalize="words" defaultValue={legalLastName} containerStyles={[styles.mt5, styles.mh5]} diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx index ea53fb7ba81e..9c3426cac991 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx @@ -36,14 +36,16 @@ function GetPhysicalCardPhone({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const {phoneNumber} = draftValues ?? {}; + const {phoneNumber = ''} = draftValues ?? {}; const onValidate = (values: OnyxEntry): OnValidateResult => { + const {phoneNumber: phoneNumberToValidate = ''} = values ?? {}; + const errors: OnValidateResult = {}; - if (!(parsePhoneNumber(values?.phoneNumber ?? '').possible && Str.isValidPhone(values?.phoneNumber ?? ''))) { + if (!(parsePhoneNumber(phoneNumberToValidate).possible && Str.isValidPhone(phoneNumberToValidate))) { errors.phoneNumber = 'common.error.phoneNumber'; - } else if (!values?.phoneNumber) { + } else if (!phoneNumberToValidate) { errors.phoneNumber = 'common.error.fieldRequired'; } @@ -66,7 +68,7 @@ function GetPhysicalCardPhone({ name="phoneNumber" label={translate('getPhysicalCard.phoneNumber')} aria-label={translate('getPhysicalCard.phoneNumber')} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} defaultValue={phoneNumber} containerStyles={[styles.mh5]} shouldSaveDraft diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index fdde34898f84..f299f5e161fb 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -24,20 +24,28 @@ type DateOfBirthForm = Form & { type GetPhysicalCardForm = Form & { /** Address line 1 for delivery */ addressLine1?: string; + /** Address line 2 for delivery */ addressLine2?: string; + /** City for delivery */ city?: string; + /** Country for delivery */ country?: string; + /** First name for delivery */ legalFirstName?: string; + /** Last name for delivery */ legalLastName?: string; + /** Phone number for delivery */ phoneNumber?: string; + /** State for delivery */ state?: string; + /** Zip code for delivery */ zipPostCode?: string; }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 3fb7f47f1137..fda23d340e16 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -1,11 +1,11 @@ import type Account from './Account'; import type AccountData from './AccountData'; -import type { BankAccountList } from './BankAccount'; +import type {BankAccountList} from './BankAccount'; import type BankAccount from './BankAccount'; import type Beta from './Beta'; import type BlockedFromConcierge from './BlockedFromConcierge'; import type Card from './Card'; -import type { CardList } from './Card'; +import type {CardList} from './Card'; import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; @@ -13,18 +13,18 @@ import type Download from './Download'; import type {AddDebitCardForm, DateOfBirthForm, GetPhysicalCardForm} from './Form'; import type Form from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; -import type { FundList } from './Fund'; +import type {FundList} from './Fund'; import type Fund from './Fund'; import type IOU from './IOU'; import type Locale from './Locale'; -import type { LoginList } from './Login'; +import type {LoginList} from './Login'; import type Login from './Login'; import type MapboxAccessToken from './MapboxAccessToken'; import type Modal from './Modal'; import type Network from './Network'; -import type { OnyxUpdateEvent, OnyxUpdatesFromServer } from './OnyxUpdatesFromServer'; +import type {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; import type PersonalBankAccount from './PersonalBankAccount'; -import type { PersonalDetailsList } from './PersonalDetails'; +import type {PersonalDetailsList} from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; import type PlaidData from './PlaidData'; import type Policy from './Policy'; @@ -41,7 +41,7 @@ import type RecentWaypoint from './RecentWaypoint'; import type ReimbursementAccount from './ReimbursementAccount'; import type ReimbursementAccountDraft from './ReimbursementAccountDraft'; import type Report from './Report'; -import type { ReportActions } from './ReportAction'; +import type {ReportActions} from './ReportAction'; import type ReportAction from './ReportAction'; import type ReportActionReactions from './ReportActionReactions'; import type ReportActionsDraft from './ReportActionsDraft'; @@ -56,7 +56,7 @@ import type SecurityGroup from './SecurityGroup'; import type Session from './Session'; import type Task from './Task'; import type Transaction from './Transaction'; -import type { TransactionViolation, ViolationName } from './TransactionViolation'; +import type {TransactionViolation, ViolationName} from './TransactionViolation'; import type User from './User'; import type UserLocation from './UserLocation'; import type UserWallet from './UserWallet'; @@ -66,7 +66,6 @@ import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; - export type { Account, AccountData, From fe4b1788b7c5adc4ed32246b32fbc3693e89e997 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Wed, 10 Jan 2024 10:17:43 -0800 Subject: [PATCH 059/446] Fix bugs --- src/libs/actions/IOU.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 27326f896f14..b1f0b00d7f7e 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1054,11 +1054,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t } // Add optimistic transaction violations - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories), - }); + optimisticData.push(ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); // Clear out the error fields and loading states on success successData.push({ @@ -2272,7 +2268,6 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // STEP 4: Compose the optimistic data const currentTime = DateUtils.getDBTime(); const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories); - // TODO const previousViolationsOnyxData = { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, @@ -2309,11 +2304,7 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans lastVisibleActionCreated: currentTime, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: updatedViolationsOnyxData, - }, + updatedViolationsOnyxData, ...(!isScanning ? [ { From a1899701ed39638bfedd73ea840f8d193b15e4b5 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Wed, 10 Jan 2024 10:50:46 -0800 Subject: [PATCH 060/446] Fix data passing --- src/libs/actions/IOU.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index b1f0b00d7f7e..474028af01f0 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1054,7 +1054,8 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t } // Add optimistic transaction violations - optimisticData.push(ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); + const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + optimisticData.push(ViolationsUtils.getViolationsOnyxData(transaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); // Clear out the error fields and loading states on success successData.push({ @@ -1098,7 +1099,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`], + value: currentTransactionViolations, }); return { @@ -2267,12 +2268,15 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // STEP 4: Compose the optimistic data const currentTime = DateUtils.getDBTime(); - const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories); - const previousViolationsOnyxData = { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`], - }; + const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData( + transaction, + currentTransactionViolations, + policy.requiresTag, + policyTags, + policy.requiresCategory, + policyCategories, + ); const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -2431,7 +2435,7 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: previousViolationsOnyxData, + value: currentTransactionViolations, }, ]; From 0b0ed8c8a22540e1fbb49e0849885c7201e4c724 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Thu, 11 Jan 2024 18:22:11 +1300 Subject: [PATCH 061/446] fix lint error --- src/components/PDFView/index.native.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 6f1dfb5dc405..af3980f663ef 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -43,7 +43,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused const [password, setPassword] = useState(''); const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); - const styles = useThemeStyles(); + const themeStyles = useThemeStyles(); const isKeyboardShown = useKeyboardState(); const StyleUtils = useStyleUtils(); @@ -113,22 +113,22 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused }; function renderPDFView() { - const pdfStyles = [styles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(windowWidth, windowHeight)]; + const pdfStyles = [themeStyles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(windowWidth, windowHeight)]; // If we haven't yet successfully validated the password and loaded the PDF, // then we need to hide the react-native-pdf/PDF component so that PDFPasswordForm // is positioned nicely. We're specifically hiding it because we still need to render // the PDF component so that it can validate the password. if (shouldRequestPassword) { - pdfStyles.push(styles.invisible); + pdfStyles.push(themeStyles.invisible); } - const containerStyles = shouldRequestPassword && isSmallScreenWidth ? [styles.w100, styles.flex1] : [styles.alignItemsCenter, styles.flex1]; + const containerStyles = shouldRequestPassword && isSmallScreenWidth ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; return ( {failedToLoadPDF && ( - + {translate('attachmentView.failedToLoadPDF')} )} @@ -147,7 +147,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused /> )} {shouldRequestPassword && ( - + Date: Thu, 11 Jan 2024 18:28:06 +1300 Subject: [PATCH 062/446] fix lint error --- src/components/PDFView/index.native.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index af3980f663ef..456b0c38d56e 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -8,8 +8,8 @@ import Text from '@components/Text'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import useStyleUtils from '@styles/useStyleUtils'; -import useThemeStyles from '@styles/useThemeStyles'; +import useStyleUtils from '@hooks//useStyleUtils'; +import useThemeStyles from '@hooks//useThemeStyles'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; From b42a4b26b2735f5ef7f4bed48742cf60904aa2f4 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Thu, 11 Jan 2024 18:38:14 +1300 Subject: [PATCH 063/446] fixing weird lint error --- src/pages/RoomInvitePage.js | 2 +- src/pages/workspace/WorkspaceInvitePage.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index 5254339dd7ba..e8db700fc315 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -1,4 +1,4 @@ -import {parsePhoneNumber} from 'awesome-phonenumber'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 1f75ea0f1aa0..c30ad6d5ebf3 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -1,4 +1,4 @@ -import {parsePhoneNumber} from 'awesome-phonenumber'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; From c9e539716e72bcc9ff34e9072f0b0f020506c311 Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Thu, 11 Jan 2024 18:42:22 +1300 Subject: [PATCH 064/446] fixing lint error --- src/components/PDFView/index.native.js | 4 ++-- src/pages/RoomInvitePage.js | 2 +- src/pages/workspace/WorkspaceInvitePage.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 456b0c38d56e..5ed1655d7eab 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -5,11 +5,11 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; +import useStyleUtils from '@hooks//useStyleUtils'; +import useThemeStyles from '@hooks//useThemeStyles'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import useStyleUtils from '@hooks//useStyleUtils'; -import useThemeStyles from '@hooks//useThemeStyles'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index e8db700fc315..d2ef900b051f 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -1,4 +1,3 @@ -import {parsePhoneNumber} from '@libs/PhoneNumber'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -19,6 +18,7 @@ import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index c30ad6d5ebf3..c7a1da7b64ff 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -1,4 +1,3 @@ -import {parsePhoneNumber} from '@libs/PhoneNumber'; import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -19,6 +18,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; From 69ad6950c34bf5d2b6f6da84e5b4dc097a178e2e Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 11 Jan 2024 11:47:23 -0800 Subject: [PATCH 065/446] Temp fix for tag issue --- src/libs/ViolationsUtils.ts | 10 ++++++++-- src/types/onyx/PolicyTag.ts | 12 +++++++++++- src/types/onyx/index.ts | 3 ++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index 2637686e726b..f7b5482c560a 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -2,8 +2,9 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyCategories, PolicyTags, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {PolicyCategories, PolicyTagList, PolicyTags, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Phrase, PhraseParameters} from './Localize'; +import * as PolicyUtils from './PolicyUtils'; const ViolationsUtils = { /** @@ -14,7 +15,7 @@ const ViolationsUtils = { transaction: Transaction, transactionViolations: TransactionViolation[], policyRequiresTags: boolean, - policyTags: PolicyTags, + policyTagList: PolicyTagList, policyRequiresCategories: boolean, policyCategories: PolicyCategories, ): { @@ -50,7 +51,12 @@ const ViolationsUtils = { } } + if (policyRequiresTags) { + // TODO, this fixes it but TS rightly complains about + // @ts-ignore + const tagListName: string = PolicyUtils.getTagListName(policyTagList); + const policyTags = policyTagList[tagListName].tags; const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'tagOutOfPolicy'); const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === 'missingTag'); const isTagInPolicy = Boolean(policyTags[transaction.tag]?.enabled); diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 58a21dcf4df5..149da7eb3341 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -12,4 +12,14 @@ type PolicyTag = { type PolicyTags = Record; -export type {PolicyTag, PolicyTags}; +type PolicyTagList = Record; + +export type {PolicyTag, PolicyTags, PolicyTagList}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 7bd9c321be5e..d307c61b0baa 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,7 +31,7 @@ import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type PolicyReportField from './PolicyReportField'; -import type {PolicyTag, PolicyTags} from './PolicyTag'; +import type {PolicyTag, PolicyTags, PolicyTagList} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -103,6 +103,7 @@ export type { PolicyMembers, PolicyTag, PolicyTags, + PolicyTagList, PrivatePersonalDetails, RecentWaypoint, RecentlyUsedCategories, From 41e87caeb581dd95d5b8ef8b8be8886281089626 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 11 Jan 2024 16:18:24 -0800 Subject: [PATCH 066/446] Use full policyTags data as ViolationsUtils input --- src/libs/ViolationsUtils.ts | 11 ++++------- src/types/onyx/PolicyTag.ts | 8 ++++++-- tests/unit/ViolationUtilsTest.js | 8 +++++++- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index f7b5482c560a..6db4eb58cfa9 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -2,9 +2,8 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyCategories, PolicyTagList, PolicyTags, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Phrase, PhraseParameters} from './Localize'; -import * as PolicyUtils from './PolicyUtils'; const ViolationsUtils = { /** @@ -15,7 +14,7 @@ const ViolationsUtils = { transaction: Transaction, transactionViolations: TransactionViolation[], policyRequiresTags: boolean, - policyTagList: PolicyTagList, + policyTagList: PolicyTagList, policyRequiresCategories: boolean, policyCategories: PolicyCategories, ): { @@ -53,10 +52,8 @@ const ViolationsUtils = { if (policyRequiresTags) { - // TODO, this fixes it but TS rightly complains about - // @ts-ignore - const tagListName: string = PolicyUtils.getTagListName(policyTagList); - const policyTags = policyTagList[tagListName].tags; + const policyTagListName = Object.keys(policyTagList)[0]; + const policyTags = policyTagList[policyTagListName].tags; const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'tagOutOfPolicy'); const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === 'missingTag'); const isTagInPolicy = Boolean(policyTags[transaction.tag]?.enabled); diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 149da7eb3341..a2872971a7f5 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -12,7 +12,9 @@ type PolicyTag = { type PolicyTags = Record; -type PolicyTagList = Record = Record = Record; + }>; + + type PolicyTagList = PolicyTagListGeneric; export type {PolicyTag, PolicyTags, PolicyTagList}; diff --git a/tests/unit/ViolationUtilsTest.js b/tests/unit/ViolationUtilsTest.js index cc84c547da2e..1f16d41f598e 100644 --- a/tests/unit/ViolationUtilsTest.js +++ b/tests/unit/ViolationUtilsTest.js @@ -128,7 +128,13 @@ describe('getViolationsOnyxData', () => { describe('policyRequiresTags', () => { beforeEach(() => { policyRequiresTags = true; - policyTags = {Lunch: {enabled: true}, Dinner: {enabled: true}}; + policyTags = { + Tag: { + name: 'Tag', + required: true, + tags: {Lunch: {enabled: true}, Dinner: {enabled: true}}, + }, + }; transaction.tag = 'Lunch'; }); From 0230d2351e64d4ecbfd6c0f7d53d134ea81eb6c6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Jan 2024 17:22:20 +0100 Subject: [PATCH 067/446] add a new report flag --- src/types/onyx/Report.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index f6af87038d00..0c2970f4d3f6 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -160,6 +160,9 @@ type Report = { /** If the report contains reportFields, save the field id and its value */ reportFields?: Record; + + /** Whether the user can do self approve or submit of an expense report */ + isPreventSelfApprovalEnabled?: boolean; }; export default Report; From c2f14f83e5fe41318b054dee0f582865403905f1 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 12 Jan 2024 17:22:42 +0100 Subject: [PATCH 068/446] draft implementation of buildNextStep --- src/libs/NextStepUtils.ts | 129 ++++++++++++- tests/unit/NextStepUtilsTest.ts | 315 ++++++++++++++++++++++++++++++++ 2 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 tests/unit/NextStepUtilsTest.ts diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index a76a7d3c75c4..c45ccec43e24 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,6 +1,23 @@ import Str from 'expensify-common/lib/str'; +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportNextStep} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportNextStep'; import EmailUtils from './EmailUtils'; +import * as ReportUtils from './ReportUtils'; + +let currentUserAccountID: number | undefined; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + if (!value) { + return; + } + + currentUserAccountID = value.accountID; + }, +}); function parseMessage(messages: Message[] | undefined) { let nextStepHTML = ''; @@ -27,5 +44,113 @@ function parseMessage(messages: Message[] | undefined) { return `${formattedHtml}`; } -// eslint-disable-next-line import/prefer-default-export -export {parseMessage}; +/** + * + * @param report + * @param isPaidWithWallet - Whether a report has been paid with wallet or outside of Expensify + * @returns next step + */ +function buildNextStep(report: Report, isPaidWithWallet = false): ReportNextStep | null { + const { + statusNum = CONST.REPORT.STATUS.OPEN, + // TODO: Clarify default value + isPreventSelfApprovalEnabled = false, + } = report; + const policy = ReportUtils.getPolicy(report.policyID ?? ''); + const isSelfApproval = policy.submitsTo === currentUserAccountID; + const submitterDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(policy.submitsTo, true) ?? ''; + const type: ReportNextStep['type'] = 'neutral'; + let optimisticNextStep: ReportNextStep | null; + + switch (statusNum) { + case CONST.REPORT.STATUS.OPEN: { + const message = [ + { + text: 'Waiting for', + }, + { + text: submitterDisplayName, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: 'these expenses.', + }, + ]; + const preventedSelfApprovalMessage = [ + { + text: "Oops! Looks like you're submitting to ", + }, + { + text: 'yourself', + type: 'strong', + }, + { + text: '. Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', + }, + ]; + + optimisticNextStep = { + type, + title: 'Next Steps:', + message: isPreventSelfApprovalEnabled && isSelfApproval ? preventedSelfApprovalMessage : message, + }; + break; + } + + case CONST.REPORT.STATUS.SUBMITTED: + optimisticNextStep = { + type, + title: 'Next Steps:', + message: [{text: 'Waiting for'}, {text: submitterDisplayName, type: 'strong'}, {text: 'to'}, {text: 'review', type: 'strong'}, {text: ' %expenses.'}], + }; + break; + + case CONST.REPORT.STATUS.APPROVED: { + const message = [ + { + text: isSelfApproval ? Str.recapitalize(submitterDisplayName) : submitterDisplayName, + type: 'strong', + }, + { + text: 'have marked these expenses as', + }, + { + text: 'paid', + type: 'strong', + }, + ]; + + if (!isPaidWithWallet) { + message.push({text: 'outside of Expensify.'}); + } + + optimisticNextStep = { + type, + title: 'Finished!', + message, + }; + break; + } + + default: + optimisticNextStep = null; + } + + return optimisticNextStep; +} + +export {parseMessage, buildNextStep}; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts new file mode 100644 index 000000000000..5de218911375 --- /dev/null +++ b/tests/unit/NextStepUtilsTest.ts @@ -0,0 +1,315 @@ +import Str from 'expensify-common/lib/str'; +import CONST from '@src/CONST'; +import type {ReportNextStep} from '@src/types/onyx'; +import * as NextStepUtils from '../../src/libs/NextStepUtils'; +import * as ReportUtils from '../../src/libs/ReportUtils'; + +describe('libs/NextStepUtils', () => { + describe('buildNextStep', () => { + const fakeSubmitterEmail = 'submitter@expensify.com'; + const fakeSelfSubmitterEmail = 'you'; + const fakeChatReportID = '1'; + const fakePolicyID = '2'; + const fakePayeeAccountID = 3; + const report = ReportUtils.buildOptimisticExpenseReport(fakeChatReportID, fakePolicyID, fakePayeeAccountID, -500, CONST.CURRENCY.USD); + + const optimisticNextStep: ReportNextStep = { + type: 'neutral', + title: '', + message: [], + }; + + beforeEach(() => { + report.statusNum = CONST.REPORT.STATUS.OPEN; + optimisticNextStep.title = ''; + optimisticNextStep.message = []; + }); + + it('generates an optimistic nextStep once a report has been opened', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: 'these expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been self opened', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSelfSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: 'these expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been opened with prevented self submitting', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: "Oops! Looks like you're submitting to ", + }, + { + text: 'yourself', + type: 'strong', + }, + { + text: '. Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been submitted', () => { + report.statusNum = CONST.REPORT.STATUS.SUBMITTED; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been self submitted', () => { + report.statusNum = CONST.REPORT.STATUS.SUBMITTED; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSelfSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been approved', () => { + report.statusNum = CONST.REPORT.STATUS.APPROVED; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been self approved', () => { + report.statusNum = CONST.REPORT.STATUS.APPROVED; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for', + }, + { + text: fakeSelfSubmitterEmail, + type: 'strong', + }, + { + text: 'to', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been paid with wallet', () => { + report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: fakeSubmitterEmail, + type: 'strong', + }, + { + text: 'have marked these expenses as', + }, + { + text: 'paid', + type: 'strong', + }, + ]; + + const result = NextStepUtils.buildNextStep(report, true); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been self paid with wallet', () => { + report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: Str.recapitalize(fakeSelfSubmitterEmail), + type: 'strong', + }, + { + text: 'have marked these expenses as', + }, + { + text: 'paid', + type: 'strong', + }, + ]; + + const result = NextStepUtils.buildNextStep(report, true); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been paid outside of Expensify', () => { + report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: fakeSubmitterEmail, + type: 'strong', + }, + { + text: 'have marked these expenses as', + }, + { + text: 'paid', + type: 'strong', + }, + { + text: 'outside of Expensify.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + it('generates an optimistic nextStep once a report has been paid self outside of Expensify', () => { + report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: Str.recapitalize(fakeSelfSubmitterEmail), + type: 'strong', + }, + { + text: 'have marked these expenses as', + }, + { + text: 'paid', + type: 'strong', + }, + { + text: 'outside of Expensify.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); +}); From ad37f60a6122a8e92018ade0f605331ab59312fb Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 12 Jan 2024 18:10:46 +0000 Subject: [PATCH 069/446] refactor(typescript): apply pull request feedback --- src/components/MoneyRequestHeader.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 878f0d3996f2..2f20fe808e59 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -14,7 +14,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetails, Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; @@ -47,7 +47,7 @@ type MoneyRequestHeaderProps = MoneyRequestHeaderOnyxProps & { parentReportAction: ReportAction & OriginalMessageIOU; /** Personal details so we can get the ones for the report participants */ - personalDetails: OnyxCollection; + personalDetails: OnyxEntry; }; function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy, personalDetails}: MoneyRequestHeaderProps) { @@ -64,12 +64,10 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const deleteTransaction = useCallback(() => { const { - originalMessage: {IOUTransactionID}, + originalMessage: {IOUTransactionID = ''}, } = parentReportAction; - if (IOUTransactionID) { - IOU.deleteMoneyRequest(IOUTransactionID, parentReportAction, true); - setIsDeleteModalVisible(false); - } + IOU.deleteMoneyRequest(IOUTransactionID, parentReportAction, true); + setIsDeleteModalVisible(false); }, [parentReportAction, setIsDeleteModalVisible]); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); From 4237770a84762af4549ec8c3713a4990e139373b Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 13:37:23 -0700 Subject: [PATCH 070/446] Update onyx --- package-lock.json | 18 +++++++++--------- package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 373610463b38..ac012bea728f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,7 +94,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.118", + "react-native-onyx": "1.0.126", "react-native-pager-view": "6.2.2", "react-native-pdf": "^6.7.4", "react-native-performance": "^5.1.0", @@ -47034,17 +47034,17 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.118", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz", - "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==", + "version": "1.0.126", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.126.tgz", + "integrity": "sha512-tUJI1mQaWXLfyBFYQQWM6mm9GiCqIXGvjbqJkH1fLY3OqbGW6DyH4CxC+qJrqfi4bKZgZHp5xlBHhkPV4pKK2A==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", "underscore": "^1.13.6" }, "engines": { - "node": ">=16.15.1 <=20.9.0", - "npm": ">=8.11.0 <=10.1.0" + "node": "20.9.0", + "npm": "10.1.0" }, "peerDependencies": { "idb-keyval": "^6.2.1", @@ -89702,9 +89702,9 @@ } }, "react-native-onyx": { - "version": "1.0.118", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz", - "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==", + "version": "1.0.126", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.126.tgz", + "integrity": "sha512-tUJI1mQaWXLfyBFYQQWM6mm9GiCqIXGvjbqJkH1fLY3OqbGW6DyH4CxC+qJrqfi4bKZgZHp5xlBHhkPV4pKK2A==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index e8b724587ca0..4a28617f649d 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.118", + "react-native-onyx": "1.0.126", "react-native-pager-view": "6.2.2", "react-native-pdf": "^6.7.4", "react-native-performance": "^5.1.0", From 83b1547492510fc2390c5969db15d40fc092a4b8 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 13:43:03 -0700 Subject: [PATCH 071/446] Fix types --- .../home/report/withReportAndReportActionOrNotFound.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index 83c62bca5e4a..ccad69282fcb 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -117,15 +117,14 @@ export default function (WrappedComponent: canEvict: false, }, parentReportAction: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, - selector: (parentReportActions, props) => { + key: (props) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${props?.report?.parentReportID ?? 0}`, + selector: (parentReportActions: OnyxEntry, props: WithOnyxInstanceState): OnyxEntry => { const parentReportActionID = props?.report?.parentReportActionID; if (!parentReportActionID) { - return {}; + return null; } - return parentReportActions[parentReportActionID]; + return parentReportActions?.[parentReportActionID] ?? null; }, - canEvict: false, }, }), withWindowDimensions, From 244005198afb8fb078561e7bd8b1f80affcd148e Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 13:43:21 -0700 Subject: [PATCH 072/446] Remove extra onyx subscription --- src/pages/FlagCommentPage.js | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/pages/FlagCommentPage.js b/src/pages/FlagCommentPage.js index 47d2ad356dad..c75f76135aaf 100644 --- a/src/pages/FlagCommentPage.js +++ b/src/pages/FlagCommentPage.js @@ -44,13 +44,13 @@ const propTypes = { ...withLocalizePropTypes, /* Onyx Props */ - /** All the report actions from the parent report */ - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + /** The full action from the parent report */ + parentReportAction: PropTypes.shape(reportActionPropTypes), }; const defaultProps = { reportActions: {}, - parentReportActions: {}, + parentReportAction: {}, report: {}, }; @@ -124,19 +124,18 @@ function FlagCommentPage(props) { // Handle threads if needed if (reportAction === undefined || reportAction.reportActionID === undefined) { - reportAction = props.parentReportActions[props.report.parentReportActionID] || {}; + reportAction = props.parentReportAction; } return reportAction; - }, [props.report, props.reportActions, props.route.params.reportActionID, props.parentReportActions]); + }, [props.reportActions, props.route.params.reportActionID, props.parentReportAction]); const flagComment = (severity) => { let reportID = getReportID(props.route); const reportAction = getActionToFlag(); - const parentReportAction = props.parentReportActions[props.report.parentReportActionID] || {}; // Handle threads if needed - if (ReportUtils.isChatThread(props.report) && reportAction.reportActionID === parentReportAction.reportActionID) { + if (ReportUtils.isChatThread(props.report) && reportAction.reportActionID === props.parentReportAction.reportActionID) { reportID = ReportUtils.getParentReport(props.report).reportID; } @@ -200,10 +199,4 @@ FlagCommentPage.displayName = 'FlagCommentPage'; export default compose( withLocalize, withReportAndReportActionOrNotFound, - withOnyx({ - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID || report.reportID}`, - canEvict: false, - }, - }), )(FlagCommentPage); From 05b979d1411ef255ac353b3e5828008c171c7026 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 14:10:32 -0700 Subject: [PATCH 073/446] Add all withOnyx properties and import type --- src/pages/home/report/withReportAndReportActionOrNotFound.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index ccad69282fcb..f58315f52b88 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -4,6 +4,7 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {useCallback, useEffect} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {WithOnyxInstanceState} from 'react-native-onyx/lib/types'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import withWindowDimensions from '@components/withWindowDimensions'; import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; @@ -125,6 +126,7 @@ export default function (WrappedComponent: } return parentReportActions?.[parentReportActionID] ?? null; }, + canEvict: false, }, }), withWindowDimensions, From 6f7b07f3be4920e96d274c1b68679e85912ef0f6 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 14:14:10 -0700 Subject: [PATCH 074/446] Remove unused imports --- src/pages/FlagCommentPage.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/FlagCommentPage.js b/src/pages/FlagCommentPage.js index c75f76135aaf..55346027a0ec 100644 --- a/src/pages/FlagCommentPage.js +++ b/src/pages/FlagCommentPage.js @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; import {ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -17,7 +16,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import reportActionPropTypes from './home/report/reportActionPropTypes'; import withReportAndReportActionOrNotFound from './home/report/withReportAndReportActionOrNotFound'; From fe4cb1a237a69ed68a04b45184f57f1e356f9433 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 12 Jan 2024 14:47:42 -0700 Subject: [PATCH 075/446] Style --- src/pages/FlagCommentPage.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/FlagCommentPage.js b/src/pages/FlagCommentPage.js index 55346027a0ec..330f2fc6c44e 100644 --- a/src/pages/FlagCommentPage.js +++ b/src/pages/FlagCommentPage.js @@ -194,7 +194,4 @@ FlagCommentPage.propTypes = propTypes; FlagCommentPage.defaultProps = defaultProps; FlagCommentPage.displayName = 'FlagCommentPage'; -export default compose( - withLocalize, - withReportAndReportActionOrNotFound, -)(FlagCommentPage); +export default compose(withLocalize, withReportAndReportActionOrNotFound)(FlagCommentPage); From a09e4ac881643945daefd623724a2be3273aa017 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 11 Jan 2024 16:54:15 -0800 Subject: [PATCH 076/446] getViolationsOnyxData requires new transaction data but previous transaction violations. Fix and make that more clear with argument names --- src/libs/ViolationsUtils.ts | 21 ++++++++++----------- src/libs/actions/IOU.js | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index 6db4eb58cfa9..97891235b5d3 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -11,7 +11,7 @@ const ViolationsUtils = { * violations. */ getViolationsOnyxData( - transaction: Transaction, + updatedTransaction: Transaction, transactionViolations: TransactionViolation[], policyRequiresTags: boolean, policyTagList: PolicyTagList, @@ -27,15 +27,15 @@ const ViolationsUtils = { if (policyRequiresCategories) { const hasCategoryOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'categoryOutOfPolicy'); const hasMissingCategoryViolation = transactionViolations.some((violation) => violation.name === 'missingCategory'); - const isCategoryInPolicy = Boolean(policyCategories[transaction.category]?.enabled); + const isCategoryInPolicy = Boolean(policyCategories[updatedTransaction.category]?.enabled); // Add 'categoryOutOfPolicy' violation if category is not in policy - if (!hasCategoryOutOfPolicyViolation && transaction.category && !isCategoryInPolicy) { + if (!hasCategoryOutOfPolicyViolation && updatedTransaction.category && !isCategoryInPolicy) { newTransactionViolations.push({name: 'categoryOutOfPolicy', type: 'violation', userMessage: ''}); } // Remove 'categoryOutOfPolicy' violation if category is in policy - if (hasCategoryOutOfPolicyViolation && transaction.category && isCategoryInPolicy) { + if (hasCategoryOutOfPolicyViolation && updatedTransaction.category && isCategoryInPolicy) { newTransactionViolations = reject(newTransactionViolations, {name: 'categoryOutOfPolicy'}); } @@ -45,26 +45,25 @@ const ViolationsUtils = { } // Add 'missingCategory' violation if category is required and not set - if (!hasMissingCategoryViolation && policyRequiresCategories && !transaction.category) { + if (!hasMissingCategoryViolation && policyRequiresCategories && !updatedTransaction.category) { newTransactionViolations.push({name: 'missingCategory', type: 'violation', userMessage: ''}); } } - if (policyRequiresTags) { const policyTagListName = Object.keys(policyTagList)[0]; const policyTags = policyTagList[policyTagListName].tags; const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'tagOutOfPolicy'); const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === 'missingTag'); - const isTagInPolicy = Boolean(policyTags[transaction.tag]?.enabled); + const isTagInPolicy = Boolean(policyTags[updatedTransaction.tag]?.enabled); // Add 'tagOutOfPolicy' violation if tag is not in policy - if (!hasTagOutOfPolicyViolation && transaction.tag && !isTagInPolicy) { + if (!hasTagOutOfPolicyViolation && updatedTransaction.tag && !isTagInPolicy) { newTransactionViolations.push({name: 'tagOutOfPolicy', type: 'violation', userMessage: ''}); } // Remove 'tagOutOfPolicy' violation if tag is in policy - if (hasTagOutOfPolicyViolation && transaction.tag && isTagInPolicy) { + if (hasTagOutOfPolicyViolation && updatedTransaction.tag && isTagInPolicy) { newTransactionViolations = reject(newTransactionViolations, {name: 'tagOutOfPolicy'}); } @@ -74,14 +73,14 @@ const ViolationsUtils = { } // Add 'missingTag violation' if tag is required and not set - if (!hasMissingTagViolation && !transaction.tag && policyRequiresTags) { + if (!hasMissingTagViolation && !updatedTransaction.tag && policyRequiresTags) { newTransactionViolations.push({name: 'missingTag', type: 'violation', userMessage: ''}); } } return { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${updatedTransaction.transactionID}`, value: newTransactionViolations, }; }, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 474028af01f0..550b7176a103 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1055,7 +1055,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t // Add optimistic transaction violations const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - optimisticData.push(ViolationsUtils.getViolationsOnyxData(transaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); + optimisticData.push(ViolationsUtils.getViolationsOnyxData(updatedTransaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); // Clear out the error fields and loading states on success successData.push({ @@ -2270,7 +2270,7 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans const currentTime = DateUtils.getDBTime(); const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData( - transaction, + updatedTransaction, currentTransactionViolations, policy.requiresTag, policyTags, From 72cfa451cb7dc4fe5a9943139cb587c462026cec Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Fri, 12 Jan 2024 14:37:08 -0800 Subject: [PATCH 077/446] Improve types --- src/types/onyx/PolicyTag.ts | 25 ++++++++++++------------- src/types/onyx/index.ts | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index a2872971a7f5..1171ade17006 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -12,18 +12,17 @@ type PolicyTag = { type PolicyTags = Record; -// Using a generic to indicate that the top-level key and name should be the -// same value. Not meant for direct use, just used by the alias below. -type PolicyTagListGeneric = Record; - - type PolicyTagList = PolicyTagListGeneric; +type PolicyTagList = Record< + T, + { + /** Name of the tag list */ + name: T; + + /** Flag that determines if tags are required */ + required: boolean; + + tags: PolicyTags; + } +>; export type {PolicyTag, PolicyTags, PolicyTagList}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index d307c61b0baa..e4a9123af56f 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,7 +31,7 @@ import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type PolicyReportField from './PolicyReportField'; -import type {PolicyTag, PolicyTags, PolicyTagList} from './PolicyTag'; +import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; From 8ff7c692224605c4d616b671799a7b0ed85da822 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Fri, 12 Jan 2024 14:52:44 -0800 Subject: [PATCH 078/446] Fix case of passing empty object to policyTags --- src/libs/ViolationsUtils.ts | 8 ++++---- src/types/onyx/PolicyTag.ts | 6 +++++- src/types/onyx/index.ts | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index 97891235b5d3..09b3b0632723 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -2,7 +2,7 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {MaybePolicyTagList, PolicyCategories, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Phrase, PhraseParameters} from './Localize'; const ViolationsUtils = { @@ -14,7 +14,7 @@ const ViolationsUtils = { updatedTransaction: Transaction, transactionViolations: TransactionViolation[], policyRequiresTags: boolean, - policyTagList: PolicyTagList, + policyTagList: MaybePolicyTagList, policyRequiresCategories: boolean, policyCategories: PolicyCategories, ): { @@ -52,10 +52,10 @@ const ViolationsUtils = { if (policyRequiresTags) { const policyTagListName = Object.keys(policyTagList)[0]; - const policyTags = policyTagList[policyTagListName].tags; + const policyTags = policyTagList[policyTagListName]?.tags; const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'tagOutOfPolicy'); const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === 'missingTag'); - const isTagInPolicy = Boolean(policyTags[updatedTransaction.tag]?.enabled); + const isTagInPolicy = Boolean(policyTags?.[updatedTransaction.tag]?.enabled); // Add 'tagOutOfPolicy' violation if tag is not in policy if (!hasTagOutOfPolicyViolation && updatedTransaction.tag && !isTagInPolicy) { diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 1171ade17006..ca8545775a5c 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -25,4 +25,8 @@ type PolicyTagList = Record< } >; -export type {PolicyTag, PolicyTags, PolicyTagList}; +// When queried from Onyx, if there is no matching policy tag list, the data +// returned will be an empty object. +type MaybePolicyTagList = PolicyTagList | Record; + +export type {PolicyTag, PolicyTags, PolicyTagList, MaybePolicyTagList}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e4a9123af56f..20cf1ae69897 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,7 +31,7 @@ import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type PolicyReportField from './PolicyReportField'; -import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; +import type {MaybePolicyTagList, PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -104,6 +104,7 @@ export type { PolicyTag, PolicyTags, PolicyTagList, + MaybePolicyTagList, PrivatePersonalDetails, RecentWaypoint, RecentlyUsedCategories, From f2942ea411fd776f58639334416ebfa417c69ecd Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Fri, 12 Jan 2024 15:29:16 -0800 Subject: [PATCH 079/446] We should only run getViolationsOnyxData if there's a policy --- src/libs/actions/IOU.js | 52 ++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 550b7176a103..ab3bf0882e54 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1053,9 +1053,13 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t } } - // Add optimistic transaction violations + // Add optimistic transaction violations if there is a policy const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - optimisticData.push(ViolationsUtils.getViolationsOnyxData(updatedTransaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories)); + if (policy && policy.id) { + optimisticData.push( + ViolationsUtils.getViolationsOnyxData(updatedTransaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories), + ); + } // Clear out the error fields and loading states on success successData.push({ @@ -1095,12 +1099,14 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t value: iouReport, }); - // Reset transaction violations to their original state - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: currentTransactionViolations, - }); + // If there is a policy, restore transaction violations to their original state + if (policy && policy.id) { + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: currentTransactionViolations, + }); + } return { params, @@ -2268,15 +2274,6 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // STEP 4: Compose the optimistic data const currentTime = DateUtils.getDBTime(); - const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData( - updatedTransaction, - currentTransactionViolations, - policy.requiresTag, - policyTags, - policy.requiresCategory, - policyCategories, - ); const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -2308,7 +2305,6 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans lastVisibleActionCreated: currentTime, }, }, - updatedViolationsOnyxData, ...(!isScanning ? [ { @@ -2432,12 +2428,26 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans lastVisibleActionCreated: transactionThread.lastVisibleActionCreated, }, }, - { + ]; + + // Add transaction violations if there is a policy + if (policy && policy.id) { + const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData( + updatedTransaction, + currentTransactionViolations, + policy.requiresTag, + policyTags, + policy.requiresCategory, + policyCategories, + ); + optimisticData.push(updatedViolationsOnyxData); + failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, value: currentTransactionViolations, - }, - ]; + }); + } // STEP 6: Call the API endpoint const {created, amount, currency, comment, merchant, category, billable, tag} = ReportUtils.getTransactionDetails(updatedTransaction); From fe878dc037d442d763c526060eb82c16e5de2158 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 13 Jan 2024 18:28:06 +0530 Subject: [PATCH 080/446] fix submit behavior on native devices --- src/components/Form/FormProvider.js | 20 ++++++++++++++++++-- src/components/Form/FormWrapper.js | 8 +++----- src/components/Form/InputWrapper.js | 11 ++++++++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 50b24e368fc6..361a0f096f8d 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -238,7 +238,7 @@ const FormProvider = forwardRef( })); const registerInput = useCallback( - (inputID, propsToParse = {}) => { + (inputID, shouldSubmitEdit, propsToParse = {}) => { const newRef = inputRefs.current[inputID] || propsToParse.ref || createRef(); if (inputRefs.current[inputID] !== newRef) { inputRefs.current[inputID] = newRef; @@ -256,6 +256,20 @@ const FormProvider = forwardRef( inputValues[inputID] = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; } + // If the input is a submit editing input, we need to set the onSubmitEditing prop + // to the submit function of the form + const onSubmitEditingObject = shouldSubmitEdit + ? { + onSubmitEditing: (e) => { + submit(); + if (!propsToParse.onSubmitEditing) { + return; + } + propsToParse.onSubmitEditing(e); + }, + } + : {}; + const errorFields = lodashGet(formState, 'errorFields', {}); const fieldErrorMessage = _.chain(errorFields[inputID]) @@ -268,6 +282,8 @@ const FormProvider = forwardRef( return { ...propsToParse, + returnKeyType: shouldSubmitEdit ? 'go' : propsToParse.returnKeyType, + ...onSubmitEditingObject, ref: typeof propsToParse.ref === 'function' ? (node) => { @@ -365,7 +381,7 @@ const FormProvider = forwardRef( }, }; }, - [draftValues, formID, errors, formState, hasServerError, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], + [draftValues, inputValues, formState, errors, submit, setTouchedInput, shouldValidateOnBlur, onValidate, hasServerError, shouldValidateOnChange, formID], ); const value = useMemo(() => ({registerInput}), [registerInput]); diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js index da34262a8af8..af31e0b6f70a 100644 --- a/src/components/Form/FormWrapper.js +++ b/src/components/Form/FormWrapper.js @@ -1,10 +1,9 @@ import PropTypes from 'prop-types'; import React, {useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, StyleSheet} from 'react-native'; +import {Keyboard, ScrollView, StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; -import FormSubmit from '@components/FormSubmit'; import refPropTypes from '@components/refPropTypes'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; @@ -107,11 +106,10 @@ function FormWrapper(props) { const scrollViewContent = useCallback( (safeAreaPaddingBottomStyle) => ( - {children} {isSubmitButtonVisible && ( @@ -155,7 +153,7 @@ function FormWrapper(props) { disablePressOnEnter /> )} - + ), [ children, diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 9a31210195c4..7f49660478ff 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -16,8 +16,17 @@ const defaultProps = { valueType: 'string', }; +const canUseSubmitEditing = (multiline, autoGrowHeight, submitOnEnter) => { + const isMultiline = multiline || autoGrowHeight; + if (!isMultiline) { + return true; + } + return Boolean(submitOnEnter); +}; + function InputWrapper(props) { const {InputComponent, inputID, forwardedRef, ...rest} = props; + const shouldSubmitEdit = canUseSubmitEditing(rest.multiline, rest.autoGrowHeight, rest.submitOnEnter); const {registerInput} = useContext(FormContext); // There are inputs that dont have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were @@ -25,7 +34,7 @@ function InputWrapper(props) { // For now this side effect happened only in `TextInput` components. const shouldSetTouchedOnBlurOnly = InputComponent === TextInput; // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } InputWrapper.propTypes = propTypes; From 36301d1fa489e2965b0dd09c55b6bcc01c3a66f6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 13 Jan 2024 18:32:14 +0530 Subject: [PATCH 081/446] remove unused logic --- src/components/FormSubmit/index.native.tsx | 18 ---- src/components/FormSubmit/index.tsx | 86 ------------------- src/components/FormSubmit/types.ts | 13 --- .../TextInput/BaseTextInput/index.native.tsx | 4 - .../TextInput/BaseTextInput/index.tsx | 4 - 5 files changed, 125 deletions(-) delete mode 100644 src/components/FormSubmit/index.native.tsx delete mode 100644 src/components/FormSubmit/index.tsx delete mode 100644 src/components/FormSubmit/types.ts diff --git a/src/components/FormSubmit/index.native.tsx b/src/components/FormSubmit/index.native.tsx deleted file mode 100644 index 5eae7b51d988..000000000000 --- a/src/components/FormSubmit/index.native.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import type {FormSubmitProps, FormSubmitRef} from './types'; - -function FormSubmit({style, children}: FormSubmitProps, ref: FormSubmitRef) { - return ( - - {children} - - ); -} - -FormSubmit.displayName = 'FormSubmit'; - -export default React.forwardRef(FormSubmit); diff --git a/src/components/FormSubmit/index.tsx b/src/components/FormSubmit/index.tsx deleted file mode 100644 index 2ccd006bf322..000000000000 --- a/src/components/FormSubmit/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import type {KeyboardEvent} from 'react'; -import React, {useEffect} from 'react'; -import {View} from 'react-native'; -import * as ComponentUtils from '@libs/ComponentUtils'; -import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; -import CONST from '@src/CONST'; -import type {FormSubmitProps, FormSubmitRef} from './types'; - -function FormSubmit({children, onSubmit, style}: FormSubmitProps, ref: FormSubmitRef) { - /** - * Calls the submit callback when ENTER is pressed on a form element. - */ - const submitForm = (event: KeyboardEvent) => { - // ENTER is pressed with modifier key or during text composition, do not submit the form - if (event.shiftKey || event.key !== CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey || isEnterWhileComposition(event)) { - return; - } - - const eventTarget = event.target as HTMLElement; - - const tagName = eventTarget?.tagName ?? ''; - - // ENTER is pressed on INPUT or SELECT element, call the submit callback. - if (tagName === 'INPUT' || tagName === 'SELECT') { - onSubmit(); - return; - } - - // Pressing Enter on TEXTAREA element adds a new line. When `dataset.submitOnEnter` prop is passed, call the submit callback. - if (tagName === 'TEXTAREA' && (eventTarget?.dataset?.submitOnEnter ?? 'false') === 'true') { - event.preventDefault(); - onSubmit(); - return; - } - - // ENTER is pressed on checkbox element, call the submit callback. - if (eventTarget?.role === 'checkbox') { - onSubmit(); - } - }; - - const preventDefaultFormBehavior = (e: SubmitEvent) => e.preventDefault(); - - useEffect(() => { - if (!(ref && 'current' in ref)) { - return; - } - - const form = ref.current as HTMLFormElement | null; - - if (!form) { - return; - } - - // Prevent the browser from applying its own validation, which affects the email input - form.setAttribute('novalidate', ''); - - form.addEventListener('submit', preventDefaultFormBehavior); - - return () => { - if (!form) { - return; - } - - form.removeEventListener('submit', preventDefaultFormBehavior); - }; - }, [ref]); - - return ( - // React-native-web prevents event bubbling on TextInput for key presses - // https://github.com/necolas/react-native-web/blob/fa47f80d34ee6cde2536b2a2241e326f84b633c4/packages/react-native-web/src/exports/TextInput/index.js#L272 - // Thus use capture phase. - - {children} - - ); -} - -FormSubmit.displayName = 'FormSubmitWithRef'; - -export default React.forwardRef(FormSubmit); diff --git a/src/components/FormSubmit/types.ts b/src/components/FormSubmit/types.ts deleted file mode 100644 index 722a3fbf746e..000000000000 --- a/src/components/FormSubmit/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type {ForwardedRef} from 'react'; -import type React from 'react'; -import type {StyleProp, View, ViewStyle} from 'react-native'; - -type FormSubmitProps = { - children: React.ReactNode; - onSubmit: () => void; - style?: StyleProp; -}; - -type FormSubmitRef = ForwardedRef; - -export type {FormSubmitProps, FormSubmitRef}; diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index d19d835d68bb..a31aa0073bdc 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -50,7 +50,6 @@ function BaseTextInput( hint = '', onInputChange = () => {}, shouldDelayFocus = false, - submitOnEnter = false, multiline = false, shouldInterceptSwipe = false, autoCorrect = true, @@ -363,9 +362,6 @@ function BaseTextInput( selection={inputProps.selection} readOnly={isReadOnly} defaultValue={defaultValue} - // FormSubmit Enter key handler does not have access to direct props. - // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && submitOnEnter}} /> {inputProps.isLoading && ( {}, shouldDelayFocus = false, - submitOnEnter = false, multiline = false, shouldInterceptSwipe = false, autoCorrect = true, @@ -383,9 +382,6 @@ function BaseTextInput( selection={inputProps.selection} readOnly={isReadOnly} defaultValue={defaultValue} - // FormSubmit Enter key handler does not have access to direct props. - // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && submitOnEnter}} /> {inputProps.isLoading && ( Date: Sat, 13 Jan 2024 18:46:28 +0530 Subject: [PATCH 082/446] small bug fix --- src/components/Form/FormProvider.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 361a0f096f8d..6ffe63892a01 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -270,6 +270,8 @@ const FormProvider = forwardRef( } : {}; + const isMultiline = propsToParse.multiline || propsToParse.autoGrowHeight; + const errorFields = lodashGet(formState, 'errorFields', {}); const fieldErrorMessage = _.chain(errorFields[inputID]) @@ -283,6 +285,7 @@ const FormProvider = forwardRef( return { ...propsToParse, returnKeyType: shouldSubmitEdit ? 'go' : propsToParse.returnKeyType, + blurOnSubmit: (isMultiline && shouldSubmitEdit) || propsToParse.blurOnSubmit, ...onSubmitEditingObject, ref: typeof propsToParse.ref === 'function' From 7f0d3c4f66378c07a7f6ca862fd03fbc2242d6ce Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 4 Jan 2024 10:42:54 +0100 Subject: [PATCH 083/446] [TS migration] Migrate 'CommunicationsLink.js' component to TypeScript --- src/components/CommunicationsLink.js | 51 --------------------------- src/components/CommunicationsLink.tsx | 42 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 51 deletions(-) delete mode 100644 src/components/CommunicationsLink.js create mode 100644 src/components/CommunicationsLink.tsx diff --git a/src/components/CommunicationsLink.js b/src/components/CommunicationsLink.js deleted file mode 100644 index 01ae0354a66d..000000000000 --- a/src/components/CommunicationsLink.js +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import Clipboard from '@libs/Clipboard'; -import ContextMenuItem from './ContextMenuItem'; -import * as Expensicons from './Icon/Expensicons'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const propTypes = { - /** Children to wrap in CommunicationsLink. */ - children: PropTypes.node.isRequired, - - /** Styles to be assigned to Container */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Value to be copied or passed via tap. */ - value: PropTypes.string.isRequired, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - containerStyles: [], -}; - -function CommunicationsLink(props) { - const styles = useThemeStyles(); - return ( - - - {props.children} - Clipboard.setString(props.value)} - /> - - - ); -} - -CommunicationsLink.propTypes = propTypes; -CommunicationsLink.defaultProps = defaultProps; -CommunicationsLink.displayName = 'CommunicationsLink'; - -export default withLocalize(CommunicationsLink); diff --git a/src/components/CommunicationsLink.tsx b/src/components/CommunicationsLink.tsx new file mode 100644 index 000000000000..646326e0a632 --- /dev/null +++ b/src/components/CommunicationsLink.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Clipboard from '@libs/Clipboard'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import ContextMenuItem from './ContextMenuItem'; +import * as Expensicons from './Icon/Expensicons'; + +type CommunicationsLinkProps = ChildrenProps & { + /** Styles to be assigned to Container */ + containerStyles?: StyleProp; + + /** Value to be copied or passed via tap. */ + value: string; +}; + +function CommunicationsLink({value, containerStyles, children}: CommunicationsLinkProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + {children} + Clipboard.setString(value)} + /> + + + ); +} + +CommunicationsLink.displayName = 'CommunicationsLink'; + +export default CommunicationsLink; From 37bbacc60158ac87db4c357bbffc868244197264 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Mon, 15 Jan 2024 13:14:36 -0800 Subject: [PATCH 084/446] Feedback: Don't use Maybe type --- src/libs/ViolationsUtils.ts | 4 ++-- src/types/onyx/PolicyTag.ts | 10 ++++------ src/types/onyx/index.ts | 3 +-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index 09b3b0632723..dd128a68c703 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -2,7 +2,7 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {MaybePolicyTagList, PolicyCategories, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {PolicyTagList, PolicyCategories, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Phrase, PhraseParameters} from './Localize'; const ViolationsUtils = { @@ -14,7 +14,7 @@ const ViolationsUtils = { updatedTransaction: Transaction, transactionViolations: TransactionViolation[], policyRequiresTags: boolean, - policyTagList: MaybePolicyTagList, + policyTagList: PolicyTagList, policyRequiresCategories: boolean, policyCategories: PolicyCategories, ): { diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index ca8545775a5c..7c3636551746 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -12,6 +12,8 @@ type PolicyTag = { type PolicyTags = Record; +// When queried from Onyx, if there is no matching policy tag list, the data +// returned will be an empty object, represented by Record. type PolicyTagList = Record< T, { @@ -23,10 +25,6 @@ type PolicyTagList = Record< tags: PolicyTags; } ->; - -// When queried from Onyx, if there is no matching policy tag list, the data -// returned will be an empty object. -type MaybePolicyTagList = PolicyTagList | Record; +> | Record; -export type {PolicyTag, PolicyTags, PolicyTagList, MaybePolicyTagList}; +export type {PolicyTag, PolicyTags, PolicyTagList}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 20cf1ae69897..e4a9123af56f 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,7 +31,7 @@ import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type PolicyReportField from './PolicyReportField'; -import type {MaybePolicyTagList, PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; +import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -104,7 +104,6 @@ export type { PolicyTag, PolicyTags, PolicyTagList, - MaybePolicyTagList, PrivatePersonalDetails, RecentWaypoint, RecentlyUsedCategories, From 0a3e81856b1a2550ca714d22b13fbb5a7779a508 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Mon, 15 Jan 2024 13:21:48 -0800 Subject: [PATCH 085/446] Ensure an array is always passed to getViolationsOnyxData --- src/libs/actions/IOU.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ab3bf0882e54..ff16336b97dd 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1057,7 +1057,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; if (policy && policy.id) { optimisticData.push( - ViolationsUtils.getViolationsOnyxData(updatedTransaction, currentTransactionViolations, policy.requiresTag, policyTags, policy.requiresCategory, policyCategories), + ViolationsUtils.getViolationsOnyxData(updatedTransaction, currentTransactionViolations || [], policy.requiresTag, policyTags, policy.requiresCategory, policyCategories), ); } @@ -2435,7 +2435,7 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; const updatedViolationsOnyxData = ViolationsUtils.getViolationsOnyxData( updatedTransaction, - currentTransactionViolations, + currentTransactionViolations || [], policy.requiresTag, policyTags, policy.requiresCategory, From 8d51fa2ff527f1d494e224f62aae59af3034c7af Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 16 Jan 2024 18:18:04 +0530 Subject: [PATCH 086/446] make submitOnEnter use consistent --- src/pages/EditRequestDescriptionPage.js | 4 ++-- src/pages/iou/MoneyRequestDescriptionPage.js | 4 ++-- src/pages/iou/request/step/IOURequestStepDescription.js | 4 ++-- src/pages/tasks/NewTaskDescriptionPage.js | 4 ++-- src/pages/tasks/NewTaskDetailsPage.js | 4 ++-- src/pages/tasks/TaskDescriptionPage.js | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pages/EditRequestDescriptionPage.js b/src/pages/EditRequestDescriptionPage.js index 9b2a9e465746..b459c17a3ee3 100644 --- a/src/pages/EditRequestDescriptionPage.js +++ b/src/pages/EditRequestDescriptionPage.js @@ -9,7 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -77,7 +77,7 @@ function EditRequestDescriptionPage({defaultDescription, onSubmit}) { }} autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} /> diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index fe3100b8c3bd..d98eed9c7727 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -12,8 +12,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as IOU from '@libs/actions/IOU'; -import * as Browser from '@libs/Browser'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; @@ -142,7 +142,7 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { }} autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} /> diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.js index 849f3276667e..8eb060c925ca 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.js +++ b/src/pages/iou/request/step/IOURequestStepDescription.js @@ -8,7 +8,7 @@ import TextInput from '@components/TextInput'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; @@ -107,7 +107,7 @@ function IOURequestStepDescription({ autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} inputStyle={[styles.verticalAlignTop]} - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} /> diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js index b11e7c163755..06c8ed97bb48 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.js +++ b/src/pages/tasks/NewTaskDescriptionPage.js @@ -11,7 +11,7 @@ import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; @@ -79,7 +79,7 @@ function NewTaskDescriptionPage(props) { updateMultilineInputRange(el); }} autoGrowHeight - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} containerStyles={[styles.autoGrowHeightMultilineInput]} /> diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index 3dab58dfad04..b2a76eb07ce3 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -11,7 +11,7 @@ import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -111,7 +111,7 @@ function NewTaskDetailsPage(props) { label={props.translate('newTaskPage.descriptionOptional')} accessibilityLabel={props.translate('newTaskPage.descriptionOptional')} autoGrowHeight - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} containerStyles={[styles.autoGrowHeightMultilineInput]} defaultValue={parser.htmlToMarkdown(parser.replace(taskDescription))} value={taskDescription} diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index 3a6999d4408a..10205c65eacf 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -15,7 +15,7 @@ import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; @@ -126,7 +126,7 @@ function TaskDescriptionPage(props) { updateMultilineInputRange(inputRef.current); }} autoGrowHeight - submitOnEnter={!Browser.isMobile()} + submitOnEnter={!canUseTouchScreen()} containerStyles={[styles.autoGrowHeightMultilineInput]} /> From 3854a9a7d0e78fd4969768ac5d2f805b096089d4 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 16 Jan 2024 18:19:05 +0530 Subject: [PATCH 087/446] fix prettier --- src/pages/iou/MoneyRequestDescriptionPage.js | 2 +- src/pages/iou/request/step/IOURequestStepDescription.js | 2 +- src/pages/tasks/NewTaskDescriptionPage.js | 2 +- src/pages/tasks/NewTaskDetailsPage.js | 2 +- src/pages/tasks/TaskDescriptionPage.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index d98eed9c7727..7d3c31ca12ba 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -12,8 +12,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as IOU from '@libs/actions/IOU'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.js index 8eb060c925ca..7473239d92b5 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.js +++ b/src/pages/iou/request/step/IOURequestStepDescription.js @@ -8,8 +8,8 @@ import TextInput from '@components/TextInput'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as IOU from '@userActions/IOU'; diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js index 06c8ed97bb48..7620afaf2dc8 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.js +++ b/src/pages/tasks/NewTaskDescriptionPage.js @@ -11,8 +11,8 @@ import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as Task from '@userActions/Task'; diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index b2a76eb07ce3..adf26820f7b9 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -11,8 +11,8 @@ import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as Task from '@userActions/Task'; diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index 10205c65eacf..db8974632ab7 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -15,8 +15,8 @@ import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import compose from '@libs/compose'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; From c02df48b2368b969542700a494a6659b4c618d91 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 16 Jan 2024 17:13:10 +0000 Subject: [PATCH 088/446] refactor(typescript): apply pull request feedback --- src/components/MoneyRequestHeader.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 2f20fe808e59..f3c38b752542 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -63,10 +63,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const isActionOwner = parentReportAction.actorAccountID === (session?.accountID ?? null); const deleteTransaction = useCallback(() => { - const { - originalMessage: {IOUTransactionID = ''}, - } = parentReportAction; - IOU.deleteMoneyRequest(IOUTransactionID, parentReportAction, true); + IOU.deleteMoneyRequest(parentReportAction.originalMessage?.IOUTransactionID ?? '', parentReportAction, true); setIsDeleteModalVisible(false); }, [parentReportAction, setIsDeleteModalVisible]); From 8b77219fd3d973c32f96ba46dba2184540e64603 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 16 Jan 2024 18:22:50 +0100 Subject: [PATCH 089/446] fix comments --- src/ONYXKEYS.ts | 2 +- src/libs/CardUtils.ts | 8 ++++---- src/libs/GetPhysicalCardUtils.ts | 2 +- src/libs/actions/FormActions.ts | 2 +- src/libs/actions/Wallet.ts | 7 +------ src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 10 +++++----- .../settings/Wallet/Card/GetPhysicalCardAddress.tsx | 2 +- .../settings/Wallet/Card/GetPhysicalCardConfirm.tsx | 2 +- src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx | 6 +++--- .../settings/Wallet/Card/GetPhysicalCardPhone.tsx | 4 ++-- src/types/onyx/index.ts | 2 +- 11 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index bb0fdc16188b..3353651fbfc3 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -526,7 +526,7 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.GetPhysicalCardForm; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.GetPhysicalCardForm | undefined; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.GetPhysicalCardForm; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 11b68244fbe1..ba7809bba907 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1,10 +1,10 @@ import lodash from 'lodash'; import Onyx from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx/lib/types'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import CONST from '@src/CONST'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card} from '@src/types/onyx'; +import type {Card, CardList} from '@src/types/onyx'; import * as Localize from './Localize'; let allCards: OnyxValues[typeof ONYXKEYS.CARD_LIST] = {}; @@ -73,11 +73,11 @@ function getYearFromExpirationDateString(expirationDateString: string) { * @param cardList - collection of assigned cards * @returns collection of assigned cards grouped by domain */ -function getDomainCards(cardList: OnyxCollection) { +function getDomainCards(cardList: OnyxEntry): Record { // Check for domainName to filter out personal credit cards. const activeCards = Object.values(cardList ?? {}).filter((card) => !!card?.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.some((element) => element === card.state)); - return lodash.groupBy(activeCards, (card) => card?.domainName); + return lodash.groupBy(activeCards, (card) => card.domainName as string); } /** diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index 77a0b8d26708..82d991efb3aa 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -76,7 +76,7 @@ function getUpdatedDraftValues(draftValues: OnyxEntry, priv * @param draftValues * @returns */ -function getUpdatedPrivatePersonalDetails(draftValues: OnyxEntry): PrivatePersonalDetails { +function getUpdatedPrivatePersonalDetails(draftValues: OnyxEntry): PrivatePersonalDetails { const {addressLine1, addressLine2, city = '', country = '', legalFirstName, legalLastName, phoneNumber, state = '', zipPostCode = ''} = draftValues ?? {}; return { legalFirstName, diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index 6b73636e6d82..e0275d717472 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -28,7 +28,7 @@ function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDee * @param formID */ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { - Onyx.merge(FormUtils.getDraftKey(formID), undefined); + Onyx.set(FormUtils.getDraftKey(formID), {}); } export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index bc2fb518d8e6..8486402d2799 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -286,12 +286,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str } function requestPhysicalExpensifyCard(cardID: number, authToken: string, privatePersonalDetails: PrivatePersonalDetails) { - const { - legalFirstName, - legalLastName, - phoneNumber, - address: {city, country, state, street, zip}, - } = privatePersonalDetails; + const {legalFirstName = '', legalLastName = '', phoneNumber = '', address: {city = '', country = '', state = '', street = '', zip = ''} = {}} = privatePersonalDetails; type RequestPhysicalExpensifyCardParams = { authToken: string; diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 3877ec4c9009..96af02d64dd7 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -34,7 +34,7 @@ type BaseGetPhysicalCardOnyxProps = { privatePersonalDetails: OnyxEntry; /** Draft values used by the get physical card form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; /** Session info for the currently logged in user. */ session: OnyxEntry; @@ -72,7 +72,6 @@ type BaseGetPhysicalCardProps = BaseGetPhysicalCardOnyxProps & { onValidate?: OnValidate; }; - function DefaultRenderContent({onSubmit, submitButtonText, children, onValidate}: RenderContentProps) { const styles = useThemeStyles(); @@ -87,7 +86,7 @@ function DefaultRenderContent({onSubmit, submitButtonText, children, onValidate} > {children} - ) + ); } function BaseGetPhysicalCard({ @@ -150,8 +149,9 @@ function BaseGetPhysicalCard({ if (isConfirmation) { const domainCards = CardUtils.getDomainCards(cardList)[domain]; const virtualCard = domainCards.find((card) => card?.isVirtual); - const cardID = virtualCard?.cardID ?? ''; - Wallet.requestPhysicalExpensifyCard(cardID, session?.authToken, updatedPrivatePersonalDetails); + const cardID = virtualCard?.cardID ?? 0; + + Wallet.requestPhysicalExpensifyCard(cardID, session?.authToken ?? '', updatedPrivatePersonalDetails); // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx index 578a36afc31f..849e37835528 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardAddress.tsx @@ -15,7 +15,7 @@ import type {RenderContentProps} from './BaseGetPhysicalCard'; type GetPhysicalCardAddressOnyxProps = { /** Draft values used by the get physical card form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; }; type GetPhysicalCardAddressProps = GetPhysicalCardAddressOnyxProps & StackScreenProps; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx index 967d919b239c..af4343d67af0 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardConfirm.tsx @@ -31,7 +31,7 @@ const goToGetPhysicalCardAddress = (domain: string) => { type GetPhysicalCardConfirmOnyxProps = { /** Draft values used by the get physical card form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; }; type GetPhysicalCardConfirmProps = GetPhysicalCardConfirmOnyxProps & StackScreenProps; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx index 4f9996a2feb5..6604f08ed015 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx @@ -1,8 +1,8 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; -import InputWrapper from '@components/Form/InputWrapper'; import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import InputWrapper from '@components/Form/InputWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -22,7 +22,7 @@ type OnValidateResult = { type GetPhysicalCardNameOnyxProps = { /** Draft values used by the get physical card form */ - draftValues: OnyxEntry; + draftValues: OnyxEntry; }; type GetPhysicalCardNameProps = GetPhysicalCardNameOnyxProps & StackScreenProps; @@ -77,7 +77,7 @@ function GetPhysicalCardName({ defaultValue={legalFirstName} shouldSaveDraft /> - ; + draftValues: OnyxEntry; }; type GetPhysicalCardPhoneProps = GetPhysicalCardPhoneOnyxProps & StackScreenProps; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e477567ee3b8..901476159bbd 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -31,8 +31,8 @@ import type Policy from './Policy'; import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; -import type {PolicyTag, PolicyTags} from './PolicyTag'; import type PolicyReportField from './PolicyReportField'; +import type {PolicyTag, PolicyTags} from './PolicyTag'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; From 36492af9f46ced30292587ce7b13802d480128a5 Mon Sep 17 00:00:00 2001 From: dhairyasenjaliya Date: Tue, 16 Jan 2024 23:15:08 +0530 Subject: [PATCH 090/446] Fix to display both error validation --- src/pages/tasks/NewTaskDetailsPage.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index f57cca7799ca..8a1357efa9ae 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -60,7 +60,8 @@ function NewTaskDetailsPage(props) { ErrorUtils.addErrorMessage(errors, 'taskTitle', 'newTaskPage.pleaseEnterTaskName'); } else if (values.taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) { ErrorUtils.addErrorMessage(errors, 'taskTitle', ['common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); - } else if (values.taskDescription.length > CONST.SUPPORTING_CHARACTER_LIMIT) { + } + if (values.taskDescription.length > CONST.SUPPORTING_CHARACTER_LIMIT) { ErrorUtils.addErrorMessage(errors, 'taskDescription', [ 'common.error.characterLimitExceedCounter', {length: values.taskDescription.length, limit: CONST.SUPPORTING_CHARACTER_LIMIT}, From 04d2c446e47d8fb86d86690a06b5df12ad3a80a5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 16 Jan 2024 18:45:31 +0100 Subject: [PATCH 091/446] cover all basic scenarios --- src/libs/NextStepUtils.ts | 254 +++++++++---- tests/unit/NextStepUtilsTest.ts | 612 +++++++++++++++++--------------- 2 files changed, 512 insertions(+), 354 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index c45ccec43e24..901f1ffee96e 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -5,6 +5,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, ReportNextStep} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportNextStep'; import EmailUtils from './EmailUtils'; +import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as ReportUtils from './ReportUtils'; let currentUserAccountID: number | undefined; @@ -44,108 +45,229 @@ function parseMessage(messages: Message[] | undefined) { return `${formattedHtml}`; } +type BuildNextStepParameters = { + isPaidWithWallet?: boolean; +}; + /** + * Generates an optimistic nextStep based on a current report status and other properties. * * @param report - * @param isPaidWithWallet - Whether a report has been paid with wallet or outside of Expensify - * @returns next step + * @param parameters + * @param parameters.isPaidWithWallet - Whether a report has been paid with wallet or outside of Expensify + * @returns nextStep */ -function buildNextStep(report: Report, isPaidWithWallet = false): ReportNextStep | null { +function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const { statusNum = CONST.REPORT.STATUS.OPEN, // TODO: Clarify default value isPreventSelfApprovalEnabled = false, + ownerAccountID = -1, } = report; const policy = ReportUtils.getPolicy(report.policyID ?? ''); - const isSelfApproval = policy.submitsTo === currentUserAccountID; + const isOwner = currentUserAccountID === ownerAccountID; + const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; + const isSelfApproval = currentUserAccountID === policy.submitsTo; const submitterDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(policy.submitsTo, true) ?? ''; const type: ReportNextStep['type'] = 'neutral'; let optimisticNextStep: ReportNextStep | null; switch (statusNum) { - case CONST.REPORT.STATUS.OPEN: { - const message = [ - { - text: 'Waiting for', - }, - { - text: submitterDisplayName, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'submit', - type: 'strong', - }, - { - text: 'these expenses.', - }, - ]; - const preventedSelfApprovalMessage = [ - { - text: "Oops! Looks like you're submitting to ", - }, - { - text: 'yourself', - type: 'strong', - }, - { - text: '. Approving your own reports is ', - }, - { - text: 'forbidden', - type: 'strong', - }, - { - text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', - }, - ]; - + // Generates an optimistic nextStep once a report has been opened + case CONST.REPORT.STATUS.OPEN: + // Self review optimisticNextStep = { type, title: 'Next Steps:', - message: isPreventSelfApprovalEnabled && isSelfApproval ? preventedSelfApprovalMessage : message, + message: [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: ' these expenses. This report may be selected at random for manual approval.', + }, + ], }; + + // TODO: Clarify date + // Scheduled submit enabled + if (policy.isHarvestingEnabled) { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + } + + // Prevented self submitting + if (isPreventSelfApprovalEnabled && isSelfApproval) { + optimisticNextStep.message = [ + { + text: "Oops! Looks like you're submitting to ", + }, + { + text: 'yourself', + type: 'strong', + }, + { + text: '. Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', + }, + ]; + } + break; - } + // Generates an optimistic nextStep once a report has been submitted case CONST.REPORT.STATUS.SUBMITTED: + // Self review & another reviewer optimisticNextStep = { type, title: 'Next Steps:', - message: [{text: 'Waiting for'}, {text: submitterDisplayName, type: 'strong'}, {text: 'to'}, {text: 'review', type: 'strong'}, {text: ' %expenses.'}], + message: [ + { + text: 'Waiting for ', + }, + { + text: submitterDisplayName, + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ], }; + + // Another owner + if (!isOwner) { + optimisticNextStep.message = [ + { + text: ownerLogin, + type: 'strong', + }, + { + text: ' is waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' these %expenses.', + }, + ]; + } + break; - case CONST.REPORT.STATUS.APPROVED: { - const message = [ - { - text: isSelfApproval ? Str.recapitalize(submitterDisplayName) : submitterDisplayName, - type: 'strong', - }, - { - text: 'have marked these expenses as', - }, - { - text: 'paid', - type: 'strong', - }, - ]; - - if (!isPaidWithWallet) { - message.push({text: 'outside of Expensify.'}); + // Generates an optimistic nextStep once a report has been approved + case CONST.REPORT.STATUS.APPROVED: + // Self review + optimisticNextStep = { + type, + title: 'Next Steps:', + message: [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ], + }; + + // Another owner + if (!isOwner) { + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: 'No further action required!', + }, + ]; } + break; + + // Generates an optimistic nextStep once a report has been paid + case CONST.REPORT.STATUS.REIMBURSED: + // Paid with wallet optimisticNextStep = { type, title: 'Finished!', - message, + message: [ + { + text: 'You', + type: 'strong', + }, + { + text: ' have marked these expenses as ', + }, + { + text: 'paid', + type: 'strong', + }, + ], }; + + // Paid outside of Expensify + if (typeof isPaidWithWallet === 'boolean' && !isPaidWithWallet) { + optimisticNextStep.message?.push({text: ' outside of Expensify'}); + } + + optimisticNextStep.message?.push({text: '.'}); + break; - } + // Resets a nextStep default: optimisticNextStep = null; } diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 5de218911375..6fe5d1dd31f1 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -1,17 +1,35 @@ -import Str from 'expensify-common/lib/str'; +import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; -import type {ReportNextStep} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report, ReportNextStep} from '@src/types/onyx'; import * as NextStepUtils from '../../src/libs/NextStepUtils'; import * as ReportUtils from '../../src/libs/ReportUtils'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +Onyx.init({keys: ONYXKEYS}); describe('libs/NextStepUtils', () => { describe('buildNextStep', () => { - const fakeSubmitterEmail = 'submitter@expensify.com'; - const fakeSelfSubmitterEmail = 'you'; - const fakeChatReportID = '1'; - const fakePolicyID = '2'; - const fakePayeeAccountID = 3; - const report = ReportUtils.buildOptimisticExpenseReport(fakeChatReportID, fakePolicyID, fakePayeeAccountID, -500, CONST.CURRENCY.USD); + const currentUserEmail = 'current-user@expensify.com'; + const currentUserAccountID = 37; + const strangeEmail = 'stranger@expensify.com'; + const strangeAccountID = 50; + const policyID = '1'; + const policy: Policy = { + // Important props + id: policyID, + owner: currentUserEmail, + submitsTo: currentUserAccountID, + isHarvestingEnabled: false, + // Required props + name: 'Policy', + role: 'admin', + type: 'team', + outputCurrency: CONST.CURRENCY.USD, + areChatRoomsEnabled: true, + isPolicyExpenseChatEnabled: true, + }; + const report = ReportUtils.buildOptimisticExpenseReport('fake-chat-report-id-1', policyID, 1, -500, CONST.CURRENCY.USD) as Report; const optimisticNextStep: ReportNextStep = { type: 'neutral', @@ -19,297 +37,315 @@ describe('libs/NextStepUtils', () => { message: [], }; + beforeAll(() => { + // @ts-expect-error Preset necessary values + Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID: currentUserAccountID}, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]: policy, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: { + [strangeAccountID]: { + accountID: strangeAccountID, + login: strangeEmail, + avatar: '', + }, + }, + }).then(waitForBatchedUpdates); + }); + beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS.OPEN; + report.ownerAccountID = currentUserAccountID; optimisticNextStep.title = ''; optimisticNextStep.message = []; }); - it('generates an optimistic nextStep once a report has been opened', () => { - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'submit', - type: 'strong', - }, - { - text: 'these expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); + describe('it generates an optimistic nextStep once a report has been opened', () => { + beforeEach(() => { + report.statusNum = CONST.REPORT.STATUS.OPEN; + }); + + test('self review', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: ' these expenses. This report may be selected at random for manual approval.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + // TODO: Clarify date + test('scheduled submit enabled', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('prevented self submitting', () => { + report.isPreventSelfApprovalEnabled = true; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: "Oops! Looks like you're submitting to ", + }, + { + text: 'yourself', + type: 'strong', + }, + { + text: '. Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', + }, + ]; + + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + submitsTo: currentUserAccountID, + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); }); - it('generates an optimistic nextStep once a report has been self opened', () => { - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSelfSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'submit', - type: 'strong', - }, - { - text: 'these expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); + describe('it generates an optimistic nextStep once a report has been submitted', () => { + beforeEach(() => { + report.statusNum = CONST.REPORT.STATUS.SUBMITTED; + }); + + test('self review', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + test('another reviewer', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: strangeEmail, + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + submitsTo: strangeAccountID, + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('another owner', () => { + report.ownerAccountID = strangeAccountID; + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: strangeEmail, + type: 'strong', + }, + { + text: ' is waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' these %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); }); - it('generates an optimistic nextStep once a report has been opened with prevented self submitting', () => { - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: "Oops! Looks like you're submitting to ", - }, - { - text: 'yourself', - type: 'strong', - }, - { - text: '. Approving your own reports is ', - }, - { - text: 'forbidden', - type: 'strong', - }, - { - text: ' by your policy. Please submit this report to someone else or contact your admin to change the person you submit to.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been submitted', () => { - report.statusNum = CONST.REPORT.STATUS.SUBMITTED; - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'review', - type: 'strong', - }, - { - text: ' %expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); + describe('it generates an optimistic nextStep once a report has been approved', () => { + beforeEach(() => { + report.statusNum = CONST.REPORT.STATUS.APPROVED; + }); + + test('self review', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'review', + type: 'strong', + }, + { + text: ' %expenses.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + test('another owner', () => { + report.ownerAccountID = strangeAccountID; + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: 'No further action required!', + }, + ]; + + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); }); - it('generates an optimistic nextStep once a report has been self submitted', () => { - report.statusNum = CONST.REPORT.STATUS.SUBMITTED; - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSelfSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'review', - type: 'strong', - }, - { - text: ' %expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been approved', () => { - report.statusNum = CONST.REPORT.STATUS.APPROVED; - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'review', - type: 'strong', - }, - { - text: ' %expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been self approved', () => { - report.statusNum = CONST.REPORT.STATUS.APPROVED; - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for', - }, - { - text: fakeSelfSubmitterEmail, - type: 'strong', - }, - { - text: 'to', - }, - { - text: 'review', - type: 'strong', - }, - { - text: ' %expenses.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been paid with wallet', () => { - report.statusNum = CONST.REPORT.STATUS.REIMBURSED; - optimisticNextStep.title = 'Finished!'; - optimisticNextStep.message = [ - { - text: fakeSubmitterEmail, - type: 'strong', - }, - { - text: 'have marked these expenses as', - }, - { - text: 'paid', - type: 'strong', - }, - ]; - - const result = NextStepUtils.buildNextStep(report, true); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been self paid with wallet', () => { - report.statusNum = CONST.REPORT.STATUS.REIMBURSED; - optimisticNextStep.title = 'Finished!'; - optimisticNextStep.message = [ - { - text: Str.recapitalize(fakeSelfSubmitterEmail), - type: 'strong', - }, - { - text: 'have marked these expenses as', - }, - { - text: 'paid', - type: 'strong', - }, - ]; - - const result = NextStepUtils.buildNextStep(report, true); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been paid outside of Expensify', () => { - report.statusNum = CONST.REPORT.STATUS.REIMBURSED; - optimisticNextStep.title = 'Finished!'; - optimisticNextStep.message = [ - { - text: fakeSubmitterEmail, - type: 'strong', - }, - { - text: 'have marked these expenses as', - }, - { - text: 'paid', - type: 'strong', - }, - { - text: 'outside of Expensify.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); - }); - - it('generates an optimistic nextStep once a report has been paid self outside of Expensify', () => { - report.statusNum = CONST.REPORT.STATUS.REIMBURSED; - optimisticNextStep.title = 'Finished!'; - optimisticNextStep.message = [ - { - text: Str.recapitalize(fakeSelfSubmitterEmail), - type: 'strong', - }, - { - text: 'have marked these expenses as', - }, - { - text: 'paid', - type: 'strong', - }, - { - text: 'outside of Expensify.', - }, - ]; - - const result = NextStepUtils.buildNextStep(report); - - expect(result).toStrictEqual(optimisticNextStep); + describe('it generates an optimistic nextStep once a report has been paid', () => { + beforeEach(() => { + report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + }); + + test('paid with wallet', () => { + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: 'You', + type: 'strong', + }, + { + text: ' have marked these expenses as ', + }, + { + text: 'paid', + type: 'strong', + }, + { + text: '.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report, {isPaidWithWallet: true}); + + expect(result).toStrictEqual(optimisticNextStep); + }); + + test('paid outside of Expensify', () => { + optimisticNextStep.title = 'Finished!'; + optimisticNextStep.message = [ + { + text: 'You', + type: 'strong', + }, + { + text: ' have marked these expenses as ', + }, + { + text: 'paid', + type: 'strong', + }, + { + text: ' outside of Expensify', + }, + { + text: '.', + }, + ]; + + const result = NextStepUtils.buildNextStep(report, {isPaidWithWallet: false}); + + expect(result).toStrictEqual(optimisticNextStep); + }); }); }); }); From 69a50b1f2379e331bf694bcbced96486c9e08e19 Mon Sep 17 00:00:00 2001 From: dhairyasenjaliya Date: Wed, 17 Jan 2024 00:45:52 +0530 Subject: [PATCH 092/446] Fix legal name errors --- src/pages/settings/Profile/PersonalDetails/LegalNamePage.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js index c96bc41ec0e1..61d9a5a8aa8f 100644 --- a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js +++ b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js @@ -59,8 +59,7 @@ function LegalNamePage(props) { ErrorUtils.addErrorMessage(errors, 'legalFirstName', 'privatePersonalDetails.error.hasInvalidCharacter'); } else if (_.isEmpty(values.legalFirstName)) { errors.legalFirstName = 'common.error.fieldRequired'; - } - if (values.legalFirstName.length > CONST.TITLE_CHARACTER_LIMIT) { + } else if (values.legalFirstName.length > CONST.TITLE_CHARACTER_LIMIT) { ErrorUtils.addErrorMessage(errors, 'legalFirstName', ['common.error.characterLimitExceedCounter', {length: values.legalFirstName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } @@ -68,8 +67,7 @@ function LegalNamePage(props) { ErrorUtils.addErrorMessage(errors, 'legalLastName', 'privatePersonalDetails.error.hasInvalidCharacter'); } else if (_.isEmpty(values.legalLastName)) { errors.legalLastName = 'common.error.fieldRequired'; - } - if (values.legalLastName.length > CONST.TITLE_CHARACTER_LIMIT) { + } else if (values.legalLastName.length > CONST.TITLE_CHARACTER_LIMIT) { ErrorUtils.addErrorMessage(errors, 'legalLastName', ['common.error.characterLimitExceedCounter', {length: values.legalLastName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } From ede3bbe35c4564871666fea79d55b946d0a8afa4 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Tue, 16 Jan 2024 16:06:24 -0800 Subject: [PATCH 093/446] More prettier changes --- src/libs/ViolationsUtils.ts | 2 +- src/types/onyx/PolicyTag.ts | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index dd128a68c703..e24ac5277283 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -2,7 +2,7 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyTagList, PolicyCategories, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; import type {Phrase, PhraseParameters} from './Localize'; const ViolationsUtils = { diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 7c3636551746..cea555a2a0f9 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -14,17 +14,19 @@ type PolicyTags = Record; // When queried from Onyx, if there is no matching policy tag list, the data // returned will be an empty object, represented by Record. -type PolicyTagList = Record< - T, - { - /** Name of the tag list */ - name: T; +type PolicyTagList = + | Record< + T, + { + /** Name of the tag list */ + name: T; - /** Flag that determines if tags are required */ - required: boolean; + /** Flag that determines if tags are required */ + required: boolean; - tags: PolicyTags; - } -> | Record; + tags: PolicyTags; + } + > + | Record; export type {PolicyTag, PolicyTags, PolicyTagList}; From 4c08eb6fba7d4740e70ab4fbed79c1dd2c66ae2b Mon Sep 17 00:00:00 2001 From: kadiealexander <59587260+kadiealexander@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:45:06 +1300 Subject: [PATCH 094/446] Update src/components/PDFView/index.native.js Co-authored-by: 0xmiroslav <97473779+0xmiroslav@users.noreply.github.com> --- src/components/PDFView/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 5ed1655d7eab..4395f7432e91 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -44,7 +44,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); const themeStyles = useThemeStyles(); - const isKeyboardShown = useKeyboardState(); + const {isKeyboardShown} = useKeyboardState(); const StyleUtils = useStyleUtils(); useEffect(() => { From ba7057d11c79c7dabfa9bc007fb2c898be1ffd8e Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 17 Jan 2024 09:48:56 +0100 Subject: [PATCH 095/446] fix clean function --- src/libs/CardUtils.ts | 2 +- src/libs/actions/FormActions.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index ba7809bba907..1e2b5ab0be3c 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -77,7 +77,7 @@ function getDomainCards(cardList: OnyxEntry): Record { // Check for domainName to filter out personal credit cards. const activeCards = Object.values(cardList ?? {}).filter((card) => !!card?.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.some((element) => element === card.state)); - return lodash.groupBy(activeCards, (card) => card.domainName as string); + return lodash.groupBy(activeCards, (card) => card.domainName); } /** diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts index e0275d717472..c6f511c7caf1 100644 --- a/src/libs/actions/FormActions.ts +++ b/src/libs/actions/FormActions.ts @@ -28,7 +28,7 @@ function setDraftValues(formID: OnyxFormKeyWithoutDraft, draftValues: NullishDee * @param formID */ function clearDraftValues(formID: OnyxFormKeyWithoutDraft) { - Onyx.set(FormUtils.getDraftKey(formID), {}); + Onyx.set(FormUtils.getDraftKey(formID), null); } export {setDraftValues, setErrorFields, setErrors, setIsLoading, clearDraftValues}; From a32e22e830f8592f22025242d7ac4eb386f95212 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Jan 2024 12:14:17 +0100 Subject: [PATCH 096/446] rename const --- src/libs/NextStepUtils.ts | 10 +++++----- tests/unit/NextStepUtilsTest.ts | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 901f1ffee96e..e5a3afad4966 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -59,7 +59,7 @@ type BuildNextStepParameters = { */ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const { - statusNum = CONST.REPORT.STATUS.OPEN, + statusNum = CONST.REPORT.STATUS_NUM.OPEN, // TODO: Clarify default value isPreventSelfApprovalEnabled = false, ownerAccountID = -1, @@ -74,7 +74,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete switch (statusNum) { // Generates an optimistic nextStep once a report has been opened - case CONST.REPORT.STATUS.OPEN: + case CONST.REPORT.STATUS_NUM.OPEN: // Self review optimisticNextStep = { type, @@ -143,7 +143,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete break; // Generates an optimistic nextStep once a report has been submitted - case CONST.REPORT.STATUS.SUBMITTED: + case CONST.REPORT.STATUS_NUM.SUBMITTED: // Self review & another reviewer optimisticNextStep = { type, @@ -199,7 +199,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete break; // Generates an optimistic nextStep once a report has been approved - case CONST.REPORT.STATUS.APPROVED: + case CONST.REPORT.STATUS_NUM.APPROVED: // Self review optimisticNextStep = { type, @@ -238,7 +238,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete break; // Generates an optimistic nextStep once a report has been paid - case CONST.REPORT.STATUS.REIMBURSED: + case CONST.REPORT.STATUS_NUM.REIMBURSED: // Paid with wallet optimisticNextStep = { type, diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 6fe5d1dd31f1..e2637a3bdb85 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -60,7 +60,7 @@ describe('libs/NextStepUtils', () => { describe('it generates an optimistic nextStep once a report has been opened', () => { beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS.OPEN; + report.statusNum = CONST.REPORT.STATUS_NUM.OPEN; }); test('self review', () => { @@ -150,7 +150,7 @@ describe('libs/NextStepUtils', () => { describe('it generates an optimistic nextStep once a report has been submitted', () => { beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS.SUBMITTED; + report.statusNum = CONST.REPORT.STATUS_NUM.SUBMITTED; }); test('self review', () => { @@ -246,7 +246,7 @@ describe('libs/NextStepUtils', () => { describe('it generates an optimistic nextStep once a report has been approved', () => { beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS.APPROVED; + report.statusNum = CONST.REPORT.STATUS_NUM.APPROVED; }); test('self review', () => { @@ -293,7 +293,7 @@ describe('libs/NextStepUtils', () => { describe('it generates an optimistic nextStep once a report has been paid', () => { beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS.REIMBURSED; + report.statusNum = CONST.REPORT.STATUS_NUM.REIMBURSED; }); test('paid with wallet', () => { From bbe860da7e34094a932aa4538109f2234ffbce74 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Jan 2024 12:14:37 +0100 Subject: [PATCH 097/446] clear todo --- src/libs/NextStepUtils.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index e5a3afad4966..13ae204c0296 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -58,12 +58,7 @@ type BuildNextStepParameters = { * @returns nextStep */ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { - const { - statusNum = CONST.REPORT.STATUS_NUM.OPEN, - // TODO: Clarify default value - isPreventSelfApprovalEnabled = false, - ownerAccountID = -1, - } = report; + const {statusNum = CONST.REPORT.STATUS_NUM.OPEN, isPreventSelfApprovalEnabled = false, ownerAccountID = -1} = report; const policy = ReportUtils.getPolicy(report.policyID ?? ''); const isOwner = currentUserAccountID === ownerAccountID; const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; From b0ad3787dfd3a8f9b8f74f23f20d6a91fcd854ba Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 17 Jan 2024 18:50:20 +0530 Subject: [PATCH 098/446] added props to custom components --- src/components/AddressSearch/index.tsx | 4 ++++ src/components/AddressSearch/types.ts | 6 ++++++ src/components/RoomNameInput/index.js | 4 +++- src/components/RoomNameInput/index.native.js | 4 +++- src/components/RoomNameInput/roomNameInputPropTypes.js | 8 ++++++++ 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 89e87eeebe54..ddaecf94dbbd 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -42,6 +42,8 @@ function AddressSearch( onBlur, onInputChange, onPress, + onSubmitEditing, + returnKeyType, predefinedPlaces = [], preferredLocale, renamedInputKeys = { @@ -380,6 +382,8 @@ function AddressSearch( defaultValue, inputID, shouldSaveDraft, + returnKeyType, + onSubmitEditing, onFocus: () => { setIsFocused(true); }, diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 8016f1b2ea39..75d6464cd992 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -63,6 +63,12 @@ type AddressSearchProps = { /** A callback function when an address has been auto-selected */ onPress?: (props: OnPressProps) => void; + /** On submit editing handler provided by the FormProvider */ + onSubmitEditing?: () => void; + + /** Return key type provided to the TextInput */ + returnKeyType?: string; + /** Customize the TextInput container */ containerStyles?: StyleProp; diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index 61f004a47b96..e3c5a86ff945 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -6,7 +6,7 @@ import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, onSubmitEditing, returnKeyType, shouldDelayFocus}) { const {translate} = useLocalize(); const [selection, setSelection] = useState(); @@ -52,6 +52,8 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value={value.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice. selection={selection} onSelectionChange={(event) => setSelection(event.nativeEvent.selection)} + onSubmitEditing={onSubmitEditing} + returnKeyType={returnKeyType} errorText={errorText} autoCapitalize="none" onBlur={(event) => isFocused && onBlur(event)} diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.js index a2c09996ad34..bae347fca3d2 100644 --- a/src/components/RoomNameInput/index.native.js +++ b/src/components/RoomNameInput/index.native.js @@ -7,7 +7,7 @@ import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, onSubmitEditing, returnKeyType, shouldDelayFocus}) { const {translate} = useLocalize(); /** @@ -42,6 +42,8 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 onBlur={(event) => isFocused && onBlur(event)} + onSubmitEditing={onSubmitEditing} + returnKeyType={returnKeyType} autoFocus={isFocused && autoFocus} autoCapitalize="none" shouldDelayFocus={shouldDelayFocus} diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 60be8430b056..6d88c0e62793 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -17,6 +17,12 @@ const propTypes = { /** A ref forwarded to the TextInput */ forwardedRef: refPropTypes, + /** On submit editing handler provided by the FormProvider */ + onSubmitEditing: PropTypes.func, + + /** Return key type provided to the TextInput */ + returnKeyType: PropTypes.string, + /** The ID used to uniquely identify the input in a Form */ inputID: PropTypes.string, @@ -39,6 +45,8 @@ const defaultProps = { disabled: false, errorText: '', forwardedRef: () => {}, + onSubmitEditing: () => {}, + returnKeyType: undefined, inputID: undefined, onBlur: () => {}, From 9387b10f451560091d7655a97dcac1af87163c5a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Jan 2024 17:35:13 +0100 Subject: [PATCH 099/446] integrate timestamp handling --- src/libs/NextStepUtils.ts | 57 +++++++-- src/types/onyx/Policy.ts | 3 + tests/unit/NextStepUtilsTest.ts | 200 ++++++++++++++++++++++++++++---- 3 files changed, 231 insertions(+), 29 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 13ae204c0296..8c44b84ed734 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,3 +1,4 @@ +import {format, lastDayOfMonth} from 'date-fns'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; @@ -90,28 +91,66 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete type: 'strong', }, { - text: ' these expenses. This report may be selected at random for manual approval.', + text: ' these expenses.', }, ], }; - // TODO: Clarify date // Scheduled submit enabled if (policy.isHarvestingEnabled) { optimisticNextStep.message = [ { text: 'These expenses are scheduled to ', }, - { - text: 'automatically submit!', - type: 'strong', - }, - { - text: ' No further action required!', - }, ]; + + if (policy.autoReportingFrequency) { + const currentDate = new Date(); + + let autoSubmissionDate: string; + + if (policy.autoReportingOffset === 'lastDayOfMonth') { + const currentDateWithLastDayOfMonth = lastDayOfMonth(currentDate); + + autoSubmissionDate = format(currentDateWithLastDayOfMonth, 'do'); + } else if (policy.autoReportingOffset === 'lastBusinessDayOfMonth') { + // TODO: Get from the backend + // const currentLastBusinessDayOfMonth = + autoSubmissionDate = ''; + } else if (Number.isNaN(Number(policy.autoReportingOffset))) { + autoSubmissionDate = ''; + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + autoSubmissionDate = format(currentDate.setDate(+policy.autoReportingOffset!), 'do'); + } + + const harvestingSuffixes = { + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE]: 'later today', + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY]: 'on Sunday', + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY]: 'on the 1st and 16th of each month', + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY]: `on the ${autoSubmissionDate} of each month`, + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP]: 'at the end of your trip', + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL]: '', + }; + + optimisticNextStep.message.push({ + text: `automatically submit ${harvestingSuffixes[policy.autoReportingFrequency]}!`, + type: 'strong', + }); + } else { + optimisticNextStep.message.push({ + text: `automatically submit!`, + type: 'strong', + }); + } + + optimisticNextStep.message.push({ + text: ' No further action required!', + }); } + // TODO add "This report may be selected at random for manual approval." + // Prevented self submitting if (isPreventSelfApprovalEnabled && isSelfApproval) { optimisticNextStep.message = [ diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index da4522487a7a..0be879fbfd68 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -74,6 +74,9 @@ type Policy = { /** The scheduled submit frequency set up on the this policy */ autoReportingFrequency?: ValueOf; + /** The scheduled submission date */ + autoReportingOffset?: string; + /** Whether the scheduled submit is enabled */ isHarvestingEnabled?: boolean; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index e2637a3bdb85..33089b26ea0e 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -1,3 +1,4 @@ +import {format, lastDayOfMonth} from 'date-fns'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -81,7 +82,7 @@ describe('libs/NextStepUtils', () => { type: 'strong', }, { - text: ' these expenses. This report may be selected at random for manual approval.', + text: ' these expenses.', }, ]; @@ -90,28 +91,187 @@ describe('libs/NextStepUtils', () => { expect(result).toStrictEqual(optimisticNextStep); }); - // TODO: Clarify date - test('scheduled submit enabled', () => { + describe('scheduled submit enabled', () => { optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'These expenses are scheduled to ', - }, - { - text: 'automatically submit!', - type: 'strong', - }, - { - text: ' No further action required!', - }, - ]; - return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, - }).then(() => { - const result = NextStepUtils.buildNextStep(report); + test('daily', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit later today!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'immediate', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); - expect(result).toStrictEqual(optimisticNextStep); + test('weekly', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit on Sunday!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'weekly', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('twice a month', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit on the 1st and 16th of each month!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'semimonthly', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('monthly on the 2nd', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit on the 2nd of each month!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'monthly', + autoReportingOffset: '2', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('monthly on the last day', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: `automatically submit on the ${format(lastDayOfMonth(new Date()), 'do')} of each month!`, + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'monthly', + autoReportingOffset: 'lastDayOfMonth', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('trip', () => { + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: 'automatically submit at the end of your trip!', + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'trip', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + + test('manual', () => { + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: ' these expenses.', + }, + { + text: ' This report may be selected at random for manual approval.', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'manual', + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); }); }); From 090d8ff545547c63a2940b9e83af02ceb8ec7cba Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Jan 2024 17:42:31 +0100 Subject: [PATCH 100/446] add a case with random for manual approval --- src/libs/NextStepUtils.ts | 6 +++++- src/types/onyx/Policy.ts | 3 +++ tests/unit/NextStepUtilsTest.ts | 35 +++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 8c44b84ed734..4014bcd549fc 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -149,7 +149,11 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete }); } - // TODO add "This report may be selected at random for manual approval." + if (isOwner && policy.isAutoApprovalEnabled) { + optimisticNextStep.message?.push({ + text: ' This report may be selected at random for manual approval.', + }); + } // Prevented self submitting if (isPreventSelfApprovalEnabled && isSelfApproval) { diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 0be879fbfd68..08a13fa12e38 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -65,6 +65,9 @@ type Policy = { /** Whether chat rooms can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ areChatRoomsEnabled: boolean; + /** Whether auto approval enabled */ + isAutoApprovalEnabled?: boolean; + /** Whether policy expense chats can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ isPolicyExpenseChatEnabled: boolean; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 33089b26ea0e..9eb3e0a14555 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -22,6 +22,7 @@ describe('libs/NextStepUtils', () => { owner: currentUserEmail, submitsTo: currentUserAccountID, isHarvestingEnabled: false, + isAutoApprovalEnabled: false, // Required props name: 'Policy', role: 'admin', @@ -91,6 +92,40 @@ describe('libs/NextStepUtils', () => { expect(result).toStrictEqual(optimisticNextStep); }); + test('self review and auto approval enabled', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: 'Waiting for ', + }, + { + text: 'you', + type: 'strong', + }, + { + text: ' to ', + }, + { + text: 'submit', + type: 'strong', + }, + { + text: ' these expenses.', + }, + { + text: ' This report may be selected at random for manual approval.', + }, + ]; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isAutoApprovalEnabled: true, + }).then(() => { + const result = NextStepUtils.buildNextStep(report); + + expect(result).toStrictEqual(optimisticNextStep); + }); + }); + describe('scheduled submit enabled', () => { optimisticNextStep.title = 'Next Steps:'; From f77cea193bd5775f668dfd8e67a9852a7dc6df40 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 17 Jan 2024 17:48:16 +0100 Subject: [PATCH 101/446] add comment --- src/libs/NextStepUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 4014bcd549fc..a0880c8a96e6 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -149,6 +149,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete }); } + // Self review and auto approval enabled if (isOwner && policy.isAutoApprovalEnabled) { optimisticNextStep.message?.push({ text: ' This report may be selected at random for manual approval.', From df4e6f0708957a04d26ce6f06f65a92a4fd34fd6 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 18 Jan 2024 17:38:27 +0700 Subject: [PATCH 102/446] fix lint --- src/components/AddressSearch/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index f32c83104c94..9b4254a9bc45 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -1,7 +1,7 @@ import type {RefObject} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, View, ViewStyle} from 'react-native'; import type {Place} from 'react-native-google-places-autocomplete'; -import type { MaybePhraseKey } from '@libs/Localize'; +import type {MaybePhraseKey} from '@libs/Localize'; import type Locale from '@src/types/onyx/Locale'; type CurrentLocationButtonProps = { From 58baa0afd7821999d980a71c471ebc1ee20b2a06 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Jan 2024 13:54:40 +0100 Subject: [PATCH 103/446] integrate predictedNextStatus argument --- src/libs/NextStepUtils.ts | 9 +++--- tests/unit/NextStepUtilsTest.ts | 53 +++++++++++---------------------- 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index a0880c8a96e6..4cebc1325b27 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,6 +1,7 @@ import {format, lastDayOfMonth} from 'date-fns'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, ReportNextStep} from '@src/types/onyx'; @@ -54,12 +55,12 @@ type BuildNextStepParameters = { * Generates an optimistic nextStep based on a current report status and other properties. * * @param report - * @param parameters + * @param predictedNextStatus - a next expected status of the report * @param parameters.isPaidWithWallet - Whether a report has been paid with wallet or outside of Expensify * @returns nextStep */ -function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { - const {statusNum = CONST.REPORT.STATUS_NUM.OPEN, isPreventSelfApprovalEnabled = false, ownerAccountID = -1} = report; +function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { + const {isPreventSelfApprovalEnabled = false, ownerAccountID = -1} = report; const policy = ReportUtils.getPolicy(report.policyID ?? ''); const isOwner = currentUserAccountID === ownerAccountID; const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; @@ -68,7 +69,7 @@ function buildNextStep(report: Report, {isPaidWithWallet}: BuildNextStepParamete const type: ReportNextStep['type'] = 'neutral'; let optimisticNextStep: ReportNextStep | null; - switch (statusNum) { + switch (predictedNextStatus) { // Generates an optimistic nextStep once a report has been opened case CONST.REPORT.STATUS_NUM.OPEN: // Self review diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 9eb3e0a14555..bd4c7c9a0834 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -31,13 +31,12 @@ describe('libs/NextStepUtils', () => { areChatRoomsEnabled: true, isPolicyExpenseChatEnabled: true, }; - const report = ReportUtils.buildOptimisticExpenseReport('fake-chat-report-id-1', policyID, 1, -500, CONST.CURRENCY.USD) as Report; - const optimisticNextStep: ReportNextStep = { type: 'neutral', title: '', message: [], }; + const report = ReportUtils.buildOptimisticExpenseReport('fake-chat-report-id-1', policyID, 1, -500, CONST.CURRENCY.USD) as Report; beforeAll(() => { // @ts-expect-error Preset necessary values @@ -61,10 +60,6 @@ describe('libs/NextStepUtils', () => { }); describe('it generates an optimistic nextStep once a report has been opened', () => { - beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS_NUM.OPEN; - }); - test('self review', () => { optimisticNextStep.title = 'Next Steps:'; optimisticNextStep.message = [ @@ -87,7 +82,7 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -120,7 +115,7 @@ describe('libs/NextStepUtils', () => { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isAutoApprovalEnabled: true, }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -147,7 +142,7 @@ describe('libs/NextStepUtils', () => { isHarvestingEnabled: true, autoReportingFrequency: 'immediate', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -171,7 +166,7 @@ describe('libs/NextStepUtils', () => { isHarvestingEnabled: true, autoReportingFrequency: 'weekly', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -195,7 +190,7 @@ describe('libs/NextStepUtils', () => { isHarvestingEnabled: true, autoReportingFrequency: 'semimonthly', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -220,7 +215,7 @@ describe('libs/NextStepUtils', () => { autoReportingFrequency: 'monthly', autoReportingOffset: '2', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -245,7 +240,7 @@ describe('libs/NextStepUtils', () => { autoReportingFrequency: 'monthly', autoReportingOffset: 'lastDayOfMonth', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -269,7 +264,7 @@ describe('libs/NextStepUtils', () => { isHarvestingEnabled: true, autoReportingFrequency: 'trip', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -303,7 +298,7 @@ describe('libs/NextStepUtils', () => { isHarvestingEnabled: true, autoReportingFrequency: 'manual', }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -336,7 +331,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { submitsTo: currentUserAccountID, }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); expect(result).toStrictEqual(optimisticNextStep); }); @@ -344,10 +339,6 @@ describe('libs/NextStepUtils', () => { }); describe('it generates an optimistic nextStep once a report has been submitted', () => { - beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS_NUM.SUBMITTED; - }); - test('self review', () => { optimisticNextStep.title = 'Next Steps:'; optimisticNextStep.message = [ @@ -370,7 +361,7 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); expect(result).toStrictEqual(optimisticNextStep); }); @@ -400,7 +391,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { submitsTo: strangeAccountID, }).then(() => { - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED); expect(result).toStrictEqual(optimisticNextStep); }); @@ -433,17 +424,13 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED); expect(result).toStrictEqual(optimisticNextStep); }); }); describe('it generates an optimistic nextStep once a report has been approved', () => { - beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS_NUM.APPROVED; - }); - test('self review', () => { optimisticNextStep.title = 'Next Steps:'; optimisticNextStep.message = [ @@ -466,7 +453,7 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); expect(result).toStrictEqual(optimisticNextStep); }); @@ -480,17 +467,13 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); expect(result).toStrictEqual(optimisticNextStep); }); }); describe('it generates an optimistic nextStep once a report has been paid', () => { - beforeEach(() => { - report.statusNum = CONST.REPORT.STATUS_NUM.REIMBURSED; - }); - test('paid with wallet', () => { optimisticNextStep.title = 'Finished!'; optimisticNextStep.message = [ @@ -510,7 +493,7 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report, {isPaidWithWallet: true}); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: true}); expect(result).toStrictEqual(optimisticNextStep); }); @@ -537,7 +520,7 @@ describe('libs/NextStepUtils', () => { }, ]; - const result = NextStepUtils.buildNextStep(report, {isPaidWithWallet: false}); + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: false}); expect(result).toStrictEqual(optimisticNextStep); }); From 13d2d7f83bf0843b1368b38e5e554b00a110aafd Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Jan 2024 14:55:51 +0100 Subject: [PATCH 104/446] replace review with approve once submit --- src/libs/NextStepUtils.ts | 4 ++-- tests/unit/NextStepUtilsTest.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 4cebc1325b27..321d72187545 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -200,7 +200,7 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { text: ' to ', }, { - text: 'review', + text: 'approve', type: 'strong', }, { @@ -380,7 +380,7 @@ describe('libs/NextStepUtils', () => { text: ' to ', }, { - text: 'review', + text: 'approve', type: 'strong', }, { @@ -416,7 +416,7 @@ describe('libs/NextStepUtils', () => { text: ' to ', }, { - text: 'review', + text: 'approve', type: 'strong', }, { From 81616da30daeb78c9a77067812cf0206459a2c22 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Jan 2024 17:05:07 +0100 Subject: [PATCH 105/446] handle manager case --- src/libs/NextStepUtils.ts | 12 ++++++++---- tests/unit/NextStepUtilsTest.ts | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 321d72187545..cdc7c625b189 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -60,8 +60,9 @@ type BuildNextStepParameters = { * @returns nextStep */ function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { - const {isPreventSelfApprovalEnabled = false, ownerAccountID = -1} = report; + const {isPreventSelfApprovalEnabled = false, ownerAccountID = -1, managerID} = report; const policy = ReportUtils.getPolicy(report.policyID ?? ''); + const isManager = currentUserAccountID === managerID; const isOwner = currentUserAccountID === ownerAccountID; const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; const isSelfApproval = currentUserAccountID === policy.submitsTo; @@ -183,7 +184,9 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { beforeEach(() => { report.ownerAccountID = currentUserAccountID; + report.managerID = currentUserAccountID; optimisticNextStep.title = ''; optimisticNextStep.message = []; }); @@ -353,7 +354,7 @@ describe('libs/NextStepUtils', () => { text: ' to ', }, { - text: 'approve', + text: 'review', type: 'strong', }, { @@ -367,6 +368,7 @@ describe('libs/NextStepUtils', () => { }); test('another reviewer', () => { + report.managerID = strangeAccountID; optimisticNextStep.title = 'Next Steps:'; optimisticNextStep.message = [ { @@ -416,7 +418,7 @@ describe('libs/NextStepUtils', () => { text: ' to ', }, { - text: 'approve', + text: 'review', type: 'strong', }, { From 003d387200ff20b92e897bae0796e603591e802c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Jan 2024 18:05:10 +0100 Subject: [PATCH 106/446] improve adding the random for manual approval message --- src/libs/NextStepUtils.ts | 14 +++++++------- tests/unit/NextStepUtilsTest.ts | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index cdc7c625b189..43569f4c8106 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -151,13 +151,6 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { report.managerID = currentUserAccountID; optimisticNextStep.title = ''; optimisticNextStep.message = []; + + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy).then(waitForBatchedUpdates); }); describe('it generates an optimistic nextStep once a report has been opened', () => { From 85731fbdaadb584ddc1b8ac8f20a25bcec4d2075 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 18 Jan 2024 18:31:33 +0100 Subject: [PATCH 107/446] integrate optimistic next step generation --- src/libs/actions/IOU.js | 107 +++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 41 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 430d88b98569..66e804cf76ab 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -14,6 +14,7 @@ import * as IOUUtils from '@libs/IOUUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; +import * as NextStepUtils from '@libs/NextStepUtils'; import * as NumberUtils from '@libs/NumberUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Permissions from '@libs/Permissions'; @@ -330,6 +331,7 @@ function getReceiptError(receipt, filename, isScanRequest = true) { * @param {Array} policyTags * @param {Array} policyCategories * @param {Boolean} hasOutstandingChildRequest + * @param {Object} [optimisticNextStep] * @returns {Array} - An array containing the optimistic data, success data, and failure data. */ function buildOnyxDataForMoneyRequest( @@ -349,6 +351,7 @@ function buildOnyxDataForMoneyRequest( policyTags, policyCategories, hasOutstandingChildRequest = false, + optimisticNextStep, ) { const isScanRequest = TransactionUtils.isScanRequest(transaction); const optimisticData = [ @@ -431,6 +434,14 @@ function buildOnyxDataForMoneyRequest( }); } + if (!_.isEmpty(optimisticNextStep)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, + value: optimisticNextStep, + }); + } + const successData = [ ...(isNewChatReport ? [ @@ -810,6 +821,10 @@ function getMoneyRequestInformation( // so the employee has to submit to their manager manually. const hasOutstandingChildRequest = isPolicyExpenseChat && needsToBeManuallySubmitted; + const optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.OPEN); + // eslint-disable-next-line no-console + console.log('optimisticNextStep', optimisticNextStep); + // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( chatReport, @@ -828,6 +843,7 @@ function getMoneyRequestInformation( policyTags, policyCategories, hasOutstandingChildRequest, + optimisticNextStep, ); return { @@ -3024,6 +3040,7 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho } const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, null); + const optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY}); const optimisticData = [ { @@ -3065,6 +3082,11 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, value: {[iouReport.policyID]: paymentMethodType}, }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, + value: optimisticNextStep, + }, ]; const successData = [ @@ -3099,20 +3121,12 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, value: chatReport, }, - ]; - - if (!_.isNull(currentNextStep)) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, - value: null, - }); - failureData.push({ + { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`, value: currentNextStep, - }); - } + }, + ]; // In case the report preview action is loaded locally, let's update it. if (optimisticReportPreviewAction) { @@ -3184,9 +3198,9 @@ function sendMoneyWithWallet(report, amount, currency, comment, managerID, recip } function approveMoneyRequest(expenseReport) { - const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, null); - const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(expenseReport.total, expenseReport.currency, expenseReport.reportID); + const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, null); + const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.APPROVED); const optimisticReportActionsData = { onyxMethod: Onyx.METHOD.MERGE, @@ -3209,7 +3223,15 @@ function approveMoneyRequest(expenseReport) { statusNum: CONST.REPORT.STATUS_NUM.APPROVED, }, }; - const optimisticData = [optimisticIOUReportData, optimisticReportActionsData]; + const optimisticData = [ + optimisticIOUReportData, + optimisticReportActionsData, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: optimisticNextStep, + }, + ]; const successData = [ { @@ -3233,20 +3255,12 @@ function approveMoneyRequest(expenseReport) { }, }, }, - ]; - - if (!_.isNull(currentNextStep)) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, - value: null, - }); - failureData.push({ + { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, value: currentNextStep, - }); - } + }, + ]; API.write('ApproveMoneyRequest', {reportID: expenseReport.reportID, approvedReportActionID: optimisticApprovedReportAction.reportActionID}, {optimisticData, successData, failureData}); } @@ -3255,11 +3269,11 @@ function approveMoneyRequest(expenseReport) { * @param {Object} expenseReport */ function submitReport(expenseReport) { - const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, null); - const optimisticSubmittedReportAction = ReportUtils.buildOptimisticSubmittedReportAction(expenseReport.total, expenseReport.currency, expenseReport.reportID); const parentReport = ReportUtils.getReport(expenseReport.parentReportID); const isCurrentUserManager = currentUserPersonalDetails.accountID === expenseReport.managerID; + const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, null); + const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.SUBMITTED); const optimisticData = [ { @@ -3283,6 +3297,11 @@ function submitReport(expenseReport) { statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, }, }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: optimisticNextStep, + }, ...(parentReport.reportID ? [ { @@ -3330,6 +3349,11 @@ function submitReport(expenseReport) { stateNum: CONST.REPORT.STATE_NUM.OPEN, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: currentNextStep, + }, ...(parentReport.reportID ? [ { @@ -3344,19 +3368,6 @@ function submitReport(expenseReport) { : []), ]; - if (!_.isNull(currentNextStep)) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, - value: null, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, - value: currentNextStep, - }); - } - API.write( 'SubmitReport', { @@ -3376,6 +3387,10 @@ function cancelPayment(expenseReport, chatReport) { const optimisticReportAction = ReportUtils.buildOptimisticCancelPaymentReportAction(expenseReport.reportID); const policy = ReportUtils.getPolicy(chatReport.policyID); const isFree = policy && policy.type === CONST.POLICY.TYPE.FREE; + const statusNum = isFree ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN; + const currentNextStep = lodashGet(allNextSteps, `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, null); + const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, statusNum); + const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -3395,9 +3410,14 @@ function cancelPayment(expenseReport, chatReport) { lastMessageText: lodashGet(optimisticReportAction, 'message.0.text', ''), lastMessageHtml: lodashGet(optimisticReportAction, 'message.0.html', ''), stateNum: isFree ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.OPEN, - statusNum: isFree ? CONST.REPORT.STATUS_NUM.SUBMITTED : CONST.REPORT.STATUS_NUM.OPEN, + statusNum, }, }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: optimisticNextStep, + }, ...(chatReport.reportID ? [ { @@ -3442,6 +3462,11 @@ function cancelPayment(expenseReport, chatReport) { statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, + value: currentNextStep, + }, ...(chatReport.reportID ? [ { From 592553b93b7eb1daefcc3960519b8a029be9d6f4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 01:56:34 +0700 Subject: [PATCH 108/446] move translatableTextPropTypes to TS file --- src/components/CountrySelector.js | 2 +- src/components/MagicCodeInput.js | 2 +- .../RoomNameInput/roomNameInputPropTypes.js | 2 +- src/components/StatePicker/index.js | 2 +- .../TextInput/BaseTextInput/baseTextInputPropTypes.js | 2 +- src/components/ValuePicker/index.js | 2 +- src/libs/Localize/index.ts | 11 ++++++++++- 7 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/CountrySelector.js b/src/components/CountrySelector.js index 01d297d35467..36fa55c91f47 100644 --- a/src/components/CountrySelector.js +++ b/src/components/CountrySelector.js @@ -3,12 +3,12 @@ import React, {useEffect} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; import FormHelpMessage from './FormHelpMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import refPropTypes from './refPropTypes'; -import translatableTextPropTypes from './translatableTextPropTypes'; const propTypes = { /** Form error text. e.g when no country is selected */ diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index b075edc9aeca..c1d3d726e375 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -7,6 +7,7 @@ import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; +import {translatableTextPropTypes} from '@libs/Localize'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import FormHelpMessage from './FormHelpMessage'; @@ -14,7 +15,6 @@ import networkPropTypes from './networkPropTypes'; import {withNetwork} from './OnyxProvider'; import Text from './Text'; import TextInput from './TextInput'; -import translatableTextPropTypes from './translatableTextPropTypes'; const TEXT_INPUT_EMPTY_STATE = ''; diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 7f69de3a53f2..f634c6e0b3d6 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import refPropTypes from '@components/refPropTypes'; -import translatableTextPropTypes from '@components/translatableTextPropTypes'; +import {translatableTextPropTypes} from '@libs/Localize'; const propTypes = { /** Callback to execute when the text input is modified correctly */ diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js index dce3c7dc801b..918280f9f953 100644 --- a/src/components/StatePicker/index.js +++ b/src/components/StatePicker/index.js @@ -6,9 +6,9 @@ import _ from 'underscore'; import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import refPropTypes from '@components/refPropTypes'; -import translatableTextPropTypes from '@components/translatableTextPropTypes'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {translatableTextPropTypes} from '@libs/Localize'; import StateSelectorModal from './StateSelectorModal'; const propTypes = { diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js index ddc3a5e2f9c2..e6077bde71b3 100644 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import sourcePropTypes from '@components/Image/sourcePropTypes'; -import translatableTextPropTypes from '@components/translatableTextPropTypes'; +import {translatableTextPropTypes} from '@libs/Localize'; const propTypes = { /** Input label */ diff --git a/src/components/ValuePicker/index.js b/src/components/ValuePicker/index.js index 7f6b310c6374..28fa1ab26af2 100644 --- a/src/components/ValuePicker/index.js +++ b/src/components/ValuePicker/index.js @@ -5,9 +5,9 @@ import {View} from 'react-native'; import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import refPropTypes from '@components/refPropTypes'; -import translatableTextPropTypes from '@components/translatableTextPropTypes'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import {translatableTextPropTypes} from '@libs/Localize'; import variables from '@styles/variables'; import ValueSelectorModal from './ValueSelectorModal'; diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 4e28bdb30549..54a00c3db6f2 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import * as RNLocalize from 'react-native-localize'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; @@ -97,6 +98,14 @@ function translateLocal(phrase: TKey, ...variable return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables); } +/** + * Traslatable text with phrase key and/or variables + * Use MaybePhraseKey for Typescript + * + * E.g. ['common.error.characterLimitExceedCounter', {length: 5, limit: 20}] + */ +const translatableTextPropTypes = PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]); + type MaybePhraseKey = string | [string, Record & {isTranslated?: boolean}] | []; /** @@ -174,4 +183,4 @@ function getDevicePreferredLocale(): string { } export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; -export type {PhraseParameters, Phrase, MaybePhraseKey}; +export type {PhraseParameters, Phrase, MaybePhraseKey, translatableTextPropTypes}; From ef01f3763274c676dc520dc1a3bd446c27e00ee9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 01:59:55 +0700 Subject: [PATCH 109/446] remove redundant file --- src/components/translatableTextPropTypes.js | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/components/translatableTextPropTypes.js diff --git a/src/components/translatableTextPropTypes.js b/src/components/translatableTextPropTypes.js deleted file mode 100644 index 10130ab2da3e..000000000000 --- a/src/components/translatableTextPropTypes.js +++ /dev/null @@ -1,9 +0,0 @@ -import PropTypes from 'prop-types'; - -/** - * Traslatable text with phrase key and/or variables - * Use Localize.MaybePhraseKey instead for Typescript - * - * E.g. ['common.error.characterLimitExceedCounter', {length: 5, limit: 20}] - */ -export default PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]); From e520a8e8628ab7d7b91cd071212b6b8cd80839de Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 02:08:12 +0700 Subject: [PATCH 110/446] fix lint --- src/libs/Localize/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 54a00c3db6f2..c8363043567a 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -182,5 +182,5 @@ function getDevicePreferredLocale(): string { return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT; } -export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; -export type {PhraseParameters, Phrase, MaybePhraseKey, translatableTextPropTypes}; +export {translatableTextPropTypes, translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; +export type {PhraseParameters, Phrase, MaybePhraseKey}; From a0dcf260c7ebf060fc79fbe813ce147ca75eb007 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 18 Jan 2024 16:45:49 -0800 Subject: [PATCH 111/446] Migrate LogOutPreviousUserPage to TS --- src/libs/Navigation/types.ts | 2 +- src/libs/SessionUtils.ts | 4 +- src/pages/LogOutPreviousUserPage.tsx | 60 ++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 src/pages/LogOutPreviousUserPage.tsx diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 8d227fa6f697..0f2d717399c5 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -409,7 +409,7 @@ type AuthScreensParamList = { shouldForceLogin: string; email: string; shortLivedAuthToken: string; - exitTo: string; + exitTo: Routes; }; [SCREENS.CONCIERGE]: undefined; [SCREENS.REPORT_ATTACHMENTS]: { diff --git a/src/libs/SessionUtils.ts b/src/libs/SessionUtils.ts index c73513c747af..52521d5146cc 100644 --- a/src/libs/SessionUtils.ts +++ b/src/libs/SessionUtils.ts @@ -4,7 +4,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; /** * Determine if the transitioning user is logging in as a new user. */ -function isLoggingInAsNewUser(transitionURL: string, sessionEmail: string): boolean { +function isLoggingInAsNewUser(transitionURL?: string, sessionEmail?: string): boolean { // The OldDot mobile app does not URL encode the parameters, but OldDot web // does. We don't want to deploy OldDot mobile again, so as a work around we // compare the session email to both the decoded and raw email from the transition link. @@ -20,7 +20,7 @@ function isLoggingInAsNewUser(transitionURL: string, sessionEmail: string): bool // If they do not match it might be due to encoding, so check the raw value // Capture the un-encoded text in the email param const emailParamRegex = /[?&]email=([^&]*)/g; - const matches = emailParamRegex.exec(transitionURL); + const matches = emailParamRegex.exec(transitionURL ?? ''); const linkedEmail = matches?.[1] ?? null; return linkedEmail !== sessionEmail; } diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx new file mode 100644 index 000000000000..8d2d1a5f9b1d --- /dev/null +++ b/src/pages/LogOutPreviousUserPage.tsx @@ -0,0 +1,60 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useEffect} from 'react'; +import {Linking} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import * as SessionUtils from '@libs/SessionUtils'; +import type {AuthScreensParamList} from '@navigation/types'; +import * as SessionActions from '@userActions/Session'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Session} from '@src/types/onyx'; + +type LogOutPreviousUserPageOnyxProps = { + /** The data about the current session which will be set once the user is authenticated and we return to this component as an AuthScreen */ + session: OnyxEntry; +}; + +type LogOutPreviousUserPageProps = LogOutPreviousUserPageOnyxProps & StackScreenProps; + +// This page is responsible for handling transitions from OldDot. Specifically, it logs the current user +// out if the transition is for another user. +// +// This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate +function LogOutPreviousUserPage({session, route}: LogOutPreviousUserPageProps) { + useEffect(() => { + Linking.getInitialURL().then((transitionURL) => { + const sessionEmail = session?.email; + const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail); + + if (isLoggingInAsNewUser) { + SessionActions.signOutAndRedirectToSignIn(); + } + + // We need to signin and fetch a new authToken, if a user was already authenticated in NewDot, and was redirected to OldDot + // and their authToken stored in Onyx becomes invalid. + // This workflow is triggered while setting up VBBA. User is redirected from NewDot to OldDot to set up 2FA, and then redirected back to NewDot + // On Enabling 2FA, authToken stored in Onyx becomes expired and hence we need to fetch new authToken + const shouldForceLogin = route.params.shouldForceLogin === 'true'; + if (shouldForceLogin) { + const email = route.params.email; + const shortLivedAuthToken = route.params.shortLivedAuthToken; + SessionActions.signInWithShortLivedAuthToken(email, shortLivedAuthToken); + } + }); + + // We only want to run this effect once on mount (when the page first loads after transitioning from OldDot) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ; +} + +LogOutPreviousUserPage.displayName = 'LogOutPreviousUserPage'; + +export default withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(LogOutPreviousUserPage); From 1a58bfb1826f89452ae1b127d40d003e8a82a60b Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 18 Jan 2024 17:23:36 -0800 Subject: [PATCH 112/446] Remove unnecessary change --- src/libs/Navigation/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 0f2d717399c5..8d227fa6f697 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -409,7 +409,7 @@ type AuthScreensParamList = { shouldForceLogin: string; email: string; shortLivedAuthToken: string; - exitTo: Routes; + exitTo: string; }; [SCREENS.CONCIERGE]: undefined; [SCREENS.REPORT_ATTACHMENTS]: { From 347c1f5e64ca1030c08fc973915236623b4b3d99 Mon Sep 17 00:00:00 2001 From: kadiealexander <59587260+kadiealexander@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:47:05 +1300 Subject: [PATCH 113/446] Update src/components/PDFView/index.native.js Co-authored-by: 0xmiroslav <97473779+0xmiroslav@users.noreply.github.com> --- src/components/PDFView/index.native.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 4395f7432e91..856ca17b89ab 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -5,8 +5,8 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; -import useStyleUtils from '@hooks//useStyleUtils'; -import useThemeStyles from '@hooks//useThemeStyles'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; From a74f76b22bdf8767fec5e52eeef524216ce6a39a Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Fri, 19 Jan 2024 17:57:05 +1300 Subject: [PATCH 114/446] running prettier --- src/components/PDFView/index.native.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 856ca17b89ab..3802fe7a2ea6 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -5,10 +5,10 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; From 93121c7470d259307234fa821b4e0cc68b4640ba Mon Sep 17 00:00:00 2001 From: dhairyasenjaliya Date: Fri, 19 Jan 2024 14:07:49 +0530 Subject: [PATCH 115/446] fix typo From fa2c8fb084c825c2af6c34cf0d10e10d265fdc17 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 19 Jan 2024 14:00:09 +0100 Subject: [PATCH 116/446] minor improvements --- src/CONST.ts | 1 + src/libs/NextStepUtils.ts | 36 ++++++++++++++++----------------- src/types/onyx/Policy.ts | 3 --- tests/unit/NextStepUtilsTest.ts | 2 +- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index e3cce4b613af..5de9b9f29a28 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -182,6 +182,7 @@ const CONST = { UNIX_EPOCH: '1970-01-01 00:00:00.000', MAX_DATE: '9999-12-31', MIN_DATE: '0001-01-01', + ORDINAL_DAY_OF_MONTH: 'do', }, SMS: { DOMAIN: '@expensify.sms', diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 43569f4c8106..df477a887926 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -105,47 +105,45 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf; - /** The scheduled submission date */ - autoReportingOffset?: string; - /** Whether the scheduled submit is enabled */ isHarvestingEnabled?: boolean; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 6b6b046fd427..820ffb937450 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -216,7 +216,7 @@ describe('libs/NextStepUtils', () => { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'monthly', - autoReportingOffset: '2', + autoReportingOffset: 2, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); From dfec8f60dbce63a758a5fa5ab3171e2aa0fca8c2 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 19 Jan 2024 14:01:25 +0100 Subject: [PATCH 117/446] remove log --- src/libs/actions/IOU.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ba6227512f7a..f5858a234823 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -822,8 +822,6 @@ function getMoneyRequestInformation( const hasOutstandingChildRequest = isPolicyExpenseChat && needsToBeManuallySubmitted; const optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.OPEN); - // eslint-disable-next-line no-console - console.log('optimisticNextStep', optimisticNextStep); // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( From 59a036dede1a9d8e7d0e91898ec3d0312b326061 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 19 Jan 2024 15:03:01 +0100 Subject: [PATCH 118/446] improve and fix tests --- src/libs/NextStepUtils.ts | 3 +- tests/unit/NextStepUtilsTest.ts | 65 ++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index df477a887926..371ef7cda8ec 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -99,7 +99,7 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); test('self review and auto approval enabled', () => { @@ -115,17 +115,19 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isAutoApprovalEnabled: true, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); describe('scheduled submit enabled', () => { - optimisticNextStep.title = 'Next Steps:'; + beforeEach(() => { + optimisticNextStep.title = 'Next Steps:'; + }); test('daily', () => { optimisticNextStep.message = [ @@ -141,13 +143,13 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'immediate', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -165,13 +167,13 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'weekly', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -189,13 +191,13 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'semimonthly', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -213,14 +215,14 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'monthly', autoReportingOffset: 2, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -238,14 +240,14 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'monthly', autoReportingOffset: 'lastDayOfMonth', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -263,13 +265,13 @@ describe('libs/NextStepUtils', () => { }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'trip', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -292,18 +294,15 @@ describe('libs/NextStepUtils', () => { { text: ' these expenses.', }, - { - text: ' This report may be selected at random for manual approval.', - }, ]; - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, autoReportingFrequency: 'manual', }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); }); @@ -336,7 +335,7 @@ describe('libs/NextStepUtils', () => { }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); }); @@ -366,7 +365,7 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); test('another reviewer', () => { @@ -397,7 +396,7 @@ describe('libs/NextStepUtils', () => { }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -430,7 +429,7 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -459,7 +458,7 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); test('another owner', () => { @@ -473,7 +472,7 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.APPROVED); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); }); @@ -499,7 +498,7 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: true}); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); }); test('paid outside of Expensify', () => { @@ -526,7 +525,15 @@ describe('libs/NextStepUtils', () => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.REIMBURSED, {isPaidWithWallet: false}); - expect(result).toStrictEqual(optimisticNextStep); + expect(result).toMatchObject(optimisticNextStep); + }); + }); + + describe('it generates a nullable optimistic nextStep', () => { + test('closed status', () => { + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.CLOSED); + + expect(result).toBeNull(); }); }); }); From 685460b21cadc78631c9da1df3b410a358a6cd11 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 19 Jan 2024 17:22:48 +0100 Subject: [PATCH 119/446] [TS migration] Migrate 'WorkspaceInitial' page --- ...nitialPage.js => WorkspaceInitialPage.tsx} | 154 +++++++++--------- .../withPolicyAndFullscreenLoading.tsx | 1 + 2 files changed, 75 insertions(+), 80 deletions(-) rename src/pages/workspace/{WorkspaceInitialPage.js => WorkspaceInitialPage.tsx} (76%) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.tsx similarity index 76% rename from src/pages/workspace/WorkspaceInitialPage.js rename to src/pages/workspace/WorkspaceInitialPage.tsx index 80813c847239..9839c13125ea 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -1,13 +1,14 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import Avatar from '@components/Avatar'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -25,66 +26,66 @@ import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import * as App from '@userActions/App'; import * as Policy from '@userActions/Policy'; import * as ReimbursementAccount from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {policyDefaultProps, policyPropTypes} from './withPolicy'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -const propTypes = { - ...policyPropTypes, +type WorkspaceMenuItems = { + translationKey: TranslationPaths; + icon: IconAsset; + action: () => void; + brickRoadIndicator?: ValueOf; +}; - /** All reports shared with the user (coming from Onyx) */ - reports: PropTypes.objectOf(reportPropTypes), +type WorkspaceInitialPageOnyxProps = { + /** All reports shared with the user */ + reports: OnyxCollection; /** Bank account attached to free plan */ - reimbursementAccount: ReimbursementAccountProps.reimbursementAccountPropTypes, + reimbursementAccount: OnyxEntry; }; -const defaultProps = { - reports: {}, - ...policyDefaultProps, - reimbursementAccount: {}, -}; +type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInitialPageOnyxProps & StackScreenProps; -/** - * @param {string} policyID - */ -function openEditor(policyID) { +function openEditor(policyID: string) { Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policyID)); } -/** - * @param {string} policyID - */ -function dismissError(policyID) { +function dismissError(policyID: string) { Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); Policy.removeWorkspace(policyID); } -function WorkspaceInitialPage(props) { +function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reportsProp, policyMembers, reimbursementAccount}: WorkspaceInitialPageProps) { const styles = useThemeStyles(); - const policy = props.policyDraft && props.policyDraft.id ? props.policyDraft : props.policy; + const policy = policyDraft?.id ? policyDraft : policyProp; const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); - const hasPolicyCreationError = Boolean(policy.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.errors); + const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.errors); const waitForNavigate = useWaitForNavigation(); const {singleExecution, isExecuting} = useSingleExecution(); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); - const policyID = useMemo(() => policy.id, [policy]); + const policyID = useMemo(() => policy?.id, [policy]); const [policyReports, adminsRoom, announceRoom] = useMemo(() => { - const reports = []; - let admins; - let announce; - _.each(props.reports, (report) => { + const reports: OnyxTypes.Report[] = []; + let admins: OnyxTypes.Report | undefined; + let announce: OnyxTypes.Report | undefined; + + Object.values(reportsProp ?? {}).forEach((report) => { if (!report || report.policyID !== policyID) { return; } @@ -104,101 +105,97 @@ function WorkspaceInitialPage(props) { announce = report; } }); + return [reports, admins, announce]; - }, [policyID, props.reports]); + }, [policyID, reportsProp]); - /** - * Call the delete policy and hide the modal - */ + /** Call the delete policy and hide the modal */ const confirmDeleteAndHideModal = useCallback(() => { - Policy.deleteWorkspace(policyID, policyReports, policy.name); + Policy.deleteWorkspace(policyID ?? '', policyReports, policy?.name ?? ''); setIsDeleteModalOpen(false); // Pop the deleted workspace page before opening workspace settings. Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); - }, [policyID, policy.name, policyReports]); + }, [policyID, policy?.name, policyReports]); useEffect(() => { - const policyDraftId = lodashGet(props.policyDraft, 'id', null); + const policyDraftId = policyDraft?.id; + if (!policyDraftId) { return; } - App.savePolicyDraftByNewWorkspace(props.policyDraft.id, props.policyDraft.name, '', props.policyDraft.makeMeAdmin); + App.savePolicyDraftByNewWorkspace(policyDraft.id, policyDraft.name, '', policyDraft.makeMeAdmin); // We only care when the component renders the first time // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { - if (!isCurrencyModalOpen || policy.outputCurrency !== CONST.CURRENCY.USD) { + if (!isCurrencyModalOpen || policy?.outputCurrency !== CONST.CURRENCY.USD) { return; } setIsCurrencyModalOpen(false); - }, [policy.outputCurrency, isCurrencyModalOpen]); + }, [policy?.outputCurrency, isCurrencyModalOpen]); - /** - * Call update workspace currency and hide the modal - */ + /** Call update workspace currency and hide the modal */ const confirmCurrencyChangeAndHideModal = useCallback(() => { - Policy.updateGeneralSettings(policyID, policy.name, CONST.CURRENCY.USD); + Policy.updateGeneralSettings(policyID ?? '', policy?.name ?? '', CONST.CURRENCY.USD); setIsCurrencyModalOpen(false); - ReimbursementAccount.navigateToBankAccountRoute(policyID); - }, [policyID, policy.name]); + ReimbursementAccount.navigateToBankAccountRoute(policyID ?? ''); + }, [policyID, policy?.name]); - const policyName = lodashGet(policy, 'name', ''); - const hasMembersError = PolicyUtils.hasPolicyMemberError(props.policyMembers); - const hasGeneralSettingsError = !_.isEmpty(lodashGet(policy, 'errorFields.generalSettings', {})) || !_.isEmpty(lodashGet(policy, 'errorFields.avatar', {})); - const hasCustomUnitsError = PolicyUtils.hasCustomUnitsError(policy); - const menuItems = [ + const policyName = policy?.name ?? ''; + const hasMembersError = PolicyUtils.hasPolicyMemberError(policyMembers); + const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatar ?? {}); + const menuItems: WorkspaceMenuItems[] = [ { translationKey: 'workspace.common.settings', icon: Expensicons.Gear, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)))), - brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy?.id ?? '')))), + brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, { translationKey: 'workspace.common.card', icon: Expensicons.ExpensifyCard, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policy?.id ?? '')))), }, { translationKey: 'workspace.common.reimburse', icon: Expensicons.Receipt, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy.id)))), - error: hasCustomUnitsError, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy?.id ?? '')))), }, { translationKey: 'workspace.common.bills', icon: Expensicons.Bill, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policy?.id ?? '')))), }, { translationKey: 'workspace.common.invoices', icon: Expensicons.Invoice, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policy?.id ?? '')))), }, { translationKey: 'workspace.common.travel', icon: Expensicons.Luggage, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policy?.id ?? '')))), }, { translationKey: 'workspace.common.members', icon: Expensicons.Users, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policy.id)))), - brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policy?.id ?? '')))), + brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, { translationKey: 'workspace.common.bankAccount', icon: Expensicons.Bank, action: () => - policy.outputCurrency === CONST.CURRENCY.USD + policy?.outputCurrency === CONST.CURRENCY.USD ? singleExecution(waitForNavigate(() => ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRouteWithoutParams())))() : setIsCurrencyModalOpen(true), - brickRoadIndicator: !_.isEmpty(props.reimbursementAccount.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', + brickRoadIndicator: !isEmptyObject(reimbursementAccount?.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, ]; - const threeDotsMenuItems = useMemo(() => { + const threeDotsMenuItems: ThreeDotsMenuItem[] = useMemo(() => { const items = [ { icon: Expensicons.Trashcan, @@ -227,7 +224,7 @@ function WorkspaceInitialPage(props) { // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = - _.isEmpty(policy) || + isEmptyObject(policy) || !PolicyUtils.isPolicyAdmin(policy) || // We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)); @@ -241,7 +238,7 @@ function WorkspaceInitialPage(props) { Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} shouldShow={shouldShowNotFoundPage} - subtitleKey={_.isEmpty(policy) ? undefined : 'workspace.common.notAuthorized'} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} > dismissError(policy.id)} - errors={policy.errors} + pendingAction={policy?.pendingAction} + onClose={() => dismissError(policy?.id ?? '')} + errors={policy?.errors} errorRowStyles={[styles.ph5, styles.pv2]} > @@ -269,14 +266,14 @@ function WorkspaceInitialPage(props) { openEditor(policy.id)))} + onPress={singleExecution(waitForNavigate(() => openEditor(policy?.id ?? '')))} accessibilityLabel={translate('workspace.common.settings')} role={CONST.ROLE.BUTTON} > - {!_.isEmpty(policy.name) && ( + {!!policy?.name && ( ( + {menuItems.map((item) => ( ({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, @@ -364,4 +357,5 @@ export default compose( key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, }), + withPolicyAndFullscreenLoading, )(WorkspaceInitialPage); diff --git a/src/pages/workspace/withPolicyAndFullscreenLoading.tsx b/src/pages/workspace/withPolicyAndFullscreenLoading.tsx index 892facb92823..d3375bb948a4 100644 --- a/src/pages/workspace/withPolicyAndFullscreenLoading.tsx +++ b/src/pages/workspace/withPolicyAndFullscreenLoading.tsx @@ -63,3 +63,4 @@ export default function withPolicyAndFullscreenLoading Date: Sat, 20 Jan 2024 21:53:55 +0530 Subject: [PATCH 120/446] Add formatted amount to pay elsewhere --- src/components/SettlementButton.js | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js index 0c8e193af4cc..e8a7597daa4c 100644 --- a/src/components/SettlementButton.js +++ b/src/components/SettlementButton.js @@ -160,7 +160,7 @@ function SettlementButton({ value: CONST.IOU.PAYMENT_TYPE.VBBA, }, [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: { - text: translate('iou.payElsewhere'), + text: translate('iou.payElsewhere', {formattedAmount}), icon: Expensicons.Cash, value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index b6da38df21a0..f3ebd36f0d40 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -593,7 +593,7 @@ export default { settledExpensify: 'Paid', settledElsewhere: 'Paid elsewhere', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), - payElsewhere: 'Pay elsewhere', + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} elsewhere` : `Pay elsewhere`), nextStep: 'Next Steps', finished: 'Finished', requestAmount: ({amount}: RequestAmountParams) => `request ${amount}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 2478c8ba8bd2..3e7887d3221c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -586,7 +586,7 @@ export default { settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`), - payElsewhere: 'Pagar de otra forma', + payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} de otra forma` : `Pagar de otra forma`), nextStep: 'Pasos Siguientes', finished: 'Finalizado', requestAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, From 93f21dae4c3a1d936e302fe30eea49e361f38c98 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 12:29:03 +0530 Subject: [PATCH 121/446] switch to form element instead of view --- src/components/Form/FormWrapper.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js index 0d468dbcd0d2..f95b7f7eb801 100644 --- a/src/components/Form/FormWrapper.js +++ b/src/components/Form/FormWrapper.js @@ -1,9 +1,10 @@ import PropTypes from 'prop-types'; import React, {useCallback, useMemo, useRef} from 'react'; -import {Keyboard, ScrollView, StyleSheet, View} from 'react-native'; +import {Keyboard, ScrollView, StyleSheet} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import FormElement from '@components/FormElement'; import refPropTypes from '@components/refPropTypes'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; @@ -110,7 +111,7 @@ function FormWrapper(props) { const scrollViewContent = useCallback( (safeAreaPaddingBottomStyle) => ( - )} - + ), [ children, From 0de53952178e9f73c075cdcc095b46902f76c422 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 22 Jan 2024 17:30:32 +0700 Subject: [PATCH 122/446] fix: duplicated waypoints after dragging --- src/components/DistanceRequest/index.js | 22 ++++++++++-- src/libs/actions/Transaction.ts | 35 ++++++++++--------- .../request/step/IOURequestStepDistance.js | 20 +++++++++-- 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/components/DistanceRequest/index.js b/src/components/DistanceRequest/index.js index b63ce337a1d9..4a360cffecfb 100644 --- a/src/components/DistanceRequest/index.js +++ b/src/components/DistanceRequest/index.js @@ -194,17 +194,33 @@ function DistanceRequest({transactionID, report, transaction, route, isEditingRe } const newWaypoints = {}; + let emptyWaypointIndex = -1; _.each(data, (waypoint, index) => { newWaypoints[`waypoint${index}`] = lodashGet(waypoints, waypoint, {}); + // Find waypoint that BECOMES empty after dragging + if (_.isEmpty(newWaypoints[`waypoint${index}`]) && !_.isEmpty(lodashGet(waypoints, `waypoint${index}`, {}))) { + emptyWaypointIndex = index; + } }); setOptimisticWaypoints(newWaypoints); // eslint-disable-next-line rulesdir/no-thenable-actions-in-views - Transaction.updateWaypoints(transactionID, newWaypoints).then(() => { - setOptimisticWaypoints(null); + Transaction.updateWaypoints(transactionID, newWaypoints, true).then(() => { + if (emptyWaypointIndex === -1) { + setOptimisticWaypoints(null); + return; + } + // This is a workaround because at this point, transaction data has not been updated yet + const updatedTransaction = { + ...transaction, + ...Transaction.getUpdatedWaypointsTransaction(newWaypoints), + }; + Transaction.removeWaypoint(updatedTransaction, emptyWaypointIndex, true).then(() => { + setOptimisticWaypoints(null); + }); }); }, - [transactionID, waypoints, waypointsList], + [transactionID, transaction, waypoints, waypointsList], ); const submitWaypoints = useCallback(() => { diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 430de0557674..3e2ffba3949d 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -100,7 +100,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp } } -function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: boolean) { +function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: boolean): Promise { // Index comes from the route params and is a string const index = Number(currentIndex); const existingWaypoints = transaction?.comment?.waypoints ?? {}; @@ -109,7 +109,7 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: const waypointValues = Object.values(existingWaypoints); const removed = waypointValues.splice(index, 1); if (removed.length === 0) { - return; + return Promise.resolve(); } const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed[0] ?? {}); @@ -155,10 +155,9 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: }; } if (isDraft) { - Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction); - return; + return Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction); } - Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, newTransaction); + return Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, newTransaction); } function getOnyxDataForRouteRequest(transactionID: string, isDraft = false): OnyxData { @@ -234,15 +233,8 @@ function getRouteForDraft(transactionID: string, waypoints: WaypointCollection) ); } -/** - * Updates all waypoints stored in the transaction specified by the provided transactionID. - * - * @param transactionID - The ID of the transaction to be updated - * @param waypoints - An object containing all the waypoints - * which will replace the existing ones. - */ -function updateWaypoints(transactionID: string, waypoints: WaypointCollection, isDraft = false): Promise { - return Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { +function getUpdatedWaypointsTransaction(waypoints: WaypointCollection) { + return { comment: { waypoints, }, @@ -261,7 +253,18 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i }, }, }, - }); + }; +} + +/** + * Updates all waypoints stored in the transaction specified by the provided transactionID. + * + * @param transactionID - The ID of the transaction to be updated + * @param waypoints - An object containing all the waypoints + * which will replace the existing ones. + */ +function updateWaypoints(transactionID: string, waypoints: WaypointCollection, isDraft = false): Promise { + return Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, getUpdatedWaypointsTransaction(waypoints)); } -export {addStop, createInitialWaypoints, saveWaypoint, removeWaypoint, getRoute, getRouteForDraft, updateWaypoints}; +export {addStop, createInitialWaypoints, saveWaypoint, removeWaypoint, getRoute, getRouteForDraft, getUpdatedWaypointsTransaction, updateWaypoints}; diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 9549a93c8124..c199bef4a01e 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -150,17 +150,33 @@ function IOURequestStepDistance({ } const newWaypoints = {}; + let emptyWaypointIndex = -1; _.each(data, (waypoint, index) => { newWaypoints[`waypoint${index}`] = lodashGet(waypoints, waypoint, {}); + // Find waypoint that BECOMES empty after dragging + if (_.isEmpty(newWaypoints[`waypoint${index}`]) && !_.isEmpty(lodashGet(waypoints, `waypoint${index}`, {}))) { + emptyWaypointIndex = index; + } }); setOptimisticWaypoints(newWaypoints); // eslint-disable-next-line rulesdir/no-thenable-actions-in-views Transaction.updateWaypoints(transactionID, newWaypoints, true).then(() => { - setOptimisticWaypoints(null); + if (emptyWaypointIndex === -1) { + setOptimisticWaypoints(null); + return; + } + // This is a workaround because at this point, transaction data has not been updated yet + const updatedTransaction = { + ...transaction, + ...Transaction.getUpdatedWaypointsTransaction(newWaypoints), + }; + Transaction.removeWaypoint(updatedTransaction, emptyWaypointIndex, true).then(() => { + setOptimisticWaypoints(null); + }); }); }, - [transactionID, waypoints, waypointsList], + [transactionID, transaction, waypoints, waypointsList], ); const submitWaypoints = useCallback(() => { From 4848e99953ac4c0fcdbfc278cdbf657a5a29f4f6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 19:13:09 +0530 Subject: [PATCH 123/446] adjustment according to recommendations --- src/components/Form/FormProvider.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index f4118912c8f9..246b19dcd932 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -271,6 +271,7 @@ const FormProvider = forwardRef( } propsToParse.onSubmitEditing(e); }, + returnKeyType: 'go', } : {}; @@ -288,7 +289,6 @@ const FormProvider = forwardRef( return { ...propsToParse, - returnKeyType: shouldSubmitEdit ? 'go' : propsToParse.returnKeyType, blurOnSubmit: (isMultiline && shouldSubmitEdit) || propsToParse.blurOnSubmit, ...onSubmitEditingObject, ref: From 9e7c34e219424158674b06084d7ea4eaa2d4198e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 22 Jan 2024 16:28:00 +0100 Subject: [PATCH 124/446] Code improvements --- src/pages/workspace/WorkspaceInitialPage.tsx | 42 ++++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 9839c13125ea..5e230869c4a9 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -22,7 +22,6 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -41,7 +40,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -type WorkspaceMenuItems = { +type WorkspaceMenuItem = { translationKey: TranslationPaths; icon: IconAsset; action: () => void; @@ -79,7 +78,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); - const policyID = useMemo(() => policy?.id, [policy]); + const policyID = useMemo(() => policy?.id ?? '', [policy]); const [policyReports, adminsRoom, announceRoom] = useMemo(() => { const reports: OnyxTypes.Report[] = []; let admins: OnyxTypes.Report | undefined; @@ -111,7 +110,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports /** Call the delete policy and hide the modal */ const confirmDeleteAndHideModal = useCallback(() => { - Policy.deleteWorkspace(policyID ?? '', policyReports, policy?.name ?? ''); + Policy.deleteWorkspace(policyID, policyReports, policy?.name ?? ''); setIsDeleteModalOpen(false); // Pop the deleted workspace page before opening workspace settings. Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); @@ -138,50 +137,50 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports /** Call update workspace currency and hide the modal */ const confirmCurrencyChangeAndHideModal = useCallback(() => { - Policy.updateGeneralSettings(policyID ?? '', policy?.name ?? '', CONST.CURRENCY.USD); + Policy.updateGeneralSettings(policyID, policy?.name ?? '', CONST.CURRENCY.USD); setIsCurrencyModalOpen(false); - ReimbursementAccount.navigateToBankAccountRoute(policyID ?? ''); + ReimbursementAccount.navigateToBankAccountRoute(policyID); }, [policyID, policy?.name]); const policyName = policy?.name ?? ''; const hasMembersError = PolicyUtils.hasPolicyMemberError(policyMembers); const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatar ?? {}); - const menuItems: WorkspaceMenuItems[] = [ + const menuItems: WorkspaceMenuItem[] = [ { translationKey: 'workspace.common.settings', icon: Expensicons.Gear, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policyID)))), brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, { translationKey: 'workspace.common.card', icon: Expensicons.ExpensifyCard, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policyID)))), }, { translationKey: 'workspace.common.reimburse', icon: Expensicons.Receipt, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policyID)))), }, { translationKey: 'workspace.common.bills', icon: Expensicons.Bill, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policyID)))), }, { translationKey: 'workspace.common.invoices', icon: Expensicons.Invoice, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policyID)))), }, { translationKey: 'workspace.common.travel', icon: Expensicons.Luggage, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policyID)))), }, { translationKey: 'workspace.common.members', icon: Expensicons.Users, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policy?.id ?? '')))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)))), brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, { @@ -255,7 +254,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports dismissError(policy?.id ?? '')} + onClose={() => dismissError(policyID)} errors={policy?.errors} errorRowStyles={[styles.ph5, styles.pv2]} > @@ -266,14 +265,16 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports openEditor(policy?.id ?? '')))} + onPress={singleExecution(waitForNavigate(() => openEditor(policyID)))} accessibilityLabel={translate('workspace.common.settings')} role={CONST.ROLE.BUTTON} > ({ reports: { key: ONYXKEYS.COLLECTION.REPORT, @@ -356,6 +357,5 @@ export default compose( reimbursementAccount: { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, - }), - withPolicyAndFullscreenLoading, -)(WorkspaceInitialPage); + })(WorkspaceInitialPage), +); From e834c5fe137c16e44f5bf1d23d4e90bbea614591 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 23 Jan 2024 11:22:06 +0700 Subject: [PATCH 125/446] reapply changes --- src/pages/SearchPage/index.js | 2 +- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 2 +- .../MoneyRequestParticipantsSelector.js | 2 +- src/pages/tasks/TaskShareDestinationSelectorModal.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index 211f3622e06c..8e6cacc2073a 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -50,7 +50,7 @@ function SearchPage({betas, reports}) { const themeStyles = useThemeStyles(); const personalDetails = usePersonalDetails(); - const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 0949081435c4..171f3c421171 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -83,7 +83,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); - const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 9567b17ecdf5..6257c2065592 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -88,7 +88,7 @@ function MoneyRequestParticipantsSelector({ const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); - const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; const newChatOptions = useMemo(() => { const chatOptions = OptionsListUtils.getFilteredOptions( diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js index 64fd5f50b61f..fc0f4880efd9 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js @@ -145,7 +145,7 @@ function TaskShareDestinationSelectorModal(props) { showTitleTooltip shouldShowOptions={didScreenTransitionEnd} textInputLabel={props.translate('optionsSelector.nameEmailOrPhoneNumber')} - textInputAlert={isOffline ? `${props.translate('common.youAppearToBeOffline')} ${props.translate('search.resultsAreLimited')}` : ''} + textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} autoFocus={false} ref={inputCallbackRef} From ed0cce8af3ebdca68b3d1d2ce2a7df1c373f17d0 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 23 Jan 2024 11:30:22 +0700 Subject: [PATCH 126/446] fix lint --- src/pages/tasks/TaskShareDestinationSelectorModal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.js b/src/pages/tasks/TaskShareDestinationSelectorModal.js index fc0f4880efd9..b8d9229e6158 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.js +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.js @@ -145,7 +145,7 @@ function TaskShareDestinationSelectorModal(props) { showTitleTooltip shouldShowOptions={didScreenTransitionEnd} textInputLabel={props.translate('optionsSelector.nameEmailOrPhoneNumber')} - textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} + textInputAlert={isOffline ? [`${props.translate('common.youAppearToBeOffline')} ${props.translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} autoFocus={false} ref={inputCallbackRef} From 5888b46da4ec069cdd01e71e59d8d461490843db Mon Sep 17 00:00:00 2001 From: Shahe Shahinyan Date: Tue, 23 Jan 2024 14:58:23 +0400 Subject: [PATCH 127/446] fix login background image moving down --- .../SignInPageLayout/BackgroundImage/index.ios.js | 4 +++- src/pages/signin/SignInPageLayout/index.js | 1 + src/styles/index.ts | 10 +++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js index da6a6b9ee4fb..646234469611 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js @@ -6,6 +6,7 @@ import MobileBackgroundImage from '@assets/images/home-background--mobile-new.sv import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import defaultPropTypes from './propTypes'; +import useWindowDimensions from "@hooks/useWindowDimensions"; const defaultProps = { isSmallScreen: false, @@ -20,12 +21,13 @@ const propTypes = { function BackgroundImage(props) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {windowHeight} = useWindowDimensions() const src = useMemo(() => (props.isSmallScreen ? MobileBackgroundImage : DesktopBackgroundImage), [props.isSmallScreen]); return ( ); } diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.js index e2f9c28f9fcd..823a08ba6b7d 100644 --- a/src/pages/signin/SignInPageLayout/index.js +++ b/src/pages/signin/SignInPageLayout/index.js @@ -162,6 +162,7 @@ function SignInPageLayout(props) { ref={scrollViewRef} > + signInBackground: { position: 'absolute', - bottom: 0, left: 0, minHeight: 700, }, + signInBackgroundFillView: { + position: 'absolute', + left: 0, + bottom: 0, + height: '50%', + width: '100%', + backgroundColor: theme.signInPage + }, + signInPageInner: { marginLeft: 'auto', marginRight: 'auto', From 03a41e373463a19c5981a52b74305200e08bdfd2 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 23 Jan 2024 18:41:30 +0700 Subject: [PATCH 128/446] fix remaining cases --- src/components/DotIndicatorMessage.tsx | 8 ++++---- src/components/TextInput/BaseTextInput/types.ts | 2 +- src/libs/ErrorUtils.ts | 12 ++++++------ src/pages/iou/steps/MoneyRequestAmountForm.js | 6 +++--- .../Profile/CustomStatus/StatusClearAfterPage.js | 12 ++++-------- src/pages/settings/Wallet/TransferBalancePage.js | 2 +- src/pages/signin/LoginForm/BaseLoginForm.js | 2 +- src/pages/signin/UnlinkLoginForm.js | 2 +- src/pages/workspace/WorkspaceInvitePage.js | 3 +-- src/stories/Form.stories.js | 2 +- 10 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index d18704fdfb05..113a262a6f97 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -23,7 +23,7 @@ type DotIndicatorMessageProps = { * timestamp: 'message', * } */ - messages: Record; + messages: Record; /** The type of message, 'error' shows a red dot, 'success' shows a green dot */ type: 'error' | 'success'; @@ -36,8 +36,8 @@ type DotIndicatorMessageProps = { }; /** Check if the error includes a receipt. */ -function isReceiptError(message: string | ReceiptError): message is ReceiptError { - if (typeof message === 'string') { +function isReceiptError(message: Localize.MaybePhraseKey | ReceiptError): message is ReceiptError { + if (typeof message === 'string' || Array.isArray(message)) { return false; } return (message?.error ?? '') === CONST.IOU.RECEIPT_ERROR; @@ -58,7 +58,7 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica .map((key) => messages[key]); // Removing duplicates using Set and transforming the result into an array - const uniqueMessages = [...new Set(sortedMessages)].map((message) => Localize.translateIfPhraseKey(message)); + const uniqueMessages = [...new Set(sortedMessages)].map((message) => (isReceiptError(message) ? message : Localize.translateIfPhraseKey(message))); const isErrorMessage = type === 'error'; diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 21875d4dcc64..9931f8ec90d4 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -65,7 +65,7 @@ type CustomBaseTextInputProps = { hideFocusedState?: boolean; /** Hint text to display below the TextInput */ - hint?: string; + hint?: MaybePhraseKey; /** Prefix character */ prefixCharacter?: string; diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index aa74d84e7b65..208b824ef872 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -52,7 +52,7 @@ function getMicroSecondOnyxErrorObject(error: Record): Record(onyxData: T } const key = Object.keys(errors).sort().reverse()[0]; - return getErrorWithTranslationData(errors[key]); + return getErrorMessageWithTranslationData(errors[key]); } type OnyxDataWithErrorFields = { @@ -83,7 +83,7 @@ function getLatestErrorField(onyxData } const key = Object.keys(errorsForField).sort().reverse()[0]; - return {[key]: getErrorWithTranslationData(errorsForField[key])}; + return {[key]: getErrorMessageWithTranslationData(errorsForField[key])}; } function getEarliestErrorField(onyxData: TOnyxData, fieldName: string): Record { @@ -94,7 +94,7 @@ function getEarliestErrorField(onyxDa } const key = Object.keys(errorsForField).sort()[0]; - return {[key]: getErrorWithTranslationData(errorsForField[key])}; + return {[key]: getErrorMessageWithTranslationData(errorsForField[key])}; } type ErrorsList = Record; @@ -111,10 +111,10 @@ function getErrorsWithTranslationData(errors: Localize.MaybePhraseKey | ErrorsLi if (typeof errors === 'string' || Array.isArray(errors)) { // eslint-disable-next-line @typescript-eslint/naming-convention - return {'0': getErrorWithTranslationData(errors)}; + return {'0': getErrorMessageWithTranslationData(errors)}; } - return mapValues(errors, getErrorWithTranslationData); + return mapValues(errors, getErrorMessageWithTranslationData); } /** diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js index 536944f4a2d8..de9b9708eb3e 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.js +++ b/src/pages/iou/steps/MoneyRequestAmountForm.js @@ -228,12 +228,12 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward */ const submitAndNavigateToNextPage = useCallback(() => { if (isAmountInvalid(currentAmount)) { - setFormError(translate('iou.error.invalidAmount')); + setFormError('iou.error.invalidAmount'); return; } if (isTaxAmountInvalid(currentAmount, taxAmount, isTaxAmountForm)) { - setFormError(translate('iou.error.invalidTaxAmount', {amount: formattedTaxAmount})); + setFormError(['iou.error.invalidTaxAmount', {amount: formattedTaxAmount}]); return; } @@ -243,7 +243,7 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward initializeAmount(backendAmount); onSubmitButtonPress({amount: currentAmount, currency}); - }, [onSubmitButtonPress, currentAmount, taxAmount, currency, isTaxAmountForm, formattedTaxAmount, translate, initializeAmount]); + }, [onSubmitButtonPress, currentAmount, taxAmount, currency, isTaxAmountForm, formattedTaxAmount, initializeAmount]); /** * Input handler to check for a forward-delete key (or keyboard shortcut) press. diff --git a/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js b/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js index 84ca74c2842f..61208447495d 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js +++ b/src/pages/settings/Profile/CustomStatus/StatusClearAfterPage.js @@ -54,21 +54,17 @@ function getSelectedStatusType(data) { } const useValidateCustomDate = (data) => { - const {translate} = useLocalize(); const [customDateError, setCustomDateError] = useState(''); const [customTimeError, setCustomTimeError] = useState(''); const validate = () => { const {dateValidationErrorKey, timeValidationErrorKey} = ValidationUtils.validateDateTimeIsAtLeastOneMinuteInFuture(data); - const dateError = dateValidationErrorKey ? translate(dateValidationErrorKey) : ''; - setCustomDateError(dateError); - - const timeError = timeValidationErrorKey ? translate(timeValidationErrorKey) : ''; - setCustomTimeError(timeError); + setCustomDateError(dateValidationErrorKey); + setCustomTimeError(timeValidationErrorKey); return { - dateError, - timeError, + dateValidationErrorKey, + timeValidationErrorKey, }; }; diff --git a/src/pages/settings/Wallet/TransferBalancePage.js b/src/pages/settings/Wallet/TransferBalancePage.js index 44c9bde8cd3c..3dfb1f059933 100644 --- a/src/pages/settings/Wallet/TransferBalancePage.js +++ b/src/pages/settings/Wallet/TransferBalancePage.js @@ -167,7 +167,7 @@ function TransferBalancePage(props) { const transferAmount = props.userWallet.currentBalance - calculatedFee; const isTransferable = transferAmount > 0; const isButtonDisabled = !isTransferable || !selectedAccount; - const errorMessage = !_.isEmpty(props.walletTransfer.errors) ? ErrorUtils.getErrorsWithTranslationData(_.chain(props.walletTransfer.errors).values().first().value()) : ''; + const errorMessage = ErrorUtils.getLatestErrorMessage(props.walletTransfer); const shouldShowTransferView = PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, props.bankAccountList) && diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index 40b8b7d97bd1..e9ec74e506e2 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -297,7 +297,7 @@ function LoginForm(props) { )} { diff --git a/src/pages/signin/UnlinkLoginForm.js b/src/pages/signin/UnlinkLoginForm.js index 1d278760f13c..52eb710e2ea5 100644 --- a/src/pages/signin/UnlinkLoginForm.js +++ b/src/pages/signin/UnlinkLoginForm.js @@ -68,7 +68,7 @@ function UnlinkLoginForm(props) { )} {!_.isEmpty(props.account.errors) && ( diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 6b8a77f739af..e453ea632863 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -15,7 +15,6 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as ErrorUtils from '@libs/ErrorUtils'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -306,7 +305,7 @@ function WorkspaceInvitePage(props) { isAlertVisible={shouldShowAlertPrompt} buttonText={translate('common.next')} onSubmit={inviteUser} - message={ErrorUtils.getErrorsWithTranslationData(props.policy.alertMessage)} + message={[props.policy.alertMessage, {isTranslated: true}]} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.js index 6a4274c87eda..8a152d040a1f 100644 --- a/src/stories/Form.stories.js +++ b/src/stories/Form.stories.js @@ -68,7 +68,7 @@ function Template(args) { label="Street" inputID="street" containerStyles={[defaultStyles.mt4]} - hint="No PO box" + hint="common.noPO" /> Date: Tue, 23 Jan 2024 19:52:38 +0530 Subject: [PATCH 129/446] changes according to recommendation --- src/components/Form/InputWrapper.js | 16 ++++++++++++---- src/pages/EditRequestDescriptionPage.js | 3 +-- src/pages/iou/MoneyRequestDescriptionPage.js | 3 +-- .../request/step/IOURequestStepDescription.js | 3 +-- src/pages/tasks/NewTaskDescriptionPage.js | 3 +-- src/pages/tasks/NewTaskDetailsPage.js | 3 +-- src/pages/tasks/TaskDescriptionPage.js | 3 +-- 7 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 7f49660478ff..71f9a3273164 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, {forwardRef, useContext} from 'react'; import refPropTypes from '@components/refPropTypes'; import TextInput from '@components/TextInput'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import FormContext from './FormContext'; const propTypes = { @@ -9,24 +10,31 @@ const propTypes = { inputID: PropTypes.string.isRequired, valueType: PropTypes.string, forwardedRef: refPropTypes, + + /** Whether the input allows the form to be submitted when the user presses enter. + * This is useful for inputs that are not multiline and don't have a submit button by default. + * This property is ignored on mobile devices as they don't have a shift + enter key to create a newline. + */ + inputAllowsSubmit: PropTypes.bool, }; const defaultProps = { forwardedRef: undefined, valueType: 'string', + inputAllowsSubmit: false, }; -const canUseSubmitEditing = (multiline, autoGrowHeight, submitOnEnter) => { +const canUseSubmitEditing = (multiline, autoGrowHeight, inputAllowsSubmit) => { const isMultiline = multiline || autoGrowHeight; if (!isMultiline) { return true; } - return Boolean(submitOnEnter); + return Boolean(inputAllowsSubmit) && !canUseTouchScreen(); }; function InputWrapper(props) { - const {InputComponent, inputID, forwardedRef, ...rest} = props; - const shouldSubmitEdit = canUseSubmitEditing(rest.multiline, rest.autoGrowHeight, rest.submitOnEnter); + const {InputComponent, inputID, forwardedRef, inputAllowsSubmit, ...rest} = props; + const shouldSubmitEdit = canUseSubmitEditing(rest.multiline, rest.autoGrowHeight, inputAllowsSubmit); const {registerInput} = useContext(FormContext); // There are inputs that dont have onBlur methods, to simulate the behavior of onBlur in e.g. checkbox, we had to // use different methods like onPress. This introduced a problem that inputs that have the onBlur method were diff --git a/src/pages/EditRequestDescriptionPage.js b/src/pages/EditRequestDescriptionPage.js index b459c17a3ee3..d64cee3c5dc3 100644 --- a/src/pages/EditRequestDescriptionPage.js +++ b/src/pages/EditRequestDescriptionPage.js @@ -9,7 +9,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -77,7 +76,7 @@ function EditRequestDescriptionPage({defaultDescription, onSubmit}) { }} autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit /> diff --git a/src/pages/iou/MoneyRequestDescriptionPage.js b/src/pages/iou/MoneyRequestDescriptionPage.js index 7d3c31ca12ba..74fe27af9dfd 100644 --- a/src/pages/iou/MoneyRequestDescriptionPage.js +++ b/src/pages/iou/MoneyRequestDescriptionPage.js @@ -13,7 +13,6 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as IOU from '@libs/actions/IOU'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; @@ -142,7 +141,7 @@ function MoneyRequestDescriptionPage({iou, route, selectedTab}) { }} autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit /> diff --git a/src/pages/iou/request/step/IOURequestStepDescription.js b/src/pages/iou/request/step/IOURequestStepDescription.js index 7473239d92b5..fe0f65f9ea3b 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.js +++ b/src/pages/iou/request/step/IOURequestStepDescription.js @@ -9,7 +9,6 @@ import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as IOU from '@userActions/IOU'; @@ -107,7 +106,7 @@ function IOURequestStepDescription({ autoGrowHeight containerStyles={[styles.autoGrowHeightMultilineInput]} inputStyle={[styles.verticalAlignTop]} - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit /> diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.js index 7620afaf2dc8..e1d8945fe58f 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.js +++ b/src/pages/tasks/NewTaskDescriptionPage.js @@ -12,7 +12,6 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as Task from '@userActions/Task'; @@ -79,7 +78,7 @@ function NewTaskDescriptionPage(props) { updateMultilineInputRange(el); }} autoGrowHeight - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit containerStyles={[styles.autoGrowHeightMultilineInput]} /> diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index adf26820f7b9..29d84ad60b8b 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -12,7 +12,6 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as Task from '@userActions/Task'; @@ -111,7 +110,7 @@ function NewTaskDetailsPage(props) { label={props.translate('newTaskPage.descriptionOptional')} accessibilityLabel={props.translate('newTaskPage.descriptionOptional')} autoGrowHeight - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit containerStyles={[styles.autoGrowHeightMultilineInput]} defaultValue={parser.htmlToMarkdown(parser.replace(taskDescription))} value={taskDescription} diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index db8974632ab7..453f55e5a89d 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -16,7 +16,6 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; @@ -126,7 +125,7 @@ function TaskDescriptionPage(props) { updateMultilineInputRange(inputRef.current); }} autoGrowHeight - submitOnEnter={!canUseTouchScreen()} + inputAllowsSubmit containerStyles={[styles.autoGrowHeightMultilineInput]} /> From cb77b60e2ca2689bc7011be5154fb2fa70d717c4 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 24 Jan 2024 13:48:14 +0530 Subject: [PATCH 130/446] Update tagOutOfPolicy violation copy We update the copy to use 'Tag' when tagName is not available --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 0a754a883d07..e8cdadd3c27a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2114,7 +2114,7 @@ export default { }, smartscanFailed: 'Receipt scanning failed. Enter details manually.', someTagLevelsRequired: 'Missing tag', - tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `${tagName ?? ''} no longer valid`, + tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `${tagName ?? 'Tag'} no longer valid`, taxAmountChanged: 'Tax amount was modified', taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? ''} no longer valid`, taxRateChanged: 'Tax rate was modified', From e36610905b11f2f1df86a48a81cd1a70c8a20f44 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 24 Jan 2024 13:49:32 +0530 Subject: [PATCH 131/446] Update taxOutOfPolicy violation copy We update the copy to use 'Tax' when taxName is not available --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index e8cdadd3c27a..6b8f56489903 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2116,7 +2116,7 @@ export default { someTagLevelsRequired: 'Missing tag', tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `${tagName ?? 'Tag'} no longer valid`, taxAmountChanged: 'Tax amount was modified', - taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? ''} no longer valid`, + taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? 'Tax'} no longer valid`, taxRateChanged: 'Tax rate was modified', taxRequired: 'Missing tax rate', }, From 8481f664890c039efd7ee81011848f5a49399011 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 24 Jan 2024 17:18:30 +0100 Subject: [PATCH 132/446] integrate last business day calculation --- src/libs/NextStepUtils.ts | 11 +++++------ tests/unit/NextStepUtilsTest.ts | 29 ++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 371ef7cda8ec..9a5ec66db948 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,4 +1,4 @@ -import {format, lastDayOfMonth} from 'date-fns'; +import {format, lastDayOfMonth, setDate} from 'date-fns'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -6,6 +6,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, ReportNextStep} from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportNextStep'; +import DateUtils from './DateUtils'; import EmailUtils from './EmailUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as ReportUtils from './ReportUtils'; @@ -113,12 +114,10 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { text: 'These expenses are scheduled to ', }, { - text: `automatically submit on the ${format(lastDayOfMonth(new Date()), 'do')} of each month!`, + text: `automatically submit on the ${format(lastDayOfMonth(new Date()), CONST.DATE.ORDINAL_DAY_OF_MONTH)} of each month!`, type: 'strong', }, { @@ -251,6 +252,32 @@ describe('libs/NextStepUtils', () => { }); }); + test('monthly on the last business day', () => { + const lastBusinessDayOfMonth = DateUtils.getLastBusinessDayOfMonth(new Date()); + optimisticNextStep.message = [ + { + text: 'These expenses are scheduled to ', + }, + { + text: `automatically submit on the ${format(new Date().setDate(lastBusinessDayOfMonth), CONST.DATE.ORDINAL_DAY_OF_MONTH)} of each month!`, + type: 'strong', + }, + { + text: ' No further action required!', + }, + ]; + + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + isHarvestingEnabled: true, + autoReportingFrequency: 'monthly', + autoReportingOffset: 'lastBusinessDayOfMonth', + }).then(() => { + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); + + expect(result).toMatchObject(optimisticNextStep); + }); + }); + test('trip', () => { optimisticNextStep.message = [ { From 10f9139aa38b5242f86c6e7aa1e4a8b71cf4378f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jan 2024 18:04:27 +0100 Subject: [PATCH 133/446] Migrate WorkspaceInviteMessagePage to TypeScript --- src/ONYXKEYS.ts | 1 + src/libs/OptionsListUtils.js | 2 +- ...Page.js => WorkspaceInviteMessagePage.tsx} | 138 ++++++++---------- .../workspace/WorkspacePageWithSections.tsx | 8 +- .../invoices/WorkspaceInvoicesPage.tsx | 9 +- src/pages/workspace/withPolicy.tsx | 7 +- 6 files changed, 76 insertions(+), 89 deletions(-) rename src/pages/workspace/{WorkspaceInviteMessagePage.js => WorkspaceInviteMessagePage.tsx} (60%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9693c907a5fe..a9e59a55e172 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -460,6 +460,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; + [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 2973228af51f..2bd15fc96983 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -200,7 +200,7 @@ function getPersonalDetailsForAccountIDs(accountIDs, personalDetails) { /** * Return true if personal details data is ready, i.e. report list options can be created. - * @param {Object} personalDetails + * @param {Object | null} personalDetails * @returns {Boolean} */ function isPersonalDetailsReady(personalDetails) { diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.tsx similarity index 60% rename from src/pages/workspace/WorkspaceInviteMessagePage.js rename to src/pages/workspace/WorkspaceInviteMessagePage.tsx index 00bdce30891a..22bdd9c8db94 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -1,9 +1,10 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; +import lodashDebounce from 'lodash/debounce'; import React, {useEffect, useState} from 'react'; import {Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -13,118 +14,96 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withNavigationFocus from '@components/withNavigationFocus'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import * as Link from '@userActions/Link'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetailsList} from '@src/types/onyx'; +import type {Errors, Icon} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import SearchInputManager from './SearchInputManager'; -import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; -const personalDetailsPropTypes = PropTypes.shape({ - /** The accountID of the person */ - accountID: PropTypes.number.isRequired, - - /** The login of the person (either email or phone number) */ - login: PropTypes.string, - - /** 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, except for anon users) */ - avatar: PropTypes.string, - - /** This is either the user's full name, or their login if full name is an empty string */ - displayName: PropTypes.string, -}); - -const propTypes = { +type WorkspaceInviteMessagePageOnyxProps = { /** All of the personal details for everyone */ - allPersonalDetails: PropTypes.objectOf(personalDetailsPropTypes), - - invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number), + allPersonalDetails: OnyxEntry; - /** 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, + /** An object containing the accountID for every invited user email */ + invitedEmailsToAccountIDsDraft: OnyxEntry>; - ...policyPropTypes, + /** Updated workspace invite message */ + workspaceInviteMessageDraft: OnyxEntry; }; -const defaultProps = { - ...policyDefaultProps, - allPersonalDetails: {}, - invitedEmailsToAccountIDsDraft: {}, -}; +type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & + WorkspaceInviteMessagePageOnyxProps & + StackScreenProps; -function WorkspaceInviteMessagePage(props) { +function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsToAccountIDsDraft, policy, route, allPersonalDetails}: WorkspaceInviteMessagePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [welcomeNote, setWelcomeNote] = useState(); + const [welcomeNote, setWelcomeNote] = useState(); const {inputCallbackRef} = useAutoFocusInput(); const getDefaultWelcomeNote = () => - props.workspaceInviteMessageDraft || + // workspaceInviteMessageDraft can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + workspaceInviteMessageDraft || translate('workspace.inviteMessage.welcomeNote', { - workspaceName: props.policy.name, + workspaceName: policy?.name ?? '', }); useEffect(() => { - if (!_.isEmpty(props.invitedEmailsToAccountIDsDraft)) { + if (!isEmptyObject(invitedEmailsToAccountIDsDraft)) { setWelcomeNote(getDefaultWelcomeNote()); return; } - Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID), true); + Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const debouncedSaveDraft = _.debounce((newDraft) => { - Policy.setWorkspaceInviteMessageDraft(props.route.params.policyID, newDraft); + const debouncedSaveDraft = lodashDebounce((newDraft: string) => { + Policy.setWorkspaceInviteMessageDraft(route.params.policyID, newDraft); }); const sendInvitation = () => { Keyboard.dismiss(); - Policy.addMembersToWorkspace(props.invitedEmailsToAccountIDsDraft, welcomeNote, props.route.params.policyID); - Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {}); + Policy.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, welcomeNote ?? '', route.params.policyID); + Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {}); SearchInputManager.searchInput = ''; // Pop the invite message page before navigating to the members page. Navigation.goBack(ROUTES.HOME); - Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID)); }; - /** - * Opens privacy url as an external link - * @param {Object} event - */ - const openPrivacyURL = (event) => { - event.preventDefault(); + /** Opens privacy url as an external link */ + const openPrivacyURL = (event: GestureResponderEvent | KeyboardEvent | undefined) => { + event?.preventDefault(); Link.openExternalLink(CONST.PRIVACY_URL); }; - const validate = () => { - const errorFields = {}; - if (_.isEmpty(props.invitedEmailsToAccountIDsDraft)) { + const validate = (): Errors => { + const errorFields: Errors = {}; + if (isEmptyObject(invitedEmailsToAccountIDsDraft)) { errorFields.welcomeMessage = 'workspace.inviteMessage.inviteNoMembersError'; } return errorFields; }; - const policyName = lodashGet(props.policy, 'name'); + const policyName = policy?.name; return ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} > Navigation.dismissModal()} - onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID))} /> + {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} { + onChangeText={(text: string) => { setWelcomeNote(text); debouncedSaveDraft(text); }} - ref={(el) => { - if (!el) { + ref={(element) => { + if (!element) { return; } - inputCallbackRef(el); - updateMultilineInputRange(el); + inputCallbackRef(element); + updateMultilineInputRange(element); }} /> @@ -210,13 +198,10 @@ function WorkspaceInviteMessagePage(props) { ); } -WorkspaceInviteMessagePage.propTypes = propTypes; -WorkspaceInviteMessagePage.defaultProps = defaultProps; WorkspaceInviteMessagePage.displayName = 'WorkspaceInviteMessagePage'; -export default compose( - withPolicyAndFullscreenLoading, - withOnyx({ +export default withPolicyAndFullscreenLoading( + withOnyx({ allPersonalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, @@ -226,6 +211,5 @@ export default compose( workspaceInviteMessageDraft: { key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${route.params.policyID.toString()}`, }, - }), - withNavigationFocus, -)(WorkspaceInviteMessagePage); + })(WorkspaceInviteMessagePage), +); diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 8817f813a990..48e8d60a5a51 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -1,4 +1,3 @@ -import type {RouteProp} from '@react-navigation/native'; import React, {useEffect, useMemo, useRef} from 'react'; import type {ReactNode} from 'react'; import {View} from 'react-native'; @@ -20,6 +19,7 @@ import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {PolicyRoute} from './withPolicy'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -40,7 +40,7 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps & headerText: string; /** The route object passed to this page from the navigator */ - route: RouteProp<{params: {policyID: string}}>; + route: PolicyRoute; /** Main content of the page */ children: (hasVBA?: boolean, policyID?: string, isUsingECard?: boolean) => ReactNode; @@ -92,7 +92,7 @@ function WorkspacePageWithSections({ const isLoading = reimbursementAccount?.isLoading ?? true; const achState = reimbursementAccount?.achData?.state ?? ''; const isUsingECard = user?.isUsingExpensifyCard ?? false; - const policyID = route.params.policyID; + const policyID = route.params?.policyID; const policyName = policy?.name; const hasVBA = achState === BankAccount.STATE.OPEN; const content = children(hasVBA, policyID, isUsingECard); @@ -132,7 +132,7 @@ function WorkspacePageWithSections({ subtitle={policyName} shouldShowGetAssistanceButton guidesCallTaskID={guidesCallTaskID} - onBackButtonPress={() => Navigation.goBack(backButtonRoute ?? ROUTES.WORKSPACE_INITIAL.getRoute(policyID))} + onBackButtonPress={() => Navigation.goBack(backButtonRoute ?? ROUTES.WORKSPACE_INITIAL.getRoute(policyID ?? ''))} /> {(isLoading || firstRender.current) && shouldShowLoading ? ( diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx index 79ff76204c69..ffd9a700ae7e 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx @@ -1,15 +1,14 @@ -import type {RouteProp} from '@react-navigation/native'; +import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import useLocalize from '@hooks/useLocalize'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; import WorkspaceInvoicesNoVBAView from './WorkspaceInvoicesNoVBAView'; import WorkspaceInvoicesVBAView from './WorkspaceInvoicesVBAView'; -/** Defined route object that contains the policyID param, WorkspacePageWithSections is a common component for Workspaces and expect the route prop that includes the policyID */ -type WorkspaceInvoicesPageProps = { - route: RouteProp<{params: {policyID: string}}>; -}; +type WorkspaceInvoicesPageProps = StackScreenProps; function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) { const {translate} = useLocalize(); diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index ec38b61fb0dc..aee03f1f74e9 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -5,13 +5,16 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {forwardRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import policyMemberPropType from '@pages/policyMemberPropType'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; -type PolicyRoute = RouteProp<{params: {policyID: string}}>; +type PolicyRoute = RouteProp>; function getPolicyIDFromRoute(route: PolicyRoute): string { return route?.params?.policyID ?? ''; @@ -131,4 +134,4 @@ export default function (WrappedComponent: } export {policyPropTypes, policyDefaultProps}; -export type {WithPolicyOnyxProps, WithPolicyProps}; +export type {WithPolicyOnyxProps, WithPolicyProps, PolicyRoute}; From 371e1665a81aa89ad8a02593b70c2ac0fa184fe5 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Wed, 24 Jan 2024 16:34:32 -0800 Subject: [PATCH 134/446] Support client-side violations in new commands --- src/libs/actions/IOU.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 574a1a027440..06084fc0704f 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1167,12 +1167,15 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val, p * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {String} val + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestBillable(transactionID, transactionThreadReportID, val) { +function updateMoneyRequestBillable(transactionID, transactionThreadReportID, val, policy, policyTags, policyCategories) { const transactionChanges = { billable: val, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestBillable', params, onyxData); } @@ -1218,12 +1221,15 @@ function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag, po * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {String} category + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestCategory(transactionID, transactionThreadReportID, category) { +function updateMoneyRequestCategory(transactionID, transactionThreadReportID, category, policy, policyTags, policyCategories) { const transactionChanges = { category, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestCategory', params, onyxData); } @@ -1233,12 +1239,15 @@ function updateMoneyRequestCategory(transactionID, transactionThreadReportID, ca * @param {String} transactionID * @param {Number} transactionThreadReportID * @param {String} comment + * @param {Object} policy - May be undefined, an empty object, or an object matching the Policy type (src/types/onyx/Policy.ts) + * @param {Array} policyTags + * @param {Array} policyCategories */ -function updateMoneyRequestDescription(transactionID, transactionThreadReportID, comment) { +function updateMoneyRequestDescription(transactionID, transactionThreadReportID, comment, policy, policyTags, policyCategories) { const transactionChanges = { comment, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); API.write('UpdateMoneyRequestDescription', params, onyxData); } From 118057b886eac8fcf04fa0c9544e5dd16bc885e0 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 25 Jan 2024 09:46:15 +0100 Subject: [PATCH 135/446] Reuse policyName through the file --- src/pages/workspace/WorkspaceInitialPage.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 1db94faaef1c..4ac77af0bc2e 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -83,7 +83,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); - const policyID = useMemo(() => policy?.id ?? '', [policy]); + const policyID = policy?.id ?? ''; + const policyName = policy?.name ?? ''; const [policyReports, adminsRoom, announceRoom] = useMemo(() => { const reports: OnyxTypes.Report[] = []; let admins: OnyxTypes.Report | undefined; @@ -115,11 +116,11 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports /** Call the delete policy and hide the modal */ const confirmDeleteAndHideModal = useCallback(() => { - Policy.deleteWorkspace(policyID, policyReports.filter(shouldArchiveReport), policy?.name ?? ''); + Policy.deleteWorkspace(policyID, policyReports.filter(shouldArchiveReport), policyName); setIsDeleteModalOpen(false); // Pop the deleted workspace page before opening workspace settings. Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); - }, [policyID, policy?.name, policyReports]); + }, [policyID, policyName, policyReports]); useEffect(() => { const policyDraftId = policyDraft?.id; @@ -142,12 +143,11 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports /** Call update workspace currency and hide the modal */ const confirmCurrencyChangeAndHideModal = useCallback(() => { - Policy.updateGeneralSettings(policyID, policy?.name ?? '', CONST.CURRENCY.USD); + Policy.updateGeneralSettings(policyID, policyName, CONST.CURRENCY.USD); setIsCurrencyModalOpen(false); ReimbursementAccount.navigateToBankAccountRoute(policyID); - }, [policyID, policy?.name]); + }, [policyID, policyName]); - const policyName = policy?.name ?? ''; const hasMembersError = PolicyUtils.hasPolicyMemberError(policyMembers); const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatar ?? {}); const menuItems: WorkspaceMenuItem[] = [ @@ -193,7 +193,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports icon: Expensicons.Bank, action: () => policy?.outputCurrency === CONST.CURRENCY.USD - ? singleExecution(waitForNavigate(() => ReimbursementAccount.navigateToBankAccountRoute(policy.id, Navigation.getActiveRouteWithoutParams())))() + ? singleExecution(waitForNavigate(() => ReimbursementAccount.navigateToBankAccountRoute(policyID, Navigation.getActiveRouteWithoutParams())))() : setIsCurrencyModalOpen(true), brickRoadIndicator: !isEmptyObject(reimbursementAccount?.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, @@ -287,12 +287,12 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports /> - {!!policy?.name && ( + {!!policyName && ( openEditor(policy.id)))} + onPress={singleExecution(waitForNavigate(() => openEditor(policyID)))} accessibilityLabel={translate('workspace.common.settings')} role={CONST.ROLE.BUTTON} > @@ -300,7 +300,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports numberOfLines={1} style={[styles.textHeadline, styles.alignSelfCenter, styles.pre]} > - {policy.name} + {policyName} From 4ce1443b839da2a75ada8193ecbbf6aea46958dd Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 15:48:02 +0100 Subject: [PATCH 136/446] ref: move ReportDetailsPage to TS --- src/components/DisplayNames/types.ts | 2 +- src/libs/PolicyUtils.ts | 2 +- src/libs/ReportUtils.ts | 2 +- ...rtDetailsPage.js => ReportDetailsPage.tsx} | 182 +++++++++--------- .../home/report/withReportOrNotFound.tsx | 8 +- 5 files changed, 94 insertions(+), 102 deletions(-) rename src/pages/{ReportDetailsPage.js => ReportDetailsPage.tsx} (65%) diff --git a/src/components/DisplayNames/types.ts b/src/components/DisplayNames/types.ts index 2e6f36d5cc07..7da1819c9f01 100644 --- a/src/components/DisplayNames/types.ts +++ b/src/components/DisplayNames/types.ts @@ -20,7 +20,7 @@ type DisplayNamesProps = { fullTitle: string; /** Array of objects that map display names to their corresponding tooltip */ - displayNamesWithTooltips: DisplayNameWithTooltip[]; + displayNamesWithTooltips?: DisplayNameWithTooltip[]; /** Number of lines before wrapping */ numberOfLines: number; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index b8ed62f93082..47916dc474ba 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -112,7 +112,7 @@ function isExpensifyGuideTeam(email: string): boolean { */ const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === CONST.POLICY.ROLE.ADMIN; -const isPolicyMember = (policyID: string, policies: Record): boolean => Object.values(policies).some((policy) => policy?.id === policyID); +const isPolicyMember = (policyID: string, policies: OnyxCollection): boolean => Object.values(policies ?? {}).some((policy) => policy?.id === policyID); /** * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e9c3b1710cc0..fb15feb38d59 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4540,7 +4540,7 @@ function shouldAutoFocusOnKeyPress(event: KeyboardEvent): boolean { /** * Navigates to the appropriate screen based on the presence of a private note for the current user. */ -function navigateToPrivateNotes(report: Report, session: Session) { +function navigateToPrivateNotes(report: OnyxEntry, session: OnyxEntry) { if (isEmpty(report) || isEmpty(session) || !session.accountID) { return; } diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.tsx similarity index 65% rename from src/pages/ReportDetailsPage.js rename to src/pages/ReportDetailsPage.tsx index 3e682d592370..9a9ca49c68eb 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.tsx @@ -1,103 +1,99 @@ -import PropTypes from 'prop-types'; +import type {FC} from 'react'; import React, {useEffect, useMemo} from 'react'; import {ScrollView, View} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {SvgProps} from 'react-native-svg'; +import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DisplayNames from '@components/DisplayNames'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MultipleAvatars from '@components/MultipleAvatars'; -import {withNetwork} from '@components/OnyxProvider'; import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; -import participantPropTypes from '@components/participantPropTypes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import RoomHeaderAvatars from '@components/RoomHeaderAvatars'; import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; import withReportOrNotFound from './home/report/withReportOrNotFound'; -import reportPropTypes from './reportPropTypes'; -const propTypes = { - ...withLocalizePropTypes, - - /** The report currently being looked at */ - report: reportPropTypes.isRequired, - - /** The policies which the user has access to and which the report could be tied to */ - policies: PropTypes.shape({ - /** Name of the policy */ - name: PropTypes.string, - }), - - /** Route params */ - route: PropTypes.shape({ - params: PropTypes.shape({ - /** Report ID passed via route r/:reportID/details */ - reportID: PropTypes.string, - }), - }).isRequired, - - /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), +type ReportDetailsPageMenuItem = { + key: DeepValueOf; + translationKey: TranslationPaths; + icon: FC; + isAnonymousAction: boolean; + action: () => void; + brickRoadIndicator?: ValueOf; + subtitle?: number; }; -const defaultProps = { - policies: {}, - personalDetails: {}, +type ReportDetailsPageOnyxProps = { + personalDetails: OnyxCollection; + session: OnyxEntry; }; +type ReportDetailsPageProps = { + report: OnyxEntry; +} & ReportDetailsPageOnyxProps & + WithReportOrNotFoundProps; -function ReportDetailsPage(props) { +function ReportDetailsPage({policies, report, session, personalDetails}: ReportDetailsPageProps) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); const styles = useThemeStyles(); - const policy = useMemo(() => props.policies[`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`], [props.policies, props.report.policyID]); - const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy), [policy]); - const isPolicyMember = useMemo(() => PolicyUtils.isPolicyMember(props.report.policyID, props.policies), [props.report.policyID, props.policies]); - const shouldUseFullTitle = useMemo(() => ReportUtils.shouldUseFullTitleToDisplay(props.report), [props.report]); - const isChatRoom = useMemo(() => ReportUtils.isChatRoom(props.report), [props.report]); - const isThread = useMemo(() => ReportUtils.isChatThread(props.report), [props.report]); - const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(props.report), [props.report]); - const isArchivedRoom = useMemo(() => ReportUtils.isArchivedRoom(props.report), [props.report]); - const isMoneyRequestReport = useMemo(() => ReportUtils.isMoneyRequestReport(props.report), [props.report]); + const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`], [policies, report?.policyID]); + const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy ?? null), [policy]); + const isPolicyMember = useMemo(() => PolicyUtils.isPolicyMember(report?.policyID ?? '', policies), [report?.policyID, policies]); + const shouldUseFullTitle = useMemo(() => ReportUtils.shouldUseFullTitleToDisplay(report), [report]); + const isChatRoom = useMemo(() => ReportUtils.isChatRoom(report), [report]); + const isThread = useMemo(() => ReportUtils.isChatThread(report), [report]); + const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(report), [report]); + const isArchivedRoom = useMemo(() => ReportUtils.isArchivedRoom(report), [report]); + const isMoneyRequestReport = useMemo(() => ReportUtils.isMoneyRequestReport(report), [report]); // eslint-disable-next-line react-hooks/exhaustive-deps -- policy is a dependency because `getChatRoomSubtitle` calls `getPolicyName` which in turn retrieves the value from the `policy` value stored in Onyx - const chatRoomSubtitle = useMemo(() => ReportUtils.getChatRoomSubtitle(props.report), [props.report, policy]); - const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(props.report); - const participants = useMemo(() => ReportUtils.getVisibleMemberIDs(props.report), [props.report]); + const chatRoomSubtitle = useMemo(() => ReportUtils.getChatRoomSubtitle(report), [report, policy]); + const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); + const participants = useMemo(() => ReportUtils.getVisibleMemberIDs(report), [report]); - const isGroupDMChat = useMemo(() => ReportUtils.isDM(props.report) && participants.length > 1, [props.report, participants.length]); + const isGroupDMChat = useMemo(() => ReportUtils.isDM(report) && participants.length > 1, [report, participants.length]); - const isPrivateNotesFetchTriggered = !_.isUndefined(props.report.isLoadingPrivateNotes); + const isPrivateNotesFetchTriggered = report?.isLoadingPrivateNotes !== undefined; useEffect(() => { // Do not fetch private notes if isLoadingPrivateNotes is already defined, or if network is offline. - if (isPrivateNotesFetchTriggered || props.network.isOffline) { + if (isPrivateNotesFetchTriggered || isOffline) { return; } - Report.getReportPrivateNote(props.report.reportID); - }, [props.report.reportID, props.network.isOffline, isPrivateNotesFetchTriggered]); + Report.getReportPrivateNote(report?.reportID ?? ''); + }, [report?.reportID, isOffline, isPrivateNotesFetchTriggered]); - const menuItems = useMemo(() => { + const menuItems: ReportDetailsPageMenuItem[] = useMemo(() => { const items = []; if (!isGroupDMChat) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.SHARE_CODE, - translationKey: 'common.shareCode', + translationKey: 'common.shareCode' as const, icon: Expensicons.QrCode, isAnonymousAction: true, - action: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(props.report.reportID)), + action: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(report?.reportID ?? '')), }); } @@ -111,61 +107,62 @@ function ReportDetailsPage(props) { if ((!isUserCreatedPolicyRoom && participants.length) || (isUserCreatedPolicyRoom && isPolicyMember)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.MEMBERS, - translationKey: 'common.members', + translationKey: 'common.members' as const, icon: Expensicons.Users, subtitle: participants.length, isAnonymousAction: false, action: () => { - if (isUserCreatedPolicyRoom && !props.report.parentReportID) { - Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(props.report.reportID)); + if (isUserCreatedPolicyRoom && !report?.parentReportID) { + Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? '')); } else { - Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report?.reportID ?? '')); } }, }); - } else if (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember) && !props.report.parentReportID) { + } else if (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember) && !report?.parentReportID) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.INVITE, - translationKey: 'common.invite', + translationKey: 'common.invite' as const, icon: Expensicons.Users, isAnonymousAction: false, action: () => { - Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.ROOM_INVITE.getRoute(report?.reportID ?? '')); }, }); } items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS, - translationKey: 'common.settings', + translationKey: 'common.settings' as const, icon: Expensicons.Gear, isAnonymousAction: false, action: () => { - Navigation.navigate(ROUTES.REPORT_SETTINGS.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '')); }, }); // Prevent displaying private notes option for threads and task reports - if (!isThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(props.report)) { + if (!isThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(report)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.PRIVATE_NOTES, - translationKey: 'privateNotes.title', + translationKey: 'privateNotes.title' as const, icon: Expensicons.Pencil, isAnonymousAction: false, - action: () => ReportUtils.navigateToPrivateNotes(props.report, props.session), - brickRoadIndicator: Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', + action: () => ReportUtils.navigateToPrivateNotes(report, session), + brickRoadIndicator: Report.hasErrorInPrivateNotes(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }); } return items; - }, [isArchivedRoom, participants.length, isThread, isMoneyRequestReport, props.report, isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom, props.session]); + }, [isArchivedRoom, participants.length, isThread, isMoneyRequestReport, report, isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom, session]); const displayNamesWithTooltips = useMemo(() => { const hasMultipleParticipants = participants.length > 1; - return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, props.personalDetails), hasMultipleParticipants); - }, [participants, props.personalDetails]); + // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. + return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails), hasMultipleParticipants); + }, [participants, personalDetails]); - const icons = useMemo(() => ReportUtils.getIcons(props.report, props.personalDetails, props.policies), [props.report, props.personalDetails, props.policies]); + const icons = useMemo(() => ReportUtils.getIcons(report, personalDetails, policies), [report, personalDetails, policies]); const chatRoomSubtitleText = chatRoomSubtitle ? ( - + { Navigation.goBack(); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.report.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID ?? '')); }} /> @@ -199,14 +196,14 @@ function ReportDetailsPage(props) { ) : ( )} { - Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(props.report.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(report?.policyID ?? '')); }} > {chatRoomSubtitleText} @@ -229,28 +227,28 @@ function ReportDetailsPage(props) { ) : ( chatRoomSubtitleText )} - {!_.isEmpty(parentNavigationSubtitleData) && isMoneyRequestReport && ( + {!isEmptyObject(parentNavigationSubtitleData) && isMoneyRequestReport && ( )} - {_.map(menuItems, (item) => { + {menuItems.map((item) => { const brickRoadIndicator = - ReportUtils.hasReportNameError(props.report) && item.key === CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + ReportUtils.hasReportNameError(report) && item.key === CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined; return ( ); })} @@ -261,22 +259,14 @@ function ReportDetailsPage(props) { } ReportDetailsPage.displayName = 'ReportDetailsPage'; -ReportDetailsPage.propTypes = propTypes; -ReportDetailsPage.defaultProps = defaultProps; -export default compose( - withLocalize, - withReportOrNotFound(), - withNetwork(), - withOnyx({ +export default withReportOrNotFound()( + withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, session: { key: ONYXKEYS.SESSION, }, - }), -)(ReportDetailsPage); + })(ReportDetailsPage), +); diff --git a/src/pages/home/report/withReportOrNotFound.tsx b/src/pages/home/report/withReportOrNotFound.tsx index 7613bafeacdc..a8facc3e1c76 100644 --- a/src/pages/home/report/withReportOrNotFound.tsx +++ b/src/pages/home/report/withReportOrNotFound.tsx @@ -22,16 +22,16 @@ type OnyxProps = { isLoadingReportData: OnyxEntry; }; -type ComponentProps = OnyxProps & { +type WithReportOrNotFoundProps = OnyxProps & { route: RouteProp<{params: {reportID: string}}>; }; export default function ( shouldRequireReportID = true, -): ( +): ( WrappedComponent: React.ComponentType>, ) => React.ComponentType, keyof OnyxProps>> { - return function (WrappedComponent: ComponentType>) { + return function (WrappedComponent: ComponentType>) { function WithReportOrNotFound(props: TProps, ref: ForwardedRef) { const contentShown = React.useRef(false); @@ -89,3 +89,5 @@ export default function ( })(React.forwardRef(WithReportOrNotFound)); }; } + +export type {WithReportOrNotFoundProps}; From 8bf49b65d002766aa90e71ff7a03eba6c01a854d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 15:53:52 +0100 Subject: [PATCH 137/446] fix: typecheck --- src/components/DisplayNames/DisplayNamesWithTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx index ce0ae7ddcf4f..1cacb0e20c5d 100644 --- a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx +++ b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx @@ -56,7 +56,7 @@ function DisplayNamesWithToolTip({shouldUseFullTitle, fullTitle, displayNamesWit > {shouldUseFullTitle ? ReportUtils.formatReportLastMessageText(fullTitle) - : displayNamesWithTooltips.map(({displayName, accountID, avatar, login}, index) => ( + : displayNamesWithTooltips?.map(({displayName, accountID, avatar, login}, index) => ( // eslint-disable-next-line react/no-array-index-key Date: Thu, 25 Jan 2024 15:59:50 +0100 Subject: [PATCH 138/446] fix: add comments to props --- src/pages/ReportDetailsPage.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 9a9ca49c68eb..bceaeddc0349 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -44,10 +44,14 @@ type ReportDetailsPageMenuItem = { }; type ReportDetailsPageOnyxProps = { + /** Personal details of all the users */ personalDetails: OnyxCollection; + + /** Session info for the currently logged in user. */ session: OnyxEntry; }; type ReportDetailsPageProps = { + /** The report currently being looked at */ report: OnyxEntry; } & ReportDetailsPageOnyxProps & WithReportOrNotFoundProps; From 7467e2c5a4fa037024c7a13b38898eca0d704436 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 25 Jan 2024 17:29:11 +0100 Subject: [PATCH 139/446] prettify codebase --- src/libs/NextStepUtils.ts | 64 +++++++++++++++++---------------- src/types/onyx/Report.ts | 3 -- tests/unit/NextStepUtilsTest.ts | 26 +++++++------- 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 9a5ec66db948..f606afbc6702 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -57,17 +57,18 @@ type BuildNextStepParameters = { * * @param report * @param predictedNextStatus - a next expected status of the report - * @param parameters.isPaidWithWallet - Whether a report has been paid with wallet or outside of Expensify + * @param parameters.isPaidWithWallet - Whether a report has been paid with the wallet or outside of Expensify * @returns nextStep */ function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { - const {isPreventSelfApprovalEnabled = false, ownerAccountID = -1, managerID} = report; const policy = ReportUtils.getPolicy(report.policyID ?? ''); - const isManager = currentUserAccountID === managerID; + const {submitsTo, isHarvestingEnabled, isPreventSelfApprovalEnabled, isAutoApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; + const {ownerAccountID = -1, managerID = -1} = report; const isOwner = currentUserAccountID === ownerAccountID; + const isManager = currentUserAccountID === managerID; + const isSelfApproval = currentUserAccountID === submitsTo; const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; - const isSelfApproval = currentUserAccountID === policy.submitsTo; - const submitterDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(policy.submitsTo, true) ?? ''; + const submitterDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(submitsTo, true) ?? ''; const type: ReportNextStep['type'] = 'neutral'; let optimisticNextStep: ReportNextStep | null; @@ -100,7 +101,7 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf; - - /** Whether the user can do self approve or submit of an expense report */ - isPreventSelfApprovalEnabled?: boolean; }; export default Report; diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 5c6aef46a87f..5de6f539ea53 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -1,4 +1,4 @@ -import {format, lastDayOfMonth} from 'date-fns'; +import {format, lastDayOfMonth, setDate} from 'date-fns'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -146,7 +146,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'immediate', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -170,7 +170,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'weekly', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -194,7 +194,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'semimonthly', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -218,7 +218,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'monthly', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: 2, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -243,8 +243,8 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'monthly', - autoReportingOffset: 'lastDayOfMonth', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, + autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_DAY_OF_MONTH, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -259,7 +259,7 @@ describe('libs/NextStepUtils', () => { text: 'These expenses are scheduled to ', }, { - text: `automatically submit on the ${format(new Date().setDate(lastBusinessDayOfMonth), CONST.DATE.ORDINAL_DAY_OF_MONTH)} of each month!`, + text: `automatically submit on the ${format(setDate(new Date(), lastBusinessDayOfMonth), CONST.DATE.ORDINAL_DAY_OF_MONTH)} of each month!`, type: 'strong', }, { @@ -269,8 +269,8 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'monthly', - autoReportingOffset: 'lastBusinessDayOfMonth', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, + autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -294,7 +294,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'trip', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -325,7 +325,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { isHarvestingEnabled: true, - autoReportingFrequency: 'manual', + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -335,7 +335,6 @@ describe('libs/NextStepUtils', () => { }); test('prevented self submitting', () => { - report.isPreventSelfApprovalEnabled = true; optimisticNextStep.title = 'Next Steps:'; optimisticNextStep.message = [ { @@ -359,6 +358,7 @@ describe('libs/NextStepUtils', () => { return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { submitsTo: currentUserAccountID, + isPreventSelfApprovalEnabled: true, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); From 38bfb704fc7a94d0c3e15ef5b9bb89a576876619 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 25 Jan 2024 17:50:22 +0100 Subject: [PATCH 140/446] remove one case for now --- src/libs/NextStepUtils.ts | 9 +-------- src/types/onyx/Policy.ts | 3 --- tests/unit/NextStepUtilsTest.ts | 35 --------------------------------- 3 files changed, 1 insertion(+), 46 deletions(-) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index f606afbc6702..dec6acaecec2 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -62,7 +62,7 @@ type BuildNextStepParameters = { */ function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const policy = ReportUtils.getPolicy(report.policyID ?? ''); - const {submitsTo, isHarvestingEnabled, isPreventSelfApprovalEnabled, isAutoApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; + const {submitsTo, isHarvestingEnabled, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; const {ownerAccountID = -1, managerID = -1} = report; const isOwner = currentUserAccountID === ownerAccountID; const isManager = currentUserAccountID === managerID; @@ -174,13 +174,6 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { owner: currentUserEmail, submitsTo: currentUserAccountID, isHarvestingEnabled: false, - isAutoApprovalEnabled: false, // Required props name: 'Policy', role: 'admin', @@ -91,40 +90,6 @@ describe('libs/NextStepUtils', () => { expect(result).toMatchObject(optimisticNextStep); }); - test('self review and auto approval enabled', () => { - optimisticNextStep.title = 'Next Steps:'; - optimisticNextStep.message = [ - { - text: 'Waiting for ', - }, - { - text: 'you', - type: 'strong', - }, - { - text: ' to ', - }, - { - text: 'submit', - type: 'strong', - }, - { - text: ' these expenses.', - }, - { - text: ' This report may be selected at random for manual approval.', - }, - ]; - - return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isAutoApprovalEnabled: true, - }).then(() => { - const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); - - expect(result).toMatchObject(optimisticNextStep); - }); - }); - describe('scheduled submit enabled', () => { beforeEach(() => { optimisticNextStep.title = 'Next Steps:'; From 9ae124c5d49769a6593ee8fce9b2c7afda301623 Mon Sep 17 00:00:00 2001 From: Shahe Shahinyan Date: Fri, 26 Jan 2024 12:48:58 +0400 Subject: [PATCH 141/446] Remove extra view --- .../signin/SignInPageLayout/BackgroundImage/index.ios.js | 1 - src/styles/index.ts | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js index 9428cc3581db..45d7bf3050ef 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js @@ -27,7 +27,6 @@ function BackgroundImage(props) { return ( - position: 'absolute', top: 0, left: 0, + backgroundColor: colors.productDark200, + justifyContent: "flex-end" }, - signInBackgroundTopView: { - flex: 1, - width: '100%', - backgroundColor: colors.productDark200 - }, + signInBackgroundImage: { height: 700, }, From 6319242393ac692609a235f594988e1671efbfa4 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 26 Jan 2024 17:31:32 +0700 Subject: [PATCH 142/446] fix: incorrect error message appear when enter Cyrillic letters into Zip field --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index fc426002809a..876edc3a33e9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1150,7 +1150,7 @@ export default { dateShouldBeBefore: ({dateString}: DateShouldBeBeforeParams) => `Date should be before ${dateString}.`, dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `Date should be after ${dateString}.`, hasInvalidCharacter: 'Name can only include Latin characters.', - incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams) => `Incorrect zip code format.${zipFormat ? ` Acceptable format: ${zipFormat}` : ''}`, + incorrectZipFormat: (params: IncorrectZipFormatParams) => `Incorrect zip code format.${params?.zipFormat ? ` Acceptable format: ${params?.zipFormat}` : ''}`, }, }, resendValidationForm: { From fe08d30f0ba5a896aba9bda6804b19421f813df5 Mon Sep 17 00:00:00 2001 From: Shahe Shahinyan Date: Fri, 26 Jan 2024 15:11:33 +0400 Subject: [PATCH 143/446] fix lint error --- .../signin/SignInPageLayout/BackgroundImage/index.ios.js | 4 ++-- src/styles/index.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js index 45d7bf3050ef..6e933fc7f2a2 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js @@ -1,3 +1,4 @@ +import {View} from "react-native"; import {Image} from 'expo-image'; import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; @@ -5,9 +6,8 @@ import DesktopBackgroundImage from '@assets/images/home-background--desktop.svg' import MobileBackgroundImage from '@assets/images/home-background--mobile-new.svg'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import defaultPropTypes from './propTypes'; import useWindowDimensions from "@hooks/useWindowDimensions"; -import {View} from "react-native"; +import defaultPropTypes from './propTypes'; const defaultProps = { isSmallScreen: false, diff --git a/src/styles/index.ts b/src/styles/index.ts index 5476642d47b6..52ebb580254b 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1267,7 +1267,6 @@ const styles = (theme: ThemeColors) => justifyContent: "flex-end" }, - signInBackgroundImage: { height: 700, }, From ffb817a6583936e521c88ed4bc3b992ef6c70312 Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Fri, 26 Jan 2024 14:13:39 +0100 Subject: [PATCH 144/446] Migrate 'App.js' file to TypeScript --- src/{App.js => App.tsx} | 2 ++ .../CustomStatusBarAndBackground/index.tsx | 2 +- src/components/ThemeProvider.tsx | 8 ------- ...ontext.js => ReportAttachmentsContext.tsx} | 23 +++++++++---------- src/types/global.d.ts | 2 ++ 5 files changed, 16 insertions(+), 21 deletions(-) rename src/{App.js => App.tsx} (93%) rename src/pages/home/report/{ReportAttachmentsContext.js => ReportAttachmentsContext.tsx} (55%) diff --git a/src/App.js b/src/App.tsx similarity index 93% rename from src/App.js rename to src/App.tsx index 3553900bbc7f..6aa3c58b0d05 100644 --- a/src/App.js +++ b/src/App.tsx @@ -66,6 +66,7 @@ function App() { PopoverContextProvider, CurrentReportIDContextProvider, ReportAttachmentsProvider, + // @ts-expect-error TODO: Remove this once ReactChild is replaced with ReactNode in react-native-picker-select. PickerStateProvider, EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, @@ -74,6 +75,7 @@ function App() { + {/* @ts-expect-error TODO: Remove this once Expensify (https://github.com/Expensify/App/issues/25231) is migrated to TypeScript. */} diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index f66a0204ac5e..42ea96fe41bb 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -10,7 +10,7 @@ import updateStatusBarAppearance from './updateStatusBarAppearance'; type CustomStatusBarAndBackgroundProps = { /** Whether the CustomStatusBar is nested within another CustomStatusBar. * A nested CustomStatusBar will disable the "root" CustomStatusBar. */ - isNested: boolean; + isNested?: boolean; }; function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBackgroundProps) { diff --git a/src/components/ThemeProvider.tsx b/src/components/ThemeProvider.tsx index 76371bbbc9e1..600f6c191527 100644 --- a/src/components/ThemeProvider.tsx +++ b/src/components/ThemeProvider.tsx @@ -1,5 +1,3 @@ -/* eslint-disable react/jsx-props-no-spreading */ -import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; import useThemePreferenceWithStaticOverride from '@hooks/useThemePreferenceWithStaticOverride'; // eslint-disable-next-line no-restricted-imports @@ -7,11 +5,6 @@ import themes from '@styles/theme'; import ThemeContext from '@styles/theme/context/ThemeContext'; import type {ThemePreferenceWithoutSystem} from '@styles/theme/types'; -const propTypes = { - /** Rendered child component */ - children: PropTypes.node.isRequired, -}; - type ThemeProviderProps = React.PropsWithChildren & { theme?: ThemePreferenceWithoutSystem; }; @@ -24,7 +17,6 @@ function ThemeProvider({children, theme: staticThemePreference}: ThemeProviderPr return {children}; } -ThemeProvider.propTypes = propTypes; ThemeProvider.displayName = 'ThemeProvider'; export default ThemeProvider; diff --git a/src/pages/home/report/ReportAttachmentsContext.js b/src/pages/home/report/ReportAttachmentsContext.tsx similarity index 55% rename from src/pages/home/report/ReportAttachmentsContext.js rename to src/pages/home/report/ReportAttachmentsContext.tsx index 5602612a6cd6..cd641adcda35 100644 --- a/src/pages/home/report/ReportAttachmentsContext.js +++ b/src/pages/home/report/ReportAttachmentsContext.tsx @@ -1,17 +1,17 @@ -import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useRef} from 'react'; import useCurrentReportID from '@hooks/useCurrentReportID'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; -const ReportAttachmentsContext = React.createContext(); - -const propTypes = { - /** Rendered child component */ - children: PropTypes.node.isRequired, +type ReportAttachmentsContextValue = { + isAttachmentHidden?: (reportActionID: string) => boolean; + updateHiddenAttachments?: (reportActionID: string, isHidden: boolean) => void; }; -function ReportAttachmentsProvider(props) { +const ReportAttachmentsContext = React.createContext({}); + +function ReportAttachmentsProvider({children}: ChildrenProps) { const currentReportID = useCurrentReportID(); - const hiddenAttachments = useRef({}); + const hiddenAttachments = useRef>({}); useEffect(() => { // We only want to store the attachment visibility for the current report. @@ -21,8 +21,8 @@ function ReportAttachmentsProvider(props) { const contextValue = useMemo( () => ({ - isAttachmentHidden: (reportActionID) => hiddenAttachments.current[reportActionID], - updateHiddenAttachments: (reportActionID, value) => { + isAttachmentHidden: (reportActionID: string) => hiddenAttachments.current[reportActionID], + updateHiddenAttachments: (reportActionID: string, value: boolean) => { hiddenAttachments.current = { ...hiddenAttachments.current, [reportActionID]: value, @@ -32,10 +32,9 @@ function ReportAttachmentsProvider(props) { [], ); - return {props.children}; + return {children}; } -ReportAttachmentsProvider.propTypes = propTypes; ReportAttachmentsProvider.displayName = 'ReportAttachmentsProvider'; export default ReportAttachmentsContext; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 0a122f083c8d..3e3a7ca162ee 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -32,4 +32,6 @@ declare module '*.lottie' { interface Window { enableMemoryOnlyKeys: () => void; disableMemoryOnlyKeys: () => void; + setSupportToken: (token: string, email: string, accountID: number) => void; + Onyx: Record; } From 38b0b0ca01bfd4d1c724868537ee810f1ae946e0 Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Fri, 26 Jan 2024 15:37:53 +0100 Subject: [PATCH 145/446] update Onyx type --- src/types/global.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 3e3a7ca162ee..e68a438c2dd8 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -33,5 +33,5 @@ interface Window { enableMemoryOnlyKeys: () => void; disableMemoryOnlyKeys: () => void; setSupportToken: (token: string, email: string, accountID: number) => void; - Onyx: Record; + Onyx: Record; } From 6c1d85e254aa1770713dc59297307f5ab6d500a0 Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Fri, 26 Jan 2024 15:51:58 +0100 Subject: [PATCH 146/446] bump react-native-picker-select version --- package-lock.json | 12 ++++++------ package.json | 2 +- src/App.tsx | 1 - 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1328d498c79..0823f5453eeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,7 +100,7 @@ "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -45197,8 +45197,8 @@ }, "node_modules/react-native-picker-select": { "version": "8.1.0", - "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", - "integrity": "sha512-NpXXyK+UuANYOysjUb9pCoq9SookRYPfpOcM4shxOD4+2Fkh7TYt2LBUpAdBicMHmtaR43RWXVQk9pMimOhg2w==", + "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", + "integrity": "sha512-tnAoOobs7WVgXy6OOr0zaUHF18pL4rEcKcGWHQIDj3eZGgYPpNB5pXbd1po6iE1bHU233G40TCJNKLCtSOFvTA==", "license": "MIT", "dependencies": { "lodash.isequal": "^4.5.0" @@ -86013,9 +86013,9 @@ "requires": {} }, "react-native-picker-select": { - "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", - "integrity": "sha512-NpXXyK+UuANYOysjUb9pCoq9SookRYPfpOcM4shxOD4+2Fkh7TYt2LBUpAdBicMHmtaR43RWXVQk9pMimOhg2w==", - "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", + "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", + "integrity": "sha512-tnAoOobs7WVgXy6OOr0zaUHF18pL4rEcKcGWHQIDj3eZGgYPpNB5pXbd1po6iE1bHU233G40TCJNKLCtSOFvTA==", + "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", "requires": { "lodash.isequal": "^4.5.0" } diff --git a/package.json b/package.json index 96de7fb0ab77..b26dfb3bc64a 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", diff --git a/src/App.tsx b/src/App.tsx index 6aa3c58b0d05..8da4374a4731 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -66,7 +66,6 @@ function App() { PopoverContextProvider, CurrentReportIDContextProvider, ReportAttachmentsProvider, - // @ts-expect-error TODO: Remove this once ReactChild is replaced with ReactNode in react-native-picker-select. PickerStateProvider, EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, From 042f320b6c0f33c2d9f0f06d9f1b2afead5e553e Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Fri, 26 Jan 2024 16:03:12 +0100 Subject: [PATCH 147/446] make ReportAttachmentsContext params required --- src/pages/home/report/ReportAttachmentsContext.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportAttachmentsContext.tsx b/src/pages/home/report/ReportAttachmentsContext.tsx index cd641adcda35..d118d4d8bee9 100644 --- a/src/pages/home/report/ReportAttachmentsContext.tsx +++ b/src/pages/home/report/ReportAttachmentsContext.tsx @@ -3,11 +3,14 @@ import useCurrentReportID from '@hooks/useCurrentReportID'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; type ReportAttachmentsContextValue = { - isAttachmentHidden?: (reportActionID: string) => boolean; - updateHiddenAttachments?: (reportActionID: string, isHidden: boolean) => void; + isAttachmentHidden: (reportActionID: string) => boolean; + updateHiddenAttachments: (reportActionID: string, isHidden: boolean) => void; }; -const ReportAttachmentsContext = React.createContext({}); +const ReportAttachmentsContext = React.createContext({ + isAttachmentHidden: () => false, + updateHiddenAttachments: () => {}, +}); function ReportAttachmentsProvider({children}: ChildrenProps) { const currentReportID = useCurrentReportID(); From 8810e0abd4df346e558f0a002c44d2d317fe7483 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 26 Jan 2024 16:08:59 +0100 Subject: [PATCH 148/446] update form type, remove unused ts-expect --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 4 ++-- src/pages/settings/Wallet/Card/GetPhysicalCardName.tsx | 2 -- src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx | 1 - src/types/onyx/Form.ts | 4 ++-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 487e7057517f..627cfddcaf62 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -16,9 +16,10 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {CardList, GetPhysicalCardForm, LoginList, PrivatePersonalDetails, Session} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -type OnValidate = (values: OnyxEntry) => void; +type OnValidate = (values: OnyxEntry) => Errors; type RenderContentProps = ChildrenProps & { onSubmit: () => void; @@ -76,7 +77,6 @@ function DefaultRenderContent({onSubmit, submitButtonText, children, onValidate} const styles = useThemeStyles(); return ( - // @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript. ; -type GetPhysicalCardForm = Form & { +type GetPhysicalCardForm = Form<{ /** Address line 1 for delivery */ addressLine1?: string; @@ -81,7 +81,7 @@ type GetPhysicalCardForm = Form & { /** Zip code for delivery */ zipPostCode?: string; -}; +}>; export default Form; From 38ffff6f91b8895a20c9c53fc2fd0aac730f5cb7 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Fri, 26 Jan 2024 11:47:43 -0800 Subject: [PATCH 149/446] Pass policy args to all commands --- src/pages/EditRequestPage.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 18f10e09bdbc..3b71e7046158 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -186,21 +186,21 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p ({category: newCategory}) => { // In case the same category has been selected, reset the category. const updatedCategory = newCategory === transactionCategory ? '' : newCategory; - IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory); + IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory, policy, policyTags, policyCategories); Navigation.dismissModal(); }, - [transactionCategory, transaction.transactionID, report.reportID], + [transactionCategory, transaction.transactionID, report.reportID, policy, policyTags, policyCategories], ); const saveComment = useCallback( ({comment: newComment}) => { // Only update comment if it has changed if (newComment.trim() !== transactionDescription) { - IOU.updateMoneyRequestDescription(transaction.transactionID, report.reportID, newComment.trim()); + IOU.updateMoneyRequestDescription(transaction.transactionID, report.reportID, newComment.trim(), policy, policyTags, policyCategories); } Navigation.dismissModal(); }, - [transactionDescription, transaction.transactionID, report.reportID], + [transactionDescription, transaction.transactionID, report.reportID, policy, policyTags, policyCategories], ); if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DESCRIPTION) { From 58719e8053127e3fba727f0a5689d55c75071f9e Mon Sep 17 00:00:00 2001 From: tienifr Date: Sun, 28 Jan 2024 23:40:34 +0700 Subject: [PATCH 150/446] revert unnecessary change --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 154be33cb7d4..485bf9a3adc0 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -612,7 +612,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ {button} ); - }, [confirm, bankAccountRoute, iouCurrencyCode, iouType, isReadOnly, policyID, selectedParticipants, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); + }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); const {image: receiptImage, thumbnail: receiptThumbnail} = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; return ( From 07484dde21884177349364bd81c8d5395351f6dd Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Mon, 29 Jan 2024 00:04:32 +0100 Subject: [PATCH 151/446] use android notificaiton API to retrieve active notifications --- .../CustomNotificationProvider.java | 133 ++++++------ .../NotificationCache.java | 193 ------------------ 2 files changed, 76 insertions(+), 250 deletions(-) delete mode 100644 android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index c60476ad3f0a..fc9015e22a10 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -3,6 +3,7 @@ import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE; import static androidx.core.app.NotificationCompat.PRIORITY_MAX; +import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; @@ -15,6 +16,11 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.os.Build; +import android.os.Bundle; +import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; +import android.service.notification.StatusBarNotification; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; @@ -26,6 +32,7 @@ import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; import androidx.core.graphics.drawable.IconCompat; +import androidx.versionedparcelable.ParcelUtils; import com.urbanairship.AirshipConfigOptions; import com.urbanairship.json.JsonMap; @@ -39,8 +46,11 @@ import java.net.URL; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -49,9 +59,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import com.expensify.chat.customairshipextender.NotificationCache.NotificationData; -import com.expensify.chat.customairshipextender.NotificationCache.NotificationMessage; - public class CustomNotificationProvider extends ReactNotificationProvider { // Resize icons to 100 dp x 100 dp private static final int MAX_ICON_SIZE_DPS = 100; @@ -73,6 +80,13 @@ public class CustomNotificationProvider extends ReactNotificationProvider { private static final String PAYLOAD_KEY = "payload"; private static final String ONYX_DATA_KEY = "onyxData"; + // Notification extras keys + public static final String EXTRAS_REPORT_ID_KEY = "reportID"; + public static final String EXTRAS_AVATAR_KEY = "avatar"; + public static final String EXTRAS_NAME_KEY = "name"; + public static final String EXTRAS_ACCOUNT_ID_KEY = "accountID"; + + private final ExecutorService executorService = Executors.newCachedThreadPool(); public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { @@ -158,7 +172,7 @@ public Bitmap getCroppedBitmap(Bitmap bitmap) { /** * Applies the message style to the notification builder. It also takes advantage of the - * notification cache to build conversations style notifications. + * android notification API to build conversations style notifications. * * @param builder Notification builder that will receive the message style * @param payload Notification payload, which contains all the data we need to build the notifications. @@ -170,10 +184,9 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil return; } - // Retrieve and check for cached notifications - NotificationData notificationData = NotificationCache.getNotificationData(reportID); - boolean hasExistingNotification = notificationData.messages.size() >= 1; - + // Retrieve and check for existing notifications + StatusBarNotification existingReportNotification = getActiveNotificationByReportId(context, reportID); + boolean hasExistingNotification = existingReportNotification != null; try { JsonMap reportMap = payload.get(ONYX_DATA_KEY).getList().get(1).getMap().get("value").getMap(); String reportId = reportMap.keySet().iterator().next(); @@ -187,31 +200,15 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil String message = alert != null ? alert : messageData.get("message").getList().get(0).getMap().get("text").getString(); String conversationName = payload.get("roomName") == null ? "" : payload.get("roomName").getString(""); - // Retrieve or create the Person object who sent the latest report comment - Person person = notificationData.getPerson(accountID); - Bitmap personIcon = notificationData.getIcon(accountID); - - if (personIcon == null) { - personIcon = fetchIcon(context, avatar); - } + // create the Person object who sent the latest report comment + Bitmap personIcon = fetchIcon(context, avatar); builder.setLargeIcon(personIcon); - // Persist the person and icon to the notification cache - if (person == null) { - IconCompat iconCompat = IconCompat.createWithBitmap(personIcon); - person = new Person.Builder() - .setIcon(iconCompat) - .setKey(accountID) - .setName(name) - .build(); - - notificationData.putPerson(accountID, name, personIcon); - } + Person person = createMessagePersonObject(IconCompat.createWithBitmap(personIcon), accountID, name); - // Despite not using conversation style for the initial notification from each chat, we need to cache it to enable conversation style for future notifications + // Create latest received message object long createdTimeInMillis = getMessageTimeInMillis(messageData.get("created").getString("")); - notificationData.messages.add(new NotificationMessage(accountID, message, createdTimeInMillis)); - + NotificationCompat.MessagingStyle.Message newMessage = new NotificationCompat.MessagingStyle.Message(message, createdTimeInMillis, person); // Conversational styling should be applied to groups chats, rooms, and any 1:1 chats with more than one notification (ensuring the large profile image is always shown) if (!conversationName.isEmpty() || hasExistingNotification) { @@ -220,30 +217,72 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil .setGroupConversation(true) .setConversationTitle(conversationName); + // Add all conversation messages to the notification, including the last one we just received. - for (NotificationMessage cachedMessage : notificationData.messages) { - messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, notificationData.getPerson(cachedMessage.accountID)); + NotificationCompat.MessagingStyle previousStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(existingReportNotification.getNotification()); + List messages = previousStyle != null ? previousStyle.getMessages() : new ArrayList<>(List.of(recreatePreviousMessage(existingReportNotification))); + + // add the last one message we just received. + messages.add(newMessage); + + for (NotificationCompat.MessagingStyle.Message activeMessage : messages) { + messagingStyle.addMessage(activeMessage); } + builder.setStyle(messagingStyle); } + // save reportID and person info for future merging + builder.addExtras(createMessageExtrasBundle(reportID, person)); + // Clear the previous notification associated to this conversation so it looks like we are // replacing them with this new one we just built. - if (notificationData.prevNotificationID != -1) { - NotificationManagerCompat.from(context).cancel(notificationData.prevNotificationID); + if (hasExistingNotification) { + int previousNotificationID = existingReportNotification.getId(); + NotificationManagerCompat.from(context).cancel(previousNotificationID); } } catch (Exception e) { e.printStackTrace(); } + } - // Store the new notification ID so we can replace the notification if this conversation - // receives more messages - notificationData.prevNotificationID = notificationID; + private Person createMessagePersonObject (IconCompat icon, String key, String name) { + return new Person.Builder().setIcon(icon).setKey(key).setName(name).build(); + } - NotificationCache.setNotificationData(reportID, notificationData); + private NotificationCompat.MessagingStyle.Message recreatePreviousMessage (StatusBarNotification statusBarNotification) { + // Get previous message + Notification previousNotification = statusBarNotification.getNotification(); + String previousMessage = previousNotification.extras.getString("android.text"); + long time = statusBarNotification.getNotification().when; + // Recreate Person object + IconCompat avatarBitmap = ParcelUtils.getVersionedParcelable(previousNotification.extras, EXTRAS_AVATAR_KEY); + String previousName = previousNotification.extras.getString(EXTRAS_NAME_KEY); + String previousAccountID = previousNotification.extras.getString(EXTRAS_ACCOUNT_ID_KEY); + Person previousPerson = createMessagePersonObject(avatarBitmap, previousAccountID, previousName); + + return new NotificationCompat.MessagingStyle.Message(previousMessage, time, previousPerson); } + private Bundle createMessageExtrasBundle(long reportID, Person person) { + Bundle extrasBundle = new Bundle(); + extrasBundle.putLong(EXTRAS_REPORT_ID_KEY, reportID); + ParcelUtils.putVersionedParcelable(extrasBundle, EXTRAS_AVATAR_KEY, person.getIcon()); + extrasBundle.putString(EXTRAS_ACCOUNT_ID_KEY, person.getKey()); + extrasBundle.putString(EXTRAS_NAME_KEY, person.getName().toString()); + + return extrasBundle; + } + + private StatusBarNotification getActiveNotificationByReportId(@NonNull Context context, long reportId) { + List notifications = NotificationManagerCompat.from(context).getActiveNotifications(); + for (StatusBarNotification currentNotification : notifications) { + long associatedReportId = currentNotification.getNotification().extras.getLong("reportID", -1); + if (associatedReportId == reportId) return currentNotification; + } + return null; + } /** * Safely retrieve the message time in milliseconds */ @@ -260,26 +299,6 @@ private long getMessageTimeInMillis(String createdTime) { return Calendar.getInstance().getTimeInMillis(); } - /** - * Remove the notification data from the cache when the user dismisses the notification. - * - * @param message Push notification's message - */ - public void onDismissNotification(PushMessage message) { - try { - JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap(); - long reportID = payload.get("reportID").getLong(-1); - - if (reportID == -1) { - return; - } - - NotificationCache.setNotificationData(reportID, null); - } catch (Exception e) { - Log.e(TAG, "Failed to delete conversation cache. SendID=" + message.getSendId(), e); - } - } - private Bitmap fetchIcon(@NonNull Context context, String urlString) { URL parsedUrl = null; try { diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java deleted file mode 100644 index 7ddc17d37b4d..000000000000 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.expensify.chat.customairshipextender; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.util.Base64; - -import androidx.core.app.Person; -import androidx.core.graphics.drawable.IconCompat; - -import com.expensify.chat.MainApplication; -import com.urbanairship.UAirship; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; - -public class NotificationCache { - - private static final String CACHE_FILE_NAME = "notification-cache"; - private static HashMap cache = null; - - /* - * Get NotificationData for an existing notification or create a new instance - * if it doesn't exist - */ - public static NotificationData getNotificationData(long reportID) { - if (cache == null) { - cache = readFromInternalStorage(); - } - - NotificationData notificationData = cache.get(Long.toString(reportID)); - - if (notificationData == null) { - notificationData = new NotificationData(); - setNotificationData(reportID, notificationData); - } - - return notificationData; - } - - /* - * Set and persist NotificationData in the cache - */ - public static void setNotificationData(long reportID, NotificationData data) { - if (cache == null) { - cache = readFromInternalStorage(); - } - - cache.put(Long.toString(reportID), data); - writeToInternalStorage(); - } - - private static void writeToInternalStorage() { - Context context = UAirship.getApplicationContext(); - - FileOutputStream fos = null; - ObjectOutputStream oos = null; - try { - File outputFile = new File(context.getFilesDir(), CACHE_FILE_NAME); - fos = new FileOutputStream(outputFile); - oos = new ObjectOutputStream(fos); - oos.writeObject(cache); - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (oos != null) { - oos.close(); - } - if (fos != null) { - fos.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - private static HashMap readFromInternalStorage() { - HashMap result; - Context context = UAirship.getApplicationContext(); - - FileInputStream fis = null; - ObjectInputStream ois = null; - try { - File fileCache = new File(context.getFilesDir(), CACHE_FILE_NAME); - fis = new FileInputStream(fileCache); - ois = new ObjectInputStream(fis); - result = (HashMap) ois.readObject(); - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - result = new HashMap<>(); - } finally { - try { - if (ois != null) { - ois.close(); - } - if (fis != null) { - fis.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - return result; - } - - /** - * A class for caching data for notifications. We use this to track active notifications so we - * can thread related notifications together - */ - public static class NotificationData implements Serializable { - private final HashMap names = new HashMap<>(); - - // A map of accountID => base64 encoded Bitmap - // In order to make Bitmaps serializable, we encode them as base64 strings - private final HashMap icons = new HashMap<>(); - public ArrayList messages = new ArrayList<>(); - - public int prevNotificationID = -1; - - public NotificationData() {} - - public Bitmap getIcon(String accountID) { - return decodeToBitmap(icons.get(accountID)); - } - - public void putIcon(String accountID, Bitmap bitmap) { - icons.put(accountID, encodeToBase64(bitmap)); - } - - public Person getPerson(String accountID) { - if (!names.containsKey(accountID) || !icons.containsKey(accountID)) { - return null; - } - - String name = names.get(accountID); - Bitmap icon = getIcon(accountID); - - return new Person.Builder() - .setIcon(IconCompat.createWithBitmap(icon)) - .setKey(accountID) - .setName(name) - .build(); - } - - public void putPerson(String accountID, String name, Bitmap icon) { - names.put(accountID, name); - putIcon(accountID, icon); - } - - public static String encodeToBase64(Bitmap bitmap) { - if (bitmap == null) { - return ""; - } - - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); - byte[] byteArray = byteArrayOutputStream.toByteArray(); - return Base64.encodeToString(byteArray, Base64.DEFAULT); - } - - public static Bitmap decodeToBitmap(String base64String) { - if (base64String == null) { - return null; - } - - byte[] decodedBytes = Base64.decode(base64String, Base64.DEFAULT); - return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length); - } - } - - public static class NotificationMessage implements Serializable { - public String accountID; - public String text; - public long time; - - NotificationMessage(String accountID, String text, long time) { - this.accountID = accountID; - this.text = text; - this.time = time; - } - } -} From 76ae394c2adc160365bc8cd7939689166cb13d03 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Mon, 29 Jan 2024 00:07:10 +0100 Subject: [PATCH 152/446] reintroduce removing notifications for android native --- .../clearReportNotifications/index.android.ts | 9 --------- .../{index.ios.ts => index.native.ts} | 0 2 files changed, 9 deletions(-) delete mode 100644 src/libs/Notification/clearReportNotifications/index.android.ts rename src/libs/Notification/clearReportNotifications/{index.ios.ts => index.native.ts} (100%) diff --git a/src/libs/Notification/clearReportNotifications/index.android.ts b/src/libs/Notification/clearReportNotifications/index.android.ts deleted file mode 100644 index 8c2a44ba29d3..000000000000 --- a/src/libs/Notification/clearReportNotifications/index.android.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type ClearReportNotifications from './types'; - -/** - * This is a temporary fix for issues with our Notification Cache not being cleared in Android. - * More info here: https://github.com/Expensify/App/issues/33367#issuecomment-1865196381 - */ -const clearReportNotifications: ClearReportNotifications = () => {}; - -export default clearReportNotifications; diff --git a/src/libs/Notification/clearReportNotifications/index.ios.ts b/src/libs/Notification/clearReportNotifications/index.native.ts similarity index 100% rename from src/libs/Notification/clearReportNotifications/index.ios.ts rename to src/libs/Notification/clearReportNotifications/index.native.ts From dfb569fb10d66521057a0475860760d260e438d5 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 29 Jan 2024 13:59:48 +0700 Subject: [PATCH 153/446] fix console error --- src/pages/settings/Profile/ProfilePage.js | 3 ++- tests/actions/IOUTest.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 99cc5cf7e35a..22a945310c4f 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -15,6 +15,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import * as UserUtils from '@libs/UserUtils'; import userPropTypes from '@pages/settings/userPropTypes'; @@ -34,7 +35,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: translatableTextPropTypes, }), ), diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index f389fb37b1f3..d184bbe0f922 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -683,7 +683,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); expect(transaction.errors).toBeTruthy(); - expect(_.values(transaction.errors)[0]).toStrictEqual(['iou.error.genericCreateFailureMessage', {isTranslated: false}]); + expect(_.values(transaction.errors)[0]).toEqual(expect.arrayContaining(['iou.error.genericCreateFailureMessage', {isTranslated: false}])); resolve(); }, }); From f2f26703e2178efa372746495b9cd60c0b3801f4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 29 Jan 2024 14:18:16 +0700 Subject: [PATCH 154/446] fix crash for attachment --- src/libs/OptionsListUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ef2888eebfb2..93a739dc273b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -523,7 +523,8 @@ function getLastMessageTextForReport(report: OnyxEntry): string { } else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) { lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction); } else if (ReportUtils.isReportMessageAttachment({text: report?.lastMessageText ?? '', html: report?.lastMessageHtml, translationKey: report?.lastMessageTranslationKey, type: ''})) { - lastMessageTextFromReport = `[${Localize.translateLocal((report?.lastMessageTranslationKey ?? 'common.attachment') as TranslationPaths)}]`; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- It's possible for lastMessageTranslationKey to be empty '', so we must fallback to common.attachment. + lastMessageTextFromReport = `[${Localize.translateLocal((report?.lastMessageTranslationKey || 'common.attachment') as TranslationPaths)}]`; } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) { const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); From f8bb87b586aa3246dc0693950afd9533dfa8ede0 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 29 Jan 2024 14:34:47 +0700 Subject: [PATCH 155/446] fix type console error --- src/components/transactionPropTypes.js | 3 ++- src/pages/settings/InitialSettingsPage.js | 3 ++- .../settings/Profile/Contacts/ContactMethodDetailsPage.js | 3 ++- src/pages/settings/Profile/Contacts/ContactMethodsPage.js | 3 ++- src/pages/settings/Profile/Contacts/NewContactMethodPage.js | 3 ++- .../Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js | 3 ++- src/pages/settings/Profile/ProfilePage.js | 2 +- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js | 3 ++- src/pages/settings/Wallet/ExpensifyCardPage.js | 3 ++- src/pages/workspace/WorkspaceNewRoomPage.js | 3 ++- src/pages/workspace/withPolicy.tsx | 3 ++- 11 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/components/transactionPropTypes.js b/src/components/transactionPropTypes.js index c70a2e524583..bdcf60bec5da 100644 --- a/src/components/transactionPropTypes.js +++ b/src/components/transactionPropTypes.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import _ from 'underscore'; +import {translatableTextPropTypes} from '@libs/Localize'; import CONST from '@src/CONST'; import sourcePropTypes from './Image/sourcePropTypes'; @@ -80,5 +81,5 @@ export default PropTypes.shape({ }), /** Server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), }); diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 6e310b9a62bd..4917aea8524b 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -27,6 +27,7 @@ import useWaitForNavigation from '@hooks/useWaitForNavigation'; import * as CardUtils from '@libs/CardUtils'; import compose from '@libs/compose'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -99,7 +100,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), }), ), diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index 7cafbe21ff6b..a9acf37ae556 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -21,6 +21,7 @@ import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeSt import compose from '@libs/compose'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import * as Session from '@userActions/Session'; import * as User from '@userActions/User'; @@ -44,7 +45,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js index 34399daf55e3..e958373bf9fd 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js @@ -16,6 +16,7 @@ import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -36,7 +37,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js index 8f6982e24b98..69fe8490f6aa 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js @@ -15,6 +15,7 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as User from '@userActions/User'; @@ -37,7 +38,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js index 8b19c7bdd233..5c1fa30a88f1 100644 --- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js @@ -18,6 +18,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import * as ValidationUtils from '@libs/ValidationUtils'; import * as Session from '@userActions/Session'; import * as User from '@userActions/User'; @@ -45,7 +46,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 22a945310c4f..b959e3b66b97 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -35,7 +35,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: translatableTextPropTypes, + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), }), ), diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js index da77d1fa6a15..f23f48fc2011 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js @@ -12,6 +12,7 @@ import * as Wallet from '@libs/actions/Wallet'; import * as CardUtils from '@libs/CardUtils'; import FormUtils from '@libs/FormUtils'; import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import assignedCardPropTypes from '@pages/settings/Wallet/assignedCardPropTypes'; import CONST from '@src/CONST'; @@ -69,7 +70,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index d7d63f0271ca..755790dfec81 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -18,6 +18,7 @@ import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import FormUtils from '@libs/FormUtils'; import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Card from '@userActions/Card'; @@ -56,7 +57,7 @@ const propTypes = { validatedDate: PropTypes.string, /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Field-specific pending states for offline UI status */ pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index b616b519ff32..7da9f425c5a8 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -23,6 +23,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {translatableTextPropTypes} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -69,7 +70,7 @@ const propTypes = { isLoading: PropTypes.bool, /** Field errors in the form */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), }), /** Session details for the user */ diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index ec38b61fb0dc..e8f83a3a895e 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -5,6 +5,7 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {forwardRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import {translatableTextPropTypes} from '@libs/Localize'; import policyMemberPropType from '@pages/policyMemberPropType'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -53,7 +54,7 @@ const policyPropTypes = { * } * } */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)), /** Whether or not the policy requires tags */ requiresTag: PropTypes.bool, From 7602ff6065802a99d076b93cac275be893716a52 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Mon, 29 Jan 2024 10:43:19 +0100 Subject: [PATCH 156/446] remove unused imports --- .../customairshipextender/CustomNotificationProvider.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index fc9015e22a10..61f607e699ed 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -17,9 +17,6 @@ import android.graphics.Rect; import android.os.Build; import android.os.Bundle; -import android.os.Message; -import android.os.Parcel; -import android.os.Parcelable; import android.service.notification.StatusBarNotification; import android.util.DisplayMetrics; import android.util.Log; @@ -46,13 +43,9 @@ import java.net.URL; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Calendar; -import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; From 3d4a3e24fc5ca7baf97b9debbd288c8e9c4f0575 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 29 Jan 2024 16:25:12 +0100 Subject: [PATCH 157/446] Migrate WorkspaceInvitePage to TypeScript --- src/ONYXKEYS.ts | 4 +- src/libs/OptionsListUtils.ts | 7 +- .../workspace/WorkspaceInviteMessagePage.tsx | 26 +- ...eInvitePage.js => WorkspaceInvitePage.tsx} | 224 +++++++++--------- .../onyx/InvitedEmailsToAccountIDsDraft.ts | 3 + src/types/onyx/index.ts | 2 + 6 files changed, 129 insertions(+), 137 deletions(-) rename src/pages/workspace/{WorkspaceInvitePage.js => WorkspaceInvitePage.tsx} (58%) create mode 100644 src/types/onyx/InvitedEmailsToAccountIDsDraft.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8f8627a2927d..7aa89de1caa4 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -453,8 +453,8 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; - [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; - [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; + [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDsDraft | undefined; + [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string | undefined; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 6332a57deec0..93664f1dbc21 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -310,9 +310,9 @@ function getPersonalDetailsForAccountIDs(accountIDs: number[] | undefined, perso /** * Return true if personal details data is ready, i.e. report list options can be created. */ -function isPersonalDetailsReady(personalDetails: OnyxEntry): boolean { - const personalDetailsKeys = Object.keys(personalDetails ?? {}); - return personalDetailsKeys.some((key) => personalDetails?.[key]?.accountID); +function isPersonalDetailsReady(personalDetails: OnyxEntry | ReportUtils.OptionData[]): boolean { + const personalDetailsValues = Array.isArray(personalDetails) ? personalDetails : Object.values(personalDetails ?? {}); + return personalDetailsValues.some((personalDetail) => personalDetail?.accountID); } /** @@ -1993,3 +1993,4 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, }; +export type {MemberForList}; diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 22bdd9c8db94..bd206811ff5a 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -11,6 +11,7 @@ import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MultipleAvatars from '@components/MultipleAvatars'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; @@ -28,8 +29,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PersonalDetailsList} from '@src/types/onyx'; -import type {Errors, Icon} from '@src/types/onyx/OnyxCommon'; +import type {InvitedEmailsToAccountIDsDraft, PersonalDetailsList} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import SearchInputManager from './SearchInputManager'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -40,10 +41,10 @@ type WorkspaceInviteMessagePageOnyxProps = { allPersonalDetails: OnyxEntry; /** An object containing the accountID for every invited user email */ - invitedEmailsToAccountIDsDraft: OnyxEntry>; + invitedEmailsToAccountIDsDraft: OnyxEntry; /** Updated workspace invite message */ - workspaceInviteMessageDraft: OnyxEntry; + workspaceInviteMessageDraft: OnyxEntry; }; type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & @@ -124,7 +125,6 @@ function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsT onCloseButtonPress={() => Navigation.dismissModal()} onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID))} /> - {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} { + ref={(element: AnimatedTextInputRef) => { if (!element) { return; } diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.tsx similarity index 58% rename from src/pages/workspace/WorkspaceInvitePage.js rename to src/pages/workspace/WorkspaceInvitePage.tsx index 72f3747c127c..172dd12314fa 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -1,11 +1,10 @@ import {useNavigation} from '@react-navigation/native'; +import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -14,85 +13,73 @@ import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import {MemberForList} from '@libs/OptionsListUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; +import type {OptionData} from '@libs/ReportUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {Beta, InvitedEmailsToAccountIDsDraft, PersonalDetailsList} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import SearchInputManager from './SearchInputManager'; -import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; -const personalDetailsPropTypes = PropTypes.shape({ - /** The login of the person (either email or phone number) */ - login: PropTypes.string, +type SelectedOption = Partial; - /** 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, except for anon users) */ - avatar: PropTypes.string, - - /** This is either the user's full name, or their login if full name is an empty string */ - displayName: PropTypes.string, -}); +type WorkspaceInvitePageOnyxProps = { + /** All of the personal details for everyone */ + personalDetails: OnyxEntry; -const propTypes = { /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropTypes), - - /** URL Route params */ - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** policyID passed via route: /workspace/:policyID/invite */ - policyID: PropTypes.string, - }), - }).isRequired, - - isLoadingReportData: PropTypes.bool, - invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number), - ...policyPropTypes, -}; + betas: OnyxEntry; -const defaultProps = { - personalDetails: {}, - betas: [], - isLoadingReportData: true, - invitedEmailsToAccountIDsDraft: {}, - ...policyDefaultProps, + /** An object containing the accountID for every invited user email */ + invitedEmailsToAccountIDsDraft: OnyxEntry; }; -function WorkspaceInvitePage(props) { +type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInvitePageOnyxProps & StackScreenProps; + +function WorkspaceInvitePage({ + route, + policyMembers, + personalDetails: personalDetailsProp, + betas, + invitedEmailsToAccountIDsDraft, + policy, + isLoadingReportData = true, +}: WorkspaceInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); - const [selectedOptions, setSelectedOptions] = useState([]); - const [personalDetails, setPersonalDetails] = useState([]); - const [usersToInvite, setUsersToInvite] = useState([]); + const [selectedOptions, setSelectedOptions] = useState([]); + const [personalDetails, setPersonalDetails] = useState([]); + const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const navigation = useNavigation(); + const navigation = useNavigation>(); const openWorkspaceInvitePage = () => { - const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails); - Policy.openWorkspaceInvitePage(props.route.params.policyID, _.keys(policyMemberEmailsToAccountIDs)); + const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); + Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); }; useEffect(() => { setSearchTerm(SearchInputManager.searchInput); return () => { - Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {}); + Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {}); }; - }, [props.route.params.policyID]); + }, [route.params.policyID]); useEffect(() => { - Policy.clearErrors(props.route.params.policyID); + Policy.clearErrors(route.params.policyID); openWorkspaceInvitePage(); // eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component }, []); @@ -111,54 +98,66 @@ function WorkspaceInvitePage(props) { useNetwork({onReconnect: openWorkspaceInvitePage}); - const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]); + const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(policyMembers, personalDetailsProp), [policyMembers, personalDetailsProp]); useEffect(() => { - const newUsersToInviteDict = {}; - const newPersonalDetailsDict = {}; - const newSelectedOptionsDict = {}; + const newUsersToInviteDict: Record = {}; + const newPersonalDetailsDict: Record = {}; + const newSelectedOptionsDict: Record = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers, true); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetailsProp, betas ?? [], searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information - const detailsMap = {}; - _.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); + const detailsMap: Record = {}; + inviteOptions.personalDetails.forEach((detail) => { + if (!detail.login) { + return; + } - const newSelectedOptions = []; - _.each(_.keys(props.invitedEmailsToAccountIDsDraft), (login) => { - if (!_.has(detailsMap, login)) { + detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail); + }); + + const newSelectedOptions: SelectedOption[] = []; + Object.keys(invitedEmailsToAccountIDsDraft ?? {}).forEach((login) => { + if (!(login in detailsMap)) { return; } newSelectedOptions.push({...detailsMap[login], isSelected: true}); }); - _.each(selectedOptions, (option) => { - newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option); + selectedOptions.forEach((option) => { + newSelectedOptions.push(option.login && option.login in detailsMap ? {...detailsMap[option.login], isSelected: true} : option); }); const userToInvite = inviteOptions.userToInvite; // Only add the user to the invites list if it is valid - if (userToInvite) { + if (typeof userToInvite?.accountID === 'number') { newUsersToInviteDict[userToInvite.accountID] = userToInvite; } // Add all personal details to the new dict - _.each(inviteOptions.personalDetails, (details) => { + inviteOptions.personalDetails.forEach((details) => { + if (typeof details.accountID !== 'number') { + return; + } newPersonalDetailsDict[details.accountID] = details; }); // Add all selected options to the new dict - _.each(newSelectedOptions, (option) => { + newSelectedOptions.forEach((option) => { + if (typeof option.accountID !== 'number') { + return; + } newSelectedOptionsDict[option.accountID] = option; }); // Strip out dictionary keys and update arrays - setUsersToInvite(_.values(newUsersToInviteDict)); - setPersonalDetails(_.values(newPersonalDetailsDict)); - setSelectedOptions(_.values(newSelectedOptionsDict)); + setUsersToInvite(Object.values(newUsersToInviteDict)); + setPersonalDetails(Object.values(newPersonalDetailsDict)); + setSelectedOptions(Object.values(newSelectedOptionsDict)); // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [props.personalDetails, props.policyMembers, props.betas, searchTerm, excludedUsers]); + }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); const sections = useMemo(() => { const sectionsArr = []; @@ -171,13 +170,13 @@ function WorkspaceInvitePage(props) { // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; if (searchTerm !== '') { - filterSelectedOptions = _.filter(selectedOptions, (option) => { - const accountID = lodashGet(option, 'accountID', null); - const isOptionInPersonalDetails = _.some(personalDetails, (personalDetail) => personalDetail.accountID === accountID); + filterSelectedOptions = selectedOptions.filter((option) => { + const accountID = option.accountID; + const isOptionInPersonalDetails = Object.values(personalDetails).some((personalDetail) => personalDetail.accountID === accountID); const parsedPhoneNumber = parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchTerm))); - const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchTerm.toLowerCase(); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchTerm.toLowerCase(); - const isPartOfSearchTerm = option.text.toLowerCase().includes(searchValue) || option.login.toLowerCase().includes(searchValue); + const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); return isPartOfSearchTerm || isOptionInPersonalDetails; }); } @@ -191,20 +190,20 @@ function WorkspaceInvitePage(props) { indexOffset += filterSelectedOptions.length; // Filtering out selected users from the search results - const selectedLogins = _.map(selectedOptions, ({login}) => login); - const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login)); - const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, OptionsListUtils.formatMemberForList); + const selectedLogins = selectedOptions.map(({login}) => login); + const personalDetailsWithoutSelected = Object.values(personalDetails).filter(({login}) => !selectedLogins.some((selectedLogin) => selectedLogin === login)); + const personalDetailsFormatted = personalDetailsWithoutSelected.map((item) => OptionsListUtils.formatMemberForList(item)); sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, - shouldShow: !_.isEmpty(personalDetailsFormatted), + shouldShow: !isEmptyObject(personalDetailsFormatted), indexOffset, }); indexOffset += personalDetailsFormatted.length; - _.each(usersToInvite, (userToInvite) => { - const hasUnselectedUserToInvite = !_.contains(selectedLogins, userToInvite.login); + Object.values(usersToInvite).forEach((userToInvite) => { + const hasUnselectedUserToInvite = !selectedLogins.some((selectedLogin) => selectedLogin === userToInvite.login); if (hasUnselectedUserToInvite) { sectionsArr.push({ @@ -219,14 +218,14 @@ function WorkspaceInvitePage(props) { return sectionsArr; }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); - const toggleOption = (option) => { - Policy.clearErrors(props.route.params.policyID); + const toggleOption = (option: OptionData) => { + Policy.clearErrors(route.params.policyID); - const isOptionInList = _.some(selectedOptions, (selectedOption) => selectedOption.login === option.login); + const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); - let newSelectedOptions; + let newSelectedOptions: SelectedOption[]; if (isOptionInList) { - newSelectedOptions = _.reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); + newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); } else { newSelectedOptions = [...selectedOptions, {...option, isSelected: true}]; } @@ -234,14 +233,14 @@ function WorkspaceInvitePage(props) { setSelectedOptions(newSelectedOptions); }; - const validate = () => { - const errors = {}; + const validate = (): boolean => { + const errors: Errors = {}; if (selectedOptions.length <= 0) { - errors.noUserSelected = true; + errors.noUserSelected = 'true'; } - Policy.setWorkspaceErrors(props.route.params.policyID, errors); - return _.size(errors) <= 0; + Policy.setWorkspaceErrors(route.params.policyID, errors); + return isEmptyObject(errors); }; const inviteUser = () => { @@ -249,27 +248,24 @@ function WorkspaceInvitePage(props) { return; } - const invitedEmailsToAccountIDs = {}; - _.each(selectedOptions, (option) => { - const login = option.login || ''; - const accountID = lodashGet(option, 'accountID', ''); + const invitedEmailsToAccountIDs: Record = {}; + selectedOptions.forEach((option) => { + const login = option.login ?? ''; + const accountID = option.accountID ?? ''; if (!login.toLowerCase().trim() || !accountID) { return; } invitedEmailsToAccountIDs[login] = Number(accountID); }); - Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, invitedEmailsToAccountIDs); - Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(props.route.params.policyID)); + Policy.setWorkspaceInviteMembersDraft(route.params.policyID, invitedEmailsToAccountIDs); + Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(route.params.policyID)); }; - const [policyName, shouldShowAlertPrompt] = useMemo( - () => [lodashGet(props.policy, 'name'), _.size(lodashGet(props.policy, 'errors', {})) > 0 || lodashGet(props.policy, 'alertMessage', '').length > 0], - [props.policy], - ); + const [policyName, shouldShowAlertPrompt] = useMemo(() => [policy?.name ?? '', !isEmptyObject(policy?.errors) || !!policy?.alertMessage], [policy]); const headerMessage = useMemo(() => { const searchValue = searchTerm.trim().toLowerCase(); - if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.includes(searchValue)) { + if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.some((email) => email === searchValue)) { return translate('messages.errorMessageInvalidEmail'); } if ( @@ -289,8 +285,8 @@ function WorkspaceInvitePage(props) { testID={WorkspaceInvitePage.displayName} > Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} > { - Policy.clearErrors(props.route.params.policyID); - Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID)); + Policy.clearErrors(route.params.policyID); + Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID)); }} /> @@ -325,7 +321,7 @@ function WorkspaceInvitePage(props) { isAlertVisible={shouldShowAlertPrompt} buttonText={translate('common.next')} onSubmit={inviteUser} - message={props.policy.alertMessage} + message={policy?.alertMessage} containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto, styles.mb5]} enabledWhenOffline disablePressOnEnter @@ -336,24 +332,18 @@ function WorkspaceInvitePage(props) { ); } -WorkspaceInvitePage.propTypes = propTypes; -WorkspaceInvitePage.defaultProps = defaultProps; WorkspaceInvitePage.displayName = 'WorkspaceInvitePage'; -export default compose( - withPolicyAndFullscreenLoading, - withOnyx({ +export default withPolicyAndFullscreenLoading( + withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, betas: { key: ONYXKEYS.BETAS, }, - isLoadingReportData: { - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - }, invitedEmailsToAccountIDsDraft: { key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, }, - }), -)(WorkspaceInvitePage); + })(WorkspaceInvitePage), +); diff --git a/src/types/onyx/InvitedEmailsToAccountIDsDraft.ts b/src/types/onyx/InvitedEmailsToAccountIDsDraft.ts new file mode 100644 index 000000000000..d29282b0aee9 --- /dev/null +++ b/src/types/onyx/InvitedEmailsToAccountIDsDraft.ts @@ -0,0 +1,3 @@ +type InvitedEmailsToAccountIDsDraft = Record; + +export default InvitedEmailsToAccountIDsDraft; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 5b04cae58671..a222efbbeed3 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -15,6 +15,7 @@ import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; import type Fund from './Fund'; import type IntroSelected from './IntroSelected'; +import type InvitedEmailsToAccountIDsDraft from './InvitedEmailsToAccountIDsDraft'; import type IOU from './IOU'; import type Locale from './Locale'; import type {LoginList} from './Login'; @@ -150,5 +151,6 @@ export type { NewRoomForm, IKnowATeacherForm, IntroSchoolPrincipalForm, + InvitedEmailsToAccountIDsDraft, PrivateNotesForm, }; From 4f70618c0b5010fbf887118981cf2c10b7dca2ef Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 29 Jan 2024 17:20:21 +0100 Subject: [PATCH 158/446] TypeScript fixes --- src/components/SelectionList/BaseListItem.tsx | 2 +- .../SelectionList/BaseSelectionList.tsx | 6 ++--- src/components/SelectionList/types.ts | 25 +++++++++++-------- src/libs/OptionsListUtils.ts | 7 ++++-- src/pages/workspace/WorkspaceInvitePage.tsx | 10 +++++--- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 71845931ba52..37e5ff0bff1c 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -55,7 +55,7 @@ function BaseListItem({ onSelectRow(item)} disabled={isDisabled} - accessibilityLabel={item.text} + accessibilityLabel={item.text ?? ''} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={styles.hoveredComponentBG} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 1d73873d836b..07125a0fb005 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -300,14 +300,14 @@ function BaseSelectionList( selectRow(item, true)} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} - keyForList={item.keyForList} + keyForList={item.keyForList ?? undefined} /> ); }; @@ -471,7 +471,7 @@ function BaseSelectionList( getItemLayout={getItemLayout} onScroll={onScroll} onScrollBeginDrag={onScrollBeginDrag} - keyExtractor={(item) => item.keyForList} + keyExtractor={(item) => item.keyForList ?? ''} extraData={focusedIndex} indicatorStyle="white" keyboardShouldPersistTaps="always" diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index a82ddef6febb..3e1d74a2e05d 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -34,28 +34,28 @@ type CommonListItemProps = { type User = { /** Text to display */ - text: string; + text?: string; /** Alternate text to display */ - alternateText?: string; + alternateText?: string | null; /** Key used internally by React */ - keyForList: string; + keyForList?: string | null; /** Whether this option is selected */ isSelected?: boolean; /** Whether this option is disabled for selection */ - isDisabled?: boolean; + isDisabled?: boolean | null; /** User accountID */ - accountID?: number; + accountID?: number | null; /** User login */ - login?: string; + login?: string | null; /** Element to show on the right side of the item */ - rightElement?: ReactElement; + rightElement?: ReactElement | null; /** Icons for the user (can be multiple if it's a Workspace) */ icons?: Icon[]; @@ -85,19 +85,19 @@ type UserListItemProps = CommonListItemProps & { type RadioItem = { /** Text to display */ - text: string; + text?: string; /** Alternate text to display */ - alternateText?: string; + alternateText?: string | null; /** Key used internally by React */ - keyForList: string; + keyForList?: string | null; /** Whether this option is selected */ isSelected?: boolean; /** Whether this option is disabled for selection */ - isDisabled?: boolean; + isDisabled?: boolean | null; /** Represents the index of the section it came from */ sectionIndex?: number; @@ -129,6 +129,9 @@ type Section = { /** Whether this section items disabled for selection */ isDisabled?: boolean; + + /** Whether this section should be shown or not */ + shouldShow?: boolean; }; type BaseSelectionListProps = Partial & { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 93664f1dbc21..67437957999d 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -5,6 +5,7 @@ import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; +import type {ReactElement} from 'react'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; @@ -109,7 +110,7 @@ type MemberForList = { isDisabled: boolean | null; accountID?: number | null; login: string | null; - rightElement: React.ReactNode | null; + rightElement: ReactElement | null; icons?: OnyxCommon.Icon[]; pendingAction?: OnyxCommon.PendingAction; }; @@ -1810,7 +1811,9 @@ function getShareDestinationOptions( * @param member - personalDetails or userToInvite * @param config - keys to overwrite the default values */ -function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}): MemberForList | undefined { +function formatMemberForList(member: ReportUtils.OptionData, config?: ReportUtils.OptionData | EmptyObject): MemberForList; +function formatMemberForList(member: null | undefined, config?: ReportUtils.OptionData | EmptyObject): undefined; +function formatMemberForList(member: ReportUtils.OptionData | null | undefined, config: ReportUtils.OptionData | EmptyObject = {}): MemberForList | undefined { if (!member) { return undefined; } diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 172dd12314fa..3a8023b6256f 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -2,6 +2,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useEffect, useMemo, useState} from 'react'; +import type {SectionListData} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -10,6 +11,7 @@ import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; +import type {Section} from '@components/SelectionList/types'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -17,7 +19,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as LoginUtils from '@libs/LoginUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import {MemberForList} from '@libs/OptionsListUtils'; +import type {MemberForList} from '@libs/OptionsListUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; @@ -159,8 +161,8 @@ function WorkspaceInvitePage({ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); - const sections = useMemo(() => { - const sectionsArr = []; + const sections: Array>> = useMemo(() => { + const sectionsArr: Array>> = []; let indexOffset = 0; if (!didScreenTransitionEnd) { @@ -218,7 +220,7 @@ function WorkspaceInvitePage({ return sectionsArr; }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); - const toggleOption = (option: OptionData) => { + const toggleOption = (option: SelectedOption) => { Policy.clearErrors(route.params.policyID); const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); From b87e86d50cafee15d9b390bab8fe45109e86eb74 Mon Sep 17 00:00:00 2001 From: Robert Kozik Date: Mon, 29 Jan 2024 23:29:01 +0100 Subject: [PATCH 159/446] remove any occurrences of onDismissNotification method and CustomNotificationListener.java --- .../CustomAirshipExtender.java | 3 -- .../CustomNotificationListener.java | 53 ------------------- 2 files changed, 56 deletions(-) delete mode 100644 android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java index 0cae0bd2de6d..86529e19401e 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java @@ -14,8 +14,5 @@ public void onAirshipReady(@NonNull Context context, @NonNull UAirship airship) CustomNotificationProvider notificationProvider = new CustomNotificationProvider(context, airship.getAirshipConfigOptions()); pushManager.setNotificationProvider(notificationProvider); - - NotificationListener notificationListener = airship.getPushManager().getNotificationListener(); - pushManager.setNotificationListener(new CustomNotificationListener(notificationListener, notificationProvider)); } } diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java deleted file mode 100644 index 8149ca118d58..000000000000 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.expensify.chat.customairshipextender; - -import androidx.annotation.NonNull; -import com.urbanairship.push.NotificationActionButtonInfo; -import com.urbanairship.push.NotificationInfo; -import com.urbanairship.push.NotificationListener; -import com.urbanairship.push.PushMessage; -import org.jetbrains.annotations.NotNull; - -/** - * Allows us to clear the notification cache when the user dismisses a notification. - */ -public class CustomNotificationListener implements NotificationListener { - private final NotificationListener parent; - private final CustomNotificationProvider provider; - - CustomNotificationListener(NotificationListener parent, CustomNotificationProvider provider) { - this.parent = parent; - this.provider = provider; - } - - @Override - public void onNotificationPosted(@NonNull @NotNull NotificationInfo notificationInfo) { - parent.onNotificationPosted(notificationInfo); - } - - @Override - public boolean onNotificationOpened(@NonNull @NotNull NotificationInfo notificationInfo) { - // The notification is also dismissed when it's tapped so handle that as well - PushMessage message = notificationInfo.getMessage(); - provider.onDismissNotification(message); - - return parent.onNotificationOpened(notificationInfo); - } - - @Override - public boolean onNotificationForegroundAction(@NonNull @NotNull NotificationInfo notificationInfo, @NonNull @NotNull NotificationActionButtonInfo actionButtonInfo) { - return parent.onNotificationForegroundAction(notificationInfo, actionButtonInfo); - } - - @Override - public void onNotificationBackgroundAction(@NonNull @NotNull NotificationInfo notificationInfo, @NonNull @NotNull NotificationActionButtonInfo actionButtonInfo) { - parent.onNotificationBackgroundAction(notificationInfo, actionButtonInfo); - } - - @Override - public void onNotificationDismissed(@NonNull @NotNull NotificationInfo notificationInfo) { - parent.onNotificationDismissed(notificationInfo); - - PushMessage message = notificationInfo.getMessage(); - provider.onDismissNotification(message); - } -} From b8ecd41057398adc0401315dd44427ce0e422c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Mon, 29 Jan 2024 18:57:17 -0600 Subject: [PATCH 160/446] Add comments to reimbursement choices in CONST.ts --- src/CONST.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 27eb04b66ecc..4a3489015b76 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1323,9 +1323,9 @@ const CONST = { OWNER_EMAIL_FAKE: '_FAKE_', OWNER_ACCOUNT_ID_FAKE: 0, REIMBURSEMENT_CHOICES: { - REIMBURSEMENT_YES: 'reimburseYes', - REIMBURSEMENT_NO: 'reimburseNo', - REIMBURSEMENT_MANUAL: 'reimburseManual', + REIMBURSEMENT_YES: 'reimburseYes', // Direct + REIMBURSEMENT_NO: 'reimburseNo', // None + REIMBURSEMENT_MANUAL: 'reimburseManual', // Indirect }, ID_FAKE: '_FAKE_', }, From 9d1fd95b6030b32a34d9b2a1803b391db18bed71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Mon, 29 Jan 2024 18:57:29 -0600 Subject: [PATCH 161/446] Add reimburser email field to Policy type --- src/types/onyx/Policy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index eca7e9d1ee06..df7b633df9d1 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -147,6 +147,9 @@ type Policy = { /** When tax tracking is enabled */ isTaxTrackingEnabled?: boolean; + + /** The email of the reimburser set when reimbursement is direct */ + reimburserEmail?: string; }; export default Policy; From 147dbb32dffabf9f93abfd4525aab45d0676325a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Mon, 29 Jan 2024 18:57:52 -0600 Subject: [PATCH 162/446] Update pay button logic in ReportPreview component --- src/components/ReportActionItem/ReportPreview.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 6b4cd440e7e8..958b4f898f70 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -158,6 +158,7 @@ function ReportPreview(props) { const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(props.iouReport); const policyType = lodashGet(props.policy, 'type'); const isAutoReimbursable = ReportUtils.canBeAutoReimbursed(props.iouReport, props.policy); + const isReimburser = lodashGet(props.session, 'email') === lodashGet(props.policy, 'reimburserEmail'); const iouSettled = ReportUtils.isSettled(props.iouReportID); const iouCanceled = ReportUtils.isArchivedRoom(props.chatReport); @@ -246,7 +247,7 @@ function ReportPreview(props) { const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(props.policy, 'role') === CONST.POLICY.ROLE.ADMIN; const isPayer = isPaidGroupPolicy ? // In a paid group policy, the admin approver can pay the report directly by skipping the approval step - isPolicyAdmin && (isApproved || isCurrentUserManager) + isReimburser && (isApproved || isCurrentUserManager) : isPolicyAdmin || (isMoneyRequestReport && isCurrentUserManager); const shouldShowPayButton = useMemo( () => isPayer && !isDraftExpenseReport && !iouSettled && !props.iouReport.isWaitingOnBankAccount && reimbursableSpend !== 0 && !iouCanceled && !isAutoReimbursable, From 0f2c953239cbd358eb2b0b8e928ef38208055c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Mon, 29 Jan 2024 18:58:19 -0600 Subject: [PATCH 163/446] Update pay logic for payer in MoneyReportHeader --- src/components/MoneyReportHeader.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 4b4e3915f969..46e8126a0d78 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -60,9 +60,10 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt const isAutoReimbursable = ReportUtils.canBeAutoReimbursed(moneyRequestReport, policy); const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicy(moneyRequestReport); const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && session?.accountID === moneyRequestReport.managerID; + const isReimburser = session?.email === policy?.reimburserEmail; const isPayer = isPaidGroupPolicy ? // In a group policy, the admin approver can pay the report directly by skipping the approval step - isPolicyAdmin && (isApproved || isManager) + isReimburser && (isApproved || isManager) : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport); const shouldShowPayButton = useMemo( From 2dde9b95ab68f7afd6732525d59e620d4f09e089 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 30 Jan 2024 11:33:10 +0700 Subject: [PATCH 164/446] fix logic show error zip format --- src/languages/en.ts | 3 +-- src/languages/es.ts | 3 +-- src/languages/types.ts | 3 --- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 2994cb3d26f7..3a4cedef7737 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -25,7 +25,6 @@ import type { FormattedMaxLengthParams, GoBackMessageParams, GoToRoomParams, - IncorrectZipFormatParams, InstantSummaryParams, LocalTimeParams, LoggedInAsParams, @@ -1156,7 +1155,7 @@ export default { dateShouldBeBefore: ({dateString}: DateShouldBeBeforeParams) => `Date should be before ${dateString}.`, dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `Date should be after ${dateString}.`, hasInvalidCharacter: 'Name can only include Latin characters.', - incorrectZipFormat: (params: IncorrectZipFormatParams) => `Incorrect zip code format.${params?.zipFormat ? ` Acceptable format: ${params?.zipFormat}` : ''}`, + incorrectZipFormat: (zipFormat?: string) => `Incorrect zip code format.${zipFormat ? ` Acceptable format: ${zipFormat}` : ''}`, }, }, resendValidationForm: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 427097d5d16b..b8fc6655a48f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -24,7 +24,6 @@ import type { FormattedMaxLengthParams, GoBackMessageParams, GoToRoomParams, - IncorrectZipFormatParams, InstantSummaryParams, LocalTimeParams, LoggedInAsParams, @@ -1153,7 +1152,7 @@ export default { error: { dateShouldBeBefore: ({dateString}: DateShouldBeBeforeParams) => `La fecha debe ser anterior a ${dateString}.`, dateShouldBeAfter: ({dateString}: DateShouldBeAfterParams) => `La fecha debe ser posterior a ${dateString}.`, - incorrectZipFormat: ({zipFormat}: IncorrectZipFormatParams) => `Formato de código postal incorrecto.${zipFormat ? ` Formato aceptable: ${zipFormat}` : ''}`, + incorrectZipFormat: (zipFormat?: string) => `Formato de código postal incorrecto.${zipFormat ? ` Formato aceptable: ${zipFormat}` : ''}`, hasInvalidCharacter: 'El nombre sólo puede incluir caracteres latinos.', }, }, diff --git a/src/languages/types.ts b/src/languages/types.ts index 11adf01ac252..020037f7cf72 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -163,8 +163,6 @@ type DateShouldBeBeforeParams = {dateString: string}; type DateShouldBeAfterParams = {dateString: string}; -type IncorrectZipFormatParams = {zipFormat?: string}; - type WeSentYouMagicSignInLinkParams = {login: string; loginType: string}; type ToValidateLoginParams = {primaryLogin: string; secondaryLogin: string}; @@ -315,7 +313,6 @@ export type { FormattedMaxLengthParams, GoBackMessageParams, GoToRoomParams, - IncorrectZipFormatParams, InstantSummaryParams, LocalTimeParams, LoggedInAsParams, From 2fe100c6b2c483332d96b854243d09f87ec391d1 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 30 Jan 2024 10:41:47 +0100 Subject: [PATCH 165/446] Type improvements --- src/components/SelectionList/BaseListItem.tsx | 2 +- .../SelectionList/BaseSelectionList.tsx | 6 +++--- src/components/SelectionList/types.ts | 20 +++++++++---------- src/libs/OptionsListUtils.ts | 20 +++++++++---------- src/pages/RoomInvitePage.js | 6 +++--- src/pages/workspace/WorkspaceInvitePage.tsx | 18 ++++++++--------- 6 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 37e5ff0bff1c..71845931ba52 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -55,7 +55,7 @@ function BaseListItem({ onSelectRow(item)} disabled={isDisabled} - accessibilityLabel={item.text ?? ''} + accessibilityLabel={item.text} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} hoverStyle={styles.hoveredComponentBG} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 07125a0fb005..1d73873d836b 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -300,14 +300,14 @@ function BaseSelectionList( selectRow(item, true)} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} - keyForList={item.keyForList ?? undefined} + keyForList={item.keyForList} /> ); }; @@ -471,7 +471,7 @@ function BaseSelectionList( getItemLayout={getItemLayout} onScroll={onScroll} onScrollBeginDrag={onScrollBeginDrag} - keyExtractor={(item) => item.keyForList ?? ''} + keyExtractor={(item) => item.keyForList} extraData={focusedIndex} indicatorStyle="white" keyboardShouldPersistTaps="always" diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 3e1d74a2e05d..e34f0f28be42 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -34,25 +34,25 @@ type CommonListItemProps = { type User = { /** Text to display */ - text?: string; + text: string; /** Alternate text to display */ - alternateText?: string | null; + alternateText?: string; /** Key used internally by React */ - keyForList?: string | null; + keyForList: string; /** Whether this option is selected */ isSelected?: boolean; /** Whether this option is disabled for selection */ - isDisabled?: boolean | null; + isDisabled?: boolean; /** User accountID */ - accountID?: number | null; + accountID?: number; /** User login */ - login?: string | null; + login?: string; /** Element to show on the right side of the item */ rightElement?: ReactElement | null; @@ -85,19 +85,19 @@ type UserListItemProps = CommonListItemProps & { type RadioItem = { /** Text to display */ - text?: string; + text: string; /** Alternate text to display */ - alternateText?: string | null; + alternateText?: string; /** Key used internally by React */ - keyForList?: string | null; + keyForList: string; /** Whether this option is selected */ isSelected?: boolean; /** Whether this option is disabled for selection */ - isDisabled?: boolean | null; + isDisabled?: boolean; /** Represents the index of the section it came from */ sectionIndex?: number; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 67437957999d..3f262f360c18 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -16,7 +16,6 @@ import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import Timing from './actions/Timing'; @@ -104,12 +103,12 @@ type GetOptionsConfig = { type MemberForList = { text: string; - alternateText: string | null; - keyForList: string | null; + alternateText: string; + keyForList: string; isSelected: boolean; - isDisabled: boolean | null; - accountID?: number | null; - login: string | null; + isDisabled: boolean; + accountID?: number; + login: string; rightElement: ReactElement | null; icons?: OnyxCommon.Icon[]; pendingAction?: OnyxCommon.PendingAction; @@ -1811,14 +1810,14 @@ function getShareDestinationOptions( * @param member - personalDetails or userToInvite * @param config - keys to overwrite the default values */ -function formatMemberForList(member: ReportUtils.OptionData, config?: ReportUtils.OptionData | EmptyObject): MemberForList; -function formatMemberForList(member: null | undefined, config?: ReportUtils.OptionData | EmptyObject): undefined; -function formatMemberForList(member: ReportUtils.OptionData | null | undefined, config: ReportUtils.OptionData | EmptyObject = {}): MemberForList | undefined { +function formatMemberForList(member: ReportUtils.OptionData): MemberForList; +function formatMemberForList(member: null | undefined): undefined; +function formatMemberForList(member: ReportUtils.OptionData | null | undefined): MemberForList | undefined { if (!member) { return undefined; } - const accountID = member.accountID; + const accountID = member.accountID ?? undefined; return { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -1834,7 +1833,6 @@ function formatMemberForList(member: ReportUtils.OptionData | null | undefined, rightElement: null, icons: member.icons, pendingAction: member.pendingAction, - ...config, }; } diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index 588a90e98649..40c9559c9619 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -89,7 +89,7 @@ function RoomInvitePage(props) { // Update selectedOptions with the latest personalDetails information const detailsMap = {}; - _.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail, false))); + _.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); const newSelectedOptions = []; _.forEach(selectedOptions, (option) => { newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option); @@ -145,7 +145,7 @@ function RoomInvitePage(props) { // Filtering out selected users from the search results const selectedLogins = _.map(selectedOptions, ({login}) => login); const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login)); - const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail, false)); + const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail)); const hasUnselectedUserToInvite = userToInvite && !_.contains(selectedLogins, userToInvite.login); sectionsArr.push({ @@ -159,7 +159,7 @@ function RoomInvitePage(props) { if (hasUnselectedUserToInvite) { sectionsArr.push({ title: undefined, - data: [OptionsListUtils.formatMemberForList(userToInvite, false)], + data: [OptionsListUtils.formatMemberForList(userToInvite)], shouldShow: true, indexOffset, }); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 3a8023b6256f..541b8413b0ec 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -36,7 +36,7 @@ import SearchInputManager from './SearchInputManager'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; -type SelectedOption = Partial; +type MembersSection = SectionListData>; type WorkspaceInvitePageOnyxProps = { /** All of the personal details for everyone */ @@ -63,7 +63,7 @@ function WorkspaceInvitePage({ const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); - const [selectedOptions, setSelectedOptions] = useState([]); + const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); @@ -105,12 +105,12 @@ function WorkspaceInvitePage({ useEffect(() => { const newUsersToInviteDict: Record = {}; const newPersonalDetailsDict: Record = {}; - const newSelectedOptionsDict: Record = {}; + const newSelectedOptionsDict: Record = {}; const inviteOptions = OptionsListUtils.getMemberInviteOptions(personalDetailsProp, betas ?? [], searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information - const detailsMap: Record = {}; + const detailsMap: Record = {}; inviteOptions.personalDetails.forEach((detail) => { if (!detail.login) { return; @@ -119,7 +119,7 @@ function WorkspaceInvitePage({ detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail); }); - const newSelectedOptions: SelectedOption[] = []; + const newSelectedOptions: MemberForList[] = []; Object.keys(invitedEmailsToAccountIDsDraft ?? {}).forEach((login) => { if (!(login in detailsMap)) { return; @@ -161,8 +161,8 @@ function WorkspaceInvitePage({ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [personalDetailsProp, policyMembers, betas, searchTerm, excludedUsers]); - const sections: Array>> = useMemo(() => { - const sectionsArr: Array>> = []; + const sections: MembersSection[] = useMemo(() => { + const sectionsArr: MembersSection[] = []; let indexOffset = 0; if (!didScreenTransitionEnd) { @@ -220,12 +220,12 @@ function WorkspaceInvitePage({ return sectionsArr; }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); - const toggleOption = (option: SelectedOption) => { + const toggleOption = (option: MemberForList) => { Policy.clearErrors(route.params.policyID); const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); - let newSelectedOptions: SelectedOption[]; + let newSelectedOptions: MemberForList[]; if (isOptionInList) { newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); } else { From f8896692f83bf0393714d5e5400b9395b2a0f684 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 30 Jan 2024 10:57:16 +0100 Subject: [PATCH 166/446] Update formatMemberForList typing --- src/libs/OptionsListUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 3f262f360c18..73c867381609 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1810,9 +1810,9 @@ function getShareDestinationOptions( * @param member - personalDetails or userToInvite * @param config - keys to overwrite the default values */ -function formatMemberForList(member: ReportUtils.OptionData): MemberForList; -function formatMemberForList(member: null | undefined): undefined; -function formatMemberForList(member: ReportUtils.OptionData | null | undefined): MemberForList | undefined { +function formatMemberForList(member: ReportUtils.OptionData, config?: Partial): MemberForList; +function formatMemberForList(member: null | undefined, config?: Partial): undefined; +function formatMemberForList(member: ReportUtils.OptionData | null | undefined, config: Partial = {}): MemberForList | undefined { if (!member) { return undefined; } @@ -1833,6 +1833,7 @@ function formatMemberForList(member: ReportUtils.OptionData | null | undefined): rightElement: null, icons: member.icons, pendingAction: member.pendingAction, + ...config, }; } From 661b4581aca424d990a8419bf88585c19bffb468 Mon Sep 17 00:00:00 2001 From: Shahe Shahinyan Date: Tue, 30 Jan 2024 14:00:23 +0400 Subject: [PATCH 167/446] Remove extra changes --- src/styles/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index 52ebb580254b..1353e55193a6 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1268,7 +1268,7 @@ const styles = (theme: ThemeColors) => }, signInBackgroundImage: { - height: 700, + minHeight: 700, }, signInPageInner: { From ece412c4055b3c2bbfa1ac06c9b0819bd9dfcfc7 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 30 Jan 2024 11:59:29 +0100 Subject: [PATCH 168/446] Fix TS errors in updated code --- src/pages/workspace/WorkspaceInitialPage.tsx | 8 ++++---- src/types/onyx/Policy.ts | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index a6624e327626..79aab976c05f 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -210,18 +210,18 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reports: reports // Menu options to navigate to the chat report of #admins and #announce room. // For navigation, the chat report ids may be unavailable due to the missing chat reports in Onyx. // In such cases, let us use the available chat report ids from the policy. - if (adminsRoom || policy.chatReportIDAdmins) { + if (!!adminsRoom || policy?.chatReportIDAdmins) { items.push({ icon: Expensicons.Hashtag, text: translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS}), - onSelected: () => Navigation.dismissModal(adminsRoom ? adminsRoom.reportID : policy.chatReportIDAdmins.toString()), + onSelected: () => Navigation.dismissModal(adminsRoom ? adminsRoom.reportID : policy?.chatReportIDAdmins?.toString()), }); } - if (announceRoom || policy.chatReportIDAnnounce) { + if (!!announceRoom || policy?.chatReportIDAnnounce) { items.push({ icon: Expensicons.Hashtag, text: translate('workspace.common.goToRoom', {roomName: CONST.REPORT.WORKSPACE_CHAT_ROOMS.ANNOUNCE}), - onSelected: () => Navigation.dismissModal(announceRoom ? announceRoom.reportID : policy.chatReportIDAnnounce.toString()), + onSelected: () => Navigation.dismissModal(announceRoom ? announceRoom.reportID : policy?.chatReportIDAnnounce?.toString()), }); } return items; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index eca7e9d1ee06..7029918abb98 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -147,6 +147,12 @@ type Policy = { /** When tax tracking is enabled */ isTaxTrackingEnabled?: boolean; + + /** Admins chat report id */ + chatReportIDAdmins?: string; + + /** Announce chat report id */ + chatReportIDAnnounce?: string; }; export default Policy; From 888c5fc67d9fda330769dc094de74cebcffe7f9b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 30 Jan 2024 12:42:45 +0100 Subject: [PATCH 169/446] fix: resolved comments --- src/pages/ReportDetailsPage.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index bceaeddc0349..d858b47e9cc2 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,3 +1,4 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import type {FC} from 'react'; import React, {useEffect, useMemo} from 'react'; import {ScrollView, View} from 'react-native'; @@ -19,6 +20,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -27,6 +29,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -50,11 +53,7 @@ type ReportDetailsPageOnyxProps = { /** Session info for the currently logged in user. */ session: OnyxEntry; }; -type ReportDetailsPageProps = { - /** The report currently being looked at */ - report: OnyxEntry; -} & ReportDetailsPageOnyxProps & - WithReportOrNotFoundProps; +type ReportDetailsPageProps = ReportDetailsPageOnyxProps & WithReportOrNotFoundProps & StackScreenProps; function ReportDetailsPage({policies, report, session, personalDetails}: ReportDetailsPageProps) { const {translate} = useLocalize(); @@ -89,12 +88,12 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD }, [report?.reportID, isOffline, isPrivateNotesFetchTriggered]); const menuItems: ReportDetailsPageMenuItem[] = useMemo(() => { - const items = []; + const items: ReportDetailsPageMenuItem[] = []; if (!isGroupDMChat) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.SHARE_CODE, - translationKey: 'common.shareCode' as const, + translationKey: 'common.shareCode', icon: Expensicons.QrCode, isAnonymousAction: true, action: () => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.getRoute(report?.reportID ?? '')), @@ -111,7 +110,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD if ((!isUserCreatedPolicyRoom && participants.length) || (isUserCreatedPolicyRoom && isPolicyMember)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.MEMBERS, - translationKey: 'common.members' as const, + translationKey: 'common.members', icon: Expensicons.Users, subtitle: participants.length, isAnonymousAction: false, @@ -126,7 +125,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD } else if (isUserCreatedPolicyRoom && (!participants.length || !isPolicyMember) && !report?.parentReportID) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.INVITE, - translationKey: 'common.invite' as const, + translationKey: 'common.invite', icon: Expensicons.Users, isAnonymousAction: false, action: () => { @@ -137,7 +136,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS, - translationKey: 'common.settings' as const, + translationKey: 'common.settings', icon: Expensicons.Gear, isAnonymousAction: false, action: () => { @@ -149,7 +148,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD if (!isThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(report)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.PRIVATE_NOTES, - translationKey: 'privateNotes.title' as const, + translationKey: 'privateNotes.title', icon: Expensicons.Pencil, isAnonymousAction: false, action: () => ReportUtils.navigateToPrivateNotes(report, session), From b2a5a009825a18f06cbdc19ef6a7dc6c337d4089 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 30 Jan 2024 12:48:10 +0100 Subject: [PATCH 170/446] fix: typecheck --- src/pages/ReportDetailsPage.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index d858b47e9cc2..fc905e35899e 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,10 +1,8 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import type {FC} from 'react'; import React, {useEffect, useMemo} from 'react'; import {ScrollView, View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {SvgProps} from 'react-native-svg'; import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DisplayNames from '@components/DisplayNames'; @@ -33,13 +31,14 @@ import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type IconAsset from '@src/types/utils/IconAsset'; import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; import withReportOrNotFound from './home/report/withReportOrNotFound'; type ReportDetailsPageMenuItem = { key: DeepValueOf; translationKey: TranslationPaths; - icon: FC; + icon: IconAsset; isAnonymousAction: boolean; action: () => void; brickRoadIndicator?: ValueOf; @@ -161,7 +160,6 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD const displayNamesWithTooltips = useMemo(() => { const hasMultipleParticipants = participants.length > 1; - // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails), hasMultipleParticipants); }, [participants, personalDetails]); From cf9fcfbdc671b8c3d9155a56747db761900bc244 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 30 Jan 2024 14:46:15 +0100 Subject: [PATCH 171/446] add new harvesting field of policy --- src/types/onyx/Policy.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index eca7e9d1ee06..ffa619291516 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -88,9 +88,14 @@ type Policy = { /** The scheduled submit frequency set up on the this policy */ autoReportingFrequency?: ValueOf; - /** Whether the scheduled submit is enabled */ + /** @deprecated Whether the scheduled submit is enabled */ isHarvestingEnabled?: boolean; + /** Whether the scheduled submit is enabled */ + harvesting?: { + enabled: boolean; + }; + /** Whether the scheduled submit is enabled */ isPreventSelfApprovalEnabled?: boolean; From 821ee65b43a8c2add86e75184b848c3403c03362 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 30 Jan 2024 14:47:17 +0100 Subject: [PATCH 172/446] clarify comments --- src/types/onyx/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index ffa619291516..719e0ba1fb9d 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -96,7 +96,7 @@ type Policy = { enabled: boolean; }; - /** Whether the scheduled submit is enabled */ + /** Whether the self approval or submitting is enabled */ isPreventSelfApprovalEnabled?: boolean; /** When the monthly scheduled submit should happen */ From 9a36327265853796e28ebb4caaa3c6c177a9d8fa Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 30 Jan 2024 14:59:12 +0100 Subject: [PATCH 173/446] integrate new harvesting field --- src/components/MoneyReportHeader.tsx | 4 +-- .../ReportActionItem/ReportPreview.tsx | 4 +-- src/libs/NextStepUtils.ts | 4 +-- src/libs/actions/IOU.js | 2 +- tests/unit/NextStepUtilsTest.ts | 36 ++++++++++++++----- tests/utils/LHNTestUtils.js | 4 ++- tests/utils/collections/policies.ts | 4 ++- 7 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 4b4e3915f969..c2e6ff341416 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -86,8 +86,8 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => chatReport?.isOwnPolicyExpenseChat && !policy.isHarvestingEnabled, - [chatReport?.isOwnPolicyExpenseChat, policy.isHarvestingEnabled], + () => chatReport?.isOwnPolicyExpenseChat && !(policy.harvesting?.enabled ?? policy.isHarvestingEnabled), + [chatReport?.isOwnPolicyExpenseChat, policy.harvesting?.enabled, policy.isHarvestingEnabled], ); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)]; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index b2fece085f57..52e9d94eaefd 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -159,8 +159,8 @@ function ReportPreview({ // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( - () => chatReport?.isOwnPolicyExpenseChat && !policy?.isHarvestingEnabled, - [chatReport?.isOwnPolicyExpenseChat, policy?.isHarvestingEnabled], + () => chatReport?.isOwnPolicyExpenseChat && !(policy?.harvesting?.enabled ?? policy?.isHarvestingEnabled), + [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled, policy?.isHarvestingEnabled], ); const getDisplayAmount = (): string => { diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index dec6acaecec2..85986e57057e 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -62,7 +62,7 @@ type BuildNextStepParameters = { */ function buildNextStep(report: Report, predictedNextStatus: ValueOf, {isPaidWithWallet}: BuildNextStepParameters = {}): ReportNextStep | null { const policy = ReportUtils.getPolicy(report.policyID ?? ''); - const {submitsTo, isHarvestingEnabled, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; + const {submitsTo, harvesting, isHarvestingEnabled, isPreventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; const {ownerAccountID = -1, managerID = -1} = report; const isOwner = currentUserAccountID === ownerAccountID; const isManager = currentUserAccountID === managerID; @@ -101,7 +101,7 @@ function buildNextStep(report: Report, predictedNextStatus: ValueOf { id: policyID, owner: currentUserEmail, submitsTo: currentUserAccountID, - isHarvestingEnabled: false, + harvesting: { + enabled: false, + }, // Required props name: 'Policy', role: 'admin', @@ -110,7 +112,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -134,7 +138,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -158,7 +164,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -182,7 +190,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: 2, }).then(() => { @@ -207,7 +217,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_DAY_OF_MONTH, }).then(() => { @@ -233,7 +245,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY, autoReportingOffset: CONST.POLICY.AUTO_REPORTING_OFFSET.LAST_BUSINESS_DAY_OF_MONTH, }).then(() => { @@ -258,7 +272,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); @@ -289,7 +305,9 @@ describe('libs/NextStepUtils', () => { ]; return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL, }).then(() => { const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.OPEN); diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index 6c72558e5df3..04246c1c438a 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -256,7 +256,9 @@ function getFakePolicy(id = 1, name = 'Workspace-Test-001') { lastModified: 1697323926777105, autoReporting: true, autoReportingFrequency: 'immediate', - isHarvestingEnabled: true, + harvesting: { + enabled: true, + }, autoReportingOffset: 1, isPreventSelfApprovalEnabled: true, submitsTo: 123456, diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts index 8547c171c7a7..4223c7e41941 100644 --- a/tests/utils/collections/policies.ts +++ b/tests/utils/collections/policies.ts @@ -11,7 +11,9 @@ export default function createRandomPolicy(index: number): Policy { autoReporting: randBoolean(), isPolicyExpenseChatEnabled: randBoolean(), autoReportingFrequency: rand(Object.values(CONST.POLICY.AUTO_REPORTING_FREQUENCIES)), - isHarvestingEnabled: randBoolean(), + harvesting: { + enabled: randBoolean(), + }, autoReportingOffset: 1, isPreventSelfApprovalEnabled: randBoolean(), submitsTo: index, From 21e645d7a45afd49a9828e0847db78f1d9c62b3f Mon Sep 17 00:00:00 2001 From: Hezekiel Tamire Date: Tue, 30 Jan 2024 17:57:35 +0300 Subject: [PATCH 174/446] used export default to make the file a module --- src/libs/checkForUpdates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/checkForUpdates.ts b/src/libs/checkForUpdates.ts index 51ce12335e29..4633eecffe0d 100644 --- a/src/libs/checkForUpdates.ts +++ b/src/libs/checkForUpdates.ts @@ -16,4 +16,4 @@ function checkForUpdates(platformSpecificUpdater: PlatformSpecificUpdater) { }, UPDATE_INTERVAL); } -module.exports = checkForUpdates; +export default checkForUpdates; From 04503940dc67d5223a44690c1ea819a53ea67953 Mon Sep 17 00:00:00 2001 From: Shahe Shahinyan Date: Tue, 30 Jan 2024 18:58:59 +0400 Subject: [PATCH 175/446] run prettier --- .../SignInPageLayout/BackgroundImage/index.ios.js | 7 +++---- src/pages/signin/SignInPageLayout/index.js | 10 +++++++++- src/styles/index.ts | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js index 6e933fc7f2a2..8a4656aeb4d8 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js @@ -1,12 +1,12 @@ -import {View} from "react-native"; import {Image} from 'expo-image'; import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; +import {View} from 'react-native'; import DesktopBackgroundImage from '@assets/images/home-background--desktop.svg'; import MobileBackgroundImage from '@assets/images/home-background--mobile-new.svg'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from "@hooks/useWindowDimensions"; +import useWindowDimensions from '@hooks/useWindowDimensions'; import defaultPropTypes from './propTypes'; const defaultProps = { @@ -22,7 +22,7 @@ const propTypes = { function BackgroundImage(props) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {windowHeight} = useWindowDimensions() + const {windowHeight} = useWindowDimensions(); const src = useMemo(() => (props.isSmallScreen ? MobileBackgroundImage : DesktopBackgroundImage), [props.isSmallScreen]); return ( @@ -32,7 +32,6 @@ function BackgroundImage(props) { style={styles.signInBackgroundImage} /> - ); } diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.js index d37f5fedf1de..14abc0e4128d 100644 --- a/src/pages/signin/SignInPageLayout/index.js +++ b/src/pages/signin/SignInPageLayout/index.js @@ -161,7 +161,15 @@ function SignInPageLayout(props) { keyboardShouldPersistTaps="handled" ref={scrollViewRef} > - + top: 0, left: 0, backgroundColor: colors.productDark200, - justifyContent: "flex-end" + justifyContent: 'flex-end', }, signInBackgroundImage: { From 8a9c61b5603ead20b3df620aad75728c75deca64 Mon Sep 17 00:00:00 2001 From: Hezekiel Tamire Date: Tue, 30 Jan 2024 18:00:44 +0300 Subject: [PATCH 176/446] migrate index.js to ts --- src/setup/{index.js => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/setup/{index.js => index.ts} (100%) diff --git a/src/setup/index.js b/src/setup/index.ts similarity index 100% rename from src/setup/index.js rename to src/setup/index.ts From 43e8533b4fd4a0366d2593a6f331f0ae16345064 Mon Sep 17 00:00:00 2001 From: Hezekiel Tamire Date: Tue, 30 Jan 2024 18:11:29 +0300 Subject: [PATCH 177/446] added loading to session type --- src/types/onyx/Session.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/onyx/Session.ts b/src/types/onyx/Session.ts index e17bfaf1c2b9..c4428969ee70 100644 --- a/src/types/onyx/Session.ts +++ b/src/types/onyx/Session.ts @@ -20,6 +20,9 @@ type Session = { /** Currently logged in user encrypted authToken */ encryptedAuthToken?: string; + /** Boolean that indicates whether it is loading or not */ + loading: boolean, + /** Currently logged in user accountID */ accountID?: number; From ef0b1bdd6787d196f57cc84f99ae9c6115b0cfbb Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 30 Jan 2024 16:12:45 +0100 Subject: [PATCH 178/446] migrate ConnectBankAccountButton to TypeScript --- src/components/ConnectBankAccountButton.js | 57 --------------------- src/components/ConnectBankAccountButton.tsx | 44 ++++++++++++++++ 2 files changed, 44 insertions(+), 57 deletions(-) delete mode 100644 src/components/ConnectBankAccountButton.js create mode 100644 src/components/ConnectBankAccountButton.tsx diff --git a/src/components/ConnectBankAccountButton.js b/src/components/ConnectBankAccountButton.js deleted file mode 100644 index f036918d9429..000000000000 --- a/src/components/ConnectBankAccountButton.js +++ /dev/null @@ -1,57 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; -import * as ReimbursementAccount from '@userActions/ReimbursementAccount'; -import Button from './Button'; -import * as Expensicons from './Icon/Expensicons'; -import networkPropTypes from './networkPropTypes'; -import {withNetwork} from './OnyxProvider'; -import Text from './Text'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const propTypes = { - ...withLocalizePropTypes, - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** PolicyID for navigating to bank account route of that policy */ - policyID: PropTypes.string.isRequired, - - /** Button styles, also applied for offline message wrapper */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), -}; - -const defaultProps = { - style: [], -}; - -function ConnectBankAccountButton(props) { - const styles = useThemeStyles(); - const activeRoute = Navigation.getActiveRouteWithoutParams(); - return props.network.isOffline ? ( - - {`${props.translate('common.youAppearToBeOffline')} ${props.translate('common.thisFeatureRequiresInternet')}`} - - ) : ( -