From ffb21f156543aa780d9ab15585952a916aea2c9d Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 9 Mar 2024 03:25:09 +0530 Subject: [PATCH 001/259] add POC for fallingback to default attachment view when pdf fails to load --- .../BaseAttachmentViewPdf.js | 4 + .../DefaultAttachmentView.tsx | 59 +++++++++++++ .../AttachmentView/AttachmentViewPdf/index.js | 3 +- .../AttachmentViewPdf/propTypes.js | 8 ++ .../Attachments/AttachmentView/index.js | 86 ++++++++++++------- .../InvertedFlatList/BaseInvertedFlatList.tsx | 2 +- src/components/PDFView/WebPDFDocument.js | 4 +- src/components/PDFView/index.js | 1 + src/components/PDFView/index.native.js | 59 ++++++++++--- src/styles/index.ts | 14 +++ 10 files changed, 192 insertions(+), 48 deletions(-) create mode 100644 src/components/Attachments/AttachmentView/AttachmentViewPdf/DefaultAttachmentView.tsx diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 2f16b63aacc6..3f3b995c02ba 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -28,6 +28,8 @@ function BaseAttachmentViewPdf({ onLoadComplete, errorLabelStyles, style, + isUsedAsChatAttachment, + onError, }) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled; @@ -89,6 +91,8 @@ function BaseAttachmentViewPdf({ onScaleChanged={onScaleChanged} onLoadComplete={onLoadComplete} errorLabelStyles={errorLabelStyles} + isUsedAsChatAttachment={isUsedAsChatAttachment} + onError={onError} /> ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/DefaultAttachmentView.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/DefaultAttachmentView.tsx new file mode 100644 index 000000000000..ad102fd5caad --- /dev/null +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/DefaultAttachmentView.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import {ActivityIndicator, View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import _ from 'underscore'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type DefaultAttachmentViewProps = { + file: File; + shouldShowLoadingSpinnerIcon: boolean; + shouldShowDownloadIcon: boolean; + containerStyles: StyleProp[]; +}; + +function DefaultAttachmentView({file, shouldShowLoadingSpinnerIcon, shouldShowDownloadIcon, containerStyles}: DefaultAttachmentViewProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + + + + {file && file.name} + {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( + + + + + + )} + {shouldShowLoadingSpinnerIcon && ( + + + + + + )} + + ); +} + +export default DefaultAttachmentView; diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js index d6a402613c34..de740f4fdd1f 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style, isUsedAsChatAttachment, onError}) { return ( ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js index a34010f0ba8b..bcb0fabde352 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js @@ -15,6 +15,12 @@ const attachmentViewPdfPropTypes = { /** Styles for the error label */ errorLabelStyles: stylePropTypes, + + /** Callback when the pdf fails to load */ + onError: PropTypes.func, + + /** Whether the attachment is used as a chat attachment */ + isUsedAsChatAttachment: PropTypes.bool, }; const attachmentViewPdfDefaultProps = { @@ -23,6 +29,8 @@ const attachmentViewPdfDefaultProps = { }, style: [], errorLabelStyles: [], + onError: () => {}, + isUsedAsChatAttachment: false, }; export {attachmentViewPdfPropTypes, attachmentViewPdfDefaultProps}; diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index f6a56dc73088..76e546be8930 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -1,16 +1,13 @@ import Str from 'expensify-common/lib/str'; import PropTypes from 'prop-types'; import React, {memo, useEffect, useState} from 'react'; -import {ActivityIndicator, ScrollView, View} from 'react-native'; +import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import * as AttachmentsPropTypes from '@components/Attachments/propTypes'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -25,6 +22,7 @@ import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; +import DefaultAttachmentView from './AttachmentViewPdf/DefaultAttachmentView'; import AttachmentViewVideo from './AttachmentViewVideo'; import {attachmentViewDefaultProps, attachmentViewPropTypes} from './propTypes'; @@ -107,6 +105,7 @@ function AttachmentView({ const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file && Str.isVideo(file.name)); + const [isPdfFailedToLoad, setIsPdfFailedToLoad] = useState(false); useEffect(() => { if (!isFocused && !(file && isUsedInAttachmentModal)) { @@ -169,6 +168,17 @@ function AttachmentView({ } }; + if (isPdfFailedToLoad) { + return ( + + ); + } + // We need the following View component on android native // So that the event will propagate properly and // the Password protected preview will be shown for pdf attachement we are about to send. @@ -186,6 +196,10 @@ function AttachmentView({ errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} + isUsedAsChatAttachment={!(isUsedInAttachmentModal || isUsedInCarousel)} + onError={() => { + setIsPdfFailedToLoad(true); + }} /> ); @@ -229,36 +243,42 @@ function AttachmentView({ } return ( - - - - + + // + // + // + // - {file && file.name} - {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( - - - - - - )} - {shouldShowLoadingSpinnerIcon && ( - - - - - - )} - + // {file && file.name} + // {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( + // + // + // + // + // + // )} + // {shouldShowLoadingSpinnerIcon && ( + // + // + // + // + // + // )} + // ); } diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index e28400505280..d0a4322300c3 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -4,7 +4,7 @@ import type {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; const WINDOW_SIZE = 15; -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; +const AUTOSCROLL_TO_TOP_THRESHOLD = 250; function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { return ( diff --git a/src/components/PDFView/WebPDFDocument.js b/src/components/PDFView/WebPDFDocument.js index dd9d1e066b19..d2e93e3ba506 100644 --- a/src/components/PDFView/WebPDFDocument.js +++ b/src/components/PDFView/WebPDFDocument.js @@ -48,6 +48,8 @@ const propTypes = { * - `undefined` if password isn't needed to view the PDF file * - `null` if the password is required but hasn't been provided yet */ password: PropTypes.string, + /** Callback invoked when the PDF document fails to load */ + onError: PropTypes.func.isRequired, }; const defaultProps = { @@ -95,7 +97,7 @@ const WebPDFDocument = memo( return ( } - error={{translate('attachmentView.failedToLoadPDF')}} + onLoadError={onError} file={sourceURL} options={{ cMapUrl: 'cmaps/', diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index 9706f8e06cc1..2e39594fc210 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -270,6 +270,7 @@ class PDFView extends Component { pageWidth={pageWidth} password={this.state.password} initiatePasswordChallenge={this.initiatePasswordChallenge} + onError={this.props.onError} /> {(this.state.password === PDFViewConstants.REQUIRED_PASSWORD_MISSING || this.state.isCheckingPassword) && ( diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 558f6636a325..6c0f5acd1081 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -2,17 +2,22 @@ import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import PDF from 'react-native-pdf'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import Text from '@components/Text'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; +import Text from '@components/Text'; const propTypes = { ...pdfViewPropTypes, @@ -33,7 +38,10 @@ const propTypes = { * is (temporarily) rendered. */ -function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, errorLabelStyles}) { +const THUMBNAIL_HEIGHT = 250; +const THUMBNAIL_WIDTH = 250; + +function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, errorLabelStyles, onError, isUsedAsChatAttachment}) { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); @@ -46,6 +54,8 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused const themeStyles = useThemeStyles(); const {isKeyboardShown} = useKeyboardState(); const StyleUtils = useStyleUtils(); + const {isOffline} = useNetwork(); + const theme = useTheme(); useEffect(() => { onToggleKeyboard(isKeyboardShown); @@ -84,6 +94,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused setShouldShowLoadingIndicator(false); setShouldRequestPassword(false); setShouldAttemptPDFLoad(false); + onError(error); }; /** @@ -113,9 +124,33 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused setSuccessToLoadPDF(true); onLoadComplete(path); }; + const rendeFailedToLoadPDF = () => { + if (!isUsedAsChatAttachment) { + return ( + + {translate('attachmentView.failedToLoadPDF')} + + ); + } + + return ( + + + + + + ); + }; function renderPDFView() { - const pdfStyles = [themeStyles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(windowWidth, windowHeight)]; + const pdfHeight = isUsedAsChatAttachment ? THUMBNAIL_HEIGHT : windowHeight; + const pdfWeight = isUsedAsChatAttachment ? THUMBNAIL_WIDTH : windowWidth; + const pdfStyles = [StyleUtils.getWidthAndHeightStyle(pdfWeight, pdfHeight), themeStyles.imageModalPDF]; // 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 @@ -124,22 +159,18 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused if (shouldRequestPassword) { pdfStyles.push(themeStyles.invisible); } - - const containerStyles = shouldRequestPassword && isSmallScreenWidth ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; + const containerStyles = + (shouldRequestPassword && isSmallScreenWidth) || isUsedAsChatAttachment ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; + const loadingIndicatorStyles = isUsedAsChatAttachment ? [themeStyles.chatItemPDFAttachmentLoading, StyleUtils.getWidthAndHeightStyle(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)] : []; return ( - {failedToLoadPDF && ( - - {translate('attachmentView.failedToLoadPDF')} - - )} {shouldAttemptPDFLoad && ( } - source={{uri: sourceURL, cache: true, expiration: 864000}} + renderActivityIndicator={() => } + source={{uri: sourceURL}} style={pdfStyles} onError={handleFailureToLoadPDF} password={password} @@ -162,6 +193,10 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused ); } + // // uncomment this code to see failedToLoadPDF + // if (failedToLoadPDF) { + // return rendeFailedToLoadPDF(); + // } return onPress && !successToLoadPDF ? ( borderRadius: variables.buttonBorderRadius, }, + componentBorderRadiusNormal: { + borderRadius: variables.componentBorderRadiusNormal, + }, + bottomTabBarContainer: { flexDirection: 'row', height: variables.bottomTabHeight, @@ -2111,6 +2115,16 @@ const styles = (theme: ThemeColors) => width: 200, }, + chatItemPDFAttachmentLoading: { + backgroundColor: 'transparent', + borderColor: theme.border, + borderWidth: 1, + borderRadius: variables.componentBorderRadiusNormal, + textAlign: 'center', + verticalAlign: 'middle', + opacity: 1, + }, + sidebarVisible: { borderRightWidth: 1, }, From bde48faf072512f5d79d29ce293ae83b44241171 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 9 Mar 2024 03:26:09 +0530 Subject: [PATCH 002/259] adds chnages for attachement view --- .../Attachments/AttachmentView/index.js | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 76e546be8930..f15b235144cb 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -12,7 +12,6 @@ import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContex import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; @@ -100,7 +99,6 @@ function AttachmentView({ optionalVideoDuration, }) { const {updateCurrentlyPlayingURL} = usePlaybackContext(); - const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); @@ -155,7 +153,7 @@ function AttachmentView({ // Check both source and file.name since PDFs dragged into the text field // will appear with a source that is a blob - if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { + if (!isPdfFailedToLoad && ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename'))))) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; const onPDFLoadComplete = (path) => { @@ -168,17 +166,6 @@ function AttachmentView({ } }; - if (isPdfFailedToLoad) { - return ( - - ); - } - // We need the following View component on android native // So that the event will propagate properly and // the Password protected preview will be shown for pdf attachement we are about to send. @@ -249,36 +236,6 @@ function AttachmentView({ shouldShowLoadingSpinnerIcon={shouldShowLoadingSpinnerIcon} containerStyles={containerStyles} /> - // - // - // - // - - // {file && file.name} - // {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( - // - // - // - // - // - // )} - // {shouldShowLoadingSpinnerIcon && ( - // - // - // - // - // - // )} - // ); } From 757661bf2c41ad8f7bc0abeed6102202890c7679 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 9 Mar 2024 03:32:34 +0530 Subject: [PATCH 003/259] formatting --- 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 6c0f5acd1081..492a12e1dc66 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -6,6 +6,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import Text from '@components/Text'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -17,7 +18,6 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; -import Text from '@components/Text'; const propTypes = { ...pdfViewPropTypes, From 26c1f13c9fd6b7ede36ecf38c1d17e0ebb69fd7b Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 9 Mar 2024 03:41:08 +0530 Subject: [PATCH 004/259] fixes conflicts --- .../Attachments/AttachmentView/index.js | 435 ++++++++++-------- 1 file changed, 247 insertions(+), 188 deletions(-) diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 492a12e1dc66..461548f0d2b1 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -1,219 +1,278 @@ -import React, {useCallback, useEffect, useState} from 'react'; -import {View} from 'react-native'; -import PDF from 'react-native-pdf'; -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import Str from 'expensify-common/lib/str'; +import PropTypes from 'prop-types'; +import React, {memo, useEffect, useState} from 'react'; +import {ActivityIndicator, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import * as AttachmentsPropTypes from '@components/Attachments/propTypes'; +import DistanceEReceipt from '@components/DistanceEReceipt'; +import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; -import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; -import useKeyboardState from '@hooks/useKeyboardState'; -import useLocalize from '@hooks/useLocalize'; +import Tooltip from '@components/Tooltip'; +import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; +import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import compose from '@libs/compose'; +import * as TransactionUtils from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import PDFPasswordForm from './PDFPasswordForm'; -import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; +import ONYXKEYS from '@src/ONYXKEYS'; +import AttachmentViewImage from './AttachmentViewImage'; +import AttachmentViewPdf from './AttachmentViewPdf'; +import AttachmentViewVideo from './AttachmentViewVideo'; +import {attachmentViewDefaultProps, attachmentViewPropTypes} from './propTypes'; const propTypes = { - ...pdfViewPropTypes, + ...attachmentViewPropTypes, + ...withLocalizePropTypes, + + /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */ + source: AttachmentsPropTypes.attachmentSourcePropType.isRequired, + + /** Flag to show/hide download icon */ + shouldShowDownloadIcon: PropTypes.bool, + + /** Flag to show the loading indicator */ + shouldShowLoadingSpinnerIcon: PropTypes.bool, + + /** Notify parent that the UI should be modified to accommodate keyboard */ + onToggleKeyboard: PropTypes.func, + + /** Extra styles to pass to View wrapper */ + // eslint-disable-next-line react/forbid-prop-types + containerStyles: PropTypes.arrayOf(PropTypes.object), + + /** Denotes whether it is a workspace avatar or not */ + isWorkspaceAvatar: PropTypes.bool, + + /** Denotes whether it is an icon (ex: SVG) */ + maybeIcon: PropTypes.bool, + + /** The id of the transaction related to the attachment */ + // eslint-disable-next-line react/no-unused-prop-types + transactionID: PropTypes.string, + + /** The id of the report action related to the attachment */ + reportActionID: PropTypes.string, + + isHovered: PropTypes.bool, + + optionalVideoDuration: PropTypes.number, }; -/** - * 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 the user. - * - * In order to render things nicely during a password challenge we need - * to keep track of additional state. In particular, the - * react-native-pdf/PDF component is both conditionally rendered and hidden - * depending upon the situation. It needs to be rerendered on each password - * submission because it doesn't dynamically handle updates to its - * password property. And we need to hide it during password challenges - * so that PDFPasswordForm doesn't bounce when react-native-pdf/PDF - * is (temporarily) rendered. - */ - -const THUMBNAIL_HEIGHT = 250; -const THUMBNAIL_WIDTH = 250; - -function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, errorLabelStyles, onError, isUsedAsChatAttachment}) { - 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 {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); - const {translate} = useLocalize(); - const themeStyles = useThemeStyles(); - const {isKeyboardShown} = useKeyboardState(); - const StyleUtils = useStyleUtils(); - const {isOffline} = useNetwork(); +const defaultProps = { + ...attachmentViewDefaultProps, + shouldShowDownloadIcon: false, + shouldShowLoadingSpinnerIcon: false, + onToggleKeyboard: () => {}, + containerStyles: [], + isWorkspaceAvatar: false, + maybeIcon: false, + transactionID: '', + reportActionID: '', + isHovered: false, + optionalVideoDuration: 0, +}; + +function AttachmentView({ + source, + file, + isAuthTokenRequired, + onPress, + shouldShowLoadingSpinnerIcon, + shouldShowDownloadIcon, + containerStyles, + onToggleKeyboard, + translate, + isFocused, + isUsedInCarousel, + isUsedInAttachmentModal, + isWorkspaceAvatar, + maybeIcon, + fallbackSource, + transaction, + reportActionID, + isHovered, + optionalVideoDuration, +}) { + const {updateCurrentlyPlayingURL} = usePlaybackContext(); const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const [loadComplete, setLoadComplete] = useState(false); + const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file && Str.isVideo(file.name)); useEffect(() => { - onToggleKeyboard(isKeyboardShown); - }); - - /** - * Initiate password challenge if message received from react-native-pdf/PDF - * indicates that a password is required or invalid. - * - * For a password challenge the message is "Password required or incorrect password." - * Note that the message doesn't specify whether the password is simply empty or - * invalid. - */ - - 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)) { - initiatePasswordChallenge(); + if (!isFocused && !(file && isUsedInAttachmentModal)) { return; } - setFailedToLoadPDF(true); - setShouldShowLoadingIndicator(false); - setShouldRequestPassword(false); - setShouldAttemptPDFLoad(false); - onError(error); - }; - - /** - * When the password is submitted via PDFPasswordForm, save the password - * 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} pdfPassword Password submitted via PDFPasswordForm - */ - 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. - * @param {Number} numberOfPages - * @param {Number} path - Path to cache location - */ - const finishPDFLoad = (numberOfPages, path) => { - setShouldRequestPassword(false); - setShouldShowLoadingIndicator(false); - setSuccessToLoadPDF(true); - onLoadComplete(path); - }; - const rendeFailedToLoadPDF = () => { - if (!isUsedAsChatAttachment) { - return ( - - {translate('attachmentView.failedToLoadPDF')} - - ); + updateCurrentlyPlayingURL(isVideo ? source : null); + }, [isFocused, isVideo, source, updateCurrentlyPlayingURL, file, isUsedInAttachmentModal]); + + const [imageError, setImageError] = useState(false); + + useNetwork({onReconnect: () => setImageError(false)}); + + // Handles case where source is a component (ex: SVG) or a number + // Number may represent a SVG or an image + if ((maybeIcon && typeof source === 'number') || _.isFunction(source)) { + let iconFillColor = ''; + let additionalStyles = []; + if (isWorkspaceAvatar) { + const defaultWorkspaceAvatarColor = StyleUtils.getDefaultWorkspaceAvatarColor(file.name); + iconFillColor = defaultWorkspaceAvatarColor.fill; + additionalStyles = [defaultWorkspaceAvatarColor]; } return ( - - - - + + ); + } + + if (TransactionUtils.hasEReceipt(transaction)) { + return ( + + + + ); - }; - - function renderPDFView() { - const pdfHeight = isUsedAsChatAttachment ? THUMBNAIL_HEIGHT : windowHeight; - const pdfWeight = isUsedAsChatAttachment ? THUMBNAIL_WIDTH : windowWidth; - const pdfStyles = [StyleUtils.getWidthAndHeightStyle(pdfWeight, pdfHeight), themeStyles.imageModalPDF]; - - // 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); - } - const containerStyles = - (shouldRequestPassword && isSmallScreenWidth) || isUsedAsChatAttachment ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; - const loadingIndicatorStyles = isUsedAsChatAttachment ? [themeStyles.chatItemPDFAttachmentLoading, StyleUtils.getWidthAndHeightStyle(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)] : []; + } + + // Check both source and file.name since PDFs dragged into the text field + // will appear with a source that is a blob + if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { + const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; + + const onPDFLoadComplete = (path) => { + const id = (transaction && transaction.transactionID) || reportActionID; + if (path && id) { + CachedPDFPaths.add(id, path); + } + if (!loadComplete) { + setLoadComplete(true); + } + }; + // We need the following View component on android native + // So that the event will propagate properly and + // the Password protected preview will be shown for pdf attachement we are about to send. return ( - - {shouldAttemptPDFLoad && ( - } - source={{uri: sourceURL}} - style={pdfStyles} - onError={handleFailureToLoadPDF} - password={password} - onLoadComplete={finishPDFLoad} - onPageSingleTap={onPress} - onScaleChanged={onScaleChanged} - /> - )} - {shouldRequestPassword && ( - - setIsPasswordInvalid(false)} - isPasswordInvalid={isPasswordInvalid} - shouldShowLoadingIndicator={shouldShowLoadingIndicator} - /> - - )} + + ); } - // // uncomment this code to see failedToLoadPDF - // if (failedToLoadPDF) { - // return rendeFailedToLoadPDF(); - // } - - return onPress && !successToLoadPDF ? ( - - {renderPDFView()} - - ) : ( - renderPDFView() + + if (TransactionUtils.isDistanceRequest(transaction)) { + return ; + } + + // For this check we use both source and file.name since temporary file source is a blob + // both PDFs and images will appear as images when pasted into the text field. + // We also check for numeric source since this is how static images (used for preview) are represented in RN. + const isImage = typeof source === 'number' || Str.isImage(source); + if (isImage || (file && Str.isImage(file.name))) { + return ( + { + setImageError(true); + }} + /> + ); + } + + if (isVideo) { + return ( + + ); + } + + return ( + + + + + + {file && file.name} + {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( + + + + + + )} + {shouldShowLoadingSpinnerIcon && ( + + + + + + )} + ); } -PDFView.displayName = 'PDFView'; -PDFView.propTypes = propTypes; -PDFView.defaultProps = defaultProps; +AttachmentView.propTypes = propTypes; +AttachmentView.defaultProps = defaultProps; +AttachmentView.displayName = 'AttachmentView'; -export default PDFView; +export default compose( + memo, + withLocalize, + withOnyx({ + transaction: { + key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + }, + }), +)(AttachmentView); From 36b581464aa1c9f6fbbb08dbc70f939b55017756 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 9 Mar 2024 03:57:23 +0530 Subject: [PATCH 005/259] fix conflict after merge --- .../DefaultAttachmentView.tsx | 59 ------------------- .../Attachments/AttachmentView/index.js | 7 ++- src/components/PDFView/index.native.js | 1 + 3 files changed, 7 insertions(+), 60 deletions(-) delete mode 100644 src/components/Attachments/AttachmentView/AttachmentViewPdf/DefaultAttachmentView.tsx diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/DefaultAttachmentView.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/DefaultAttachmentView.tsx deleted file mode 100644 index ad102fd5caad..000000000000 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/DefaultAttachmentView.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import {ActivityIndicator, View} from 'react-native'; -import type {StyleProp, ViewStyle} from 'react-native'; -import _ from 'underscore'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; -import useLocalize from '@hooks/useLocalize'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; - -type DefaultAttachmentViewProps = { - file: File; - shouldShowLoadingSpinnerIcon: boolean; - shouldShowDownloadIcon: boolean; - containerStyles: StyleProp[]; -}; - -function DefaultAttachmentView({file, shouldShowLoadingSpinnerIcon, shouldShowDownloadIcon, containerStyles}: DefaultAttachmentViewProps) { - const theme = useTheme(); - const styles = useThemeStyles(); - const {translate} = useLocalize(); - - return ( - - - - - - {file && file.name} - {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( - - - - - - )} - {shouldShowLoadingSpinnerIcon && ( - - - - - - )} - - ); -} - -export default DefaultAttachmentView; diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 461548f0d2b1..fc11ccfd8fac 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -107,6 +107,7 @@ function AttachmentView({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); + const [isPdfFailedToLoad, setIsPdfFailedToLoad] = useState(false); const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file && Str.isVideo(file.name)); useEffect(() => { @@ -157,7 +158,7 @@ function AttachmentView({ // Check both source and file.name since PDFs dragged into the text field // will appear with a source that is a blob - if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { + if (!isPdfFailedToLoad && ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename'))))) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; const onPDFLoadComplete = (path) => { @@ -187,6 +188,10 @@ function AttachmentView({ errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} + isUsedAsChatAttachment={!(isUsedInAttachmentModal || isUsedInCarousel)} + onError={() => { + setIsPdfFailedToLoad(true); + }} /> ); diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 492a12e1dc66..f7901b035d8b 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -124,6 +124,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused setSuccessToLoadPDF(true); onLoadComplete(path); }; + // eslint-disable-next-line no-unused-vars const rendeFailedToLoadPDF = () => { if (!isUsedAsChatAttachment) { return ( From 75a733c60b7f82a3221bb24a10b744da622978db Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 14 Mar 2024 01:32:31 +0530 Subject: [PATCH 006/259] clean up and lint fix --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 2 -- .../AttachmentView/AttachmentViewPdf/index.js | 4 ++-- .../AttachmentView/AttachmentViewPdf/propTypes.js | 3 --- src/components/Attachments/AttachmentView/index.js | 6 +----- src/components/PDFView/index.native.js | 11 +++++------ 5 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 3f3b995c02ba..289909a66dab 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -29,7 +29,6 @@ function BaseAttachmentViewPdf({ errorLabelStyles, style, isUsedAsChatAttachment, - onError, }) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled; @@ -92,7 +91,6 @@ function BaseAttachmentViewPdf({ onLoadComplete={onLoadComplete} errorLabelStyles={errorLabelStyles} isUsedAsChatAttachment={isUsedAsChatAttachment} - onError={onError} /> ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js index de740f4fdd1f..af665d73a3ba 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style, isUsedAsChatAttachment, onError}) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style, isUsedAsChatAttachment}) { return ( ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js index bcb0fabde352..b6ad86124f63 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js @@ -16,9 +16,6 @@ const attachmentViewPdfPropTypes = { /** Styles for the error label */ errorLabelStyles: stylePropTypes, - /** Callback when the pdf fails to load */ - onError: PropTypes.func, - /** Whether the attachment is used as a chat attachment */ isUsedAsChatAttachment: PropTypes.bool, }; diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index fc11ccfd8fac..6ba3035afc86 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -107,7 +107,6 @@ function AttachmentView({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); - const [isPdfFailedToLoad, setIsPdfFailedToLoad] = useState(false); const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file && Str.isVideo(file.name)); useEffect(() => { @@ -158,7 +157,7 @@ function AttachmentView({ // Check both source and file.name since PDFs dragged into the text field // will appear with a source that is a blob - if (!isPdfFailedToLoad && ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename'))))) { + if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; const onPDFLoadComplete = (path) => { @@ -189,9 +188,6 @@ function AttachmentView({ style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} isUsedAsChatAttachment={!(isUsedInAttachmentModal || isUsedInCarousel)} - onError={() => { - setIsPdfFailedToLoad(true); - }} /> ); diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index f7901b035d8b..addbcec9b3bf 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -41,7 +41,7 @@ const propTypes = { const THUMBNAIL_HEIGHT = 250; const THUMBNAIL_WIDTH = 250; -function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, errorLabelStyles, onError, isUsedAsChatAttachment}) { +function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, errorLabelStyles, isUsedAsChatAttachment}) { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); @@ -94,7 +94,6 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused setShouldShowLoadingIndicator(false); setShouldRequestPassword(false); setShouldAttemptPDFLoad(false); - onError(error); }; /** @@ -194,10 +193,10 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused ); } - // // uncomment this code to see failedToLoadPDF - // if (failedToLoadPDF) { - // return rendeFailedToLoadPDF(); - // } + + if (failedToLoadPDF) { + return rendeFailedToLoadPDF(); + } return onPress && !successToLoadPDF ? ( Date: Sat, 9 Mar 2024 03:25:09 +0530 Subject: [PATCH 007/259] add POC for fallingback to default attachment view when pdf fails to load --- .../BaseAttachmentViewPdf.js | 2 + .../AttachmentView/AttachmentViewPdf/index.js | 4 +- .../AttachmentViewPdf/propTypes.js | 3 ++ .../Attachments/AttachmentView/index.js | 16 ++++++++ src/components/PDFView/index.native.js | 38 +------------------ 5 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 289909a66dab..3f3b995c02ba 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -29,6 +29,7 @@ function BaseAttachmentViewPdf({ errorLabelStyles, style, isUsedAsChatAttachment, + onError, }) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled; @@ -91,6 +92,7 @@ function BaseAttachmentViewPdf({ onLoadComplete={onLoadComplete} errorLabelStyles={errorLabelStyles} isUsedAsChatAttachment={isUsedAsChatAttachment} + onError={onError} /> ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js index af665d73a3ba..6377bc6e585a 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style, isUsedAsChatAttachment}) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style, onError}) { return ( ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js index b6ad86124f63..bcb0fabde352 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js @@ -16,6 +16,9 @@ const attachmentViewPdfPropTypes = { /** Styles for the error label */ errorLabelStyles: stylePropTypes, + /** Callback when the pdf fails to load */ + onError: PropTypes.func, + /** Whether the attachment is used as a chat attachment */ isUsedAsChatAttachment: PropTypes.bool, }; diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 6ba3035afc86..863f37733972 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -26,6 +26,7 @@ import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; +import DefaultAttachmentView from './AttachmentViewPdf/DefaultAttachmentView'; import AttachmentViewVideo from './AttachmentViewVideo'; import {attachmentViewDefaultProps, attachmentViewPropTypes} from './propTypes'; @@ -108,6 +109,7 @@ function AttachmentView({ const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file && Str.isVideo(file.name)); + const [isPdfFailedToLoad, setIsPdfFailedToLoad] = useState(false); useEffect(() => { if (!isFocused && !(file && isUsedInAttachmentModal)) { @@ -170,6 +172,17 @@ function AttachmentView({ } }; + if (isPdfFailedToLoad) { + return ( + + ); + } + // We need the following View component on android native // So that the event will propagate properly and // the Password protected preview will be shown for pdf attachement we are about to send. @@ -188,6 +201,9 @@ function AttachmentView({ style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} isUsedAsChatAttachment={!(isUsedInAttachmentModal || isUsedInCarousel)} + onError={() => { + setIsPdfFailedToLoad(true); + }} /> ); diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index addbcec9b3bf..2f9bb1a40573 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -2,19 +2,13 @@ import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import PDF from 'react-native-pdf'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import Text from '@components/Text'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; @@ -41,7 +35,7 @@ const propTypes = { const THUMBNAIL_HEIGHT = 250; const THUMBNAIL_WIDTH = 250; -function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, errorLabelStyles, isUsedAsChatAttachment}) { +function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, onError, isUsedAsChatAttachment}) { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); @@ -54,8 +48,6 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused const themeStyles = useThemeStyles(); const {isKeyboardShown} = useKeyboardState(); const StyleUtils = useStyleUtils(); - const {isOffline} = useNetwork(); - const theme = useTheme(); useEffect(() => { onToggleKeyboard(isKeyboardShown); @@ -94,6 +86,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused setShouldShowLoadingIndicator(false); setShouldRequestPassword(false); setShouldAttemptPDFLoad(false); + onError(error); }; /** @@ -123,29 +116,6 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused setSuccessToLoadPDF(true); onLoadComplete(path); }; - // eslint-disable-next-line no-unused-vars - const rendeFailedToLoadPDF = () => { - if (!isUsedAsChatAttachment) { - return ( - - {translate('attachmentView.failedToLoadPDF')} - - ); - } - - return ( - - - - - - ); - }; function renderPDFView() { const pdfHeight = isUsedAsChatAttachment ? THUMBNAIL_HEIGHT : windowHeight; @@ -194,10 +164,6 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused ); } - if (failedToLoadPDF) { - return rendeFailedToLoadPDF(); - } - return onPress && !successToLoadPDF ? ( Date: Sat, 16 Mar 2024 03:17:27 +0530 Subject: [PATCH 008/259] fixes missing prop --- .../Attachments/AttachmentView/AttachmentViewPdf/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js index 6377bc6e585a..f151bf84b7a8 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style, onError}) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style, onError, isUsedAsChatAttachment}) { return ( ); } From b4ac9a1cb3518627a16a401654a80ef47ef6625d Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 16 Mar 2024 03:20:24 +0530 Subject: [PATCH 009/259] remove unused import --- src/components/Attachments/AttachmentView/index.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 863f37733972..56ebad34e963 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -26,7 +26,6 @@ import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; -import DefaultAttachmentView from './AttachmentViewPdf/DefaultAttachmentView'; import AttachmentViewVideo from './AttachmentViewVideo'; import {attachmentViewDefaultProps, attachmentViewPropTypes} from './propTypes'; @@ -159,7 +158,7 @@ function AttachmentView({ // Check both source and file.name since PDFs dragged into the text field // will appear with a source that is a blob - if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { + if (!isPdfFailedToLoad && ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename'))))) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; const onPDFLoadComplete = (path) => { @@ -172,17 +171,6 @@ function AttachmentView({ } }; - if (isPdfFailedToLoad) { - return ( - - ); - } - // We need the following View component on android native // So that the event will propagate properly and // the Password protected preview will be shown for pdf attachement we are about to send. From 03726012f10ad31c29f082ba614a9175d0110b62 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 16 Mar 2024 03:23:31 +0530 Subject: [PATCH 010/259] change variable name --- src/components/PDFView/index.native.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 2f9bb1a40573..c0f36f310346 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -32,8 +32,8 @@ const propTypes = { * is (temporarily) rendered. */ -const THUMBNAIL_HEIGHT = 250; -const THUMBNAIL_WIDTH = 250; +const LOADING_THUMBNAIL_HEIGHT = 250; +const LOADING_THUMBNAIL_WIDTH = 250; function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, onError, isUsedAsChatAttachment}) { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); @@ -118,8 +118,8 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused }; function renderPDFView() { - const pdfHeight = isUsedAsChatAttachment ? THUMBNAIL_HEIGHT : windowHeight; - const pdfWeight = isUsedAsChatAttachment ? THUMBNAIL_WIDTH : windowWidth; + const pdfHeight = isUsedAsChatAttachment ? LOADING_THUMBNAIL_HEIGHT : windowHeight; + const pdfWeight = isUsedAsChatAttachment ? LOADING_THUMBNAIL_WIDTH : windowWidth; const pdfStyles = [StyleUtils.getWidthAndHeightStyle(pdfWeight, pdfHeight), themeStyles.imageModalPDF]; // If we haven't yet successfully validated the password and loaded the PDF, @@ -131,7 +131,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused } const containerStyles = (shouldRequestPassword && isSmallScreenWidth) || isUsedAsChatAttachment ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; - const loadingIndicatorStyles = isUsedAsChatAttachment ? [themeStyles.chatItemPDFAttachmentLoading, StyleUtils.getWidthAndHeightStyle(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)] : []; + const loadingIndicatorStyles = isUsedAsChatAttachment ? [themeStyles.chatItemPDFAttachmentLoading, StyleUtils.getWidthAndHeightStyle(LOADING_THUMBNAIL_WIDTH, LOADING_THUMBNAIL_HEIGHT)] : []; return ( From a20811ae16bab37acca9005f8b481a52f5be6d31 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 16 Mar 2024 03:57:40 +0530 Subject: [PATCH 011/259] fixes lint --- .../AttachmentCarousel/CarouselItem.js | 1 + .../Attachments/AttachmentView/index.js | 2 +- src/components/PDFView/WebPDFDocument.js | 9 +-------- src/components/PDFView/index.native.js | 18 +++++++++--------- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index b2c9fed64467..e924cb8c13e9 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -109,6 +109,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) { isHovered={isModalHovered} isFocused={isFocused} optionalVideoDuration={item.duration} + isUsedInCarousel /> diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 56ebad34e963..4a167ba35c5d 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -188,7 +188,7 @@ function AttachmentView({ errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} - isUsedAsChatAttachment={!(isUsedInAttachmentModal || isUsedInCarousel)} + isUsedAsChatAttachment={!isUsedInAttachmentModal && !isUsedInCarousel} onError={() => { setIsPdfFailedToLoad(true); }} diff --git a/src/components/PDFView/WebPDFDocument.js b/src/components/PDFView/WebPDFDocument.js index d2e93e3ba506..3b1b3ba7c765 100644 --- a/src/components/PDFView/WebPDFDocument.js +++ b/src/components/PDFView/WebPDFDocument.js @@ -5,16 +5,11 @@ import {Document} from 'react-pdf'; import {VariableSizeList as List} from 'react-window'; import _ from 'underscore'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import Text from '@components/Text'; import stylePropTypes from '@styles/stylePropTypes'; import CONST from '@src/CONST'; import PageRenderer from './WebPDFPageRenderer'; const propTypes = { - /** Index of the PDF page to be displayed passed by VariableSizeList */ - errorLabelStyles: stylePropTypes, - /** Returns translated string for given locale and phrase */ - translate: PropTypes.func.isRequired, /** The source URL from which to load PDF file to be displayed */ sourceURL: PropTypes.string.isRequired, /** Callback invoked when the PDF document is loaded successfully */ @@ -53,7 +48,6 @@ const propTypes = { }; const defaultProps = { - errorLabelStyles: [], numPages: null, listStyle: undefined, password: undefined, @@ -61,8 +55,6 @@ const defaultProps = { const WebPDFDocument = memo( ({ - errorLabelStyles, - translate, sourceURL, onDocumentLoadSuccess, pageViewportsLength, @@ -78,6 +70,7 @@ const WebPDFDocument = memo( listStyle, initiatePasswordChallenge, password, + onError, }) => { const onPassword = useCallback( (callback, reason) => { diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index c0f36f310346..87262a566bc7 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -1,5 +1,5 @@ -import React, {useCallback, useEffect, useState} from 'react'; -import {View} from 'react-native'; +import React, { useCallback, useEffect, useState } from 'react'; +import { View } from 'react-native'; import PDF from 'react-native-pdf'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; @@ -11,7 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; -import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; +import { defaultProps, propTypes as pdfViewPropTypes } from './pdfViewPropTypes'; const propTypes = { ...pdfViewPropTypes, @@ -35,7 +35,7 @@ const propTypes = { const LOADING_THUMBNAIL_HEIGHT = 250; const LOADING_THUMBNAIL_WIDTH = 250; -function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, onError, isUsedAsChatAttachment}) { +function PDFView({ onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, onError, isUsedAsChatAttachment }) { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); @@ -43,10 +43,10 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); const [password, setPassword] = useState(''); - const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); - const {translate} = useLocalize(); + const { windowWidth, windowHeight, isSmallScreenWidth } = useWindowDimensions(); + const { translate } = useLocalize(); const themeStyles = useThemeStyles(); - const {isKeyboardShown} = useKeyboardState(); + const { isKeyboardShown } = useKeyboardState(); const StyleUtils = useStyleUtils(); useEffect(() => { @@ -130,7 +130,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused pdfStyles.push(themeStyles.invisible); } const containerStyles = - (shouldRequestPassword && isSmallScreenWidth) || isUsedAsChatAttachment ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; + isUsedAsChatAttachment || (shouldRequestPassword && isSmallScreenWidth) ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; const loadingIndicatorStyles = isUsedAsChatAttachment ? [themeStyles.chatItemPDFAttachmentLoading, StyleUtils.getWidthAndHeightStyle(LOADING_THUMBNAIL_WIDTH, LOADING_THUMBNAIL_HEIGHT)] : []; return ( @@ -140,7 +140,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused fitPolicy={0} trustAllCerts={false} renderActivityIndicator={() => } - source={{uri: sourceURL}} + source={{ uri: sourceURL, cache: true, expiration: 864000 }} style={pdfStyles} onError={handleFailureToLoadPDF} password={password} From 3ace4d305de71b924bd53a82d9e54f864fc07461 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 16 Mar 2024 04:00:16 +0530 Subject: [PATCH 012/259] use correct conditional and use js docs for props --- .../BaseAnchorForAttachmentsOnly.tsx | 1 + src/components/Attachments/AttachmentView/index.js | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index d389ac4b92f0..3e7189ce0304 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -64,6 +64,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow file={{name: displayName}} shouldShowDownloadIcon={!isOffline} shouldShowLoadingSpinnerIcon={isDownloading} + isUsedAsChatAttachment /> )} diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 4a167ba35c5d..0d25de1bd799 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -62,9 +62,14 @@ const propTypes = { /** The id of the report action related to the attachment */ reportActionID: PropTypes.string, + /** Whether the attachment is currently being hovered over */ isHovered: PropTypes.bool, + /** The duration of the video */ optionalVideoDuration: PropTypes.number, + + /** Whether the attachment is being used as a chat attachment */ + isUsedAsChatAttachment: PropTypes.bool, }; const defaultProps = { @@ -79,6 +84,7 @@ const defaultProps = { reportActionID: '', isHovered: false, optionalVideoDuration: 0, + isUsedAsChatAttachment: false, }; function AttachmentView({ @@ -101,6 +107,7 @@ function AttachmentView({ reportActionID, isHovered, optionalVideoDuration, + isUsedAsChatAttachment, }) { const {updateCurrentlyPlayingURL} = usePlaybackContext(); const theme = useTheme(); @@ -188,7 +195,7 @@ function AttachmentView({ errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} - isUsedAsChatAttachment={!isUsedInAttachmentModal && !isUsedInCarousel} + isUsedAsChatAttachment={isUsedAsChatAttachment} onError={() => { setIsPdfFailedToLoad(true); }} From 830a392f5097a91c9d3689c7f500bb54ec0c28e2 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 16 Mar 2024 04:06:08 +0530 Subject: [PATCH 013/259] prettier diffs --- src/components/PDFView/index.native.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js index 87262a566bc7..1c3cd02308fb 100644 --- a/src/components/PDFView/index.native.js +++ b/src/components/PDFView/index.native.js @@ -1,5 +1,5 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { View } from 'react-native'; +import React, {useCallback, useEffect, useState} from 'react'; +import {View} from 'react-native'; import PDF from 'react-native-pdf'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; @@ -11,7 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import PDFPasswordForm from './PDFPasswordForm'; -import { defaultProps, propTypes as pdfViewPropTypes } from './pdfViewPropTypes'; +import {defaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; const propTypes = { ...pdfViewPropTypes, @@ -35,7 +35,7 @@ const propTypes = { const LOADING_THUMBNAIL_HEIGHT = 250; const LOADING_THUMBNAIL_WIDTH = 250; -function PDFView({ onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, onError, isUsedAsChatAttachment }) { +function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, onError, isUsedAsChatAttachment}) { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); @@ -43,10 +43,10 @@ function PDFView({ onToggleKeyboard, onLoadComplete, fileName, onPress, isFocuse const [failedToLoadPDF, setFailedToLoadPDF] = useState(false); const [successToLoadPDF, setSuccessToLoadPDF] = useState(false); const [password, setPassword] = useState(''); - const { windowWidth, windowHeight, isSmallScreenWidth } = useWindowDimensions(); - const { translate } = useLocalize(); + const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const {translate} = useLocalize(); const themeStyles = useThemeStyles(); - const { isKeyboardShown } = useKeyboardState(); + const {isKeyboardShown} = useKeyboardState(); const StyleUtils = useStyleUtils(); useEffect(() => { @@ -131,7 +131,9 @@ function PDFView({ onToggleKeyboard, onLoadComplete, fileName, onPress, isFocuse } const containerStyles = isUsedAsChatAttachment || (shouldRequestPassword && isSmallScreenWidth) ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; - const loadingIndicatorStyles = isUsedAsChatAttachment ? [themeStyles.chatItemPDFAttachmentLoading, StyleUtils.getWidthAndHeightStyle(LOADING_THUMBNAIL_WIDTH, LOADING_THUMBNAIL_HEIGHT)] : []; + const loadingIndicatorStyles = isUsedAsChatAttachment + ? [themeStyles.chatItemPDFAttachmentLoading, StyleUtils.getWidthAndHeightStyle(LOADING_THUMBNAIL_WIDTH, LOADING_THUMBNAIL_HEIGHT)] + : []; return ( @@ -140,7 +142,7 @@ function PDFView({ onToggleKeyboard, onLoadComplete, fileName, onPress, isFocuse fitPolicy={0} trustAllCerts={false} renderActivityIndicator={() => } - source={{ uri: sourceURL, cache: true, expiration: 864000 }} + source={{uri: sourceURL, cache: true, expiration: 864000}} style={pdfStyles} onError={handleFailureToLoadPDF} password={password} From 3813026c23140b62b6b566ec587f4d1da9bdc15c Mon Sep 17 00:00:00 2001 From: Ishpaul Singh <104348397+ishpaul777@users.noreply.github.com> Date: Sat, 16 Mar 2024 04:10:54 +0530 Subject: [PATCH 014/259] Update src/components/Attachments/AttachmentCarousel/CarouselItem.js --- src/components/Attachments/AttachmentCarousel/CarouselItem.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index e924cb8c13e9..b2c9fed64467 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -109,7 +109,6 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) { isHovered={isModalHovered} isFocused={isFocused} optionalVideoDuration={item.duration} - isUsedInCarousel /> From a8cfafea38386727c62d8cdb4081fae9d0b0d62a Mon Sep 17 00:00:00 2001 From: Ishpaul Singh <104348397+ishpaul777@users.noreply.github.com> Date: Sat, 16 Mar 2024 04:21:58 +0530 Subject: [PATCH 015/259] remove unused styles --- src/styles/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index ffd05b97473d..2efb29a95ea5 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -516,10 +516,6 @@ const styles = (theme: ThemeColors) => borderRadius: variables.buttonBorderRadius, }, - componentBorderRadiusNormal: { - borderRadius: variables.componentBorderRadiusNormal, - }, - bottomTabBarContainer: { flexDirection: 'row', height: variables.bottomTabHeight, From a45f6f0ba20490a03bbc5c74d77097eb30a3cd1b Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Tue, 19 Mar 2024 13:37:32 +0530 Subject: [PATCH 016/259] remove error label styles --- .../AttachmentViewPdf/BaseAttachmentViewPdf.js | 2 -- .../Attachments/AttachmentView/AttachmentViewPdf/index.js | 3 +-- .../AttachmentView/AttachmentViewPdf/propTypes.js | 4 ---- src/components/Attachments/AttachmentView/index.js | 1 - src/components/PDFView/index.js | 1 - src/components/PDFView/pdfViewPropTypes.js | 8 +++++--- 6 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js index 3f3b995c02ba..8ae5a483c9fa 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js @@ -26,7 +26,6 @@ function BaseAttachmentViewPdf({ onScaleChanged: onScaleChangedProp, onToggleKeyboard, onLoadComplete, - errorLabelStyles, style, isUsedAsChatAttachment, onError, @@ -90,7 +89,6 @@ function BaseAttachmentViewPdf({ onToggleKeyboard={onToggleKeyboard} onScaleChanged={onScaleChanged} onLoadComplete={onLoadComplete} - errorLabelStyles={errorLabelStyles} isUsedAsChatAttachment={isUsedAsChatAttachment} onError={onError} /> diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js index f151bf84b7a8..1d5ffbccf40c 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style, onError, isUsedAsChatAttachment}) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, onError, isUsedAsChatAttachment}) { return ( diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js index bcb0fabde352..75984e6ac031 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/propTypes.js @@ -13,9 +13,6 @@ const attachmentViewPdfPropTypes = { /** Additional style props */ style: stylePropTypes, - /** Styles for the error label */ - errorLabelStyles: stylePropTypes, - /** Callback when the pdf fails to load */ onError: PropTypes.func, @@ -28,7 +25,6 @@ const attachmentViewPdfDefaultProps = { name: '', }, style: [], - errorLabelStyles: [], onError: () => {}, isUsedAsChatAttachment: false, }; diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 0d25de1bd799..701f6343eb25 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -192,7 +192,6 @@ function AttachmentView({ onPress={onPress} onToggleKeyboard={onToggleKeyboard} onLoadComplete={onPDFLoadComplete} - errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedInCarousel={isUsedInCarousel} isUsedAsChatAttachment={isUsedAsChatAttachment} diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index 2e39594fc210..838fd843b089 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -254,7 +254,6 @@ class PDFView extends Component { > {}, onLoadComplete: () => {}, isFocused: false, - errorLabelStyles: [], }; export {propTypes, defaultProps}; From cfd7a8b5de5c63f058364e9dfc6811c5def6401b Mon Sep 17 00:00:00 2001 From: Brandon Henry Date: Tue, 26 Mar 2024 21:10:53 -0500 Subject: [PATCH 017/259] added new styling for headermessage --- src/components/SelectionList/BaseSelectionList.tsx | 3 ++- src/components/SelectionList/types.ts | 3 +++ src/pages/SearchPage/index.tsx | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 015fd284c0b4..6e31f84b24ed 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -69,6 +69,7 @@ function BaseSelectionList( listHeaderWrapperStyle, isRowMultilineSupported = false, textInputRef, + headerMessageStyle = [styles.ph5, styles.pb5] }: BaseSelectionListProps, ref: ForwardedRef, ) { @@ -514,7 +515,7 @@ function BaseSelectionList( )} {!!headerMessage && ( - + {headerMessage} )} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 8b070e1aa5cb..9a2cbdd4c13c 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -229,6 +229,9 @@ type BaseSelectionListProps = Partial & { /** Message to display at the top of the list */ headerMessage?: string; + /** Styles to apply to the header message */ + headerMessageStyle?: StyleProp; + /** Text to display on the confirm button */ confirmButtonText?: string; diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index 07096ce6c2d5..f535bb014943 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -168,6 +168,7 @@ function SearchPage({betas, reports, isSearchingForReports, navigation}: SearchP textInputHint={offlineMessage} onChangeText={setSearchValue} headerMessage={headerMessage} + headerMessageStyle={headerMessage === translate('common.noResultsFound') ? [themeStyles.ph4, themeStyles.pb5] : undefined} onLayout={setPerformanceTimersEnd} autoFocus onSelectRow={selectReport} From 32f350390c152890ab3caa67a47fc2bae9833e8b Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 29 Mar 2024 02:03:33 +0530 Subject: [PATCH 018/259] fix conflicts --- .../Attachments/AttachmentView/AttachmentViewPdf/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts index f1f141a625db..da48480e8479 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts @@ -13,6 +13,10 @@ type AttachmentViewPdfProps = Pick void; + + onError?: () => void; + + isUsedAsChatAttachment?: boolean; }; export default AttachmentViewPdfProps; From 1c814c51a14085a59de008c08db62912a7fe9476 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 29 Mar 2024 02:09:57 +0530 Subject: [PATCH 019/259] fix lint --- .../Attachments/AttachmentView/AttachmentViewPdf/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index d38ffcf7abab..78265c3a9a6a 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import type AttachmentViewPdfProps from './types'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, onError, isUsedAsChatAttachment} : AttachmentViewPdfProps) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, onError, isUsedAsChatAttachment}: AttachmentViewPdfProps) { return ( Date: Fri, 29 Mar 2024 03:38:54 +0530 Subject: [PATCH 020/259] fallback to defaultview component --- .../AttachmentViewPdf/index.tsx | 13 +++- .../AttachmentView/AttachmentViewPdf/types.ts | 5 ++ .../DefaultAttachmentView/index.tsx | 58 ++++++++++++++++ .../Attachments/AttachmentView/index.tsx | 68 ++++++------------- src/components/PDFView/index.js | 3 +- 5 files changed, 96 insertions(+), 51 deletions(-) create mode 100644 src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index 78265c3a9a6a..10bcf28e7797 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx @@ -2,7 +2,17 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import type AttachmentViewPdfProps from './types'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, onError, isUsedAsChatAttachment}: AttachmentViewPdfProps) { +function AttachmentViewPdf({ + file, + encryptedSourceUrl, + isFocused, + onPress, + onToggleKeyboard, + onLoadComplete, + style, + renderFallbackAttachmentView, + isUsedAsChatAttachment, +}: AttachmentViewPdfProps) { return ( ); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts index da48480e8479..ecc6e5016446 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts @@ -14,9 +14,14 @@ type AttachmentViewPdfProps = Pick void; + /** Triggered when the PDF fails to load */ onError?: () => void; + /** Whether the PDF is used as a chat attachment */ isUsedAsChatAttachment?: boolean; + + /** Render a fallback view when the PDF fails to load */ + renderFallbackAttachmentView?: () => JSX.Element; }; export default AttachmentViewPdfProps; diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx new file mode 100644 index 000000000000..7360144bf09c --- /dev/null +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type DefaultAttachmentViewProps = { + fileName: string; + shouldShowDownloadIcon?: boolean; + shouldShowLoadingSpinnerIcon?: boolean; + containerStyles?: StyleProp; +}; + +function DefaultAttachmentView({fileName, shouldShowDownloadIcon, shouldShowLoadingSpinnerIcon, containerStyles}: DefaultAttachmentViewProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + + + + {fileName} + {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( + + + + + + )} + {shouldShowLoadingSpinnerIcon && ( + + + + + + )} + + ); +} + +export default DefaultAttachmentView; diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index e3b609852e5e..1621d8d9509d 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -1,18 +1,15 @@ import Str from 'expensify-common/lib/str'; -import React, {memo, useEffect, useState} from 'react'; -import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; -import {ActivityIndicator, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import type {Attachment, AttachmentSource} from '@components/Attachments/types'; +import React, { memo, useEffect, useState } from 'react'; +import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native'; +import { View } from 'react-native'; +import type { OnyxEntry } from 'react-native-onyx'; +import { withOnyx } from 'react-native-onyx'; +import type { Attachment, AttachmentSource } from '@components/Attachments/types'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; import ScrollView from '@components/ScrollView'; -import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; -import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; +import { usePlaybackContext } from '@components/VideoPlayerContexts/PlaybackContext'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -21,13 +18,14 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as TransactionUtils from '@libs/TransactionUtils'; -import type {ColorValue} from '@styles/utils/types'; +import type { ColorValue } from '@styles/utils/types'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Transaction} from '@src/types/onyx'; +import type { Transaction } from '@src/types/onyx'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; import AttachmentViewVideo from './AttachmentViewVideo'; +import DefaultAttachmentView from './DefaultAttachmentView'; type AttachmentViewOnyxProps = { transaction: OnyxEntry; @@ -95,8 +93,8 @@ function AttachmentView({ duration, isUsedAsChatAttachment, }: AttachmentViewProps) { - const {translate} = useLocalize(); - const {updateCurrentlyPlayingURL} = usePlaybackContext(); + const { translate } = useLocalize(); + const { updateCurrentlyPlayingURL } = usePlaybackContext(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -113,7 +111,7 @@ function AttachmentView({ const [imageError, setImageError] = useState(false); - useNetwork({onReconnect: () => setImageError(false)}); + useNetwork({ onReconnect: () => setImageError(false) }); // Handles case where source is a component (ex: SVG) or a number // Number may represent a SVG or an image @@ -239,36 +237,12 @@ function AttachmentView({ } return ( - - - - - - {file?.name} - {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( - - - - - - )} - {shouldShowLoadingSpinnerIcon && ( - - - - - - )} - + ); } @@ -277,9 +251,9 @@ AttachmentView.displayName = 'AttachmentView'; export default memo( withOnyx({ transaction: { - key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + key: ({ transactionID }) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, }, })(AttachmentView), ); -export type {AttachmentViewProps}; +export type { AttachmentViewProps }; diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index e69b52b74e95..bd4f9a584378 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -6,7 +6,6 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import Text from '@components/Text'; import withLocalize from '@components/withLocalize'; import withThemeStyles from '@components/withThemeStyles'; import withWindowDimensions from '@components/withWindowDimensions'; @@ -94,7 +93,7 @@ class PDFView extends Component { maxCanvasHeight={this.props.maxCanvasHeight} maxCanvasArea={this.props.maxCanvasArea} LoadingComponent={} - ErrorComponent={{this.props.translate('attachmentView.failedToLoadPDF')}} + ErrorComponent={this.props.renderFallbackAttachmentView} renderPasswordForm={({isPasswordInvalid, onSubmit, onPasswordChange}) => ( Date: Fri, 29 Mar 2024 04:38:22 +0530 Subject: [PATCH 021/259] fix prop pass --- .../Attachments/AttachmentView/AttachmentViewPdf/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index 10bcf28e7797..3afb02db5068 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx @@ -24,6 +24,7 @@ function AttachmentViewPdf({ onToggleKeyboard={onToggleKeyboard} onLoadComplete={onLoadComplete} isUsedAsChatAttachment={isUsedAsChatAttachment} + renderFallbackAttachmentView={renderFallbackAttachmentView} /> ); } From 7d5d22a60c27c70622f097ae0ebd92c0e8dcf03e Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 29 Mar 2024 05:01:20 +0530 Subject: [PATCH 022/259] prettier diffs --- .../Attachments/AttachmentView/index.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 1621d8d9509d..d08f10582c52 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -1,15 +1,15 @@ import Str from 'expensify-common/lib/str'; -import React, { memo, useEffect, useState } from 'react'; -import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native'; -import { View } from 'react-native'; -import type { OnyxEntry } from 'react-native-onyx'; -import { withOnyx } from 'react-native-onyx'; -import type { Attachment, AttachmentSource } from '@components/Attachments/types'; +import React, {memo, useEffect, useState} from 'react'; +import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; import ScrollView from '@components/ScrollView'; -import { usePlaybackContext } from '@components/VideoPlayerContexts/PlaybackContext'; +import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -18,10 +18,10 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as TransactionUtils from '@libs/TransactionUtils'; -import type { ColorValue } from '@styles/utils/types'; +import type {ColorValue} from '@styles/utils/types'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; -import type { Transaction } from '@src/types/onyx'; +import type {Transaction} from '@src/types/onyx'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; import AttachmentViewVideo from './AttachmentViewVideo'; @@ -93,8 +93,8 @@ function AttachmentView({ duration, isUsedAsChatAttachment, }: AttachmentViewProps) { - const { translate } = useLocalize(); - const { updateCurrentlyPlayingURL } = usePlaybackContext(); + const {translate} = useLocalize(); + const {updateCurrentlyPlayingURL} = usePlaybackContext(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -111,7 +111,7 @@ function AttachmentView({ const [imageError, setImageError] = useState(false); - useNetwork({ onReconnect: () => setImageError(false) }); + useNetwork({onReconnect: () => setImageError(false)}); // Handles case where source is a component (ex: SVG) or a number // Number may represent a SVG or an image @@ -251,9 +251,9 @@ AttachmentView.displayName = 'AttachmentView'; export default memo( withOnyx({ transaction: { - key: ({ transactionID }) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, }, })(AttachmentView), ); -export type { AttachmentViewProps }; +export type {AttachmentViewProps}; From 0af56c671213f1b2ade9f045259edcf27db8a7ef Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 30 Mar 2024 01:50:13 +0530 Subject: [PATCH 023/259] remove renderfallbackview prop --- .../AttachmentViewPdf/BaseAttachmentViewPdf.tsx | 4 ++-- .../AttachmentView/AttachmentViewPdf/index.tsx | 14 ++------------ .../AttachmentView/AttachmentViewPdf/types.ts | 9 +++------ .../AttachmentView/DefaultAttachmentView/index.tsx | 4 ++-- .../Attachments/AttachmentView/index.tsx | 6 +++--- src/components/PDFView/index.js | 9 ++++++++- src/components/PDFView/index.native.js | 4 ++-- src/components/PDFView/pdfViewPropTypes.js | 5 ++++- 8 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx index 9971c2d03820..eb22535d1d5f 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx @@ -15,7 +15,7 @@ function BaseAttachmentViewPdf({ onLoadComplete, style, isUsedAsChatAttachment, - onError, + onLoadError, }: AttachmentViewPdfProps) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const isScrollEnabled = attachmentCarouselPagerContext === null ? undefined : attachmentCarouselPagerContext.isScrollEnabled; @@ -78,7 +78,7 @@ function BaseAttachmentViewPdf({ onScaleChanged={onScaleChanged} onLoadComplete={onLoadComplete} isUsedAsChatAttachment={isUsedAsChatAttachment} - onError={onError} + onLoadError={onLoadError} /> ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index 3afb02db5068..c1ded9782a7a 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx @@ -2,17 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import type AttachmentViewPdfProps from './types'; -function AttachmentViewPdf({ - file, - encryptedSourceUrl, - isFocused, - onPress, - onToggleKeyboard, - onLoadComplete, - style, - renderFallbackAttachmentView, - isUsedAsChatAttachment, -}: AttachmentViewPdfProps) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, isUsedAsChatAttachment, containerStyles}: AttachmentViewPdfProps) { return ( ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts index ecc6e5016446..1c3cd8adfba4 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts @@ -8,20 +8,17 @@ type AttachmentViewPdfProps = Pick; - /** Styles for the error label */ - errorLabelStyles?: StyleProp; - /** Triggered when the PDF's onScaleChanged event is triggered */ onScaleChanged?: (scale: number) => void; /** Triggered when the PDF fails to load */ - onError?: () => void; + onLoadError?: () => void; /** Whether the PDF is used as a chat attachment */ isUsedAsChatAttachment?: boolean; - /** Render a fallback view when the PDF fails to load */ - renderFallbackAttachmentView?: () => JSX.Element; + /** Additional container styles */ + containerStyles?: StyleProp; }; export default AttachmentViewPdfProps; diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx index 7360144bf09c..23d39ec096b5 100644 --- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -10,13 +10,13 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; type DefaultAttachmentViewProps = { - fileName: string; + fileName?: string; shouldShowDownloadIcon?: boolean; shouldShowLoadingSpinnerIcon?: boolean; containerStyles?: StyleProp; }; -function DefaultAttachmentView({fileName, shouldShowDownloadIcon, shouldShowLoadingSpinnerIcon, containerStyles}: DefaultAttachmentViewProps) { +function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles}: DefaultAttachmentViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index d08f10582c52..5e9a1b2bf4f6 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -176,9 +176,9 @@ function AttachmentView({ onToggleKeyboard={onToggleKeyboard} onLoadComplete={onPDFLoadComplete} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} - isUsedInCarousel={isUsedInCarousel} isUsedAsChatAttachment={isUsedAsChatAttachment} - onError={() => { + containerStyles={containerStyles} + onLoadError={() => { setIsPdfFailedToLoad(true); }} /> @@ -238,7 +238,7 @@ function AttachmentView({ return ( } - ErrorComponent={this.props.renderFallbackAttachmentView} + ErrorComponent={ + + } renderPasswordForm={({isPasswordInvalid, onSubmit, onPasswordChange}) => ( Date: Sat, 30 Mar 2024 01:57:32 +0530 Subject: [PATCH 024/259] fix the comment for the prop --- src/components/Attachments/AttachmentView/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 5e9a1b2bf4f6..f32265bdc01f 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -68,7 +68,7 @@ type AttachmentViewProps = AttachmentViewOnyxProps & /* Whether it is hovered or not */ isHovered?: boolean; - /** Duration of the video */ + /** Whether the attachment is used as a chat attachment */ isUsedAsChatAttachment?: boolean; }; From 6436ddbef5f2928851b20ba4448489fbb2c74b8b Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 30 Mar 2024 02:32:19 +0530 Subject: [PATCH 025/259] fixes loading for pdf on web --- src/components/FullscreenLoadingIndicator.tsx | 5 +++-- src/components/PDFView/index.js | 9 ++++++++- src/components/PDFView/pdfViewPropTypes.js | 3 +++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index bd3082db5fa4..57c2d7dc955e 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -6,13 +6,14 @@ import useThemeStyles from '@hooks/useThemeStyles'; type FullScreenLoadingIndicatorProps = { style?: StyleProp; + isFullScreen?: boolean; }; -function FullScreenLoadingIndicator({style}: FullScreenLoadingIndicatorProps) { +function FullScreenLoadingIndicator({style, isFullScreen = true}: FullScreenLoadingIndicatorProps) { const theme = useTheme(); const styles = useThemeStyles(); return ( - + } + LoadingComponent={} ErrorComponent={ Date: Sat, 30 Mar 2024 02:40:55 +0530 Subject: [PATCH 026/259] fixes proptype warning --- src/components/PDFView/index.js | 30 +++++++++++++++++----- src/components/PDFView/pdfViewPropTypes.js | 3 --- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index 64904fc84b3e..d2ee3d448718 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -8,8 +8,8 @@ import DefaultAttachmentView from '@components/Attachments/AttachmentView/Defaul import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import withLocalize from '@components/withLocalize'; +import withStyleUtils, {withStyleUtilsPropTypes} from '@components/withStyleUtils'; import withThemeStyles from '@components/withThemeStyles'; -import withStyleUtils from '@components/withStyleUtils'; import withWindowDimensions from '@components/withWindowDimensions'; import compose from '@libs/compose'; import variables from '@styles/variables'; @@ -17,10 +17,16 @@ import * as CanvasSize from '@userActions/CanvasSize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import PDFPasswordForm from './PDFPasswordForm'; -import * as pdfViewPropTypes from './pdfViewPropTypes'; +import {propTypes as pdfViewPropTypes, defaultProps as pdfViewDefaultProps} from './pdfViewPropTypes'; const LOADING_THUMBNAIL_HEIGHT = 250; const LOADING_THUMBNAIL_WIDTH = 250; + +const PDFViewPropTypes = { + ...pdfViewPropTypes, + ...withStyleUtilsPropTypes, +}; + class PDFView extends Component { constructor(props) { super(props); @@ -83,8 +89,13 @@ class PDFView extends Component { const styles = this.props.themeStyles; const outerContainerStyle = [styles.w100, styles.h100, styles.justifyContentCenter, styles.alignItemsCenter]; const loadingIndicatorStyles = this.props.isUsedAsChatAttachment - ? [this.props.themeStyles.chatItemPDFAttachmentLoading, this.props.StyleUtils.getWidthAndHeightStyle(LOADING_THUMBNAIL_WIDTH, LOADING_THUMBNAIL_HEIGHT), styles.justifyContentCenter, styles.alignItemsCenter] - : []; + ? [ + this.props.themeStyles.chatItemPDFAttachmentLoading, + this.props.StyleUtils.getWidthAndHeightStyle(LOADING_THUMBNAIL_WIDTH, LOADING_THUMBNAIL_HEIGHT), + styles.justifyContentCenter, + styles.alignItemsCenter, + ] + : []; return ( } + LoadingComponent={ + + } ErrorComponent={ Date: Sat, 30 Mar 2024 02:47:58 +0530 Subject: [PATCH 027/259] prettier diffs --- src/components/PDFView/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PDFView/index.js b/src/components/PDFView/index.js index d2ee3d448718..5051e75266cb 100644 --- a/src/components/PDFView/index.js +++ b/src/components/PDFView/index.js @@ -17,7 +17,7 @@ import * as CanvasSize from '@userActions/CanvasSize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import PDFPasswordForm from './PDFPasswordForm'; -import {propTypes as pdfViewPropTypes, defaultProps as pdfViewDefaultProps} from './pdfViewPropTypes'; +import {defaultProps as pdfViewDefaultProps, propTypes as pdfViewPropTypes} from './pdfViewPropTypes'; const LOADING_THUMBNAIL_HEIGHT = 250; const LOADING_THUMBNAIL_WIDTH = 250; From 9bd4c96250b25ac591c8904fa9709510dbcfda50 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Tue, 2 Apr 2024 23:32:51 +0530 Subject: [PATCH 028/259] adds onError prop in react-fast-pdf --- package-lock.json | 246 +++++++----------- package.json | 4 +- .../AttachmentViewPdf/index.tsx | 3 +- .../DefaultAttachmentView/index.tsx | 6 + src/components/FullscreenLoadingIndicator.tsx | 5 +- src/components/PDFView/index.js | 48 ++-- 6 files changed, 130 insertions(+), 182 deletions(-) diff --git a/package-lock.json b/package-lock.json index 809b4bd5c4df..7fc027a16e9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -72,7 +72,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.6", + "react-fast-pdf": "https://github.com/ishpaul777/react-fast-pdf/tree/fix-blankArea-after-loading-pdf", "react-map-gl": "^7.1.3", "react-native": "0.73.2", "react-native-android-location-enabler": "^2.0.1", @@ -3098,8 +3098,10 @@ }, "node_modules/@expensify/react-native-live-markdown": { "version": "0.1.35", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.35.tgz", - "integrity": "sha512-W0FFIiU/sT+AwIrIOUHiNAHYjODAkEdYsf75tfBbkA6v2byHPxUlbzaJrZEQc0HgbvtAfTf9iQQqGWjNqe4pog==", + "license": "MIT", + "workspaces": [ + "example" + ], "engines": { "node": ">= 18.0.0" }, @@ -6402,8 +6404,6 @@ }, "node_modules/@jest/types": { "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "license": "MIT", "optional": true, @@ -6795,8 +6795,9 @@ "license": "BSD-3-Clause" }, "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.10", - "license": "BSD-3-Clause", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", "optional": true, "dependencies": { "detect-libc": "^2.0.0", @@ -6815,7 +6816,8 @@ }, "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "optional": true, "dependencies": { "semver": "^6.0.0" @@ -6829,26 +6831,13 @@ }, "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { "version": "6.3.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "optional": true, "bin": { "semver": "bin/semver.js" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { - "version": "5.0.0", - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@mapbox/point-geometry": { "version": "0.1.0", "license": "ISC" @@ -8187,8 +8176,7 @@ }, "node_modules/@react-native-community/cli-doctor/node_modules/ip": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", - "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" + "license": "MIT" }, "node_modules/@react-native-community/cli-doctor/node_modules/strip-ansi": { "version": "5.2.0", @@ -8271,8 +8259,7 @@ }, "node_modules/@react-native-community/cli-hermes/node_modules/ip": { "version": "1.1.9", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", - "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==" + "license": "MIT" }, "node_modules/@react-native-community/cli-hermes/node_modules/supports-color": { "version": "7.2.0", @@ -8445,8 +8432,7 @@ }, "node_modules/@react-native-community/cli-server-api/node_modules/@jest/types": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", @@ -8460,8 +8446,7 @@ }, "node_modules/@react-native-community/cli-server-api/node_modules/@types/yargs": { "version": "15.0.19", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", - "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -8488,8 +8473,7 @@ }, "node_modules/@react-native-community/cli-server-api/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -8517,8 +8501,7 @@ }, "node_modules/@react-native-community/cli-server-api/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -8542,8 +8525,7 @@ }, "node_modules/@react-native-community/cli-server-api/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -19039,8 +19021,6 @@ }, "node_modules/@types/yargs": { "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, "license": "MIT", "optional": true, @@ -19051,8 +19031,6 @@ }, "node_modules/@types/yargs-parser": { "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "license": "MIT" }, "node_modules/@types/yauzl": { @@ -19991,8 +19969,6 @@ }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "dev": true, "license": "BSD-2-Clause" }, @@ -20007,7 +19983,8 @@ }, "node_modules/abbrev": { "version": "1.1.1", - "license": "ISC", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "node_modules/abort-controller": { @@ -20934,8 +20911,6 @@ }, "node_modules/async": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "dev": true, "license": "MIT" }, @@ -22399,9 +22374,8 @@ }, "node_modules/bs-logger": { "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, + "license": "MIT", "dependencies": { "fast-json-stable-stringify": "2.x" }, @@ -22920,8 +22894,9 @@ }, "node_modules/canvas": { "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", "hasInstallScript": true, - "license": "MIT", "optional": true, "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", @@ -23334,8 +23309,7 @@ }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -23661,9 +23635,8 @@ }, "node_modules/concurrently": { "version": "8.2.2", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", - "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.2", "date-fns": "^2.30.0", @@ -23688,9 +23661,8 @@ }, "node_modules/concurrently/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -23703,9 +23675,8 @@ }, "node_modules/concurrently/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -23719,9 +23690,8 @@ }, "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -23731,9 +23701,8 @@ }, "node_modules/concurrently/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -23743,24 +23712,21 @@ }, "node_modules/concurrently/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/concurrently/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/concurrently/node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -25335,8 +25301,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.1", - "license": "Apache-2.0", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "optional": true, "engines": { "node": ">=8" @@ -25671,8 +25638,6 @@ }, "node_modules/duplexer": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true, "license": "MIT" }, @@ -27640,8 +27605,7 @@ }, "node_modules/expo-image": { "version": "1.11.0", - "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-1.11.0.tgz", - "integrity": "sha512-CfkSGWIGidxxqzErke16bCS9aefS04qvgjk+T9Nc34LAb3ysBAqCv5hoCnvylHOvi/7jTCC/ouLm5oLLqkDSRQ==", + "license": "MIT", "dependencies": { "@react-native/assets-registry": "~0.73.1" }, @@ -27651,16 +27615,14 @@ }, "node_modules/expo-image-loader": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-4.6.0.tgz", - "integrity": "sha512-RHQTDak7/KyhWUxikn2yNzXL7i2cs16cMp6gEAgkHOjVhoCJQoOJ0Ljrt4cKQ3IowxgCuOrAgSUzGkqs7omj8Q==", + "license": "MIT", "peerDependencies": { "expo": "*" } }, "node_modules/expo-image-manipulator": { "version": "11.8.0", - "resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-11.8.0.tgz", - "integrity": "sha512-ZWVrHnYmwJq6h7auk+ropsxcNi+LyZcPFKQc8oy+JA0SaJosfShvkCm7RADWAunHmfPCmjHrhwPGEu/rs7WG/A==", + "license": "MIT", "dependencies": { "expo-image-loader": "~4.6.0" }, @@ -29657,8 +29619,6 @@ }, "node_modules/html-entities": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", "funding": [ { "type": "github", @@ -29668,7 +29628,8 @@ "type": "patreon", "url": "https://patreon.com/mdevils" } - ] + ], + "license": "MIT" }, "node_modules/html-escaper": { "version": "2.0.2", @@ -30573,9 +30534,8 @@ }, "node_modules/ip": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ip-regex": { "version": "2.1.0", @@ -34355,8 +34315,6 @@ }, "node_modules/klaw-sync": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", - "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", "dev": true, "license": "MIT", "dependencies": { @@ -34676,9 +34634,8 @@ }, "node_modules/lodash.memoize": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -36710,6 +36667,21 @@ "url": "https://github.com/sponsors/antelle" } }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "license": "BSD-2-Clause", @@ -37624,8 +37596,6 @@ }, "node_modules/patch-package": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", - "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", "dev": true, "license": "MIT", "dependencies": { @@ -37655,8 +37625,6 @@ }, "node_modules/patch-package/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -37671,8 +37639,6 @@ }, "node_modules/patch-package/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -37688,8 +37654,6 @@ }, "node_modules/patch-package/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -37701,15 +37665,11 @@ }, "node_modules/patch-package/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/patch-package/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -37718,8 +37678,6 @@ }, "node_modules/patch-package/node_modules/open": { "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -37735,8 +37693,6 @@ }, "node_modules/patch-package/node_modules/rimraf": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "license": "ISC", "dependencies": { @@ -37748,8 +37704,6 @@ }, "node_modules/patch-package/node_modules/slash": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", "dev": true, "license": "MIT", "engines": { @@ -37758,8 +37712,6 @@ }, "node_modules/patch-package/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -37771,8 +37723,6 @@ }, "node_modules/patch-package/node_modules/tmp": { "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, "license": "MIT", "dependencies": { @@ -38956,8 +38906,7 @@ }, "node_modules/react-content-loader": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/react-content-loader/-/react-content-loader-7.0.0.tgz", - "integrity": "sha512-xaBwpO7eiJyEc4ndym+g6wcruV9W2y3DKqbw4U48QFBsv0IeAVZO+aCUb8GptlDLWM8n5zi2HcFSGlj5r+53Tg==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -39051,8 +39000,8 @@ }, "node_modules/react-fast-pdf": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.6.tgz", - "integrity": "sha512-CdAnBSZaLCGLSEuiqWLzzXhV9Wvdf1VRixaXCrb3NFrXyeltahF7PY+u7eU6ynrWZGmNI6g0cMLPv0DQhJEeew==", + "resolved": "git+ssh://git@github.com/ishpaul777/react-fast-pdf.git#0a8a073ad5f0d376c1827a2dacfafd197500bf3b", + "license": "MIT", "dependencies": { "react-pdf": "^7.7.0", "react-window": "^1.8.10" @@ -39109,14 +39058,6 @@ } } }, - "node_modules/react-fast-pdf/node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/react-freeze": { "version": "1.0.3", "license": "MIT", @@ -39407,6 +39348,7 @@ "node_modules/react-native-image-size": { "version": "1.1.3", "resolved": "git+ssh://git@github.com/Expensify/react-native-image-size.git#bf3ad41a61c4f6f80ed4d497599ef5247a2dd002", + "integrity": "sha512-uIZLaqqjSivO+iBGx3VpZRSn/cYy18ct6S1H35gK8n74eJFb/Ds6qUJ+jGw5PUt0KEzw+aXLgPq6gHDXT5Q29A==", "license": "MIT" }, "node_modules/react-native-key-command": { @@ -39836,8 +39778,7 @@ }, "node_modules/react-native/node_modules/@jest/types": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", @@ -39851,8 +39792,7 @@ }, "node_modules/react-native/node_modules/@types/yargs": { "version": "15.0.19", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", - "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -39879,8 +39819,7 @@ }, "node_modules/react-native/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -39920,8 +39859,7 @@ }, "node_modules/react-native/node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -39976,8 +39914,7 @@ }, "node_modules/react-native/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -40593,8 +40530,7 @@ }, "node_modules/react-window": { "version": "1.8.10", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", - "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.0.0", "memoize-one": ">=3.1.1 <6" @@ -41551,9 +41487,8 @@ }, "node_modules/rxjs": { "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } @@ -42048,6 +41983,8 @@ }, "node_modules/simple-concat": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "funding": [ { "type": "github", @@ -42062,12 +41999,12 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "optional": true }, "node_modules/simple-get": { "version": "3.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", "optional": true, "dependencies": { "decompress-response": "^4.2.0", @@ -42077,7 +42014,8 @@ }, "node_modules/simple-get/node_modules/decompress-response": { "version": "4.2.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", "optional": true, "dependencies": { "mimic-response": "^2.0.0" @@ -42088,7 +42026,8 @@ }, "node_modules/simple-get/node_modules/mimic-response": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", "optional": true, "engines": { "node": ">=8" @@ -42537,8 +42476,6 @@ }, "node_modules/spawn-command": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", - "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, "node_modules/spdx-correct": { @@ -44234,9 +44171,8 @@ }, "node_modules/ts-jest": { "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", - "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", "dev": true, + "license": "MIT", "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", @@ -44277,9 +44213,8 @@ }, "node_modules/ts-jest/node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -45340,6 +45275,14 @@ "version": "0.1.1", "license": "MIT" }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "license": "MIT", @@ -46625,8 +46568,7 @@ }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -46650,16 +46592,14 @@ }, "node_modules/yargs/node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs/node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", "engines": { "node": ">=12" } diff --git a/package.json b/package.json index 9f1fbc894e1f..3b770debe4f0 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.6", + "react-fast-pdf": "https://github.com/ishpaul777/react-fast-pdf/tree/fix-blankArea-after-loading-pdf", "react-map-gl": "^7.1.3", "react-native": "0.73.2", "react-native-android-location-enabler": "^2.0.1", @@ -248,8 +248,8 @@ "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^4.0.0", - "copy-webpack-plugin": "^10.1.0", "concurrently": "^8.2.2", + "copy-webpack-plugin": "^10.1.0", "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index c1ded9782a7a..500d14082be7 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import type AttachmentViewPdfProps from './types'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, isUsedAsChatAttachment, containerStyles}: AttachmentViewPdfProps) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, isUsedAsChatAttachment, containerStyles, onLoadError}: AttachmentViewPdfProps) { return ( ); } diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx index 23d39ec096b5..6130aeed866c 100644 --- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -10,9 +10,13 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; type DefaultAttachmentViewProps = { + /** The name of the file */ fileName?: string; + /** Should show the download icon */ shouldShowDownloadIcon?: boolean; + /** Should show the loading spinner icon */ shouldShowLoadingSpinnerIcon?: boolean; + /** Additional styles for the container */ containerStyles?: StyleProp; }; @@ -55,4 +59,6 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa ); } +DefaultAttachmentView.displayName = 'DefaultAttachmentView'; + export default DefaultAttachmentView; diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index 57c2d7dc955e..bd3082db5fa4 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -6,14 +6,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; type FullScreenLoadingIndicatorProps = { style?: StyleProp; - isFullScreen?: boolean; }; -function FullScreenLoadingIndicator({style, isFullScreen = true}: FullScreenLoadingIndicatorProps) { +function FullScreenLoadingIndicator({style}: FullScreenLoadingIndicatorProps) { const theme = useTheme(); const styles = useThemeStyles(); return ( - + + + + ); + } + + return ; + } + renderPDFView() { const styles = this.props.themeStyles; const outerContainerStyle = [styles.w100, styles.h100, styles.justifyContentCenter, styles.alignItemsCenter]; - const loadingIndicatorStyles = this.props.isUsedAsChatAttachment - ? [ - this.props.themeStyles.chatItemPDFAttachmentLoading, - this.props.StyleUtils.getWidthAndHeightStyle(LOADING_THUMBNAIL_WIDTH, LOADING_THUMBNAIL_HEIGHT), - styles.justifyContentCenter, - styles.alignItemsCenter, - ] - : []; return ( - } - ErrorComponent={ - - } + LoadingComponent={this.renderLoadingIndicator()} + shouldShowErrorComponent={false} + onLoadError={this.props.onLoadError} renderPasswordForm={({isPasswordInvalid, onSubmit, onPasswordChange}) => ( Date: Sun, 7 Apr 2024 01:51:43 +0530 Subject: [PATCH 029/259] resolve conflicts --- src/components/PDFView/index.tsx | 48 +++++++++++++++++++++++--------- src/components/PDFView/types.ts | 9 ++++-- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index ff28a8f88849..df6543290b96 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -1,30 +1,36 @@ import 'core-js/features/array/at'; // eslint-disable-next-line no-restricted-imports -import type {CSSProperties} from 'react'; -import React, {memo, useCallback, useEffect, useState} from 'react'; -import {PDFPreviewer} from 'react-fast-pdf'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import type { CSSProperties } from 'react'; +import React, { memo, useCallback, useEffect, useState } from 'react'; +import { PDFPreviewer } from 'react-fast-pdf'; +import { ActivityIndicator, View } from 'react-native'; +import { withOnyx } from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import useTheme from '@hooks/useTheme'; +import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; import variables from '@styles/variables'; import * as CanvasSize from '@userActions/CanvasSize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import PDFPasswordForm from './PDFPasswordForm'; -import type {PDFViewOnyxProps, PDFViewProps} from './types'; +import type { PDFViewOnyxProps, PDFViewProps } from './types'; -function PDFView({onToggleKeyboard, fileName, onPress, isFocused, sourceURL, errorLabelStyles, maxCanvasArea, maxCanvasHeight, maxCanvasWidth, style}: PDFViewProps) { +const LOADING_THUMBNAIL_HEIGHT = 250; +const LOADING_THUMBNAIL_WIDTH = 250; + +function PDFView({ onToggleKeyboard, fileName, onPress, isFocused, sourceURL, maxCanvasArea, maxCanvasHeight, maxCanvasWidth, style, isUsedAsChatAttachment, onLoadError }: PDFViewProps) { const [isKeyboardOpen, setIsKeyboardOpen] = useState(false); const styles = useThemeStyles(); - const {windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); + const { windowHeight, isSmallScreenWidth } = useWindowDimensions(); const prevWindowHeight = usePrevious(windowHeight); - const {translate} = useLocalize(); + const { translate } = useLocalize(); /** * On small screens notify parent that the keyboard has opened or closed. @@ -79,6 +85,21 @@ function PDFView({onToggleKeyboard, fileName, onPress, isFocused, sourceURL, err } }, [isKeyboardOpen, prevWindowHeight, toggleKeyboardOnSmallScreens, windowHeight]); + const renderLoadingIndicator = () => { + if (isUsedAsChatAttachment) { + return ( + + + + ); + } + + return ; + } + const renderPDFView = () => { const outerContainerStyle = [styles.w100, styles.h100, styles.justifyContentCenter, styles.alignItemsCenter]; @@ -95,9 +116,10 @@ function PDFView({onToggleKeyboard, fileName, onPress, isFocused, sourceURL, err maxCanvasWidth={maxCanvasWidth} maxCanvasHeight={maxCanvasHeight} maxCanvasArea={maxCanvasArea} - LoadingComponent={} - ErrorComponent={{translate('attachmentView.failedToLoadPDF')}} - renderPasswordForm={({isPasswordInvalid, onSubmit, onPasswordChange}) => ( + LoadingComponent={renderLoadingIndicator()} + shouldShowErrorComponent={false} + onLoadError={onLoadError} + renderPasswordForm={({ isPasswordInvalid, onSubmit, onPasswordChange }) => ( ; + /** Callback to call when the PDF fails to load */ + onLoadError?: () => void; + + /** Whether the PDF is used as a chat attachment */ + isUsedAsChatAttachment?: boolean; }; type PDFViewOnyxProps = { From 15eea966597d1465650cfe61dd3dec2face0d9f5 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sun, 7 Apr 2024 01:52:50 +0530 Subject: [PATCH 030/259] code formatting --- .../AttachmentViewPdf/index.tsx | 13 ++++++- src/components/PDFView/index.native.tsx | 5 +-- src/components/PDFView/index.tsx | 35 +++++++++++-------- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index 9984779557eb..ba108092ab8f 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx @@ -2,7 +2,18 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import type AttachmentViewPdfProps from './types'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, isUsedAsChatAttachment, containerStyles, onLoadError}: AttachmentViewPdfProps) { +function AttachmentViewPdf({ + file, + encryptedSourceUrl, + isFocused, + onPress, + onToggleKeyboard, + onLoadComplete, + style, + isUsedAsChatAttachment, + containerStyles, + onLoadError, +}: AttachmentViewPdfProps) { return ( { if (isUsedAsChatAttachment) { return ( - + ; - } + }; const renderPDFView = () => { const outerContainerStyle = [styles.w100, styles.h100, styles.justifyContentCenter, styles.alignItemsCenter]; @@ -119,7 +126,7 @@ function PDFView({ onToggleKeyboard, fileName, onPress, isFocused, sourceURL, ma LoadingComponent={renderLoadingIndicator()} shouldShowErrorComponent={false} onLoadError={onLoadError} - renderPasswordForm={({ isPasswordInvalid, onSubmit, onPasswordChange }) => ( + renderPasswordForm={({isPasswordInvalid, onSubmit, onPasswordChange}) => ( Date: Sun, 7 Apr 2024 02:16:30 +0530 Subject: [PATCH 031/259] remove unnecessary change --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79f0a58cc10d..5e00815f1e3e 100644 --- a/package.json +++ b/package.json @@ -248,8 +248,8 @@ "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^4.0.0", - "concurrently": "^8.2.2", "copy-webpack-plugin": "^10.1.0", + "concurrently": "^8.2.2", "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", From 69129794b08fe9d66175d41bb0c35d9083965562 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sun, 7 Apr 2024 02:21:23 +0530 Subject: [PATCH 032/259] fix type checks --- .../Attachments/AttachmentView/AttachmentViewPdf/index.tsx | 2 -- .../Attachments/AttachmentView/AttachmentViewPdf/types.ts | 3 --- src/components/Attachments/AttachmentView/index.tsx | 1 - 3 files changed, 6 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index ba108092ab8f..3cae1ee45f1b 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx @@ -11,7 +11,6 @@ function AttachmentViewPdf({ onLoadComplete, style, isUsedAsChatAttachment, - containerStyles, onLoadError, }: AttachmentViewPdfProps) { return ( @@ -24,7 +23,6 @@ function AttachmentViewPdf({ onToggleKeyboard={onToggleKeyboard} onLoadComplete={onLoadComplete} isUsedAsChatAttachment={isUsedAsChatAttachment} - containerStyles={containerStyles} onLoadError={onLoadError} /> ); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts index 1c3cd8adfba4..c20593449d58 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts @@ -16,9 +16,6 @@ type AttachmentViewPdfProps = Pick; }; export default AttachmentViewPdfProps; diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index f32265bdc01f..bc8f7476b4d5 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -177,7 +177,6 @@ function AttachmentView({ onLoadComplete={onPDFLoadComplete} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedAsChatAttachment={isUsedAsChatAttachment} - containerStyles={containerStyles} onLoadError={() => { setIsPdfFailedToLoad(true); }} From 917095c5f6f29feb6c5b9d1bc29d56acd2cc9c06 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sun, 7 Apr 2024 02:23:07 +0530 Subject: [PATCH 033/259] lint code --- src/components/Attachments/AttachmentView/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index bc8f7476b4d5..1743a1904ad3 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -163,6 +163,10 @@ function AttachmentView({ } }; + const onPDFLoadError = () => { + setIsPdfFailedToLoad(true); + } + // We need the following View component on android native // So that the event will propagate properly and // the Password protected preview will be shown for pdf attachement we are about to send. @@ -177,9 +181,7 @@ function AttachmentView({ onLoadComplete={onPDFLoadComplete} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} isUsedAsChatAttachment={isUsedAsChatAttachment} - onLoadError={() => { - setIsPdfFailedToLoad(true); - }} + onLoadError={onPDFLoadError} /> ); From c9681471de4445131308598d7c644f8ffd9c7b1d Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sun, 7 Apr 2024 02:24:22 +0530 Subject: [PATCH 034/259] prettier diffs --- .../AttachmentView/AttachmentViewPdf/index.tsx | 12 +----------- src/components/Attachments/AttachmentView/index.tsx | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index 3cae1ee45f1b..e8f6568f98c0 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx @@ -2,17 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import type AttachmentViewPdfProps from './types'; -function AttachmentViewPdf({ - file, - encryptedSourceUrl, - isFocused, - onPress, - onToggleKeyboard, - onLoadComplete, - style, - isUsedAsChatAttachment, - onLoadError, -}: AttachmentViewPdfProps) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, isUsedAsChatAttachment, onLoadError}: AttachmentViewPdfProps) { return ( { setIsPdfFailedToLoad(true); - } + }; // We need the following View component on android native // So that the event will propagate properly and From cadf1f1dee2058299d5ed4e792b75ebb7fbaac6d Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sun, 7 Apr 2024 02:32:19 +0530 Subject: [PATCH 035/259] resolve lint failure --- src/components/PDFView/index.native.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/PDFView/index.native.tsx b/src/components/PDFView/index.native.tsx index 0fb063f57e8b..db76c005b37a 100644 --- a/src/components/PDFView/index.native.tsx +++ b/src/components/PDFView/index.native.tsx @@ -127,9 +127,9 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused if (shouldRequestPassword) { pdfStyles.push(themeStyles.invisible); } - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const containerStyles = - isUsedAsChatAttachment || (shouldRequestPassword && isSmallScreenWidth) ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (isUsedAsChatAttachment || (shouldRequestPassword && isSmallScreenWidth)) ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; const loadingIndicatorStyles = isUsedAsChatAttachment ? [themeStyles.chatItemPDFAttachmentLoading, StyleUtils.getWidthAndHeightStyle(LOADING_THUMBNAIL_WIDTH, LOADING_THUMBNAIL_HEIGHT)] : []; From 54eb29517e53a484d21a7a9a5a37d2bfdf91f05c Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sun, 7 Apr 2024 02:39:32 +0530 Subject: [PATCH 036/259] prettier diffs --- src/components/PDFView/index.native.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PDFView/index.native.tsx b/src/components/PDFView/index.native.tsx index db76c005b37a..7d523ef13805 100644 --- a/src/components/PDFView/index.native.tsx +++ b/src/components/PDFView/index.native.tsx @@ -129,7 +129,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused } const containerStyles = // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (isUsedAsChatAttachment || (shouldRequestPassword && isSmallScreenWidth)) ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; + isUsedAsChatAttachment || (shouldRequestPassword && isSmallScreenWidth) ? [themeStyles.w100, themeStyles.flex1] : [themeStyles.alignItemsCenter, themeStyles.flex1]; const loadingIndicatorStyles = isUsedAsChatAttachment ? [themeStyles.chatItemPDFAttachmentLoading, StyleUtils.getWidthAndHeightStyle(LOADING_THUMBNAIL_WIDTH, LOADING_THUMBNAIL_HEIGHT)] : []; From 9c1527be8952d96dbc1d758b08717b346833eb8d Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 11 Apr 2024 22:34:06 +0530 Subject: [PATCH 037/259] remove unnecessary styles --- src/components/PDFView/index.tsx | 2 -- src/styles/index.ts | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index 2725e3c2a6aa..e63a4b5ac4c1 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -92,8 +92,6 @@ function PDFView({onToggleKeyboard, fileName, onPress, isFocused, sourceURL, max style={[ styles.chatItemPDFAttachmentLoading, StyleUtils.getWidthAndHeightStyle(LOADING_THUMBNAIL_WIDTH, LOADING_THUMBNAIL_HEIGHT), - styles.alignItemsCenter, - styles.justifyContentCenter, ]} > borderColor: theme.border, borderWidth: 1, borderRadius: variables.componentBorderRadiusNormal, - textAlign: 'center', - verticalAlign: 'middle', - opacity: 1, + ...flex.alignItemsCenter, + ...flex.justifyContentCenter, }, sidebarVisible: { From 68d480ddc07f37d0644c9bfe4d42f62abf195093 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 17 Apr 2024 14:31:32 +0200 Subject: [PATCH 038/259] Display rter violation in MoneyRequestHeader & ReportPreview --- src/components/MoneyRequestHeader.tsx | 66 ++++++++++++------- .../ReportActionItem/ReportPreview.tsx | 21 +++++- src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/TransactionUtils.ts | 24 ++++++- src/types/onyx/TransactionViolation.ts | 1 + 6 files changed, 88 insertions(+), 28 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 2f80cf3c6e59..a7511b44f049 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 {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -16,7 +16,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy, Report, ReportAction, ReportActions, Session, Transaction} from '@src/types/onyx'; +import type {Policy, Report, ReportAction, ReportActions, Session, Transaction, TransactionViolations} from '@src/types/onyx'; import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; @@ -37,6 +37,9 @@ type MoneyRequestHeaderOnyxProps = { /** All the data for the transaction */ transaction: OnyxEntry; + /** The violations of the transaction */ + transactionViolations: OnyxCollection; + /** All report actions */ // eslint-disable-next-line react/no-unused-prop-types parentReportActions: OnyxEntry; @@ -56,7 +59,7 @@ type MoneyRequestHeaderProps = MoneyRequestHeaderOnyxProps & { parentReportAction: OnyxEntry; }; -function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, shownHoldUseExplanation = false, policy}: MoneyRequestHeaderProps) { +function MoneyRequestHeader({session, parentReport, report, parentReportAction, transactionViolations, transaction, shownHoldUseExplanation = false, policy}: MoneyRequestHeaderProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); @@ -86,9 +89,6 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, setIsDeleteModalVisible(false); }, [parentReport?.reportID, parentReportAction, setIsDeleteModalVisible]); - const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); - const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction); - const isDeletedParentAction = ReportActionsUtils.isDeletedAction(parentReportAction); const canHoldOrUnholdRequest = !isSettled && !isApproved && !isDeletedParentAction; @@ -106,6 +106,21 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, } }; + const getPendingType = () => { + if (TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction)) { + return {pendingType: 'PENDING', pendingTitle: translate('iou.pending'), pendingDescription: translate('iou.transactionPendingText')}; + } + if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { + return {pendingType: 'SCANNING', pendingTitle: ReceiptScan, pendingDescription: translate('iou.receiptScanInProgressDescription')}; + } + if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '', transactionViolations))) { + return {pendingType: 'RTER', pendingTitle: Expensicons.Hourglass, pendingDescription: translate('iou.pendingMatchWithCreditCardDescription')}; + } + return {}; + }; + + const {pendingType, pendingTitle, pendingDescription} = getPendingType(); + useEffect(() => { if (canDeleteRequest) { return; @@ -170,7 +185,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, <> Navigation.goBack(undefined, false, true)} /> - {isPending && ( - - )} - {isScanning && ( + {pendingType && ( + typeof pendingTitle === 'string' ? ( + pendingTitle + ) : ( + + ) } - description={translate('iou.receiptScanInProgressDescription')} + description={pendingDescription} shouldShowBorderBottom /> )} @@ -230,7 +242,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, MoneyRequestHeader.displayName = 'MoneyRequestHeader'; -const MoneyRequestHeaderWithTransaction = withOnyx>({ +const MoneyRequestHeaderWithTransaction = withOnyx>({ transaction: { key: ({report, parentReportActions}) => { const parentReportAction = (report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : {}) as ReportAction & OriginalMessageIOU; @@ -241,9 +253,15 @@ const MoneyRequestHeaderWithTransaction = withOnyx, Omit>({ +export default withOnyx< + Omit, + Omit +>({ session: { key: ONYXKEYS.SESSION, }, diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 190343e48abd..98f4cae7dee4 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -26,6 +26,7 @@ import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -140,6 +141,8 @@ function ReportPreview({ const hasErrors = hasMissingSmartscanFields || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations)) || ReportUtils.hasActionsWithErrors(iouReportID); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); + const showRTERViolationMessage = + allTransactions.length === 1 && TransactionUtils.hasPendingUI(allTransactions[0], TransactionUtils.getTransactionViolations(allTransactions[0].transactionID, transactionViolations)); let formattedMerchant = numberOfRequests === 1 ? TransactionUtils.getMerchant(allTransactions[0]) : null; const formattedDescription = numberOfRequests === 1 ? TransactionUtils.getDescription(allTransactions[0]) : null; @@ -148,7 +151,7 @@ function ReportPreview({ formattedMerchant = null; } - const shouldShowSubmitButton = isOpenExpenseReport && reimbursableSpend !== 0; + const shouldShowSubmitButton = isOpenExpenseReport && reimbursableSpend !== 0 && !showRTERViolationMessage; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport); // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on @@ -235,6 +238,9 @@ function ReportPreview({ const parsedSubtitle = new ExpensiMark().replace(formattedDescription ?? moneyRequestComment); return {isSupportTextHtml: !!parsedSubtitle, supportText: parsedSubtitle ? `${parsedSubtitle}` : ''}; } + // if (true) { + // return {isSupportTextHtml: false, supportText: 'ABC'}; + // } return { isSupportTextHtml: false, supportText: translate('iou.requestCount', { @@ -318,6 +324,19 @@ function ReportPreview({ )} + {showRTERViolationMessage && ( + + + + {translate('iou.pendingMatchWithCreditCard')} + + + )} {shouldShowSettlementButton && ( diff --git a/src/languages/en.ts b/src/languages/en.ts index 314f4f990a49..5667b3fc83e7 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -633,6 +633,8 @@ export default { canceled: 'Canceled', posted: 'Posted', deleteReceipt: 'Delete receipt', + pendingMatchWithCreditCard: 'Receipt pending match with credit card.', + pendingMatchWithCreditCardDescription: 'Receipt pending match with credit card. Marks as cash to ignore and request payment.', routePending: 'Route pending...', receiptScanning: 'Receipt scanning…', receiptScanInProgress: 'Receipt scan in progress.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 387f2b4c1841..4366d61425aa 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -629,6 +629,8 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', + pendingMatchWithCreditCard: 'Recibo pendiente de conciliar con la tarjeta de crédito.', + pendingMatchWithCreditCardDescription: 'Recibo pendiente de conciliar con tarjeta de crédito. Marcar como efectivo para ignorar y solicitar pago.', routePending: 'Ruta pendiente...', receiptScanning: 'Escaneo en curso…', receiptScanInProgress: 'Escaneo en curso…', diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 89b89ff7b584..a54fa66191a4 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -5,7 +5,7 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {RecentWaypoint, Report, TaxRate, TaxRates, TaxRatesWithDefault, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {RecentWaypoint, Report, TaxRate, TaxRates, TaxRatesWithDefault, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {IOURequestType} from './actions/IOU'; @@ -514,6 +514,22 @@ function hasMissingSmartscanFields(transaction: OnyxEntry): boolean return Boolean(transaction && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction)); } +/** + * Check if there is pending rter violation in transactionViolations. + */ +function hasPendingRTERViolation(transactionViolations?: TransactionViolations | null): boolean { + return Boolean( + transactionViolations?.some((transactionViolation: TransactionViolation) => transactionViolation.name === CONST.VIOLATIONS.RTER && transactionViolation.data?.pendingPattern), + ); +} + +/** + * Check if the transaction is pending or has a pending rter violation. + */ +function hasPendingUI(transaction: OnyxEntry, transactionViolations?: TransactionViolations | null): boolean { + return Boolean(isReceiptBeingScanned(transaction) || isPending(transaction) || (transaction && hasPendingRTERViolation(transactionViolations))); +} + /** * Check if the transaction has a defined route */ @@ -605,13 +621,13 @@ function isOnHold(transaction: OnyxEntry): boolean { /** * Checks if any violations for the provided transaction are of type 'violation' */ -function hasViolation(transactionID: string, transactionViolations: OnyxCollection): boolean { +function hasViolation(transactionID: string, transactionViolations: OnyxCollection): boolean { return Boolean( transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION), ); } -function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection): TransactionViolation[] | null { +function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection | null): TransactionViolations | null { return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; } @@ -698,6 +714,8 @@ export { isCreatedMissing, areRequiredFieldsEmpty, hasMissingSmartscanFields, + hasPendingRTERViolation, + hasPendingUI, getWaypointIndex, waypointHasValidAddress, getRecentTransactions, diff --git a/src/types/onyx/TransactionViolation.ts b/src/types/onyx/TransactionViolation.ts index 28de4582bd5e..ab2037ff336a 100644 --- a/src/types/onyx/TransactionViolation.ts +++ b/src/types/onyx/TransactionViolation.ts @@ -29,6 +29,7 @@ type TransactionViolation = { tagListIndex?: number; tagListName?: string; errorIndexes?: number[]; + pendingPattern?: boolean; }; }; From 3409d9cfcfb71aefbfa985ac0834f7b4115ba88b Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 17 Apr 2024 16:36:57 +0200 Subject: [PATCH 039/259] Display rter violations in MoneyReportHeader --- src/components/MoneyReportHeader.tsx | 29 +++++++++++++++++++++++++--- src/libs/TransactionUtils.ts | 24 ++++++++++++++++++----- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 14227d6a2051..b68cbd464137 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -10,6 +11,8 @@ 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 variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -20,8 +23,10 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Button from './Button'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; +import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; +import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import SettlementButton from './SettlementButton'; @@ -56,6 +61,7 @@ type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & { function MoneyReportHeader({session, policy, chatReport, nextStep, report: moneyRequestReport, transactionThreadReport, reportActions}: MoneyReportHeaderProps) { const styles = useThemeStyles(); + const theme = useTheme(); const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false); const {translate} = useLocalize(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); @@ -83,6 +89,9 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); + const transactionIDs = TransactionUtils.getAllReportTransactions(chatReport?.reportID).map((transaction) => transaction.transactionID); + const haveAllPendingRTERViolation = TransactionUtils.haveAllPendingRTERViolation(transactionIDs); + const cancelPayment = useCallback(() => { if (!chatReport) { return; @@ -97,9 +106,9 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); - const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !haveAllPendingRTERViolation; - const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; + const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !haveAllPendingRTERViolation; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length; @@ -188,7 +197,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money shouldShowBackButton={isSmallScreenWidth} onBackButtonPress={() => Navigation.goBack(undefined, false, true)} // Shows border if no buttons or next steps are showing below the header - shouldShowBorderBottom={!(shouldShowAnyButton && isSmallScreenWidth) && !(shouldShowNextStep && !isSmallScreenWidth)} + shouldShowBorderBottom={!(shouldShowAnyButton && isSmallScreenWidth) && !(shouldShowNextStep && !isSmallScreenWidth) && !haveAllPendingRTERViolation} shouldShowThreeDotsButton threeDotsMenuItems={threeDotsMenuItems} threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} @@ -226,6 +235,20 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money )} + {haveAllPendingRTERViolation && ( + + } + description={translate('iou.pendingMatchWithCreditCardDescription')} + shouldShowBorderBottom + /> + )} {shouldShowSettlementButton && isSmallScreenWidth && ( diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index a54fa66191a4..df21a992a047 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -16,7 +16,6 @@ import * as NumberUtils from './NumberUtils'; import {getCleanedTagName} from './PolicyUtils'; let allTransactions: OnyxCollection = {}; - Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, waitForCollectionCallback: true, @@ -28,6 +27,13 @@ Onyx.connect({ }, }); +let allTransactionViolations: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + waitForCollectionCallback: true, + callback: (value) => (allTransactionViolations = value), +}); + let allReports: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, @@ -514,6 +520,10 @@ function hasMissingSmartscanFields(transaction: OnyxEntry): boolean return Boolean(transaction && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction)); } +function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection | null): TransactionViolations | null { + return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; +} + /** * Check if there is pending rter violation in transactionViolations. */ @@ -523,6 +533,13 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | ); } +/** + * Check if there is pending rter violation in transactionViolations. + */ +function haveAllPendingRTERViolation(transactionIds: string[]): boolean { + return transactionIds.map((transactionId) => hasPendingRTERViolation(getTransactionViolations(transactionId, allTransactionViolations))).every((value) => value); +} + /** * Check if the transaction is pending or has a pending rter violation. */ @@ -627,10 +644,6 @@ function hasViolation(transactionID: string, transactionViolations: OnyxCollecti ); } -function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection | null): TransactionViolations | null { - return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; -} - /** * this is the formulae to calculate tax */ @@ -715,6 +728,7 @@ export { areRequiredFieldsEmpty, hasMissingSmartscanFields, hasPendingRTERViolation, + haveAllPendingRTERViolation, hasPendingUI, getWaypointIndex, waypointHasValidAddress, From 5275089ec8167f03667de8e53ecef56532a492ee Mon Sep 17 00:00:00 2001 From: smelaa Date: Thu, 18 Apr 2024 14:33:15 +0200 Subject: [PATCH 040/259] Fix util function --- src/libs/TransactionUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 44ccd387e670..3f374d46a6de 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -535,7 +535,8 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | * Check if there is pending rter violation in transactionViolations. */ function haveAllPendingRTERViolation(transactionIds: string[]): boolean { - return transactionIds.map((transactionId) => hasPendingRTERViolation(getTransactionViolations(transactionId, allTransactionViolations))).every((value) => value); + const transactionsWithRTERViolations = transactionIds.map((transactionId) => hasPendingRTERViolation(getTransactionViolations(transactionId, allTransactionViolations))); + return transactionsWithRTERViolations.length !== 0 && transactionsWithRTERViolations.every((value) => value === true); } /** From 046dbc2d40d1c88603b2f6eb2c408b42e0a0b283 Mon Sep 17 00:00:00 2001 From: smelaa Date: Thu, 18 Apr 2024 16:14:20 +0200 Subject: [PATCH 041/259] Add rter violation information to MoneyRequestPreviewContent header --- .../MoneyRequestPreviewContent.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 9a7549b9aac6..a3a8bfd131c3 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -4,6 +4,7 @@ import lodashSortBy from 'lodash/sortBy'; import React from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; +import type {SvgProps} from 'react-native-svg'; import ConfirmedRoute from '@components/ConfirmedRoute'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -134,7 +135,11 @@ function MoneyRequestPreviewContent({ showContextMenuForReport(event, contextMenuAnchor, reportID, action, checkIfContextMenuActive); }; - const getPreviewHeaderText = (): string => { + const getPreviewHeaderTextAndIcon: () => {headerMessage: string; headerIcon?: React.FC} = () => { + if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '', transactionViolations))) { + return {headerMessage: translate('iou.pendingMatchWithCreditCard'), headerIcon: Expensicons.Hourglass}; + } + let message = translate('iou.cash'); if (isDistanceRequest) { @@ -149,13 +154,13 @@ function MoneyRequestPreviewContent({ message = translate('iou.card'); if (TransactionUtils.isPending(transaction)) { message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.pending')}`; - return message; + return {headerMessage: message}; } } if (isSettled && !iouReport?.isCancelledIOU && !isPartialHold) { message += ` ${CONST.DOT_SEPARATOR} ${getSettledMessage()}`; - return message; + return {headerMessage: message}; } if (shouldShowRBR && transaction) { @@ -166,7 +171,8 @@ function MoneyRequestPreviewContent({ const isTooLong = violationsCount > 1 || violationMessage.length > 15; const hasViolationsAndFieldErrors = violationsCount > 0 && hasFieldErrors; - return `${message} ${CONST.DOT_SEPARATOR} ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`; + message = `${message} ${CONST.DOT_SEPARATOR} ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`; + return {headerMessage: message, headerIcon: undefined}; } const isMerchantMissing = TransactionUtils.isMerchantMissing(transaction); @@ -189,9 +195,11 @@ function MoneyRequestPreviewContent({ } else if (!(isSettled && !isSettlementOrApprovalPartial) && isOnHold) { message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.hold')}`; } - return message; + return {headerMessage: message}; }; + const {headerMessage, headerIcon} = getPreviewHeaderTextAndIcon(); + const getDisplayAmountText = (): string => { if (isScanning) { return translate('iou.receiptScanning'); @@ -249,7 +257,15 @@ function MoneyRequestPreviewContent({ - {getPreviewHeaderText()} + {headerIcon && ( + + )} + {headerMessage} {!isSettled && shouldShowRBR && ( Date: Sat, 20 Apr 2024 15:23:25 +0200 Subject: [PATCH 042/259] Check and remove unnecessary @ts-expect-error suppressions --- __mocks__/@react-navigation/native/index.ts | 3 +- src/Expensify.tsx | 2 +- .../BaseAnchorForAttachmentsOnly.tsx | 3 +- src/hooks/useAutoFocusInput.ts | 1 - src/libs/ObjectUtils.ts | 5 +- src/libs/migrations/NVPMigration.ts | 20 +- .../EnablePayments/AdditionalDetailsStep.tsx | 3 +- .../DateOfBirthUBO.tsx | 3 +- .../substeps/IncorporationDateBusiness.tsx | 3 +- .../PersonalInfo/substeps/DateOfBirth.tsx | 3 +- .../ReportActionCompose.tsx | 10 +- src/pages/home/report/ReportFooter.tsx | 7 +- .../PersonalDetails/StateSelectionPage.tsx | 5 +- tests/actions/IOUTest.ts | 1884 ++++++++--------- tests/actions/PolicyTest.ts | 8 +- tests/actions/ReportTest.ts | 6 - tests/perf-test/ReportScreen.perf-test.tsx | 9 +- tests/perf-test/SearchPage.perf-test.tsx | 11 +- tests/perf-test/SignInPage.perf-test.tsx | 2 +- tests/ui/UnreadIndicatorsTest.tsx | 1 - tests/unit/APITest.ts | 83 +- tests/unit/EmojiTest.ts | 1 - tests/unit/MiddlewareTest.ts | 1 - tests/unit/NetworkTest.ts | 1 - tests/unit/ReportActionsUtilsTest.ts | 4 +- tests/unit/RequestTest.ts | 1 - tests/unit/awaitStagingDeploysTest.ts | 1 - tests/utils/TestHelper.ts | 3 +- 28 files changed, 1001 insertions(+), 1083 deletions(-) diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index 9a6680ba0b6e..86772991ddac 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -3,8 +3,7 @@ import {useIsFocused as realUseIsFocused, useTheme as realUseTheme} from '@react // We only want these mocked for storybook, not jest const useIsFocused: typeof realUseIsFocused = process.env.NODE_ENV === 'test' ? realUseIsFocused : () => true; -// @ts-expect-error as we're mocking this function -const useTheme: typeof realUseTheme = process.env.NODE_ENV === 'test' ? realUseTheme : () => ({}); +const useTheme = process.env.NODE_ENV === 'test' ? realUseTheme : () => ({}); export * from '@react-navigation/core'; export * from '@react-navigation/native'; diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 6a57d6fdcc10..4920205d731d 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -74,7 +74,7 @@ type ExpensifyOnyxProps = { type ExpensifyProps = ExpensifyOnyxProps; -const SplashScreenHiddenContext = React.createContext({}); +const SplashScreenHiddenContext = React.createContext<{isSplashHidden?: boolean}>({}); function Expensify({ isCheckingPublicRoom = true, diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index da8e3694a7d2..3ce6ef4b36a9 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -52,8 +52,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow }} onPressIn={onPressIn} onPressOut={onPressOut} - // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24980) is migrated to TypeScript. - onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} shouldUseHapticsOnLongPress accessibilityLabel={displayName} role={CONST.ROLE.BUTTON} diff --git a/src/hooks/useAutoFocusInput.ts b/src/hooks/useAutoFocusInput.ts index e8e7c1187a63..c1f58901cee0 100644 --- a/src/hooks/useAutoFocusInput.ts +++ b/src/hooks/useAutoFocusInput.ts @@ -13,7 +13,6 @@ export default function useAutoFocusInput(): UseAutoFocusInput { const [isInputInitialized, setIsInputInitialized] = useState(false); const [isScreenTransitionEnded, setIsScreenTransitionEnded] = useState(false); - // @ts-expect-error TODO: Remove this when Expensify.js is migrated. const {isSplashHidden} = useContext(Expensify.SplashScreenHiddenContext); const inputRef = useRef(null); diff --git a/src/libs/ObjectUtils.ts b/src/libs/ObjectUtils.ts index 9ffa461506c8..fd4c0956e877 100644 --- a/src/libs/ObjectUtils.ts +++ b/src/libs/ObjectUtils.ts @@ -4,8 +4,9 @@ const shallowCompare = (obj1?: object, obj2?: object) => { return true; } if (obj1 && obj2) { - // @ts-expect-error we know that obj1 and obj2 are params of a route. - return Object.keys(obj1).length === Object.keys(obj2).length && Object.keys(obj1).every((key) => obj1[key] === obj2[key]); + const keys1 = Object.keys(obj1) as Array; + const keys2 = Object.keys(obj2) as Array; + return keys1.length === keys2.length && keys1.every((key) => obj1[key] === obj2[key]); } return false; }; diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index 9ab774328f78..fc5bce22641f 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -1,6 +1,9 @@ import after from 'lodash/after'; import Onyx from 'react-native-onyx'; +import type {KeyValueMapping, OnyxEntry} from 'react-native-onyx'; +import type {Account} from 'src/types/onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxKey} from '@src/ONYXKEYS'; // These are the oldKeyName: newKeyName of the NVPs we can migrate without any processing const migrations = { @@ -27,35 +30,30 @@ export default function () { for (const [oldKey, newKey] of Object.entries(migrations)) { const connectionID = Onyx.connect({ - // @ts-expect-error oldKey is a variable - key: oldKey, + key: oldKey as OnyxKey, callback: (value) => { Onyx.disconnect(connectionID); if (value === null) { resolveWhenDone(); return; } - // @ts-expect-error These keys are variables, so we can't check the type Onyx.multiSet({ [newKey]: value, [oldKey]: null, - }).then(resolveWhenDone); + } as KeyValueMapping).then(resolveWhenDone); }, }); } const connectionIDAccount = Onyx.connect({ key: ONYXKEYS.ACCOUNT, - callback: (value) => { + callback: (value: OnyxEntry) => { Onyx.disconnect(connectionIDAccount); - // @ts-expect-error we are removing this property, so it is not in the type anymore if (!value?.activePolicyID) { resolveWhenDone(); return; } - // @ts-expect-error we are removing this property, so it is not in the type anymore const activePolicyID = value.activePolicyID; const newValue = {...value}; - // @ts-expect-error we are removing this property, so it is not in the type anymore delete newValue.activePolicyID; Onyx.multiSet({ [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: activePolicyID, @@ -72,14 +70,12 @@ export default function () { resolveWhenDone(); return; } - const newValue = {}; + const newValue = {} as Record; for (const key of Object.keys(value)) { - // @ts-expect-error We have no fixed types here newValue[`nvp_${key}`] = value[key]; - // @ts-expect-error We have no fixed types here newValue[key] = null; } - Onyx.multiSet(newValue).then(resolveWhenDone); + Onyx.multiSet(newValue as KeyValueMapping).then(resolveWhenDone); }, }); }); diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.tsx b/src/pages/EnablePayments/AdditionalDetailsStep.tsx index 46e7191f222b..8dd15915df2e 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.tsx +++ b/src/pages/EnablePayments/AdditionalDetailsStep.tsx @@ -206,8 +206,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO placeholder={translate('common.phoneNumberPlaceholder')} shouldSaveDraft /> - {/* @ts-expect-error TODO: Remove this once DatePicker (https://github.com/Expensify/App/issues/25148) is migrated to TypeScript. */} - + {translate('beneficialOwnerInfoStep.enterTheDateOfBirthOfTheOwner')} - {/* @ts-expect-error TODO: Remove this once DatePicker (https://github.com/Expensify/App/issues/25148) is migrated to TypeScript. */} - + {translate('businessInfoStep.selectYourCompanysIncorporationDate')} - {/* @ts-expect-error TODO: Remove this once DatePicker (https://github.com/Expensify/App/issues/25148) is migrated to TypeScript. */} - + {translate('personalInfoStep.enterYourDateOfBirth')} - {/* @ts-expect-error TODO: Remove this once DatePicker (https://github.com/Expensify/App/issues/25148) is migrated to TypeScript. */} - + void; focus: (shouldDelay?: boolean) => void; - replaceSelectionWithText: (text: string, shouldAddTrailSpace: boolean) => void; + replaceSelectionWithText: (text: string, shouldAddTrailSpace: Emoji) => void; prepareCommentAndResetComposer: () => string; isFocused: () => boolean; }; @@ -73,7 +73,7 @@ type ReportActionComposeOnyxProps = { type ReportActionComposeProps = ReportActionComposeOnyxProps & WithCurrentUserPersonalDetailsProps & - Pick & { + Pick & { /** A method to call when the form is submitted */ onSubmit: (newComment: string | undefined) => void; @@ -91,6 +91,9 @@ type ReportActionComposeProps = ReportActionComposeOnyxProps & /** A method to call when the input is blur */ onComposerBlur?: () => void; + + /** Should the input be disabled */ + disabled?: boolean; }; // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will @@ -102,7 +105,7 @@ const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); function ReportActionCompose({ blockedFromConcierge, currentUserPersonalDetails = {}, - disabled, + disabled = false, isComposerFullSize = false, onSubmit, pendingAction, @@ -479,7 +482,6 @@ function ReportActionCompose({ composerRef.current?.replaceSelectionWithText(...args)} emojiPickerID={report?.reportID} shiftVertical={emojiShiftVertical} diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 04fbd0308390..9f9b45c18b96 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -117,7 +117,11 @@ function ReportFooter({ ); const onSubmitComment = useCallback( - (text: string) => { + (text: string | undefined) => { + if (text === undefined) { + return; + } + const isTaskCreated = handleCreateTask(text); if (isTaskCreated) { return; @@ -146,7 +150,6 @@ function ReportFooter({ { const backTo = params?.backTo ?? ''; - let backToRoute = ''; + let backToRoute = '' as Route; if (backTo) { - backToRoute = appendParam(backTo, 'state', currentState ?? ''); + backToRoute = appendParam(backTo, 'state', currentState ?? '') as Route; } - // @ts-expect-error Navigation.goBack does take a param Navigation.goBack(backToRoute); }} /> diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index aa0af7e5f037..d2c108a4260a 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -24,6 +24,7 @@ import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; +import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForNetworkPromises from '../utils/waitForNetworkPromises'; @@ -51,9 +52,10 @@ describe('actions/IOU', () => { }); }); + let mockFetch: MockFetch; beforeEach(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); + mockFetch = fetch as MockFetch; return Onyx.clear().then(waitForBatchedUpdates); }); @@ -68,179 +70,175 @@ describe('actions/IOU', () => { let transactionID: string | undefined; let transactionThread: OnyxEntry; let transactionThreadCreatedAction: OnyxEntry; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch.pause?.(); IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); - return ( - waitForBatchedUpdates() - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connectionID); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); - // A chat report, a transaction thread, and an iou report should be created - const chatReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.CHAT); - const iouReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.IOU); - expect(Object.keys(chatReports).length).toBe(2); - expect(Object.keys(iouReports).length).toBe(1); - const chatReport = chatReports[0]; - const transactionThreadReport = chatReports[1]; - const iouReport = iouReports[0]; - iouReportID = iouReport?.reportID; - transactionThread = transactionThreadReport; + // A chat report, a transaction thread, and an iou report should be created + const chatReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.CHAT); + const iouReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.IOU); + expect(Object.keys(chatReports).length).toBe(2); + expect(Object.keys(iouReports).length).toBe(1); + const chatReport = chatReports[0]; + const transactionThreadReport = chatReports[1]; + const iouReport = iouReports[0]; + iouReportID = iouReport?.reportID; + transactionThread = transactionThreadReport; - expect(iouReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + expect(iouReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); - // They should be linked together - expect(chatReport?.participantAccountIDs).toEqual([CARLOS_ACCOUNT_ID]); - expect(chatReport?.iouReportID).toBe(iouReport?.reportID); + // They should be linked together + expect(chatReport?.participantAccountIDs).toEqual([CARLOS_ACCOUNT_ID]); + expect(chatReport?.iouReportID).toBe(iouReport?.reportID); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForIOUReport) => { - Onyx.disconnect(connectionID); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); - // The IOU report should have a CREATED action and IOU action - expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); - const createdActions = Object.values(reportActionsForIOUReport ?? {}).filter( - (reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, - ); - const iouActions = Object.values(reportActionsForIOUReport ?? {}).filter( - (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, - ); - expect(Object.values(createdActions).length).toBe(1); - expect(Object.values(iouActions).length).toBe(1); - createdAction = createdActions?.[0] ?? null; - iouAction = iouActions?.[0] ?? null; + // The IOU report should have a CREATED action and IOU action + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); + const createdActions = Object.values(reportActionsForIOUReport ?? {}).filter( + (reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ); + const iouActions = Object.values(reportActionsForIOUReport ?? {}).filter( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ); + expect(Object.values(createdActions).length).toBe(1); + expect(Object.values(iouActions).length).toBe(1); + createdAction = createdActions?.[0] ?? null; + iouAction = iouActions?.[0] ?? null; - // The CREATED action should not be created after the IOU action - expect(Date.parse(createdAction?.created ?? '')).toBeLessThan(Date.parse(iouAction?.created ?? '')); + // The CREATED action should not be created after the IOU action + expect(Date.parse(createdAction?.created ?? '')).toBeLessThan(Date.parse(iouAction?.created ?? '')); - // The IOUReportID should be correct - expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); + // The IOUReportID should be correct + expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); - // The comment should be included in the IOU action - expect(iouAction.originalMessage.comment).toBe(comment); + // The comment should be included in the IOU action + expect(iouAction.originalMessage.comment).toBe(comment); - // The amount in the IOU action should be correct - expect(iouAction.originalMessage.amount).toBe(amount); + // The amount in the IOU action should be correct + expect(iouAction.originalMessage.amount).toBe(amount); - // The IOU type should be correct - expect(iouAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + // The IOU type should be correct + expect(iouAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - // Both actions should be pending - expect(createdAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(iouAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + // Both actions should be pending + expect(createdAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(iouAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForTransactionThread) => { - Onyx.disconnect(connectionID); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForTransactionThread) => { + Onyx.disconnect(connectionID); - // The transaction thread should have a CREATED action - expect(Object.values(reportActionsForTransactionThread ?? {}).length).toBe(1); - const createdActions = Object.values(reportActionsForTransactionThread ?? {}).filter( - (reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, - ); - expect(Object.values(createdActions).length).toBe(1); - transactionThreadCreatedAction = createdActions[0]; + // The transaction thread should have a CREATED action + expect(Object.values(reportActionsForTransactionThread ?? {}).length).toBe(1); + const createdActions = Object.values(reportActionsForTransactionThread ?? {}).filter( + (reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ); + expect(Object.values(createdActions).length).toBe(1); + transactionThreadCreatedAction = createdActions[0]; - expect(transactionThreadCreatedAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connectionID); + expect(transactionThreadCreatedAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); - // There should be one transaction - expect(Object.values(allTransactions ?? {}).length).toBe(1); - const transaction = Object.values(allTransactions ?? []).find((t) => !isEmptyObject(t)); - transactionID = transaction?.transactionID; + // There should be one transaction + expect(Object.values(allTransactions ?? {}).length).toBe(1); + const transaction = Object.values(allTransactions ?? []).find((t) => !isEmptyObject(t)); + transactionID = transaction?.transactionID; - // The transaction should be attached to the IOU report - expect(transaction?.reportID).toBe(iouReportID); + // The transaction should be attached to the IOU report + expect(transaction?.reportID).toBe(iouReportID); - // Its amount should match the amount of the expense - expect(transaction?.amount).toBe(amount); + // Its amount should match the amount of the expense + expect(transaction?.amount).toBe(amount); - // The comment should be correct - expect(transaction?.comment.comment).toBe(comment); + // The comment should be correct + expect(transaction?.comment.comment).toBe(comment); - // It should be pending - expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + // It should be pending + expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - // The transactionID on the iou action should match the one from the transactions collection - expect((iouAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(transactionID); + // The transactionID on the iou action should match the one from the transactions collection + expect((iouAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(transactionID); - expect(transaction?.merchant).toBe(merchant); + expect(transaction?.merchant).toBe(merchant); - resolve(); - }, - }); - }), - ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - .then(fetch.resume) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForIOUReport) => { - Onyx.disconnect(connectionID); - expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); - Object.values(reportActionsForIOUReport ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - waitForCollectionCallback: false, - callback: (transaction) => { - Onyx.disconnect(connectionID); - expect(transaction?.pendingAction).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + resolve(); + }, + }); + }), + ) + .then(mockFetch.resume) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); + Object.values(reportActionsForIOUReport ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + waitForCollectionCallback: false, + callback: (transaction) => { + Onyx.disconnect(connectionID); + expect(transaction?.pendingAction).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('updates existing chat report if there is one', () => { @@ -260,152 +258,147 @@ describe('actions/IOU', () => { let iouAction: OnyxEntry; let iouCreatedAction: OnyxEntry; let transactionID: string | undefined; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); - return ( - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport) - .then(() => - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, { - [createdAction.reportActionID]: createdAction, - }), - ) - .then(() => { - IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connectionID); + mockFetch.pause?.(); + return Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport) + .then(() => + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, { + [createdAction.reportActionID]: createdAction, + }), + ) + .then(() => { + IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); - // The same chat report should be reused, a transaction thread and an IOU report should be created - expect(Object.values(allReports ?? {}).length).toBe(3); - expect(Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT)?.reportID).toBe(chatReport.reportID); - chatReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT) ?? chatReport; - const iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); - iouReportID = iouReport?.reportID; + // The same chat report should be reused, a transaction thread and an IOU report should be created + expect(Object.values(allReports ?? {}).length).toBe(3); + expect(Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT)?.reportID).toBe(chatReport.reportID); + chatReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT) ?? chatReport; + const iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); + iouReportID = iouReport?.reportID; - expect(iouReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + expect(iouReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); - // They should be linked together - expect(chatReport.iouReportID).toBe(iouReportID); + // They should be linked together + expect(chatReport.iouReportID).toBe(iouReportID); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, - waitForCollectionCallback: false, - callback: (allIOUReportActions) => { - Onyx.disconnect(connectionID); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (allIOUReportActions) => { + Onyx.disconnect(connectionID); - iouCreatedAction = - Object.values(allIOUReportActions ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) ?? null; - iouAction = - Object.values(allIOUReportActions ?? {}).find( - (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, - ) ?? null; + iouCreatedAction = Object.values(allIOUReportActions ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) ?? null; + iouAction = + Object.values(allIOUReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ) ?? null; - // The CREATED action should not be created after the IOU action - expect(Date.parse(iouCreatedAction?.created ?? '')).toBeLessThan(Date.parse(iouAction?.created ?? '')); + // The CREATED action should not be created after the IOU action + expect(Date.parse(iouCreatedAction?.created ?? '')).toBeLessThan(Date.parse(iouAction?.created ?? '')); - // The IOUReportID should be correct - expect(iouAction?.originalMessage?.IOUReportID).toBe(iouReportID); + // The IOUReportID should be correct + expect(iouAction?.originalMessage?.IOUReportID).toBe(iouReportID); - // The comment should be included in the IOU action - expect(iouAction?.originalMessage?.comment).toBe(comment); + // The comment should be included in the IOU action + expect(iouAction?.originalMessage?.comment).toBe(comment); - // The amount in the IOU action should be correct - expect(iouAction?.originalMessage?.amount).toBe(amount); + // The amount in the IOU action should be correct + expect(iouAction?.originalMessage?.amount).toBe(amount); - // The IOU action type should be correct - expect(iouAction?.originalMessage?.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + // The IOU action type should be correct + expect(iouAction?.originalMessage?.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - // The IOU action should be pending - expect(iouAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + // The IOU action should be pending + expect(iouAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connectionID); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); - // There should be one transaction - expect(Object.values(allTransactions ?? {}).length).toBe(1); - const transaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); - transactionID = transaction?.transactionID; + // There should be one transaction + expect(Object.values(allTransactions ?? {}).length).toBe(1); + const transaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); + transactionID = transaction?.transactionID; - // The transaction should be attached to the IOU report - expect(transaction?.reportID).toBe(iouReportID); + // The transaction should be attached to the IOU report + expect(transaction?.reportID).toBe(iouReportID); - // Its amount should match the amount of the expense - expect(transaction?.amount).toBe(amount); + // Its amount should match the amount of the expense + expect(transaction?.amount).toBe(amount); - // The comment should be correct - expect(transaction?.comment.comment).toBe(comment); + // The comment should be correct + expect(transaction?.comment.comment).toBe(comment); - expect(transaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); + expect(transaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); - // It should be pending - expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + // It should be pending + expect(transaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - // The transactionID on the iou action should match the one from the transactions collection - expect((iouAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(transactionID); + // The transactionID on the iou action should match the one from the transactions collection + expect((iouAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(transactionID); - resolve(); - }, - }); - }), - ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - .then(fetch.resume) - .then(waitForBatchedUpdates) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForIOUReport) => { - Onyx.disconnect(connectionID); - expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); - Object.values(reportActionsForIOUReport ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - callback: (transaction) => { - Onyx.disconnect(connectionID); - expect(transaction?.pendingAction).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + resolve(); + }, + }); + }), + ) + .then(mockFetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(2); + Object.values(reportActionsForIOUReport ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + callback: (transaction) => { + Onyx.disconnect(connectionID); + expect(transaction?.pendingAction).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('updates existing IOU report if there is one', () => { @@ -460,146 +453,142 @@ describe('actions/IOU', () => { }; let newIOUAction: OnyxEntry; let newTransaction: OnyxEntry; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); - return ( - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, chatReport) - .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, iouReport)) - .then(() => - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, { - [createdAction.reportActionID]: createdAction, - [iouAction.reportActionID]: iouAction, - }), - ) - .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${existingTransaction.transactionID}`, existingTransaction)) - .then(() => { - if (chatReport) { - IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); - } - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connectionID); + mockFetch.pause?.(); + return Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, chatReport) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, iouReport)) + .then(() => + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, { + [createdAction.reportActionID]: createdAction, + [iouAction.reportActionID]: iouAction, + }), + ) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${existingTransaction.transactionID}`, existingTransaction)) + .then(() => { + if (chatReport) { + IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); - // No new reports should be created - expect(Object.values(allReports ?? {}).length).toBe(3); - expect(Object.values(allReports ?? {}).find((report) => report?.reportID === chatReportID)).toBeTruthy(); - expect(Object.values(allReports ?? {}).find((report) => report?.reportID === iouReportID)).toBeTruthy(); + // No new reports should be created + expect(Object.values(allReports ?? {}).length).toBe(3); + expect(Object.values(allReports ?? {}).find((report) => report?.reportID === chatReportID)).toBeTruthy(); + expect(Object.values(allReports ?? {}).find((report) => report?.reportID === iouReportID)).toBeTruthy(); - chatReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT) ?? null; - iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU) ?? null; + chatReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT) ?? null; + iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU) ?? null; - // The total on the iou report should be updated - expect(iouReport?.total).toBe(11000); + // The total on the iou report should be updated + expect(iouReport?.total).toBe(11000); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForIOUReport) => { - Onyx.disconnect(connectionID); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); - expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); - newIOUAction = - Object.values(reportActionsForIOUReport ?? {}).find( - (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => - reportAction?.reportActionID !== createdAction.reportActionID && reportAction?.reportActionID !== iouAction?.reportActionID, - ) ?? null; + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); + newIOUAction = + Object.values(reportActionsForIOUReport ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => + reportAction?.reportActionID !== createdAction.reportActionID && reportAction?.reportActionID !== iouAction?.reportActionID, + ) ?? null; - // The IOUReportID should be correct - expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); + // The IOUReportID should be correct + expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); - // The comment should be included in the IOU action - expect(newIOUAction?.originalMessage.comment).toBe(comment); + // The comment should be included in the IOU action + expect(newIOUAction?.originalMessage.comment).toBe(comment); - // The amount in the IOU action should be correct - expect(newIOUAction?.originalMessage.amount).toBe(amount); + // The amount in the IOU action should be correct + expect(newIOUAction?.originalMessage.amount).toBe(amount); - // The type of the IOU action should be correct - expect(newIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + // The type of the IOU action should be correct + expect(newIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - // The IOU action should be pending - expect(newIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + // The IOU action should be pending + expect(newIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connectionID); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); - // There should be two transactions - expect(Object.values(allTransactions ?? {}).length).toBe(2); + // There should be two transactions + expect(Object.values(allTransactions ?? {}).length).toBe(2); - newTransaction = Object.values(allTransactions ?? {}).find((transaction) => transaction?.transactionID !== existingTransaction.transactionID) ?? null; + newTransaction = Object.values(allTransactions ?? {}).find((transaction) => transaction?.transactionID !== existingTransaction.transactionID) ?? null; - expect(newTransaction?.reportID).toBe(iouReportID); - expect(newTransaction?.amount).toBe(amount); - expect(newTransaction?.comment.comment).toBe(comment); - expect(newTransaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); - expect(newTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(newTransaction?.reportID).toBe(iouReportID); + expect(newTransaction?.amount).toBe(amount); + expect(newTransaction?.comment.comment).toBe(comment); + expect(newTransaction?.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); + expect(newTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - // The transactionID on the iou action should match the one from the transactions collection - expect((newIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(newTransaction?.transactionID); + // The transactionID on the iou action should match the one from the transactions collection + expect((newIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(newTransaction?.transactionID); - resolve(); - }, - }); - }), - ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - .then(fetch.resume) - .then(waitForNetworkPromises) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForIOUReport) => { - Onyx.disconnect(connectionID); - expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); - Object.values(reportActionsForIOUReport ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connectionID); - Object.values(allTransactions ?? {}).forEach((transaction) => expect(transaction?.pendingAction).toBeFalsy()); - resolve(); - }, - }); - }), - ) - ); + resolve(); + }, + }); + }), + ) + .then(mockFetch.resume) + .then(waitForNetworkPromises) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connectionID); + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); + Object.values(reportActionsForIOUReport ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + Object.values(allTransactions ?? {}).forEach((transaction) => expect(transaction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ); }); it('correctly implements RedBrickRoad error handling', () => { @@ -612,8 +601,7 @@ describe('actions/IOU', () => { let transactionID: string; let transactionThreadReport: OnyxEntry; let transactionThreadAction: OnyxEntry; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch.pause?.(); IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); return ( waitForBatchedUpdates() @@ -724,10 +712,8 @@ describe('actions/IOU', () => { }), ) .then((): Promise => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.fail(); - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - return fetch.resume() as Promise; + mockFetch.fail?.(); + return mockFetch.resume?.() as Promise; }) .then( () => @@ -915,8 +901,8 @@ describe('actions/IOU', () => { ) // Cleanup - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - .then(fetch.succeed) + + .then(mockFetch.succeed) ); }); }); @@ -1054,349 +1040,344 @@ describe('actions/IOU', () => { (item) => item[julesChatCreatedAction.reportActionID].reportID ?? '', ); - return ( - Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, { - ...reportCollectionDataSet, + return Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, { + ...reportCollectionDataSet, + }) + .then(() => + Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { + ...carlosActionsCollectionDataSet, + ...julesCreatedActionsCollectionDataSet, + ...julesActionsCollectionDataSet, + }), + ) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${julesExistingTransaction?.transactionID}`, julesExistingTransaction)) + .then(() => { + // When we split a bill offline + mockFetch.pause?.(); + IOU.splitBill( + // TODO: Migrate after the backend accepts accountIDs + { + participants: [ + [CARLOS_EMAIL, String(CARLOS_ACCOUNT_ID)], + [JULES_EMAIL, String(JULES_ACCOUNT_ID)], + [VIT_EMAIL, String(VIT_ACCOUNT_ID)], + ].map(([email, accountID]) => ({login: email, accountID: Number(accountID)})), + currentUserLogin: RORY_EMAIL, + currentUserAccountID: RORY_ACCOUNT_ID, + amount, + comment, + currency: CONST.CURRENCY.USD, + merchant, + created: '', + tag: '', + existingSplitChatReportID: '', + }, + ); + return waitForBatchedUpdates(); }) - .then(() => - Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { - ...carlosActionsCollectionDataSet, - ...julesCreatedActionsCollectionDataSet, - ...julesActionsCollectionDataSet, - }), - ) - .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${julesExistingTransaction?.transactionID}`, julesExistingTransaction)) - .then(() => { - // When we split a bill offline - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); - IOU.splitBill( - // TODO: Migrate after the backend accepts accountIDs - { - participants: [ - [CARLOS_EMAIL, String(CARLOS_ACCOUNT_ID)], - [JULES_EMAIL, String(JULES_ACCOUNT_ID)], - [VIT_EMAIL, String(VIT_ACCOUNT_ID)], - ].map(([email, accountID]) => ({login: email, accountID: Number(accountID)})), - currentUserLogin: RORY_EMAIL, - currentUserAccountID: RORY_ACCOUNT_ID, - amount, - comment, - currency: CONST.CURRENCY.USD, - merchant, - created: '', - tag: '', - existingSplitChatReportID: '', - }, - ); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connectionID); - - // There should now be 10 reports - expect(Object.values(allReports ?? {}).length).toBe(10); - - // 1. The chat report with Rory + Carlos - carlosChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === carlosChatReport?.reportID) ?? null; - expect(isEmptyObject(carlosChatReport)).toBe(false); - expect(carlosChatReport?.pendingFields).toBeFalsy(); - - // 2. The IOU report with Rory + Carlos (new) - carlosIOUReport = - Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU && report.managerID === CARLOS_ACCOUNT_ID) ?? null; - expect(isEmptyObject(carlosIOUReport)).toBe(false); - expect(carlosIOUReport?.total).toBe(amount / 4); - - // 3. The chat report with Rory + Jules - julesChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === julesChatReport?.reportID) ?? null; - expect(isEmptyObject(julesChatReport)).toBe(false); - expect(julesChatReport?.pendingFields).toBeFalsy(); - - // 4. The IOU report with Rory + Jules - julesIOUReport = Object.values(allReports ?? {}).find((report) => report?.reportID === julesIOUReport?.reportID) ?? null; - expect(isEmptyObject(julesIOUReport)).toBe(false); - expect(julesChatReport?.pendingFields).toBeFalsy(); - expect(julesIOUReport?.total).toBe((julesExistingTransaction?.amount ?? 0) + amount / 4); - - // 5. The chat report with Rory + Vit (new) - vitChatReport = - Object.values(allReports ?? {}).find( - (report) => report?.type === CONST.REPORT.TYPE.CHAT && isEqual(report.participantAccountIDs, [VIT_ACCOUNT_ID]), - ) ?? null; - expect(isEmptyObject(vitChatReport)).toBe(false); - expect(vitChatReport?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); - - // 6. The IOU report with Rory + Vit (new) - vitIOUReport = - Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU && report.managerID === VIT_ACCOUNT_ID) ?? null; - expect(isEmptyObject(vitIOUReport)).toBe(false); - expect(vitIOUReport?.total).toBe(amount / 4); - - // 7. The group chat with everyone - groupChat = - Object.values(allReports ?? {}).find( - (report) => - report?.type === CONST.REPORT.TYPE.CHAT && - isEqual(report.participantAccountIDs, [CARLOS_ACCOUNT_ID, JULES_ACCOUNT_ID, VIT_ACCOUNT_ID, RORY_ACCOUNT_ID]), - ) ?? null; - expect(isEmptyObject(groupChat)).toBe(false); - expect(groupChat?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); - - // The 1:1 chat reports and the IOU reports should be linked together - expect(carlosChatReport?.iouReportID).toBe(carlosIOUReport?.reportID); - expect(carlosIOUReport?.chatReportID).toBe(carlosChatReport?.reportID); - expect(carlosIOUReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); - - expect(julesChatReport?.iouReportID).toBe(julesIOUReport?.reportID); - expect(julesIOUReport?.chatReportID).toBe(julesChatReport?.reportID); - - expect(vitChatReport?.iouReportID).toBe(vitIOUReport?.reportID); - expect(vitIOUReport?.chatReportID).toBe(vitChatReport?.reportID); - expect(carlosIOUReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); - - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); - - // There should be reportActions on all 7 chat reports + 3 IOU reports in each 1:1 chat - expect(Object.values(allReportActions ?? {}).length).toBe(10); - - const carlosReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${carlosChatReport?.iouReportID}`]; - const julesReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${julesChatReport?.iouReportID}`]; - const vitReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${vitChatReport?.iouReportID}`]; - const groupReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${groupChat?.reportID}`]; - - // Carlos DM should have two reportActions – the existing CREATED action and a pending IOU action - expect(Object.values(carlosReportActions ?? {}).length).toBe(2); - carlosIOUCreatedAction = - Object.values(carlosReportActions ?? {}).find( - (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, - ) ?? null; - carlosIOUAction = - Object.values(carlosReportActions ?? {}).find( - (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, - ) ?? null; - expect(carlosIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(carlosIOUAction?.originalMessage.IOUReportID).toBe(carlosIOUReport?.reportID); - expect(carlosIOUAction?.originalMessage.amount).toBe(amount / 4); - expect(carlosIOUAction?.originalMessage.comment).toBe(comment); - expect(carlosIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - expect(Date.parse(carlosIOUCreatedAction?.created ?? '')).toBeLessThan(Date.parse(carlosIOUAction?.created ?? '')); - - // Jules DM should have three reportActions, the existing CREATED action, the existing IOU action, and a new pending IOU action - expect(Object.values(julesReportActions ?? {}).length).toBe(3); - expect(julesReportActions?.[julesCreatedAction.reportActionID]).toStrictEqual(julesCreatedAction); - julesIOUCreatedAction = - Object.values(julesReportActions ?? {}).find( - (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, - ) ?? null; - julesIOUAction = - Object.values(julesReportActions ?? {}).find( - (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => - reportAction.reportActionID !== julesCreatedAction.reportActionID && - reportAction.reportActionID !== julesExistingIOUAction.reportActionID, - ) ?? null; - expect(julesIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(julesIOUAction?.originalMessage.IOUReportID).toBe(julesIOUReport?.reportID); - expect(julesIOUAction?.originalMessage.amount).toBe(amount / 4); - expect(julesIOUAction?.originalMessage.comment).toBe(comment); - expect(julesIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - expect(Date.parse(julesIOUCreatedAction?.created ?? '')).toBeLessThan(Date.parse(julesIOUAction?.created ?? '')); - - // Vit DM should have two reportActions – a pending CREATED action and a pending IOU action - expect(Object.values(vitReportActions ?? {}).length).toBe(2); - vitCreatedAction = - Object.values(vitReportActions ?? {}).find( - (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, - ) ?? null; - vitIOUAction = - Object.values(vitReportActions ?? {}).find( - (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, - ) ?? null; - expect(vitCreatedAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(vitIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(vitIOUAction?.originalMessage.IOUReportID).toBe(vitIOUReport?.reportID); - expect(vitIOUAction?.originalMessage.amount).toBe(amount / 4); - expect(vitIOUAction?.originalMessage.comment).toBe(comment); - expect(vitIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - expect(Date.parse(vitCreatedAction?.created ?? '')).toBeLessThan(Date.parse(vitIOUAction?.created ?? '')); - - // Group chat should have two reportActions – a pending CREATED action and a pending IOU action w/ type SPLIT - expect(Object.values(groupReportActions ?? {}).length).toBe(2); - groupCreatedAction = - Object.values(groupReportActions ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) ?? null; - groupIOUAction = - Object.values(groupReportActions ?? {}).find( - (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, - ) ?? null; - expect(groupCreatedAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(groupIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(groupIOUAction?.originalMessage).not.toHaveProperty('IOUReportID'); - expect(groupIOUAction?.originalMessage?.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.SPLIT); - expect(Date.parse(groupCreatedAction?.created ?? '')).toBeLessThanOrEqual(Date.parse(groupIOUAction?.created ?? '')); - - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connectionID); - - /* There should be 5 transactions - * – one existing one with Jules - * - one for each of the three IOU reports - * - one on the group chat w/ deleted report - */ - expect(Object.values(allTransactions ?? {}).length).toBe(5); - expect(allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${julesExistingTransaction?.transactionID}`]).toBeTruthy(); - - carlosTransaction = - Object.values(allTransactions ?? {}).find( - (transaction) => transaction?.transactionID === (carlosIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID, - ) ?? null; - julesTransaction = - Object.values(allTransactions ?? {}).find( - (transaction) => transaction?.transactionID === (julesIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID, - ) ?? null; - vitTransaction = - Object.values(allTransactions ?? {}).find( - (transaction) => transaction?.transactionID === (vitIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID, - ) ?? null; - groupTransaction = Object.values(allTransactions ?? {}).find((transaction) => transaction?.reportID === CONST.REPORT.SPLIT_REPORTID) ?? null; - - expect(carlosTransaction?.reportID).toBe(carlosIOUReport?.reportID); - expect(julesTransaction?.reportID).toBe(julesIOUReport?.reportID); - expect(vitTransaction?.reportID).toBe(vitIOUReport?.reportID); - expect(groupTransaction).toBeTruthy(); - - expect(carlosTransaction?.amount).toBe(amount / 4); - expect(julesTransaction?.amount).toBe(amount / 4); - expect(vitTransaction?.amount).toBe(amount / 4); - expect(groupTransaction?.amount).toBe(amount); + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); - expect(carlosTransaction?.comment.comment).toBe(comment); - expect(julesTransaction?.comment.comment).toBe(comment); - expect(vitTransaction?.comment.comment).toBe(comment); - expect(groupTransaction?.comment.comment).toBe(comment); + // There should now be 10 reports + expect(Object.values(allReports ?? {}).length).toBe(10); + + // 1. The chat report with Rory + Carlos + carlosChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === carlosChatReport?.reportID) ?? null; + expect(isEmptyObject(carlosChatReport)).toBe(false); + expect(carlosChatReport?.pendingFields).toBeFalsy(); + + // 2. The IOU report with Rory + Carlos (new) + carlosIOUReport = + Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU && report.managerID === CARLOS_ACCOUNT_ID) ?? null; + expect(isEmptyObject(carlosIOUReport)).toBe(false); + expect(carlosIOUReport?.total).toBe(amount / 4); + + // 3. The chat report with Rory + Jules + julesChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === julesChatReport?.reportID) ?? null; + expect(isEmptyObject(julesChatReport)).toBe(false); + expect(julesChatReport?.pendingFields).toBeFalsy(); + + // 4. The IOU report with Rory + Jules + julesIOUReport = Object.values(allReports ?? {}).find((report) => report?.reportID === julesIOUReport?.reportID) ?? null; + expect(isEmptyObject(julesIOUReport)).toBe(false); + expect(julesChatReport?.pendingFields).toBeFalsy(); + expect(julesIOUReport?.total).toBe((julesExistingTransaction?.amount ?? 0) + amount / 4); + + // 5. The chat report with Rory + Vit (new) + vitChatReport = + Object.values(allReports ?? {}).find( + (report) => report?.type === CONST.REPORT.TYPE.CHAT && isEqual(report.participantAccountIDs, [VIT_ACCOUNT_ID]), + ) ?? null; + expect(isEmptyObject(vitChatReport)).toBe(false); + expect(vitChatReport?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + + // 6. The IOU report with Rory + Vit (new) + vitIOUReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU && report.managerID === VIT_ACCOUNT_ID) ?? null; + expect(isEmptyObject(vitIOUReport)).toBe(false); + expect(vitIOUReport?.total).toBe(amount / 4); + + // 7. The group chat with everyone + groupChat = + Object.values(allReports ?? {}).find( + (report) => + report?.type === CONST.REPORT.TYPE.CHAT && + isEqual(report.participantAccountIDs, [CARLOS_ACCOUNT_ID, JULES_ACCOUNT_ID, VIT_ACCOUNT_ID, RORY_ACCOUNT_ID]), + ) ?? null; + expect(isEmptyObject(groupChat)).toBe(false); + expect(groupChat?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + + // The 1:1 chat reports and the IOU reports should be linked together + expect(carlosChatReport?.iouReportID).toBe(carlosIOUReport?.reportID); + expect(carlosIOUReport?.chatReportID).toBe(carlosChatReport?.reportID); + expect(carlosIOUReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + + expect(julesChatReport?.iouReportID).toBe(julesIOUReport?.reportID); + expect(julesIOUReport?.chatReportID).toBe(julesChatReport?.reportID); + + expect(vitChatReport?.iouReportID).toBe(vitIOUReport?.reportID); + expect(vitIOUReport?.chatReportID).toBe(vitChatReport?.reportID); + expect(carlosIOUReport?.notificationPreference).toBe(CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); - expect(carlosTransaction?.merchant).toBe(merchant); - expect(julesTransaction?.merchant).toBe(merchant); - expect(vitTransaction?.merchant).toBe(merchant); - expect(groupTransaction?.merchant).toBe(merchant); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); - expect(carlosTransaction?.comment.source).toBe(CONST.IOU.TYPE.SPLIT); - expect(julesTransaction?.comment.source).toBe(CONST.IOU.TYPE.SPLIT); - expect(vitTransaction?.comment.source).toBe(CONST.IOU.TYPE.SPLIT); + // There should be reportActions on all 7 chat reports + 3 IOU reports in each 1:1 chat + expect(Object.values(allReportActions ?? {}).length).toBe(10); + + const carlosReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${carlosChatReport?.iouReportID}`]; + const julesReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${julesChatReport?.iouReportID}`]; + const vitReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${vitChatReport?.iouReportID}`]; + const groupReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${groupChat?.reportID}`]; + + // Carlos DM should have two reportActions – the existing CREATED action and a pending IOU action + expect(Object.values(carlosReportActions ?? {}).length).toBe(2); + carlosIOUCreatedAction = + Object.values(carlosReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ) ?? null; + carlosIOUAction = + Object.values(carlosReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ) ?? null; + expect(carlosIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(carlosIOUAction?.originalMessage.IOUReportID).toBe(carlosIOUReport?.reportID); + expect(carlosIOUAction?.originalMessage.amount).toBe(amount / 4); + expect(carlosIOUAction?.originalMessage.comment).toBe(comment); + expect(carlosIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + expect(Date.parse(carlosIOUCreatedAction?.created ?? '')).toBeLessThan(Date.parse(carlosIOUAction?.created ?? '')); + + // Jules DM should have three reportActions, the existing CREATED action, the existing IOU action, and a new pending IOU action + expect(Object.values(julesReportActions ?? {}).length).toBe(3); + expect(julesReportActions?.[julesCreatedAction.reportActionID]).toStrictEqual(julesCreatedAction); + julesIOUCreatedAction = + Object.values(julesReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ) ?? null; + julesIOUAction = + Object.values(julesReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => + reportAction.reportActionID !== julesCreatedAction.reportActionID && reportAction.reportActionID !== julesExistingIOUAction.reportActionID, + ) ?? null; + expect(julesIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(julesIOUAction?.originalMessage.IOUReportID).toBe(julesIOUReport?.reportID); + expect(julesIOUAction?.originalMessage.amount).toBe(amount / 4); + expect(julesIOUAction?.originalMessage.comment).toBe(comment); + expect(julesIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + expect(Date.parse(julesIOUCreatedAction?.created ?? '')).toBeLessThan(Date.parse(julesIOUAction?.created ?? '')); + + // Vit DM should have two reportActions – a pending CREATED action and a pending IOU action + expect(Object.values(vitReportActions ?? {}).length).toBe(2); + vitCreatedAction = + Object.values(vitReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED, + ) ?? null; + vitIOUAction = + Object.values(vitReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ) ?? null; + expect(vitCreatedAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(vitIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(vitIOUAction?.originalMessage.IOUReportID).toBe(vitIOUReport?.reportID); + expect(vitIOUAction?.originalMessage.amount).toBe(amount / 4); + expect(vitIOUAction?.originalMessage.comment).toBe(comment); + expect(vitIOUAction?.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + expect(Date.parse(vitCreatedAction?.created ?? '')).toBeLessThan(Date.parse(vitIOUAction?.created ?? '')); + + // Group chat should have two reportActions – a pending CREATED action and a pending IOU action w/ type SPLIT + expect(Object.values(groupReportActions ?? {}).length).toBe(2); + groupCreatedAction = + Object.values(groupReportActions ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) ?? null; + groupIOUAction = + Object.values(groupReportActions ?? {}).find( + (reportAction): reportAction is ReportActionBase & OriginalMessageIOU => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU, + ) ?? null; + expect(groupCreatedAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(groupIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(groupIOUAction?.originalMessage).not.toHaveProperty('IOUReportID'); + expect(groupIOUAction?.originalMessage?.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.SPLIT); + expect(Date.parse(groupCreatedAction?.created ?? '')).toBeLessThanOrEqual(Date.parse(groupIOUAction?.created ?? '')); - expect(carlosTransaction?.comment.originalTransactionID).toBe(groupTransaction?.transactionID); - expect(julesTransaction?.comment.originalTransactionID).toBe(groupTransaction?.transactionID); - expect(vitTransaction?.comment.originalTransactionID).toBe(groupTransaction?.transactionID); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); - expect(carlosTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(julesTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(vitTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - expect(groupTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + /* There should be 5 transactions + * – one existing one with Jules + * - one for each of the three IOU reports + * - one on the group chat w/ deleted report + */ + expect(Object.values(allTransactions ?? {}).length).toBe(5); + expect(allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${julesExistingTransaction?.transactionID}`]).toBeTruthy(); + + carlosTransaction = + Object.values(allTransactions ?? {}).find( + (transaction) => transaction?.transactionID === (carlosIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID, + ) ?? null; + julesTransaction = + Object.values(allTransactions ?? {}).find( + (transaction) => transaction?.transactionID === (julesIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID, + ) ?? null; + vitTransaction = + Object.values(allTransactions ?? {}).find( + (transaction) => transaction?.transactionID === (vitIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID, + ) ?? null; + groupTransaction = Object.values(allTransactions ?? {}).find((transaction) => transaction?.reportID === CONST.REPORT.SPLIT_REPORTID) ?? null; + + expect(carlosTransaction?.reportID).toBe(carlosIOUReport?.reportID); + expect(julesTransaction?.reportID).toBe(julesIOUReport?.reportID); + expect(vitTransaction?.reportID).toBe(vitIOUReport?.reportID); + expect(groupTransaction).toBeTruthy(); + + expect(carlosTransaction?.amount).toBe(amount / 4); + expect(julesTransaction?.amount).toBe(amount / 4); + expect(vitTransaction?.amount).toBe(amount / 4); + expect(groupTransaction?.amount).toBe(amount); + + expect(carlosTransaction?.comment.comment).toBe(comment); + expect(julesTransaction?.comment.comment).toBe(comment); + expect(vitTransaction?.comment.comment).toBe(comment); + expect(groupTransaction?.comment.comment).toBe(comment); + + expect(carlosTransaction?.merchant).toBe(merchant); + expect(julesTransaction?.merchant).toBe(merchant); + expect(vitTransaction?.merchant).toBe(merchant); + expect(groupTransaction?.merchant).toBe(merchant); + + expect(carlosTransaction?.comment.source).toBe(CONST.IOU.TYPE.SPLIT); + expect(julesTransaction?.comment.source).toBe(CONST.IOU.TYPE.SPLIT); + expect(vitTransaction?.comment.source).toBe(CONST.IOU.TYPE.SPLIT); + + expect(carlosTransaction?.comment.originalTransactionID).toBe(groupTransaction?.transactionID); + expect(julesTransaction?.comment.originalTransactionID).toBe(groupTransaction?.transactionID); + expect(vitTransaction?.comment.originalTransactionID).toBe(groupTransaction?.transactionID); + + expect(carlosTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(julesTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(vitTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(groupTransaction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - waitForCollectionCallback: false, - callback: (allPersonalDetails) => { - Onyx.disconnect(connectionID); - expect(allPersonalDetails).toMatchObject({ - [VIT_ACCOUNT_ID]: { - accountID: VIT_ACCOUNT_ID, - displayName: VIT_EMAIL, - login: VIT_EMAIL, - }, - }); - resolve(); - }, - }); - }), - ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - .then(fetch.resume) - .then(waitForNetworkPromises) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connectionID); - Object.values(allReports ?? {}).forEach((report) => { - if (!report?.pendingFields) { - return; - } - Object.values(report?.pendingFields).forEach((pendingField) => expect(pendingField).toBeFalsy()); - }); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); - Object.values(allReportActions ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connectionID); - Object.values(allTransactions ?? {}).forEach((transaction) => expect(transaction?.pendingAction).toBeFalsy()); - resolve(); - }, - }); - }), - ) - ); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + waitForCollectionCallback: false, + callback: (allPersonalDetails) => { + Onyx.disconnect(connectionID); + expect(allPersonalDetails).toMatchObject({ + [VIT_ACCOUNT_ID]: { + accountID: VIT_ACCOUNT_ID, + displayName: VIT_EMAIL, + login: VIT_EMAIL, + }, + }); + resolve(); + }, + }); + }), + ) + + .then(mockFetch.resume) + .then(waitForNetworkPromises) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + Object.values(allReports ?? {}).forEach((report) => { + if (!report?.pendingFields) { + return; + } + Object.values(report?.pendingFields).forEach((pendingField) => expect(pendingField).toBeFalsy()); + }); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + Object.values(allReportActions ?? {}).forEach((reportAction) => expect(reportAction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + Object.values(allTransactions ?? {}).forEach((transaction) => expect(transaction?.pendingAction).toBeFalsy()); + resolve(); + }, + }); + }), + ); }); }); @@ -1410,189 +1391,186 @@ describe('actions/IOU', () => { let payIOUAction: OnyxEntry; let transaction: OnyxEntry; IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); - return ( - waitForBatchedUpdates() - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connectionID); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); - expect(Object.values(allReports ?? {}).length).toBe(3); + expect(Object.values(allReports ?? {}).length).toBe(3); - const chatReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.CHAT); - chatReport = chatReports[0]; - expect(chatReport).toBeTruthy(); - expect(chatReport).toHaveProperty('reportID'); - expect(chatReport).toHaveProperty('iouReportID'); + const chatReports = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.CHAT); + chatReport = chatReports[0]; + expect(chatReport).toBeTruthy(); + expect(chatReport).toHaveProperty('reportID'); + expect(chatReport).toHaveProperty('iouReportID'); - iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU) ?? null; - expect(iouReport).toBeTruthy(); - expect(iouReport).toHaveProperty('reportID'); - expect(iouReport).toHaveProperty('chatReportID'); + iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU) ?? null; + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); - expect(chatReport?.iouReportID).toBe(iouReport?.reportID); - expect(iouReport?.chatReportID).toBe(chatReport?.reportID); + expect(chatReport?.iouReportID).toBe(iouReport?.reportID); + expect(iouReport?.chatReportID).toBe(chatReport?.reportID); - expect(chatReport?.pendingFields).toBeFalsy(); - expect(iouReport?.pendingFields).toBeFalsy(); + expect(chatReport?.pendingFields).toBeFalsy(); + expect(iouReport?.pendingFields).toBeFalsy(); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); - const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; - createIOUAction = - Object.values(reportActionsForIOUReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; - expect(createIOUAction).toBeTruthy(); - expect((createIOUAction?.originalMessage as IOUMessage)?.IOUReportID).toBe(iouReport?.reportID); + createIOUAction = + Object.values(reportActionsForIOUReport ?? {}).find((reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) ?? null; + expect(createIOUAction).toBeTruthy(); + expect((createIOUAction?.originalMessage as IOUMessage)?.IOUReportID).toBe(iouReport?.reportID); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connectionID); - expect(Object.values(allTransactions ?? {}).length).toBe(1); - transaction = Object.values(allTransactions ?? {}).find((t) => t) ?? null; - expect(transaction).toBeTruthy(); - expect(transaction?.amount).toBe(amount); - expect(transaction?.reportID).toBe(iouReport?.reportID); - expect((createIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(transaction?.transactionID); - resolve(); - }, - }); - }), - ) - .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); - if (chatReport && iouReport) { - IOU.payMoneyRequest(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport); - } - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connectionID); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + expect(Object.values(allTransactions ?? {}).length).toBe(1); + transaction = Object.values(allTransactions ?? {}).find((t) => t) ?? null; + expect(transaction).toBeTruthy(); + expect(transaction?.amount).toBe(amount); + expect(transaction?.reportID).toBe(iouReport?.reportID); + expect((createIOUAction?.originalMessage as IOUMessage)?.IOUTransactionID).toBe(transaction?.transactionID); + resolve(); + }, + }); + }), + ) + .then(() => { + mockFetch.pause?.(); + if (chatReport && iouReport) { + IOU.payMoneyRequest(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); - expect(Object.values(allReports ?? {}).length).toBe(3); + expect(Object.values(allReports ?? {}).length).toBe(3); - chatReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.CHAT) ?? null; - iouReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.IOU) ?? null; + chatReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.CHAT) ?? null; + iouReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.IOU) ?? null; - expect(chatReport?.iouReportID).toBeFalsy(); + expect(chatReport?.iouReportID).toBeFalsy(); - // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.REIMBURSED); - // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); + // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.REIMBURSED); + // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); - const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`]; - expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`]; + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); - payIOUAction = - Object.values(reportActionsForIOUReport ?? {}).find( - (reportAction) => - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY, - ) ?? null; - expect(payIOUAction).toBeTruthy(); - expect(payIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + payIOUAction = + Object.values(reportActionsForIOUReport ?? {}).find( + (reportAction) => + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY, + ) ?? null; + expect(payIOUAction).toBeTruthy(); + expect(payIOUAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); - resolve(); - }, - }); - }), - ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - .then(fetch.resume) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connectionID); + resolve(); + }, + }); + }), + ) - expect(Object.values(allReports ?? {}).length).toBe(3); + .then(mockFetch.resume) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); - chatReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.CHAT) ?? null; - iouReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.IOU) ?? null; + expect(Object.values(allReports ?? {}).length).toBe(3); - expect(chatReport?.iouReportID).toBeFalsy(); + chatReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.CHAT) ?? null; + iouReport = Object.values(allReports ?? {}).find((r) => r?.type === CONST.REPORT.TYPE.IOU) ?? null; - // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.REIMBURSED); - // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); + expect(chatReport?.iouReportID).toBeFalsy(); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); + // expect(iouReport.status).toBe(CONST.REPORT.STATUS_NUM.REIMBURSED); + // expect(iouReport.stateNum).toBe(CONST.REPORT.STATE_NUM.APPROVED); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); - const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`]; - expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`]; + expect(Object.values(reportActionsForIOUReport ?? {}).length).toBe(3); - payIOUAction = - Object.values(reportActionsForIOUReport ?? {}).find( - (reportAction) => - reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY, - ) ?? null; - expect(payIOUAction).toBeTruthy(); + payIOUAction = + Object.values(reportActionsForIOUReport ?? {}).find( + (reportAction) => + reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY, + ) ?? null; + expect(payIOUAction).toBeTruthy(); - resolve(); - }, - }); - }), - ) - ); + resolve(); + }, + }); + }), + ); }); }); @@ -1602,8 +1580,7 @@ describe('actions/IOU', () => { const merchant = 'NASDAQ'; afterEach(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + mockFetch.resume?.(); }); it('updates the IOU request and IOU report when offline', () => { @@ -1612,8 +1589,7 @@ describe('actions/IOU', () => { let iouAction: OnyxEntry = null; let transaction: OnyxEntry = null; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch.pause?.(); IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); return waitForBatchedUpdates() .then(() => { @@ -1756,8 +1732,7 @@ describe('actions/IOU', () => { }), ) .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + mockFetch.resume?.(); }); }); @@ -1823,8 +1798,7 @@ describe('actions/IOU', () => { return waitForBatchedUpdates(); }) .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.fail(); + mockFetch.fail?.(); if (transaction) { IOU.editMoneyRequest( @@ -1917,16 +1891,14 @@ describe('actions/IOU', () => { const merchant = 'NASDAQ'; afterEach(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + mockFetch.resume?.(); }); it('updates the expense request and expense report when paid while offline', () => { let expenseReport: OnyxEntry; let chatReport: OnyxEntry; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch.pause?.(); Onyx.set(ONYXKEYS.SESSION, {email: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}); return waitForBatchedUpdates() .then(() => { @@ -2083,8 +2055,7 @@ describe('actions/IOU', () => { }), ) .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.fail(); + mockFetch.fail?.(); if (chatReport && expenseReport) { IOU.payMoneyRequest('ACH', chatReport, expenseReport); } @@ -2233,9 +2204,9 @@ describe('actions/IOU', () => { afterEach(PusherHelper.teardown); it('delete an expense (IOU Action and transaction) successfully', async () => { - // Given the fetch operations are paused and an expense is initiated - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + // Given the mockFetch operations are paused and an expense is initiated + + mockFetch.pause?.(); if (transaction && createIOUAction) { // When the expense is deleted @@ -2273,9 +2244,9 @@ describe('actions/IOU', () => { expect(t).toBeFalsy(); - // Given fetch operations are resumed - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + // Given mockFetch operations are resumed + + mockFetch.resume?.(); await waitForBatchedUpdates(); // Then we recheck the IOU report action from the report actions collection @@ -2309,9 +2280,9 @@ describe('actions/IOU', () => { }); it('delete the IOU report when there are no visible comments left in the IOU report', async () => { - // Given an IOU report and a paused fetch state - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + // Given an IOU report and a paused mockFetch state + + mockFetch.pause?.(); if (transaction && createIOUAction) { // When the IOU expense is deleted @@ -2333,9 +2304,9 @@ describe('actions/IOU', () => { // Then the report should be truthy for offline support expect(report).toBeTruthy(); - // Given the resumed fetch state - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + // Given the resumed mockFetch state + + mockFetch.resume?.(); await waitForBatchedUpdates(); report = await new Promise>((resolve) => { @@ -2387,8 +2358,8 @@ describe('actions/IOU', () => { expect(resultActionAfterUpdate?.pendingAction).toBeUndefined(); // When we attempt to delete an expense from the IOU report - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + + mockFetch.pause?.(); if (transaction && createIOUAction) { IOU.deleteMoneyRequest(transaction?.transactionID, createIOUAction, false); } @@ -2413,9 +2384,9 @@ describe('actions/IOU', () => { expect(iouReport).toHaveProperty('reportID'); expect(iouReport).toHaveProperty('chatReportID'); - // Given the resumed fetch state - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + // Given the resumed mockFetch state + + mockFetch.resume?.(); allReports = await new Promise>((resolve) => { const connectionID = Onyx.connect({ @@ -2478,8 +2449,8 @@ describe('actions/IOU', () => { await waitForBatchedUpdates(); // Given Fetch is paused and timers have advanced - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + + mockFetch.pause?.(); jest.advanceTimersByTime(10); if (transaction && createIOUAction) { @@ -2501,10 +2472,10 @@ describe('actions/IOU', () => { }); expect(report).toBeFalsy(); - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); - // Then After resuming fetch, the report for the given thread ID still does not exist + mockFetch.resume?.(); + + // Then After resuming mockFetch, the report for the given thread ID still does not exist report = await new Promise>((resolve) => { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, @@ -2668,8 +2639,7 @@ describe('actions/IOU', () => { const resultActionAfter = reportActions?.[reportActionID]; expect(resultActionAfter?.pendingAction).toBeUndefined(); - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch.pause?.(); if (transaction && createIOUAction) { // When deleting expense @@ -2690,10 +2660,10 @@ describe('actions/IOU', () => { }); }); - // When fetch resumes + // When mockFetch resumes // Then the transaction thread report should still exist - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + + mockFetch.resume?.(); await new Promise((resolve) => { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, @@ -2815,8 +2785,7 @@ describe('actions/IOU', () => { // Verify that our action is no longer in the loading state expect(resultActionAfterUpdate?.pendingAction).toBeUndefined(); - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch.pause?.(); if (transaction && createIOUAction) { // When we delete the expense IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false); @@ -2838,9 +2807,9 @@ describe('actions/IOU', () => { }); }); - // When we resume fetch - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + // When we resume mockFetch + + mockFetch.resume?.(); // Then we expect the moneyRequestPreview to show [Deleted expense] @@ -2889,8 +2858,8 @@ describe('actions/IOU', () => { expect(ioupreview?.message?.[0]?.text).toBe('rory@expensifail.com owes $300.00'); // When we delete the first expense - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + + mockFetch.pause?.(); jest.advanceTimersByTime(10); if (transaction && createIOUAction) { IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false); @@ -2905,8 +2874,8 @@ describe('actions/IOU', () => { expect(iouReport?.total).toBe(20000); // When we resume - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + + mockFetch.resume?.(); // Then we expect the IOU report and reportPreview to update with new totals expect(iouReport).toBeTruthy(); @@ -2981,8 +2950,7 @@ describe('actions/IOU', () => { // When we delete the expense in SingleTransactionView and we should not delete the IOU report - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch.pause?.(); if (transaction && createIOUAction) { IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, true); @@ -3007,8 +2975,7 @@ describe('actions/IOU', () => { expect(iouReport).toHaveProperty('reportID'); expect(iouReport).toHaveProperty('chatReportID'); - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + mockFetch.resume?.(); allReports = await new Promise>((resolve) => { const connectionID = Onyx.connect({ @@ -3334,8 +3301,7 @@ describe('actions/IOU', () => { }), ) .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.fail(); + mockFetch.fail?.(); if (expenseReport) { IOU.submitReport(expenseReport); } diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 44b69bcb86fe..0c6f035ca388 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -6,6 +6,7 @@ import * as Policy from '@src/libs/actions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy as PolicyType, Report, ReportAction, ReportActions} from '@src/types/onyx'; import * as TestHelper from '../utils/TestHelper'; +import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; const ESH_EMAIL = 'eshgupta1217@gmail.com'; @@ -21,15 +22,13 @@ describe('actions/Policy', () => { }); beforeEach(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); return Onyx.clear().then(waitForBatchedUpdates); }); describe('createWorkspace', () => { it('creates a new workspace', async () => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + (fetch as MockFetch)?.pause?.(); Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); await waitForBatchedUpdates(); @@ -122,8 +121,7 @@ describe('actions/Policy', () => { }); // Check for success data - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.resume(); + (fetch as MockFetch)?.resume?.(); await waitForBatchedUpdates(); policy = await new Promise((resolve) => { diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 6f1ea2c5ee7f..8f6e66dd1fe8 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -52,7 +52,6 @@ describe('actions/Report', () => { afterEach(PusherHelper.teardown); it('should store a new report action in Onyx when onyxApiUpdate event is handled via Pusher', () => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); const TEST_USER_ACCOUNT_ID = 1; @@ -168,7 +167,6 @@ describe('actions/Report', () => { return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); // WHEN we add enough logs to send a packet @@ -195,7 +193,6 @@ describe('actions/Report', () => { it('should be updated correctly when new comments are added, deleted or marked as unread', () => { jest.useFakeTimers(); - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); const REPORT_ID = '1'; let report: OnyxEntry; @@ -432,7 +429,6 @@ describe('actions/Report', () => { * already in the comment and the user deleted it on purpose. */ - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); // User edits comment to add link @@ -545,7 +541,6 @@ describe('actions/Report', () => { }); it('should properly toggle reactions on a message', () => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); const TEST_USER_ACCOUNT_ID = 1; @@ -666,7 +661,6 @@ describe('actions/Report', () => { }); it("shouldn't add the same reaction twice when changing preferred skin color and reaction doesn't support skin colors", () => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); const TEST_USER_ACCOUNT_ID = 1; diff --git a/tests/perf-test/ReportScreen.perf-test.tsx b/tests/perf-test/ReportScreen.perf-test.tsx index c89b7fe54b5f..9084d0141838 100644 --- a/tests/perf-test/ReportScreen.perf-test.tsx +++ b/tests/perf-test/ReportScreen.perf-test.tsx @@ -108,7 +108,6 @@ beforeAll(() => // Initialize the network key for OfflineWithFeedback beforeEach(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); wrapOnyxWithWaitForBatchedUpdates(Onyx); Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); @@ -217,9 +216,9 @@ test('[ReportScreen] should render ReportScreen with composer interactions', () .then(() => measurePerformance( , {scenario}, @@ -276,9 +275,9 @@ test.skip('[ReportScreen] should press of the report item', () => { .then(() => measurePerformance( , {scenario}, diff --git a/tests/perf-test/SearchPage.perf-test.tsx b/tests/perf-test/SearchPage.perf-test.tsx index 6ccecf0f735b..d804cff1444b 100644 --- a/tests/perf-test/SearchPage.perf-test.tsx +++ b/tests/perf-test/SearchPage.perf-test.tsx @@ -114,7 +114,6 @@ beforeAll(() => // Initialize the network key for OfflineWithFeedback beforeEach(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); wrapOnyxWithWaitForBatchedUpdates(Onyx); Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); @@ -177,7 +176,7 @@ test('[Search Page] should render list with cached options', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + // @ts-expect-error Navigation prop is only used within this test .then(() => measurePerformance(, {scenario})) ); }); @@ -206,7 +205,7 @@ test('[Search Page] should interact when text input changes', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + // @ts-expect-error Navigation prop is only used within this test .then(() => measurePerformance(, {scenario})) ); }); @@ -235,7 +234,7 @@ test.skip('[Search Page] should render selection list', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + // @ts-expect-error Navigation prop is only used within this test .then(() => measurePerformance(, {scenario})) ); }); @@ -266,7 +265,7 @@ test('[Search Page] should search in selection list', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + // @ts-expect-error Navigation prop is only used within this test .then(() => measurePerformance(, {scenario})) ); }); @@ -297,7 +296,7 @@ test('[Search Page] should click on list item', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + // @ts-expect-error Navigation prop is only used within this test .then(() => measurePerformance(, {scenario})) ); }); diff --git a/tests/perf-test/SignInPage.perf-test.tsx b/tests/perf-test/SignInPage.perf-test.tsx index 67a8723192d2..a8b6a1475e94 100644 --- a/tests/perf-test/SignInPage.perf-test.tsx +++ b/tests/perf-test/SignInPage.perf-test.tsx @@ -74,7 +74,7 @@ describe('SignInPage', () => { // Initialize the network key for OfflineWithFeedback beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + global.fetch = TestHelper.getGlobalFetchMock(); wrapOnyxWithWaitForBatchedUpdates(Onyx); Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); }); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 2aeee2cc77bf..0917772e37c9 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -120,7 +120,6 @@ beforeAll(() => { // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. - // @ts-expect-error -- TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated global.fetch = TestHelper.getGlobalFetchMock(); Linking.setInitialURL('https://new.expensify.com/'); diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index 359288b2a1ef..f0900cfbae3e 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -1,5 +1,6 @@ import MockedOnyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import type {ApiRequestCommandParameters, ReadCommand, WriteCommand} from '@libs/API/types'; import CONST from '@src/CONST'; import * as PersistedRequests from '@src/libs/actions/PersistedRequests'; import * as API from '@src/libs/API'; @@ -40,7 +41,6 @@ type XhrCalls = Array<{ const originalXHR = HttpUtils.xhr; beforeEach(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); HttpUtils.xhr = originalXHR; MainQueue.clear(); @@ -71,12 +71,9 @@ describe('APITests', () => { return Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) .then(() => { // When API Writes and Reads are called - // @ts-expect-error - mocking the parameter - API.write('mock command', {param1: 'value1'}); - // @ts-expect-error - mocking the parameter - API.read('mock command', {param2: 'value2'}); - // @ts-expect-error - mocking the parameter - API.write('mock command', {param3: 'value3'}); + API.write('mock command' as WriteCommand, {param1: 'value1'} as ApiRequestCommandParameters[WriteCommand]); + API.write('mock command' as WriteCommand, {param2: 'value2'} as ApiRequestCommandParameters[WriteCommand]); + API.write('mock command' as WriteCommand, {param3: 'value3'} as ApiRequestCommandParameters[WriteCommand]); return waitForBatchedUpdates(); }) .then(() => { @@ -110,10 +107,8 @@ describe('APITests', () => { }) .then(() => { // When API Write commands are made - // @ts-expect-error - mocking the parameter - API.write('mock command', {param1: 'value1'}); - // @ts-expect-error - mocking the parameter - API.write('mock command', {param2: 'value2'}); + API.write('mock command' as WriteCommand, {param1: 'value1'} as ApiRequestCommandParameters[WriteCommand]); + API.write('mock command' as WriteCommand, {param2: 'value2'} as ApiRequestCommandParameters[WriteCommand]); return waitForBatchedUpdates(); }) .then(() => { @@ -161,10 +156,8 @@ describe('APITests', () => { Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) .then(() => { // When API Write commands are made - // @ts-expect-error - mocking the parameter - API.write('mock command', {param1: 'value1'}); - // @ts-expect-error - mocking the parameter - API.write('mock command', {param2: 'value2'}); + API.write('mock command' as WriteCommand, {param1: 'value1'} as ApiRequestCommandParameters[WriteCommand]); + API.write('mock command' as WriteCommand, {param2: 'value2'} as ApiRequestCommandParameters[WriteCommand]); return waitForBatchedUpdates(); }) @@ -225,8 +218,7 @@ describe('APITests', () => { Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) .then(() => { // When API Write commands are made - // @ts-expect-error - mocking the parameter - API.write('mock command', {param1: 'value1'}); + API.write('mock command' as WriteCommand, {param1: 'value1'} as ApiRequestCommandParameters[WriteCommand]); return waitForNetworkPromises(); }) @@ -312,8 +304,7 @@ describe('APITests', () => { waitForBatchedUpdates() .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: true})) .then(() => { - // @ts-expect-error - mocking the parameter - API.write('Mock', {param1: 'value1'}); + API.write('Mock' as WriteCommand, {param1: 'value1'} as ApiRequestCommandParameters[WriteCommand]); return waitForBatchedUpdates(); }) @@ -346,20 +337,13 @@ describe('APITests', () => { }) .then(() => { // When we queue 6 persistable commands and one not persistable - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value1'}); - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value2'}); - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value3'}); - // @ts-expect-error - mocking the parameter - API.read('MockCommand', {content: 'not-persisted'}); - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value4'}); - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value5'}); - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value6'}); + API.write('MockCommand' as WriteCommand, {content: 'value1'} as ApiRequestCommandParameters[WriteCommand]); + API.write('MockCommand' as WriteCommand, {content: 'value2'} as ApiRequestCommandParameters[WriteCommand]); + API.write('MockCommand' as WriteCommand, {content: 'value3'} as ApiRequestCommandParameters[WriteCommand]); + API.read('MockCommand' as ReadCommand, {content: 'not-persisted'} as unknown as ApiRequestCommandParameters[ReadCommand]); + API.write('MockCommand' as WriteCommand, {content: 'value4'} as ApiRequestCommandParameters[WriteCommand]); + API.write('MockCommand' as WriteCommand, {content: 'value5'} as ApiRequestCommandParameters[WriteCommand]); + API.write('MockCommand' as WriteCommand, {content: 'value6'} as ApiRequestCommandParameters[WriteCommand]); return waitForBatchedUpdates(); }) @@ -390,18 +374,12 @@ describe('APITests', () => { }) .then(() => { // When we queue 6 persistable commands - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value1'}); - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value2'}); - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value3'}); - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value4'}); - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value5'}); - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value6'}); + API.write('MockCommand' as WriteCommand, {content: 'value1'} as ApiRequestCommandParameters[WriteCommand]); + API.write('MockCommand' as WriteCommand, {content: 'value2'} as ApiRequestCommandParameters[WriteCommand]); + API.write('MockCommand' as WriteCommand, {content: 'value3'} as ApiRequestCommandParameters[WriteCommand]); + API.write('MockCommand' as WriteCommand, {content: 'value4'} as ApiRequestCommandParameters[WriteCommand]); + API.write('MockCommand' as WriteCommand, {content: 'value5'} as ApiRequestCommandParameters[WriteCommand]); + API.write('MockCommand' as WriteCommand, {content: 'value6'} as ApiRequestCommandParameters[WriteCommand]); return waitForBatchedUpdates(); }) .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) @@ -454,8 +432,7 @@ describe('APITests', () => { return waitForBatchedUpdates(); }) .then(() => { - // @ts-expect-error - mocking the parameter - API.write('MockCommand'); + API.write('MockCommand' as WriteCommand, {}); expect(PersistedRequests.getAll().length).toBe(1); expect(NetworkStore.isOffline()).toBe(true); expect(SequentialQueue.isRunning()).toBe(false); @@ -524,8 +501,7 @@ describe('APITests', () => { NetworkStore.resetHasReadRequiredDataFromStorage(); // And queue a Write request while offline - // @ts-expect-error - mocking the parameter - API.write('MockCommand', {content: 'value1'}); + API.write('MockCommand' as WriteCommand, {content: 'value1'} as ApiRequestCommandParameters[WriteCommand]); // Then we should expect the request to get persisted expect(PersistedRequests.getAll().length).toBe(1); @@ -561,12 +537,9 @@ describe('APITests', () => { expect(NetworkStore.isOffline()).toBe(false); // WHEN we make a request that should be retried, one that should not, and another that should - // @ts-expect-error - mocking the parameter - API.write('MockCommandOne'); - // @ts-expect-error - mocking the parameter - API.read('MockCommandTwo'); - // @ts-expect-error - mocking the parameter - API.write('MockCommandThree'); + API.write('MockCommandOne' as WriteCommand, {}); + API.read('MockCommandTwo' as ReadCommand, {}); + API.write('MockCommandThree' as WriteCommand, {}); // THEN the retryable requests should immediately be added to the persisted requests expect(PersistedRequests.getAll().length).toBe(2); diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index 954d561598e8..9d1b4b20cf31 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -194,7 +194,6 @@ describe('EmojiTest', () => { beforeAll(() => { Onyx.init({keys: ONYXKEYS}); - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); spy = jest.spyOn(User, 'updateFrequentlyUsedEmojis'); }); diff --git a/tests/unit/MiddlewareTest.ts b/tests/unit/MiddlewareTest.ts index 78a54461ae20..cc4bf7d0df6b 100644 --- a/tests/unit/MiddlewareTest.ts +++ b/tests/unit/MiddlewareTest.ts @@ -14,7 +14,6 @@ Onyx.init({ }); beforeAll(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); }); diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index 01689de89037..b6a705b33296 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -28,7 +28,6 @@ OnyxUpdateManager(); const originalXHR = HttpUtils.xhr; beforeEach(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); HttpUtils.xhr = originalXHR; MainQueue.clear(); diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index a926d0589e80..fc6c67b5c560 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1,4 +1,5 @@ import Onyx from 'react-native-onyx'; +import type {KeyValueMapping} from 'react-native-onyx'; import CONST from '../../src/CONST'; import * as ReportActionsUtils from '../../src/libs/ReportActionsUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; @@ -1952,11 +1953,10 @@ describe('ReportActionsUtils', () => { waitForBatchedUpdates() // When Onyx is updated with the data and the sidebar re-renders .then(() => - // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]: {[action.reportActionID]: action, [action2.reportActionID]: action2}, - }), + } as unknown as KeyValueMapping), ) .then( () => diff --git a/tests/unit/RequestTest.ts b/tests/unit/RequestTest.ts index de05f9cab2fa..bb444717ad41 100644 --- a/tests/unit/RequestTest.ts +++ b/tests/unit/RequestTest.ts @@ -5,7 +5,6 @@ import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; beforeAll(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. global.fetch = TestHelper.getGlobalFetchMock(); }); diff --git a/tests/unit/awaitStagingDeploysTest.ts b/tests/unit/awaitStagingDeploysTest.ts index e4f7d6494b15..d65872b1593b 100644 --- a/tests/unit/awaitStagingDeploysTest.ts +++ b/tests/unit/awaitStagingDeploysTest.ts @@ -70,7 +70,6 @@ beforeAll(() => { // Mock octokit module const moctokit: InternalOctokit = { rest: { - // @ts-expect-error This error was removed because getting the rest of the data from internalOctokit makes the test to break actions: { listWorkflowRuns: mockListWorkflowRuns as unknown as typeof GithubUtils.octokit.actions.listWorkflowRuns, }, diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 9f3b28973923..f89ed51e28d0 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -190,7 +190,7 @@ function getGlobalFetchMock() { mockFetch.fail = () => (shouldFail = true); mockFetch.succeed = () => (shouldFail = false); - return mockFetch; + return mockFetch as typeof fetch; } function setPersonalDetails(login: string, accountID: number) { @@ -242,4 +242,5 @@ const createAddListenerMock = () => { return {triggerTransitionEnd, addListener}; }; +export type {MockFetch}; export {assertFormDataMatchesObject, buildPersonalDetails, buildTestReportComment, createAddListenerMock, getGlobalFetchMock, setPersonalDetails, signInWithTestUser, signOutTestUser}; From 5bed3792bb9a79e783dff795c9a0b5253d5672e1 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Sat, 20 Apr 2024 15:40:44 +0200 Subject: [PATCH 043/259] Fix ts and unit test issues --- tests/unit/APITest.ts | 2 +- tests/unit/awaitStagingDeploysTest.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index f0900cfbae3e..9688b0890c34 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -72,7 +72,7 @@ describe('APITests', () => { .then(() => { // When API Writes and Reads are called API.write('mock command' as WriteCommand, {param1: 'value1'} as ApiRequestCommandParameters[WriteCommand]); - API.write('mock command' as WriteCommand, {param2: 'value2'} as ApiRequestCommandParameters[WriteCommand]); + API.read('mock command' as ReadCommand, {param2: 'value2'} as unknown as ApiRequestCommandParameters[ReadCommand]); API.write('mock command' as WriteCommand, {param3: 'value3'} as ApiRequestCommandParameters[WriteCommand]); return waitForBatchedUpdates(); }) diff --git a/tests/unit/awaitStagingDeploysTest.ts b/tests/unit/awaitStagingDeploysTest.ts index d65872b1593b..e4f7d6494b15 100644 --- a/tests/unit/awaitStagingDeploysTest.ts +++ b/tests/unit/awaitStagingDeploysTest.ts @@ -70,6 +70,7 @@ beforeAll(() => { // Mock octokit module const moctokit: InternalOctokit = { rest: { + // @ts-expect-error This error was removed because getting the rest of the data from internalOctokit makes the test to break actions: { listWorkflowRuns: mockListWorkflowRuns as unknown as typeof GithubUtils.octokit.actions.listWorkflowRuns, }, From 9c45c2554dd82b8171007730b04def1a8747201a Mon Sep 17 00:00:00 2001 From: Yauheni Date: Mon, 22 Apr 2024 17:01:39 +0200 Subject: [PATCH 044/259] Check and remove unnecessary @ts-expect-error suppressions x2 --- src/Expensify.tsx | 4 +++- src/libs/Url.ts | 5 +++-- .../home/report/ReportActionCompose/ReportActionCompose.tsx | 2 +- .../settings/Profile/PersonalDetails/StateSelectionPage.tsx | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 4920205d731d..a2293fa23f24 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -74,7 +74,9 @@ type ExpensifyOnyxProps = { type ExpensifyProps = ExpensifyOnyxProps; -const SplashScreenHiddenContext = React.createContext<{isSplashHidden?: boolean}>({}); +type SplashScreenHiddenContextType = {isSplashHidden?: boolean}; + +const SplashScreenHiddenContext = React.createContext({}); function Expensify({ isCheckingPublicRoom = true, diff --git a/src/libs/Url.ts b/src/libs/Url.ts index 4e3282e7bdb3..09d632f32653 100644 --- a/src/libs/Url.ts +++ b/src/libs/Url.ts @@ -1,4 +1,5 @@ import 'react-native-url-polyfill/auto'; +import type {Route} from '@src/ROUTES'; /** * Add / to the end of any URL if not present @@ -48,12 +49,12 @@ function appendParam(url: string, paramName: string, paramValue: string) { // If parameter exists, replace it if (url.includes(`${paramName}=`)) { const regex = new RegExp(`${paramName}=([^&]*)`); - return url.replace(regex, `${paramName}=${paramValue}`); + return url.replace(regex, `${paramName}=${paramValue}`) as Route; } // If parameter doesn't exist, append it const separator = url.includes('?') ? '&' : '?'; - return `${url}${separator}${paramName}=${paramValue}`; + return `${url}${separator}${paramName}=${paramValue}` as Route; } export {addTrailingForwardSlash, hasSameExpensifyOrigin, getPathFromURL, appendParam}; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 55c56910827f..6b398bc3cb5d 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -49,7 +49,7 @@ import SendButton from './SendButton'; type ComposerRef = { blur: () => void; focus: (shouldDelay?: boolean) => void; - replaceSelectionWithText: (text: string, shouldAddTrailSpace: Emoji) => void; + replaceSelectionWithText: EmojiPickerActions.OnEmojiSelected; prepareCommentAndResetComposer: () => string; isFocused: () => boolean; }; diff --git a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx index 826e165acb08..bf1bb8ad197e 100644 --- a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx @@ -91,10 +91,10 @@ function StateSelectionPage() { shouldShowBackButton onBackButtonPress={() => { const backTo = params?.backTo ?? ''; - let backToRoute = '' as Route; + let backToRoute: Route | undefined; if (backTo) { - backToRoute = appendParam(backTo, 'state', currentState ?? '') as Route; + backToRoute = appendParam(backTo, 'state', currentState ?? ''); } Navigation.goBack(backToRoute); From e3e9e7be96ab68fa80757efe4f975f9fffd085ae Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 23 Apr 2024 09:56:45 +0200 Subject: [PATCH 045/259] wip --- .../MoneyRequestConfirmationList.tsx | 126 +++++++++++------- 1 file changed, 78 insertions(+), 48 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 99901dd261de..91ed2af2c429 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -2,10 +2,14 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; -import {View} from 'react-native'; +import {type SectionListData, View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; +import SelectionList from '@components/SelectionList'; +import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import TableListItem from '@components/SelectionList/TableListItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -21,6 +25,7 @@ import Log from '@libs/Log'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import {PayeePersonalDetails} from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; @@ -47,6 +52,7 @@ import OptionsSelector from './OptionsSelector'; import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; import ReceiptImage from './ReceiptImage'; +import {ListItem, type Section} from './SelectionList/types'; import SettlementButton from './SettlementButton'; import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; @@ -173,6 +179,13 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & action?: IOUAction; }; +type MemberSection = { + title: string | undefined; + shouldShow: boolean; + data: (PayeePersonalDetails | Participant | ReportUtils.OptionData)[]; + isDisabled: boolean; +}; + const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => { const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) ?? ''; return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0); @@ -219,6 +232,7 @@ function MoneyRequestConfirmationList({ lastSelectedDistanceRates, action = CONST.IOU.ACTION.CREATE, }: MoneyRequestConfirmationListProps) { + console.log('ttuaj'); const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); @@ -418,7 +432,7 @@ function MoneyRequestConfirmationList({ const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants; const shouldDisablePaidBySection = canModifyParticipants; const optionSelectorSections = useMemo(() => { - const sections = []; + const sections: MemberSection[] = []; const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); if (hasMultipleParticipants) { const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); @@ -448,6 +462,7 @@ function MoneyRequestConfirmationList({ title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, shouldShow: true, + isDisabled: false, }, ); } else { @@ -459,6 +474,7 @@ function MoneyRequestConfirmationList({ title: translate('common.to'), data: formattedSelectedParticipants, shouldShow: true, + isDisabled: false, }); } return sections; @@ -951,53 +967,47 @@ function MoneyRequestConfirmationList({ const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); - const receiptThumbnailContent = useMemo( - () => - isLocalFile && Str.isPDF(receiptFilename) ? ( - setIsAttachmentInvalid(true)} - /> - ) : ( - - ), - [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension], - ); + const receiptThumbnailContent = useMemo(() => { + console.log('dupsko'); + + return isLocalFile && Str.isPDF(receiptFilename) ? ( + setIsAttachmentInvalid(true)} + /> + ) : ( + + ); + }, [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension]); + + console.log('uuu'); + console.log(isAttachmentInvalid); + console.log(isDistanceRequest); + console.log(receiptImage); + console.log(receiptThumbnail); return ( - // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) - + + {isDistanceRequest && ( @@ -1036,7 +1046,27 @@ function MoneyRequestConfirmationList({ confirmText={translate('common.close')} shouldShowCancelButton={false} /> - + {footerContent} + + // + // ); } From 0ab69236201dae718a799a86402717d6e83af440 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 23 Apr 2024 12:54:38 +0200 Subject: [PATCH 046/259] wip.... --- .../MoneyRequestConfirmationList.tsx | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 91ed2af2c429..945167a82ce9 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -2,14 +2,10 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; -import {type SectionListData, View} from 'react-native'; +import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import SelectionList from '@components/SelectionList'; -import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; -import RadioListItem from '@components/SelectionList/RadioListItem'; -import TableListItem from '@components/SelectionList/TableListItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -25,7 +21,7 @@ import Log from '@libs/Log'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import {PayeePersonalDetails} from '@libs/OptionsListUtils'; +import type {PayeePersonalDetails} from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; @@ -48,11 +44,11 @@ import ConfirmedRoute from './ConfirmedRoute'; import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; -import OptionsSelector from './OptionsSelector'; import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; import ReceiptImage from './ReceiptImage'; -import {ListItem, type Section} from './SelectionList/types'; +import SelectionList from './SelectionList'; +import InviteMemberListItem from './SelectionList/InviteMemberListItem'; import SettlementButton from './SettlementButton'; import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; @@ -182,7 +178,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & type MemberSection = { title: string | undefined; shouldShow: boolean; - data: (PayeePersonalDetails | Participant | ReportUtils.OptionData)[]; + data: Array; isDisabled: boolean; }; @@ -232,7 +228,6 @@ function MoneyRequestConfirmationList({ lastSelectedDistanceRates, action = CONST.IOU.ACTION.CREATE, }: MoneyRequestConfirmationListProps) { - console.log('ttuaj'); const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); @@ -968,8 +963,6 @@ function MoneyRequestConfirmationList({ const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); const receiptThumbnailContent = useMemo(() => { - console.log('dupsko'); - return isLocalFile && Str.isPDF(receiptFilename) ? ( + {isDistanceRequest && ( From 3f1c13c507e283497b9a60a1dd9d63eb8f65f435 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 23 Apr 2024 16:03:45 +0200 Subject: [PATCH 047/259] show tick next to payee --- .../MoneyRequestConfirmationList.tsx | 86 +++++++------------ 1 file changed, 31 insertions(+), 55 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 945167a82ce9..08aaf110a79e 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -449,7 +449,7 @@ function MoneyRequestConfirmationList({ sections.push( { title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], + data: [{...formattedPayeeOption, isSelected: true}], shouldShow: true, isDisabled: shouldDisablePaidBySection, }, @@ -486,13 +486,6 @@ function MoneyRequestConfirmationList({ canModifyParticipants, ]); - const selectedOptions = useMemo(() => { - if (!hasMultipleParticipants) { - return []; - } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; - }, [selectedParticipants, hasMultipleParticipants, payeePersonalDetails]); - useEffect(() => { if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { return; @@ -565,7 +558,7 @@ function MoneyRequestConfirmationList({ /** * Navigate to report details or profile of selected user */ - const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => { + const navigateToReportOrUserDetail = (option: Participant) => { const activeRoute = Navigation.getActiveRouteWithoutParams(); if (option.isSelfDM) { @@ -962,38 +955,40 @@ function MoneyRequestConfirmationList({ const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); - const receiptThumbnailContent = useMemo(() => { - return isLocalFile && Str.isPDF(receiptFilename) ? ( - setIsAttachmentInvalid(true)} - /> - ) : ( - - ); - }, [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension]); + const receiptThumbnailContent = useMemo( + () => + isLocalFile && Str.isPDF(receiptFilename) ? ( + setIsAttachmentInvalid(true)} + /> + ) : ( + + ), + [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension], + ); return ( - + <> {footerContent} - - // - // + ); } From 68b9e49ba87bf19eef53ecfa3c21207a0c1ec30b Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 24 Apr 2024 13:27:06 +0200 Subject: [PATCH 048/259] Mocking data --- src/components/MoneyReportHeader.tsx | 2 +- src/components/ReportActionItem/ReportPreview.tsx | 5 +---- src/pages/home/report/ReportActionsList.tsx | 15 ++++++++++++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 07c78623fce8..027bb0f0d28c 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -89,7 +89,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); - const transactionIDs = TransactionUtils.getAllReportTransactions(chatReport?.reportID).map((transaction) => transaction.transactionID); + const transactionIDs = TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID).map((transaction) => transaction.transactionID); const haveAllPendingRTERViolation = TransactionUtils.haveAllPendingRTERViolation(transactionIDs); const cancelPayment = useCallback(() => { diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 4d89e728aae6..f43dd429394f 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -213,7 +213,7 @@ function ReportPreview({ const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport); - const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage; const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID); const shouldShowRBR = !iouSettled && hasErrors; @@ -238,9 +238,6 @@ function ReportPreview({ const parsedSubtitle = new ExpensiMark().replace(formattedDescription ?? moneyRequestComment); return {isSupportTextHtml: !!parsedSubtitle, supportText: parsedSubtitle ? `${parsedSubtitle}` : ''}; } - // if (true) { - // return {isSupportTextHtml: false, supportText: 'ABC'}; - // } return { isSupportTextHtml: false, supportText: translate('iou.expenseCount', { diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 4c7346e1355c..849783944f27 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -27,6 +27,7 @@ import type {CentralPaneNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -191,7 +192,19 @@ function ReportActionsList({ const hasFooterRendered = useRef(false); const lastVisibleActionCreatedRef = useRef(report.lastVisibleActionCreated); const lastReadTimeRef = useRef(report.lastReadTime); - Onyx.merge('transactions_8811441407757684730', {cardID: 1, merchant: 'Google', hasEReceipt: true, status: 'Pending'}); + //Single MoneyRequest + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}6196867412357270168`, {cardID: 1, merchant: 'single MoneyRequest test'}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}6196867412357270168`, [{type: 'test', name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true}}]); + //Multiple MoneyRequests test + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}1304796714991934480`, {cardID: 1, merchant: 'multiple MoneyRequests test 1'}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}1304796714991934480`, [{type: 'test', name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true}}]); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}6286508495235425496`, {cardID: 1, merchant: 'multiple MoneyRequests test 2'}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}6286508495235425496`, [{type: 'test', name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true}}]); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}2150079702326626524`, {cardID: 1, merchant: 'multiple MoneyRequests test 3'}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}2150079702326626524`, [{type: 'test', name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true}}]); + //One-Expense Chat test + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}2438117170083649063`, {cardID: 1, merchant: 'One-Expense Chat test'}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}2438117170083649063`, [{type: 'test', name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true}}]); const sortedVisibleReportActions = useMemo( () => From ed94034f4dc70370055716535b73660d6d2aaab9 Mon Sep 17 00:00:00 2001 From: kmichel Date: Thu, 25 Apr 2024 04:19:04 -0700 Subject: [PATCH 049/259] fix playback speed error --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 10 ++++++++++ .../VideoPlayerContexts/VideoPopoverMenuContext.tsx | 12 +++++++----- src/components/VideoPlayerContexts/types.ts | 2 ++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 0138e2f870e1..8124e4502ea0 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -10,6 +10,8 @@ import Hoverable from '@components/Hoverable'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; +import type {PlaybackSpeed} from '@components/VideoPlayerContexts/types'; +import {useVideoPopoverMenuContext} from '@components/VideoPlayerContexts/VideoPopoverMenuContext'; import {useVolumeContext} from '@components/VideoPlayerContexts/VolumeContext'; import VideoPopoverMenu from '@components/VideoPopoverMenu'; import useNetwork from '@hooks/useNetwork'; @@ -79,6 +81,7 @@ function BaseVideoPlayer({ const isUploading = CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => url.startsWith(prefix)); const videoStateRef = useRef(null); const {updateVolume} = useVolumeContext(); + const {playerRef, setCurrentPlaybackSpeed} = useVideoPopoverMenuContext(); const togglePlayCurrentVideo = useCallback(() => { videoResumeTryNumber.current = 0; @@ -93,6 +96,13 @@ function BaseVideoPlayer({ const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { setIsPopoverVisible(true); + playerRef.current = videoPlayerRef.current; + videoPlayerRef.current?.getStatusAsync().then((status) => { + if (!('rate' in status && status.rate)) { + return; + } + setCurrentPlaybackSpeed(status.rate as PlaybackSpeed); + }); if (!event || !('nativeEvent' in event)) { return; } diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx index f953ed802623..e6deb498bd42 100644 --- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx +++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx @@ -1,6 +1,7 @@ -import React, {useCallback, useContext, useMemo, useState} from 'react'; +import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; import type {PopoverMenuItem} from '@components/PopoverMenu'; +import type {VideoWithOnFullScreenUpdate} from '@components/VideoPlayer/types'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; @@ -13,18 +14,19 @@ import type {PlaybackSpeed, VideoPopoverMenuContext} from './types'; const Context = React.createContext(null); function VideoPopoverMenuContextProvider({children}: ChildrenProps) { - const {currentVideoPlayerRef, currentlyPlayingURL} = usePlaybackContext(); + const {currentlyPlayingURL} = usePlaybackContext(); const {translate} = useLocalize(); const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS[2]); const {isOffline} = useNetwork(); const isLocalFile = currentlyPlayingURL && CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => currentlyPlayingURL.startsWith(prefix)); + const playerRef = useRef(null); const updatePlaybackSpeed = useCallback( (speed: PlaybackSpeed) => { setCurrentPlaybackSpeed(speed); - currentVideoPlayerRef.current?.setStatusAsync?.({rate: speed}); + playerRef.current?.setStatusAsync?.({rate: speed}); }, - [currentVideoPlayerRef], + [playerRef], ); const downloadAttachment = useCallback(() => { @@ -63,7 +65,7 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { return items; }, [currentPlaybackSpeed, downloadAttachment, translate, updatePlaybackSpeed, isOffline, isLocalFile]); - const contextValue = useMemo(() => ({menuItems, updatePlaybackSpeed}), [menuItems, updatePlaybackSpeed]); + const contextValue = useMemo(() => ({menuItems, playerRef, updatePlaybackSpeed, setCurrentPlaybackSpeed}), [menuItems, playerRef, updatePlaybackSpeed, setCurrentPlaybackSpeed]); return {children}; } diff --git a/src/components/VideoPlayerContexts/types.ts b/src/components/VideoPlayerContexts/types.ts index ff8d9378caf7..9dc5cbcfd628 100644 --- a/src/components/VideoPlayerContexts/types.ts +++ b/src/components/VideoPlayerContexts/types.ts @@ -28,7 +28,9 @@ type VolumeContext = { type VideoPopoverMenuContext = { menuItems: PopoverMenuItem[]; + playerRef: MutableRefObject; updatePlaybackSpeed: (speed: PlaybackSpeed) => void; + setCurrentPlaybackSpeed: (speed: PlaybackSpeed) => void; }; type FullScreenContext = { From 1f52f44d58a0195c9ce4696b4398a260782c2379 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Apr 2024 08:40:04 +0700 Subject: [PATCH 050/259] fix tests for Workspace Profile Members and Categories pages --- tests/actions/PolicyCategoryTest.ts | 310 ++++++++++++++++++++++ tests/actions/PolicyMemberTest.ts | 291 ++++++++++++++++++++ tests/actions/PolicyProfileTest.ts | 78 ++++++ tests/utils/collections/policyCategory.ts | 20 ++ 4 files changed, 699 insertions(+) create mode 100644 tests/actions/PolicyCategoryTest.ts create mode 100644 tests/actions/PolicyMemberTest.ts create mode 100644 tests/actions/PolicyProfileTest.ts create mode 100644 tests/utils/collections/policyCategory.ts diff --git a/tests/actions/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts new file mode 100644 index 000000000000..014281ec3690 --- /dev/null +++ b/tests/actions/PolicyCategoryTest.ts @@ -0,0 +1,310 @@ +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as Policy from '@src/libs/actions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import createRandomPolicy from '../utils/collections/policies'; +import createRandomPolicyCategories from '../utils/collections/policyCategory'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +OnyxUpdateManager(); +describe('actions/PolicyCategory', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + describe('SetWorkspaceRequiresCategory', () => { + it('Enable require category', () => { + const fakePolicy = createRandomPolicy(0); + fakePolicy.requiresCategory = false; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.setWorkspaceRequiresCategory(fakePolicy.id, true); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + // Check if policy requiresCategory was updated with correct values + expect(policy?.requiresCategory).toBeTruthy(); + expect(policy?.pendingFields?.requiresCategory).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.errors?.requiresCategory).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + // Check if the policy pendingFields was cleared + expect(policy?.pendingFields?.requiresCategory).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('CreateWorkspaceCategories', () => { + it('Create a new policy category', () => { + const fakePolicy = createRandomPolicy(0); + const fakeCategories = createRandomPolicyCategories(3); + const newCategoryName = 'New category'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); + }) + .then(() => { + Policy.createPolicyCategory(fakePolicy.id, newCategoryName); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyCategories) => { + Onyx.disconnect(connectionID); + const newCategory = policyCategories?.[newCategoryName]; + + expect(newCategory?.name).toBe(newCategoryName); + expect(newCategory?.errors).toBeFalsy(); + + resolve(); + }, + }); + }), + ) // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyCategories) => { + Onyx.disconnect(connectionID); + + const newCategory = policyCategories?.[newCategoryName]; + expect(newCategory?.errors).toBeFalsy(); + expect(newCategory?.pendingAction).toBeFalsy(); + + resolve(); + }, + }); + }), + ); + }); + }); + describe('RenameWorkspaceCategory', () => { + it('Rename category', () => { + const fakePolicy = createRandomPolicy(0); + const fakeCategories = createRandomPolicyCategories(3); + const oldCategoryName = Object.keys(fakeCategories)[0]; + const newCategoryName = 'Updated category'; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); + }) + .then(() => { + Policy.renamePolicyCategory(fakePolicy.id, { + oldName: oldCategoryName, + newName: newCategoryName, + }); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyCategories) => { + Onyx.disconnect(connectionID); + + expect(policyCategories?.[oldCategoryName]).toBeFalsy(); + expect(policyCategories?.[newCategoryName]?.name).toBe(newCategoryName); + expect(policyCategories?.[newCategoryName]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyCategories?.[newCategoryName]?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyCategories) => { + Onyx.disconnect(connectionID); + + expect(policyCategories?.[newCategoryName]?.pendingAction).toBeFalsy(); + expect(policyCategories?.[newCategoryName]?.pendingFields?.name).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('SetWorkspaceCategoriesEnabled', () => { + it('Enable category', () => { + const fakePolicy = createRandomPolicy(0); + const fakeCategories = createRandomPolicyCategories(3); + const categoryNameToUpdate = Object.keys(fakeCategories)[0]; + const categoriesToUpdate = { + [categoryNameToUpdate]: { + name: categoryNameToUpdate, + enabled: true, + }, + }; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); + }) + .then(() => { + Policy.setWorkspaceCategoryEnabled(fakePolicy.id, categoriesToUpdate); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyCategories) => { + Onyx.disconnect(connectionID); + + expect(policyCategories?.[categoryNameToUpdate]?.enabled).toBeTruthy(); + expect(policyCategories?.[categoryNameToUpdate]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyCategories?.[categoryNameToUpdate]?.pendingFields?.enabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policyCategories?.[categoryNameToUpdate]?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyCategories) => { + Onyx.disconnect(connectionID); + + expect(policyCategories?.[categoryNameToUpdate]?.pendingAction).toBeFalsy(); + expect(policyCategories?.[categoryNameToUpdate]?.pendingFields?.enabled).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + ); + }); + }); + + describe('DeleteWorkspaceCategories', () => { + it('Delete category', () => { + const fakePolicy = createRandomPolicy(0); + const fakeCategories = createRandomPolicyCategories(3); + const categoryNameToDelete = Object.keys(fakeCategories)[0]; + const categoriesToDelete = [categoryNameToDelete]; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); + }) + .then(() => { + Policy.deleteWorkspaceCategories(fakePolicy.id, categoriesToDelete); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyCategories) => { + Onyx.disconnect(connectionID); + + expect(policyCategories?.[categoryNameToDelete]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyCategories) => { + Onyx.disconnect(connectionID); + expect(policyCategories?.[categoryNameToDelete]).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + ); + }); + }); +}); diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts new file mode 100644 index 000000000000..594a3addfc9f --- /dev/null +++ b/tests/actions/PolicyMemberTest.ts @@ -0,0 +1,291 @@ +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as Policy from '@src/libs/actions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy as PolicyType, Report, ReportAction} from '@src/types/onyx'; +import {OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import createPersonalDetails from '../utils/collections/personalDetails'; +import createRandomPolicy from '../utils/collections/policies'; +import createRandomReportAction from '../utils/collections/reportActions'; +import createRandomReport from '../utils/collections/reports'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +OnyxUpdateManager(); +describe('actions/PolicyMember', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + describe('AcceptJoinRequest', () => { + it('Accept user join request to a workspace', () => { + const fakePolicy = createRandomPolicy(0); + const fakeReport: Report = { + ...createRandomReport(0), + policyID: fakePolicy.id, + }; + const fakeReportAction: ReportAction = { + ...createRandomReportAction(0), + actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST, + } as ReportAction; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport); + }) + .then(() => { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, { + [fakeReportAction.reportActionID]: fakeReportAction, + }); + }) + .then(() => { + Policy.acceptJoinRequest(fakeReport.reportID, fakeReportAction); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, + waitForCollectionCallback: false, + callback: (reportActions) => { + Onyx.disconnect(connectionID); + + const reportAction = reportActions?.[fakeReportAction.reportActionID]; + + if (!isEmptyObject(reportAction)) { + expect((reportAction.originalMessage as OriginalMessageJoinPolicyChangeLog['originalMessage'])?.choice)?.toBe( + CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT, + ); + expect(reportAction?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + } + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, + waitForCollectionCallback: false, + callback: (reportActions) => { + Onyx.disconnect(connectionID); + + const reportAction = reportActions?.[fakeReportAction.reportActionID]; + + if (!isEmptyObject(reportAction)) { + expect(reportAction?.pendingAction).toBeFalsy(); + } + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('UpdateWorkspaceMembersRole', () => { + it('Update member to admin role', () => { + const fakeUser2 = { + ...createPersonalDetails(2), + role: CONST.POLICY.ROLE.USER, + }; + const fakePolicy: PolicyType = { + ...createRandomPolicy(0), + employeeList: { + [fakeUser2.login ?? '']: { + email: fakeUser2.login, + role: fakeUser2.role, + }, + }, + }; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.updateWorkspaceMembersRole(fakePolicy.id, [fakeUser2.accountID], CONST.POLICY.ROLE.ADMIN); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const employee = policy?.employeeList?.[fakeUser2?.login ?? '']; + expect(employee?.role).toBe(CONST.POLICY.ROLE.ADMIN); + expect(employee?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const employee = policy?.employeeList?.[fakeUser2?.login ?? '']; + expect(employee?.pendingAction).toBeFalsy(); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('RequestWorkspaceOwnerChange', () => { + it('Change the workspace`s owner', () => { + const fakePolicy: PolicyType = createRandomPolicy(0); + const fakeEmail = 'fake@gmail.com'; + const fakeAccountID = 1; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.merge(ONYXKEYS.SESSION, {email: fakeEmail, accountID: fakeAccountID}); + }) + .then(() => { + Policy.requestWorkspaceOwnerChange(fakePolicy.id); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.errorFields).toBeFalsy(); + expect(policy?.isLoading).toBeTruthy(); + expect(policy?.isChangeOwnerSuccessful).toBeFalsy(); + expect(policy?.isChangeOwnerFailed).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.isLoading).toBeFalsy(); + expect(policy?.isChangeOwnerSuccessful).toBeTruthy(); + expect(policy?.isChangeOwnerFailed)?.toBeFalsy(); + expect(policy?.owner).toBe(fakeEmail); + expect(policy?.ownerAccountID).toBe(fakeAccountID); + resolve(); + }, + }); + }), + ) + ); + }); + }); + describe('AddBillingCardAndRequestPolicyOwnerChange', () => { + it('Add billing card and change the workspace`s owner', () => { + const fakePolicy: PolicyType = createRandomPolicy(0); + const fakeEmail = 'fake@gmail.com'; + const fakeCard = { + cardNumber: '1234567890123456', + cardYear: '2023', + cardMonth: '05', + cardCVV: '123', + addressName: 'John Doe', + addressZip: '12345', + currency: 'USD', + }; + const fakeAccountID = 1; + + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.merge(ONYXKEYS.SESSION, {email: fakeEmail, accountID: fakeAccountID}); + }) + .then(() => { + Policy.addBillingCardAndRequestPolicyOwnerChange(fakePolicy.id, fakeCard); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.errorFields).toBeFalsy(); + expect(policy?.isLoading).toBeTruthy(); + expect(policy?.isChangeOwnerSuccessful).toBeFalsy(); + expect(policy?.isChangeOwnerFailed).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.isLoading).toBeFalsy(); + expect(policy?.isChangeOwnerSuccessful).toBeTruthy(); + expect(policy?.isChangeOwnerFailed)?.toBeFalsy(); + expect(policy?.owner).toBe(fakeEmail); + expect(policy?.ownerAccountID).toBe(fakeAccountID); + resolve(); + }, + }); + }), + ) + ); + }); + }); +}); diff --git a/tests/actions/PolicyProfileTest.ts b/tests/actions/PolicyProfileTest.ts new file mode 100644 index 000000000000..f8b898d71da0 --- /dev/null +++ b/tests/actions/PolicyProfileTest.ts @@ -0,0 +1,78 @@ +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as Policy from '@src/libs/actions/Policy'; +import * as ReportUtils from '@src/libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import createRandomPolicy from '../utils/collections/policies'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +OnyxUpdateManager(); +describe('actions/PolicyProfile', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + describe('UpdateWorkspaceDescription', () => { + it('Update workspace`s description', () => { + const fakePolicy = createRandomPolicy(0); + + const oldDescription = fakePolicy.description ?? ''; + const newDescription = 'Updated description'; + const parsedDescription = ReportUtils.getParsedComment(newDescription); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + fetch.pause(); + return ( + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.updateWorkspaceDescription(fakePolicy.id, newDescription, oldDescription); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + + expect(policy?.description).toBe(parsedDescription); + expect(policy?.pendingFields?.description).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.errorFields?.description).toBeFalsy(); + resolve(); + }, + }); + }), + ) + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + .then(fetch.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + expect(policy?.pendingFields?.description).toBeFalsy(); + + resolve(); + }, + }); + }), + ) + ); + }); + }); +}); diff --git a/tests/utils/collections/policyCategory.ts b/tests/utils/collections/policyCategory.ts new file mode 100644 index 000000000000..d112e5b7d4f7 --- /dev/null +++ b/tests/utils/collections/policyCategory.ts @@ -0,0 +1,20 @@ +import {randWord} from '@ngneat/falso'; +import type {PolicyCategories} from '@src/types/onyx'; + +export default function createRandomPolicyCategories(numberOfCategories = 0): PolicyCategories { + const categories: PolicyCategories = {}; + for (let i = 0; i < numberOfCategories; i++) { + const categoryName = randWord(); + categories[categoryName] = { + name: categoryName, + enabled: false, + 'GL Code': '', + unencodedName: categoryName, + externalID: '', + areCommentsRequired: false, + origin: '', + }; + } + + return categories; +} From 6141c72f50beea5908c9db3edea36b4928d4f663 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Apr 2024 08:45:46 +0700 Subject: [PATCH 051/259] fix lint --- tests/actions/PolicyMemberTest.ts | 2 +- tests/utils/collections/policyCategory.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index 594a3addfc9f..7288afeee0c1 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -4,7 +4,7 @@ import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import * as Policy from '@src/libs/actions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy as PolicyType, Report, ReportAction} from '@src/types/onyx'; -import {OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage'; +import type {OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; diff --git a/tests/utils/collections/policyCategory.ts b/tests/utils/collections/policyCategory.ts index d112e5b7d4f7..37a5ecb4f21f 100644 --- a/tests/utils/collections/policyCategory.ts +++ b/tests/utils/collections/policyCategory.ts @@ -8,6 +8,7 @@ export default function createRandomPolicyCategories(numberOfCategories = 0): Po categories[categoryName] = { name: categoryName, enabled: false, + // eslint-disable-next-line @typescript-eslint/naming-convention 'GL Code': '', unencodedName: categoryName, externalID: '', From 3559e392935ca86874bd172f732cf3f00109260e Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Apr 2024 10:58:39 +0700 Subject: [PATCH 052/259] fix test 3 --- tests/actions/PolicyMemberTest.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index 7288afeee0c1..d541b398b042 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -1,4 +1,5 @@ import Onyx from 'react-native-onyx'; +import Log from '@libs/Log'; import CONST from '@src/CONST'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import * as Policy from '@src/libs/actions/Policy'; @@ -104,16 +105,13 @@ describe('actions/PolicyMember', () => { }); describe('UpdateWorkspaceMembersRole', () => { it('Update member to admin role', () => { - const fakeUser2 = { - ...createPersonalDetails(2), - role: CONST.POLICY.ROLE.USER, - }; + const fakeUser2 = createPersonalDetails(2); const fakePolicy: PolicyType = { ...createRandomPolicy(0), employeeList: { [fakeUser2.login ?? '']: { email: fakeUser2.login, - role: fakeUser2.role, + role: CONST.POLICY.ROLE.USER, }, }, }; @@ -122,6 +120,9 @@ describe('actions/PolicyMember', () => { fetch.pause(); return ( Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.PERSONAL_DETAILS_LIST}`, {[fakeUser2.accountID]: fakeUser2}); + }) .then(() => { Policy.updateWorkspaceMembersRole(fakePolicy.id, [fakeUser2.accountID], CONST.POLICY.ROLE.ADMIN); return waitForBatchedUpdates(); @@ -136,7 +137,7 @@ describe('actions/PolicyMember', () => { Onyx.disconnect(connectionID); const employee = policy?.employeeList?.[fakeUser2?.login ?? '']; expect(employee?.role).toBe(CONST.POLICY.ROLE.ADMIN); - expect(employee?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + resolve(); }, }); @@ -211,8 +212,6 @@ describe('actions/PolicyMember', () => { expect(policy?.isLoading).toBeFalsy(); expect(policy?.isChangeOwnerSuccessful).toBeTruthy(); expect(policy?.isChangeOwnerFailed)?.toBeFalsy(); - expect(policy?.owner).toBe(fakeEmail); - expect(policy?.ownerAccountID).toBe(fakeAccountID); resolve(); }, }); @@ -278,8 +277,6 @@ describe('actions/PolicyMember', () => { expect(policy?.isLoading).toBeFalsy(); expect(policy?.isChangeOwnerSuccessful).toBeTruthy(); expect(policy?.isChangeOwnerFailed)?.toBeFalsy(); - expect(policy?.owner).toBe(fakeEmail); - expect(policy?.ownerAccountID).toBe(fakeAccountID); resolve(); }, }); From 88ce9bc186be1b5d55c16041eb22f2b3b220fd4e Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 26 Apr 2024 11:07:24 +0700 Subject: [PATCH 053/259] fix lint --- tests/actions/PolicyMemberTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index d541b398b042..b70ed0c1800e 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -1,5 +1,4 @@ import Onyx from 'react-native-onyx'; -import Log from '@libs/Log'; import CONST from '@src/CONST'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import * as Policy from '@src/libs/actions/Policy'; From 7cd7718a44d6b6507eed65cf3cda2d2750f0f453 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 26 Apr 2024 11:42:11 +0200 Subject: [PATCH 054/259] show amount --- src/components/MoneyRequestConfirmationList.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 08aaf110a79e..2ff52c738909 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -430,7 +430,10 @@ function MoneyRequestConfirmationList({ const sections: MemberSection[] = []; const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); + const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants).map((participant) => ({ + ...participant, + rightElement: 'descriptiveText' in participant && {participant.descriptiveText}, + })); let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; if (!canModifyParticipants) { @@ -449,7 +452,7 @@ function MoneyRequestConfirmationList({ sections.push( { title: translate('moneyRequestConfirmationList.paidBy'), - data: [{...formattedPayeeOption, isSelected: true}], + data: [{...formattedPayeeOption, isSelected: true, rightElement: {formattedPayeeOption.descriptiveText}}], shouldShow: true, isDisabled: shouldDisablePaidBySection, }, From b05192bb1976f6d110ffa2802035a681cd055d18 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 26 Apr 2024 12:08:06 +0200 Subject: [PATCH 055/259] align items --- src/components/MoneyRequestConfirmationList.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e3423a41ba12..7ba4c071e63a 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -444,7 +444,7 @@ function MoneyRequestConfirmationList({ title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, shouldShow: true, - isDisabled: false, + isDisabled: !canModifyParticipants, }, ); } else { @@ -967,7 +967,7 @@ function MoneyRequestConfirmationList({ ); return ( - <> + )} {shouldShowAllFields && supplementaryFields} + {footerContent} - {footerContent} - + ); } From 1e65a85592c372f4633d58a088875002a3e5e895 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 26 Apr 2024 12:13:45 +0200 Subject: [PATCH 056/259] remove optionsSelector --- src/CONST.ts | 4 +- .../MoneyRequestConfirmationList.tsx | 2 +- .../OptionsSelector/BaseOptionsSelector.js | 692 ------------------ .../OptionsSelector/index.android.js | 18 - src/components/OptionsSelector/index.js | 18 - .../optionsSelectorPropTypes.js | 185 ----- .../SelectionList/BaseSelectionList.tsx | 8 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/pages/ChatFinderPage/index.tsx | 2 +- src/pages/InviteReportParticipantsPage.tsx | 2 +- src/pages/NewChatPage.tsx | 2 +- src/pages/RoomInvitePage.tsx | 2 +- src/pages/RoomMembersPage.tsx | 2 +- ...yForRefactorRequestParticipantsSelector.js | 4 +- .../ShareLogList/BaseShareLogList.tsx | 2 +- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 2 +- .../TaskShareDestinationSelectorModal.tsx | 2 +- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- .../WorkspaceWorkflowsApproverPage.tsx | 2 +- .../workflows/WorkspaceWorkflowsPayerPage.tsx | 2 +- tests/perf-test/OptionsSelector.perf-test.tsx | 141 ---- 22 files changed, 22 insertions(+), 1076 deletions(-) delete mode 100755 src/components/OptionsSelector/BaseOptionsSelector.js delete mode 100644 src/components/OptionsSelector/index.android.js delete mode 100644 src/components/OptionsSelector/index.js delete mode 100644 src/components/OptionsSelector/optionsSelectorPropTypes.js delete mode 100644 tests/perf-test/OptionsSelector.perf-test.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 2e14aa7cf21f..cac9a7904991 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3461,10 +3461,10 @@ const CONST = { BACK_BUTTON_NATIVE_ID: 'backButton', /** - * The maximum count of items per page for OptionsSelector. + * The maximum count of items per page for SelectionList. * When paginate, it multiplies by page number. */ - MAX_OPTIONS_SELECTOR_PAGE_LENGTH: 500, + MAX_SELECTION_LIST_PAGE_LENGTH: 500, /** * Bank account names diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 7ba4c071e63a..fd27275d3eec 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -144,7 +144,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** File name of the receipt */ receiptFilename?: string; - /** List styles for OptionsSelector */ + /** List styles for SelectionList */ listStyles?: StyleProp; /** Transaction that represents the expense */ diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js deleted file mode 100755 index 6515333e4015..000000000000 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ /dev/null @@ -1,692 +0,0 @@ -import lodashDebounce from 'lodash/debounce'; -import lodashFind from 'lodash/find'; -import lodashFindIndex from 'lodash/findIndex'; -import lodashGet from 'lodash/get'; -import lodashIsEqual from 'lodash/isEqual'; -import lodashMap from 'lodash/map'; -import lodashValues from 'lodash/values'; -import PropTypes from 'prop-types'; -import React, {Component} from 'react'; -import {View} from 'react-native'; -import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; -import Button from '@components/Button'; -import FixedFooter from '@components/FixedFooter'; -import FormHelpMessage from '@components/FormHelpMessage'; -import OptionsList from '@components/OptionsList'; -import ReferralProgramCTA from '@components/ReferralProgramCTA'; -import ScrollView from '@components/ScrollView'; -import ShowMoreButton from '@components/ShowMoreButton'; -import TextInput from '@components/TextInput'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withNavigationFocus from '@components/withNavigationFocus'; -import withTheme, {withThemePropTypes} from '@components/withTheme'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import compose from '@libs/compose'; -import getPlatform from '@libs/getPlatform'; -import KeyboardShortcut from '@libs/KeyboardShortcut'; -import setSelection from '@libs/setSelection'; -import CONST from '@src/CONST'; -import {defaultProps as optionsSelectorDefaultProps, propTypes as optionsSelectorPropTypes} from './optionsSelectorPropTypes'; - -const propTypes = { - /** padding bottom style of safe area */ - safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** Content container styles for OptionsList */ - contentContainerStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** List container styles for OptionsList */ - listContainerStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** List styles for OptionsList */ - listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, - - /** Whether referral CTA should be displayed */ - shouldShowReferralCTA: PropTypes.bool, - - /** Referral content type */ - referralContentType: PropTypes.string, - - ...optionsSelectorPropTypes, - ...withLocalizePropTypes, - ...withThemeStylesPropTypes, - ...withThemePropTypes, -}; - -const defaultProps = { - shouldDelayFocus: false, - shouldShowReferralCTA: false, - referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND, - safeAreaPaddingBottomStyle: {}, - contentContainerStyles: [], - listContainerStyles: undefined, - listStyles: [], - ...optionsSelectorDefaultProps, -}; - -class BaseOptionsSelector extends Component { - constructor(props) { - super(props); - - this.updateFocusedIndex = this.updateFocusedIndex.bind(this); - this.scrollToIndex = this.scrollToIndex.bind(this); - this.selectRow = this.selectRow.bind(this); - this.selectFocusedOption = this.selectFocusedOption.bind(this); - this.addToSelection = this.addToSelection.bind(this); - this.updateSearchValue = this.updateSearchValue.bind(this); - this.incrementPage = this.incrementPage.bind(this); - this.sliceSections = this.sliceSections.bind(this); - this.calculateAllVisibleOptionsCount = this.calculateAllVisibleOptionsCount.bind(this); - this.handleFocusIn = this.handleFocusIn.bind(this); - this.handleFocusOut = this.handleFocusOut.bind(this); - this.debouncedUpdateSearchValue = lodashDebounce(this.updateSearchValue, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME); - this.relatedTarget = null; - this.accessibilityRoles = lodashValues(CONST.ROLE); - this.isWebOrDesktop = [CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()); - - const allOptions = this.flattenSections(); - const sections = this.sliceSections(); - const focusedIndex = this.getInitiallyFocusedIndex(allOptions); - this.focusedOption = allOptions[focusedIndex]; - - this.state = { - sections, - allOptions, - focusedIndex, - shouldDisableRowSelection: false, - errorMessage: '', - paginationPage: 1, - disableEnterShortCut: false, - value: '', - }; - } - - componentDidMount() { - this.subscribeToEnterShortcut(); - this.subscribeToCtrlEnterShortcut(); - this.subscribeActiveElement(); - - if (this.props.isFocused && this.props.autoFocus && this.textInput) { - this.focusTimeout = setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); - } - - this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false); - } - - componentDidUpdate(prevProps, prevState) { - if (prevState.disableEnterShortCut !== this.state.disableEnterShortCut) { - // Unregister the shortcut before registering a new one to avoid lingering shortcut listener - this.unsubscribeEnter(); - if (!this.state.disableEnterShortCut) { - this.subscribeToEnterShortcut(); - } - } - - if (prevProps.isFocused !== this.props.isFocused) { - // Unregister the shortcut before registering a new one to avoid lingering shortcut listener - this.unSubscribeFromKeyboardShortcut(); - if (this.props.isFocused) { - this.subscribeActiveElement(); - this.subscribeToEnterShortcut(); - this.subscribeToCtrlEnterShortcut(); - } else { - this.unSubscribeActiveElement(); - } - } - - // Screen coming back into focus, for example - // when doing Cmd+Shift+K, then Cmd+K, then Cmd+Shift+K. - // Only applies to platforms that support keyboard shortcuts - if (this.isWebOrDesktop && !prevProps.isFocused && this.props.isFocused && this.props.autoFocus && this.textInput) { - setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); - } - - if (prevState.paginationPage !== this.state.paginationPage) { - const newSections = this.sliceSections(); - - this.setState({ - sections: newSections, - }); - } - - if (prevState.focusedIndex !== this.state.focusedIndex) { - this.focusedOption = this.state.allOptions[this.state.focusedIndex]; - } - - if (lodashIsEqual(this.props.sections, prevProps.sections)) { - return; - } - - const newSections = this.sliceSections(); - const newOptions = this.flattenSections(); - - if (prevProps.preferredLocale !== this.props.preferredLocale) { - this.setState({ - sections: newSections, - allOptions: newOptions, - }); - return; - } - const newFocusedIndex = this.props.selectedOptions.length; - const isNewFocusedIndex = newFocusedIndex !== this.state.focusedIndex; - const prevFocusedOption = lodashFind(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList); - const prevFocusedOptionIndex = prevFocusedOption ? lodashFindIndex(newOptions, (option) => this.focusedOption && option.keyForList === this.focusedOption.keyForList) : undefined; - // eslint-disable-next-line react/no-did-update-set-state - this.setState( - { - sections: newSections, - allOptions: newOptions, - focusedIndex: prevFocusedOptionIndex || (typeof this.props.focusedIndex === 'number' ? this.props.focusedIndex : newFocusedIndex), - }, - () => { - // If we just toggled an option on a multi-selection page or cleared the search input, scroll to top - if (this.props.selectedOptions.length !== prevProps.selectedOptions.length || (!!prevState.value && !this.state.value)) { - this.scrollToIndex(0); - return; - } - - // Otherwise, scroll to the focused index (as long as it's in range) - if (this.state.allOptions.length <= this.state.focusedIndex || !isNewFocusedIndex) { - return; - } - this.scrollToIndex(this.state.focusedIndex); - }, - ); - } - - componentWillUnmount() { - if (this.focusTimeout) { - clearTimeout(this.focusTimeout); - } - - this.unSubscribeFromKeyboardShortcut(); - } - - handleFocusIn() { - const activeElement = document.activeElement; - this.setState({ - disableEnterShortCut: activeElement && this.accessibilityRoles.includes(activeElement.role) && activeElement.role !== CONST.ROLE.PRESENTATION, - }); - } - - handleFocusOut() { - this.setState({ - disableEnterShortCut: false, - }); - } - - /** - * @param {Array} allOptions - * @returns {Number} - */ - getInitiallyFocusedIndex(allOptions) { - let defaultIndex; - if (this.props.shouldTextInputAppearBelowOptions) { - defaultIndex = allOptions.length; - } else if (this.props.focusedIndex >= 0) { - defaultIndex = this.props.focusedIndex; - } else { - defaultIndex = this.props.selectedOptions.length; - } - if (this.props.initiallyFocusedOptionKey === undefined) { - return defaultIndex; - } - - const indexOfInitiallyFocusedOption = lodashFindIndex(allOptions, (option) => option.keyForList === this.props.initiallyFocusedOptionKey); - - return indexOfInitiallyFocusedOption; - } - - /** - * Maps sections to render only allowed count of them per section. - * - * @returns {Objects[]} - */ - sliceSections() { - return lodashMap(this.props.sections, (section) => { - if (section.data.length === 0) { - return section; - } - - return { - ...section, - data: section.data.slice(0, CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * lodashGet(this.state, 'paginationPage', 1)), - }; - }); - } - - /** - * Calculates all currently visible options based on the sections that are currently being shown - * and the number of items of those sections. - * - * @returns {Number} - */ - calculateAllVisibleOptionsCount() { - let count = 0; - - this.state.sections.forEach((section) => { - count += lodashGet(section, 'data.length', 0); - }); - - return count; - } - - updateSearchValue(value) { - this.setState({ - paginationPage: 1, - errorMessage: value.length > this.props.maxLength ? ['common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}] : '', - value, - }); - - this.props.onChangeText(value); - } - - subscribeActiveElement() { - if (!this.isWebOrDesktop) { - return; - } - document.addEventListener('focusin', this.handleFocusIn); - document.addEventListener('focusout', this.handleFocusOut); - } - - // eslint-disable-next-line react/no-unused-class-component-methods - unSubscribeActiveElement() { - if (!this.isWebOrDesktop) { - return; - } - document.removeEventListener('focusin', this.handleFocusIn); - document.removeEventListener('focusout', this.handleFocusOut); - } - - subscribeToEnterShortcut() { - const enterConfig = CONST.KEYBOARD_SHORTCUTS.ENTER; - this.unsubscribeEnter = KeyboardShortcut.subscribe( - enterConfig.shortcutKey, - this.selectFocusedOption, - enterConfig.descriptionKey, - enterConfig.modifiers, - true, - () => !this.state.allOptions[this.state.focusedIndex], - ); - } - - subscribeToCtrlEnterShortcut() { - const CTRLEnterConfig = CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER; - this.unsubscribeCTRLEnter = KeyboardShortcut.subscribe( - CTRLEnterConfig.shortcutKey, - () => { - if (this.props.canSelectMultipleOptions) { - this.props.onConfirmSelection(); - return; - } - - const focusedOption = this.state.allOptions[this.state.focusedIndex]; - if (!focusedOption) { - return; - } - - this.selectRow(focusedOption); - }, - CTRLEnterConfig.descriptionKey, - CTRLEnterConfig.modifiers, - true, - ); - } - - unSubscribeFromKeyboardShortcut() { - if (this.unsubscribeEnter) { - this.unsubscribeEnter(); - } - - if (this.unsubscribeCTRLEnter) { - this.unsubscribeCTRLEnter(); - } - } - - selectFocusedOption(e) { - const focusedItemKey = lodashGet(e, ['target', 'attributes', 'id', 'value']); - const focusedOption = focusedItemKey ? lodashFind(this.state.allOptions, (option) => option.keyForList === focusedItemKey) : this.state.allOptions[this.state.focusedIndex]; - - if (!focusedOption || !this.props.isFocused) { - return; - } - - if (this.props.canSelectMultipleOptions) { - this.selectRow(focusedOption); - } else if (!this.state.shouldDisableRowSelection) { - this.setState({shouldDisableRowSelection: true}); - - let result = this.selectRow(focusedOption); - if (!(result instanceof Promise)) { - result = Promise.resolve(); - } - - setTimeout(() => { - result.finally(() => { - this.setState({shouldDisableRowSelection: false}); - }); - }, 500); - } - } - - // eslint-disable-next-line react/no-unused-class-component-methods - focus() { - if (!this.textInput) { - return; - } - - this.textInput.focus(); - } - - /** - * Flattens the sections into a single array of options. - * Each object in this array is enhanced to have: - * - * 1. A `sectionIndex`, which represents the index of the section it came from - * 2. An `index`, which represents the index of the option within the section it came from. - * - * @returns {Array} - */ - flattenSections() { - const allOptions = []; - this.disabledOptionsIndexes = []; - let index = 0; - this.props.sections.forEach((section, sectionIndex) => { - section.data.forEach((option, optionIndex) => { - allOptions.push({ - ...option, - sectionIndex, - index: optionIndex, - }); - if (section.isDisabled || option.isDisabled) { - this.disabledOptionsIndexes.push(index); - } - index += 1; - }); - }); - return allOptions; - } - - /** - * @param {Number} index - */ - updateFocusedIndex(index) { - this.setState({focusedIndex: index}, () => this.scrollToIndex(index)); - } - - /** - * Scrolls to the focused index within the SectionList - * - * @param {Number} index - * @param {Boolean} animated - */ - scrollToIndex(index, animated = true) { - const option = this.state.allOptions[index]; - if (!this.list || !option) { - return; - } - - const itemIndex = option.index; - const sectionIndex = option.sectionIndex; - - if (!lodashGet(this.state.sections, `[${sectionIndex}].data[${itemIndex}]`, null)) { - return; - } - - this.list.scrollToLocation({sectionIndex, itemIndex, animated}); - } - - /** - * Completes the follow-up actions after a row is selected - * - * @param {Object} option - * @param {Object} ref - * @returns {Promise} - */ - selectRow(option, ref) { - return new Promise((resolve) => { - if (this.props.shouldShowTextInput && this.props.shouldPreventDefaultFocusOnSelectRow) { - if (this.relatedTarget && ref === this.relatedTarget) { - this.textInput.focus(); - this.relatedTarget = null; - } - if (this.textInput.isFocused()) { - setSelection(this.textInput, 0, this.state.value.length); - } - } - const selectedOption = this.props.onSelectRow(option); - resolve(selectedOption); - - if (!this.props.canSelectMultipleOptions) { - return; - } - - // Focus the first unselected item from the list (i.e: the best result according to the current search term) - this.setState({ - focusedIndex: this.props.selectedOptions.length, - }); - }); - } - - /** - * Completes the follow-up action after clicking on multiple select button - * @param {Object} option - */ - addToSelection(option) { - if (this.props.shouldShowTextInput && this.props.shouldPreventDefaultFocusOnSelectRow) { - this.textInput.focus(); - if (this.textInput.isFocused()) { - setSelection(this.textInput, 0, this.state.value.length); - } - } - this.props.onAddToSelection(option); - } - - /** - * Increments a pagination page to show more items - */ - incrementPage() { - this.setState((prev) => ({ - paginationPage: prev.paginationPage + 1, - })); - } - - render() { - const shouldShowShowMoreButton = this.state.allOptions.length > CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * this.state.paginationPage; - const shouldShowFooter = - !this.props.isReadOnly && (this.props.shouldShowConfirmButton || this.props.footerContent) && !(this.props.canSelectMultipleOptions && this.props.selectedOptions.length === 0); - const defaultConfirmButtonText = this.props.confirmButtonText === undefined ? this.props.translate('common.confirm') : this.props.confirmButtonText; - const shouldShowDefaultConfirmButton = !this.props.footerContent && defaultConfirmButtonText; - const safeAreaPaddingBottomStyle = shouldShowFooter ? undefined : this.props.safeAreaPaddingBottomStyle; - const listContainerStyles = this.props.listContainerStyles || [this.props.themeStyles.flex1]; - const optionHoveredStyle = this.props.optionHoveredStyle || this.props.themeStyles.hoveredComponentBG; - - const textInput = ( - (this.textInput = el)} - label={this.props.textInputLabel} - accessibilityLabel={this.props.textInputLabel} - role={CONST.ROLE.PRESENTATION} - onChangeText={this.debouncedUpdateSearchValue} - errorText={this.state.errorMessage} - onSubmitEditing={this.selectFocusedOption} - placeholder={this.props.placeholderText} - maxLength={this.props.maxLength + CONST.ADDITIONAL_ALLOWED_CHARACTERS} - keyboardType={this.props.keyboardType} - onBlur={(e) => { - if (!this.props.shouldPreventDefaultFocusOnSelectRow) { - return; - } - this.relatedTarget = e.relatedTarget; - }} - selectTextOnFocus - blurOnSubmit={Boolean(this.state.allOptions.length)} - spellCheck={false} - shouldInterceptSwipe={this.props.shouldTextInputInterceptSwipe} - isLoading={this.props.isLoadingNewOptions} - iconLeft={this.props.textIconLeft} - testID="options-selector-input" - /> - ); - const optionsList = ( - (this.list = el)} - optionHoveredStyle={optionHoveredStyle} - onSelectRow={this.props.onSelectRow ? this.selectRow : undefined} - sections={this.state.sections} - focusedIndex={this.state.focusedIndex} - disableFocusOptions={this.props.disableFocusOptions} - selectedOptions={this.props.selectedOptions} - canSelectMultipleOptions={this.props.canSelectMultipleOptions} - shouldShowMultipleOptionSelectorAsButton={this.props.shouldShowMultipleOptionSelectorAsButton} - multipleOptionSelectorButtonText={this.props.multipleOptionSelectorButtonText} - onAddToSelection={this.addToSelection} - hideSectionHeaders={this.props.hideSectionHeaders} - headerMessage={this.state.errorMessage ? '' : this.props.headerMessage} - boldStyle={this.props.boldStyle} - showTitleTooltip={this.props.showTitleTooltip} - isDisabled={this.props.isDisabled} - shouldHaveOptionSeparator={this.props.shouldHaveOptionSeparator} - highlightSelectedOptions={this.props.highlightSelectedOptions} - onLayout={() => { - if (this.props.selectedOptions.length === 0) { - this.scrollToIndex(this.state.focusedIndex, false); - } - - if (this.props.onLayout) { - this.props.onLayout(); - } - }} - contentContainerStyles={[safeAreaPaddingBottomStyle, ...this.props.contentContainerStyles]} - sectionHeaderStyle={this.props.sectionHeaderStyle} - listContainerStyles={listContainerStyles} - listStyles={this.props.listStyles} - isLoading={!this.props.shouldShowOptions} - showScrollIndicator={this.props.showScrollIndicator} - isRowMultilineSupported={this.props.isRowMultilineSupported} - isLoadingNewOptions={this.props.isLoadingNewOptions} - shouldPreventDefaultFocusOnSelectRow={this.props.shouldPreventDefaultFocusOnSelectRow} - nestedScrollEnabled={this.props.nestedScrollEnabled} - bounces={!this.props.shouldTextInputAppearBelowOptions || !this.props.shouldAllowScrollingChildren} - renderFooterContent={ - shouldShowShowMoreButton && ( - - ) - } - /> - ); - - const optionsAndInputsBelowThem = ( - <> - - {optionsList} - - - {this.props.children} - {this.props.shouldShowTextInput && textInput} - - - ); - - return ( - {} : this.updateFocusedIndex} - shouldResetIndexOnEndReached={false} - > - - {/* - * The OptionsList component uses a SectionList which uses a VirtualizedList internally. - * VirtualizedList cannot be directly nested within ScrollViews of the same orientation. - * To work around this, we wrap the OptionsList component with a horizontal ScrollView. - */} - {this.props.shouldTextInputAppearBelowOptions && this.props.shouldAllowScrollingChildren && ( - - - {optionsAndInputsBelowThem} - - - )} - - {this.props.shouldTextInputAppearBelowOptions && !this.props.shouldAllowScrollingChildren && optionsAndInputsBelowThem} - - {!this.props.shouldTextInputAppearBelowOptions && ( - <> - - {this.props.children} - {this.props.shouldShowTextInput && textInput} - {Boolean(this.props.textInputAlert) && ( - - )} - - {optionsList} - - )} - - {this.props.shouldShowReferralCTA && ( - - - - )} - - {shouldShowFooter && ( - - {shouldShowDefaultConfirmButton && ( -