diff --git a/android/app/build.gradle b/android/app/build.gradle index 0b2271d16716..62316cf4cf38 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001045605 - versionName "1.4.56-5" + versionCode 1001045700 + versionName "1.4.57-0" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 37741d484d63..071a7f4802da 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.56 + 1.4.57 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.56.5 + 1.4.57.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index e228808076d0..c12ea4bfdf91 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.56 + 1.4.57 CFBundleSignature ???? CFBundleVersion - 1.4.56.5 + 1.4.57.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 11f83011d47a..afa6db80106f 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.56 + 1.4.57 CFBundleVersion - 1.4.56.5 + 1.4.57.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 6e7f23674852..34c2b335abeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.56-5", + "version": "1.4.57-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.56-5", + "version": "1.4.57-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 83c3095585be..453310e2859a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.56-5", + "version": "1.4.57-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx index fda0c5441734..20b927913bfb 100644 --- a/src/components/DistanceEReceipt.tsx +++ b/src/components/DistanceEReceipt.tsx @@ -15,9 +15,9 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import ImageSVG from './ImageSVG'; import PendingMapView from './MapView/PendingMapView'; +import ReceiptImage from './ReceiptImage'; import ScrollView from './ScrollView'; import Text from './Text'; -import ThumbnailImage from './ThumbnailImage'; type DistanceEReceiptProps = { /** The transaction for the distance request */ @@ -30,7 +30,7 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) { const thumbnail = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction).thumbnail : null; const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction) ?? {}; const formattedTransactionAmount = CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency); - const thumbnailSource = tryResolveUrlFromApiRoot((thumbnail as string) || ''); + const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? ''); const waypoints = useMemo(() => transaction?.comment?.waypoints ?? {}, [transaction?.comment?.waypoints]); const sortedWaypoints = useMemo( () => @@ -58,11 +58,9 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) { {TransactionUtils.isFetchingWaypointsFromServer(transaction) || !thumbnailSource ? ( ) : ( - )} diff --git a/src/components/EReceiptThumbnail.tsx b/src/components/EReceiptThumbnail.tsx index 023dcc16e696..63889f76e67c 100644 --- a/src/components/EReceiptThumbnail.tsx +++ b/src/components/EReceiptThumbnail.tsx @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; +import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -14,6 +15,7 @@ import * as eReceiptBGs from './Icon/EReceiptBGs'; import * as Expensicons from './Icon/Expensicons'; import * as MCCIcons from './Icon/MCCIcons'; import Image from './Image'; +import Text from './Text'; type EReceiptThumbnailOnyxProps = { transaction: OnyxEntry; @@ -26,6 +28,15 @@ type EReceiptThumbnailProps = EReceiptThumbnailOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types transactionID: string; + /** Border radius to be applied on the parent view. */ + borderRadius?: number; + + /** The file extension of the receipt that the preview thumbnail is being displayed for. */ + fileExtension?: string; + + /** Whether it is a receipt thumbnail we are displaying. */ + isReceiptThumbnail?: boolean; + /** Center the eReceipt Icon vertically */ centerIconV?: boolean; @@ -42,13 +53,14 @@ const backgroundImages = { [CONST.ERECEIPT_COLORS.PINK]: eReceiptBGs.EReceiptBG_Pink, }; -function EReceiptThumbnail({transaction, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) { +function EReceiptThumbnail({transaction, borderRadius, fileExtension, isReceiptThumbnail = false, centerIconV = true, iconSize = 'large'}: EReceiptThumbnailProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const colorCode = isReceiptThumbnail ? StyleUtils.getFileExtensionColorCode(fileExtension) : StyleUtils.getEReceiptColorCode(transaction); - const backgroundImage = useMemo(() => backgroundImages[StyleUtils.getEReceiptColorCode(transaction)], [StyleUtils, transaction]); + const backgroundImage = useMemo(() => backgroundImages[colorCode], [colorCode]); - const colorStyles = StyleUtils.getEReceiptColorStyles(StyleUtils.getEReceiptColorCode(transaction)); + const colorStyles = StyleUtils.getEReceiptColorStyles(colorCode); const primaryColor = colorStyles?.backgroundColor; const secondaryColor = colorStyles?.color; const transactionDetails = ReportUtils.getTransactionDetails(transaction); @@ -58,15 +70,21 @@ function EReceiptThumbnail({transaction, centerIconV = true, iconSize = 'large'} let receiptIconWidth: number = variables.eReceiptIconWidth; let receiptIconHeight: number = variables.eReceiptIconHeight; let receiptMCCSize: number = variables.eReceiptMCCHeightWidth; + let labelFontSize: number = variables.fontSizeNormal; + let labelLineHeight: number = variables.lineHeightLarge; if (iconSize === 'small') { receiptIconWidth = variables.eReceiptIconWidthSmall; receiptIconHeight = variables.eReceiptIconHeightSmall; receiptMCCSize = variables.eReceiptMCCHeightWidthSmall; + labelFontSize = variables.fontSizeExtraSmall; + labelLineHeight = variables.lineHeightXSmall; } else if (iconSize === 'medium') { receiptIconWidth = variables.eReceiptIconWidthMedium; receiptIconHeight = variables.eReceiptIconHeightMedium; receiptMCCSize = variables.eReceiptMCCHeightWidthMedium; + labelFontSize = variables.fontSizeLabel; + labelLineHeight = variables.lineHeightNormal; } return ( @@ -77,6 +95,7 @@ function EReceiptThumbnail({transaction, centerIconV = true, iconSize = 'large'} styles.overflowHidden, styles.alignItemsCenter, centerIconV ? styles.justifyContentCenter : {}, + borderRadius ? {borderRadius} : {}, ]} > - {MCCIcon ? ( + {isReceiptThumbnail && fileExtension && ( + + {fileExtension.toUpperCase()} + + )} + {MCCIcon && !isReceiptThumbnail ? ( ({ transaction: { key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, }, })(EReceiptThumbnail); -export type {EReceiptThumbnailProps, EReceiptThumbnailOnyxProps}; +export type {IconSize, EReceiptThumbnailProps, EReceiptThumbnailOnyxProps}; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js index 7caab5e6fb55..c6f9f601f4df 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.js @@ -24,7 +24,12 @@ const useEmojiPickerMenu = () => { const [preferredSkinTone] = usePreferredEmojiSkinTone(); const {windowHeight} = useWindowDimensions(); const StyleUtils = useStyleUtils(); - const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight); + /** + * At EmojiPicker has set innerContainerStyle with maxHeight: '95%' by styles.popoverInnerContainer + * to avoid the list style to be cut off due to the list height being larger than the container height + * so we need to calculate listStyle based on the height of the window and innerContainerStyle at the EmojiPicker + */ + const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight * 0.95); useEffect(() => { setFilteredEmojis(allEmojis); diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 4550a7aef5d2..79e2c5b12a12 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -35,10 +35,10 @@ import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import ConfirmedRoute from './ConfirmedRoute'; import FormHelpMessage from './FormHelpMessage'; -import Image from './Image'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import OptionsSelector from './OptionsSelector'; import ReceiptEmptyState from './ReceiptEmptyState'; +import ReceiptImage from './ReceiptImage'; import SettlementButton from './SettlementButton'; import ShowMoreButton from './ShowMoreButton'; import Switch from './Switch'; @@ -577,8 +577,12 @@ function MoneyRequestConfirmationList({ ); }, [isReadOnly, iouType, bankAccountRoute, iouCurrencyCode, policyID, selectedParticipants.length, confirm, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); - const {image: receiptImage, thumbnail: receiptThumbnail} = - receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); + const { + image: receiptImage, + thumbnail: receiptThumbnail, + isThumbnail, + fileExtension, + } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); 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) )} - + {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {receiptImage || receiptThumbnail ? ( - ) : ( // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index f27cd507d668..513fdbfb1fd5 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -37,12 +37,12 @@ import ConfirmedRoute from './ConfirmedRoute'; import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; import * as Expensicons from './Icon/Expensicons'; -import Image from './Image'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import optionPropTypes from './optionPropTypes'; import OptionsSelector from './OptionsSelector'; import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; +import ReceiptImage from './ReceiptImage'; import SettlementButton from './SettlementButton'; import Switch from './Switch'; import tagPropTypes from './tagPropTypes'; @@ -897,6 +897,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const { image: receiptImage, thumbnail: receiptThumbnail, + isThumbnail, + fileExtension, isLocalFile, } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; @@ -911,16 +913,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ onPassword={() => setIsAttachmentInvalid(true)} /> ) : ( - ), - [receiptFilename, receiptImage, styles, receiptThumbnail, isLocalFile, isAttachmentInvalid], + [isLocalFile, receiptFilename, receiptImage, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, receiptThumbnail, fileExtension], ); return ( diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx new file mode 100644 index 000000000000..08892f11b021 --- /dev/null +++ b/src/components/ReceiptImage.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import {View} from 'react-native'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type IconAsset from '@src/types/utils/IconAsset'; +import EReceiptThumbnail from './EReceiptThumbnail'; +import type {IconSize} from './EReceiptThumbnail'; +import Image from './Image'; +import PDFThumbnail from './PDFThumbnail'; +import ThumbnailImage from './ThumbnailImage'; + +type Style = {height: number; borderRadius: number; margin: number}; + +type ReceiptImageProps = ( + | { + /** Transaction ID of the transaction the receipt belongs to */ + transactionID: string; + + /** Whether it is EReceipt */ + isEReceipt: boolean; + + /** Whether it is receipt preview thumbnail we are displaying */ + isThumbnail?: boolean; + + /** Url of the receipt image */ + source?: string; + + /** Whether it is a pdf thumbnail we are displaying */ + isPDFThumbnail?: boolean; + } + | { + transactionID?: string; + isEReceipt?: boolean; + isThumbnail: boolean; + source?: string; + isPDFThumbnail?: boolean; + } + | { + transactionID?: string; + isEReceipt?: boolean; + isThumbnail?: boolean; + source: string; + isPDFThumbnail?: boolean; + } + | { + transactionID?: string; + isEReceipt?: boolean; + isThumbnail?: boolean; + source: string; + isPDFThumbnail: string; + } +) & { + /** Whether we should display the receipt with ThumbnailImage component */ + shouldUseThumbnailImage?: boolean; + + /** Whether the receipt image requires an authToken */ + isAuthTokenRequired?: boolean; + + /** Any additional styles to apply */ + style?: Style; + + /** The file extension of the receipt file */ + fileExtension?: string; + + /** number of images displayed in the same parent container */ + iconSize?: IconSize; + + /** If the image fails to load – show the provided fallback icon */ + fallbackIcon?: IconAsset; + + /** The size of the fallback icon */ + fallbackIconSize?: number; +}; + +function ReceiptImage({ + transactionID, + isPDFThumbnail = false, + isThumbnail = false, + shouldUseThumbnailImage = false, + isEReceipt = false, + source, + isAuthTokenRequired, + style, + fileExtension, + iconSize, + fallbackIcon, + fallbackIconSize, +}: ReceiptImageProps) { + const styles = useThemeStyles(); + + if (isPDFThumbnail) { + return ( + + ); + } + + if (isEReceipt || isThumbnail) { + const props = isThumbnail && {borderRadius: style?.borderRadius, fileExtension, isReceiptThumbnail: true}; + return ( + + + + ); + } + + if (shouldUseThumbnailImage) { + return ( + + ); + } + + return ( + + ); +} + +export type {ReceiptImageProps}; +export default ReceiptImage; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index bb0308ee4509..5c382ca8ee33 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -259,6 +259,8 @@ function MoneyRequestView({ ) : ( { - if (thumbnail) { - return typeof thumbnail === 'string' ? {uri: thumbnail} : thumbnail; - } - - return typeof image === 'string' ? {uri: image} : image; - }, [image, thumbnail]); + let propsObj: ReceiptImageProps; if (isEReceipt) { - receiptImageComponent = ( - - - - ); + propsObj = {isEReceipt: true, transactionID: transaction.transactionID, iconSize: isSingleImage ? 'medium' : ('small' as IconSize)}; } else if (thumbnail && !isLocalFile) { - receiptImageComponent = ( - - ); + propsObj = { + shouldUseThumbnailImage: true, + source: thumbnailSource, + fallbackIcon: Expensicons.Receipt, + fallbackIconSize: isSingleImage ? variables.iconSizeSuperLarge : variables.iconSizeExtraLarge, + }; } else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') { - receiptImageComponent = ( - - ); + propsObj = {isPDFThumbnail: true, source: attachmentModalSource}; } else { - receiptImageComponent = ( - - ); + propsObj = { + isThumbnail, + ...(isThumbnail && {iconSize: (isSingleImage ? 'medium' : 'small') as IconSize, fileExtension}), + source: thumbnail ?? image ?? '', + }; } if (enablePreviewModal) { @@ -113,14 +102,14 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr accessibilityLabel={translate('accessibilityHints.viewAttachment')} accessibilityRole={CONST.ROLE.BUTTON} > - {receiptImageComponent} + )} ); } - return receiptImageComponent; + return ; } ReportActionItemImage.displayName = 'ReportActionItemImage'; diff --git a/src/components/ReportActionItem/ReportActionItemImages.tsx b/src/components/ReportActionItem/ReportActionItemImages.tsx index c66dc36d1ed5..ee8cb0849ca0 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.tsx +++ b/src/components/ReportActionItem/ReportActionItemImages.tsx @@ -65,21 +65,23 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report return ( - {shownImages.map(({thumbnail, image, transaction, isLocalFile, filename}, index) => { + {shownImages.map(({thumbnail, isThumbnail, image, transaction, isLocalFile, fileExtension, filename}, index) => { // Show a border to separate multiple images. Shown to the right for each except the last. const shouldShowBorder = shownImages.length > 1 && index < shownImages.length - 1; const borderStyle = shouldShowBorder ? styles.reportActionItemImageBorder : {}; return ( diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.tsx similarity index 53% rename from src/components/TagPicker/index.js rename to src/components/TagPicker/index.tsx index 341ea9cddae9..af8acd19e8c4 100644 --- a/src/components/TagPicker/index.js +++ b/src/components/TagPicker/index.tsx @@ -1,7 +1,7 @@ -import lodashGet from 'lodash/get'; import React, {useMemo, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {EdgeInsets} from 'react-native-safe-area-context'; import OptionsSelector from '@components/OptionsSelector'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -10,22 +10,64 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {defaultProps, propTypes} from './tagPickerPropTypes'; +import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; -function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption, insets, onSubmit}) { +type SelectedTagOption = { + name: string; + enabled: boolean; + accountID: number | null; +}; + +type TagPickerOnyxProps = { + /** Collection of tag list on a policy */ + policyTags: OnyxEntry; + + /** List of recently used tags */ + policyRecentlyUsedTags: OnyxEntry; +}; + +type TagPickerProps = TagPickerOnyxProps & { + /** The policyID we are getting tags for */ + // It's used in withOnyx HOC. + // eslint-disable-next-line react/no-unused-prop-types + policyID: string; + + /** The selected tag of the money request */ + selectedTag: string; + + /** The name of tag list we are getting tags for */ + tagListName: string; + + /** Callback to submit the selected tag */ + onSubmit: () => void; + + /** + * Safe area insets required for reflecting the portion of the view, + * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. + */ + insets: EdgeInsets; + + /** Should show the selected option that is disabled? */ + shouldShowDisabledAndSelectedOption?: boolean; + + /** Indicates which tag list index was selected */ + tagListIndex: number; +}; + +function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, insets, onSubmit}: TagPickerProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); - const policyRecentlyUsedTagsList = lodashGet(policyRecentlyUsedTags, tag, []); - const policyTagList = PolicyUtils.getTagList(policyTags, tagIndex); + const policyRecentlyUsedTagsList = useMemo(() => policyRecentlyUsedTags?.[tagListName] ?? [], [policyRecentlyUsedTags, tagListName]); + const policyTagList = PolicyUtils.getTagList(policyTags, tagListIndex); const policyTagsCount = PolicyUtils.getCountOfEnabledTagsOfList(policyTagList.tags); const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD; const shouldShowTextInput = !isTagsCountBelowThreshold; - const selectedOptions = useMemo(() => { + const selectedOptions: SelectedTagOption[] = useMemo(() => { if (!selectedTag) { return []; } @@ -39,13 +81,13 @@ function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTa ]; }, [selectedTag]); - const enabledTags = useMemo(() => { + const enabledTags: PolicyTags | Array = useMemo(() => { if (!shouldShowDisabledAndSelectedOption) { return policyTagList.tags; } - const selectedNames = _.map(selectedOptions, (s) => s.name); - const tags = [...selectedOptions, ..._.filter(policyTagList.tags, (policyTag) => policyTag.enabled && !selectedNames.includes(policyTag.name))]; - return tags; + const selectedNames = selectedOptions.map((s) => s.name); + + return [...selectedOptions, ...Object.values(policyTagList.tags).filter((policyTag) => policyTag.enabled && !selectedNames.includes(policyTag.name))]; }, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]); const sections = useMemo( @@ -53,12 +95,13 @@ function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTa [searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList], ); - const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(lodashGet(sections, '[0].data.length', 0) > 0, searchValue); + const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList((sections?.[0]?.data?.length ?? 0) > 0, searchValue); - const selectedOptionKey = lodashGet(_.filter(lodashGet(sections, '[0].data', []), (policyTag) => policyTag.searchText === selectedTag)[0], 'keyForList'); + const selectedOptionKey = sections[0]?.data?.filter((policyTag) => policyTag.searchText === selectedTag)?.[0]?.keyForList; return ( ({ policyTags: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, }, @@ -92,3 +133,5 @@ export default withOnyx({ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, }, })(TagPicker); + +export type {SelectedTagOption}; diff --git a/src/components/TagPicker/tagPickerPropTypes.js b/src/components/TagPicker/tagPickerPropTypes.js deleted file mode 100644 index cbdc73f5d056..000000000000 --- a/src/components/TagPicker/tagPickerPropTypes.js +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import tagPropTypes from '@components/tagPropTypes'; -import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; - -const propTypes = { - /** The policyID we are getting tags for */ - policyID: PropTypes.string.isRequired, - - /** The selected tag of the money request */ - selectedTag: PropTypes.string.isRequired, - - /** The name of tag list we are getting tags for */ - tag: PropTypes.string.isRequired, - - /** The index of a tag list */ - tagIndex: PropTypes.number.isRequired, - - /** Callback to submit the selected tag */ - onSubmit: PropTypes.func.isRequired, - - /** - * Safe area insets required for reflecting the portion of the view, - * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. - */ - insets: safeAreaInsetPropTypes.isRequired, - - /* Onyx Props */ - /** Collection of tags attached to a policy */ - policyTags: tagPropTypes, - - /** List of recently used tags */ - policyRecentlyUsedTags: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)), - - /** Should show the selected option that is disabled? */ - shouldShowDisabledAndSelectedOption: PropTypes.bool, -}; - -const defaultProps = { - policyTags: {}, - policyRecentlyUsedTags: {}, - shouldShowDisabledAndSelectedOption: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js index 341b3828eafb..91737ad3938a 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.js +++ b/src/components/VideoPlayer/BaseVideoPlayer.js @@ -9,6 +9,7 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import VideoPopoverMenu from '@components/VideoPopoverMenu'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -54,6 +55,7 @@ function BaseVideoPlayer({ videoResumeTryNumber, } = usePlaybackContext(); const {isFullScreenRef} = useFullScreenContext(); + const {isOffline} = useNetwork(); const [duration, setDuration] = useState(videoDuration * 1000); const [position, setPosition] = useState(0); const [isPlaying, setIsPlaying] = useState(false); @@ -253,13 +255,20 @@ function BaseVideoPlayer({ style={[styles.w100, styles.h100, videoPlayerStyle]} videoStyle={[styles.w100, styles.h100, videoStyle]} source={{ - uri: sourceURL, + // if video is loading and is offline, we want to change uri to null to + // reset the video player after connection is back + uri: !isLoading || (isLoading && !isOffline) ? sourceURL : null, }} shouldPlay={false} useNativeControls={false} resizeMode={resizeMode} isLooping={isLooping} - onReadyForDisplay={onVideoLoaded} + onReadyForDisplay={(e) => { + if (isCurrentlyURLSet && !isUploading) { + playVideo(); + } + onVideoLoaded(e); + }} onPlaybackStatusUpdate={handlePlaybackStatusUpdate} onFullscreenUpdate={handleFullscreenUpdate} /> diff --git a/src/hooks/useViewportOffsetTop/index.native.ts b/src/hooks/useViewportOffsetTop/index.native.ts new file mode 100644 index 000000000000..b166c360dacc --- /dev/null +++ b/src/hooks/useViewportOffsetTop/index.native.ts @@ -0,0 +1,7 @@ +/** + * Native doesn't support DOM so default value is 0 + */ + +export default function useViewportOffsetTop(): number { + return 0; +} diff --git a/src/hooks/useViewportOffsetTop/index.ts b/src/hooks/useViewportOffsetTop/index.ts new file mode 100644 index 000000000000..56fb19187c4f --- /dev/null +++ b/src/hooks/useViewportOffsetTop/index.ts @@ -0,0 +1,47 @@ +import {useEffect, useRef, useState} from 'react'; +import addViewportResizeListener from '@libs/VisualViewport'; + +/** + * A hook that returns the offset of the top edge of the visual viewport + */ +export default function useViewportOffsetTop(shouldAdjustScrollView = false): number { + const [viewportOffsetTop, setViewportOffsetTop] = useState(0); + const initialHeight = useRef(window.visualViewport?.height ?? window.innerHeight).current; + const cachedDefaultOffsetTop = useRef(0); + useEffect(() => { + const updateOffsetTop = (event?: Event) => { + let targetOffsetTop = window.visualViewport?.offsetTop ?? 0; + if (event?.target instanceof VisualViewport) { + targetOffsetTop = event.target.offsetTop; + } + + if (shouldAdjustScrollView && window.visualViewport) { + const adjustScrollY = Math.round(initialHeight - window.visualViewport.height); + if (cachedDefaultOffsetTop.current === 0) { + cachedDefaultOffsetTop.current = targetOffsetTop; + } + + if (adjustScrollY > targetOffsetTop) { + setViewportOffsetTop(adjustScrollY); + } else if (targetOffsetTop !== 0 && adjustScrollY === targetOffsetTop) { + setViewportOffsetTop(cachedDefaultOffsetTop.current); + } else { + setViewportOffsetTop(targetOffsetTop); + } + } else { + setViewportOffsetTop(targetOffsetTop); + } + }; + updateOffsetTop(); + return addViewportResizeListener(updateOffsetTop); + }, [initialHeight, shouldAdjustScrollView]); + + useEffect(() => { + if (!shouldAdjustScrollView) { + return; + } + window.scrollTo({top: viewportOffsetTop}); + }, [shouldAdjustScrollView, viewportOffsetTop]); + + return viewportOffsetTop; +} diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index 931b41524696..31faa803ec61 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -68,6 +68,7 @@ E2EClient.getTestConfig() return E2EClient.submitTestResults({ name: config.name, error: `Test '${config.name}' not found`, + critical: false, }); } diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts index 5185a75625a3..3cb33e3bbce3 100644 --- a/src/libs/E2E/types.ts +++ b/src/libs/E2E/types.ts @@ -39,6 +39,12 @@ type TestResult = { /** Optional, if set indicates that the test run failed and has no valid results. */ error?: string; + /** + * Whether error is critical. If `true`, then server will be stopped and `e2e` tests will fail. Otherwise will simply log a warning. + * Default value is `true` + */ + critical?: boolean; + /** Render count */ renderCount?: number; }; diff --git a/src/libs/Notification/PushNotification/index.native.ts b/src/libs/Notification/PushNotification/index.native.ts index 382591e5b698..4e028ad82392 100644 --- a/src/libs/Notification/PushNotification/index.native.ts +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -84,11 +84,6 @@ function refreshNotificationOptInStatus() { const init: Init = () => { // Setup event listeners Airship.addListener(EventType.PushReceived, (notification) => { - // By default, refresh notification opt-in status to true if we receive a notification - if (!isUserOptedInToPushNotifications) { - PushNotificationActions.setPushNotificationOptInStatus(true); - } - pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload); }); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 46e217ba20b1..372881b41a86 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -7,6 +7,7 @@ import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {SelectedTagOption} from '@components/TagPicker'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -19,6 +20,7 @@ import type { PolicyCategories, PolicyTag, PolicyTagList, + PolicyTags, Report, ReportAction, ReportActions, @@ -54,12 +56,6 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -type Tag = { - enabled: boolean; - name: string; - accountID: number | null; -}; - type Option = Partial; /** @@ -131,7 +127,7 @@ type GetOptionsConfig = { categories?: PolicyCategories; recentlyUsedCategories?: string[]; includeTags?: boolean; - tags?: Record; + tags?: PolicyTags | Array; recentlyUsedTags?: string[]; canInviteUser?: boolean; includeSelectedOptions?: boolean; @@ -914,7 +910,7 @@ function sortCategories(categories: Record): Category[] { /** * Sorts tags alphabetically by name. */ -function sortTags(tags: Record | Tag[]) { +function sortTags(tags: Record | Array) { let sortedTags; if (Array.isArray(tags)) { @@ -1095,7 +1091,7 @@ function getCategoryListSections( * * @param tags - an initial tag array */ -function getTagsOptions(tags: Category[]): Option[] { +function getTagsOptions(tags: Array>): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); @@ -1112,7 +1108,13 @@ function getTagsOptions(tags: Category[]): Option[] { /** * Build the section list for tags */ -function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOptions: Category[], searchInputValue: string, maxRecentReportsToShow: number) { +function getTagListSections( + tags: Array, + recentlyUsedTags: string[], + selectedOptions: SelectedTagOption[], + searchInputValue: string, + maxRecentReportsToShow: number, +) { const tagSections = []; const sortedTags = sortTags(tags); const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); @@ -1424,7 +1426,7 @@ function getOptions( } if (includeTags) { - const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); + const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as SelectedTagOption[], searchInputValue, maxRecentReportsToShow); return { recentReports: [], @@ -1851,7 +1853,7 @@ function getFilteredOptions( categories: PolicyCategories = {}, recentlyUsedCategories: string[] = [], includeTags = false, - tags: Record = {}, + tags: PolicyTags | Array = {}, recentlyUsedTags: string[] = [], canInviteUser = true, includeSelectedOptions = false, diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 75d14be1a907..cf8937874216 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -1,11 +1,6 @@ import Str from 'expensify-common/lib/str'; import _ from 'lodash'; -import type {ImageSourcePropType} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import ReceiptDoc from '@assets/images/receipt-doc.png'; -import ReceiptGeneric from '@assets/images/receipt-generic.png'; -import ReceiptHTML from '@assets/images/receipt-html.png'; -import ReceiptSVG from '@assets/images/receipt-svg.png'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; @@ -14,16 +9,13 @@ import * as FileUtils from './fileDownload/FileUtils'; import * as TransactionUtils from './TransactionUtils'; type ThumbnailAndImageURI = { - image: ImageSourcePropType | string; - thumbnail: ImageSourcePropType | string | null; - transaction?: Transaction; + image?: string; + thumbnail?: string; + transaction?: OnyxEntry; isLocalFile?: boolean; + isThumbnail?: boolean; filename?: string; -}; - -type FileNameAndExtension = { fileExtension?: string; - fileName?: string; }; /** @@ -35,11 +27,11 @@ type FileNameAndExtension = { */ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) { - return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; + return {isThumbnail: true, isLocalFile: true}; } - // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg // If there're errors, we need to display them in preview. We can store many files in errors, but we just need to get the last one const errors = _.findLast(transaction?.errors) as ReceiptError | undefined; + // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg const path = errors?.source ?? transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI const filename = errors?.filename ?? transaction?.filename ?? receiptFileName ?? ''; @@ -48,12 +40,12 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa const isReceiptPDF = Str.isPDF(filename); if (hasEReceipt) { - return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction, filename}; + return {image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction, filename}; } // For local files, we won't have a thumbnail yet if ((isReceiptImage || isReceiptPDF) && typeof path === 'string' && (path.startsWith('blob:') || path.startsWith('file:'))) { - return {thumbnail: null, image: path, isLocalFile: true, filename}; + return {image: path, isLocalFile: true, filename}; } if (isReceiptImage) { @@ -64,22 +56,9 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa return {thumbnail: `${path.substring(0, path.length - 4)}.jpg.1024.jpg`, image: path, filename}; } - const {fileExtension} = FileUtils.splitExtensionFromFileName(filename) as FileNameAndExtension; - let image = ReceiptGeneric; - if (fileExtension === CONST.IOU.FILE_TYPES.HTML) { - image = ReceiptHTML; - } - - if (fileExtension === CONST.IOU.FILE_TYPES.DOC || fileExtension === CONST.IOU.FILE_TYPES.DOCX) { - image = ReceiptDoc; - } - - if (fileExtension === CONST.IOU.FILE_TYPES.SVG) { - image = ReceiptSVG; - } - const isLocalFile = typeof path === 'number' || path.startsWith('blob:') || path.startsWith('file:') || path.startsWith('/'); - return {thumbnail: image, image: path, isLocalFile, filename}; + const {fileExtension} = FileUtils.splitExtensionFromFileName(filename); + return {isThumbnail: true, fileExtension: Object.values(CONST.IOU.FILE_TYPES).find((type) => type === fileExtension), image: path, isLocalFile, filename}; } // eslint-disable-next-line import/prefer-default-export diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 64bac4f46b5a..11f870b891b6 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -263,9 +263,14 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? let index; if (id) { - index = sortedReportActions.findIndex((obj) => obj.reportActionID === id); + index = sortedReportActions.findIndex((reportAction) => reportAction.reportActionID === id); } else { - index = sortedReportActions.findIndex((obj) => obj.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + index = sortedReportActions.findIndex( + (reportAction) => + reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && + reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && + !reportAction.isOptimisticAction, + ); } if (index === -1) { @@ -299,6 +304,9 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? while ( (startIndex > 0 && sortedReportActions[startIndex].reportActionID === sortedReportActions[startIndex - 1].previousReportActionID) || sortedReportActions[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD || + sortedReportActions[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + sortedReportActions[startIndex - 1]?.isOptimisticAction || sortedReportActions[startIndex - 1]?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM ) { startIndex--; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index ae3bf0cdbd68..b67e44fcf5ad 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4396,15 +4396,6 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT ]; const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, - value: { - [optimisticIOUReportAction.reportActionID]: { - pendingAction: null, - }, - }, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, @@ -4634,7 +4625,7 @@ function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, value: { - [expenseReport.reportActionID ?? '']: { + [optimisticApprovedReportAction.reportActionID]: { errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), }, }, diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 3efba3e54dcd..45fc139b6742 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -342,6 +342,7 @@ function deleteWorkspace(policyID: string, policyName: string) { const reportsToArchive = Object.values(allReports ?? {}).filter( (report) => report?.policyID === policyID && (ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report)), ); + const finallyData: OnyxUpdate[] = []; reportsToArchive.forEach((report) => { const {reportID, ownerAccountID} = report ?? {}; optimisticData.push({ @@ -369,12 +370,22 @@ function deleteWorkspace(policyID: string, policyName: string) { emailClosingReport = allPersonalDetails?.[ownerAccountID]?.login ?? ''; } const optimisticClosedReportAction = ReportUtils.buildOptimisticClosedReportAction(emailClosingReport, policyName, CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED); - const optimisticReportActions: Record = {}; - optimisticReportActions[optimisticClosedReportAction.reportActionID] = optimisticClosedReportAction as ReportAction; optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: optimisticReportActions, + value: { + [optimisticClosedReportAction.reportActionID]: optimisticClosedReportAction as ReportAction, + }, + }); + + // We are temporarily adding this workaround because 'DeleteWorkspace' doesn't + // support receiving the optimistic reportActions' ids for the moment. + finallyData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [optimisticClosedReportAction.reportActionID]: null, + }, }); }); @@ -406,7 +417,7 @@ function deleteWorkspace(policyID: string, policyName: string) { const params: DeleteWorkspaceParams = {policyID}; - API.write(WRITE_COMMANDS.DELETE_WORKSPACE, params, {optimisticData, failureData}); + API.write(WRITE_COMMANDS.DELETE_WORKSPACE, params, {optimisticData, finallyData, failureData}); // Reset the lastAccessedWorkspacePolicyID if (policyID === lastAccessedWorkspacePolicyID) { diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 7d10e0e55e79..eecffd81d88b 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -37,7 +37,7 @@ const propTypes = { /** reportID for the "transaction thread" */ threadReportID: PropTypes.string, - /** The index of a tag list */ + /** Indicates which tag list index was selected */ tagIndex: PropTypes.string, }), }).isRequired, @@ -78,10 +78,10 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency; const fieldToEdit = lodashGet(route, ['params', 'field'], ''); - const tagIndex = Number(lodashGet(route, ['params', 'tagIndex'], undefined)); + const tagListIndex = Number(lodashGet(route, ['params', 'tagIndex'], undefined)); - const tag = TransactionUtils.getTag(transaction, tagIndex); - const policyTagListName = PolicyUtils.getTagListName(policyTags, tagIndex); + const tag = TransactionUtils.getTag(transaction, tagListIndex); + const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex); const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); // A flag for verifying that the current report is a sub-report of a workspace chat @@ -129,14 +129,14 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p IOU.updateMoneyRequestTag( transaction.transactionID, report.reportID, - IOUUtils.insertTagIntoTransactionTagsString(transactionTag, updatedTag, tagIndex), + IOUUtils.insertTagIntoTransactionTagsString(transactionTag, updatedTag, tagListIndex), policy, policyTags, policyCategories, ); Navigation.dismissModal(); }, - [tag, transaction.transactionID, report.reportID, transactionTag, tagIndex, policy, policyTags, policyCategories], + [tag, transaction.transactionID, report.reportID, transactionTag, tagListIndex, policy, policyTags, policyCategories], ); if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT) { @@ -159,7 +159,7 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p diff --git a/src/pages/EditRequestTagPage.js b/src/pages/EditRequestTagPage.js index b64cb925a213..1aead9ee1f6e 100644 --- a/src/pages/EditRequestTagPage.js +++ b/src/pages/EditRequestTagPage.js @@ -15,21 +15,21 @@ const propTypes = { /** The policyID we are getting tags for */ policyID: PropTypes.string.isRequired, - /** The tag name to which the default tag belongs to */ - tagName: PropTypes.string, + /** The tag list name to which the default tag belongs to */ + tagListName: PropTypes.string, - /** The index of a tag list */ - tagIndex: PropTypes.number.isRequired, + /** Indicates which tag list index was selected */ + tagListIndex: PropTypes.number.isRequired, /** Callback to fire when the Save button is pressed */ onSubmit: PropTypes.func.isRequired, }; const defaultProps = { - tagName: '', + tagListName: '', }; -function EditRequestTagPage({defaultTag, policyID, tagName, tagIndex, onSubmit}) { +function EditRequestTagPage({defaultTag, policyID, tagListName, tagListIndex, onSubmit}) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -46,14 +46,14 @@ function EditRequestTagPage({defaultTag, policyID, tagName, tagIndex, onSubmit}) {({insets}) => ( <> {translate('iou.tagSelection')} { setDraftSplitTransaction({tag: transactionChanges.tag.trim()}); }} diff --git a/src/pages/TransactionReceiptPage.tsx b/src/pages/TransactionReceiptPage.tsx index 8db9e05a5139..f6f2c90e5d2c 100644 --- a/src/pages/TransactionReceiptPage.tsx +++ b/src/pages/TransactionReceiptPage.tsx @@ -28,7 +28,7 @@ type TransactionReceiptProps = TransactionReceiptOnyxProps & StackScreenProps; + /** Tells us if the sidebar has rendered */ isSidebarLoaded: OnyxEntry; @@ -93,7 +96,7 @@ type OnyxHOCProps = { type ReportScreenNavigationProps = StackScreenProps; -type ReportScreenProps = OnyxHOCProps & ViewportOffsetTopProps & CurrentReportIDContextValue & ReportScreenOnyxProps & ReportScreenNavigationProps; +type ReportScreenProps = OnyxHOCProps & CurrentReportIDContextValue & ReportScreenOnyxProps & ReportScreenNavigationProps; /** Get the currently viewed report ID as number */ function getReportID(route: ReportScreenNavigationProps['route']): string { @@ -131,7 +134,7 @@ function ReportScreen({ markReadyForHydration, policies = {}, isSidebarLoaded = false, - viewportOffsetTop, + modal, isComposerFullSize = false, userLeavingStatus = false, currentReportID = '', @@ -259,6 +262,8 @@ function ReportScreen({ Timing.start(CONST.TIMING.CHAT_RENDER); Performance.markStart(CONST.TIMING.CHAT_RENDER); } + const [isComposerFocus, setIsComposerFocus] = useState(false); + const viewportOffsetTop = useViewportOffsetTop(Browser.isMobileSafari() && isComposerFocus && !modal?.willAlertModalBecomeVisible); const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -644,6 +649,8 @@ function ReportScreen({ {isCurrentReportLoadedFromOnyx ? ( setIsComposerFocus(true)} + onComposerBlur={() => setIsComposerFocus(false)} report={report} pendingAction={reportPendingAction} isComposerFullSize={!!isComposerFullSize} @@ -663,81 +670,82 @@ function ReportScreen({ ReportScreen.displayName = 'ReportScreen'; -export default withViewportOffsetTop( - withCurrentReportID( - withOnyx( - { - isSidebarLoaded: { - key: ONYXKEYS.IS_SIDEBAR_LOADED, - }, - sortedAllReportActions: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, - canEvict: false, - selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), - }, - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, - allowStaleData: true, - selector: reportWithoutHasDraftSelector, - }, - reportMetadata: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`, - initialValue: { - isLoadingInitialReportActions: true, - isLoadingOlderReportActions: false, - isLoadingNewerReportActions: false, - }, - }, - isComposerFullSize: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`, - initialValue: false, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - allowStaleData: true, - }, - accountManagerReportID: { - key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, - initialValue: null, - }, - userLeavingStatus: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, - initialValue: false, +export default withCurrentReportID( + withOnyx( + { + modal: { + key: ONYXKEYS.MODAL, + }, + isSidebarLoaded: { + key: ONYXKEYS.IS_SIDEBAR_LOADED, + }, + sortedAllReportActions: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + canEvict: false, + selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), + }, + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, + allowStaleData: true, + selector: reportWithoutHasDraftSelector, + }, + reportMetadata: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_METADATA}${getReportID(route)}`, + initialValue: { + isLoadingInitialReportActions: true, + isLoadingOlderReportActions: false, + isLoadingNewerReportActions: false, }, - parentReportAction: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, - selector: (parentReportActions: OnyxEntry, props: WithOnyxInstanceState): OnyxEntry => { - const parentReportActionID = props?.report?.parentReportActionID; - if (!parentReportActionID) { - return null; - } - return parentReportActions?.[parentReportActionID] ?? null; - }, - canEvict: false, + }, + isComposerFullSize: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${getReportID(route)}`, + initialValue: false, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + allowStaleData: true, + }, + accountManagerReportID: { + key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, + initialValue: null, + }, + userLeavingStatus: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, + initialValue: false, + }, + parentReportAction: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, + selector: (parentReportActions: OnyxEntry, props: WithOnyxInstanceState): OnyxEntry => { + const parentReportActionID = props?.report?.parentReportActionID; + if (!parentReportActionID) { + return null; + } + return parentReportActions?.[parentReportActionID] ?? null; }, + canEvict: false, }, - true, - )( - memo( - ReportScreen, - (prevProps, nextProps) => - prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && - lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) && - lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && - prevProps.isComposerFullSize === nextProps.isComposerFullSize && - lodashIsEqual(prevProps.betas, nextProps.betas) && - lodashIsEqual(prevProps.policies, nextProps.policies) && - prevProps.accountManagerReportID === nextProps.accountManagerReportID && - prevProps.userLeavingStatus === nextProps.userLeavingStatus && - prevProps.currentReportID === nextProps.currentReportID && - prevProps.viewportOffsetTop === nextProps.viewportOffsetTop && - lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) && - lodashIsEqual(prevProps.route, nextProps.route) && - lodashIsEqual(prevProps.report, nextProps.report), - ), + }, + true, + )( + memo( + ReportScreen, + (prevProps, nextProps) => + prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && + lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) && + lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && + prevProps.isComposerFullSize === nextProps.isComposerFullSize && + lodashIsEqual(prevProps.betas, nextProps.betas) && + lodashIsEqual(prevProps.policies, nextProps.policies) && + prevProps.accountManagerReportID === nextProps.accountManagerReportID && + prevProps.userLeavingStatus === nextProps.userLeavingStatus && + prevProps.currentReportID === nextProps.currentReportID && + lodashIsEqual(prevProps.modal, nextProps.modal) && + lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) && + lodashIsEqual(prevProps.route, nextProps.route) && + lodashIsEqual(prevProps.report, nextProps.report), ), ), ); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 1e0e322be258..70340e9e1fec 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -85,6 +85,12 @@ type ReportActionComposeProps = ReportActionComposeOnyxProps & /** Whether the report is ready for display */ isReportReadyForDisplay?: boolean; + + /** A method to call when the input is focus */ + onComposerFocus?: () => void; + + /** A method to call when the input is blur */ + onComposerBlur?: () => void; }; // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will @@ -107,6 +113,8 @@ function ReportActionCompose({ isReportReadyForDisplay = true, isEmptyChat, lastReportAction, + onComposerFocus, + onComposerBlur, }: ReportActionComposeProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -293,20 +301,25 @@ function ReportActionCompose({ isKeyboardVisibleWhenShowingModalRef.current = true; }, []); - const onBlur = useCallback((event: NativeSyntheticEvent) => { - const webEvent = event as unknown as FocusEvent; - setIsFocused(false); - if (suggestionsRef.current) { - suggestionsRef.current.resetSuggestions(); - } - if (webEvent.relatedTarget && webEvent.relatedTarget === actionButtonRef.current) { - isKeyboardVisibleWhenShowingModalRef.current = true; - } - }, []); + const onBlur = useCallback( + (event: NativeSyntheticEvent) => { + const webEvent = event as unknown as FocusEvent; + setIsFocused(false); + onComposerBlur?.(); + if (suggestionsRef.current) { + suggestionsRef.current.resetSuggestions(); + } + if (webEvent.relatedTarget && webEvent.relatedTarget === actionButtonRef.current) { + isKeyboardVisibleWhenShowingModalRef.current = true; + } + }, + [onComposerBlur], + ); const onFocus = useCallback(() => { setIsFocused(true); - }, []); + onComposerFocus?.(); + }, [onComposerFocus]); // resets the composer to normal size when // the send button is pressed. diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 3a787e1dbd0f..bd143f9ef196 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -51,6 +51,12 @@ type ReportFooterProps = ReportFooterOnyxProps & { /** Whether the composer is in full size */ isComposerFullSize?: boolean; + + /** A method to call when the input is focus */ + onComposerFocus: () => void; + + /** A method to call when the input is blur */ + onComposerBlur: () => void; }; function ReportFooter({ @@ -63,6 +69,8 @@ function ReportFooter({ isReportReadyForDisplay = true, listHeight = 0, isComposerFullSize = false, + onComposerBlur, + onComposerFocus, }: ReportFooterProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); @@ -137,6 +145,8 @@ function ReportFooter({ SidebarUtils.getOrderedReportIDs(null, chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs), + [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs], + ); + const optionListItems = useMemo(() => { - const reportIDs = SidebarUtils.getOrderedReportIDs( - null, - chatReports, - betas, - policies, - priorityMode, - allReportActions, - transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - ); + const reportIDs = optionItemsMemoized; if (deepEqual(reportIDsRef.current, reportIDs)) { return reportIDsRef.current; @@ -160,7 +156,7 @@ function SidebarLinksData({ reportIDsRef.current = reportIDs; } return reportIDsRef.current || []; - }, [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, isLoading, network.isOffline, prevPriorityMode]); + }, [optionItemsMemoized, priorityMode, isLoading, network.isOffline, prevPriorityMode]); // We need to make sure the current report is in the list of reports, but we do not want // to have to re-generate the list every time the currentReportID changes. To do that @@ -334,4 +330,27 @@ export default compose( initialValue: {}, }, }), -)(SidebarLinksData); +)( + /* + While working on audit on the App Start App metric we noticed that by memoizing SidebarLinksData we can avoid 2 additional run of getOrderedReportIDs. + With that we can reduce app start up time by ~2s on heavy account. + More details - https://github.com/Expensify/App/issues/35234#issuecomment-1926914534 + */ + memo( + SidebarLinksData, + (prevProps, nextProps) => + _.isEqual(prevProps.chatReports, nextProps.chatReports) && + _.isEqual(prevProps.allReportActions, nextProps.allReportActions) && + prevProps.isLoadingApp === nextProps.isLoadingApp && + prevProps.priorityMode === nextProps.priorityMode && + _.isEqual(prevProps.betas, nextProps.betas) && + _.isEqual(prevProps.policies, nextProps.policies) && + prevProps.network.isOffline === nextProps.network.isOffline && + _.isEqual(prevProps.insets, nextProps.insets) && + prevProps.onLinkClick === nextProps.onLinkClick && + _.isEqual(prevProps.policyMembers, nextProps.policyMembers) && + _.isEqual(prevProps.transactionViolations, nextProps.transactionViolations) && + _.isEqual(prevProps.currentUserPersonalDetails, nextProps.currentUserPersonalDetails) && + prevProps.currentReportID === nextProps.currentReportID, + ), +); diff --git a/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js b/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js index dbcf83bda62a..8b191fa0b58e 100644 --- a/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js +++ b/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js @@ -22,5 +22,8 @@ export default PropTypes.shape({ /** A path to go to when the user presses the back button */ backTo: PropTypes.string, + + /** Indicates which tag list index was selected */ + tagIndex: PropTypes.string, }), }); diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js index 79ed26b76b19..ed55628ecaa9 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.js +++ b/src/pages/iou/request/step/IOURequestStepTag.js @@ -91,15 +91,15 @@ function IOURequestStepTag({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const tagIndex = Number(rawTagIndex); - const policyTagListName = PolicyUtils.getTagListName(policyTags, tagIndex); + const tagListIndex = Number(rawTagIndex); + const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex); const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const isEditingSplitBill = isEditing && isSplitBill; const currentTransaction = isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction; const transactionTag = TransactionUtils.getTag(currentTransaction); - const tag = TransactionUtils.getTag(currentTransaction, tagIndex); + const tag = TransactionUtils.getTag(currentTransaction, tagListIndex); const reportAction = reportActions[report.parentReportActionID || reportActionID]; const canEditSplitBill = isSplitBill && reportAction && session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); @@ -119,7 +119,7 @@ function IOURequestStepTag({ */ const updateTag = (selectedTag) => { const isSelectedTag = selectedTag.searchText === tag; - const updatedTag = IOUUtils.insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : selectedTag.searchText, tagIndex); + const updatedTag = IOUUtils.insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : selectedTag.searchText, tagListIndex); if (isEditingSplitBill) { IOU.setDraftSplitTransaction(transactionID, {tag: updatedTag}); navigateBack(); @@ -147,8 +147,8 @@ function IOURequestStepTag({ {translate('iou.tagSelection')} ) => void; + + /** Function to validate the edited values of the form */ + validateEdit?: (values: FormOnyxValues) => FormInputErrors; }; -function CategoryForm({onSubmit, policyCategories, categoryName}: CategoryFormProps) { +function CategoryForm({onSubmit, policyCategories, categoryName, validateEdit}: CategoryFormProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); @@ -67,7 +70,8 @@ function CategoryForm({onSubmit, policyCategories, categoryName}: CategoryFormPr formID={ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM} onSubmit={submit} submitButtonText={translate('common.save')} - validate={validate} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + validate={validateEdit || validate} style={[styles.mh5, styles.flex1]} enabledWhenOffline > diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx index 0e5ed0589934..dbf7c8913515 100644 --- a/src/pages/workspace/categories/EditCategoryPage.tsx +++ b/src/pages/workspace/categories/EditCategoryPage.tsx @@ -2,7 +2,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import type {FormOnyxValues} from '@components/Form/types'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; @@ -32,9 +32,29 @@ function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) { const {translate} = useLocalize(); const currentCategoryName = route.params.categoryName; + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + const newCategoryName = values.categoryName.trim(); + + if (!newCategoryName) { + errors.categoryName = 'workspace.categories.categoryRequiredError'; + } else if (policyCategories?.[newCategoryName] && currentCategoryName !== newCategoryName) { + errors.categoryName = 'workspace.categories.existingCategoryError'; + } + + return errors; + }, + [policyCategories, currentCategoryName], + ); + const editCategory = useCallback( (values: FormOnyxValues) => { - Policy.renamePolicyCategory(route.params.policyID, {oldName: currentCategoryName, newName: values.categoryName}); + const newCategoryName = values.categoryName.trim(); + // Do not call the API if the edited category name is the same as the current category name + if (currentCategoryName !== newCategoryName) { + Policy.renamePolicyCategory(route.params.policyID, {oldName: currentCategoryName, newName: values.categoryName}); + } }, [currentCategoryName, route.params.policyID], ); @@ -58,6 +78,7 @@ function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) { /> diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index 92d7c0a11ac9..fbde96eb2b91 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -57,7 +57,11 @@ function EditTagPage({route, policyTags}: EditTagPageProps) { const editTag = useCallback( (values: FormOnyxValues) => { - Policy.renamePolicyTag(route.params.policyID, {oldName: currentTagName, newName: values.tagName.trim()}); + const tagName = values.tagName.trim(); + // Do not call the API if the edited tag name is the same as the current tag name + if (currentTagName !== tagName) { + Policy.renamePolicyTag(route.params.policyID, {oldName: currentTagName, newName: values.tagName.trim()}); + } Keyboard.dismiss(); Navigation.dismissModal(); }, diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index fa4c19539072..152e023b7d94 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -288,6 +288,20 @@ function getEReceiptColorCode(transaction: OnyxEntry): EReceiptColo return eReceiptColors[colorHash]; } +/** + * Helper method to return eReceipt color code for Receipt Thumbnails + */ +function getFileExtensionColorCode(fileExtension?: string): EReceiptColorName { + switch (fileExtension) { + case CONST.IOU.FILE_TYPES.DOC: + return CONST.ERECEIPT_COLORS.PINK; + case CONST.IOU.FILE_TYPES.HTML: + return CONST.ERECEIPT_COLORS.TANGERINE; + default: + return CONST.ERECEIPT_COLORS.GREEN; + } +} + /** * Helper method to return eReceipt color styles */ @@ -1139,6 +1153,7 @@ const staticStyleUtils = { parseStyleFromFunction, getEReceiptColorStyles, getEReceiptColorCode, + getFileExtensionColorCode, getNavigationModalCardStyle, getCardStyles, getOpacityStyle, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index d911a2fc4b0e..50dc4cbd34fa 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -169,7 +169,7 @@ export default { addBankAccountLeftSpacing: 3, eReceiptThumbnailSmallBreakpoint: 110, eReceiptThumbnailMediumBreakpoint: 335, - eReceiptThumnailCenterReceiptBreakpoint: 200, + eReceiptThumbnailCenterReceiptBreakpoint: 200, eReceiptIconHeight: 100, eReceiptIconWidth: 72, eReceiptEmptyIconWidth: 76, diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts index 8066b85d1e44..f469ac7fff70 100644 --- a/src/types/onyx/PolicyTag.ts +++ b/src/types/onyx/PolicyTag.ts @@ -26,7 +26,7 @@ type PolicyTagList = Record< /** Flag that determines if tags are required */ required: boolean; - /** Nested tags */ + /** List of tags */ tags: PolicyTags; /** Index by which the tag appears in the hierarchy of tags */ diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 1a7541955720..7bb83506035b 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -57,7 +57,7 @@ type Geometry = { type?: GeometryType; }; -type ReceiptSource = string | number; +type ReceiptSource = string; type Receipt = { receiptID?: number; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 33cdefb749ec..c71a88809302 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -1584,7 +1584,6 @@ describe('actions/IOU', () => { reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY, ) ?? null; expect(payIOUAction).toBeTruthy(); - expect(payIOUAction?.pendingAction).toBeFalsy(); resolve(); }, diff --git a/tests/e2e/testRunner.ts b/tests/e2e/testRunner.ts index 5edc8c068229..8d898321309e 100644 --- a/tests/e2e/testRunner.ts +++ b/tests/e2e/testRunner.ts @@ -93,9 +93,14 @@ const runTests = async (): Promise => { // Collect results while tests are being executed server.addTestResultListener((testResult) => { - if (testResult?.error != null) { + const {critical = true} = testResult; + + if (testResult?.error != null && critical) { throw new Error(`Test '${testResult.name}' failed with error: ${testResult.error}`); } + if (testResult?.error != null && !critical) { + Logger.warn(`Test '${testResult.name}' failed with error: ${testResult.error}`); + } let result = 0; if (testResult?.duration !== undefined) {