From 2a543a212015993ee58541d51307f803c4d322ff Mon Sep 17 00:00:00 2001 From: Kadie Alexander Date: Wed, 16 Aug 2023 19:46:34 +1200 Subject: [PATCH 0001/1215] 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 0002/1215] 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 0003/1215] 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 0004/1215] 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 0005/1215] 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 0006/1215] 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 0007/1215] 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 0008/1215] 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 0009/1215] 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 0010/1215] 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 0011/1215] 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 0012/1215] 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 d7ef177d026828596c934d6c421f9ece149b32ea Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Wed, 25 Oct 2023 11:20:04 +0200 Subject: [PATCH 0013/1215] feat: personal info page --- .../substeps/FullNamePersonalStep.js | 67 +++++++++++++++++++ .../ReimbursementAccount/subStepPropTypes.js | 9 +++ 2 files changed, 76 insertions(+) create mode 100644 src/pages/ReimbursementAccount/PersonalInfo/substeps/FullNamePersonalStep.js create mode 100644 src/pages/ReimbursementAccount/subStepPropTypes.js diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullNamePersonalStep.js b/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullNamePersonalStep.js new file mode 100644 index 000000000000..e74d1bb129c1 --- /dev/null +++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/FullNamePersonalStep.js @@ -0,0 +1,67 @@ +import React from 'react'; +import {View} from 'react-native'; +import useLocalize from '../../../../hooks/useLocalize'; +import styles from '../../../../styles/styles'; +import TextInput from '../../../../components/TextInput'; +import CONST from '../../../../CONST'; +import Form from '../../../../components/Form'; +import ONYXKEYS from '../../../../ONYXKEYS'; +import subStepPropTypes from '../../subStepPropTypes'; + +const propTypes = { + ...subStepPropTypes, +}; + +const validate = (values) => {}; + +function FullNamePersonalStep({onNext}) { + const {translate} = useLocalize(); + + return ( + +
+ + + props.onFieldChange({firstName: value})} + errorText={props.errors.firstName ? props.translate('bankAccount.error.firstName') : ''} + /> + + + props.onFieldChange({lastName: value})} + errorText={props.errors.lastName ? props.translate('bankAccount.error.lastName') : ''} + /> + + +
+
+ ); +} + +FullNamePersonalStep.propTypes = propTypes; +FullNamePersonalStep.defaultProps = defaultProps; +FullNamePersonalStep.displayName = 'FullNamePersonalStep'; + +export default compose(withOnyx({}))(FullNamePersonalStep); diff --git a/src/pages/ReimbursementAccount/subStepPropTypes.js b/src/pages/ReimbursementAccount/subStepPropTypes.js new file mode 100644 index 000000000000..058e0cc78c56 --- /dev/null +++ b/src/pages/ReimbursementAccount/subStepPropTypes.js @@ -0,0 +1,9 @@ +import PropTypes from 'prop-types'; + +const subStepPropTypes = { + isEditing: PropTypes.bool.isRequired, + onNext: PropTypes.func.isRequired, + onMove: PropTypes.func.isRequired, +}; + +export default subStepPropTypes; From 95188d6cb14118265db6a76cb6cbe52eadab039e Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 25 Oct 2023 16:44:38 +0700 Subject: [PATCH 0014/1215] 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 0015/1215] 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 70e00562acef33a84af3118477d9873488693307 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 25 Oct 2023 15:14:24 +0200 Subject: [PATCH 0016/1215] Created Interactive Step Header --- src/components/InteractiveStepSubHeader.js | 100 ++++++++++++++++++ .../InteractiveStepSubHeader.stories.js | 54 ++++++++++ src/styles/styles.ts | 47 ++++++++ 3 files changed, 201 insertions(+) create mode 100644 src/components/InteractiveStepSubHeader.js create mode 100644 src/stories/InteractiveStepSubHeader.stories.js diff --git a/src/components/InteractiveStepSubHeader.js b/src/components/InteractiveStepSubHeader.js new file mode 100644 index 000000000000..63850230e0d2 --- /dev/null +++ b/src/components/InteractiveStepSubHeader.js @@ -0,0 +1,100 @@ +import React, {forwardRef, useState, useImperativeHandle} from 'react'; +import PropTypes from 'prop-types'; +import map from 'lodash/map'; +import {View} from 'react-native'; + +import CONST from '../CONST'; +import variables from '../styles/variables'; +import styles from '../styles/styles'; +import colors from '../styles/colors'; +import * as Expensicons from './Icon/Expensicons'; +import PressableWithFeedback from './Pressable/PressableWithFeedback'; +import Text from './Text'; +import Icon from './Icon'; + +const propTypes = { + stepNames: PropTypes.arrayOf(PropTypes.string).isRequired, + onStepSelected: PropTypes.func.isRequired, + startStep: PropTypes.number, +}; + +const defaultProps = { + startStep: 0, +}; + +const MIN_AMOUNT_FOR_EXPANDING = 3; +const MIN_AMOUNT_OF_STEPS = 2; + +function InteractiveStepSubHeader({stepNames, startStep, onStepSelected}, ref) { + const [currentStep, setCurrentStep] = useState(startStep); + useImperativeHandle( + ref, + () => ({ + moveNext: () => { + setCurrentStep((actualStep) => actualStep + 1); + }, + }), + [], + ); + + if (stepNames.length < MIN_AMOUNT_OF_STEPS) { + console.warn('InteractiveStepSubHeader: stepNames prop must have at least 2 elements'); + return null; + } + + const amountOfUnions = stepNames.length - 1; + + return ( + + {map(stepNames, (stepName, index) => { + const isCompletedStep = currentStep > index; + const isLockedStep = currentStep < index; + const isLockedLine = currentStep < index + 1; + const hasUnion = index < amountOfUnions; + + const moveToStep = () => { + if (isLockedStep) { + return; + } + setCurrentStep(index); + onStepSelected(stepNames[index]); + }; + return ( + + + {isCompletedStep ? ( + + ) : ( + {index + 1} + )} + + {hasUnion ? : null} + + ); + })} + + ); +} + +InteractiveStepSubHeader.propTypes = propTypes; +InteractiveStepSubHeader.defaultProps = defaultProps; +InteractiveStepSubHeader.displayName = 'InteractiveStepSubHeader'; + +export default forwardRef(InteractiveStepSubHeader); diff --git a/src/stories/InteractiveStepSubHeader.stories.js b/src/stories/InteractiveStepSubHeader.stories.js new file mode 100644 index 000000000000..4e932bc5a9d7 --- /dev/null +++ b/src/stories/InteractiveStepSubHeader.stories.js @@ -0,0 +1,54 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React, {useRef} from 'react'; +import {View, Button} from 'react-native'; + +import InteractiveStepSubHeader from '../components/InteractiveStepSubHeader'; + +/** + * We use the Component Story Format for writing stories. Follow the docs here: + * + * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format + */ +const story = { + title: 'Components/InteractiveStepSubHeader', + component: InteractiveStepSubHeader, +}; + +function Template(args) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +// Arguments can be passed to the component by binding +// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +const Default = Template.bind({}); + +function BaseInteractiveStepSubHeader(props) { + const ref = useRef(null); + return ( + + +