diff --git a/.github/libs/sanitizeStringForJSONParse.ts b/.github/libs/sanitizeStringForJSONParse.ts index 3fbdaa8661f8..ddb7549b0186 100644 --- a/.github/libs/sanitizeStringForJSONParse.ts +++ b/.github/libs/sanitizeStringForJSONParse.ts @@ -16,7 +16,7 @@ const replacer = (str: string): string => * Solution partly taken from SO user Gabriel Rodríguez Flores 🙇 * https://stackoverflow.com/questions/52789718/how-to-remove-special-characters-before-json-parse-while-file-reading */ -const sanitizeStringForJSONParse = (inputString: string): string => { +const sanitizeStringForJSONParse = (inputString: string | number | boolean | null | undefined): string => { if (typeof inputString !== 'string') { throw new TypeError('Input must me of type String'); } diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index 9a6680ba0b6e..0b7dda4621ad 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -3,9 +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'; export {useIsFocused, useTheme}; diff --git a/android/app/build.gradle b/android/app/build.gradle index abdc6765ec1a..9e5a237afd1e 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 1001047400 - versionName "1.4.74-0" + versionCode 1001047404 + versionName "1.4.74-4" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md index c5578249289a..239da6518be7 100644 --- a/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md +++ b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md @@ -26,13 +26,13 @@ After adopting the new Expensify Card, domain admins can issue virtual cards to **To assign a virtual card:** -1. Head to **Settings** > **Domains** > [**Company Cards**](https://www.expensify.com/domain_companycards). -2. Click the **Issue Virtual Cards** button. -3. Enter a card name (i.e., "Google Ads"). -4. Select a domain member to assign the card to. -5. Enter a card limit. -6. Select a **Limit Type** of _Fixed_ or _Monthly_. -7. Click **Issue Card**. +Head to **Settings** > **Domains** > [**Company Cards**](https://www.expensify.com/domain_companycards) and click the **Issue Virtual Cards** button. From there: + +1. Enter a card name (i.e., "Google Ads"). +2. Select a domain member to assign the card to. +3. Enter a card limit. +4. Select a **Limit Type** of _Fixed_ or _Monthly_. +5. Click **Issue Card**. ![The Issue Virtual Cards modal is open in the middle of the screen. There are four options to set; Card Name, Assignee, Card Limit, and Limit type. A cancel (left) and save (right) button are at the bottom right of the modal.]({{site.url}}/assets/images/AdminissuedVirtualCards.png){:width="100%"} diff --git a/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md b/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md index a1b1043dff47..424c8dc9d107 100644 --- a/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md +++ b/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md @@ -16,9 +16,9 @@ The sky's the limit for this referral program! Your referral can be anyone - a f 1. There are a bunch of different ways to refer someone to New Expensify: - Start a chat - - Request money - - Send money - - Split a bill + - Submit an expense to them + - Split an expense with them + - Pay someone (them) - Assign them a task - @ mention them - Invite them to a room diff --git a/docs/redirects.csv b/docs/redirects.csv index c79c07ee3cf4..74fa9c697f5f 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -172,3 +172,4 @@ https://help.expensify.com/articles/new-expensify/getting-started/Free-plan-upgr https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account,https://help.expensify.com/new-expensify/hubs/expenses/Connect-a-Bank-Account https://help.expensify.com/articles/new-expensify/settings/Profile,https://help.expensify.com/new-expensify/hubs/settings/ https://help.expensify.com/articles/new-expensify/expenses/Referral-Program.html,https://help.expensify.com/articles/expensify-classic/expensify-partner-program/Referral-Program +https://help.expensify.com/articles/expensify-classic/workspaces/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index e318f45b4a6f..9c9e78ef70b0 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.74.0 + 1.4.74.4 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 065ef2041e6e..627c6dbf826c 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.74.0 + 1.4.74.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index afe45f0e2591..477f83aa6d00 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.74 CFBundleVersion - 1.4.74.0 + 1.4.74.4 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index d4169447b963..a46198daa88a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.74-0", + "version": "1.4.74-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.74-0", + "version": "1.4.74-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6a61cffc24a5..1a449f42bece 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.74-0", + "version": "1.4.74-4", "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/CONST.ts b/src/CONST.ts index c7bdbb2b35e7..a983a18e3d6a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3568,7 +3568,7 @@ const CONST = { TRACK_EXPENSE: 'track-expenses', }, 'track-expenses': { - VIDEO_URL: `${CLOUDFRONT_URL}/videos/guided-setup-track-business.mp4`, + VIDEO_URL: `${CLOUDFRONT_URL}/videos/guided-setup-track-business-v2.mp4`, LEARN_MORE_LINK: `${USE_EXPENSIFY_URL}/track-expenses`, }, }, @@ -3781,7 +3781,7 @@ const CONST = { [onboardingChoices.EMPLOYER]: { message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-get-paid-back.mp4`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-get-paid-back-v2.mp4`, thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-get-paid-back.jpg`, duration: 55, width: 1280, @@ -3824,7 +3824,7 @@ const CONST = { [onboardingChoices.MANAGE_TEAM]: { message: 'Here are some important tasks to help get your team’s expenses under control.', video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-manage-team.mp4`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-manage-team-v2.mp4`, thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-manage-team.jpg`, duration: 55, width: 1280, @@ -3910,7 +3910,7 @@ const CONST = { [onboardingChoices.PERSONAL_SPEND]: { message: 'Here’s how to track your spend in a few clicks.', video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-track-personal.mp4`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-track-personal-v2.mp4`, thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-track-personal.jpg`, duration: 55, width: 1280, @@ -3938,7 +3938,7 @@ const CONST = { [onboardingChoices.CHAT_SPLIT]: { message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-chat-split-bills.mp4`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-chat-split-bills-v2.mp4`, thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-chat-split-bills.jpg`, duration: 55, width: 1280, diff --git a/src/Expensify.tsx b/src/Expensify.tsx index e91a5223d7b6..6205afb9c03c 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -76,7 +76,9 @@ type ExpensifyOnyxProps = { type ExpensifyProps = ExpensifyOnyxProps; -const SplashScreenHiddenContext = React.createContext({}); +type SplashScreenHiddenContextType = {isSplashHidden?: boolean}; + +const SplashScreenHiddenContext = React.createContext({}); function Expensify({ isCheckingPublicRoom = true, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0405edbd4189..ddf37fba2354 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -282,6 +282,9 @@ const ONYXKEYS = { /** Onboarding Purpose selected by the user during Onboarding flow */ ONBOARDING_PURPOSE_SELECTED: 'onboardingPurposeSelected', + /** Onboarding policyID selected by the user during Onboarding flow */ + ONBOARDING_POLICY_ID: 'onboardingPolicyID', + /** Onboarding Purpose selected by the user during Onboarding flow */ ONBOARDING_ADMINS_CHAT_REPORT_ID: 'onboardingAdminsChatReportID', @@ -664,6 +667,7 @@ type OnyxValuesMapping = { [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; [ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string; + [ONYXKEYS.ONBOARDING_POLICY_ID]: string; [ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string; [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean; [ONYXKEYS.LAST_VISITED_PATH]: string | undefined; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0dff2992fe91..2bc04c4a99ea 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -515,10 +515,6 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/invoice-account-select', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/invoice-account-select` as const, }, - POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT: { - route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account/account-payable-select', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/company-card-expense-account/account-payable-select` as const, - }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_PREFERRED_EXPORTER: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/preferred-exporter', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/preferred-exporter` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e25b9776919a..f74002312623 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -229,7 +229,6 @@ const SCREENS = { QUICKBOOKS_ONLINE_EXPORT_INVOICE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Invoice_Account_Select', QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense', QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Account_Select', - QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Account_Payable_Select', QUICKBOOKS_ONLINE_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Non_Reimbursable_Default_Vendor_Select', QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Select', QUICKBOOKS_ONLINE_EXPORT_PREFERRED_EXPORTER: 'Workspace_Accounting_Quickbooks_Online_Export_Preferred_Exporter', diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index da8e3694a7d2..0c8af3dfc826 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} @@ -63,6 +62,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow file={{name: displayName}} shouldShowDownloadIcon={!isOffline} shouldShowLoadingSpinnerIcon={isDownloading} + isUsedAsChatAttachment /> )} diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index ec2687d634bb..2ec1883fd7de 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -71,7 +71,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr return ( - + ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index e16a81702f66..e8f6568f98c0 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, errorLabelStyles, style}: AttachmentViewPdfProps) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, isUsedAsChatAttachment, onLoadError}: AttachmentViewPdfProps) { return ( ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts index 605be3b25ec4..c20593449d58 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts @@ -1,4 +1,4 @@ -import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import type {AttachmentViewProps} from '..'; type AttachmentViewPdfProps = Pick & { @@ -8,11 +8,14 @@ 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 */ + onLoadError?: () => void; + + /** Whether the PDF is used as a chat attachment */ + isUsedAsChatAttachment?: boolean; }; 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..e06ea3064150 --- /dev/null +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -0,0 +1,67 @@ +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 = { + /** 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; +}; + +function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles}: DefaultAttachmentViewProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + + + + {fileName} + {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( + + + + + + )} + {shouldShowLoadingSpinnerIcon && ( + + + + + + )} + + ); +} + +DefaultAttachmentView.displayName = 'DefaultAttachmentView'; + +export default DefaultAttachmentView; diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 2685a5cef407..cee2264894a7 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -1,17 +1,14 @@ 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 {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 useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -28,6 +25,7 @@ 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; @@ -64,9 +62,14 @@ type AttachmentViewProps = AttachmentViewOnyxProps & /** Denotes whether it is an icon (ex: SVG) */ maybeIcon?: boolean; + /** Fallback source to use in case of error */ fallbackSource?: AttachmentSource; + /* Whether it is hovered or not */ isHovered?: boolean; + + /** Whether the attachment is used as a chat attachment */ + isUsedAsChatAttachment?: boolean; }; function AttachmentView({ @@ -88,6 +91,7 @@ function AttachmentView({ reportActionID, isHovered, duration, + isUsedAsChatAttachment, }: AttachmentViewProps) { const {translate} = useLocalize(); const {updateCurrentlyPlayingURL} = usePlaybackContext(); @@ -95,6 +99,7 @@ function AttachmentView({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); + const [hasPDFFailedToLoad, setHasPDFFailedToLoad] = useState(false); const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file?.name && Str.isVideo(file.name)); useEffect(() => { @@ -145,7 +150,9 @@ 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 ((typeof source === 'string' && Str.isPDF(source)) || (file && Str.isPDF(file.name ?? translate('attachmentView.unknownFilename')))) { + const isSourcePDF = typeof source === 'string' && Str.isPDF(source); + const isFilePDF = file && Str.isPDF(file.name ?? translate('attachmentView.unknownFilename')); + if (!hasPDFFailedToLoad && (isSourcePDF || isFilePDF)) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source as string) : (source as string); const onPDFLoadComplete = (path: string) => { @@ -158,6 +165,10 @@ function AttachmentView({ } }; + const onPDFLoadError = () => { + setHasPDFFailedToLoad(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. @@ -170,8 +181,9 @@ function AttachmentView({ onPress={onPress} onToggleKeyboard={onToggleKeyboard} onLoadComplete={onPDFLoadComplete} - errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} + isUsedAsChatAttachment={isUsedAsChatAttachment} + onLoadError={onPDFLoadError} /> ); @@ -228,36 +240,12 @@ function AttachmentView({ } return ( - - - - - - {file?.name} - {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( - - - - - - )} - {shouldShowLoadingSpinnerIcon && ( - - - - - - )} - + ); } diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 40ebebdd0488..e5c85a8f5f6d 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -45,6 +45,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC }, [selectedCategory]); const [sections, headerMessage, shouldShowTextInput] = useMemo(() => { + const categories = policyCategories ?? policyCategoriesDraft ?? {}; const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter?.((p) => !isEmptyObject(p)); const {categoryOptions} = OptionsListUtils.getFilteredOptions( [], @@ -56,15 +57,15 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC false, false, true, - policyCategories ?? policyCategoriesDraft ?? {}, + categories, validPolicyRecentlyUsedCategories, false, ); const categoryData = categoryOptions?.[0]?.data ?? []; const header = OptionsListUtils.getHeaderMessageForNonUserList(categoryData.length > 0, debouncedSearchValue); - const policiesCount = OptionsListUtils.getEnabledCategoriesCount(policyCategories ?? {}); - const isCategoriesCountBelowThreshold = policiesCount < CONST.CATEGORY_LIST_THRESHOLD; + const categoriesCount = OptionsListUtils.getEnabledCategoriesCount(categories); + const isCategoriesCountBelowThreshold = categoriesCount < CONST.CATEGORY_LIST_THRESHOLD; const showInput = !isCategoriesCountBelowThreshold; return [categoryOptions, header, showInput]; diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 4d135cdd88e2..ac7eb95191ee 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -27,6 +27,7 @@ function Composer( // On Android the selection prop is required on the TextInput but this prop has issues on IOS selection, value, + isGroupPolicyReport = false, ...props }: ComposerProps, ref: ForwardedRef, @@ -34,7 +35,7 @@ function Composer( const textInput = useRef(null); const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput); const theme = useTheme(); - const markdownStyle = useMarkdownStyle(value); + const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? ['mentionReport'] : []); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 4bc54d13b056..f7bf277050a2 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -70,13 +70,14 @@ function Composer( isReportActionCompose = false, isComposerFullSize = false, shouldContainScroll = false, + isGroupPolicyReport = false, ...props }: ComposerProps, ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); - const markdownStyle = useMarkdownStyle(value); + const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? ['mentionReport'] : []); const StyleUtils = useStyleUtils(); const textRef = useRef(null); const textInput = useRef(null); diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 531bcd03f8bf..0ff91111bd07 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -70,6 +70,9 @@ type ComposerProps = TextInputProps & { /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; + + /** Indicates whether the composer is in a group policy report. Used for disabling report mentioning style in markdown input */ + isGroupPolicyReport?: boolean; }; export type {TextSelection, ComposerProps}; diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index 54f6748986f4..5c48652f8cc5 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -7,7 +7,7 @@ type BaseInvertedFlatListProps = FlatListProps & { shouldEnableAutoScrollToTopThreshold?: boolean; }; -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; +const AUTOSCROLL_TO_TOP_THRESHOLD = 250; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { const {shouldEnableAutoScrollToTopThreshold, ...rest} = props; diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index b665c305f5cf..6bb900ecb489 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -71,7 +71,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const textUnreadStyle = optionItem?.isUnread && optionItem.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, style]; const alternateTextStyle = isInFocusMode - ? [textStyle, styles.optionAlternateText, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style] + ? [textStyle, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style] : [textStyle, styles.optionAlternateText, styles.textLabelSupporting, style]; const contentContainerStyles = isInFocusMode ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index e1a998f78cab..4eb3be871a8d 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -3,12 +3,15 @@ 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'; import * as HeaderUtils from '@libs/HeaderUtils'; 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'; @@ -19,8 +22,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'; @@ -71,6 +76,7 @@ function MoneyReportHeader({ onBackButtonPress, }: MoneyReportHeaderProps) { const styles = useThemeStyles(); + const theme = useTheme(); const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); @@ -98,6 +104,9 @@ function MoneyReportHeader({ const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); + const transactionIDs = TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID).map((transaction) => transaction.transactionID); + const allHavePendingRTERViolation = TransactionUtils.allHavePendingRTERViolation(transactionIDs); + const cancelPayment = useCallback(() => { if (!chatReport) { return; @@ -112,12 +121,12 @@ function MoneyReportHeader({ const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); - const shouldShowSettlementButton = !ReportUtils.isInvoiceReport(moneyRequestReport) && (shouldShowPayButton || shouldShowApproveButton); + const shouldShowSettlementButton = !ReportUtils.isInvoiceReport(moneyRequestReport) && (shouldShowPayButton || shouldShowApproveButton) && !allHavePendingRTERViolation; - const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; + const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !allHavePendingRTERViolation; 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; + const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !allHavePendingRTERViolation; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); @@ -203,7 +212,7 @@ function MoneyReportHeader({ shouldShowBackButton={shouldUseNarrowLayout} onBackButtonPress={onBackButtonPress} // Shows border if no buttons or next steps are showing below the header - shouldShowBorderBottom={!(shouldShowAnyButton && shouldUseNarrowLayout) && !(shouldShowNextStep && !shouldUseNarrowLayout)} + shouldShowBorderBottom={!(shouldShowAnyButton && shouldUseNarrowLayout) && !(shouldShowNextStep && !shouldUseNarrowLayout) && !allHavePendingRTERViolation} shouldShowThreeDotsButton threeDotsMenuItems={threeDotsMenuItems} threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} @@ -241,6 +250,20 @@ function MoneyReportHeader({ )} + {allHavePendingRTERViolation && ( + + } + description={translate('iou.pendingMatchWithCreditCardDescription')} + shouldShowBorderBottom + /> + )} {shouldShowSettlementButton && shouldUseNarrowLayout && ( diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index a59b50e5bdb7..9acd73beadba 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -285,6 +285,7 @@ function MoneyRequestAmountInput( touchableInputWrapperStyle={props.touchableInputWrapperStyle} maxLength={maxLength} hideFocusedState={hideFocusedState} + onMouseDown={(event) => event.stopPropagation()} /> ); } diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 65c9d800607d..7c122f120e8a 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -1,7 +1,8 @@ +import type {ReactNode} from 'react'; 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,12 +17,14 @@ 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 type IconAsset from '@src/types/utils/IconAsset'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; +import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; @@ -35,6 +38,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; @@ -65,6 +71,7 @@ function MoneyRequestHeader({ parentReport, report, parentReportAction, + transactionViolations, transaction, shownHoldUseExplanation = false, policy, @@ -101,7 +108,6 @@ function MoneyRequestHeader({ }, [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; @@ -120,6 +126,33 @@ function MoneyRequestHeader({ } }; + const getStatusIcon: (src: IconAsset) => ReactNode = (src) => ( + + ); + + const getStatusBarProps: () => MoneyRequestHeaderStatusBarProps | undefined = () => { + if (isOnHold) { + return {title: translate('iou.hold'), description: translate('iou.expenseOnHold'), danger: true, shouldShowBorderBottom: true}; + } + + if (TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction)) { + return {title: getStatusIcon(Expensicons.CreditCardHourglass), description: translate('iou.transactionPendingDescription'), shouldShowBorderBottom: true}; + } + if (isScanning) { + return {title: getStatusIcon(Expensicons.ReceiptScan), description: translate('iou.receiptScanInProgressDescription'), shouldShowBorderBottom: true}; + } + if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '', transactionViolations))) { + return {title: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription'), shouldShowBorderBottom: true}; + } + }; + + const statusBarProps = getStatusBarProps(); + useEffect(() => { if (canDeleteRequest) { return; @@ -184,7 +217,7 @@ function MoneyRequestHeader({ <> - {isPending && ( - - } - description={translate('iou.transactionPendingDescription')} - shouldShowBorderBottom={!isScanning} - /> - )} - {isScanning && ( + {statusBarProps && ( - } - description={translate('iou.receiptScanInProgressDescription')} - shouldShowBorderBottom - /> - )} - {isOnHold && ( - )} @@ -259,7 +264,7 @@ function MoneyRequestHeader({ MoneyRequestHeader.displayName = 'MoneyRequestHeader'; -const MoneyRequestHeaderWithTransaction = withOnyx>({ +const MoneyRequestHeaderWithTransaction = withOnyx>({ transaction: { key: ({report, parentReportActions}) => { const parentReportAction = (report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : {}) as ReportAction & OriginalMessageIOU; @@ -270,9 +275,15 @@ const MoneyRequestHeaderWithTransaction = withOnyx, Omit>({ +export default withOnyx< + Omit, + Omit +>({ session: { key: ONYXKEYS.SESSION, }, diff --git a/src/components/MoneyRequestHeaderStatusBar.tsx b/src/components/MoneyRequestHeaderStatusBar.tsx index b7bba0223656..4ee3079d5f1f 100644 --- a/src/components/MoneyRequestHeaderStatusBar.tsx +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -57,3 +57,5 @@ function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom MoneyRequestHeaderStatusBar.displayName = 'MoneyRequestHeaderStatusBar'; export default MoneyRequestHeaderStatusBar; + +export type {MoneyRequestHeaderStatusBarProps}; diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 16bc9aef21eb..c9bf5d7b9db2 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -64,7 +64,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp return; } - const newReports = OptionsListUtils.createOptionList({}, reports).reports; + const newReports = OptionsListUtils.createOptionList(personalDetails, reports).reports; setOptions((prevOptions) => { const newOptions = {...prevOptions}; diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index a5b911deb6ff..ce631f3b611f 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -1,4 +1,3 @@ -// @ts-expect-error - This line imports a module from 'pdfjs-dist' package which lacks TypeScript typings. import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker'; import React, {useMemo} from 'react'; import {View} from 'react-native'; diff --git a/src/components/PDFView/index.native.tsx b/src/components/PDFView/index.native.tsx index f0edba5de7d0..7d523ef13805 100644 --- a/src/components/PDFView/index.native.tsx +++ b/src/components/PDFView/index.native.tsx @@ -5,7 +5,6 @@ import PDF from 'react-native-pdf'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; 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 useStyleUtils from '@hooks/useStyleUtils'; @@ -29,7 +28,11 @@ import type {PDFViewNativeProps} from './types'; * so that PDFPasswordForm doesn't bounce when react-native-pdf/PDF * is (temporarily) rendered. */ -function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, errorLabelStyles}: PDFViewNativeProps) { + +const LOADING_THUMBNAIL_HEIGHT = 250; +const LOADING_THUMBNAIL_WIDTH = 250; + +function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused, onScaleChanged, sourceURL, onLoadError, isUsedAsChatAttachment}: PDFViewNativeProps) { const [shouldRequestPassword, setShouldRequestPassword] = useState(false); const [shouldAttemptPDFLoad, setShouldAttemptPDFLoad] = useState(true); const [shouldShowLoadingIndicator, setShouldShowLoadingIndicator] = useState(true); @@ -80,6 +83,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused setShouldShowLoadingIndicator(false); setShouldRequestPassword(false); setShouldAttemptPDFLoad(false); + onLoadError?.(); // eslint-disable-next-line @typescript-eslint/ban-types }) as (error: object) => void; @@ -112,7 +116,9 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused }; function renderPDFView() { - const pdfStyles: StyleProp = [themeStyles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(windowWidth, windowHeight)]; + const pdfWidth = isUsedAsChatAttachment ? LOADING_THUMBNAIL_WIDTH : windowWidth; + const pdfHeight = isUsedAsChatAttachment ? LOADING_THUMBNAIL_HEIGHT : windowHeight; + const pdfStyles: StyleProp = [themeStyles.imageModalPDF, StyleUtils.getWidthAndHeightStyle(pdfWidth, pdfHeight)]; // 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 @@ -121,21 +127,20 @@ 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 = + // 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)] + : []; return ( - {failedToLoadPDF && ( - - {translate('attachmentView.failedToLoadPDF')} - - )} {shouldAttemptPDFLoad && ( } + renderActivityIndicator={() => } source={{uri: sourceURL, cache: true, expiration: 864000}} style={pdfStyles} onError={handleFailureToLoadPDF} diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx index 99b5be1c8f53..76daba9ce0f6 100644 --- a/src/components/PDFView/index.tsx +++ b/src/components/PDFView/index.tsx @@ -7,9 +7,9 @@ import {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 useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import variables from '@styles/variables'; @@ -19,9 +19,13 @@ import ONYXKEYS from '@src/ONYXKEYS'; import PDFPasswordForm from './PDFPasswordForm'; 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 StyleUtils = useStyleUtils(); const {windowHeight, isSmallScreenWidth} = useWindowDimensions(); const prevWindowHeight = usePrevious(windowHeight); const {translate} = useLocalize(); @@ -95,8 +99,19 @@ function PDFView({onToggleKeyboard, fileName, onPress, isFocused, sourceURL, err maxCanvasWidth={maxCanvasWidth} maxCanvasHeight={maxCanvasHeight} maxCanvasArea={maxCanvasArea} - LoadingComponent={} - ErrorComponent={{translate('attachmentView.failedToLoadPDF')}} + LoadingComponent={ + + } + 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 = { diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 45893de809df..4ff318bc3c47 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -36,7 +36,7 @@ import CONST from '@src/CONST'; import type {IOUMessage} from '@src/types/onyx/OriginalMessage'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {MoneyRequestPreviewProps} from './types'; +import type {MoneyRequestPreviewProps, PendingMessageProps} from './types'; function MoneyRequestPreviewContent({ iouReport, @@ -84,7 +84,6 @@ function MoneyRequestPreviewContent({ const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const hasReceipt = TransactionUtils.hasReceipt(transaction); const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); - const isPending = TransactionUtils.isPending(transaction); const isOnHold = TransactionUtils.isOnHold(transaction); const isSettlementOrApprovalPartial = Boolean(iouReport?.pendingFields?.partial); const isPartialHold = isSettlementOrApprovalPartial && isOnHold; @@ -184,6 +183,21 @@ function MoneyRequestPreviewContent({ return message; }; + const getPendingMessageProps: () => PendingMessageProps = () => { + if (isScanning) { + return {shouldShow: true, messageIcon: ReceiptScan, messageDescription: translate('iou.receiptScanInProgress')}; + } + if (TransactionUtils.isPending(transaction)) { + return {shouldShow: true, messageIcon: Expensicons.CreditCardHourglass, messageDescription: translate('iou.transactionPending')}; + } + if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '', transactionViolations))) { + return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; + } + return {shouldShow: false}; + }; + + const pendingMessageProps = getPendingMessageProps(); + const getDisplayAmountText = (): string => { if (isScanning) { return translate('iou.receiptScanning'); @@ -312,26 +326,15 @@ function MoneyRequestPreviewContent({ )} - {isScanning && ( - - - {translate('iou.receiptScanInProgress')} - - )} - {isPending && ( + {pendingMessageProps.shouldShow && ( - {translate('iou.transactionPending')} + {pendingMessageProps.messageDescription} )} diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts index 0e3eb37ce6e3..9dcea80fdc05 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts +++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts @@ -2,6 +2,7 @@ import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import type * as OnyxTypes from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; type MoneyRequestPreviewOnyxProps = { /** All of the personal details for everyone */ @@ -71,4 +72,19 @@ type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & { isWhisper?: boolean; }; -export type {MoneyRequestPreviewProps, MoneyRequestPreviewOnyxProps}; +type NoPendingProps = {shouldShow: false}; + +type PendingProps = { + /** Whether to show the pending message or not */ + shouldShow: true; + + /** The icon to be displayed if a request is pending */ + messageIcon: IconAsset; + + /** The description to be displayed if a request is pending */ + messageDescription: string; +}; + +type PendingMessageProps = PendingProps | NoPendingProps; + +export type {MoneyRequestPreviewProps, MoneyRequestPreviewOnyxProps, PendingMessageProps}; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 013e8e8e47e7..0380780e0ccb 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -335,7 +335,7 @@ function MoneyRequestView({ {shouldShowMapOrReceipt && ( { if (!transaction?.transactionID) { diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 9c5821329e1c..dae827cdb5c0 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -32,6 +32,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report, ReportAction, Transaction, TransactionViolations, UserWallet} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type {PendingMessageProps} from './MoneyRequestPreview/types'; import ReportActionItemImages from './ReportActionItemImages'; type ReportPreviewOnyxProps = { @@ -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 = + numberOfRequests === 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 @@ -210,7 +213,7 @@ function ReportPreview({ const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport); - const shouldShowSettlementButton = !ReportUtils.isInvoiceReport(iouReport) && (shouldShowPayButton || shouldShowApproveButton); + const shouldShowSettlementButton = !ReportUtils.isInvoiceReport(iouReport) && (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage; const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID); const shouldShowRBR = !iouSettled && hasErrors; @@ -229,6 +232,21 @@ function ReportPreview({ const shouldShowScanningSubtitle = numberOfScanningReceipts === 1 && numberOfRequests === 1; const shouldShowPendingSubtitle = numberOfPendingRequests === 1 && numberOfRequests === 1; + const getPendingMessageProps: () => PendingMessageProps = () => { + if (shouldShowScanningSubtitle) { + return {shouldShow: true, messageIcon: Expensicons.ReceiptScan, messageDescription: translate('iou.receiptScanInProgress')}; + } + if (shouldShowPendingSubtitle) { + return {shouldShow: true, messageIcon: Expensicons.CreditCardHourglass, messageDescription: translate('iou.transactionPending')}; + } + if (showRTERViolationMessage) { + return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; + } + return {shouldShow: false}; + }; + + const pendingMessageProps = getPendingMessageProps(); + const {supportText} = useMemo(() => { if (formattedMerchant) { return {supportText: formattedMerchant}; @@ -314,26 +332,15 @@ function ReportPreview({ )} - {shouldShowScanningSubtitle && ( - - - {translate('iou.receiptScanInProgress')} - - )} - {shouldShowPendingSubtitle && ( - + {pendingMessageProps.shouldShow && ( + - {translate('iou.transactionPending')} + {pendingMessageProps.messageDescription} )} diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx index 9fe1665f6b19..882d99f38e3d 100644 --- a/src/components/TabSelector/TabSelector.tsx +++ b/src/components/TabSelector/TabSelector.tsx @@ -68,7 +68,7 @@ function TabSelector({state, navigation, onTabPress = () => {}, position}: TabSe return position.interpolate({ inputRange, outputRange: inputRange.map((i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG)), - }); + }) as unknown as Animated.AnimatedInterpolation; } return theme.border; }, diff --git a/src/components/TextInputWithCurrencySymbol/types.ts b/src/components/TextInputWithCurrencySymbol/types.ts index b31f27aef786..27c8498f253c 100644 --- a/src/components/TextInputWithCurrencySymbol/types.ts +++ b/src/components/TextInputWithCurrencySymbol/types.ts @@ -32,6 +32,11 @@ type TextInputWithCurrencySymbolProps = { */ onBlur?: ((e: NativeSyntheticEvent) => void) | undefined; + /** + * Callback that is called when the text input is pressed down + */ + onMouseDown?: ((e: React.MouseEvent) => void) | undefined; + /** Whether the currency symbol is pressable */ isCurrencyPressable: boolean; diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 462911968000..fe343b900724 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -11,6 +11,7 @@ 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'; @@ -82,7 +83,7 @@ function BaseVideoPlayer({ const isUploading = CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => url.startsWith(prefix)); const videoStateRef = useRef(null); const {updateVolume} = useVolumeContext(); - const {videoPopoverMenuPlayerRef} = useVideoPopoverMenuContext(); + const {videoPopoverMenuPlayerRef, setCurrentPlaybackSpeed} = useVideoPopoverMenuContext(); const togglePlayCurrentVideo = useCallback(() => { videoResumeTryNumber.current = 0; @@ -96,8 +97,14 @@ function BaseVideoPlayer({ }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumber]); const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { - setIsPopoverVisible(true); videoPopoverMenuPlayerRef.current = videoPlayerRef.current; + videoPlayerRef.current?.getStatusAsync().then((status) => { + if (!('rate' in status && status.rate)) { + return; + } + setIsPopoverVisible(true); + 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 29156c438d3d..9742c613de2f 100644 --- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx +++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx @@ -14,7 +14,7 @@ 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(); @@ -24,9 +24,9 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { const updatePlaybackSpeed = useCallback( (speed: PlaybackSpeed) => { setCurrentPlaybackSpeed(speed); - currentVideoPlayerRef.current?.setStatusAsync?.({rate: speed}); + videoPopoverMenuPlayerRef.current?.setStatusAsync?.({rate: speed}); }, - [currentVideoPlayerRef], + [videoPopoverMenuPlayerRef], ); const downloadAttachment = useCallback(() => { @@ -69,7 +69,10 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { return items; }, [currentPlaybackSpeed, downloadAttachment, translate, updatePlaybackSpeed, isOffline, isLocalFile]); - const contextValue = useMemo(() => ({menuItems, videoPopoverMenuPlayerRef, updatePlaybackSpeed}), [menuItems, videoPopoverMenuPlayerRef, updatePlaybackSpeed]); + const contextValue = useMemo( + () => ({menuItems, videoPopoverMenuPlayerRef, updatePlaybackSpeed, setCurrentPlaybackSpeed}), + [menuItems, videoPopoverMenuPlayerRef, updatePlaybackSpeed, setCurrentPlaybackSpeed], + ); return {children}; } diff --git a/src/components/VideoPlayerContexts/types.ts b/src/components/VideoPlayerContexts/types.ts index ea09281d7676..6216fd5dc85b 100644 --- a/src/components/VideoPlayerContexts/types.ts +++ b/src/components/VideoPlayerContexts/types.ts @@ -30,6 +30,7 @@ type VideoPopoverMenuContext = { menuItems: PopoverMenuItem[]; videoPopoverMenuPlayerRef: MutableRefObject; updatePlaybackSpeed: (speed: PlaybackSpeed) => void; + setCurrentPlaybackSpeed: (speed: PlaybackSpeed) => void; }; type FullScreenContext = { 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/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts index b1f430e232e4..57d17ffefb0b 100644 --- a/src/hooks/useMarkdownStyle.ts +++ b/src/hooks/useMarkdownStyle.ts @@ -5,12 +5,25 @@ import FontUtils from '@styles/utils/FontUtils'; import variables from '@styles/variables'; import useTheme from './useTheme'; -function useMarkdownStyle(message: string | null = null): MarkdownStyle { +function useMarkdownStyle(message: string | null = null, excludeStyles: Array = []): MarkdownStyle { const theme = useTheme(); const emojiFontSize = containsOnlyEmojis(message ?? '') ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal; - const markdownStyle = useMemo( + // this map is used to reset the styles that are not needed - passing undefined value can break the native side + const nonStylingDefaultValues: Record = useMemo( () => ({ + color: theme.text, + backgroundColor: 'transparent', + marginLeft: 0, + paddingLeft: 0, + borderColor: 'transparent', + borderWidth: 0, + }), + [theme], + ); + + const markdownStyle = useMemo(() => { + const styling = { syntax: { color: theme.syntax, }, @@ -53,9 +66,21 @@ function useMarkdownStyle(message: string | null = null): MarkdownStyle { color: theme.mentionText, backgroundColor: theme.mentionBG, }, - }), - [theme, emojiFontSize], - ); + }; + + if (excludeStyles.length) { + excludeStyles.forEach((key) => { + const style: Record = styling[key]; + if (style) { + Object.keys(style).forEach((styleKey) => { + style[styleKey] = nonStylingDefaultValues[styleKey] ?? style[styleKey]; + }); + } + }); + } + + return styling; + }, [theme, emojiFontSize, excludeStyles, nonStylingDefaultValues]); return markdownStyle; } diff --git a/src/hooks/useTabNavigatorFocus/index.ts b/src/hooks/useTabNavigatorFocus/index.ts index 3fef0e53774f..f160f4670b26 100644 --- a/src/hooks/useTabNavigatorFocus/index.ts +++ b/src/hooks/useTabNavigatorFocus/index.ts @@ -64,7 +64,6 @@ function useTabNavigatorFocus({tabIndex}: UseTabNavigatorFocusParams): boolean { // We need to get the position animation value on component initialization to determine // if the tab is focused or not. Since it's an Animated.Value the only synchronous way // to retrieve the value is to use a private method. - // @ts-expect-error -- __getValue is a private method // eslint-disable-next-line no-underscore-dangle const initialTabPositionValue = tabPositionAnimation.__getValue(); diff --git a/src/languages/en.ts b/src/languages/en.ts index 0f075fcca616..f8d122f3b69a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -647,6 +647,9 @@ export default { canceled: 'Canceled', posted: 'Posted', deleteReceipt: 'Delete receipt', + pendingMatchWithCreditCard: 'Receipt pending match with credit card.', + pendingMatchWithCreditCardDescription: 'Receipt pending match with credit card. Mark as cash to ignore and request payment.', + routePending: 'Route pending...', receiptScanning: 'Receipt scanning...', receiptScanInProgress: 'Receipt scan in progress.', receiptScanInProgressDescription: 'Receipt scan in progress. Check back later or enter the details now.', @@ -1295,7 +1298,7 @@ export default { label: 'Light', }, system: { - label: 'Use Device Settings', + label: 'Use device settings', }, }, chooseThemeBelowOrSync: 'Choose a theme below, or sync with your device settings.', @@ -1931,6 +1934,8 @@ export default { customersDescription: 'Choose whether to import customers/projects and see where customers/projects are displayed.', locationsDescription: 'Choose whether to import locations, and see where locations are displayed.', taxesDescription: 'Choose whether to import tax rates and tax defaults from your accounting integration.', + taxesJournalEntrySwitchNote: + 'Note: QuickBooks Online does not support a field for tax on Journal Entry exports. Change your export preference to Vendor Bill or Check to import taxes.', locationsAdditionalDescription: 'Locations are imported as Tags. This limits exporting expense reports as Vendor Bills or Checks to QuickBooks Online. To unlock these export options, either disable Locations import or upgrade to the Control Plan to export Locations encoded as a Report Field.', export: 'Export', @@ -2974,7 +2979,7 @@ export default { body: `Get paid to talk to your friends! Start a chat with a new Expensify account and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SUBMIT_EXPENSE]: { - buttonText1: 'Submit expense, ', + buttonText1: 'Submit an expense, ', buttonText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}.`, header: `Submit an expense, get $${CONST.REFERRAL_PROGRAM.REVENUE}`, body: `It pays to get paid! Submit an expense to a new Expensify account and get $${CONST.REFERRAL_PROGRAM.REVENUE} when they become a customer.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index ce89a67175d2..536a6182f393 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -640,6 +640,9 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', + pendingMatchWithCreditCard: 'Recibo pendiente de adjuntar con la tarjeta de crédito.', + pendingMatchWithCreditCardDescription: 'Recibo pendiente de adjuntar con tarjeta de crédito. Marca como efectivo para ignorar y solicitar pago.', + routePending: 'Ruta pendiente...', receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema encontrado' : 'Problemas encontrados'}`, fieldPending: 'Pendiente...', receiptScanning: 'Escaneando recibo...', @@ -1955,6 +1958,8 @@ export default { customersDescription: 'Elige si queres importar clientes/proyectos y donde los clientes/proyectos son mostrados.', locationsDescription: 'Elige si quieres importar lugares y donde los lugares son mostrados.', taxesDescription: 'Elige si quires importar las tasas de impuestos y los impuestos por defecto de tu integración de contaduría.', + taxesJournalEntrySwitchNote: + 'Nota: QuickBooks Online no admite un campo para impuestos al exportar entradas en el libro diario. Cambia tu preferencia de exportación a Factura de Proveedor o Cheque para importar impuestos.', locationsAdditionalDescription: 'Los lugares son importados como Etiquegas. Esto limita a exportar los informes de gastos como Factura del Proveedor o Cheques a Quicbooks Online. Para desbloquear estas opciones de exportación desactiva la importación de Lugares o cambia al Plan Control para exportar Lugares como Campos de Informes.', export: 'Exportar', @@ -3475,7 +3480,7 @@ export default { body: `¡Gana dinero por hablar con tus amigos! Inicia un chat con una cuenta nueva de Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`, }, [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SUBMIT_EXPENSE]: { - buttonText1: 'Presentar gasto, ', + buttonText1: 'Presenta un gasto, ', buttonText2: `recibe $${CONST.REFERRAL_PROGRAM.REVENUE}`, header: `Presenta un gasto y consigue $${CONST.REFERRAL_PROGRAM.REVENUE}`, body: `¡Vale la pena cobrar! Envia un gasto a una cuenta nueva de Expensify y recibe $${CONST.REFERRAL_PROGRAM.REVENUE} cuando se conviertan en clientes.`, diff --git a/src/libs/API/parameters/UpdateManyPolicyConnectionConfigurationsParams.ts b/src/libs/API/parameters/UpdateManyPolicyConnectionConfigurationsParams.ts new file mode 100644 index 000000000000..6496d3e94266 --- /dev/null +++ b/src/libs/API/parameters/UpdateManyPolicyConnectionConfigurationsParams.ts @@ -0,0 +1,8 @@ +type UpdateManyPolicyConnectionConfigurationsParams = { + policyID: string; + connectionName: string; + configUpdate: string; + idempotencyKey: string; +}; + +export default UpdateManyPolicyConnectionConfigurationsParams; diff --git a/src/libs/API/parameters/UpdatePolicyConnectionConfigParams.ts b/src/libs/API/parameters/UpdatePolicyConnectionConfigParams.ts index 2a5c6da51cec..111674b8ffb6 100644 --- a/src/libs/API/parameters/UpdatePolicyConnectionConfigParams.ts +++ b/src/libs/API/parameters/UpdatePolicyConnectionConfigParams.ts @@ -2,7 +2,7 @@ type UpdatePolicyConnectionConfigParams = { policyID: string; connectionName: string; settingName: string; - settingValue?: string; + settingValue: string; idempotencyKey: string; }; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 29044d4fac16..52f130a28dd1 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -209,6 +209,7 @@ export type {default as SetPolicyCustomTaxNameParams} from './SetPolicyCustomTax export type {default as SetPolicyForeignCurrencyDefaultParams} from './SetPolicyForeignCurrencyDefaultParams'; export type {default as SetPolicyCurrencyDefaultParams} from './SetPolicyCurrencyDefaultParams'; export type {default as UpdatePolicyConnectionConfigParams} from './UpdatePolicyConnectionConfigParams'; +export type {default as UpdateManyPolicyConnectionConfigurationsParams} from './UpdateManyPolicyConnectionConfigurationsParams'; export type {default as RemovePolicyConnectionParams} from './RemovePolicyConnectionParams'; export type {default as RenamePolicyTaxParams} from './RenamePolicyTaxParams'; export type {default as CompleteGuidedSetupParams} from './CompleteGuidedSetupParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8fe704ff220e..08bc5eddd087 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -196,6 +196,7 @@ const WRITE_COMMANDS = { DECLINE_JOIN_REQUEST: 'DeclineJoinRequest', CREATE_POLICY_TAX: 'CreatePolicyTax', UPDATE_POLICY_CONNECTION_CONFIG: 'UpdatePolicyConnectionConfiguration', + UPDATE_MANY_POLICY_CONNECTION_CONFIGS: 'UpdateManyPolicyConnectionConfigurations', REMOVE_POLICY_CONNECTION: 'RemovePolicyConnection', SET_POLICY_TAXES_ENABLED: 'SetPolicyTaxesEnabled', DELETE_POLICY_TAXES: 'DeletePolicyTaxes', @@ -422,6 +423,7 @@ type WriteCommandParameters = { // eslint-disable-next-line @typescript-eslint/no-explicit-any [WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG]: Parameters.UpdatePolicyConnectionConfigParams; + [WRITE_COMMANDS.UPDATE_MANY_POLICY_CONNECTION_CONFIGS]: Parameters.UpdateManyPolicyConnectionConfigurationsParams; [WRITE_COMMANDS.REMOVE_POLICY_CONNECTION]: Parameters.RemovePolicyConnectionParams; [WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 47d170ce5cd5..462145e56907 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -257,8 +257,6 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage').default as React.ComponentType, - [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT]: () => - require('../../../../pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPayableSelectPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES]: () => require('../../../../pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage').default as React.ComponentType, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_SELECT]: () => diff --git a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx index 6680ea302441..6e1d154ff350 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx @@ -3,7 +3,7 @@ import type {StackNavigationOptions} from '@react-navigation/stack'; import React from 'react'; import createCustomBottomTabNavigator from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; -import type {BottomTabNavigatorParamList} from '@libs/Navigation/types'; +import type {BottomTabNavigatorParamList, CentralPaneName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types'; import SidebarScreen from '@pages/home/sidebar/SidebarScreen'; import SearchPageBottomTab from '@pages/Search/SearchPageBottomTab'; import SCREENS from '@src/SCREENS'; @@ -19,7 +19,7 @@ const screenOptions: StackNavigationOptions = { }; function BottomTabNavigator() { - const activeRoute = useNavigationState(getTopmostCentralPaneRoute); + const activeRoute = useNavigationState | undefined>(getTopmostCentralPaneRoute); return ( diff --git a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts index 4c18e161c9a9..5061c7500742 100644 --- a/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts +++ b/src/libs/Navigation/AppNavigator/getPartialStateDiff.ts @@ -53,7 +53,7 @@ function getPartialStateDiff(state: State, templateState: St (stateTopmostCentralPane && templateStateTopmostCentralPane && stateTopmostCentralPane.name !== templateStateTopmostCentralPane.name && - !shallowCompare(stateTopmostCentralPane.params, templateStateTopmostCentralPane.params)) + !shallowCompare(stateTopmostCentralPane.params as Record | undefined, templateStateTopmostCentralPane.params as Record | undefined)) ) { // We need to wrap central pane routes in the central pane navigator. diff[NAVIGATORS.CENTRAL_PANE_NAVIGATOR] = templateStateTopmostCentralPane; @@ -73,7 +73,7 @@ function getPartialStateDiff(state: State, templateState: St (stateTopmostFullScreen && templateStateTopmostFullScreen && stateTopmostFullScreen.name !== templateStateTopmostFullScreen.name && - !shallowCompare(stateTopmostFullScreen.params, templateStateTopmostFullScreen.params)) + !shallowCompare(stateTopmostFullScreen.params as Record | undefined, templateStateTopmostFullScreen.params as Record | undefined)) ) { diff[NAVIGATORS.FULL_SCREEN_NAVIGATOR] = fullScreenDiff; } diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts index 45a8609482ca..50050e4b8ff4 100644 --- a/src/libs/Navigation/linkTo.ts +++ b/src/libs/Navigation/linkTo.ts @@ -160,8 +160,8 @@ export default function linkTo(navigation: NavigationContainerRef value === undefined), - omitBy(action.payload.params?.params, (value) => value === undefined), + omitBy(topmostCentralPaneRoute?.params as Record | undefined, (value) => value === undefined), + omitBy(action.payload.params?.params as Record | undefined, (value) => value === undefined), ); // In case if type is 'FORCED_UP' we replace current screen with the provided. This means the current screen no longer exists in the stack if (type === CONST.NAVIGATION.TYPE.FORCED_UP) { diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index d7808497916f..b1dd11849833 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -28,7 +28,6 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_INVOICE_ACCOUNT_SELECT, - SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_ACCOUNT_SELECT, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT, SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 92482c39e2d7..a093b778360e 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -291,9 +291,6 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_INVOICE_ACCOUNT_SELECT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECT.route}, - [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT]: { - path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT.route, - }, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_ACCOUNT_SELECT]: { path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES_ACCOUNT_SELECT.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a0cd76637716..e4820df10df9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -308,9 +308,6 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT]: { policyID: string; }; - [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT]: { - policyID: string; - }; [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_EXPORT_PREFERRED_EXPORTER]: { policyID: string; }; diff --git a/src/libs/ObjectUtils.ts b/src/libs/ObjectUtils.ts index 9ffa461506c8..644fe1c7596e 100644 --- a/src/libs/ObjectUtils.ts +++ b/src/libs/ObjectUtils.ts @@ -1,11 +1,11 @@ -// eslint-disable-next-line @typescript-eslint/ban-types -const shallowCompare = (obj1?: object, obj2?: object) => { +const shallowCompare = (obj1?: Record, obj2?: Record): boolean => { if (!obj1 && !obj2) { 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); + const keys2 = Object.keys(obj2); + return keys1.length === keys2.length && keys1.every((key) => obj1[key] === obj2[key]); } return false; }; diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 11c1fd04329f..b25274ef2946 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -1,11 +1,12 @@ import React from 'react'; +import type {MutableRefObject} from 'react'; import type {TextInput} from 'react-native'; import ROUTES from '@src/ROUTES'; import Navigation from './Navigation/Navigation'; type FocusCallback = (shouldFocusForNonBlurInputOnTapOutside?: boolean) => void; -const composerRef = React.createRef(); +const composerRef: MutableRefObject = React.createRef(); const editComposerRef = React.createRef(); // There are two types of composer: general composer (edit composer) and main composer. // The general composer callback will take priority if it exists. diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8911a0623479..b8e4c448fdc2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4808,7 +4808,7 @@ function buildOptimisticTaskReport( return { reportID: generateReportID(), reportName: title, - description, + description: getParsedComment(description ?? ''), ownerAccountID, participants, managerID: assigneeAccountID, @@ -5292,13 +5292,9 @@ function canFlagReportAction(reportAction: OnyxEntry, reportID: st report = getReport(report?.parentReportID); } const isCurrentUserAction = reportAction?.actorAccountID === currentUserAccountID; - const isOriginalMessageHaveHtml = - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT || - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED || - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOS_OOO_LIST; if (ReportActionsUtils.isWhisperAction(reportAction)) { - // Allow flagging welcome message whispers as they can be set by any room creator - if (report?.description && !isCurrentUserAction && isOriginalMessageHaveHtml && reportAction?.originalMessage?.html === report.description) { + // Allow flagging whispers that are sent by other users + if (!isCurrentUserAction && reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE) { return true; } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index aa9a87404924..79e73f1585d2 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -4,7 +4,7 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, RecentWaypoint, Report, TaxRate, TaxRates, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {Policy, RecentWaypoint, Report, TaxRate, TaxRates, 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'; @@ -15,7 +15,6 @@ import * as NumberUtils from './NumberUtils'; import {getCleanedTagName} from './PolicyUtils'; let allTransactions: OnyxCollection = {}; - Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, waitForCollectionCallback: true, @@ -27,6 +26,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, @@ -506,6 +512,40 @@ function hasMissingSmartscanFields(transaction: OnyxEntry): boolean return Boolean(transaction && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction)); } +/** + * Get all transaction violations of the transaction with given tranactionID. + */ +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. + */ +function hasPendingRTERViolation(transactionViolations?: TransactionViolations | null): boolean { + return Boolean( + transactionViolations?.some((transactionViolation: TransactionViolation) => transactionViolation.name === CONST.VIOLATIONS.RTER && transactionViolation.data?.pendingPattern), + ); +} + +/** + * Check if there is pending rter violation in all transactionViolations with given transactionIDs. + */ +function allHavePendingRTERViolation(transactionIds: string[]): boolean { + const transactionsWithRTERViolations = transactionIds.map((transactionId) => { + const transactionViolations = getTransactionViolations(transactionId, allTransactionViolations); + return hasPendingRTERViolation(transactionViolations); + }); + return transactionsWithRTERViolations.length > 0 && transactionsWithRTERViolations.every((value) => value === true); +} + +/** + * Check if the transaction is pending or has a pending rter violation. + */ +function hasPendingUI(transaction: OnyxEntry, transactionViolations?: TransactionViolations | null): boolean { + return isReceiptBeingScanned(transaction) || isPending(transaction) || (!!transaction && hasPendingRTERViolation(transactionViolations)); +} + /** * Check if the transaction has a defined route */ @@ -608,7 +648,7 @@ function isOnHoldByTransactionID(transactionID: string): 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), ); @@ -621,10 +661,6 @@ function hasNoticeTypeViolation(transactionID: string, transactionViolations: On return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'notice')); } -function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection): TransactionViolation[] | null { - return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; -} - /** * this is the formulae to calculate tax */ @@ -763,6 +799,9 @@ export { isCreatedMissing, areRequiredFieldsEmpty, hasMissingSmartscanFields, + hasPendingRTERViolation, + allHavePendingRTERViolation, + hasPendingUI, getWaypointIndex, waypointHasValidAddress, getRecentTransactions, diff --git a/src/libs/Url.ts b/src/libs/Url.ts index 80ca98c712cf..69894147a242 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; } function hasURL(text: string) { diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 7f346933873d..3e09524e1dd1 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -1611,10 +1611,11 @@ function clearAvatarErrors(policyID: string) { * Optimistically update the general settings. Set the general settings as pending until the response succeeds. * If the response fails set a general error message. Clear the error message when updating. */ -function updateGeneralSettings(policyID: string, name: string, currency: string) { +function updateGeneralSettings(policyID: string, name: string, currencyValue?: string) { const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const distanceUnit = Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); const customUnitID = distanceUnit?.customUnitID; + const currency = currencyValue ?? policy?.outputCurrency ?? CONST.CURRENCY.USD; if (!policy) { return; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 8d337df4feba..91a0ce5da930 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -59,7 +59,7 @@ import * as Environment from '@libs/Environment/Environment'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; import * as LoginUtils from '@libs/LoginUtils'; -import Navigation from '@libs/Navigation/Navigation'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import type {NetworkStatus} from '@libs/NetworkConnection'; import LocalNotification from '@libs/Notification/LocalNotification'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -2530,10 +2530,11 @@ function navigateToMostRecentReport(currentReport: OnyxEntry) { const isChatThread = ReportUtils.isChatThread(currentReport); if (lastAccessedReportID) { const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? ''); + // We are using index 1 here because on index 0, we'll always have the bottomTabNavigator. + const isFirstRoute = navigationRef?.current?.getState()?.index === 1; // If it is not a chat thread we should call Navigation.goBack to pop the current route first before navigating to last accessed report. - if (!isChatThread) { - // Fallback to the lastAccessedReportID route, if this is first route in the navigator - Navigation.goBack(lastAccessedReportRoute); + if (!isChatThread && !isFirstRoute) { + Navigation.goBack(); } Navigation.navigate(lastAccessedReportRoute, CONST.NAVIGATION.TYPE.FORCED_UP); } else { diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 89d5b46408f7..1d4415f72f4b 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -440,7 +440,8 @@ function editTask(report: OnyxTypes.Report, {title, description}: OnyxTypes.Task const reportName = (title ?? report?.reportName)?.trim(); // Description can be unset, so we default to an empty string if so - const reportDescription = ReportUtils.getParsedComment((description ?? report.description ?? '').trim()); + const newDescription = description ? ReportUtils.getParsedComment(description) : report.description; + const reportDescription = (newDescription ?? '').trim(); const optimisticData: OnyxUpdate[] = [ { diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index f403ed5a86bc..8274a4af20d6 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -262,7 +262,7 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i } function clearError(transactionID: string) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {errors: null}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {errors: null, errorFields: {route: null}}); } function markAsCash(transactionID: string, transactionThreadReportID: string, existingViolations: TransactionViolation[]) { diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts index 3f4c50924e9a..119b7da42e21 100644 --- a/src/libs/actions/Welcome.ts +++ b/src/libs/actions/Welcome.ts @@ -73,6 +73,10 @@ function setOnboardingAdminsChatReportID(adminsChatReportID?: string) { Onyx.set(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, adminsChatReportID ?? null); } +function setOnboardingPolicyID(policyID?: string) { + Onyx.set(ONYXKEYS.ONBOARDING_POLICY_ID, policyID ?? null); +} + Onyx.connect({ key: ONYXKEYS.NVP_ONBOARDING, initWithStoredValues: false, @@ -134,4 +138,4 @@ function resetAllChecks() { isLoadingReportData = true; } -export {onServerDataReady, isOnboardingFlowCompleted, setOnboardingPurposeSelected, resetAllChecks, setOnboardingAdminsChatReportID}; +export {onServerDataReady, isOnboardingFlowCompleted, setOnboardingPurposeSelected, resetAllChecks, setOnboardingAdminsChatReportID, setOnboardingPolicyID}; diff --git a/src/libs/actions/connections/index.ts b/src/libs/actions/connections/index.ts index f9d16692aedd..4a501910548b 100644 --- a/src/libs/actions/connections/index.ts +++ b/src/libs/actions/connections/index.ts @@ -1,7 +1,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; -import type {RemovePolicyConnectionParams, UpdatePolicyConnectionConfigParams} from '@libs/API/parameters'; +import type {RemovePolicyConnectionParams, UpdateManyPolicyConnectionConfigurationsParams, UpdatePolicyConnectionConfigParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import CONST from '@src/CONST'; @@ -32,6 +32,7 @@ function removePolicyConnection(policyID: string, connectionName: PolicyConnecti }; API.write(WRITE_COMMANDS.REMOVE_POLICY_CONNECTION, parameters, {optimisticData}); } + function updatePolicyConnectionConfig( policyID: string, connectionName: TConnectionName, @@ -114,4 +115,72 @@ function updatePolicyConnectionConfig>( + policyID: string, + connectionName: TConnectionName, + configUpdate: TConfigUpdate, + configCurrentData: TConfigUpdate, +) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + [connectionName]: { + config: { + configUpdate, + pendingFields: Object.fromEntries(Object.keys(configUpdate).map((settingName) => [settingName, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE])), + errorFields: Object.fromEntries(Object.keys(configUpdate).map((settingName) => [settingName, null])), + }, + }, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + [connectionName]: { + config: { + configCurrentData, + pendingFields: Object.fromEntries(Object.keys(configUpdate).map((settingName) => [settingName, null])), + errorFields: Object.fromEntries(Object.keys(configUpdate).map((settingName) => [settingName, ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')])), + }, + }, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + [connectionName]: { + config: { + pendingFields: Object.fromEntries(Object.keys(configUpdate).map((settingName) => [settingName, null])), + errorFields: Object.fromEntries(Object.keys(configUpdate).map((settingName) => [settingName, null])), + }, + }, + }, + }, + }, + ]; + + const parameters: UpdateManyPolicyConnectionConfigurationsParams = { + policyID, + connectionName, + configUpdate: JSON.stringify(configUpdate), + idempotencyKey: Object.keys(configUpdate).join(','), + }; + API.write(WRITE_COMMANDS.UPDATE_MANY_POLICY_CONNECTION_CONFIGS, parameters, {optimisticData, failureData, successData}); +} + +export {removePolicyConnection, updatePolicyConnectionConfig, updateManyPolicyConnectionConfigs}; 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 867bbadf9ea7..4756db4d43ec 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. */} - + ) => { @@ -118,4 +124,7 @@ export default withOnyx({ onboardingPurposeSelected: { key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, }, + onboardingPolicyID: { + key: ONYXKEYS.ONBOARDING_POLICY_ID, + }, })(BaseOnboardingWork); diff --git a/src/pages/OnboardingWork/types.ts b/src/pages/OnboardingWork/types.ts index 954c8c15b31d..6c06b28259e8 100644 --- a/src/pages/OnboardingWork/types.ts +++ b/src/pages/OnboardingWork/types.ts @@ -6,6 +6,9 @@ type OnboardingWorkProps = Record; type BaseOnboardingWorkOnyxProps = { /** Saved onboarding purpose selected by the user */ onboardingPurposeSelected: OnyxEntry; + + /** Saved onboarding purpose selected by the user */ + onboardingPolicyID: OnyxEntry; }; type BaseOnboardingWorkProps = BaseOnboardingWorkOnyxProps & { diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/DateOfBirthUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/DateOfBirthUBO.tsx index b60b19193dbc..f563692f0ae9 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/DateOfBirthUBO.tsx +++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/DateOfBirthUBO.tsx @@ -65,8 +65,7 @@ function DateOfBirthUBO({reimbursementAccountDraft, onNext, isEditing, beneficia submitButtonStyles={[styles.pb5, styles.mb0]} > {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. */} - + { diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 3120bbe9bed2..29b25828d24b 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -318,7 +318,6 @@ function ComposerWithSuggestions( */ const setTextInputRef = useCallback( (el: TextInput) => { - // @ts-expect-error need to reassign this ref ReportActionComposeFocusManager.composerRef.current = el; textInputRef.current = el; if (typeof animatedRef === 'function') { @@ -649,7 +648,6 @@ function ComposerWithSuggestions( const unsubscribeNavigationFocus = navigation.addListener('focus', () => { KeyDownListener.addKeyDownPressListener(focusComposerOnKeyPress); // The report isn't unmounted and can be focused again after going back from another report so we should update the composerRef again - // @ts-expect-error need to reassign this ref ReportActionComposeFocusManager.composerRef.current = textInputRef.current; setUpComposeFocusManager(); }); @@ -767,6 +765,7 @@ function ComposerWithSuggestions( onLayout={onLayout} onScroll={hideSuggestionMenu} shouldContainScroll={Browser.isMobileSafari()} + isGroupPolicyReport={isGroupPolicyReport} /> diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 90d8460a133e..adf358ab416d 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: boolean) => void; + replaceSelectionWithText: EmojiPickerActions.OnEmojiSelected; prepareCommentAndResetComposer: () => string; isFocused: () => boolean; }; @@ -73,9 +73,9 @@ type ReportActionComposeOnyxProps = { type ReportActionComposeProps = ReportActionComposeOnyxProps & WithCurrentUserPersonalDetailsProps & - Pick & { + Pick & { /** A method to call when the form is submitted */ - onSubmit: (newComment: string | undefined) => void; + onSubmit: (newComment: string) => void; /** The report currently being looked at */ report: OnyxEntry; @@ -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, @@ -482,7 +485,6 @@ function ReportActionCompose({ composerRef.current?.replaceSelectionWithText(...args)} emojiPickerID={report?.reportID} shiftVertical={emojiShiftVertical} diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index ae62cac87d7e..95bc4f5231d2 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -64,6 +64,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import {RestrictedReadOnlyContextMenuActions} from './ContextMenu/ContextMenuActions'; @@ -89,9 +90,6 @@ const getDraftMessage = (drafts: OnyxCollection, }; type ReportActionItemOnyxProps = { - /** Stores user's preferred skin tone */ - preferredSkinTone: OnyxEntry; - /** IOU report for this action, if any */ iouReport: OnyxEntry; @@ -105,6 +103,9 @@ type ReportActionItemOnyxProps = { /** Transaction associated with this report, if any */ transaction: OnyxEntry; + + /** The transaction (linked with the report action) route error */ + linkedTransactionRouteError: OnyxEntry; }; type ReportActionItemProps = { @@ -173,7 +174,6 @@ function ReportActionItem({ iouReport, isMostRecentIOUReportAction, parentReportAction, - preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, shouldDisplayNewMarker, userWallet, shouldHideThreadDividerLine = false, @@ -183,6 +183,7 @@ function ReportActionItem({ onPress = undefined, isFirstVisibleReportAction = false, shouldUseThreadDividerLine = false, + linkedTransactionRouteError, }: ReportActionItemProps) { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -672,8 +673,6 @@ function ReportActionItem({ reportID={report.reportID} index={index} ref={textInputRef} - // Avoid defining within component due to an existing Onyx bug - preferredSkinTone={preferredSkinTone} shouldDisableEmojiPicker={(ReportUtils.chatIncludesConcierge(report) && User.isBlockedFromConcierge(blockedFromConcierge)) || ReportUtils.isArchivedRoom(report)} /> )} @@ -977,7 +976,7 @@ function ReportActionItem({ draftMessage !== undefined ? undefined : action.pendingAction ?? (action.isOptimisticAction ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : undefined) } shouldHideOnDelete={!ReportActionsUtils.isThreadParentMessage(action, report.reportID)} - errors={ErrorUtils.getLatestErrorMessageField(action as ErrorUtils.OnyxDataWithErrors)} + errors={linkedTransactionRouteError ?? ErrorUtils.getLatestErrorMessageField(action as ErrorUtils.OnyxDataWithErrors)} errorRowStyles={[styles.ml10, styles.mr2]} needsOffscreenAlphaCompositing={ReportActionsUtils.isMoneyRequestAction(action)} shouldDisableStrikeThrough @@ -1019,10 +1018,6 @@ function ReportActionItem({ } export default withOnyx({ - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE, - }, iouReport: { key: ({action}) => { const iouReportID = ReportActionsUtils.getIOUReportIDFromReportActionPreview(action); @@ -1049,6 +1044,10 @@ export default withOnyx({ return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; }, }, + linkedTransactionRouteError: { + key: ({action}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${(action as OnyxTypes.OriginalMessageIOU)?.originalMessage?.IOUTransactionID ?? 0}`, + selector: (transaction: OnyxEntry) => transaction?.errorFields?.route ?? null, + }, })( memo(ReportActionItem, (prevProps, nextProps) => { const prevParentReportAction = prevProps.parentReportAction; @@ -1083,6 +1082,7 @@ export default withOnyx({ lodashIsEqual(prevProps.transactionThreadReport, nextProps.transactionThreadReport) && lodashIsEqual(prevProps.reportActions, nextProps.reportActions) && lodashIsEqual(prevProps.transaction, nextProps.transaction) && + lodashIsEqual(prevProps.linkedTransactionRouteError, nextProps.linkedTransactionRouteError) && lodashIsEqual(prevParentReportAction, nextParentReportAction) ); }), diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index eda4ef5b1033..6cb03e8dae05 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -4,7 +4,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Keyboard, View} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; @@ -58,9 +58,6 @@ type ReportActionItemMessageEditProps = { /** Whether or not the emoji picker is disabled */ shouldDisableEmojiPicker?: boolean; - - /** Stores user's preferred skin tone */ - preferredSkinTone?: OnyxEntry; }; // native ids @@ -70,9 +67,10 @@ const messageEditInput = 'messageEditInput'; const shouldUseForcedSelectionRange = shouldUseEmojiPickerSelection(); function ReportActionItemMessageEdit( - {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, + {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false}: ReportActionItemMessageEditProps, forwardedRef: ForwardedRef, ) { + const [preferredSkinTone] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, {initialValue: CONST.EMOJI_DEFAULT_SKIN_TONE}); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -421,8 +419,8 @@ function ReportActionItemMessageEdit( }} onBlur={(event: NativeSyntheticEvent) => { setIsFocused(false); - // @ts-expect-error TODO: TextInputFocusEventData doesn't contain relatedTarget. const relatedTargetId = event.nativeEvent?.relatedTarget?.id; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if ((relatedTargetId && [messageEditInput, emojiButtonID].includes(relatedTargetId)) || EmojiPickerAction.isEmojiPickerVisible()) { return; } diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 359d6ba93c15..b3bddb9ba499 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -161,7 +161,6 @@ function ReportFooter({ { const {isSmallScreenWidth} = useWindowDimensions(); const isFocused = useIsFocusedOriginal(); - const topmostCentralPane = useNavigationState(getTopmostCentralPaneRoute); + const topmostCentralPane = useNavigationState | undefined>(getTopmostCentralPaneRoute); return isFocused || (topmostCentralPane?.name === SCREENS.SEARCH.CENTRAL_PANE && isSmallScreenWidth); }; diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/Camera.tsx similarity index 66% rename from src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.tsx rename to src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/Camera.tsx index beeb8938e917..e24901dcaa51 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/Camera.tsx @@ -1,15 +1,15 @@ import React from 'react'; import type {ForwardedRef} from 'react'; -import {Camera} from 'react-native-vision-camera'; +import {Camera as VisionCamera} from 'react-native-vision-camera'; import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; import type {NavigationAwareCameraNativeProps} from './types'; // Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused. -function NavigationAwareCamera({cameraTabIndex, ...props}: NavigationAwareCameraNativeProps, ref: ForwardedRef) { +function Camera({cameraTabIndex, ...props}: NavigationAwareCameraNativeProps, ref: ForwardedRef) { const isCameraActive = useTabNavigatorFocus({tabIndex: cameraTabIndex}); return ( - ) { +function WebCamera({torchOn, onTorchAvailability, cameraTabIndex, ...props}: NavigationAwareCameraProps, ref: ForwardedRef) { const shouldShowCamera = useTabNavigatorFocus({ tabIndex: cameraTabIndex, }); @@ -26,6 +26,6 @@ function NavigationAwareCamera({torchOn, onTorchAvailability, cameraTabIndex, .. ); } -NavigationAwareCamera.displayName = 'NavigationAwareCamera'; +WebCamera.displayName = 'NavigationAwareCamera'; -export default React.forwardRef(NavigationAwareCamera); +export default React.forwardRef(WebCamera); diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts index 0e6845792122..555cb7a92367 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts @@ -12,7 +12,7 @@ type NavigationAwareCameraProps = WebcamProps & { cameraTabIndex: number; }; -type NavigationAwareCameraNativeProps = CameraProps & { +type NavigationAwareCameraNativeProps = Omit & { cameraTabIndex: number; }; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index bafad583bd74..c022a079df65 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -36,7 +36,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Receipt} from '@src/types/onyx/Transaction'; import CameraPermission from './CameraPermission'; -import NavigationAwareCamera from './NavigationAwareCamera'; +import NavigationAwareCamera from './NavigationAwareCamera/Camera'; import type {IOURequestStepOnyxProps, IOURequestStepScanProps} from './types'; function IOURequestStepScan({ @@ -480,8 +480,7 @@ function IOURequestStepScan({ { setCameraPermissionState('granted'); @@ -110,7 +109,6 @@ function IOURequestStepScan({ let deviceId; for (const track of stream.getTracks()) { const setting = track.getSettings(); - // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010 if (setting.zoom === 1) { deviceId = setting.deviceId; break; @@ -154,7 +152,6 @@ function IOURequestStepScan({ } navigator.permissions .query({ - // @ts-expect-error camera does exist in PermissionName name: 'camera', }) .then((permissionState) => { @@ -464,7 +461,6 @@ function IOURequestStepScan({ return; } trackRef.current.applyConstraints({ - // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010 advanced: [{torch: false}], }); }, []); @@ -473,7 +469,6 @@ function IOURequestStepScan({ if (trackRef.current && isFlashLightOn) { trackRef.current .applyConstraints({ - // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010 advanced: [{torch: true}], }) .then(() => { diff --git a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx index 32f72b015fa4..bf1bb8ad197e 100644 --- a/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/StateSelectionPage.tsx @@ -91,13 +91,12 @@ function StateSelectionPage() { shouldShowBackButton onBackButtonPress={() => { const backTo = params?.backTo ?? ''; - let backToRoute = ''; + let backToRoute: Route | undefined; if (backTo) { backToRoute = appendParam(backTo, 'state', currentState ?? ''); } - // @ts-expect-error Navigation.goBack does take a param Navigation.goBack(backToRoute); }} /> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx index 5910128b8faf..f0f1246dc631 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx @@ -23,7 +23,6 @@ function QuickbooksCompanyCardExpenseAccountPage({policy}: WithPolicyConnections const {nonReimbursableBillDefaultVendor, autoCreateVendor, errorFields, pendingFields, nonReimbursableExpensesExportDestination, nonReimbursableExpensesAccount} = policy?.connections?.quickbooksOnline?.config ?? {}; const {vendors} = policy?.connections?.quickbooksOnline?.data ?? {}; - const isVendorSelected = nonReimbursableExpensesExportDestination === CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL; const nonReimbursableBillDefaultVendorObject = vendors?.find((vendor) => vendor.id === nonReimbursableBillDefaultVendor); return ( - {isVendorSelected && ( + + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT.getRoute(policyID))} + brickRoadIndicator={errorFields?.nonReimbursableExpensesAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + shouldShowRightIcon + /> + + {nonReimbursableExpensesExportDestination === CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL && ( <> - - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT.getRoute(policyID))} - brickRoadIndicator={errorFields?.nonReimbursableExpensesAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - shouldShowRightIcon - /> - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.QUICK_BOOKS_CONFIG.AUTO_CREATE_VENDOR, isOn)} + onToggle={(isOn) => + Connections.updateManyPolicyConnectionConfigs( + policyID, + CONST.POLICY.CONNECTIONS.NAME.QBO, + { + [CONST.QUICK_BOOKS_CONFIG.AUTO_CREATE_VENDOR]: isOn, + [CONST.QUICK_BOOKS_CONFIG.NON_REIMBURSABLE_BILL_DEFAULT_VENDOR]: isOn + ? policy?.connections?.quickbooksOnline?.data?.vendors?.[0]?.id ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE + : CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE, + }, + { + [CONST.QUICK_BOOKS_CONFIG.AUTO_CREATE_VENDOR]: autoCreateVendor, + [CONST.QUICK_BOOKS_CONFIG.NON_REIMBURSABLE_BILL_DEFAULT_VENDOR]: + nonReimbursableBillDefaultVendorObject?.id ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE, + }, + ) + } pendingAction={pendingFields?.autoCreateVendor} /> + {autoCreateVendor && ( + + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT.getRoute(policyID))} + brickRoadIndicator={errorFields?.nonReimbursableBillDefaultVendor ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + shouldShowRightIcon + errorText={errorFields?.nonReimbursableBillDefaultVendor ? translate('common.genericErrorMessage') : undefined} + /> + + )} )} - - {isVendorSelected ? ( - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT.getRoute(policyID))} - brickRoadIndicator={errorFields?.nonReimbursableBillDefaultVendor ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - shouldShowRightIcon - errorText={errorFields?.nonReimbursableBillDefaultVendor ? translate('common.genericErrorMessage') : undefined} - /> - ) : ( - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT.getRoute(policyID))} - brickRoadIndicator={errorFields?.nonReimbursableExpensesAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - shouldShowRightIcon - errorText={errorFields?.nonReimbursableExpensesAccount ? translate('common.genericErrorMessage') : undefined} - /> - )} - diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPayableSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPayableSelectPage.tsx deleted file mode 100644 index d7104fd4bbaf..000000000000 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPayableSelectPage.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, {useCallback, useMemo} from 'react'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; -import type {ListItem} from '@components/SelectionList/types'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as Connections from '@libs/actions/connections'; -import Navigation from '@navigation/Navigation'; -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; -import withPolicyConnections from '@pages/workspace/withPolicyConnections'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import type {Account} from '@src/types/onyx/Policy'; - -type CardListItem = ListItem & { - value: Account; -}; - -function QuickbooksCompanyCardExpenseAccountPayableSelectPage({policy}: WithPolicyConnectionsProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const {accountPayable} = policy?.connections?.quickbooksOnline?.data ?? {}; - const {nonReimbursableExpensesAccount} = policy?.connections?.quickbooksOnline?.config ?? {}; - - const policyID = policy?.id ?? ''; - const sections = useMemo(() => { - const data: CardListItem[] = - accountPayable?.map((account) => ({ - value: account, - text: account.name, - keyForList: account.name, - isSelected: account.id === nonReimbursableExpensesAccount?.id, - })) ?? []; - return [{data}]; - }, [nonReimbursableExpensesAccount, accountPayable]); - - const selectAccountPayable = useCallback( - (row: CardListItem) => { - if (row.value.id !== nonReimbursableExpensesAccount?.id) { - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.QUICK_BOOKS_CONFIG.NON_REIMBURSABLE_EXPENSES_ACCOUNT, row.value); - } - Navigation.goBack(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT.getRoute(policyID)); - }, - [nonReimbursableExpensesAccount, policyID], - ); - - return ( - - - - {translate('workspace.qbo.accountsPayableDescription')}} - sections={sections} - ListItem={RadioListItem} - onSelectRow={selectAccountPayable} - initiallyFocusedOptionKey={sections[0].data.find((mode) => mode.isSelected)?.keyForList} - /> - - - ); -} - -QuickbooksCompanyCardExpenseAccountPayableSelectPage.displayName = 'QuickbooksCompanyCardExpenseAccountPayableSelectPage'; - -export default withPolicyConnections(QuickbooksCompanyCardExpenseAccountPayableSelectPage); diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx index 9b7aace8ebcf..4f757eab253f 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx @@ -15,17 +15,20 @@ import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnec import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {QBONonReimbursableExportAccountType} from '@src/types/onyx/Policy'; +import type {Account, QBONonReimbursableExportAccountType} from '@src/types/onyx/Policy'; type AccountListItem = ListItem & { value: QBONonReimbursableExportAccountType; + accounts: Account[]; + defaultVendor: string; }; function QuickbooksCompanyCardExpenseAccountSelectCardPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? ''; - const {nonReimbursableExpensesExportDestination, syncLocations} = policy?.connections?.quickbooksOnline?.config ?? {}; + const {nonReimbursableExpensesExportDestination, nonReimbursableExpensesAccount, syncLocations, nonReimbursableBillDefaultVendor} = policy?.connections?.quickbooksOnline?.config ?? {}; + const {creditCards, bankAccounts, accountPayable, vendors} = policy?.connections?.quickbooksOnline?.data ?? {}; const isLocationEnabled = Boolean(syncLocations && syncLocations !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); const sections = useMemo(() => { @@ -35,12 +38,16 @@ function QuickbooksCompanyCardExpenseAccountSelectCardPage({policy}: WithPolicyC value: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD, keyForList: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD, isSelected: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD === nonReimbursableExpensesExportDestination, + accounts: creditCards ?? [], + defaultVendor: CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE, }, { text: translate(`workspace.qbo.accounts.debit_card`), value: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD, keyForList: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD, isSelected: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD === nonReimbursableExpensesExportDestination, + accounts: bankAccounts ?? [], + defaultVendor: CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE, }, ]; if (!isLocationEnabled) { @@ -49,19 +56,34 @@ function QuickbooksCompanyCardExpenseAccountSelectCardPage({policy}: WithPolicyC value: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL, keyForList: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL, isSelected: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL === nonReimbursableExpensesExportDestination, + accounts: accountPayable ?? [], + defaultVendor: vendors?.[0]?.id ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE, }); } return [{data: options}]; - }, [translate, nonReimbursableExpensesExportDestination, isLocationEnabled]); + }, [translate, nonReimbursableExpensesExportDestination, isLocationEnabled, accountPayable, bankAccounts, creditCards, vendors]); const selectExportCompanyCard = useCallback( (row: AccountListItem) => { if (row.value !== nonReimbursableExpensesExportDestination) { - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.QUICK_BOOKS_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION, row.value); + Connections.updateManyPolicyConnectionConfigs( + policyID, + CONST.POLICY.CONNECTIONS.NAME.QBO, + { + [CONST.QUICK_BOOKS_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: row.value, + [CONST.QUICK_BOOKS_CONFIG.NON_REIMBURSABLE_EXPENSES_ACCOUNT]: row.accounts[0], + [CONST.QUICK_BOOKS_CONFIG.NON_REIMBURSABLE_BILL_DEFAULT_VENDOR]: row.defaultVendor, + }, + { + [CONST.QUICK_BOOKS_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: nonReimbursableExpensesExportDestination, + [CONST.QUICK_BOOKS_CONFIG.NON_REIMBURSABLE_EXPENSES_ACCOUNT]: nonReimbursableExpensesAccount, + [CONST.QUICK_BOOKS_CONFIG.NON_REIMBURSABLE_BILL_DEFAULT_VENDOR]: nonReimbursableBillDefaultVendor, + }, + ); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT.getRoute(policyID)); }, - [nonReimbursableExpensesExportDestination, policyID], + [nonReimbursableExpensesExportDestination, policyID, nonReimbursableExpensesAccount, nonReimbursableBillDefaultVendor], ); return ( diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx index f5c8020db929..98d81a480ddd 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx @@ -24,7 +24,7 @@ function QuickbooksCompanyCardExpenseAccountSelectPage({policy}: WithPolicyConne const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? ''; - const {creditCards, vendors, bankAccounts} = policy?.connections?.quickbooksOnline?.data ?? {}; + const {creditCards, accountPayable, bankAccounts} = policy?.connections?.quickbooksOnline?.data ?? {}; const {nonReimbursableExpensesAccount, nonReimbursableExpensesExportDestination} = policy?.connections?.quickbooksOnline?.config ?? {}; @@ -38,7 +38,7 @@ function QuickbooksCompanyCardExpenseAccountSelectPage({policy}: WithPolicyConne accounts = bankAccounts ?? []; break; case CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL: - accounts = vendors ?? []; + accounts = accountPayable ?? []; break; default: accounts = []; @@ -50,7 +50,7 @@ function QuickbooksCompanyCardExpenseAccountSelectPage({policy}: WithPolicyConne keyForList: card.name, isSelected: card.name === nonReimbursableExpensesAccount?.name, })); - }, [nonReimbursableExpensesAccount, creditCards, bankAccounts, nonReimbursableExpensesExportDestination, vendors]); + }, [nonReimbursableExpensesAccount, creditCards, bankAccounts, nonReimbursableExpensesExportDestination, accountPayable]); const selectExportAccount = useCallback( (row: CardListItem) => { @@ -72,7 +72,7 @@ function QuickbooksCompanyCardExpenseAccountSelectPage({policy}: WithPolicyConne diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx index c04a8ece794f..861f7d416902 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx @@ -23,7 +23,7 @@ function QuickbooksOutOfPocketExpenseConfigurationPage({policy}: WithPolicyConne const isTaxesEnabled = Boolean(syncTax); const shouldShowTaxError = isTaxesEnabled && reimbursableExpensesExportDestination === CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY; const shouldShowLocationError = isLocationEnabled && reimbursableExpensesExportDestination !== CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY; - const hasErrors = Boolean(errorFields?.exportEntity) || shouldShowTaxError || shouldShowLocationError; + const hasErrors = Boolean(errorFields?.reimbursableExpensesExportDestination) || shouldShowTaxError || shouldShowLocationError; return ( >; function QuickbooksOutOfPocketExpenseEntitySelectPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const {reimbursableExpensesExportDestination, syncTax, syncLocations} = policy?.connections?.quickbooksOnline?.config ?? {}; + const {reimbursableExpensesExportDestination, reimbursableExpensesAccount, syncTax, syncLocations} = policy?.connections?.quickbooksOnline?.config ?? {}; + const {bankAccounts, accountPayable, journalEntryAccounts} = policy?.connections?.quickbooksOnline?.data ?? {}; const isLocationsEnabled = Boolean(syncLocations && syncLocations !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); const isTaxesEnabled = Boolean(syncTax); const policyID = policy?.id ?? ''; @@ -40,13 +42,15 @@ function QuickbooksOutOfPocketExpenseEntitySelectPage({policy}: WithPolicyConnec keyForList: CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK, isSelected: reimbursableExpensesExportDestination === CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK, isShown: !isLocationsEnabled, + accounts: bankAccounts ?? [], }, { value: CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY, text: translate(`workspace.qbo.accounts.journal_entry`), keyForList: CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY, isSelected: reimbursableExpensesExportDestination === CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY, - isShown: !isTaxesEnabled || isLocationsEnabled, + isShown: !isTaxesEnabled, + accounts: journalEntryAccounts ?? [], }, { value: CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL, @@ -54,9 +58,10 @@ function QuickbooksOutOfPocketExpenseEntitySelectPage({policy}: WithPolicyConnec keyForList: CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL, isSelected: reimbursableExpensesExportDestination === CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL, isShown: !isLocationsEnabled, + accounts: accountPayable ?? [], }, ], - [reimbursableExpensesExportDestination, isTaxesEnabled, translate, isLocationsEnabled], + [reimbursableExpensesExportDestination, isTaxesEnabled, translate, isLocationsEnabled, bankAccounts, accountPayable, journalEntryAccounts], ); const sections: CardsSection[] = useMemo(() => [{data: data.filter((item) => item.isShown)}], [data]); @@ -64,11 +69,22 @@ function QuickbooksOutOfPocketExpenseEntitySelectPage({policy}: WithPolicyConnec const selectExportEntity = useCallback( (row: CardListItem) => { if (row.value !== reimbursableExpensesExportDestination) { - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.QUICK_BOOKS_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION, row.value); + Connections.updateManyPolicyConnectionConfigs( + policyID, + CONST.POLICY.CONNECTIONS.NAME.QBO, + { + [CONST.QUICK_BOOKS_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: row.value, + [CONST.QUICK_BOOKS_CONFIG.REIMBURSABLE_EXPENSES_ACCOUNT]: row.accounts[0], + }, + { + [CONST.QUICK_BOOKS_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: reimbursableExpensesExportDestination, + [CONST.QUICK_BOOKS_CONFIG.REIMBURSABLE_EXPENSES_ACCOUNT]: reimbursableExpensesAccount, + }, + ); } Navigation.goBack(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_OUT_OF_POCKET_EXPENSES.getRoute(policyID)); }, - [reimbursableExpensesExportDestination, policyID], + [reimbursableExpensesExportDestination, policyID, reimbursableExpensesAccount], ); return ( diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx index 9cd6e9e1653f..9b8cec10d100 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx @@ -19,7 +19,9 @@ function QuickbooksTaxesPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyID = policy?.id ?? ''; - const {syncTax, pendingFields} = policy?.connections?.quickbooksOnline?.config ?? {}; + const {syncTax, pendingFields, reimbursableExpensesExportDestination} = policy?.connections?.quickbooksOnline?.config ?? {}; + const isJournalExportEntity = reimbursableExpensesExportDestination === CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY; + return ( Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.QUICK_BOOKS_CONFIG.SYNC_TAX, !syncTax)} + disabled={!syncTax && isJournalExportEntity} /> + {isJournalExportEntity && {translate('workspace.qbo.taxesJournalEntrySwitchNote')}} diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 7b2aa78545f2..4f1cb550cf05 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -16,11 +16,13 @@ import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as UserUtils from '@libs/UserUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -56,6 +58,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const memberLogin = personalDetails?.[accountID]?.login ?? ''; const member = policy?.employeeList?.[memberLogin]; + const prevMember = usePrevious(member); const details = personalDetails?.[accountID] ?? ({} as PersonalDetails); const avatar = details.avatar ?? UserUtils.getDefaultAvatar(); const fallbackIcon = details.fallbackIcon ?? ''; @@ -95,6 +98,13 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM } }, [accountID, policy?.errorFields?.changeOwner, policy?.isChangeOwnerSuccessful, policyID]); + useEffect(() => { + if (!prevMember || prevMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || member?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return; + } + Navigation.goBack(); + }, [member, prevMember]); + const askForConfirmationToRemove = () => { setIsRemoveMemberConfirmModalVisible(true); }; @@ -102,7 +112,6 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const removeUser = useCallback(() => { Policy.removeMembers([accountID], policyID); setIsRemoveMemberConfirmModalVisible(false); - Navigation.goBack(); }, [accountID, policyID]); const navigateToProfile = useCallback(() => { @@ -127,6 +136,14 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM Navigation.navigate(ROUTES.WORKSPACE_OWNER_CHANGE_CHECK.getRoute(policyID, accountID, 'amountOwed' as ValueOf)); }, [accountID, policyID]); + // eslint-disable-next-line rulesdir/no-negated-variables + const shouldShowNotFoundPage = + !member || (member.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && prevMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + + if (shouldShowNotFoundPage) { + return ; + } + return ( Promise; log: (key: OnyxKey) => void}; // We intentionally do not offer an Onyx.get API because we believe it will lead to code patterns we don't want to use in this repo, but we can offer a workaround for the sake of debugging - // @ts-expect-error TS233 - injecting additional utility for use in runtime debugging, should not be used in any compiled code window.Onyx.get = function (key) { return new Promise((resolve) => { // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs @@ -35,9 +35,7 @@ export default function addUtilsToWindow() { }); }; - // @ts-expect-error TS233 - injecting additional utility for use in runtime debugging, should not be used in any compiled code window.Onyx.log = function (key) { - // @ts-expect-error TS2339 - using additional utility injected above window.Onyx.get(key).then((value) => { /* eslint-disable-next-line no-console */ console.log(value); diff --git a/src/stories/Form.stories.tsx b/src/stories/Form.stories.tsx index 8a1c2ca0b8f0..f4e89f6766f0 100644 --- a/src/stories/Form.stories.tsx +++ b/src/stories/Form.stories.tsx @@ -1,5 +1,6 @@ import type {Meta, StoryFn} from '@storybook/react'; import React, {useState} from 'react'; +import type {ComponentType} from 'react'; import {View} from 'react-native'; import AddressSearch from '@components/AddressSearch'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; @@ -36,29 +37,17 @@ type StorybookFormErrors = Partial>; const STORYBOOK_FORM_ID = 'TestForm' as keyof OnyxFormValuesMapping; -/** - * We use the Component Story Format for writing stories. Follow the docs here: - * - * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format - */ const story: Meta = { title: 'Components/Form', component: FormProvider, subcomponents: { - // @ts-expect-error Subcomponent passes props with unknown type causing a TS error - InputWrapper, - // @ts-expect-error Subcomponent passes props with unknown type causing a TS error - TextInput, - // @ts-expect-error Subcomponent passes props with unknown type causing a TS error - AddressSearch, - // @ts-expect-error Subcomponent passes props with unknown type causing a TS error - CheckboxWithLabel, - // @ts-expect-error Subcomponent passes props with unknown type causing a TS error - Picker, - // @ts-expect-error Subcomponent passes props with unknown type causing a TS error - StateSelector, - // @ts-expect-error Subcomponent passes props with unknown type causing a TS error - DatePicker, + InputWrapper: InputWrapper as ComponentType, + TextInput: TextInput as ComponentType, + AddressSearch: AddressSearch as ComponentType, + CheckboxWithLabel: CheckboxWithLabel as ComponentType, + Picker: Picker as ComponentType, + StateSelector: StateSelector as ComponentType, + DatePicker: DatePicker as ComponentType, }, }; diff --git a/src/styles/index.ts b/src/styles/index.ts index 451dfc0c1820..45e11dbe6cb9 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -2235,6 +2235,15 @@ const styles = (theme: ThemeColors) => width: 200, }, + chatItemPDFAttachmentLoading: { + backgroundColor: 'transparent', + borderColor: theme.border, + borderWidth: 1, + borderRadius: variables.componentBorderRadiusNormal, + ...flex.alignItemsCenter, + ...flex.justifyContentCenter, + }, + sidebarVisible: { borderRightWidth: 1, }, diff --git a/src/types/modules/dom.d.ts b/src/types/modules/dom.d.ts index 60bd9c9ae983..029b4b3d6de5 100644 --- a/src/types/modules/dom.d.ts +++ b/src/types/modules/dom.d.ts @@ -19,6 +19,51 @@ declare global { AppleIDSignInOnSuccess: AppleIDSignInOnSuccessEvent; AppleIDSignInOnFailure: AppleIDSignInOnFailureEvent; } + + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Permissions { + /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Permissions/query) */ + query(permissionDesc: {name: 'geolocation' | 'notifications' | 'persistent-storage' | 'push' | 'screen-wake-lock' | 'xr-spatial-tracking' | 'camera'}): Promise; + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface MediaTrackConstraintSet { + aspectRatio?: ConstrainDouble; + autoGainControl?: ConstrainBoolean; + channelCount?: ConstrainULong; + deviceId?: ConstrainDOMString; + displaySurface?: ConstrainDOMString; + echoCancellation?: ConstrainBoolean; + facingMode?: ConstrainDOMString; + frameRate?: ConstrainDouble; + groupId?: ConstrainDOMString; + height?: ConstrainULong; + noiseSuppression?: ConstrainBoolean; + sampleRate?: ConstrainULong; + sampleSize?: ConstrainULong; + width?: ConstrainULong; + zoom?: {ideal: number}; + torch?: boolean; + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface MediaTrackSettings { + aspectRatio?: number; + autoGainControl?: boolean; + channelCount?: number; + deviceId?: string; + displaySurface?: string; + echoCancellation?: boolean; + facingMode?: string; + frameRate?: number; + groupId?: string; + height?: number; + noiseSuppression?: boolean; + sampleRate?: number; + sampleSize?: number; + width?: number; + zoom?: number; + } } export type {AppleIDSignInOnFailureEvent, AppleIDSignInOnSuccessEvent}; diff --git a/src/types/modules/pdf.worker.d.ts b/src/types/modules/pdf.worker.d.ts new file mode 100644 index 000000000000..307d0ff53a63 --- /dev/null +++ b/src/types/modules/pdf.worker.d.ts @@ -0,0 +1 @@ +declare module 'pdfjs-dist/legacy/build/pdf.worker'; diff --git a/src/types/modules/react-native-onyx.d.ts b/src/types/modules/react-native-onyx.d.ts index 8498b03ec933..453f707165e1 100644 --- a/src/types/modules/react-native-onyx.d.ts +++ b/src/types/modules/react-native-onyx.d.ts @@ -1,4 +1,5 @@ import type Onyx from 'react-native-onyx'; +import type {CollectionKeyBase} from 'react-native-onyx/dist/types'; import type {OnyxCollectionKey, OnyxFormDraftKey, OnyxFormKey, OnyxValueKey, OnyxValues} from '@src/ONYXKEYS'; declare module 'react-native-onyx' { @@ -9,11 +10,13 @@ declare module 'react-native-onyx' { values: OnyxValues; } } - declare global { // Global methods for Onyx key management for debugging purposes // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface Window { - Onyx: typeof Onyx; + Onyx: typeof Onyx & { + get: (key: CollectionKeyBase) => Promise; + log: (key: CollectionKeyBase) => void; + }; } } diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index effbdd9d28fa..a9ff35c97343 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -196,6 +196,14 @@ declare module 'react-native' { touchHistory: TouchHistory; }; + interface TextInputFocusEventData extends TargetedEvent { + text: string; + eventCount: number; + relatedTarget?: { + id?: string; + }; + } + // https://necolas.github.io/react-native-web/docs/interactions/#responderevent-props-api // Extracted from react-native-web, packages/react-native-web/src/modules/useResponderEvents/ResponderSystem.js interface ResponderProps { @@ -360,4 +368,11 @@ declare module 'react-native' { BootSplash: BootSplashModule; HybridAppModule: HybridAppModule; } + + namespace Animated { + interface AnimatedInterpolation extends AnimatedWithChildren { + interpolate(config: InterpolationConfigType): AnimatedInterpolation; + __getValue: () => OutputT; + } + } } 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; }; }; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 67525f2e2318..ac320729b2b7 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -24,6 +24,7 @@ import type {ReportActionBase} from '@src/types/onyx/ReportAction'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PusherHelper from '../utils/PusherHelper'; +import type {MockFetch} from '../utils/TestHelper'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForNetworkPromises from '../utils/waitForNetworkPromises'; @@ -60,9 +61,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); }); @@ -77,179 +79,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?.participants).toEqual({[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT}); - expect(chatReport?.iouReportID).toBe(iouReport?.reportID); + // They should be linked together + expect(chatReport?.participants).toEqual({[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT}); + 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', () => { @@ -269,152 +267,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', () => { @@ -469,146 +462,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', () => { @@ -621,8 +610,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() @@ -733,10 +721,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( () => @@ -924,8 +910,7 @@ 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) ); }); }); @@ -1063,356 +1048,350 @@ 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.participants, {[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [VIT_ACCOUNT_ID]: VIT_PARTICIPANT}), - ) ?? 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.participants, { - [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT, - [JULES_ACCOUNT_ID]: JULES_PARTICIPANT, - [VIT_ACCOUNT_ID]: VIT_PARTICIPANT, - [RORY_ACCOUNT_ID]: RORY_PARTICIPANT, - }), - ) ?? 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); + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + 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; + // 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.participants, {[RORY_ACCOUNT_ID]: RORY_PARTICIPANT, [VIT_ACCOUNT_ID]: VIT_PARTICIPANT}), + ) ?? 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.participants, { + [CARLOS_ACCOUNT_ID]: CARLOS_PARTICIPANT, + [JULES_ACCOUNT_ID]: JULES_PARTICIPANT, + [VIT_ACCOUNT_ID]: VIT_PARTICIPANT, + [RORY_ACCOUNT_ID]: RORY_PARTICIPANT, + }), + ) ?? null; + expect(isEmptyObject(groupChat)).toBe(false); + expect(groupChat?.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); - expect(carlosTransaction?.reportID).toBe(carlosIOUReport?.reportID); - expect(julesTransaction?.reportID).toBe(julesIOUReport?.reportID); - expect(vitTransaction?.reportID).toBe(vitIOUReport?.reportID); - expect(groupTransaction).toBeTruthy(); + // 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(carlosTransaction?.amount).toBe(amount / 4); - expect(julesTransaction?.amount).toBe(amount / 4); - expect(vitTransaction?.amount).toBe(amount / 4); - expect(groupTransaction?.amount).toBe(amount); + expect(julesChatReport?.iouReportID).toBe(julesIOUReport?.reportID); + expect(julesIOUReport?.chatReportID).toBe(julesChatReport?.reportID); - 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(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(); + }, + }); + }), + ); }); }); @@ -1426,189 +1405,185 @@ 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(); + }, + }); + }), + ) + .then(mockFetch?.resume) + .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(); + 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(); + }, + }); + }), + ); }); }); @@ -1618,8 +1593,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', () => { @@ -1628,8 +1602,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(() => { @@ -1772,8 +1745,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?.(); }); }); @@ -1839,8 +1811,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( @@ -1933,16 +1904,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(() => { @@ -2099,8 +2068,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); } @@ -2250,8 +2218,7 @@ describe('actions/IOU', () => { 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(); + mockFetch?.pause?.(); if (transaction && createIOUAction) { // When the expense is deleted @@ -2290,8 +2257,7 @@ 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(); + mockFetch?.resume?.(); await waitForBatchedUpdates(); // Then we recheck the IOU report action from the report actions collection @@ -2326,8 +2292,7 @@ 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(); + mockFetch?.pause?.(); if (transaction && createIOUAction) { // When the IOU expense is deleted @@ -2350,8 +2315,7 @@ describe('actions/IOU', () => { 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(); + mockFetch?.resume?.(); await waitForBatchedUpdates(); report = await new Promise>((resolve) => { @@ -2403,8 +2367,7 @@ 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); } @@ -2430,8 +2393,7 @@ describe('actions/IOU', () => { 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(); + mockFetch?.resume?.(); allReports = await new Promise>((resolve) => { const connectionID = Onyx.connect({ @@ -2495,8 +2457,7 @@ 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) { @@ -2518,8 +2479,7 @@ 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(); + mockFetch?.resume?.(); // Then After resuming fetch, the report for the given thread ID still does not exist report = await new Promise>((resolve) => { @@ -2687,8 +2647,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 @@ -2711,8 +2670,7 @@ describe('actions/IOU', () => { // When fetch 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}`, @@ -2835,8 +2793,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); @@ -2859,8 +2816,7 @@ 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(); + mockFetch?.resume?.(); // Then we expect the moneyRequestPreview to show [Deleted expense] @@ -2909,8 +2865,7 @@ 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); @@ -2925,8 +2880,7 @@ 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(); @@ -3002,8 +2956,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); @@ -3028,8 +2981,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({ @@ -3355,8 +3307,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/PolicyCategoryTest.ts b/tests/actions/PolicyCategoryTest.ts new file mode 100644 index 000000000000..2817a1661db4 --- /dev/null +++ b/tests/actions/PolicyCategoryTest.ts @@ -0,0 +1,255 @@ +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', async () => { + 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(); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + Policy.setWorkspaceRequiresCategory(fakePolicy.id, true); + await waitForBatchedUpdates(); + await 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. + await fetch.resume(); + await waitForBatchedUpdates(); + await 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', async () => { + 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); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); + Policy.createPolicyCategory(fakePolicy.id, newCategoryName); + await waitForBatchedUpdates(); + await 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. + await fetch.resume(); + await waitForBatchedUpdates(); + await 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', async () => { + 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(); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); + Policy.renamePolicyCategory(fakePolicy.id, { + oldName: oldCategoryName, + newName: newCategoryName, + }); + await waitForBatchedUpdates(); + await 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. + await fetch.resume(); + await waitForBatchedUpdates(); + await 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', async () => { + 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(); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); + Policy.setWorkspaceCategoryEnabled(fakePolicy.id, categoriesToUpdate); + await waitForBatchedUpdates(); + await 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. + await fetch.resume(); + await waitForBatchedUpdates(); + await 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', async () => { + 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(); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${fakePolicy.id}`, fakeCategories); + Policy.deleteWorkspaceCategories(fakePolicy.id, categoriesToDelete); + await waitForBatchedUpdates(); + await 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. + await fetch.resume(); + await waitForBatchedUpdates(); + await 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..8d982d4a1892 --- /dev/null +++ b/tests/actions/PolicyMemberTest.ts @@ -0,0 +1,238 @@ +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 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'; +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', async () => { + 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(); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport); + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, { + [fakeReportAction.reportActionID]: fakeReportAction, + }); + Policy.acceptJoinRequest(fakeReport.reportID, fakeReportAction); + await waitForBatchedUpdates(); + await 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. + await fetch.resume(); + await waitForBatchedUpdates(); + await 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', async () => { + const fakeUser2 = createPersonalDetails(2); + const fakePolicy: PolicyType = { + ...createRandomPolicy(0), + employeeList: { + [fakeUser2.login ?? '']: { + email: fakeUser2.login, + role: CONST.POLICY.ROLE.USER, + }, + }, + }; + + // @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); + Onyx.set(`${ONYXKEYS.PERSONAL_DETAILS_LIST}`, {[fakeUser2.accountID]: fakeUser2}); + await waitForBatchedUpdates(); + Policy.updateWorkspaceMembersRole(fakePolicy.id, [fakeUser2.accountID], CONST.POLICY.ROLE.ADMIN); + await waitForBatchedUpdates(); + await 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); + + resolve(); + }, + }); + }); + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + await fetch.resume(); + await waitForBatchedUpdates(); + await 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', async () => { + 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(); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + Onyx.merge(ONYXKEYS.SESSION, {email: fakeEmail, accountID: fakeAccountID}); + Policy.requestWorkspaceOwnerChange(fakePolicy.id); + await waitForBatchedUpdates(); + await 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. + await fetch.resume(); + await waitForBatchedUpdates(); + await 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(); + resolve(); + }, + }); + }); + }); + }); + describe('addBillingCardAndRequestPolicyOwnerChange', () => { + it('Add billing card and change the workspace`s owner', async () => { + 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(); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + Onyx.merge(ONYXKEYS.SESSION, {email: fakeEmail, accountID: fakeAccountID}); + Policy.addBillingCardAndRequestPolicyOwnerChange(fakePolicy.id, fakeCard); + await waitForBatchedUpdates(); + await 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. + await fetch.resume(); + await waitForBatchedUpdates(); + await 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(); + resolve(); + }, + }); + }); + }); + }); +}); diff --git a/tests/actions/PolicyProfileTest.ts b/tests/actions/PolicyProfileTest.ts new file mode 100644 index 000000000000..21ee34568100 --- /dev/null +++ b/tests/actions/PolicyProfileTest.ts @@ -0,0 +1,68 @@ +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', async () => { + 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(); + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + Policy.updateWorkspaceDescription(fakePolicy.id, newDescription, oldDescription); + await waitForBatchedUpdates(); + await 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. + await fetch.resume(); + await waitForBatchedUpdates(); + await 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/actions/PolicyTagTest.ts b/tests/actions/PolicyTagTest.ts index ff4439c392fa..74ea13f3d139 100644 --- a/tests/actions/PolicyTagTest.ts +++ b/tests/actions/PolicyTagTest.ts @@ -7,6 +7,7 @@ import type {PolicyTags} from '@src/types/onyx'; import createRandomPolicy from '../utils/collections/policies'; import createRandomPolicyTags from '../utils/collections/policyTags'; import * as TestHelper from '../utils/TestHelper'; +import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; OnyxUpdateManager(); @@ -17,9 +18,10 @@ describe('actions/Policy', () => { }); }); + 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); }); @@ -28,139 +30,127 @@ describe('actions/Policy', () => { const fakePolicy = createRandomPolicy(0); fakePolicy.requiresTag = 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.setPolicyRequiresTag(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); - - // RequiresTag is enabled and pending - expect(policy?.requiresTag).toBeTruthy(); - expect(policy?.pendingFields?.requiresTag).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); - expect(policy?.pendingFields?.requiresTag).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.setPolicyRequiresTag(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); + + // RequiresTag is enabled and pending + expect(policy?.requiresTag).toBeTruthy(); + expect(policy?.pendingFields?.requiresTag).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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?.requiresTag).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('disable require tag', () => { const fakePolicy = createRandomPolicy(0); fakePolicy.requiresTag = 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(() => { - Policy.setPolicyRequiresTag(fakePolicy.id, false); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policy) => { - Onyx.disconnect(connectionID); - - // RequiresTag is disabled and pending - expect(policy?.requiresTag).toBeFalsy(); - expect(policy?.pendingFields?.requiresTag).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); - expect(policy?.pendingFields?.requiresTag).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Policy.setPolicyRequiresTag(fakePolicy.id, false); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + + // RequiresTag is disabled and pending + expect(policy?.requiresTag).toBeFalsy(); + expect(policy?.pendingFields?.requiresTag).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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?.requiresTag).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('reset require tag when api returns an error', () => { const fakePolicy = createRandomPolicy(0); fakePolicy.requiresTag = 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(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.fail(); - Policy.setPolicyRequiresTag(fakePolicy.id, false); - return waitForBatchedUpdates(); - }) - // @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?.requiresTag).toBeFalsy(); - expect(policy?.errors).toBeTruthy(); - expect(policy?.requiresTag).toBeTruthy(); - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + mockFetch?.fail?.(); + Policy.setPolicyRequiresTag(fakePolicy.id, false); + return waitForBatchedUpdates(); + }) + + .then(mockFetch?.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?.requiresTag).toBeFalsy(); + expect(policy?.errors).toBeTruthy(); + expect(policy?.requiresTag).toBeTruthy(); + resolve(); + }, + }); + }), + ); }); }); @@ -173,65 +163,61 @@ describe('actions/Policy', () => { const newTagListName = 'New tag list name'; const fakePolicyTags = createRandomPolicyTags(oldTagListName); - // @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_TAGS}${fakePolicy.id}`, fakePolicyTags); - }) - .then(() => { - Policy.renamePolicyTaglist( - fakePolicy.id, - { - oldName: oldTagListName, - newName: newTagListName, - }, - fakePolicyTags, - ); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - // Tag list name is updated and pending - expect(Object.keys(policyTags?.[oldTagListName] ?? {}).length).toBe(0); - expect(policyTags?.[newTagListName]?.name).toBe(newTagListName); - expect(policyTags?.[newTagListName]?.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(waitForBatchedUpdates) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - expect(policyTags?.[newTagListName]?.pendingAction).toBeFalsy(); - expect(Object.keys(policyTags?.[oldTagListName] ?? {}).length).toBe(0); - - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + }) + .then(() => { + Policy.renamePolicyTaglist( + fakePolicy.id, + { + oldName: oldTagListName, + newName: newTagListName, + }, + fakePolicyTags, + ); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + // Tag list name is updated and pending + expect(Object.keys(policyTags?.[oldTagListName] ?? {}).length).toBe(0); + expect(policyTags?.[newTagListName]?.name).toBe(newTagListName); + expect(policyTags?.[newTagListName]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + }), + ) + .then(mockFetch?.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + expect(policyTags?.[newTagListName]?.pendingAction).toBeFalsy(); + expect(Object.keys(policyTags?.[oldTagListName] ?? {}).length).toBe(0); + + resolve(); + }, + }); + }), + ); }); it('reset the policy tag list name when api returns error', () => { @@ -242,50 +228,45 @@ describe('actions/Policy', () => { const newTagListName = 'New tag list name'; const fakePolicyTags = createRandomPolicyTags(oldTagListName); - // @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_TAGS}${fakePolicy.id}`, fakePolicyTags); - }) - .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.fail(); - - Policy.renamePolicyTaglist( - fakePolicy.id, - { - oldName: oldTagListName, - newName: newTagListName, - }, - fakePolicyTags, - ); - return waitForBatchedUpdates(); - }) - // @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_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - expect(policyTags?.[newTagListName]).toBeFalsy(); - expect(policyTags?.[oldTagListName]).toBeTruthy(); - expect(policyTags?.errors).toBeTruthy(); - - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + }) + .then(() => { + mockFetch?.fail?.(); + + Policy.renamePolicyTaglist( + fakePolicy.id, + { + oldName: oldTagListName, + newName: newTagListName, + }, + fakePolicyTags, + ); + return waitForBatchedUpdates(); + }) + .then(mockFetch?.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + expect(policyTags?.[newTagListName]).toBeFalsy(); + expect(policyTags?.[oldTagListName]).toBeTruthy(); + expect(policyTags?.errors).toBeTruthy(); + + resolve(); + }, + }); + }), + ); }); }); @@ -298,60 +279,56 @@ describe('actions/Policy', () => { const newTagName = 'new tag'; const fakePolicyTags = createRandomPolicyTags(tagListName); - // @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_TAGS}${fakePolicy.id}`, fakePolicyTags); - }) - .then(() => { - Policy.createPolicyTag(fakePolicy.id, newTagName); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - const newTag = policyTags?.[tagListName]?.tags?.[newTagName]; - expect(newTag?.name).toBe(newTagName); - expect(newTag?.enabled).toBe(true); - expect(newTag?.errors).toBeFalsy(); - expect(newTag?.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(waitForBatchedUpdates) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - const newTag = policyTags?.[tagListName]?.tags?.[newTagName]; - expect(newTag?.errors).toBeFalsy(); - expect(newTag?.pendingAction).toBeFalsy(); - - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + }) + .then(() => { + Policy.createPolicyTag(fakePolicy.id, newTagName); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + const newTag = policyTags?.[tagListName]?.tags?.[newTagName]; + expect(newTag?.name).toBe(newTagName); + expect(newTag?.enabled).toBe(true); + expect(newTag?.errors).toBeFalsy(); + expect(newTag?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + }), + ) + .then(mockFetch?.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + const newTag = policyTags?.[tagListName]?.tags?.[newTagName]; + expect(newTag?.errors).toBeFalsy(); + expect(newTag?.pendingAction).toBeFalsy(); + + resolve(); + }, + }); + }), + ); }); it('reset new policy tag when api returns error', () => { @@ -362,42 +339,37 @@ describe('actions/Policy', () => { const newTagName = 'new tag'; const fakePolicyTags = createRandomPolicyTags(tagListName); - // @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_TAGS}${fakePolicy.id}`, fakePolicyTags); - }) - .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.fail(); - - Policy.createPolicyTag(fakePolicy.id, newTagName); - return waitForBatchedUpdates(); - }) - // @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_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - const newTag = policyTags?.[tagListName]?.tags?.[newTagName]; - expect(newTag?.errors).toBeTruthy(); - - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + }) + .then(() => { + mockFetch?.fail?.(); + + Policy.createPolicyTag(fakePolicy.id, newTagName); + return waitForBatchedUpdates(); + }) + .then(mockFetch?.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + const newTag = policyTags?.[tagListName]?.tags?.[newTagName]; + expect(newTag?.errors).toBeTruthy(); + + resolve(); + }, + }); + }), + ); }); }); @@ -416,65 +388,61 @@ describe('actions/Policy', () => { return acc; }, {}); - // @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_TAGS}${fakePolicy.id}`, fakePolicyTags); - }) - .then(() => { - Policy.setWorkspaceTagEnabled(fakePolicy.id, tagsToUpdate); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - Object.keys(tagsToUpdate).forEach((key) => { - const updatedTag = policyTags?.[tagListName]?.tags[key]; - expect(updatedTag?.enabled).toBeFalsy(); - expect(updatedTag?.errors).toBeFalsy(); - expect(updatedTag?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - expect(updatedTag?.pendingFields?.enabled).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_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - Object.keys(tagsToUpdate).forEach((key) => { - const updatedTag = policyTags?.[tagListName]?.tags[key]; - expect(updatedTag?.errors).toBeFalsy(); - expect(updatedTag?.pendingAction).toBeFalsy(); - expect(updatedTag?.pendingFields?.enabled).toBeFalsy(); - }); - - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + }) + .then(() => { + Policy.setWorkspaceTagEnabled(fakePolicy.id, tagsToUpdate); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + Object.keys(tagsToUpdate).forEach((key) => { + const updatedTag = policyTags?.[tagListName]?.tags[key]; + expect(updatedTag?.enabled).toBeFalsy(); + expect(updatedTag?.errors).toBeFalsy(); + expect(updatedTag?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTag?.pendingFields?.enabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + }); + + resolve(); + }, + }); + }), + ) + .then(mockFetch?.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + Object.keys(tagsToUpdate).forEach((key) => { + const updatedTag = policyTags?.[tagListName]?.tags[key]; + expect(updatedTag?.errors).toBeFalsy(); + expect(updatedTag?.pendingAction).toBeFalsy(); + expect(updatedTag?.pendingFields?.enabled).toBeFalsy(); + }); + + resolve(); + }, + }); + }), + ); }); it('reset policy tag enable when api returns error', () => { @@ -491,46 +459,41 @@ describe('actions/Policy', () => { return acc; }, {}); - // @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_TAGS}${fakePolicy.id}`, fakePolicyTags); - }) - .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.fail(); - - Policy.setWorkspaceTagEnabled(fakePolicy.id, tagsToUpdate); - return waitForBatchedUpdates(); - }) - // @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_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - Object.keys(tagsToUpdate).forEach((key) => { - const updatedTag = policyTags?.[tagListName]?.tags[key]; - expect(updatedTag?.errors).toBeTruthy(); - expect(updatedTag?.pendingAction).toBeFalsy(); - expect(updatedTag?.pendingFields?.enabled).toBeFalsy(); - }); - - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + }) + .then(() => { + mockFetch?.fail?.(); + + Policy.setWorkspaceTagEnabled(fakePolicy.id, tagsToUpdate); + return waitForBatchedUpdates(); + }) + .then(mockFetch?.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + Object.keys(tagsToUpdate).forEach((key) => { + const updatedTag = policyTags?.[tagListName]?.tags[key]; + expect(updatedTag?.errors).toBeTruthy(); + expect(updatedTag?.pendingAction).toBeFalsy(); + expect(updatedTag?.pendingFields?.enabled).toBeFalsy(); + }); + + resolve(); + }, + }); + }), + ); }); }); @@ -544,63 +507,59 @@ describe('actions/Policy', () => { const oldTagName = Object.keys(fakePolicyTags?.[tagListName]?.tags)[0]; const newTagName = 'New tag'; - // @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_TAGS}${fakePolicy.id}`, fakePolicyTags); - }) - .then(() => { - Policy.renamePolicyTag(fakePolicy.id, { - oldName: oldTagName, - newName: newTagName, - }); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - const tags = policyTags?.[tagListName]?.tags; - expect(tags?.[oldTagName]).toBeFalsy(); - expect(tags?.[newTagName]?.name).toBe(newTagName); - expect(tags?.[newTagName]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - expect(tags?.[newTagName]?.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_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - const tags = policyTags?.[tagListName]?.tags; - expect(tags?.[newTagName]?.pendingAction).toBeFalsy(); - expect(tags?.[newTagName]?.pendingFields?.name).toBeFalsy(); - - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + }) + .then(() => { + Policy.renamePolicyTag(fakePolicy.id, { + oldName: oldTagName, + newName: newTagName, + }); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + const tags = policyTags?.[tagListName]?.tags; + expect(tags?.[oldTagName]).toBeFalsy(); + expect(tags?.[newTagName]?.name).toBe(newTagName); + expect(tags?.[newTagName]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(tags?.[newTagName]?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + resolve(); + }, + }); + }), + ) + .then(mockFetch?.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + const tags = policyTags?.[tagListName]?.tags; + expect(tags?.[newTagName]?.pendingAction).toBeFalsy(); + expect(tags?.[newTagName]?.pendingFields?.name).toBeFalsy(); + + resolve(); + }, + }); + }), + ); }); it('reset policy tag name when api returns error', () => { @@ -612,46 +571,41 @@ describe('actions/Policy', () => { const oldTagName = Object.keys(fakePolicyTags?.[tagListName]?.tags)[0]; const newTagName = 'New tag'; - // @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_TAGS}${fakePolicy.id}`, fakePolicyTags); - }) - .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.fail(); - - Policy.renamePolicyTag(fakePolicy.id, { - oldName: oldTagName, - newName: newTagName, - }); - return waitForBatchedUpdates(); - }) - // @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_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - const tags = policyTags?.[tagListName]?.tags; - expect(tags?.[newTagName]).toBeFalsy(); - expect(tags?.[oldTagName]?.errors).toBeTruthy(); - - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + }) + .then(() => { + mockFetch?.fail?.(); + + Policy.renamePolicyTag(fakePolicy.id, { + oldName: oldTagName, + newName: newTagName, + }); + return waitForBatchedUpdates(); + }) + .then(mockFetch?.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + const tags = policyTags?.[tagListName]?.tags; + expect(tags?.[newTagName]).toBeFalsy(); + expect(tags?.[oldTagName]?.errors).toBeTruthy(); + + resolve(); + }, + }); + }), + ); }); }); @@ -664,58 +618,54 @@ describe('actions/Policy', () => { const fakePolicyTags = createRandomPolicyTags(tagListName, 2); const tagsToDelete = Object.keys(fakePolicyTags?.[tagListName]?.tags ?? {}); - // @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_TAGS}${fakePolicy.id}`, fakePolicyTags); - }) - .then(() => { - Policy.deletePolicyTags(fakePolicy.id, tagsToDelete); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - tagsToDelete.forEach((tagName) => { - expect(policyTags?.[tagListName]?.tags[tagName]?.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_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - tagsToDelete.forEach((tagName) => { - expect(policyTags?.[tagListName]?.tags[tagName]).toBeFalsy(); - }); - - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + }) + .then(() => { + Policy.deletePolicyTags(fakePolicy.id, tagsToDelete); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + tagsToDelete.forEach((tagName) => { + expect(policyTags?.[tagListName]?.tags[tagName]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + }); + + resolve(); + }, + }); + }), + ) + .then(mockFetch?.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + tagsToDelete.forEach((tagName) => { + expect(policyTags?.[tagListName]?.tags[tagName]).toBeFalsy(); + }); + + resolve(); + }, + }); + }), + ); }); it('reset the deleted policy tag when api returns error', () => { @@ -726,44 +676,39 @@ describe('actions/Policy', () => { const fakePolicyTags = createRandomPolicyTags(tagListName, 2); const tagsToDelete = Object.keys(fakePolicyTags?.[tagListName]?.tags ?? {}); - // @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_TAGS}${fakePolicy.id}`, fakePolicyTags); - }) - .then(() => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.fail(); - - Policy.deletePolicyTags(fakePolicy.id, tagsToDelete); - return waitForBatchedUpdates(); - }) - // @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_TAGS}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policyTags) => { - Onyx.disconnect(connectionID); - - tagsToDelete.forEach((tagName) => { - expect(policyTags?.[tagListName]?.tags[tagName].pendingAction).toBeFalsy(); - expect(policyTags?.[tagListName]?.tags[tagName].errors).toBeTruthy(); - }); - - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + + return Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy) + .then(() => { + Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, fakePolicyTags); + }) + .then(() => { + mockFetch?.fail?.(); + + Policy.deletePolicyTags(fakePolicy.id, tagsToDelete); + return waitForBatchedUpdates(); + }) + .then(mockFetch?.resume) + .then(waitForBatchedUpdates) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policyTags) => { + Onyx.disconnect(connectionID); + + tagsToDelete.forEach((tagName) => { + expect(policyTags?.[tagListName]?.tags[tagName].pendingAction).toBeFalsy(); + expect(policyTags?.[tagListName]?.tags[tagName].errors).toBeTruthy(); + }); + + resolve(); + }, + }); + }), + ); }); }); }); diff --git a/tests/actions/PolicyTaxTest.ts b/tests/actions/PolicyTaxTest.ts index a17179d8f7af..b1c190f9e5ac 100644 --- a/tests/actions/PolicyTaxTest.ts +++ b/tests/actions/PolicyTaxTest.ts @@ -7,6 +7,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy as PolicyType, TaxRate} from '@src/types/onyx'; import createRandomPolicy from '../utils/collections/policies'; import * as TestHelper from '../utils/TestHelper'; +import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; OnyxUpdateManager(); @@ -18,9 +19,10 @@ describe('actions/PolicyTax', () => { }); }); + 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(() => Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy)) .then(waitForBatchedUpdates); @@ -29,53 +31,48 @@ describe('actions/PolicyTax', () => { describe('SetPolicyCustomTaxName', () => { it('Set policy`s custom tax name', () => { const customTaxName = 'Custom tag name'; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); - 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?.taxRates?.name).toBe(customTaxName); - expect(policy?.taxRates?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - expect(policy?.taxRates?.errorFields).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?.taxRates?.pendingFields?.name).toBeFalsy(); - expect(policy?.taxRates?.errorFields).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + 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?.taxRates?.name).toBe(customTaxName); + expect(policy?.taxRates?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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?.taxRates?.pendingFields?.name).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('Reset policy`s custom tax name when API returns an error', () => { const customTaxName = 'Custom tag name'; const originalCustomTaxName = fakePolicy?.taxRates?.name; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); Policy.setPolicyCustomTaxName(fakePolicy.id, customTaxName); return waitForBatchedUpdates() .then( @@ -95,10 +92,8 @@ describe('actions/PolicyTax', () => { }), ) .then(() => { - // @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(waitForBatchedUpdates) .then( @@ -124,53 +119,48 @@ describe('actions/PolicyTax', () => { it('Set policy`s currency default tax', () => { const taxCode = 'id_TAX_RATE_1'; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); - 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?.taxRates?.defaultExternalID).toBe(taxCode); - expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - expect(policy?.taxRates?.errorFields).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?.taxRates?.pendingFields?.defaultExternalID).toBeFalsy(); - expect(policy?.taxRates?.errorFields).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + 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?.taxRates?.defaultExternalID).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.defaultExternalID).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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?.taxRates?.pendingFields?.defaultExternalID).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('Reset policy`s currency default tax when API returns an error', () => { const taxCode = 'id_TAX_RATE_1'; const originalDefaultExternalID = fakePolicy?.taxRates?.defaultExternalID; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); Policy.setWorkspaceCurrencyDefault(fakePolicy.id, taxCode); return waitForBatchedUpdates() .then( @@ -190,10 +180,8 @@ describe('actions/PolicyTax', () => { }), ) .then(() => { - // @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(waitForBatchedUpdates) .then( @@ -218,54 +206,49 @@ describe('actions/PolicyTax', () => { it('Set policy`s foreign currency default', () => { const taxCode = 'id_TAX_RATE_1'; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); - 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?.taxRates?.foreignTaxDefault).toBe(taxCode); - expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - expect(policy?.taxRates?.errorFields).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?.taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); - expect(policy?.taxRates?.errorFields).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + 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?.taxRates?.foreignTaxDefault).toBe(taxCode); + expect(policy?.taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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?.taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(policy?.taxRates?.errorFields).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('Reset policy`s foreign currency default when API returns an error', () => { const taxCode = 'id_TAX_RATE_1'; const originalDefaultForeignCurrencyID = fakePolicy?.taxRates?.foreignTaxDefault; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); Policy.setForeignCurrencyDefault(fakePolicy.id, taxCode); return waitForBatchedUpdates() .then( @@ -286,10 +269,8 @@ describe('actions/PolicyTax', () => { ) .then(() => { - // @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(waitForBatchedUpdates) .then( @@ -319,49 +300,45 @@ describe('actions/PolicyTax', () => { code: 'id_TAX_RATE_2', }; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); createPolicyTax(fakePolicy.id, newTaxRate); - return ( - waitForBatchedUpdates() - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policy) => { - Onyx.disconnect(connectionID); - const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; - expect(createdTax?.code).toBe(newTaxRate.code); - expect(createdTax?.name).toBe(newTaxRate.name); - expect(createdTax?.value).toBe(newTaxRate.value); - expect(createdTax?.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(waitForBatchedUpdates) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policy) => { - Onyx.disconnect(connectionID); - const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; - expect(createdTax?.errors).toBeFalsy(); - expect(createdTax?.pendingFields).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.code).toBe(newTaxRate.code); + expect(createdTax?.name).toBe(newTaxRate.name); + expect(createdTax?.value).toBe(newTaxRate.value); + expect(createdTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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 createdTax = policy?.taxRates?.taxes?.[newTaxRate.code ?? '']; + expect(createdTax?.errors).toBeFalsy(); + expect(createdTax?.pendingFields).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('Remove the optimistic tax if the API returns an error', () => { @@ -371,8 +348,7 @@ describe('actions/PolicyTax', () => { code: 'id_TAX_RATE_2', }; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); createPolicyTax(fakePolicy.id, newTaxRate); return waitForBatchedUpdates() .then( @@ -394,10 +370,8 @@ describe('actions/PolicyTax', () => { }), ) .then(() => { - // @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(waitForBatchedUpdates) .then( @@ -420,55 +394,50 @@ describe('actions/PolicyTax', () => { describe('SetPolicyTaxesEnabled', () => { it('Disable policy`s taxes', () => { const disableTaxID = 'id_TAX_RATE_1'; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); - return ( - waitForBatchedUpdates() - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policy) => { - Onyx.disconnect(connectionID); - const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; - expect(disabledTax?.isDisabled).toBeTruthy(); - expect(disabledTax?.pendingFields?.isDisabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.isDisabled).toBeTruthy(); + expect(disabledTax?.pendingFields?.isDisabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(disabledTax?.errorFields?.isDisabled).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); - const disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; - expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); - expect(disabledTax?.pendingFields?.isDisabled).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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 disabledTax = policy?.taxRates?.taxes?.[disableTaxID]; + expect(disabledTax?.errorFields?.isDisabled).toBeFalsy(); + expect(disabledTax?.pendingFields?.isDisabled).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('Disable policy`s taxes but API returns an error, then enable policy`s taxes again', () => { const disableTaxID = 'id_TAX_RATE_1'; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); setPolicyTaxesEnabled(fakePolicy.id, [disableTaxID], false); const originalTaxes = {...fakePolicy?.taxRates?.taxes}; return waitForBatchedUpdates() @@ -491,10 +460,8 @@ describe('actions/PolicyTax', () => { }), ) .then(() => { - // @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(waitForBatchedUpdates) .then( @@ -521,57 +488,52 @@ describe('actions/PolicyTax', () => { it('Rename tax', () => { const taxID = 'id_TAX_RATE_1'; const newTaxName = 'Tax rate 1 updated'; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); renamePolicyTax(fakePolicy.id, taxID, newTaxName); - return ( - waitForBatchedUpdates() - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policy) => { - Onyx.disconnect(connectionID); - const updatedTax = policy?.taxRates?.taxes?.[taxID]; - expect(updatedTax?.name).toBe(newTaxName); - expect(updatedTax?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - expect(updatedTax?.errorFields?.name).toBeFalsy(); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.name).toBe(newTaxName); + expect(updatedTax?.pendingFields?.name).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.name).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); - const updatedTax = policy?.taxRates?.taxes?.[taxID]; - expect(updatedTax?.errorFields?.name).toBeFalsy(); - expect(updatedTax?.pendingFields?.name).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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 updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.errorFields?.name).toBeFalsy(); + expect(updatedTax?.pendingFields?.name).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('Rename tax but API returns an error, then recover the original tax`s name', () => { const taxID = 'id_TAX_RATE_1'; const newTaxName = 'Tax rate 1 updated'; const originalTaxRate = {...fakePolicy?.taxRates?.taxes[taxID]}; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); renamePolicyTax(fakePolicy.id, taxID, newTaxName); return waitForBatchedUpdates() .then( @@ -593,10 +555,8 @@ describe('actions/PolicyTax', () => { }), ) .then(() => { - // @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(waitForBatchedUpdates) .then( @@ -623,49 +583,45 @@ describe('actions/PolicyTax', () => { const taxID = 'id_TAX_RATE_1'; const newTaxValue = 10; const stringTaxValue = `${newTaxValue}%`; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); - return ( - waitForBatchedUpdates() - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policy) => { - Onyx.disconnect(connectionID); - const updatedTax = policy?.taxRates?.taxes?.[taxID]; - expect(updatedTax?.value).toBe(stringTaxValue); - expect(updatedTax?.pendingFields?.value).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - expect(updatedTax?.errorFields?.value).toBeFalsy(); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.value).toBe(stringTaxValue); + expect(updatedTax?.pendingFields?.value).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(updatedTax?.errorFields?.value).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); - const updatedTax = policy?.taxRates?.taxes?.[taxID]; - expect(updatedTax?.errorFields?.value).toBeFalsy(); - expect(updatedTax?.pendingFields?.value).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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 updatedTax = policy?.taxRates?.taxes?.[taxID]; + expect(updatedTax?.errorFields?.value).toBeFalsy(); + expect(updatedTax?.pendingFields?.value).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('Update tax`s value but API returns an error, then recover the original tax`s value', () => { @@ -673,8 +629,7 @@ describe('actions/PolicyTax', () => { const newTaxValue = 10; const originalTaxRate = {...fakePolicy?.taxRates?.taxes[taxID]}; const stringTaxValue = `${newTaxValue}%`; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); updatePolicyTaxValue(fakePolicy.id, taxID, newTaxValue); return waitForBatchedUpdates() .then( @@ -696,10 +651,8 @@ describe('actions/PolicyTax', () => { }), ) .then(() => { - // @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(waitForBatchedUpdates) .then( @@ -726,113 +679,104 @@ describe('actions/PolicyTax', () => { const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; const taxID = 'id_TAX_RATE_1'; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); deletePolicyTaxes(fakePolicy.id, [taxID]); - return ( - waitForBatchedUpdates() - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policy) => { - Onyx.disconnect(connectionID); - const taxRates = policy?.taxRates; - const deletedTax = taxRates?.taxes?.[taxID]; - expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); - expect(taxRates?.foreignTaxDefault).toBe(foreignTaxDefault); - expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - expect(deletedTax?.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}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policy) => { - Onyx.disconnect(connectionID); - const taxRates = policy?.taxRates; - const deletedTax = taxRates?.taxes?.[taxID]; - expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); - expect(deletedTax).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + return waitForBatchedUpdates() + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(taxRates?.foreignTaxDefault).toBe(foreignTaxDefault); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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 taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('Delete tax that is foreignTaxDefault', () => { const taxID = 'id_TAX_RATE_1'; const firstTaxID = 'id_TAX_EXEMPT'; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); - return ( - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, {taxRates: {foreignTaxDefault: 'id_TAX_RATE_1'}}) - .then(() => { - deletePolicyTaxes(fakePolicy.id, [taxID]); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policy) => { - Onyx.disconnect(connectionID); - const taxRates = policy?.taxRates; - const deletedTax = taxRates?.taxes?.[taxID]; - expect(taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); - expect(taxRates?.foreignTaxDefault).toBe(firstTaxID); - expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - expect(deletedTax?.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}${fakePolicy.id}`, - waitForCollectionCallback: false, - callback: (policy) => { - Onyx.disconnect(connectionID); - const taxRates = policy?.taxRates; - const deletedTax = taxRates?.taxes?.[taxID]; - expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); - expect(deletedTax).toBeFalsy(); - resolve(); - }, - }); - }), - ) - ); + mockFetch?.pause?.(); + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, {taxRates: {foreignTaxDefault: 'id_TAX_RATE_1'}}) + .then(() => { + deletePolicyTaxes(fakePolicy.id, [taxID]); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + waitForCollectionCallback: false, + callback: (policy) => { + Onyx.disconnect(connectionID); + const taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(taxRates?.foreignTaxDefault).toBe(firstTaxID); + expect(deletedTax?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + expect(deletedTax?.errors).toBeFalsy(); + resolve(); + }, + }); + }), + ) + .then(mockFetch?.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 taxRates = policy?.taxRates; + const deletedTax = taxRates?.taxes?.[taxID]; + expect(taxRates?.pendingFields?.foreignTaxDefault).toBeFalsy(); + expect(deletedTax).toBeFalsy(); + resolve(); + }, + }); + }), + ); }); it('Delete tax that is not foreignTaxDefault but API return an error, then recover the delated tax', () => { const foreignTaxDefault = fakePolicy?.taxRates?.foreignTaxDefault; const taxID = 'id_TAX_RATE_1'; - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - fetch.pause(); + mockFetch?.pause?.(); deletePolicyTaxes(fakePolicy.id, [taxID]); return waitForBatchedUpdates() .then( @@ -855,10 +799,8 @@ describe('actions/PolicyTax', () => { }), ) .then(() => { - // @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(waitForBatchedUpdates) .then( diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 3e11f0a67938..6dae053afac7 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -7,6 +7,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy as PolicyType, Report, ReportAction, ReportActions} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/Report'; import * as TestHelper from '../utils/TestHelper'; +import type {MockFetch} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; const ESH_EMAIL = 'eshgupta1217@gmail.com'; @@ -23,15 +24,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(); @@ -124,8 +123,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 28aa84babfb5..4b49bdef5edd 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/ChatFinderPage.perf-test.tsx b/tests/perf-test/ChatFinderPage.perf-test.tsx index c31201c37b24..9c673d278905 100644 --- a/tests/perf-test/ChatFinderPage.perf-test.tsx +++ b/tests/perf-test/ChatFinderPage.perf-test.tsx @@ -1,5 +1,5 @@ import type * as NativeNavigation from '@react-navigation/native'; -import type {StackScreenProps} from '@react-navigation/stack'; +import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import {fireEvent, screen} from '@testing-library/react-native'; import React, {useMemo} from 'react'; import type {ComponentType} from 'react'; @@ -112,7 +112,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}); @@ -124,9 +123,9 @@ afterEach(() => { }); type ChatFinderPageProps = StackScreenProps & { - betas: OnyxEntry; - reports: OnyxCollection; - isSearchingForReports: OnyxEntry; + betas?: OnyxEntry; + reports?: OnyxCollection; + isSearchingForReports?: OnyxEntry; }; function ChatFinderPageWrapper(args: ChatFinderPageProps) { @@ -164,21 +163,26 @@ test('[ChatFinderPage] should render list with cached options', async () => { await screen.findByTestId('ChatFinderPage'); }; - const navigation = {addListener}; - - return ( - waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - ...mockedReports, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, - [ONYXKEYS.BETAS]: mockedBetas, - [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. - .then(() => measurePerformance(, {scenario})) - ); + const navigation = {addListener} as unknown as StackNavigationProp; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + .then(() => + measurePerformance( + , + {scenario}, + ), + ); }); test('[ChatFinderPage] should interact when text input changes', async () => { @@ -193,19 +197,24 @@ test('[ChatFinderPage] should interact when text input changes', async () => { fireEvent.changeText(input, 'Email Five'); }; - const navigation = {addListener}; - - return ( - waitForBatchedUpdates() - .then(() => - Onyx.multiSet({ - ...mockedReports, - [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, - [ONYXKEYS.BETAS]: mockedBetas, - [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. - .then(() => measurePerformance(, {scenario})) - ); + const navigation = {addListener} as unknown as StackNavigationProp; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + ...mockedReports, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: mockedPersonalDetails, + [ONYXKEYS.BETAS]: mockedBetas, + [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, + }), + ) + .then(() => + measurePerformance( + , + {scenario}, + ), + ); }); diff --git a/tests/perf-test/ReportScreen.perf-test.tsx b/tests/perf-test/ReportScreen.perf-test.tsx index 29a17e9b0070..379dc34489e5 100644 --- a/tests/perf-test/ReportScreen.perf-test.tsx +++ b/tests/perf-test/ReportScreen.perf-test.tsx @@ -1,4 +1,4 @@ -import type {StackScreenProps} from '@react-navigation/stack'; +import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import {screen, waitFor} from '@testing-library/react-native'; import type {ComponentType} from 'react'; import React from 'react'; @@ -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}); @@ -163,7 +162,7 @@ function ReportScreenWrapper(props: ReportScreenWrapperProps) { const report = {...createRandomReport(1), policyID: '1'}; const reportActions = ReportTestUtils.getMockedReportActionsMap(1000); -const mockRoute = {params: {reportID: '1'}}; +const mockRoute = {params: {reportID: '1', reportActionID: ''}, key: 'Report', name: 'Report' as const}; test('[ReportScreen] should render ReportScreen', () => { const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock(); @@ -185,7 +184,7 @@ test('[ReportScreen] should render ReportScreen', () => { await screen.findByTestId('report-actions-list'); }; - const navigation = {addListener}; + const navigation = {addListener} as unknown as StackNavigationProp; return waitForBatchedUpdates() .then(() => { @@ -210,9 +209,7 @@ test('[ReportScreen] should render ReportScreen', () => { .then(() => measurePerformance( , {scenario}, diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 3e2c1b419070..e5c7e0359eed 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -119,7 +119,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 b9404973d418..07633b19802b 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, isBackendReachable: false}) .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.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(); }) .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, isBackendReachable: false}) .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, isBackendReachable: false}) .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, isBackendReachable: false})) .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, isBackendReachable: true})) @@ -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 24e56ec8de23..968e543363f2 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -202,7 +202,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/MigrationTest.ts b/tests/unit/MigrationTest.ts index 69e0e80e589a..9f0a3433932c 100644 --- a/tests/unit/MigrationTest.ts +++ b/tests/unit/MigrationTest.ts @@ -1,10 +1,12 @@ /* eslint-disable @typescript-eslint/naming-convention */ import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import Log from '@src/libs/Log'; import CheckForPreviousReportActionID from '@src/libs/migrations/CheckForPreviousReportActionID'; import KeyReportActionsDraftByReportActionID from '@src/libs/migrations/KeyReportActionsDraftByReportActionID'; import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; import type {ReportActionsDraftCollectionDataSet} from '@src/types/onyx/ReportActionsDrafts'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; @@ -260,13 +262,10 @@ describe('Migrations', () => { it('Should move individual draft to a draft collection of report', () => { const setQueries: ReportActionsDraftCollectionDataSet = {}; - // @ts-expect-error preset invalid value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = 'a'; - // @ts-expect-error preset invalid value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`] = 'b'; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = 'a' as unknown as OnyxEntry; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`] = 'b' as unknown as OnyxEntry; setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`] = {3: 'c'}; - // @ts-expect-error preset invalid value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`] = 'd'; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`] = 'd' as unknown as OnyxEntry; return Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) @@ -321,8 +320,7 @@ describe('Migrations', () => { it("Shouldn't move empty individual draft to a draft collection of report", () => { const setQueries: ReportActionsDraftCollectionDataSet = {}; - // @ts-expect-error preset empty string value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = ''; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = '' as unknown as OnyxEntry; setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`] = {}; return Onyx.multiSet(setQueries) diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index 3ea0f4da9468..9c48de96d1a2 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 05fe50fe78b7..19b4a23e9028 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..64d79c9faf94 100644 --- a/tests/unit/awaitStagingDeploysTest.ts +++ b/tests/unit/awaitStagingDeploysTest.ts @@ -68,16 +68,16 @@ beforeAll(() => { asMutable(core).getInput = mockGetInput; // Mock octokit module - const moctokit: InternalOctokit = { + const moctokit = { rest: { - // @ts-expect-error This error was removed because getting the rest of the data from internalOctokit makes the test to break actions: { + ...(GithubUtils.internalOctokit as unknown as typeof GithubUtils.octokit.actions), listWorkflowRuns: mockListWorkflowRuns as unknown as typeof GithubUtils.octokit.actions.listWorkflowRuns, }, }, }; - GithubUtils.internalOctokit = moctokit; + GithubUtils.internalOctokit = moctokit as InternalOctokit; }); beforeEach(() => { diff --git a/tests/unit/sanitizeStringForJSONParseTest.ts b/tests/unit/sanitizeStringForJSONParseTest.ts index 9d39685eb1f7..f9e647eb9a2a 100644 --- a/tests/unit/sanitizeStringForJSONParseTest.ts +++ b/tests/unit/sanitizeStringForJSONParseTest.ts @@ -30,7 +30,6 @@ const validJSONData: Array<[string, string]> = [ describe('santizeStringForJSONParse', () => { describe.each(badInputs)('willDetectBadInputs', (input) => { test('sanitizeStringForJSONParse', () => { - // @ts-expect-error TODO: Remove this once sanitizeStringForJSONParse (https://github.com/Expensify/App/issues/25360) is migrated to TypeScript. expect(() => sanitizeStringForJSONParse(input)).toThrow(); }); }); diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 562176288565..bd107ba6ed56 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}; diff --git a/tests/utils/collections/policyCategory.ts b/tests/utils/collections/policyCategory.ts new file mode 100644 index 000000000000..37a5ecb4f21f --- /dev/null +++ b/tests/utils/collections/policyCategory.ts @@ -0,0 +1,21 @@ +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, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'GL Code': '', + unencodedName: categoryName, + externalID: '', + areCommentsRequired: false, + origin: '', + }; + } + + return categories; +}