From 6aa92816654f3873c65f33915daa12832f2df021 Mon Sep 17 00:00:00 2001 From: Hans Date: Sun, 20 Oct 2024 16:46:47 +0700 Subject: [PATCH 001/104] Add validatecode modal when issuing Physical card --- src/libs/actions/Wallet.ts | 3 +- .../Wallet/Card/BaseGetPhysicalCard.tsx | 59 +++++++++++++++---- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index b1f97421eea0..1a345be14100 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -257,7 +257,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str }); } -function requestPhysicalExpensifyCard(cardID: number, authToken: string, privatePersonalDetails: PrivatePersonalDetails) { +function requestPhysicalExpensifyCard(cardID: number, authToken: string, privatePersonalDetails: PrivatePersonalDetails, validateCode: string) { const {legalFirstName = '', legalLastName = '', phoneNumber = ''} = privatePersonalDetails; const {city = '', country = '', state = '', street = '', zip = ''} = PersonalDetailsUtils.getCurrentAddress(privatePersonalDetails) ?? {}; @@ -271,6 +271,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private addressState: state, addressStreet: street, addressZip: zip, + validateCode, }; const optimisticData: OnyxUpdate[] = [ diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index ae003c4afbe2..aed73018d582 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -1,13 +1,16 @@ -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {ReactNode} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; +import ValidateCodeActionModal from '@components/ValidateCodeActionModal'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as FormActions from '@libs/actions/FormActions'; +import * as User from '@libs/actions/User'; import * as Wallet from '@libs/actions/Wallet'; import * as CardUtils from '@libs/CardUtils'; import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils'; @@ -108,7 +111,9 @@ function BaseGetPhysicalCard({ }: BaseGetPhysicalCardProps) { const styles = useThemeStyles(); const isRouteSet = useRef(false); - + const {translate} = useLocalize(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const [isActionCodeModalVisible, setActionCodeModalVisible] = useState(false); const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); const cardID = cardToBeIssued?.cardID.toString() ?? '-1'; @@ -145,18 +150,37 @@ function BaseGetPhysicalCard({ }, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, cardToBeIssued, privatePersonalDetails]); const onSubmit = useCallback(() => { - const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); - // If the current step of the get physical card flow is the confirmation page - if (isConfirmation) { - Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? -1, session?.authToken ?? '', updatedPrivatePersonalDetails); - // Form draft data needs to be erased when the flow is complete, - // so that no stale data is left on Onyx - FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); + setActionCodeModalVisible(true); + }, []); + + const handleIssuePhysicalCard = useCallback( + (validateCode: string) => { + const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); + // If the current step of the get physical card flow is the confirmation page + if (isConfirmation) { + Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? -1, session?.authToken ?? '', updatedPrivatePersonalDetails, validateCode); + // Form draft data needs to be erased when the flow is complete, + // so that no stale data is left on Onyx + FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); + return; + } + GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails); + }, + [cardID, cardToBeIssued?.cardID, domain, draftValues, isConfirmation, session?.authToken, privatePersonalDetails], + ); + + const sendValidateCode = useCallback(() => { + const primaryLogin = account?.primaryLogin ?? ''; + const loginData = loginList?.[primaryLogin]; + + if (loginData?.validateCodeSent) { return; } - GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails); - }, [cardID, cardToBeIssued?.cardID, domain, draftValues, isConfirmation, session?.authToken, privatePersonalDetails]); + + User.requestValidateCodeAction(); + }, [account]); + return ( {headline} {renderContent({onSubmit, submitButtonText, children, onValidate})} + {}} + handleSubmitForm={handleIssuePhysicalCard} + title={translate('cardPage.validateCardTitle')} + onClose={() => setActionCodeModalVisible(false)} + description={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} + /> ); } From b1e52cc0a62feac8924e809fb4055e604ad7b396 Mon Sep 17 00:00:00 2001 From: Hans Date: Sun, 20 Oct 2024 17:19:18 +0700 Subject: [PATCH 002/104] adjust logic --- .../RequestPhysicalExpensifyCardParams.ts | 1 + .../Wallet/Card/BaseGetPhysicalCard.tsx | 25 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts b/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts index 91995b6e37aa..94e45a29b728 100644 --- a/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts +++ b/src/libs/API/parameters/RequestPhysicalExpensifyCardParams.ts @@ -8,6 +8,7 @@ type RequestPhysicalExpensifyCardParams = { addressState: string; addressStreet: string; addressZip: string; + validateCode: string; }; export default RequestPhysicalExpensifyCardParams; diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index aed73018d582..ac579a0942c4 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -150,22 +150,23 @@ function BaseGetPhysicalCard({ }, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, cardToBeIssued, privatePersonalDetails]); const onSubmit = useCallback(() => { - setActionCodeModalVisible(true); - }, []); + const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); + if (isConfirmation) { + setActionCodeModalVisible(true); + return; + } + GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails); + }, [isConfirmation, domain]); const handleIssuePhysicalCard = useCallback( (validateCode: string) => { const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); // If the current step of the get physical card flow is the confirmation page - if (isConfirmation) { - Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? -1, session?.authToken ?? '', updatedPrivatePersonalDetails, validateCode); - // Form draft data needs to be erased when the flow is complete, - // so that no stale data is left on Onyx - FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); - return; - } - GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails); + Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? -1, session?.authToken ?? '', updatedPrivatePersonalDetails, validateCode); + // Form draft data needs to be erased when the flow is complete, + // so that no stale data is left on Onyx + FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); }, [cardID, cardToBeIssued?.cardID, domain, draftValues, isConfirmation, session?.authToken, privatePersonalDetails], ); @@ -179,7 +180,7 @@ function BaseGetPhysicalCard({ } User.requestValidateCodeAction(); - }, [account]); + }, [account, loginList]); return ( Date: Sun, 20 Oct 2024 17:29:38 +0700 Subject: [PATCH 003/104] address linting --- ios/.ruby-version | 1 + src/libs/Permissions.ts | 2 +- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 ios/.ruby-version diff --git a/ios/.ruby-version b/ios/.ruby-version new file mode 100644 index 000000000000..fa7adc7ac72a --- /dev/null +++ b/ios/.ruby-version @@ -0,0 +1 @@ +3.3.5 diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 24de2e612208..1d6fd98d8ccc 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -4,7 +4,7 @@ import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { - return !!betas?.includes(CONST.BETAS.ALL); + return true//!!betas?.includes(CONST.BETAS.ALL); } function canUseDefaultRooms(betas: OnyxEntry): boolean { diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index ac579a0942c4..9638fc6aba60 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -156,7 +156,7 @@ function BaseGetPhysicalCard({ return; } GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails); - }, [isConfirmation, domain]); + }, [isConfirmation, domain, draftValues, privatePersonalDetails]); const handleIssuePhysicalCard = useCallback( (validateCode: string) => { @@ -168,7 +168,7 @@ function BaseGetPhysicalCard({ FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); }, - [cardID, cardToBeIssued?.cardID, domain, draftValues, isConfirmation, session?.authToken, privatePersonalDetails], + [cardID, cardToBeIssued?.cardID, draftValues, session?.authToken, privatePersonalDetails], ); const sendValidateCode = useCallback(() => { From 91bc9b1e06176f0135291fd45dd8442c3a50c9cc Mon Sep 17 00:00:00 2001 From: Hans Date: Sun, 20 Oct 2024 17:30:33 +0700 Subject: [PATCH 004/104] revert .ruby-version --- ios/.ruby-version | 1 - src/libs/Permissions.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 ios/.ruby-version diff --git a/ios/.ruby-version b/ios/.ruby-version deleted file mode 100644 index fa7adc7ac72a..000000000000 --- a/ios/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.3.5 diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 1d6fd98d8ccc..24de2e612208 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -4,7 +4,7 @@ import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { - return true//!!betas?.includes(CONST.BETAS.ALL); + return !!betas?.includes(CONST.BETAS.ALL); } function canUseDefaultRooms(betas: OnyxEntry): boolean { From 98904943d30d963af7c62aacb8deb25c7e6eb349 Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 23 Oct 2024 14:07:42 +0700 Subject: [PATCH 005/104] Onyx migration --- .../Wallet/Card/BaseGetPhysicalCard.tsx | 50 +++---------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 9638fc6aba60..f318416e5642 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {ReactNode} from 'react'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -19,7 +19,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {GetPhysicalCardForm} from '@src/types/form'; -import type {CardList, LoginList, PrivatePersonalDetails, Session} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -31,24 +30,7 @@ type RenderContentProps = ChildrenProps & { onValidate: OnValidate; }; -type BaseGetPhysicalCardOnyxProps = { - /** List of available assigned cards */ - cardList: OnyxEntry; - - /** User's private personal details */ - privatePersonalDetails: OnyxEntry; - - /** Draft values used by the get physical card form */ - draftValues: OnyxEntry; - - /** Session info for the currently logged in user. */ - session: OnyxEntry; - - /** List of available login methods */ - loginList: OnyxEntry; -}; - -type BaseGetPhysicalCardProps = BaseGetPhysicalCardOnyxProps & { +type BaseGetPhysicalCardProps = { /** Text displayed below page title */ headline: string; @@ -94,17 +76,12 @@ function DefaultRenderContent({onSubmit, submitButtonText, children, onValidate} } function BaseGetPhysicalCard({ - cardList, children, currentRoute, domain, - draftValues, - privatePersonalDetails, headline, isConfirmation = false, - loginList, renderContent = DefaultRenderContent, - session, submitButtonText, title, onValidate = () => ({}), @@ -112,6 +89,11 @@ function BaseGetPhysicalCard({ const styles = useThemeStyles(); const isRouteSet = useRef(false); const {translate} = useLocalize(); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST); + const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [draftValues] = useOnyx(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [isActionCodeModalVisible, setActionCodeModalVisible] = useState(false); const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; @@ -209,22 +191,6 @@ function BaseGetPhysicalCard({ BaseGetPhysicalCard.displayName = 'BaseGetPhysicalCard'; -export default withOnyx({ - cardList: { - key: ONYXKEYS.CARD_LIST, - }, - loginList: { - key: ONYXKEYS.LOGIN_LIST, - }, - session: { - key: ONYXKEYS.SESSION, - }, - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - draftValues: { - key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT, - }, -})(BaseGetPhysicalCard); +export default BaseGetPhysicalCard; export type {RenderContentProps}; From 55187f468b175a13fea360057d204ce20f4324ed Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 30 Oct 2024 15:21:46 +0700 Subject: [PATCH 006/104] handle generic error --- src/libs/actions/Wallet.ts | 35 ++++++++++++++++++- .../Wallet/Card/BaseGetPhysicalCard.tsx | 17 ++++++--- src/types/onyx/Card.ts | 3 ++ 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index 1a345be14100..c395c4191cb2 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -10,6 +10,7 @@ import type { VerifyIdentityParams, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import * as ErrorUtils from '@libs/ErrorUtils'; import type {PrivatePersonalDetails} from '@libs/GetPhysicalCardUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import type CONST from '@src/CONST'; @@ -281,6 +282,9 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private value: { [cardID]: { state: 4, // NOT_ACTIVATED + isLoading: true, + errors: null, + isSuccessfull: null, }, }, }, @@ -291,7 +295,36 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private }, ]; - API.write(WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD, requestParams, {optimisticData}); + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + state: 4, // NOT_ACTIVATED + isLoading: false, + errors: null, + isSuccessfull: true, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + state: 4, // NOT_ACTIVATED + isLoading: false, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + ]; + + API.write(WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD, requestParams, {optimisticData, failureData, successData}); } function resetWalletAdditionalDetailsDraft() { diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index f318416e5642..4549e33795cc 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -99,6 +99,7 @@ function BaseGetPhysicalCard({ const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); const cardID = cardToBeIssued?.cardID.toString() ?? '-1'; + const isSuccessful = cardToBeIssued?.isSuccessfull; useEffect(() => { if (isRouteSet.current || !privatePersonalDetails || !cardList) { @@ -131,6 +132,16 @@ function BaseGetPhysicalCard({ isRouteSet.current = true; }, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, cardToBeIssued, privatePersonalDetails]); + useEffect(() => { + if (!isSuccessful) { + return; + } + // Form draft data needs to be erased when the flow is complete, + // so that no stale data is left on Onyx + FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); + }, [isSuccessful, cardID]); + const onSubmit = useCallback(() => { const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); if (isConfirmation) { @@ -145,12 +156,8 @@ function BaseGetPhysicalCard({ const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); // If the current step of the get physical card flow is the confirmation page Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? -1, session?.authToken ?? '', updatedPrivatePersonalDetails, validateCode); - // Form draft data needs to be erased when the flow is complete, - // so that no stale data is left on Onyx - FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); }, - [cardID, cardToBeIssued?.cardID, draftValues, session?.authToken, privatePersonalDetails], + [cardToBeIssued?.cardID, draftValues, session?.authToken, privatePersonalDetails], ); const sendValidateCode = useCallback(() => { diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index c1d0e09d9312..dfe0695a0ece 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -115,6 +115,9 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Collection of form field errors */ errorFields?: OnyxCommon.ErrorFields; }>; + + /** Card status */ + isSuccessfull?: boolean; }>; /** Model of Expensify card details */ From 340d3b177a3d378d7c250dc0e105bba6beb5f031 Mon Sep 17 00:00:00 2001 From: Hans Date: Fri, 1 Nov 2024 10:08:45 +0700 Subject: [PATCH 007/104] update the error field --- src/libs/actions/Wallet.ts | 17 +++++++++++++-- .../Wallet/Card/BaseGetPhysicalCard.tsx | 21 +++++++------------ src/types/onyx/Card.ts | 2 +- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index c395c4191cb2..6849223974c9 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -284,7 +284,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private state: 4, // NOT_ACTIVATED isLoading: true, errors: null, - isSuccessfull: null, + isSuccessful: null, }, }, }, @@ -304,7 +304,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private state: 4, // NOT_ACTIVATED isLoading: false, errors: null, - isSuccessfull: true, + isSuccessful: true, }, }, }, @@ -331,6 +331,18 @@ function resetWalletAdditionalDetailsDraft() { Onyx.set(ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, null); } +/** + * Clear the error of specific card + * @param cardId The card id of the card that you want to clear the errors. + */ +function clearPhysicalCardError(cardId: string) { + Onyx.merge(ONYXKEYS.CARD_LIST, { + [cardId]: { + errors: null, + }, + }); +} + export { openOnfidoFlow, openInitialSettingsPage, @@ -345,4 +357,5 @@ export { setKYCWallSource, requestPhysicalExpensifyCard, resetWalletAdditionalDetailsDraft, + clearPhysicalCardError, }; diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 4549e33795cc..57eb35b295ec 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -13,6 +13,7 @@ import * as FormActions from '@libs/actions/FormActions'; import * as User from '@libs/actions/User'; import * as Wallet from '@libs/actions/Wallet'; import * as CardUtils from '@libs/CardUtils'; +import * as ErrorUtils from '@libs/ErrorUtils'; import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; @@ -99,7 +100,8 @@ function BaseGetPhysicalCard({ const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); const cardID = cardToBeIssued?.cardID.toString() ?? '-1'; - const isSuccessful = cardToBeIssued?.isSuccessfull; + const isSuccessful = cardToBeIssued?.isSuccessful; + const errorMessage = ErrorUtils.getLatestErrorMessageField(cardToBeIssued); useEffect(() => { if (isRouteSet.current || !privatePersonalDetails || !cardList) { @@ -139,6 +141,7 @@ function BaseGetPhysicalCard({ // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); + Wallet.clearPhysicalCardError(cardID); Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); }, [isSuccessful, cardID]); @@ -160,17 +163,6 @@ function BaseGetPhysicalCard({ [cardToBeIssued?.cardID, draftValues, session?.authToken, privatePersonalDetails], ); - const sendValidateCode = useCallback(() => { - const primaryLogin = account?.primaryLogin ?? ''; - const loginData = loginList?.[primaryLogin]; - - if (loginData?.validateCodeSent) { - return; - } - - User.requestValidateCodeAction(); - }, [account, loginList]); - return ( {}} + sendValidateCode={() => User.requestValidateCodeAction()} + clearError={() => Wallet.clearPhysicalCardError(cardID)} + validateError={errorMessage} handleSubmitForm={handleIssuePhysicalCard} title={translate('cardPage.validateCardTitle')} onClose={() => setActionCodeModalVisible(false)} diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index dfe0695a0ece..a7debf5df925 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -117,7 +117,7 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ }>; /** Card status */ - isSuccessfull?: boolean; + isSuccessful?: boolean; }>; /** Model of Expensify card details */ From 23c5163536340c45f71f651cef1246d081d64862 Mon Sep 17 00:00:00 2001 From: Hans Date: Fri, 1 Nov 2024 13:50:23 +0700 Subject: [PATCH 008/104] add loading field and other stuffs --- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 6 ++++- .../ValidateCodeActionModal/index.tsx | 2 ++ .../ValidateCodeActionModal/type.ts | 3 +++ src/libs/actions/Wallet.ts | 27 ++++++++++++++++++- .../Wallet/Card/BaseGetPhysicalCard.tsx | 5 +++- 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx index cc2a7314f570..2c318996c235 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -65,6 +65,9 @@ type ValidateCodeFormProps = { /** Function is called when validate code modal is mounted and on magic code resend */ sendValidateCode: () => void; + + /** Wheather the form is loading or not */ + isLoading?: boolean; }; function BaseValidateCodeForm({ @@ -78,6 +81,7 @@ function BaseValidateCodeForm({ clearError, sendValidateCode, buttonStyles, + isLoading, }: ValidateCodeFormProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -266,7 +270,7 @@ function BaseValidateCodeForm({ style={[styles.mt4]} success large - isLoading={account?.isLoading} + isLoading={account?.isLoading || isLoading} /> diff --git a/src/components/ValidateCodeActionModal/index.tsx b/src/components/ValidateCodeActionModal/index.tsx index 8c09d8caad62..21298aa06787 100644 --- a/src/components/ValidateCodeActionModal/index.tsx +++ b/src/components/ValidateCodeActionModal/index.tsx @@ -25,6 +25,7 @@ function ValidateCodeActionModal({ footer, sendValidateCode, hasMagicCodeBeenSent, + isLoading, }: ValidateCodeActionModalProps) { const themeStyles = useThemeStyles(); const firstRenderRef = useRef(true); @@ -70,6 +71,7 @@ function ValidateCodeActionModal({ {description} ) => Errors; @@ -97,6 +98,7 @@ function BaseGetPhysicalCard({ const [draftValues] = useOnyx(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [isActionCodeModalVisible, setActionCodeModalVisible] = useState(false); + const [formData] = useOnyx(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM); const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); const cardID = cardToBeIssued?.cardID.toString() ?? '-1'; @@ -176,10 +178,11 @@ function BaseGetPhysicalCard({ {headline} {renderContent({onSubmit, submitButtonText, children, onValidate})} User.requestValidateCodeAction()} clearError={() => Wallet.clearPhysicalCardError(cardID)} - validateError={errorMessage} + validateError={!isEmptyObject(formData?.errors) ? formData?.errors : errorMessage} handleSubmitForm={handleIssuePhysicalCard} title={translate('cardPage.validateCardTitle')} onClose={() => setActionCodeModalVisible(false)} From 6ce22aa89dc5cd06e86b7954f866a8b5310a359d Mon Sep 17 00:00:00 2001 From: Hans Date: Fri, 1 Nov 2024 14:22:41 +0700 Subject: [PATCH 009/104] remove cardState optimistic --- src/libs/actions/Wallet.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index cfb4b6082ff8..cf4cc2e0be59 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -282,7 +282,6 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private key: ONYXKEYS.CARD_LIST, value: { [cardID]: { - state: 4, // NOT_ACTIVATED isLoading: true, errors: null, isSuccessful: null, From 2224d58b60a34e2314c9d71f0df5623d83df9e08 Mon Sep 17 00:00:00 2001 From: Hans Date: Fri, 1 Nov 2024 14:36:49 +0700 Subject: [PATCH 010/104] prettier --- src/libs/actions/Wallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index cf4cc2e0be59..da88957b1cbd 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -14,10 +14,10 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import type {PrivatePersonalDetails} from '@libs/GetPhysicalCardUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import type CONST from '@src/CONST'; -import * as FormActions from './FormActions'; import ONYXKEYS from '@src/ONYXKEYS'; import type {WalletAdditionalQuestionDetails} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import * as FormActions from './FormActions'; type WalletQuestionAnswer = { question: string; From 60d0fdc000357704fc2896c06409bbc7b6e6063d Mon Sep 17 00:00:00 2001 From: Hans Date: Fri, 1 Nov 2024 14:41:05 +0700 Subject: [PATCH 011/104] fix lint --- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx index 2c318996c235..3e4182399318 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -270,6 +270,7 @@ function BaseValidateCodeForm({ style={[styles.mt4]} success large + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing isLoading={account?.isLoading || isLoading} /> From cf938e08e0cfa47456147b78e957bd16d755336d Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 6 Nov 2024 15:21:25 +0700 Subject: [PATCH 012/104] update successful condition --- src/libs/actions/Wallet.ts | 4 ---- .../settings/Wallet/Card/BaseGetPhysicalCard.tsx | 12 +++++++----- src/types/onyx/Card.ts | 3 --- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index da88957b1cbd..d5b3862b62b5 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -282,9 +282,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private key: ONYXKEYS.CARD_LIST, value: { [cardID]: { - isLoading: true, errors: null, - isSuccessful: null, }, }, }, @@ -310,9 +308,7 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private value: { [cardID]: { state: 4, // NOT_ACTIVATED - isLoading: false, errors: null, - isSuccessful: true, }, }, }, diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 26b70a6e529f..2b92058caedc 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -102,7 +102,7 @@ function BaseGetPhysicalCard({ const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); const cardID = cardToBeIssued?.cardID.toString() ?? '-1'; - const isSuccessful = cardToBeIssued?.isSuccessful; + const [currentCardId, setCurrentCardId] = useState(cardID); const errorMessage = ErrorUtils.getLatestErrorMessageField(cardToBeIssued); useEffect(() => { @@ -137,15 +137,16 @@ function BaseGetPhysicalCard({ }, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, cardToBeIssued, privatePersonalDetails]); useEffect(() => { - if (!isSuccessful) { + if (!isConfirmation || !!cardToBeIssued || !currentCardId) { return; } // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); - Wallet.clearPhysicalCardError(cardID); - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); - }, [isSuccessful, cardID]); + Wallet.clearPhysicalCardError(currentCardId); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(currentCardId.toString())); + setCurrentCardId(undefined); + }, [currentCardId, isConfirmation, cardToBeIssued]); const onSubmit = useCallback(() => { const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); @@ -158,6 +159,7 @@ function BaseGetPhysicalCard({ const handleIssuePhysicalCard = useCallback( (validateCode: string) => { + setCurrentCardId(cardToBeIssued?.cardID.toString()); const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); // If the current step of the get physical card flow is the confirmation page Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? -1, session?.authToken ?? '', updatedPrivatePersonalDetails, validateCode); diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index a7debf5df925..c1d0e09d9312 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -115,9 +115,6 @@ type Card = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Collection of form field errors */ errorFields?: OnyxCommon.ErrorFields; }>; - - /** Card status */ - isSuccessful?: boolean; }>; /** Model of Expensify card details */ From 51db8501e5c6811854e659f19e62ad24a593ffe7 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 7 Nov 2024 15:10:02 -0800 Subject: [PATCH 013/104] Fix re-authentication test that should be failing --- tests/unit/NetworkTest.ts | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index e482cc3261d4..ea638aab4cf7 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -67,34 +67,24 @@ describe('NetworkTests', () => { }, }); - // Given a test user login and account ID + // And given they are signed in return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN).then(() => { expect(isOffline).toBe(false); - // Mock fetch() so that it throws a TypeError to simulate a bad network connection - global.fetch = jest.fn().mockRejectedValue(new TypeError(CONST.ERROR.FAILED_TO_FETCH)); - - const actualXhr = HttpUtils.xhr; - + // Set up mocks for the requests const mockedXhr = jest.fn(); mockedXhr .mockImplementationOnce(() => + // Given the first request is made with an expired authToken Promise.resolve({ jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, }), ) - // Fail the call to re-authenticate - .mockImplementationOnce(actualXhr) - - // The next call should still be using the old authToken - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, - }), - ) + // And the call to re-authenticate fails to fetch + .mockImplementationOnce(() => Promise.reject(new Error('Failed to fetch'))) - // Succeed the call to set a new authToken + // And there's another request to Authenticate and it succeeds .mockImplementationOnce(() => Promise.resolve({ jsonCode: CONST.JSON_CODE.SUCCESS, @@ -102,7 +92,7 @@ describe('NetworkTests', () => { }), ) - // All remaining requests should succeed + // And all remaining requests should succeed .mockImplementation(() => Promise.resolve({ jsonCode: CONST.JSON_CODE.SUCCESS, @@ -111,8 +101,10 @@ describe('NetworkTests', () => { HttpUtils.xhr = mockedXhr; - // This should first trigger re-authentication and then a Failed to fetch + // When the user opens their public profile page PersonalDetails.openPublicProfilePage(TEST_USER_ACCOUNT_ID); + + // And the network is back online to process the requests return waitForBatchedUpdates() .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) .then(() => { @@ -123,12 +115,13 @@ describe('NetworkTests', () => { return waitForBatchedUpdates(); }) .then(() => { - // Then we will eventually have 1 call to OpenPublicProfilePage and 1 calls to Authenticate - const callsToOpenPublicProfilePage = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'OpenPublicProfilePage'); + // Then there will have been 2 calls to Authenticate, one for the failed re-authentication and one retry that succeeds const callsToAuthenticate = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'Authenticate'); + expect(callsToAuthenticate.length).toBe(2); + // And two calls to openPublicProfilePage, one with the expired token and one after re-authentication + const callsToOpenPublicProfilePage = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'OpenPublicProfilePage'); expect(callsToOpenPublicProfilePage.length).toBe(1); - expect(callsToAuthenticate.length).toBe(1); }); }); }); From 1fd0aae2e5f15d0584ec4a5d23e28c01e9a2997a Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 8 Nov 2024 13:59:46 +0700 Subject: [PATCH 014/104] refactor requestMoney function --- ios/NewExpensify.xcodeproj/project.pbxproj | 6 +- src/libs/actions/IOU.ts | 78 +++-- .../iou/request/step/IOURequestStepAmount.tsx | 24 +- .../step/IOURequestStepConfirmation.tsx | 53 ++-- .../step/IOURequestStepScan/index.native.tsx | 64 ++-- .../request/step/IOURequestStepScan/index.tsx | 64 ++-- tests/actions/IOUTest.ts | 278 ++++++++++++++---- 7 files changed, 373 insertions(+), 194 deletions(-) diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index d8eceab72b95..cd38fcaaaf6c 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -45,7 +45,7 @@ D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */; }; DD79042B2792E76D004484B4 /* RCTBootSplash.mm in Sources */ = {isa = PBXBuildFile; fileRef = DD79042A2792E76D004484B4 /* RCTBootSplash.mm */; }; DDCB2E57F334C143AC462B43 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */; }; - E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */ = {isa = PBXBuildFile; }; + E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9DF872C2525201700607FDC /* AirshipConfig.plist */; }; ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */; }; F0C450EA2705020500FD2970 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = F0C450E92705020500FD2970 /* colors.json */; }; @@ -178,8 +178,8 @@ buildActionMask = 2147483647; files = ( 383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */, - E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, - E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 1db2555f3393..87f35902a36a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -164,6 +164,37 @@ type GPSPoint = { long: number; }; +type RequestMoneyInformation = { + report: OnyxEntry; + payeeEmail: string | undefined; + payeeAccountID: number; + participant: Participant; + policy?: OnyxEntry; + policyTagList?: OnyxEntry; + policyCategories?: OnyxEntry; + gpsPoints?: GPSPoint; + action?: IOUAction; + reimbursible?: boolean; +}; + +type RequestMoneyTransactionData = { + attendees: Attendee[] | undefined; + amount: number; + currency: string; + comment?: string; + receipt?: Receipt; + category?: string; + tag?: string; + taxCode?: string; + taxAmount?: number; + billable?: boolean; + merchant: string; + created: string; + actionableWhisperReportActionID?: string; + linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction; + linkedTrackedExpenseReportID?: string; +}; + let allPersonalDetails: OnyxTypes.PersonalDetailsList = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -3558,33 +3589,26 @@ function shareTrackedExpense( /** * Submit expense to another user */ -function requestMoney( - report: OnyxEntry, - amount: number, - attendees: Attendee[] | undefined, - currency: string, - created: string, - merchant: string, - payeeEmail: string | undefined, - payeeAccountID: number, - participant: Participant, - comment: string, - receipt: Receipt | undefined, - category?: string, - tag?: string, - taxCode = '', - taxAmount = 0, - billable?: boolean, - policy?: OnyxEntry, - policyTagList?: OnyxEntry, - policyCategories?: OnyxEntry, - gpsPoints?: GPSPoint, - action?: IOUAction, - actionableWhisperReportActionID?: string, - linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction, - linkedTrackedExpenseReportID?: string, - reimbursible?: boolean, -) { +function requestMoney(requestMoneyInformation: RequestMoneyInformation, requestMoneyTransactionData: RequestMoneyTransactionData) { + const {report, payeeEmail, payeeAccountID, participant, policy, policyTagList, policyCategories, gpsPoints, action, reimbursible} = requestMoneyInformation; + const { + amount, + currency, + merchant, + comment = '', + receipt, + category, + tag, + taxCode = '', + taxAmount = 0, + billable, + created, + attendees, + actionableWhisperReportActionID, + linkedTrackedExpenseReportAction, + linkedTrackedExpenseReportID, + } = requestMoneyTransactionData; + // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReportOrDraftReport(report?.chatReportID) : report; diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 9c6f39ea8c5a..8aced83515f4 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -210,17 +210,19 @@ function IOURequestStepAmount({ if (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.REQUEST) { playSound(SOUNDS.DONE); IOU.requestMoney( - report, - backendAmount, - transaction?.attendees, - currency, - transaction?.created ?? '', - CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participants.at(0) ?? {}, - '', - {}, + { + report, + participant: participants.at(0) ?? {}, + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + }, + { + amount: backendAmount, + currency, + created: transaction?.created ?? '', + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + attendees: transaction?.attendees, + }, ); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index aa3a432a0e5a..f268e7a03477 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -237,32 +237,35 @@ function IOURequestStepConfirmation({ if (!participant) { return; } - IOU.requestMoney( - report, - transaction.amount, - transaction.attendees, - transaction.currency, - transaction.created, - transaction.merchant, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - trimmedComment, - receiptObj, - transaction.category, - transaction.tag, - transactionTaxCode, - transactionTaxAmount, - transaction.billable, - policy, - policyTags, - policyCategories, - gpsPoints, - action, - transaction.actionableWhisperReportActionID, - transaction.linkedTrackedExpenseReportAction, - transaction.linkedTrackedExpenseReportID, + { + report, + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, + policy, + policyTagList: policyTags, + policyCategories, + gpsPoints, + action, + }, + { + amount: transaction.amount, + attendees: transaction.attendees, + currency: transaction.currency, + created: transaction.created, + merchant: transaction.merchant, + comment: trimmedComment, + receipt: receiptObj, + category: transaction.category, + tag: transaction.tag, + taxCode: transactionTaxCode, + taxAmount: transactionTaxAmount, + billable: transaction.billable, + actionableWhisperReportActionID: transaction.actionableWhisperReportActionID, + linkedTrackedExpenseReportAction: transaction.linkedTrackedExpenseReportAction, + linkedTrackedExpenseReportID: transaction.linkedTrackedExpenseReportID, + }, ); }, [report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, policy, policyTags, policyCategories, action], diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index f7e575b898fd..ff649dc303e1 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -255,17 +255,20 @@ function IOURequestStepScan({ ); } else { IOU.requestMoney( - report, - 0, - transaction?.attendees, - transaction?.currency ?? 'USD', - transaction?.created ?? '', - '', - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - '', - receipt, + { + report, + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, + }, + { + amount: 0, + attendees: transaction?.attendees, + currency: transaction?.currency ?? 'USD', + created: transaction?.created ?? '', + merchant: '', + receipt, + }, ); } }, @@ -351,28 +354,25 @@ function IOURequestStepScan({ ); } else { IOU.requestMoney( - report, - 0, - transaction?.attendees, - transaction?.currency ?? 'USD', - transaction?.created ?? '', - '', - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - '', - receipt, - '', - '', - '', - 0, - false, - policy, - {}, - {}, { - lat: successData.coords.latitude, - long: successData.coords.longitude, + report, + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, + policy, + gpsPoints: { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }, + }, + { + amount: 0, + attendees: transaction?.attendees, + currency: transaction?.currency ?? 'USD', + created: transaction?.created ?? '', + merchant: '', + receipt, + billable: false, }, ); } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index ecf84c877496..b2984c19af05 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -284,17 +284,20 @@ function IOURequestStepScan({ ); } else { IOU.requestMoney( - report, - 0, - transaction?.attendees, - transaction?.currency ?? 'USD', - transaction?.created ?? '', - '', - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - '', - receipt, + { + report, + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, + }, + { + amount: 0, + attendees: transaction?.attendees, + currency: transaction?.currency ?? 'USD', + created: transaction?.created ?? '', + merchant: '', + receipt, + }, ); } }, @@ -381,28 +384,25 @@ function IOURequestStepScan({ ); } else { IOU.requestMoney( - report, - 0, - transaction?.attendees, - transaction?.currency ?? 'USD', - transaction?.created ?? '', - '', - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - participant, - '', - receipt, - '', - '', - '', - 0, - false, - policy, - {}, - {}, { - lat: successData.coords.latitude, - long: successData.coords.longitude, + report, + payeeEmail: currentUserPersonalDetails.login, + payeeAccountID: currentUserPersonalDetails.accountID, + participant, + policy, + gpsPoints: { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }, + }, + { + amount: 0, + attendees: transaction?.attendees, + currency: transaction?.currency ?? 'USD', + created: transaction?.created ?? '', + merchant: '', + receipt, + billable: false, }, ); } diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index c2005d221273..7c0748d5404e 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -80,7 +80,22 @@ describe('actions/IOU', () => { let transactionThread: OnyxEntry; let transactionThreadCreatedAction: OnyxEntry; mockFetch?.pause?.(); - IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + IOU.requestMoney( + { + report: {reportID: ''}, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant, + comment, + }, + ); return waitForBatchedUpdates() .then( () => @@ -279,7 +294,22 @@ describe('actions/IOU', () => { }), ) .then(() => { - IOU.requestMoney(chatReport, amount, [], CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + IOU.requestMoney( + { + report: chatReport, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment, + }, + ); return waitForBatchedUpdates(); }) .then( @@ -483,7 +513,22 @@ describe('actions/IOU', () => { .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, {}); + IOU.requestMoney( + { + report: chatReport, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment, + }, + ); } return waitForBatchedUpdates(); }) @@ -623,7 +668,22 @@ describe('actions/IOU', () => { let transactionThreadReport: OnyxEntry; let transactionThreadAction: OnyxEntry; mockFetch?.pause?.(); - IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + IOU.requestMoney( + { + report: {reportID: ''}, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment, + }, + ); return ( waitForBatchedUpdates() .then( @@ -1424,7 +1484,22 @@ describe('actions/IOU', () => { let createIOUAction: OnyxEntry>; 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, {}); + IOU.requestMoney( + { + report: {reportID: ''}, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment, + }, + ); return waitForBatchedUpdates() .then( () => @@ -1624,7 +1699,22 @@ describe('actions/IOU', () => { let transaction: OnyxEntry; mockFetch?.pause?.(); - IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + IOU.requestMoney( + { + report: {reportID: ''}, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant, + comment, + }, + ); return waitForBatchedUpdates() .then(() => { Onyx.set(ONYXKEYS.SESSION, {email: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}); @@ -1778,7 +1868,22 @@ describe('actions/IOU', () => { let iouAction: OnyxEntry>; let transaction: OnyxEntry; - IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {}); + IOU.requestMoney( + { + report: {reportID: ''}, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant, + comment, + }, + ); return waitForBatchedUpdates() .then(() => { Onyx.set(ONYXKEYS.SESSION, {email: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}); @@ -1961,17 +2066,20 @@ describe('actions/IOU', () => { .then(() => { if (chatReport) { IOU.requestMoney( - chatReport, - amount, - [], - CONST.CURRENCY.USD, - '', - merchant, - RORY_EMAIL, - RORY_ACCOUNT_ID, - {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, - comment, - {}, + { + report: {reportID: ''}, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant, + comment, + }, ); } return waitForBatchedUpdates(); @@ -2086,17 +2194,20 @@ describe('actions/IOU', () => { .then(() => { if (chatReport) { IOU.requestMoney( - chatReport, - amount, - [], - CONST.CURRENCY.USD, - '', - merchant, - RORY_EMAIL, - RORY_ACCOUNT_ID, - {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, - comment, - {}, + { + report: {reportID: ''}, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant, + comment, + }, ); } return waitForBatchedUpdates(); @@ -2183,7 +2294,22 @@ describe('actions/IOU', () => { await TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); // When a submit IOU expense is made - IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', '', TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID, {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, comment, {}); + IOU.requestMoney( + { + report: {reportID: ''}, + payeeEmail: TEST_USER_LOGIN, + payeeAccountID: TEST_USER_ACCOUNT_ID, + participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment, + }, + ); await waitForBatchedUpdates(); // When fetching all reports from Onyx @@ -2914,7 +3040,22 @@ describe('actions/IOU', () => { const amount2 = 20000; const comment2 = 'Send me money please 2'; if (chatReport) { - IOU.requestMoney(chatReport, amount2, [], CONST.CURRENCY.USD, '', '', TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID, {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, comment2, {}); + IOU.requestMoney( + { + report: chatReport, + payeeEmail: TEST_USER_LOGIN, + payeeAccountID: TEST_USER_ACCOUNT_ID, + participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, + }, + { + amount: amount2, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment: comment2, + }, + ); } await waitForBatchedUpdates(); @@ -3118,17 +3259,20 @@ describe('actions/IOU', () => { .then(() => { if (chatReport) { IOU.requestMoney( - chatReport, - amount, - [], - CONST.CURRENCY.USD, - '', - merchant, - RORY_EMAIL, - RORY_ACCOUNT_ID, - {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, - comment, - {}, + { + report: chatReport, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant, + comment, + }, ); } return waitForBatchedUpdates(); @@ -3222,17 +3366,20 @@ describe('actions/IOU', () => { .then(() => { if (chatReport) { IOU.requestMoney( - chatReport, - amount, - [], - CONST.CURRENCY.USD, - '', - merchant, - RORY_EMAIL, - RORY_ACCOUNT_ID, - {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, - comment, - {}, + { + report: chatReport, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant, + comment, + }, ); } return waitForBatchedUpdates(); @@ -3327,17 +3474,20 @@ describe('actions/IOU', () => { .then(() => { if (chatReport) { IOU.requestMoney( - chatReport, - amount, - [], - CONST.CURRENCY.USD, - '', - merchant, - RORY_EMAIL, - RORY_ACCOUNT_ID, - {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, - comment, - {}, + { + report: chatReport, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant, + comment, + }, ); } return waitForBatchedUpdates(); From 91caa85e86a9baaecdd933e96533f0356e448757 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 8 Nov 2024 14:08:26 +0700 Subject: [PATCH 015/104] revert change --- ios/NewExpensify.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index cd38fcaaaf6c..d8eceab72b95 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -45,7 +45,7 @@ D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */; }; DD79042B2792E76D004484B4 /* RCTBootSplash.mm in Sources */ = {isa = PBXBuildFile; fileRef = DD79042A2792E76D004484B4 /* RCTBootSplash.mm */; }; DDCB2E57F334C143AC462B43 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D20D83B0E39BA6D21761E72 /* ExpoModulesProvider.swift */; }; - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */ = {isa = PBXBuildFile; }; E9DF872D2525201700607FDC /* AirshipConfig.plist in Resources */ = {isa = PBXBuildFile; fileRef = E9DF872C2525201700607FDC /* AirshipConfig.plist */; }; ED222ED90E074A5481A854FA /* ExpensifyNeue-BoldItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */; }; F0C450EA2705020500FD2970 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = F0C450E92705020500FD2970 /* colors.json */; }; @@ -178,8 +178,8 @@ buildActionMask = 2147483647; files = ( 383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */, - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, - E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, + E51DC681C7DEE40AEBDDFBFE /* (null) in Frameworks */, 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; From 6fc82cf9fa7ecd0dd79d146d6437b2a173909588 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 8 Nov 2024 14:20:33 +0700 Subject: [PATCH 016/104] fix test --- tests/actions/IOUTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 7c0748d5404e..dc32f4e99b58 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -2067,7 +2067,7 @@ describe('actions/IOU', () => { if (chatReport) { IOU.requestMoney( { - report: {reportID: ''}, + report: chatReport, payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, @@ -2195,7 +2195,7 @@ describe('actions/IOU', () => { if (chatReport) { IOU.requestMoney( { - report: {reportID: ''}, + report: chatReport, payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, From 1aebb87e1d7b37c32b502ffae8936de4543e16ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 8 Nov 2024 12:22:41 +0100 Subject: [PATCH 017/104] clean: moved everything over to CategoryOptionListUtils --- src/components/CategoryPicker.tsx | 11 +- src/libs/CategoryOptionListUtils.ts | 281 +++++ src/libs/OptionsListUtils.ts | 328 +----- tests/unit/CategoryOptionListUtils.ts | 1242 ++++++++++++++++++++ tests/unit/OptionsListUtilsTest.ts | 1522 +++---------------------- 5 files changed, 1676 insertions(+), 1708 deletions(-) create mode 100644 src/libs/CategoryOptionListUtils.ts create mode 100644 tests/unit/CategoryOptionListUtils.ts diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 33d97c6909f5..97aaf59ac2cf 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -3,6 +3,8 @@ import {useOnyx} from 'react-native-onyx'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import * as CategoryOptionsListUtils from '@libs/CategoryOptionListUtils'; +import type {Category} from '@libs/CategoryOptionListUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -27,7 +29,7 @@ function CategoryPicker({selectedCategory, policyID, onSubmit}: CategoryPickerPr const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const offlineMessage = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; - const selectedOptions = useMemo(() => { + const selectedOptions = useMemo((): Category[] => { if (!selectedCategory) { return []; } @@ -35,8 +37,9 @@ function CategoryPicker({selectedCategory, policyID, onSubmit}: CategoryPickerPr return [ { name: selectedCategory, - accountID: undefined, isSelected: true, + // TODO: i added this enabled property, is true the correct default? before it was just "as" casted... + enabled: true, }, ]; }, [selectedCategory]); @@ -44,11 +47,9 @@ function CategoryPicker({selectedCategory, policyID, onSubmit}: CategoryPickerPr const [sections, headerMessage, shouldShowTextInput] = useMemo(() => { const categories = policyCategories ?? policyCategoriesDraft ?? {}; const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter?.((p) => !isEmptyObject(p)); - const {categoryOptions} = OptionsListUtils.getFilteredOptions({ + const categoryOptions = CategoryOptionsListUtils.getCategoryListSections({ searchValue: debouncedSearchValue, selectedOptions, - includeP2P: false, - includeCategories: true, categories, recentlyUsedCategories: validPolicyRecentlyUsedCategories, }); diff --git a/src/libs/CategoryOptionListUtils.ts b/src/libs/CategoryOptionListUtils.ts new file mode 100644 index 000000000000..5ea71bca4136 --- /dev/null +++ b/src/libs/CategoryOptionListUtils.ts @@ -0,0 +1,281 @@ +import lodashGet from 'lodash/get'; +import lodashSet from 'lodash/set'; +import CONST from '@src/CONST'; +import type {PolicyCategories} from '@src/types/onyx'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import times from '@src/utils/times'; +import * as Localize from './Localize'; +import type {CategorySectionBase, OptionTree} from './OptionsListUtils'; + +type CategoryTreeSection = CategorySectionBase & { + data: OptionTree[]; + indexOffset?: number; +}; + +type Category = { + name: string; + enabled: boolean; + isSelected?: boolean; + pendingAction?: OnyxCommon.PendingAction; +}; + +type Hierarchy = Record; + +/** + * Builds the options for the category tree hierarchy via indents + * + * @param options - an initial object array + * @param options[].enabled - a flag to enable/disable option in a list + * @param options[].name - a name of an option + * @param [isOneLine] - a flag to determine if text should be one line + */ +function getCategoryOptionTree(options: Record | Category[], isOneLine = false, selectedOptions: Category[] = []): OptionTree[] { + const optionCollection = new Map(); + Object.values(options).forEach((option) => { + if (isOneLine) { + if (optionCollection.has(option.name)) { + return; + } + + optionCollection.set(option.name, { + text: option.name, + keyForList: option.name, + searchText: option.name, + tooltipText: option.name, + isDisabled: !option.enabled || option.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + isSelected: !!option.isSelected, + pendingAction: option.pendingAction, + }); + + return; + } + + option.name.split(CONST.PARENT_CHILD_SEPARATOR).forEach((optionName, index, array) => { + const indents = times(index, () => CONST.INDENTS).join(''); + const isChild = array.length - 1 === index; + const searchText = array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); + const selectedParentOption = !isChild && Object.values(selectedOptions).find((op) => op.name === searchText); + const isParentOptionDisabled = !selectedParentOption || !selectedParentOption.enabled || selectedParentOption.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + + if (optionCollection.has(searchText)) { + return; + } + + optionCollection.set(searchText, { + text: `${indents}${optionName}`, + keyForList: searchText, + searchText, + tooltipText: optionName, + isDisabled: isChild ? !option.enabled || option.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : isParentOptionDisabled, + isSelected: isChild ? !!option.isSelected : !!selectedParentOption, + pendingAction: option.pendingAction, + }); + }); + }); + + return Array.from(optionCollection.values()); +} + +/** + * Builds the section list for categories + */ +function getCategoryListSections({ + categories, + searchValue, + selectedOptions = [], + recentlyUsedCategories = [], + maxRecentReportsToShow = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, +}: { + categories: PolicyCategories; + selectedOptions?: Category[]; + searchValue?: string; + recentlyUsedCategories?: string[]; + maxRecentReportsToShow?: number; +}): CategoryTreeSection[] { + const sortedCategories = sortCategories(categories); + const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); + const enabledCategoriesNames = enabledCategories.map((category) => category.name); + const selectedOptionsWithDisabledState: Category[] = []; + const categorySections: CategoryTreeSection[] = []; + const numberOfEnabledCategories = enabledCategories.length; + + selectedOptions.forEach((option) => { + if (enabledCategoriesNames.includes(option.name)) { + const categoryObj = enabledCategories.find((category) => category.name === option.name); + selectedOptionsWithDisabledState.push({...(categoryObj ?? option), isSelected: true, enabled: true}); + return; + } + selectedOptionsWithDisabledState.push({...option, isSelected: true, enabled: false}); + }); + + if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { + const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); + categorySections.push({ + // "Selected" section + title: '', + shouldShow: false, + data, + indexOffset: data.length, + }); + + return categorySections; + } + + if (searchValue) { + const categoriesForSearch = [...selectedOptionsWithDisabledState, ...enabledCategories]; + const searchCategories: Category[] = []; + + categoriesForSearch.forEach((category) => { + if (!category.name.toLowerCase().includes(searchValue.toLowerCase())) { + return; + } + searchCategories.push({ + ...category, + isSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name), + }); + }); + + const data = getCategoryOptionTree(searchCategories, true); + categorySections.push({ + // "Search" section + title: '', + shouldShow: true, + data, + indexOffset: data.length, + }); + + return categorySections; + } + + if (selectedOptions.length > 0) { + const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); + categorySections.push({ + // "Selected" section + title: '', + shouldShow: false, + data, + indexOffset: data.length, + }); + } + + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); + + if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) { + const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState); + categorySections.push({ + // "All" section when items amount less than the threshold + title: '', + shouldShow: false, + data, + indexOffset: data.length, + }); + + return categorySections; + } + + const filteredRecentlyUsedCategories = recentlyUsedCategories + .filter( + (categoryName) => + !selectedOptionNames.includes(categoryName) && categories[categoryName]?.enabled && categories[categoryName]?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + ) + .map((categoryName) => ({ + name: categoryName, + enabled: categories[categoryName].enabled ?? false, + })); + + if (filteredRecentlyUsedCategories.length > 0) { + const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow); + + const data = getCategoryOptionTree(cutRecentlyUsedCategories, true); + categorySections.push({ + // "Recent" section + title: Localize.translateLocal('common.recent'), + shouldShow: true, + data, + indexOffset: data.length, + }); + } + + const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState); + categorySections.push({ + // "All" section when items amount more than the threshold + title: Localize.translateLocal('common.all'), + shouldShow: true, + data, + indexOffset: data.length, + }); + + return categorySections; +} + +/** + * Sorts categories using a simple object. + * It builds an hierarchy (based on an object), where each category has a name and other keys as subcategories. + * Via the hierarchy we avoid duplicating and sort categories one by one. Subcategories are being sorted alphabetically. + */ +function sortCategories(categories: Record): Category[] { + // Sorts categories alphabetically by name. + const sortedCategories = Object.values(categories).sort((a, b) => a.name.localeCompare(b.name)); + + // An object that respects nesting of categories. Also, can contain only uniq categories. + const hierarchy: Hierarchy = {}; + /** + * Iterates over all categories to set each category in a proper place in hierarchy + * It gets a path based on a category name e.g. "Parent: Child: Subcategory" -> "Parent.Child.Subcategory". + * { + * Parent: { + * name: "Parent", + * Child: { + * name: "Child" + * Subcategory: { + * name: "Subcategory" + * } + * } + * } + * } + */ + sortedCategories.forEach((category) => { + const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); + const existedValue = lodashGet(hierarchy, path, {}) as Hierarchy; + lodashSet(hierarchy, path, { + ...existedValue, + name: category.name, + pendingAction: category.pendingAction, + }); + }); + + /** + * A recursive function to convert hierarchy into an array of category objects. + * The category object contains base 2 properties: "name" and "enabled". + * It iterates each key one by one. When a category has subcategories, goes deeper into them. Also, sorts subcategories alphabetically. + */ + const flatHierarchy = (initialHierarchy: Hierarchy) => + Object.values(initialHierarchy).reduce((acc: Category[], category) => { + const {name, pendingAction, ...subcategories} = category; + if (name) { + const categoryObject: Category = { + name, + pendingAction, + enabled: categories[name]?.enabled ?? false, + }; + + acc.push(categoryObject); + } + + if (!isEmptyObject(subcategories)) { + const nestedCategories = flatHierarchy(subcategories); + + acc.push(...nestedCategories.sort((a, b) => a.name.localeCompare(b.name))); + } + + return acc; + }, []); + + return flatHierarchy(hierarchy); +} + +export {getCategoryListSections, getCategoryOptionTree, sortCategories}; + +export type {Category, CategorySectionBase, CategoryTreeSection, Hierarchy}; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 1296a64e571d..2b921cac0631 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1,9 +1,6 @@ /* eslint-disable no-continue */ import {Str} from 'expensify-common'; -// eslint-disable-next-line you-dont-need-lodash-underscore/get -import lodashGet from 'lodash/get'; import lodashOrderBy from 'lodash/orderBy'; -import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -39,7 +36,6 @@ import type {Attendee, Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import times from '@src/utils/times'; import Timing from './actions/Timing'; import filterArrayByMatch from './filterArrayByMatch'; import localeCompare from './LocaleCompare'; @@ -93,15 +89,6 @@ type PayeePersonalDetails = { keyForList: string; }; -type CategorySectionBase = { - title: string | undefined; - shouldShow: boolean; -}; - -type CategorySection = CategorySectionBase & { - data: Option[]; -}; - type TaxRatesOption = { text?: string; code?: string; @@ -119,26 +106,12 @@ type TaxSection = { data: TaxRatesOption[]; }; -type CategoryTreeSection = CategorySectionBase & { - data: OptionTree[]; - indexOffset?: number; -}; - -type Category = { - name: string; - enabled: boolean; - isSelected?: boolean; - pendingAction?: OnyxCommon.PendingAction; -}; - type Tax = { modifiedName: string; isSelected?: boolean; isDisabled?: boolean; }; -type Hierarchy = Record; - type GetOptionsConfig = { reportActions?: ReportActions; betas?: OnyxEntry; @@ -159,9 +132,6 @@ type GetOptionsConfig = { includeMoneyRequests?: boolean; excludeUnknownUsers?: boolean; includeP2P?: boolean; - includeCategories?: boolean; - categories?: PolicyCategories; - recentlyUsedCategories?: string[]; includeTags?: boolean; tags?: PolicyTags | Array; recentlyUsedTags?: string[]; @@ -206,6 +176,15 @@ type MemberForList = { reportID: string; }; +type CategorySectionBase = { + title: string | undefined; + shouldShow: boolean; +}; + +type CategorySection = CategorySectionBase & { + data: Option[]; +}; + type SectionForSearchTerm = { section: CategorySection; }; @@ -214,7 +193,6 @@ type Options = { personalDetails: ReportUtils.OptionData[]; userToInvite: ReportUtils.OptionData | null; currentUserOption: ReportUtils.OptionData | null | undefined; - categoryOptions: CategoryTreeSection[]; tagOptions: CategorySection[]; taxRatesOptions: CategorySection[]; policyReportFieldOptions?: CategorySection[] | null; @@ -916,72 +894,6 @@ function hasEnabledOptions(options: PolicyCategories | PolicyTag[]): boolean { return Object.values(options).some((option: PolicyTag | PolicyCategory) => option.enabled && option.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); } -/** - * Sorts categories using a simple object. - * It builds an hierarchy (based on an object), where each category has a name and other keys as subcategories. - * Via the hierarchy we avoid duplicating and sort categories one by one. Subcategories are being sorted alphabetically. - */ -function sortCategories(categories: Record): Category[] { - // Sorts categories alphabetically by name. - const sortedCategories = Object.values(categories).sort((a, b) => a.name.localeCompare(b.name)); - - // An object that respects nesting of categories. Also, can contain only uniq categories. - const hierarchy: Hierarchy = {}; - /** - * Iterates over all categories to set each category in a proper place in hierarchy - * It gets a path based on a category name e.g. "Parent: Child: Subcategory" -> "Parent.Child.Subcategory". - * { - * Parent: { - * name: "Parent", - * Child: { - * name: "Child" - * Subcategory: { - * name: "Subcategory" - * } - * } - * } - * } - */ - sortedCategories.forEach((category) => { - const path = category.name.split(CONST.PARENT_CHILD_SEPARATOR); - const existedValue = lodashGet(hierarchy, path, {}) as Hierarchy; - lodashSet(hierarchy, path, { - ...existedValue, - name: category.name, - pendingAction: category.pendingAction, - }); - }); - - /** - * A recursive function to convert hierarchy into an array of category objects. - * The category object contains base 2 properties: "name" and "enabled". - * It iterates each key one by one. When a category has subcategories, goes deeper into them. Also, sorts subcategories alphabetically. - */ - const flatHierarchy = (initialHierarchy: Hierarchy) => - Object.values(initialHierarchy).reduce((acc: Category[], category) => { - const {name, pendingAction, ...subcategories} = category; - if (name) { - const categoryObject: Category = { - name, - pendingAction, - enabled: categories[name]?.enabled ?? false, - }; - - acc.push(categoryObject); - } - - if (!isEmptyObject(subcategories)) { - const nestedCategories = flatHierarchy(subcategories); - - acc.push(...nestedCategories.sort((a, b) => a.name.localeCompare(b.name))); - } - - return acc; - }, []); - - return flatHierarchy(hierarchy); -} - /** * Sorts tags alphabetically by name. */ @@ -992,188 +904,6 @@ function sortTags(tags: Record | Array | Category[], isOneLine = false, selectedOptions: Category[] = []): OptionTree[] { - const optionCollection = new Map(); - Object.values(options).forEach((option) => { - if (isOneLine) { - if (optionCollection.has(option.name)) { - return; - } - - optionCollection.set(option.name, { - text: option.name, - keyForList: option.name, - searchText: option.name, - tooltipText: option.name, - isDisabled: !option.enabled || option.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - isSelected: !!option.isSelected, - pendingAction: option.pendingAction, - }); - - return; - } - - option.name.split(CONST.PARENT_CHILD_SEPARATOR).forEach((optionName, index, array) => { - const indents = times(index, () => CONST.INDENTS).join(''); - const isChild = array.length - 1 === index; - const searchText = array.slice(0, index + 1).join(CONST.PARENT_CHILD_SEPARATOR); - const selectedParentOption = !isChild && Object.values(selectedOptions).find((op) => op.name === searchText); - const isParentOptionDisabled = !selectedParentOption || !selectedParentOption.enabled || selectedParentOption.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - - if (optionCollection.has(searchText)) { - return; - } - - optionCollection.set(searchText, { - text: `${indents}${optionName}`, - keyForList: searchText, - searchText, - tooltipText: optionName, - isDisabled: isChild ? !option.enabled || option.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : isParentOptionDisabled, - isSelected: isChild ? !!option.isSelected : !!selectedParentOption, - pendingAction: option.pendingAction, - }); - }); - }); - - return Array.from(optionCollection.values()); -} - -/** - * Builds the section list for categories - */ -function getCategoryListSections( - categories: PolicyCategories, - recentlyUsedCategories: string[], - selectedOptions: Category[], - searchInputValue: string, - maxRecentReportsToShow: number, -): CategoryTreeSection[] { - const sortedCategories = sortCategories(categories); - const enabledCategories = Object.values(sortedCategories).filter((category) => category.enabled); - const enabledCategoriesNames = enabledCategories.map((category) => category.name); - const selectedOptionsWithDisabledState: Category[] = []; - const categorySections: CategoryTreeSection[] = []; - const numberOfEnabledCategories = enabledCategories.length; - - selectedOptions.forEach((option) => { - if (enabledCategoriesNames.includes(option.name)) { - const categoryObj = enabledCategories.find((category) => category.name === option.name); - selectedOptionsWithDisabledState.push({...(categoryObj ?? option), isSelected: true, enabled: true}); - return; - } - selectedOptionsWithDisabledState.push({...option, isSelected: true, enabled: false}); - }); - - if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { - const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); - categorySections.push({ - // "Selected" section - title: '', - shouldShow: false, - data, - indexOffset: data.length, - }); - - return categorySections; - } - - if (searchInputValue) { - const categoriesForSearch = [...selectedOptionsWithDisabledState, ...enabledCategories]; - const searchCategories: Category[] = []; - - categoriesForSearch.forEach((category) => { - if (!category.name.toLowerCase().includes(searchInputValue.toLowerCase())) { - return; - } - searchCategories.push({ - ...category, - isSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name), - }); - }); - - const data = getCategoryOptionTree(searchCategories, true); - categorySections.push({ - // "Search" section - title: '', - shouldShow: true, - data, - indexOffset: data.length, - }); - - return categorySections; - } - - if (selectedOptions.length > 0) { - const data = getCategoryOptionTree(selectedOptionsWithDisabledState, true); - categorySections.push({ - // "Selected" section - title: '', - shouldShow: false, - data, - indexOffset: data.length, - }); - } - - const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); - const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); - - if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) { - const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState); - categorySections.push({ - // "All" section when items amount less than the threshold - title: '', - shouldShow: false, - data, - indexOffset: data.length, - }); - - return categorySections; - } - - const filteredRecentlyUsedCategories = recentlyUsedCategories - .filter( - (categoryName) => - !selectedOptionNames.includes(categoryName) && categories[categoryName]?.enabled && categories[categoryName]?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - ) - .map((categoryName) => ({ - name: categoryName, - enabled: categories[categoryName].enabled ?? false, - })); - - if (filteredRecentlyUsedCategories.length > 0) { - const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow); - - const data = getCategoryOptionTree(cutRecentlyUsedCategories, true); - categorySections.push({ - // "Recent" section - title: Localize.translateLocal('common.recent'), - shouldShow: true, - data, - indexOffset: data.length, - }); - } - - const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState); - categorySections.push({ - // "All" section when items amount more than the threshold - title: Localize.translateLocal('common.all'), - shouldShow: true, - data, - indexOffset: data.length, - }); - - return categorySections; -} - /** * Transforms the provided tags into option objects. * @@ -1711,9 +1441,6 @@ function getOptions( includeMoneyRequests = false, excludeUnknownUsers = false, includeP2P = true, - includeCategories = false, - categories = {}, - recentlyUsedCategories = [], includeTags = false, tags = {}, recentlyUsedTags = [], @@ -1734,20 +1461,6 @@ function getOptions( shouldBoldTitleByDefault = true, }: GetOptionsConfig, ): Options { - if (includeCategories) { - const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); - - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - currentUserOption: null, - categoryOptions, - tagOptions: [], - taxRatesOptions: [], - }; - } - if (includeTags) { const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as SelectedTagOption[], searchInputValue, maxRecentReportsToShow); @@ -1756,7 +1469,6 @@ function getOptions( personalDetails: [], userToInvite: null, currentUserOption: null, - categoryOptions: [], tagOptions, taxRatesOptions: [], }; @@ -1770,7 +1482,6 @@ function getOptions( personalDetails: [], userToInvite: null, currentUserOption: null, - categoryOptions: [], tagOptions: [], taxRatesOptions, }; @@ -1783,7 +1494,6 @@ function getOptions( personalDetails: [], userToInvite: null, currentUserOption: null, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], policyReportFieldOptions: transformedPolicyReportFieldOptions, @@ -2045,7 +1755,6 @@ function getOptions( recentReports: recentReportOptions, userToInvite: canInviteUser ? userToInvite : null, currentUserOption, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; @@ -2130,9 +1839,6 @@ type FilteredOptionsParams = { excludeLogins?: string[]; includeOwnedWorkspaceChats?: boolean; includeP2P?: boolean; - includeCategories?: boolean; - categories?: PolicyCategories; - recentlyUsedCategories?: string[]; includeTags?: boolean; tags?: PolicyTags | Array; recentlyUsedTags?: string[]; @@ -2171,9 +1877,6 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue excludeLogins = [], includeOwnedWorkspaceChats = false, includeP2P = true, - includeCategories = false, - categories = {}, - recentlyUsedCategories = [], includeTags = false, tags = {}, recentlyUsedTags = [], @@ -2201,9 +1904,6 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue excludeLogins, includeOwnedWorkspaceChats, includeP2P, - includeCategories, - categories, - recentlyUsedCategories, includeTags, tags, recentlyUsedTags, @@ -2245,9 +1945,6 @@ function getAttendeeOptions( includeOwnedWorkspaceChats, includeRecentReports: false, includeP2P, - includeCategories: false, - categories: {}, - recentlyUsedCategories: [], includeTags: false, tags: {}, recentlyUsedTags: [], @@ -2549,7 +2246,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt personalDetails: personalDetails ?? [], userToInvite: null, currentUserOption, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; @@ -2585,7 +2281,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt recentReports: orderOptions(recentReports, searchValue, {preferChatroomsOverThreads, preferPolicyExpenseChat, preferRecentExpenseReports}), userToInvite, currentUserOption: matchResults.currentUserOption, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; @@ -2601,7 +2296,6 @@ function getEmptyOptions(): Options { personalDetails: [], userToInvite: null, currentUserOption: null, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; @@ -2635,10 +2329,8 @@ export { getLastMessageTextForReport, getEnabledCategoriesCount, hasEnabledOptions, - sortCategories, sortAlphabetically, sortTags, - getCategoryOptionTree, hasEnabledTags, formatMemberForList, formatSectionsFromSearchTerm, @@ -2660,4 +2352,4 @@ export { hasReportErrors, }; -export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, Tax, TaxRatesOption, Option, OptionTree}; +export type {CategorySection, CategorySectionBase, MemberForList, Options, OptionList, SearchOption, PayeePersonalDetails, Tax, TaxRatesOption, Option, OptionTree}; diff --git a/tests/unit/CategoryOptionListUtils.ts b/tests/unit/CategoryOptionListUtils.ts new file mode 100644 index 000000000000..21f5e6533e77 --- /dev/null +++ b/tests/unit/CategoryOptionListUtils.ts @@ -0,0 +1,1242 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import * as CategoryOptionsListUtils from '@libs/CategoryOptionListUtils'; +import type {PolicyCategories} from '@src/types/onyx'; +import type {PendingAction} from '@src/types/onyx/OnyxCommon'; + +describe('CategoryOptionListUtils', () => { + it('getFilteredOptions() for categories', () => { + const search = 'Food'; + const emptySearch = ''; + const wrongSearch = 'bla bla'; + const recentlyUsedCategories = ['Taxi', 'Restaurant']; + const selectedOptions: CategoryOptionsListUtils.Category[] = [ + { + name: 'Medical', + enabled: true, + }, + ]; + const smallCategoriesList: PolicyCategories = { + Taxi: { + enabled: false, + name: 'Taxi', + unencodedName: 'Taxi', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + pendingAction: undefined, + }, + Restaurant: { + enabled: true, + name: 'Restaurant', + unencodedName: 'Restaurant', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + pendingAction: 'delete', + }, + Food: { + enabled: true, + name: 'Food', + unencodedName: 'Food', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + pendingAction: undefined, + }, + 'Food: Meat': { + enabled: true, + name: 'Food: Meat', + unencodedName: 'Food: Meat', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + pendingAction: undefined, + }, + }; + const smallResultList: CategoryOptionsListUtils.CategoryTreeSection[] = [ + { + title: '', + shouldShow: false, + data: [ + { + text: 'Food', + keyForList: 'Food', + searchText: 'Food', + tooltipText: 'Food', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Meat', + keyForList: 'Food: Meat', + searchText: 'Food: Meat', + tooltipText: 'Meat', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Restaurant', + keyForList: 'Restaurant', + searchText: 'Restaurant', + tooltipText: 'Restaurant', + isDisabled: true, + isSelected: false, + pendingAction: 'delete', + }, + ], + indexOffset: 3, + }, + ]; + const smallSearchResultList: CategoryOptionsListUtils.CategoryTreeSection[] = [ + { + title: '', + shouldShow: true, + indexOffset: 2, + data: [ + { + text: 'Food', + keyForList: 'Food', + searchText: 'Food', + tooltipText: 'Food', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Food: Meat', + keyForList: 'Food: Meat', + searchText: 'Food: Meat', + tooltipText: 'Food: Meat', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + ], + }, + ]; + const smallWrongSearchResultList: CategoryOptionsListUtils.CategoryTreeSection[] = [ + { + title: '', + shouldShow: true, + indexOffset: 0, + data: [], + }, + ]; + const largeCategoriesList: PolicyCategories = { + Taxi: { + enabled: false, + name: 'Taxi', + unencodedName: 'Taxi', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + Restaurant: { + enabled: true, + name: 'Restaurant', + unencodedName: 'Restaurant', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + Food: { + enabled: true, + name: 'Food', + unencodedName: 'Food', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + 'Food: Meat': { + enabled: true, + name: 'Food: Meat', + unencodedName: 'Food: Meat', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + 'Food: Milk': { + enabled: true, + name: 'Food: Milk', + unencodedName: 'Food: Milk', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + 'Food: Vegetables': { + enabled: false, + name: 'Food: Vegetables', + unencodedName: 'Food: Vegetables', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + 'Cars: Audi': { + enabled: true, + name: 'Cars: Audi', + unencodedName: 'Cars: Audi', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + 'Cars: BMW': { + enabled: false, + name: 'Cars: BMW', + unencodedName: 'Cars: BMW', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + 'Cars: Mercedes-Benz': { + enabled: true, + name: 'Cars: Mercedes-Benz', + unencodedName: 'Cars: Mercedes-Benz', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + Medical: { + enabled: false, + name: 'Medical', + unencodedName: 'Medical', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + 'Travel: Meals': { + enabled: true, + name: 'Travel: Meals', + unencodedName: 'Travel: Meals', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + 'Travel: Meals: Breakfast': { + enabled: true, + name: 'Travel: Meals: Breakfast', + unencodedName: 'Travel: Meals: Breakfast', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + 'Travel: Meals: Dinner': { + enabled: false, + name: 'Travel: Meals: Dinner', + unencodedName: 'Travel: Meals: Dinner', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + 'Travel: Meals: Lunch': { + enabled: true, + name: 'Travel: Meals: Lunch', + unencodedName: 'Travel: Meals: Lunch', + areCommentsRequired: false, + 'GL Code': '', + externalID: '', + origin: '', + }, + }; + const largeResultList: CategoryOptionsListUtils.CategoryTreeSection[] = [ + { + title: '', + shouldShow: false, + indexOffset: 1, + data: [ + { + text: 'Medical', + keyForList: 'Medical', + searchText: 'Medical', + tooltipText: 'Medical', + isDisabled: true, + isSelected: true, + pendingAction: undefined, + }, + ], + }, + { + title: 'Recent', + shouldShow: true, + indexOffset: 1, + data: [ + { + text: 'Restaurant', + keyForList: 'Restaurant', + searchText: 'Restaurant', + tooltipText: 'Restaurant', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + ], + }, + { + title: 'All', + shouldShow: true, + indexOffset: 11, + data: [ + { + text: 'Cars', + keyForList: 'Cars', + searchText: 'Cars', + tooltipText: 'Cars', + isDisabled: true, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Audi', + keyForList: 'Cars: Audi', + searchText: 'Cars: Audi', + tooltipText: 'Audi', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Mercedes-Benz', + keyForList: 'Cars: Mercedes-Benz', + searchText: 'Cars: Mercedes-Benz', + tooltipText: 'Mercedes-Benz', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Food', + keyForList: 'Food', + searchText: 'Food', + tooltipText: 'Food', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Meat', + keyForList: 'Food: Meat', + searchText: 'Food: Meat', + tooltipText: 'Meat', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Milk', + keyForList: 'Food: Milk', + searchText: 'Food: Milk', + tooltipText: 'Milk', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Restaurant', + keyForList: 'Restaurant', + searchText: 'Restaurant', + tooltipText: 'Restaurant', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Travel', + keyForList: 'Travel', + searchText: 'Travel', + tooltipText: 'Travel', + isDisabled: true, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Meals', + keyForList: 'Travel: Meals', + searchText: 'Travel: Meals', + tooltipText: 'Meals', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Breakfast', + keyForList: 'Travel: Meals: Breakfast', + searchText: 'Travel: Meals: Breakfast', + tooltipText: 'Breakfast', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Lunch', + keyForList: 'Travel: Meals: Lunch', + searchText: 'Travel: Meals: Lunch', + tooltipText: 'Lunch', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + ], + }, + ]; + const largeSearchResultList: CategoryOptionsListUtils.CategoryTreeSection[] = [ + { + title: '', + shouldShow: true, + indexOffset: 3, + data: [ + { + text: 'Food', + keyForList: 'Food', + searchText: 'Food', + tooltipText: 'Food', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Food: Meat', + keyForList: 'Food: Meat', + searchText: 'Food: Meat', + tooltipText: 'Food: Meat', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Food: Milk', + keyForList: 'Food: Milk', + searchText: 'Food: Milk', + tooltipText: 'Food: Milk', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + ], + }, + ]; + const largeWrongSearchResultList: CategoryOptionsListUtils.CategoryTreeSection[] = [ + { + title: '', + shouldShow: true, + indexOffset: 0, + data: [], + }, + ]; + const emptyCategoriesList = {}; + const emptySelectedResultList: CategoryOptionsListUtils.CategoryTreeSection[] = [ + { + title: '', + shouldShow: false, + indexOffset: 1, + data: [ + { + text: 'Medical', + keyForList: 'Medical', + searchText: 'Medical', + tooltipText: 'Medical', + isDisabled: true, + isSelected: true, + pendingAction: undefined, + }, + ], + }, + ]; + + const smallResult = CategoryOptionsListUtils.getCategoryListSections({ + searchValue: emptySearch, + categories: smallCategoriesList, + }); + expect(smallResult).toStrictEqual(smallResultList); + + const smallSearchResult = CategoryOptionsListUtils.getCategoryListSections({searchValue: search, categories: smallCategoriesList}); + expect(smallSearchResult).toStrictEqual(smallSearchResultList); + + const smallWrongSearchResult = CategoryOptionsListUtils.getCategoryListSections({searchValue: wrongSearch, categories: smallCategoriesList}); + expect(smallWrongSearchResult).toStrictEqual(smallWrongSearchResultList); + + const largeResult = CategoryOptionsListUtils.getCategoryListSections({ + searchValue: emptySearch, + selectedOptions, + categories: largeCategoriesList, + recentlyUsedCategories, + }); + expect(largeResult).toStrictEqual(largeResultList); + + const largeSearchResult = CategoryOptionsListUtils.getCategoryListSections({ + searchValue: search, + selectedOptions, + categories: largeCategoriesList, + recentlyUsedCategories, + }); + expect(largeSearchResult).toStrictEqual(largeSearchResultList); + + const largeWrongSearchResult = CategoryOptionsListUtils.getCategoryListSections({ + searchValue: wrongSearch, + selectedOptions, + categories: largeCategoriesList, + recentlyUsedCategories, + }); + expect(largeWrongSearchResult).toStrictEqual(largeWrongSearchResultList); + + const emptyResult = CategoryOptionsListUtils.getCategoryListSections({searchValue: search, selectedOptions, categories: emptyCategoriesList}); + expect(emptyResult).toStrictEqual(emptySelectedResultList); + }); + + it('getCategoryOptionTree()', () => { + const categories = { + Meals: { + enabled: true, + name: 'Meals', + }, + Restaurant: { + enabled: true, + name: 'Restaurant', + }, + Food: { + enabled: true, + name: 'Food', + }, + 'Food: Meat': { + enabled: true, + name: 'Food: Meat', + }, + 'Food: Milk': { + enabled: true, + name: 'Food: Milk', + }, + 'Cars: Audi': { + enabled: true, + name: 'Cars: Audi', + }, + 'Cars: Mercedes-Benz': { + enabled: true, + name: 'Cars: Mercedes-Benz', + }, + 'Travel: Meals': { + enabled: true, + name: 'Travel: Meals', + }, + 'Travel: Meals: Breakfast': { + enabled: true, + name: 'Travel: Meals: Breakfast', + }, + 'Travel: Meals: Lunch': { + enabled: true, + name: 'Travel: Meals: Lunch', + }, + Plain: { + enabled: true, + name: 'Plain', + }, + Audi: { + enabled: true, + name: 'Audi', + }, + Health: { + enabled: true, + name: 'Health', + }, + 'A: B: C': { + enabled: true, + name: 'A: B: C', + }, + 'A: B: C: D: E': { + enabled: true, + name: 'A: B: C: D: E', + }, + }; + const result = [ + { + text: 'Meals', + keyForList: 'Meals', + searchText: 'Meals', + tooltipText: 'Meals', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Restaurant', + keyForList: 'Restaurant', + searchText: 'Restaurant', + tooltipText: 'Restaurant', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Food', + keyForList: 'Food', + searchText: 'Food', + tooltipText: 'Food', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Meat', + keyForList: 'Food: Meat', + searchText: 'Food: Meat', + tooltipText: 'Meat', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Milk', + keyForList: 'Food: Milk', + searchText: 'Food: Milk', + tooltipText: 'Milk', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Cars', + keyForList: 'Cars', + searchText: 'Cars', + tooltipText: 'Cars', + isDisabled: true, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Audi', + keyForList: 'Cars: Audi', + searchText: 'Cars: Audi', + tooltipText: 'Audi', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Mercedes-Benz', + keyForList: 'Cars: Mercedes-Benz', + searchText: 'Cars: Mercedes-Benz', + tooltipText: 'Mercedes-Benz', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Travel', + keyForList: 'Travel', + searchText: 'Travel', + tooltipText: 'Travel', + isDisabled: true, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Meals', + keyForList: 'Travel: Meals', + searchText: 'Travel: Meals', + tooltipText: 'Meals', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Breakfast', + keyForList: 'Travel: Meals: Breakfast', + searchText: 'Travel: Meals: Breakfast', + tooltipText: 'Breakfast', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' Lunch', + keyForList: 'Travel: Meals: Lunch', + searchText: 'Travel: Meals: Lunch', + tooltipText: 'Lunch', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Plain', + keyForList: 'Plain', + searchText: 'Plain', + tooltipText: 'Plain', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Audi', + keyForList: 'Audi', + searchText: 'Audi', + tooltipText: 'Audi', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Health', + keyForList: 'Health', + searchText: 'Health', + tooltipText: 'Health', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'A', + keyForList: 'A', + searchText: 'A', + tooltipText: 'A', + isDisabled: true, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' B', + keyForList: 'A: B', + searchText: 'A: B', + tooltipText: 'B', + isDisabled: true, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' C', + keyForList: 'A: B: C', + searchText: 'A: B: C', + tooltipText: 'C', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' D', + keyForList: 'A: B: C: D', + searchText: 'A: B: C: D', + tooltipText: 'D', + isDisabled: true, + isSelected: false, + pendingAction: undefined, + }, + { + text: ' E', + keyForList: 'A: B: C: D: E', + searchText: 'A: B: C: D: E', + tooltipText: 'E', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + ]; + const resultOneLine = [ + { + text: 'Meals', + keyForList: 'Meals', + searchText: 'Meals', + tooltipText: 'Meals', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Restaurant', + keyForList: 'Restaurant', + searchText: 'Restaurant', + tooltipText: 'Restaurant', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Food', + keyForList: 'Food', + searchText: 'Food', + tooltipText: 'Food', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Food: Meat', + keyForList: 'Food: Meat', + searchText: 'Food: Meat', + tooltipText: 'Food: Meat', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Food: Milk', + keyForList: 'Food: Milk', + searchText: 'Food: Milk', + tooltipText: 'Food: Milk', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Cars: Audi', + keyForList: 'Cars: Audi', + searchText: 'Cars: Audi', + tooltipText: 'Cars: Audi', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Cars: Mercedes-Benz', + keyForList: 'Cars: Mercedes-Benz', + searchText: 'Cars: Mercedes-Benz', + tooltipText: 'Cars: Mercedes-Benz', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Travel: Meals', + keyForList: 'Travel: Meals', + searchText: 'Travel: Meals', + tooltipText: 'Travel: Meals', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Travel: Meals: Breakfast', + keyForList: 'Travel: Meals: Breakfast', + searchText: 'Travel: Meals: Breakfast', + tooltipText: 'Travel: Meals: Breakfast', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Travel: Meals: Lunch', + keyForList: 'Travel: Meals: Lunch', + searchText: 'Travel: Meals: Lunch', + tooltipText: 'Travel: Meals: Lunch', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Plain', + keyForList: 'Plain', + searchText: 'Plain', + tooltipText: 'Plain', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Audi', + keyForList: 'Audi', + searchText: 'Audi', + tooltipText: 'Audi', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'Health', + keyForList: 'Health', + searchText: 'Health', + tooltipText: 'Health', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'A: B: C', + keyForList: 'A: B: C', + searchText: 'A: B: C', + tooltipText: 'A: B: C', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + { + text: 'A: B: C: D: E', + keyForList: 'A: B: C: D: E', + searchText: 'A: B: C: D: E', + tooltipText: 'A: B: C: D: E', + isDisabled: false, + isSelected: false, + pendingAction: undefined, + }, + ]; + + expect(CategoryOptionsListUtils.getCategoryOptionTree(categories)).toStrictEqual(result); + expect(CategoryOptionsListUtils.getCategoryOptionTree(categories, true)).toStrictEqual(resultOneLine); + }); + + it('sortCategories', () => { + const categoriesIncorrectOrdering = { + Taxi: { + name: 'Taxi', + enabled: false, + }, + 'Test1: Subtest2': { + name: 'Test1: Subtest2', + enabled: true, + }, + 'Test: Test1: Subtest4': { + name: 'Test: Test1: Subtest4', + enabled: true, + }, + Taxes: { + name: 'Taxes', + enabled: true, + }, + Test: { + name: 'Test', + enabled: true, + pendingAction: 'delete' as PendingAction, + }, + Test1: { + name: 'Test1', + enabled: true, + }, + 'Travel: Nested-Travel': { + name: 'Travel: Nested-Travel', + enabled: true, + }, + 'Test1: Subtest1': { + name: 'Test1: Subtest1', + enabled: true, + }, + 'Test: Test1': { + name: 'Test: Test1', + enabled: true, + }, + 'Test: Test1: Subtest1': { + name: 'Test: Test1: Subtest1', + enabled: true, + }, + 'Test: Test1: Subtest3': { + name: 'Test: Test1: Subtest3', + enabled: false, + }, + 'Test: Test1: Subtest2': { + name: 'Test: Test1: Subtest2', + enabled: true, + }, + 'Test: Test2': { + name: 'Test: Test2', + enabled: true, + }, + Travel: { + name: 'Travel', + enabled: true, + }, + Utilities: { + name: 'Utilities', + enabled: true, + }, + 'Test: Test3: Subtest1': { + name: 'Test: Test3: Subtest1', + enabled: true, + }, + 'Test1: Subtest3': { + name: 'Test1: Subtest3', + enabled: true, + }, + }; + const result = [ + { + name: 'Taxes', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Taxi', + enabled: false, + pendingAction: undefined, + }, + { + name: 'Test', + enabled: true, + pendingAction: 'delete', + }, + { + name: 'Test: Test1', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Test: Test1: Subtest1', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Test: Test1: Subtest2', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Test: Test1: Subtest3', + enabled: false, + pendingAction: undefined, + }, + { + name: 'Test: Test1: Subtest4', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Test: Test2', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Test: Test3: Subtest1', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Test1', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Test1: Subtest1', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Test1: Subtest2', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Test1: Subtest3', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Travel', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Travel: Nested-Travel', + enabled: true, + pendingAction: undefined, + }, + { + name: 'Utilities', + enabled: true, + pendingAction: undefined, + }, + ]; + const categoriesIncorrectOrdering2 = { + 'Cars: BMW': { + enabled: false, + name: 'Cars: BMW', + }, + Medical: { + enabled: false, + name: 'Medical', + }, + 'Travel: Meals: Lunch': { + enabled: true, + name: 'Travel: Meals: Lunch', + }, + 'Cars: Mercedes-Benz': { + enabled: true, + name: 'Cars: Mercedes-Benz', + }, + Food: { + enabled: true, + name: 'Food', + }, + 'Food: Meat': { + enabled: true, + name: 'Food: Meat', + }, + 'Travel: Meals: Dinner': { + enabled: false, + name: 'Travel: Meals: Dinner', + }, + 'Food: Vegetables': { + enabled: false, + name: 'Food: Vegetables', + }, + Restaurant: { + enabled: true, + name: 'Restaurant', + }, + Taxi: { + enabled: false, + name: 'Taxi', + }, + 'Food: Milk': { + enabled: true, + name: 'Food: Milk', + }, + 'Travel: Meals': { + enabled: true, + name: 'Travel: Meals', + }, + 'Travel: Meals: Breakfast': { + enabled: true, + name: 'Travel: Meals: Breakfast', + }, + 'Cars: Audi': { + enabled: true, + name: 'Cars: Audi', + }, + }; + const result2 = [ + { + enabled: true, + name: 'Cars: Audi', + pendingAction: undefined, + }, + { + enabled: false, + name: 'Cars: BMW', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Cars: Mercedes-Benz', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Food', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Food: Meat', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Food: Milk', + pendingAction: undefined, + }, + { + enabled: false, + name: 'Food: Vegetables', + pendingAction: undefined, + }, + { + enabled: false, + name: 'Medical', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Restaurant', + pendingAction: undefined, + }, + { + enabled: false, + name: 'Taxi', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Travel: Meals', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Travel: Meals: Breakfast', + pendingAction: undefined, + }, + { + enabled: false, + name: 'Travel: Meals: Dinner', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Travel: Meals: Lunch', + pendingAction: undefined, + }, + ]; + const categoriesIncorrectOrdering3 = { + 'Movies: Mr. Nobody': { + enabled: true, + name: 'Movies: Mr. Nobody', + }, + Movies: { + enabled: true, + name: 'Movies', + }, + 'House, M.D.': { + enabled: true, + name: 'House, M.D.', + }, + 'Dr. House': { + enabled: true, + name: 'Dr. House', + }, + 'Many.dots.on.the.way.': { + enabled: true, + name: 'Many.dots.on.the.way.', + }, + 'More.Many.dots.on.the.way.': { + enabled: false, + name: 'More.Many.dots.on.the.way.', + }, + }; + const result3 = [ + { + enabled: true, + name: 'Dr. House', + pendingAction: undefined, + }, + { + enabled: true, + name: 'House, M.D.', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Many.dots.on.the.way.', + pendingAction: undefined, + }, + { + enabled: false, + name: 'More.Many.dots.on.the.way.', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Movies', + pendingAction: undefined, + }, + { + enabled: true, + name: 'Movies: Mr. Nobody', + pendingAction: undefined, + }, + ]; + + expect(CategoryOptionsListUtils.sortCategories(categoriesIncorrectOrdering)).toStrictEqual(result); + expect(CategoryOptionsListUtils.sortCategories(categoriesIncorrectOrdering2)).toStrictEqual(result2); + expect(CategoryOptionsListUtils.sortCategories(categoriesIncorrectOrdering3)).toStrictEqual(result3); + }); +}); diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 5a0cd6638a07..ca830cb48074 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -7,8 +7,7 @@ import CONST from '@src/CONST'; import * as OptionsListUtils from '@src/libs/OptionsListUtils'; import * as ReportUtils from '@src/libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Policy, PolicyCategories, Report, TaxRatesWithDefault, Transaction} from '@src/types/onyx'; -import type {PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {PersonalDetails, Policy, Report, TaxRatesWithDefault, Transaction} from '@src/types/onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; type PersonalDetailsList = Record; @@ -639,115 +638,86 @@ describe('OptionsListUtils', () => { expect(results.personalDetails.at(3)?.text).toBe('Invisible Woman'); }); - it('getFilteredOptions() for categories', () => { - const search = 'Food'; + it('getFilteredOptions() for tags', () => { + const search = 'ing'; const emptySearch = ''; const wrongSearch = 'bla bla'; - const recentlyUsedCategories = ['Taxi', 'Restaurant']; - const selectedOptions: Array> = [ + const recentlyUsedTags = ['Engineering', 'HR']; + + const selectedOptions = [ { name: 'Medical', - enabled: true, }, ]; - const smallCategoriesList: PolicyCategories = { - Taxi: { + const smallTagsList: Record = { + Engineering: { enabled: false, - name: 'Taxi', - unencodedName: 'Taxi', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', - pendingAction: undefined, + name: 'Engineering', + accountID: undefined, }, - Restaurant: { + Medical: { enabled: true, - name: 'Restaurant', - unencodedName: 'Restaurant', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', - pendingAction: 'delete', + name: 'Medical', + accountID: undefined, }, - Food: { + Accounting: { enabled: true, - name: 'Food', - unencodedName: 'Food', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', - pendingAction: undefined, + name: 'Accounting', + accountID: undefined, }, - 'Food: Meat': { + HR: { enabled: true, - name: 'Food: Meat', - unencodedName: 'Food: Meat', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', - pendingAction: undefined, + name: 'HR', + accountID: undefined, + pendingAction: 'delete', }, }; - const smallResultList: OptionsListUtils.CategoryTreeSection[] = [ + const smallResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: false, + // data sorted alphabetically by name data: [ { - text: 'Food', - keyForList: 'Food', - searchText: 'Food', - tooltipText: 'Food', + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', isDisabled: false, isSelected: false, pendingAction: undefined, }, { - text: ' Meat', - keyForList: 'Food: Meat', - searchText: 'Food: Meat', - tooltipText: 'Meat', - isDisabled: false, + text: 'HR', + keyForList: 'HR', + searchText: 'HR', + tooltipText: 'HR', + isDisabled: true, isSelected: false, - pendingAction: undefined, + pendingAction: 'delete', }, { - text: 'Restaurant', - keyForList: 'Restaurant', - searchText: 'Restaurant', - tooltipText: 'Restaurant', - isDisabled: true, + text: 'Medical', + keyForList: 'Medical', + searchText: 'Medical', + tooltipText: 'Medical', + isDisabled: false, isSelected: false, - pendingAction: 'delete', + pendingAction: undefined, }, ], - indexOffset: 3, }, ]; - const smallSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ + const smallSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, - indexOffset: 2, data: [ { - text: 'Food', - keyForList: 'Food', - searchText: 'Food', - tooltipText: 'Food', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food: Meat', - keyForList: 'Food: Meat', - searchText: 'Food: Meat', - tooltipText: 'Food: Meat', + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -755,154 +725,82 @@ describe('OptionsListUtils', () => { ], }, ]; - const smallWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ + const smallWrongSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; - const largeCategoriesList: PolicyCategories = { - Taxi: { + const largeTagsList: Record = { + Engineering: { enabled: false, - name: 'Taxi', - unencodedName: 'Taxi', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'Engineering', + accountID: undefined, }, - Restaurant: { + Medical: { enabled: true, - name: 'Restaurant', - unencodedName: 'Restaurant', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'Medical', + accountID: undefined, }, - Food: { + Accounting: { enabled: true, - name: 'Food', - unencodedName: 'Food', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'Accounting', + accountID: undefined, }, - 'Food: Meat': { + HR: { enabled: true, - name: 'Food: Meat', - unencodedName: 'Food: Meat', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'HR', + accountID: undefined, }, - 'Food: Milk': { + Food: { enabled: true, - name: 'Food: Milk', - unencodedName: 'Food: Milk', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'Food', + accountID: undefined, }, - 'Food: Vegetables': { + Traveling: { enabled: false, - name: 'Food: Vegetables', - unencodedName: 'Food: Vegetables', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'Traveling', + accountID: undefined, }, - 'Cars: Audi': { + Cleaning: { enabled: true, - name: 'Cars: Audi', - unencodedName: 'Cars: Audi', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', - }, - 'Cars: BMW': { - enabled: false, - name: 'Cars: BMW', - unencodedName: 'Cars: BMW', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'Cleaning', + accountID: undefined, }, - 'Cars: Mercedes-Benz': { + Software: { enabled: true, - name: 'Cars: Mercedes-Benz', - unencodedName: 'Cars: Mercedes-Benz', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'Software', + accountID: undefined, }, - Medical: { + OfficeSupplies: { enabled: false, - name: 'Medical', - unencodedName: 'Medical', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', - }, - 'Travel: Meals': { - enabled: true, - name: 'Travel: Meals', - unencodedName: 'Travel: Meals', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'Office Supplies', + accountID: undefined, }, - 'Travel: Meals: Breakfast': { + Taxes: { enabled: true, - name: 'Travel: Meals: Breakfast', - unencodedName: 'Travel: Meals: Breakfast', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', - }, - 'Travel: Meals: Dinner': { - enabled: false, - name: 'Travel: Meals: Dinner', - unencodedName: 'Travel: Meals: Dinner', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'Taxes', + accountID: undefined, + pendingAction: 'delete', }, - 'Travel: Meals: Lunch': { + Benefits: { enabled: true, - name: 'Travel: Meals: Lunch', - unencodedName: 'Travel: Meals: Lunch', - areCommentsRequired: false, - 'GL Code': '', - externalID: '', - origin: '', + name: 'Benefits', + accountID: undefined, }, }; - const largeResultList: OptionsListUtils.CategoryTreeSection[] = [ + const largeResultList: OptionsListUtils.CategorySection[] = [ { title: '', - shouldShow: false, - indexOffset: 1, + shouldShow: true, data: [ { text: 'Medical', keyForList: 'Medical', searchText: 'Medical', tooltipText: 'Medical', - isDisabled: true, + isDisabled: false, isSelected: true, pendingAction: undefined, }, @@ -911,13 +809,12 @@ describe('OptionsListUtils', () => { { title: 'Recent', shouldShow: true, - indexOffset: 1, data: [ { - text: 'Restaurant', - keyForList: 'Restaurant', - searchText: 'Restaurant', - tooltipText: 'Restaurant', + text: 'HR', + keyForList: 'HR', + searchText: 'HR', + tooltipText: 'HR', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -927,31 +824,31 @@ describe('OptionsListUtils', () => { { title: 'All', shouldShow: true, - indexOffset: 11, + // data sorted alphabetically by name data: [ { - text: 'Cars', - keyForList: 'Cars', - searchText: 'Cars', - tooltipText: 'Cars', - isDisabled: true, + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', + isDisabled: false, isSelected: false, pendingAction: undefined, }, { - text: ' Audi', - keyForList: 'Cars: Audi', - searchText: 'Cars: Audi', - tooltipText: 'Audi', + text: 'Benefits', + keyForList: 'Benefits', + searchText: 'Benefits', + tooltipText: 'Benefits', isDisabled: false, isSelected: false, pendingAction: undefined, }, { - text: ' Mercedes-Benz', - keyForList: 'Cars: Mercedes-Benz', - searchText: 'Cars: Mercedes-Benz', - tooltipText: 'Mercedes-Benz', + text: 'Cleaning', + keyForList: 'Cleaning', + searchText: 'Cleaning', + tooltipText: 'Cleaning', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -966,100 +863,54 @@ describe('OptionsListUtils', () => { pendingAction: undefined, }, { - text: ' Meat', - keyForList: 'Food: Meat', - searchText: 'Food: Meat', - tooltipText: 'Meat', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' Milk', - keyForList: 'Food: Milk', - searchText: 'Food: Milk', - tooltipText: 'Milk', + text: 'HR', + keyForList: 'HR', + searchText: 'HR', + tooltipText: 'HR', isDisabled: false, isSelected: false, pendingAction: undefined, }, { - text: 'Restaurant', - keyForList: 'Restaurant', - searchText: 'Restaurant', - tooltipText: 'Restaurant', + text: 'Software', + keyForList: 'Software', + searchText: 'Software', + tooltipText: 'Software', isDisabled: false, isSelected: false, pendingAction: undefined, }, { - text: 'Travel', - keyForList: 'Travel', - searchText: 'Travel', - tooltipText: 'Travel', + text: 'Taxes', + keyForList: 'Taxes', + searchText: 'Taxes', + tooltipText: 'Taxes', isDisabled: true, isSelected: false, - pendingAction: undefined, - }, - { - text: ' Meals', - keyForList: 'Travel: Meals', - searchText: 'Travel: Meals', - tooltipText: 'Meals', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' Breakfast', - keyForList: 'Travel: Meals: Breakfast', - searchText: 'Travel: Meals: Breakfast', - tooltipText: 'Breakfast', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' Lunch', - keyForList: 'Travel: Meals: Lunch', - searchText: 'Travel: Meals: Lunch', - tooltipText: 'Lunch', - isDisabled: false, - isSelected: false, - pendingAction: undefined, + pendingAction: 'delete', }, ], }, ]; - const largeSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ + const largeSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, - indexOffset: 3, data: [ { - text: 'Food', - keyForList: 'Food', - searchText: 'Food', - tooltipText: 'Food', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food: Meat', - keyForList: 'Food: Meat', - searchText: 'Food: Meat', - tooltipText: 'Food: Meat', + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', isDisabled: false, isSelected: false, pendingAction: undefined, }, { - text: 'Food: Milk', - keyForList: 'Food: Milk', - searchText: 'Food: Milk', - tooltipText: 'Food: Milk', + text: 'Cleaning', + keyForList: 'Cleaning', + searchText: 'Cleaning', + tooltipText: 'Cleaning', isDisabled: false, isSelected: false, pendingAction: undefined, @@ -1067,1137 +918,38 @@ describe('OptionsListUtils', () => { ], }, ]; - const largeWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ + const largeWrongSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, - indexOffset: 0, data: [], }, ]; - const emptyCategoriesList = {}; - const emptySelectedResultList: OptionsListUtils.CategoryTreeSection[] = [ - { - title: '', - shouldShow: false, - indexOffset: 1, - data: [ - { - text: 'Medical', - keyForList: 'Medical', - searchText: 'Medical', - tooltipText: 'Medical', - isDisabled: true, - isSelected: true, - pendingAction: undefined, - }, - ], - }, - ]; - const smallResult = OptionsListUtils.getFilteredOptions({ - reports: OPTIONS.reports, - personalDetails: OPTIONS.personalDetails, - searchValue: emptySearch, - includeP2P: false, - includeCategories: true, - categories: smallCategoriesList, - }); - expect(smallResult.categoryOptions).toStrictEqual(smallResultList); - - const smallSearchResult = OptionsListUtils.getFilteredOptions({searchValue: search, includeP2P: false, includeCategories: true, categories: smallCategoriesList}); - expect(smallSearchResult.categoryOptions).toStrictEqual(smallSearchResultList); + const smallResult = OptionsListUtils.getFilteredOptions({searchValue: emptySearch, includeP2P: false, includeTags: true, tags: smallTagsList}); + expect(smallResult.tagOptions).toStrictEqual(smallResultList); - const smallWrongSearchResult = OptionsListUtils.getFilteredOptions({searchValue: wrongSearch, includeP2P: false, includeCategories: true, categories: smallCategoriesList}); - expect(smallWrongSearchResult.categoryOptions).toStrictEqual(smallWrongSearchResultList); + const smallSearchResult = OptionsListUtils.getFilteredOptions({searchValue: search, includeP2P: false, includeTags: true, tags: smallTagsList}); + expect(smallSearchResult.tagOptions).toStrictEqual(smallSearchResultList); - const largeResult = OptionsListUtils.getFilteredOptions({ - searchValue: emptySearch, - selectedOptions, - includeP2P: false, - includeCategories: true, - categories: largeCategoriesList, - recentlyUsedCategories, - }); - expect(largeResult.categoryOptions).toStrictEqual(largeResultList); + const smallWrongSearchResult = OptionsListUtils.getFilteredOptions({searchValue: wrongSearch, includeP2P: false, includeTags: true, tags: smallTagsList}); + expect(smallWrongSearchResult.tagOptions).toStrictEqual(smallWrongSearchResultList); - const largeSearchResult = OptionsListUtils.getFilteredOptions({ - searchValue: search, - selectedOptions, + const largeResult = OptionsListUtils.getFilteredOptions({searchValue: emptySearch, selectedOptions, includeP2P: false, includeTags: true, tags: largeTagsList, recentlyUsedTags}); + expect(largeResult.tagOptions).toStrictEqual(largeResultList); - includeP2P: false, - includeCategories: true, - categories: largeCategoriesList, - recentlyUsedCategories, - }); - expect(largeSearchResult.categoryOptions).toStrictEqual(largeSearchResultList); + const largeSearchResult = OptionsListUtils.getFilteredOptions({searchValue: search, selectedOptions, includeP2P: false, includeTags: true, tags: largeTagsList, recentlyUsedTags}); + expect(largeSearchResult.tagOptions).toStrictEqual(largeSearchResultList); const largeWrongSearchResult = OptionsListUtils.getFilteredOptions({ searchValue: wrongSearch, selectedOptions, includeP2P: false, - includeCategories: true, - categories: largeCategoriesList, - recentlyUsedCategories, + includeTags: true, + tags: largeTagsList, + recentlyUsedTags, }); - expect(largeWrongSearchResult.categoryOptions).toStrictEqual(largeWrongSearchResultList); - - const emptyResult = OptionsListUtils.getFilteredOptions({searchValue: search, selectedOptions, includeP2P: false, includeCategories: true, categories: emptyCategoriesList}); - expect(emptyResult.categoryOptions).toStrictEqual(emptySelectedResultList); - }); - - it('getFilteredOptions() for tags', () => { - const search = 'ing'; - const emptySearch = ''; - const wrongSearch = 'bla bla'; - const recentlyUsedTags = ['Engineering', 'HR']; - - const selectedOptions = [ - { - name: 'Medical', - }, - ]; - const smallTagsList: Record = { - Engineering: { - enabled: false, - name: 'Engineering', - accountID: undefined, - }, - Medical: { - enabled: true, - name: 'Medical', - accountID: undefined, - }, - Accounting: { - enabled: true, - name: 'Accounting', - accountID: undefined, - }, - HR: { - enabled: true, - name: 'HR', - accountID: undefined, - pendingAction: 'delete', - }, - }; - const smallResultList: OptionsListUtils.CategorySection[] = [ - { - title: '', - shouldShow: false, - // data sorted alphabetically by name - data: [ - { - text: 'Accounting', - keyForList: 'Accounting', - searchText: 'Accounting', - tooltipText: 'Accounting', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'HR', - keyForList: 'HR', - searchText: 'HR', - tooltipText: 'HR', - isDisabled: true, - isSelected: false, - pendingAction: 'delete', - }, - { - text: 'Medical', - keyForList: 'Medical', - searchText: 'Medical', - tooltipText: 'Medical', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ], - }, - ]; - const smallSearchResultList: OptionsListUtils.CategorySection[] = [ - { - title: '', - shouldShow: true, - data: [ - { - text: 'Accounting', - keyForList: 'Accounting', - searchText: 'Accounting', - tooltipText: 'Accounting', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ], - }, - ]; - const smallWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ - { - title: '', - shouldShow: true, - data: [], - }, - ]; - const largeTagsList: Record = { - Engineering: { - enabled: false, - name: 'Engineering', - accountID: undefined, - }, - Medical: { - enabled: true, - name: 'Medical', - accountID: undefined, - }, - Accounting: { - enabled: true, - name: 'Accounting', - accountID: undefined, - }, - HR: { - enabled: true, - name: 'HR', - accountID: undefined, - }, - Food: { - enabled: true, - name: 'Food', - accountID: undefined, - }, - Traveling: { - enabled: false, - name: 'Traveling', - accountID: undefined, - }, - Cleaning: { - enabled: true, - name: 'Cleaning', - accountID: undefined, - }, - Software: { - enabled: true, - name: 'Software', - accountID: undefined, - }, - OfficeSupplies: { - enabled: false, - name: 'Office Supplies', - accountID: undefined, - }, - Taxes: { - enabled: true, - name: 'Taxes', - accountID: undefined, - pendingAction: 'delete', - }, - Benefits: { - enabled: true, - name: 'Benefits', - accountID: undefined, - }, - }; - const largeResultList: OptionsListUtils.CategorySection[] = [ - { - title: '', - shouldShow: true, - data: [ - { - text: 'Medical', - keyForList: 'Medical', - searchText: 'Medical', - tooltipText: 'Medical', - isDisabled: false, - isSelected: true, - pendingAction: undefined, - }, - ], - }, - { - title: 'Recent', - shouldShow: true, - data: [ - { - text: 'HR', - keyForList: 'HR', - searchText: 'HR', - tooltipText: 'HR', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ], - }, - { - title: 'All', - shouldShow: true, - // data sorted alphabetically by name - data: [ - { - text: 'Accounting', - keyForList: 'Accounting', - searchText: 'Accounting', - tooltipText: 'Accounting', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Benefits', - keyForList: 'Benefits', - searchText: 'Benefits', - tooltipText: 'Benefits', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Cleaning', - keyForList: 'Cleaning', - searchText: 'Cleaning', - tooltipText: 'Cleaning', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food', - keyForList: 'Food', - searchText: 'Food', - tooltipText: 'Food', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'HR', - keyForList: 'HR', - searchText: 'HR', - tooltipText: 'HR', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Software', - keyForList: 'Software', - searchText: 'Software', - tooltipText: 'Software', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Taxes', - keyForList: 'Taxes', - searchText: 'Taxes', - tooltipText: 'Taxes', - isDisabled: true, - isSelected: false, - pendingAction: 'delete', - }, - ], - }, - ]; - const largeSearchResultList: OptionsListUtils.CategorySection[] = [ - { - title: '', - shouldShow: true, - data: [ - { - text: 'Accounting', - keyForList: 'Accounting', - searchText: 'Accounting', - tooltipText: 'Accounting', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Cleaning', - keyForList: 'Cleaning', - searchText: 'Cleaning', - tooltipText: 'Cleaning', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ], - }, - ]; - const largeWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ - { - title: '', - shouldShow: true, - data: [], - }, - ]; - - const smallResult = OptionsListUtils.getFilteredOptions({searchValue: emptySearch, includeP2P: false, includeTags: true, tags: smallTagsList}); - expect(smallResult.tagOptions).toStrictEqual(smallResultList); - - const smallSearchResult = OptionsListUtils.getFilteredOptions({searchValue: search, includeP2P: false, includeTags: true, tags: smallTagsList}); - expect(smallSearchResult.tagOptions).toStrictEqual(smallSearchResultList); - - const smallWrongSearchResult = OptionsListUtils.getFilteredOptions({searchValue: wrongSearch, includeP2P: false, includeTags: true, tags: smallTagsList}); - expect(smallWrongSearchResult.tagOptions).toStrictEqual(smallWrongSearchResultList); - - const largeResult = OptionsListUtils.getFilteredOptions({searchValue: emptySearch, selectedOptions, includeP2P: false, includeTags: true, tags: largeTagsList, recentlyUsedTags}); - expect(largeResult.tagOptions).toStrictEqual(largeResultList); - - const largeSearchResult = OptionsListUtils.getFilteredOptions({searchValue: search, selectedOptions, includeP2P: false, includeTags: true, tags: largeTagsList, recentlyUsedTags}); - expect(largeSearchResult.tagOptions).toStrictEqual(largeSearchResultList); - - const largeWrongSearchResult = OptionsListUtils.getFilteredOptions({ - searchValue: wrongSearch, - selectedOptions, - includeP2P: false, - includeTags: true, - tags: largeTagsList, - recentlyUsedTags, - }); - expect(largeWrongSearchResult.tagOptions).toStrictEqual(largeWrongSearchResultList); - }); - - it('getCategoryOptionTree()', () => { - const categories = { - Meals: { - enabled: true, - name: 'Meals', - }, - Restaurant: { - enabled: true, - name: 'Restaurant', - }, - Food: { - enabled: true, - name: 'Food', - }, - 'Food: Meat': { - enabled: true, - name: 'Food: Meat', - }, - 'Food: Milk': { - enabled: true, - name: 'Food: Milk', - }, - 'Cars: Audi': { - enabled: true, - name: 'Cars: Audi', - }, - 'Cars: Mercedes-Benz': { - enabled: true, - name: 'Cars: Mercedes-Benz', - }, - 'Travel: Meals': { - enabled: true, - name: 'Travel: Meals', - }, - 'Travel: Meals: Breakfast': { - enabled: true, - name: 'Travel: Meals: Breakfast', - }, - 'Travel: Meals: Lunch': { - enabled: true, - name: 'Travel: Meals: Lunch', - }, - Plain: { - enabled: true, - name: 'Plain', - }, - Audi: { - enabled: true, - name: 'Audi', - }, - Health: { - enabled: true, - name: 'Health', - }, - 'A: B: C': { - enabled: true, - name: 'A: B: C', - }, - 'A: B: C: D: E': { - enabled: true, - name: 'A: B: C: D: E', - }, - }; - const result = [ - { - text: 'Meals', - keyForList: 'Meals', - searchText: 'Meals', - tooltipText: 'Meals', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Restaurant', - keyForList: 'Restaurant', - searchText: 'Restaurant', - tooltipText: 'Restaurant', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food', - keyForList: 'Food', - searchText: 'Food', - tooltipText: 'Food', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' Meat', - keyForList: 'Food: Meat', - searchText: 'Food: Meat', - tooltipText: 'Meat', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' Milk', - keyForList: 'Food: Milk', - searchText: 'Food: Milk', - tooltipText: 'Milk', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Cars', - keyForList: 'Cars', - searchText: 'Cars', - tooltipText: 'Cars', - isDisabled: true, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' Audi', - keyForList: 'Cars: Audi', - searchText: 'Cars: Audi', - tooltipText: 'Audi', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' Mercedes-Benz', - keyForList: 'Cars: Mercedes-Benz', - searchText: 'Cars: Mercedes-Benz', - tooltipText: 'Mercedes-Benz', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Travel', - keyForList: 'Travel', - searchText: 'Travel', - tooltipText: 'Travel', - isDisabled: true, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' Meals', - keyForList: 'Travel: Meals', - searchText: 'Travel: Meals', - tooltipText: 'Meals', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' Breakfast', - keyForList: 'Travel: Meals: Breakfast', - searchText: 'Travel: Meals: Breakfast', - tooltipText: 'Breakfast', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' Lunch', - keyForList: 'Travel: Meals: Lunch', - searchText: 'Travel: Meals: Lunch', - tooltipText: 'Lunch', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Plain', - keyForList: 'Plain', - searchText: 'Plain', - tooltipText: 'Plain', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Audi', - keyForList: 'Audi', - searchText: 'Audi', - tooltipText: 'Audi', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Health', - keyForList: 'Health', - searchText: 'Health', - tooltipText: 'Health', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'A', - keyForList: 'A', - searchText: 'A', - tooltipText: 'A', - isDisabled: true, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' B', - keyForList: 'A: B', - searchText: 'A: B', - tooltipText: 'B', - isDisabled: true, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' C', - keyForList: 'A: B: C', - searchText: 'A: B: C', - tooltipText: 'C', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' D', - keyForList: 'A: B: C: D', - searchText: 'A: B: C: D', - tooltipText: 'D', - isDisabled: true, - isSelected: false, - pendingAction: undefined, - }, - { - text: ' E', - keyForList: 'A: B: C: D: E', - searchText: 'A: B: C: D: E', - tooltipText: 'E', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ]; - const resultOneLine = [ - { - text: 'Meals', - keyForList: 'Meals', - searchText: 'Meals', - tooltipText: 'Meals', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Restaurant', - keyForList: 'Restaurant', - searchText: 'Restaurant', - tooltipText: 'Restaurant', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food', - keyForList: 'Food', - searchText: 'Food', - tooltipText: 'Food', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food: Meat', - keyForList: 'Food: Meat', - searchText: 'Food: Meat', - tooltipText: 'Food: Meat', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Food: Milk', - keyForList: 'Food: Milk', - searchText: 'Food: Milk', - tooltipText: 'Food: Milk', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Cars: Audi', - keyForList: 'Cars: Audi', - searchText: 'Cars: Audi', - tooltipText: 'Cars: Audi', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Cars: Mercedes-Benz', - keyForList: 'Cars: Mercedes-Benz', - searchText: 'Cars: Mercedes-Benz', - tooltipText: 'Cars: Mercedes-Benz', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Travel: Meals', - keyForList: 'Travel: Meals', - searchText: 'Travel: Meals', - tooltipText: 'Travel: Meals', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Travel: Meals: Breakfast', - keyForList: 'Travel: Meals: Breakfast', - searchText: 'Travel: Meals: Breakfast', - tooltipText: 'Travel: Meals: Breakfast', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Travel: Meals: Lunch', - keyForList: 'Travel: Meals: Lunch', - searchText: 'Travel: Meals: Lunch', - tooltipText: 'Travel: Meals: Lunch', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Plain', - keyForList: 'Plain', - searchText: 'Plain', - tooltipText: 'Plain', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Audi', - keyForList: 'Audi', - searchText: 'Audi', - tooltipText: 'Audi', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'Health', - keyForList: 'Health', - searchText: 'Health', - tooltipText: 'Health', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'A: B: C', - keyForList: 'A: B: C', - searchText: 'A: B: C', - tooltipText: 'A: B: C', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - { - text: 'A: B: C: D: E', - keyForList: 'A: B: C: D: E', - searchText: 'A: B: C: D: E', - tooltipText: 'A: B: C: D: E', - isDisabled: false, - isSelected: false, - pendingAction: undefined, - }, - ]; - - expect(OptionsListUtils.getCategoryOptionTree(categories)).toStrictEqual(result); - expect(OptionsListUtils.getCategoryOptionTree(categories, true)).toStrictEqual(resultOneLine); - }); - - it('sortCategories', () => { - const categoriesIncorrectOrdering = { - Taxi: { - name: 'Taxi', - enabled: false, - }, - 'Test1: Subtest2': { - name: 'Test1: Subtest2', - enabled: true, - }, - 'Test: Test1: Subtest4': { - name: 'Test: Test1: Subtest4', - enabled: true, - }, - Taxes: { - name: 'Taxes', - enabled: true, - }, - Test: { - name: 'Test', - enabled: true, - pendingAction: 'delete' as PendingAction, - }, - Test1: { - name: 'Test1', - enabled: true, - }, - 'Travel: Nested-Travel': { - name: 'Travel: Nested-Travel', - enabled: true, - }, - 'Test1: Subtest1': { - name: 'Test1: Subtest1', - enabled: true, - }, - 'Test: Test1': { - name: 'Test: Test1', - enabled: true, - }, - 'Test: Test1: Subtest1': { - name: 'Test: Test1: Subtest1', - enabled: true, - }, - 'Test: Test1: Subtest3': { - name: 'Test: Test1: Subtest3', - enabled: false, - }, - 'Test: Test1: Subtest2': { - name: 'Test: Test1: Subtest2', - enabled: true, - }, - 'Test: Test2': { - name: 'Test: Test2', - enabled: true, - }, - Travel: { - name: 'Travel', - enabled: true, - }, - Utilities: { - name: 'Utilities', - enabled: true, - }, - 'Test: Test3: Subtest1': { - name: 'Test: Test3: Subtest1', - enabled: true, - }, - 'Test1: Subtest3': { - name: 'Test1: Subtest3', - enabled: true, - }, - }; - const result = [ - { - name: 'Taxes', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Taxi', - enabled: false, - pendingAction: undefined, - }, - { - name: 'Test', - enabled: true, - pendingAction: 'delete', - }, - { - name: 'Test: Test1', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Test: Test1: Subtest1', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Test: Test1: Subtest2', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Test: Test1: Subtest3', - enabled: false, - pendingAction: undefined, - }, - { - name: 'Test: Test1: Subtest4', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Test: Test2', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Test: Test3: Subtest1', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Test1', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Test1: Subtest1', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Test1: Subtest2', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Test1: Subtest3', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Travel', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Travel: Nested-Travel', - enabled: true, - pendingAction: undefined, - }, - { - name: 'Utilities', - enabled: true, - pendingAction: undefined, - }, - ]; - const categoriesIncorrectOrdering2 = { - 'Cars: BMW': { - enabled: false, - name: 'Cars: BMW', - }, - Medical: { - enabled: false, - name: 'Medical', - }, - 'Travel: Meals: Lunch': { - enabled: true, - name: 'Travel: Meals: Lunch', - }, - 'Cars: Mercedes-Benz': { - enabled: true, - name: 'Cars: Mercedes-Benz', - }, - Food: { - enabled: true, - name: 'Food', - }, - 'Food: Meat': { - enabled: true, - name: 'Food: Meat', - }, - 'Travel: Meals: Dinner': { - enabled: false, - name: 'Travel: Meals: Dinner', - }, - 'Food: Vegetables': { - enabled: false, - name: 'Food: Vegetables', - }, - Restaurant: { - enabled: true, - name: 'Restaurant', - }, - Taxi: { - enabled: false, - name: 'Taxi', - }, - 'Food: Milk': { - enabled: true, - name: 'Food: Milk', - }, - 'Travel: Meals': { - enabled: true, - name: 'Travel: Meals', - }, - 'Travel: Meals: Breakfast': { - enabled: true, - name: 'Travel: Meals: Breakfast', - }, - 'Cars: Audi': { - enabled: true, - name: 'Cars: Audi', - }, - }; - const result2 = [ - { - enabled: true, - name: 'Cars: Audi', - pendingAction: undefined, - }, - { - enabled: false, - name: 'Cars: BMW', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Cars: Mercedes-Benz', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Food', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Food: Meat', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Food: Milk', - pendingAction: undefined, - }, - { - enabled: false, - name: 'Food: Vegetables', - pendingAction: undefined, - }, - { - enabled: false, - name: 'Medical', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Restaurant', - pendingAction: undefined, - }, - { - enabled: false, - name: 'Taxi', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Travel: Meals', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Travel: Meals: Breakfast', - pendingAction: undefined, - }, - { - enabled: false, - name: 'Travel: Meals: Dinner', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Travel: Meals: Lunch', - pendingAction: undefined, - }, - ]; - const categoriesIncorrectOrdering3 = { - 'Movies: Mr. Nobody': { - enabled: true, - name: 'Movies: Mr. Nobody', - }, - Movies: { - enabled: true, - name: 'Movies', - }, - 'House, M.D.': { - enabled: true, - name: 'House, M.D.', - }, - 'Dr. House': { - enabled: true, - name: 'Dr. House', - }, - 'Many.dots.on.the.way.': { - enabled: true, - name: 'Many.dots.on.the.way.', - }, - 'More.Many.dots.on.the.way.': { - enabled: false, - name: 'More.Many.dots.on.the.way.', - }, - }; - const result3 = [ - { - enabled: true, - name: 'Dr. House', - pendingAction: undefined, - }, - { - enabled: true, - name: 'House, M.D.', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Many.dots.on.the.way.', - pendingAction: undefined, - }, - { - enabled: false, - name: 'More.Many.dots.on.the.way.', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Movies', - pendingAction: undefined, - }, - { - enabled: true, - name: 'Movies: Mr. Nobody', - pendingAction: undefined, - }, - ]; - - expect(OptionsListUtils.sortCategories(categoriesIncorrectOrdering)).toStrictEqual(result); - expect(OptionsListUtils.sortCategories(categoriesIncorrectOrdering2)).toStrictEqual(result2); - expect(OptionsListUtils.sortCategories(categoriesIncorrectOrdering3)).toStrictEqual(result3); + expect(largeWrongSearchResult.tagOptions).toStrictEqual(largeWrongSearchResultList); }); it('sortTags', () => { From 8f626c451f339d28cd8d50a0a9a7bd749dee015d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 8 Nov 2024 12:31:47 +0100 Subject: [PATCH 018/104] accept lodash/get for now --- src/libs/CategoryOptionListUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/CategoryOptionListUtils.ts b/src/libs/CategoryOptionListUtils.ts index 5ea71bca4136..d370da441110 100644 --- a/src/libs/CategoryOptionListUtils.ts +++ b/src/libs/CategoryOptionListUtils.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line you-dont-need-lodash-underscore/get import lodashGet from 'lodash/get'; import lodashSet from 'lodash/set'; import CONST from '@src/CONST'; From f275b476ef8c4aec8c31345cdb53fc9d5371acb6 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 8 Nov 2024 08:25:59 -0800 Subject: [PATCH 019/104] WIP retry authentication with throttle --- src/ONYXKEYS.ts | 3 + src/libs/Authentication.ts | 79 +++++++++++-------------- src/libs/Middleware/Reauthentication.ts | 33 ++++++++++- src/libs/Network/SequentialQueue.ts | 12 ++-- src/libs/Network/index.ts | 14 +++++ src/libs/RequestThrottle.ts | 65 ++++++++++---------- 6 files changed, 124 insertions(+), 82 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 49dd42fa8281..4773cdab8c6b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -36,6 +36,9 @@ const ONYXKEYS = { PERSISTED_REQUESTS: 'networkRequestQueue', PERSISTED_ONGOING_REQUESTS: 'networkOngoingRequestQueue', + /** The re-authentication request to be retried as needed */ + REAUTHENTICATION_REQUEST: 'reauthenticationRequest', + /** Stores current date */ CURRENT_DATE: 'currentDate', diff --git a/src/libs/Authentication.ts b/src/libs/Authentication.ts index 34630af81733..1ab7083b2d8e 100644 --- a/src/libs/Authentication.ts +++ b/src/libs/Authentication.ts @@ -62,55 +62,48 @@ function reauthenticate(command = ''): Promise { partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, partnerUserID: credentials?.autoGeneratedLogin, partnerUserSecret: credentials?.autoGeneratedPassword, - }) - .then((response) => { - if (response.jsonCode === CONST.JSON_CODE.UNABLE_TO_RETRY) { - // If authentication fails, then the network can be unpaused - NetworkStore.setIsAuthenticating(false); + }).then((response) => { + if (response.jsonCode === CONST.JSON_CODE.UNABLE_TO_RETRY) { + // If authentication fails, then the network can be unpaused + NetworkStore.setIsAuthenticating(false); - // When a fetch() fails due to a network issue and an error is thrown we won't log the user out. Most likely they - // have a spotty connection and will need to try to reauthenticate when they come back online. We will error so it - // can be handled by callers of reauthenticate(). - throw new Error('Unable to retry Authenticate request'); - } + // When a fetch() fails due to a network issue and an error is thrown we won't log the user out. Most likely they + // have a spotty connection and will need to try to reauthenticate when they come back online. We will error so it + // can be handled by callers of reauthenticate(). + throw new Error('Unable to retry Authenticate request'); + } - // If authentication fails and we are online then log the user out - if (response.jsonCode !== 200) { - const errorMessage = ErrorUtils.getAuthenticateErrorMessage(response); - NetworkStore.setIsAuthenticating(false); - Log.hmmm('Redirecting to Sign In because we failed to reauthenticate', { - command, - error: errorMessage, - }); - redirectToSignIn(errorMessage); - return; - } + // If authentication fails and we are online then log the user out + if (response.jsonCode !== 200) { + const errorMessage = ErrorUtils.getAuthenticateErrorMessage(response); + NetworkStore.setIsAuthenticating(false); + Log.hmmm('Redirecting to Sign In because we failed to reauthenticate', { + command, + error: errorMessage, + }); + redirectToSignIn(errorMessage); + return; + } - // If we reauthenticated due to an expired delegate token, restore the delegate's original account. - // This is because the credentials used to reauthenticate were for the delegate's original account, and not for the account they were connected as. - if (Delegate.isConnectedAsDelegate()) { - Log.info('Reauthenticated while connected as a delegate. Restoring original account.'); - Delegate.restoreDelegateSession(response); - return; - } + // If we reauthenticated due to an expired delegate token, restore the delegate's original account. + // This is because the credentials used to reauthenticate were for the delegate's original account, and not for the account they were connected as. + if (Delegate.isConnectedAsDelegate()) { + Log.info('Reauthenticated while connected as a delegate. Restoring original account.'); + Delegate.restoreDelegateSession(response); + return; + } - // Update authToken in Onyx and in our local variables so that API requests will use the new authToken - updateSessionAuthTokens(response.authToken, response.encryptedAuthToken); + // Update authToken in Onyx and in our local variables so that API requests will use the new authToken + updateSessionAuthTokens(response.authToken, response.encryptedAuthToken); - // Note: It is important to manually set the authToken that is in the store here since any requests that are hooked into - // reauthenticate .then() will immediate post and use the local authToken. Onyx updates subscribers lately so it is not - // enough to do the updateSessionAuthTokens() call above. - NetworkStore.setAuthToken(response.authToken ?? null); + // Note: It is important to manually set the authToken that is in the store here since any requests that are hooked into + // reauthenticate .then() will immediate post and use the local authToken. Onyx updates subscribers lately so it is not + // enough to do the updateSessionAuthTokens() call above. + NetworkStore.setAuthToken(response.authToken ?? null); - // The authentication process is finished so the network can be unpaused to continue processing requests - NetworkStore.setIsAuthenticating(false); - }) - .catch((error) => { - // In case the authenticate call throws error, we need to sign user out as most likely they are missing credentials - NetworkStore.setIsAuthenticating(false); - Log.hmmm('Redirecting to Sign In because we failed to reauthenticate', {error}); - redirectToSignIn('passwordForm.error.fallback'); - }); + // The authentication process is finished so the network can be unpaused to continue processing requests + NetworkStore.setIsAuthenticating(false); + }); } export {reauthenticate, Authenticate}; diff --git a/src/libs/Middleware/Reauthentication.ts b/src/libs/Middleware/Reauthentication.ts index 09a01e821cb2..a67214e04420 100644 --- a/src/libs/Middleware/Reauthentication.ts +++ b/src/libs/Middleware/Reauthentication.ts @@ -1,33 +1,59 @@ +import redirectToSignIn from '@libs/actions/SignInRedirect'; import * as Authentication from '@libs/Authentication'; import Log from '@libs/Log'; import * as MainQueue from '@libs/Network/MainQueue'; import * as NetworkStore from '@libs/Network/NetworkStore'; +import type {RequestError} from '@libs/Network/SequentialQueue'; import NetworkConnection from '@libs/NetworkConnection'; import * as Request from '@libs/Request'; +import RequestThrottle from '@libs/RequestThrottle'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type Middleware from './types'; // We store a reference to the active authentication request so that we are only ever making one request to authenticate at a time. let isAuthenticating: Promise | null = null; +const reauthThrottle = new RequestThrottle(); + function reauthenticate(commandName?: string): Promise { if (isAuthenticating) { return isAuthenticating; } - isAuthenticating = Authentication.reauthenticate(commandName) + const reauthRequest = { + commandName, + }; + Onyx.set(ONYXKEYS.REAUTHENTICATION_REQUEST, reauthRequest); + + isAuthenticating = retryReauthenticate(commandName) .then((response) => { - isAuthenticating = null; return response; }) .catch((error) => { - isAuthenticating = null; throw error; + }) + .finally(() => { + Onyx.set(CONST.ONYXKEYS.REAUTHENTICATION_REQUEST, null); + isAuthenticating = null; }); return isAuthenticating; } +function retryReauthenticate(commandName?: string): Promise { + return Authentication.reauthenticate(commandName).catch((error: RequestError) => { + return reauthThrottle + .sleep(error, 'Authenticate') + .then(() => retryReauthenticate(commandName)) + .catch(() => { + NetworkStore.setIsAuthenticating(false); + Log.hmmm('Redirecting to Sign In because we failed to reauthenticate after multiple attempts', {error}); + redirectToSignIn('passwordForm.error.fallback'); + }); + }); +} + const Reauthentication: Middleware = (response, request, isFromSequentialQueue) => response .then((data) => { @@ -118,3 +144,4 @@ const Reauthentication: Middleware = (response, request, isFromSequentialQueue) }); export default Reauthentication; +export {reauthenticate}; diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 643ed64ae7f6..ec07d315a608 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -2,7 +2,7 @@ import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import Log from '@libs/Log'; import * as Request from '@libs/Request'; -import * as RequestThrottle from '@libs/RequestThrottle'; +import RequestThrottle from '@libs/RequestThrottle'; import * as PersistedRequests from '@userActions/PersistedRequests'; import * as QueuedOnyxUpdates from '@userActions/QueuedOnyxUpdates'; import CONST from '@src/CONST'; @@ -28,6 +28,7 @@ resolveIsReadyPromise?.(); let isSequentialQueueRunning = false; let currentRequestPromise: Promise | null = null; let isQueuePaused = false; +const requestThrottle = new RequestThrottle(); /** * Puts the queue into a paused state so that no requests will be processed @@ -99,7 +100,7 @@ function process(): Promise { Log.info('[SequentialQueue] Removing persisted request because it was processed successfully.', false, {request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - RequestThrottle.clear(); + requestThrottle.clear(); return process(); }) .catch((error: RequestError) => { @@ -108,17 +109,18 @@ function process(): Promise { if (error.name === CONST.ERROR.REQUEST_CANCELLED || error.message === CONST.ERROR.DUPLICATE_RECORD) { Log.info("[SequentialQueue] Removing persisted request because it failed and doesn't need to be retried.", false, {error, request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - RequestThrottle.clear(); + requestThrottle.clear(); return process(); } PersistedRequests.rollbackOngoingRequest(); - return RequestThrottle.sleep(error, requestToProcess.command) + return requestThrottle + .sleep(error, requestToProcess.command) .then(process) .catch(() => { Onyx.update(requestToProcess.failureData ?? []); Log.info('[SequentialQueue] Removing persisted request because it failed too many times.', false, {error, request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - RequestThrottle.clear(); + requestThrottle.clear(); return process(); }); }); diff --git a/src/libs/Network/index.ts b/src/libs/Network/index.ts index 2adb4a2da4c2..2a600d5d51de 100644 --- a/src/libs/Network/index.ts +++ b/src/libs/Network/index.ts @@ -1,5 +1,8 @@ +import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; +import {reauthenticate} from '@libs/Middleware/Reauthentication'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {Request} from '@src/types/onyx'; import type Response from '@src/types/onyx/Response'; import pkg from '../../../package.json'; @@ -12,6 +15,17 @@ ActiveClientManager.isReady().then(() => { // Start main queue and process once every n ms delay setInterval(MainQueue.process, CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); + + // If a reauthentication request is set make sure it is processed + Onyx.connect({ + key: ONYXKEYS.REAUTHENTICATION_REQUEST, + callback: (request) => { + if (!request) { + return; + } + reauthenticate(request.commandName); + }, + }); }); /** diff --git a/src/libs/RequestThrottle.ts b/src/libs/RequestThrottle.ts index 3bbc82ff5b45..8a6673c22a92 100644 --- a/src/libs/RequestThrottle.ts +++ b/src/libs/RequestThrottle.ts @@ -3,41 +3,44 @@ import Log from './Log'; import type {RequestError} from './Network/SequentialQueue'; import {generateRandomInt} from './NumberUtils'; -let requestWaitTime = 0; -let requestRetryCount = 0; +class RequestThrottle { + private requestWaitTime = 0; -function clear() { - requestWaitTime = 0; - requestRetryCount = 0; - Log.info(`[RequestThrottle] in clear()`); -} + private requestRetryCount = 0; -function getRequestWaitTime() { - if (requestWaitTime) { - requestWaitTime = Math.min(requestWaitTime * 2, CONST.NETWORK.MAX_RETRY_WAIT_TIME_MS); - } else { - requestWaitTime = generateRandomInt(CONST.NETWORK.MIN_RETRY_WAIT_TIME_MS, CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS); + clear() { + this.requestWaitTime = 0; + this.requestRetryCount = 0; + Log.info(`[RequestThrottle] in clear()`); } - return requestWaitTime; -} -function getLastRequestWaitTime() { - return requestWaitTime; -} - -function sleep(error: RequestError, command: string): Promise { - requestRetryCount++; - return new Promise((resolve, reject) => { - if (requestRetryCount <= CONST.NETWORK.MAX_REQUEST_RETRIES) { - const currentRequestWaitTime = getRequestWaitTime(); - Log.info( - `[RequestThrottle] Retrying request after error: '${error.name}', '${error.message}', '${error.status}'. Command: ${command}. Retry count: ${requestRetryCount}. Wait time: ${currentRequestWaitTime}`, - ); - setTimeout(resolve, currentRequestWaitTime); - return; + getRequestWaitTime() { + if (this.requestWaitTime) { + this.requestWaitTime = Math.min(this.requestWaitTime * 2, CONST.NETWORK.MAX_RETRY_WAIT_TIME_MS); + } else { + this.requestWaitTime = generateRandomInt(CONST.NETWORK.MIN_RETRY_WAIT_TIME_MS, CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS); } - reject(); - }); + return this.requestWaitTime; + } + + getLastRequestWaitTime() { + return this.requestWaitTime; + } + + sleep(error: RequestError, command: string): Promise { + this.requestRetryCount++; + return new Promise((resolve, reject) => { + if (this.requestRetryCount <= CONST.NETWORK.MAX_REQUEST_RETRIES) { + const currentRequestWaitTime = this.getRequestWaitTime(); + Log.info( + `[RequestThrottle] Retrying request after error: '${error.name}', '${error.message}', '${error.status}'. Command: ${command}. Retry count: ${this.requestRetryCount}. Wait time: ${currentRequestWaitTime}`, + ); + setTimeout(resolve, currentRequestWaitTime); + } else { + reject(); + } + }); + } } -export {clear, getRequestWaitTime, sleep, getLastRequestWaitTime}; +export default RequestThrottle; From 7ffdbbeed84bdd52c6e3413ea6f3e97a163caa5b Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 8 Nov 2024 08:33:39 -0800 Subject: [PATCH 020/104] Fix types --- src/ONYXKEYS.ts | 1 + src/libs/Authentication.ts | 6 +----- src/libs/Middleware/Reauthentication.ts | 5 ++++- tests/unit/APITest.ts | 8 +++++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 4773cdab8c6b..08feab508556 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -891,6 +891,7 @@ type OnyxValuesMapping = { [ONYXKEYS.IS_SIDEBAR_LOADED]: boolean; [ONYXKEYS.PERSISTED_REQUESTS]: OnyxTypes.Request[]; [ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: OnyxTypes.Request; + [ONYXKEYS.REAUTHENTICATION_REQUEST]: OnyxTypes.Request; [ONYXKEYS.CURRENT_DATE]: string; [ONYXKEYS.CREDENTIALS]: OnyxTypes.Credentials; [ONYXKEYS.STASHED_CREDENTIALS]: OnyxTypes.Credentials; diff --git a/src/libs/Authentication.ts b/src/libs/Authentication.ts index 1ab7083b2d8e..5e7b00472471 100644 --- a/src/libs/Authentication.ts +++ b/src/libs/Authentication.ts @@ -64,12 +64,8 @@ function reauthenticate(command = ''): Promise { partnerUserSecret: credentials?.autoGeneratedPassword, }).then((response) => { if (response.jsonCode === CONST.JSON_CODE.UNABLE_TO_RETRY) { - // If authentication fails, then the network can be unpaused - NetworkStore.setIsAuthenticating(false); - // When a fetch() fails due to a network issue and an error is thrown we won't log the user out. Most likely they - // have a spotty connection and will need to try to reauthenticate when they come back online. We will error so it - // can be handled by callers of reauthenticate(). + // have a spotty connection and will need to retry reauthenticate when they come back online. Error so it can be handled by the retry mechanism. throw new Error('Unable to retry Authenticate request'); } diff --git a/src/libs/Middleware/Reauthentication.ts b/src/libs/Middleware/Reauthentication.ts index a67214e04420..859dfa01697a 100644 --- a/src/libs/Middleware/Reauthentication.ts +++ b/src/libs/Middleware/Reauthentication.ts @@ -1,3 +1,4 @@ +import Onyx from 'react-native-onyx'; import redirectToSignIn from '@libs/actions/SignInRedirect'; import * as Authentication from '@libs/Authentication'; import Log from '@libs/Log'; @@ -24,6 +25,7 @@ function reauthenticate(commandName?: string): Promise { const reauthRequest = { commandName, }; + // eslint-disable-next-line rulesdir/prefer-actions-set-data Onyx.set(ONYXKEYS.REAUTHENTICATION_REQUEST, reauthRequest); isAuthenticating = retryReauthenticate(commandName) @@ -34,7 +36,8 @@ function reauthenticate(commandName?: string): Promise { throw error; }) .finally(() => { - Onyx.set(CONST.ONYXKEYS.REAUTHENTICATION_REQUEST, null); + // eslint-disable-next-line rulesdir/prefer-actions-set-data + Onyx.set(ONYXKEYS.REAUTHENTICATION_REQUEST, null); isAuthenticating = null; }); diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index 14c4cadcb26d..bc4b650fb6e5 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -9,7 +9,7 @@ import * as MainQueue from '@src/libs/Network/MainQueue'; import * as NetworkStore from '@src/libs/Network/NetworkStore'; import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; import * as Request from '@src/libs/Request'; -import * as RequestThrottle from '@src/libs/RequestThrottle'; +import RequestThrottle from '@src/libs/RequestThrottle'; import ONYXKEYS from '@src/ONYXKEYS'; import type ReactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; @@ -39,6 +39,7 @@ type XhrCalls = Array<{ }>; const originalXHR = HttpUtils.xhr; +const requestThrottle = new RequestThrottle(); beforeEach(() => { global.fetch = TestHelper.getGlobalFetchMock(); @@ -47,6 +48,7 @@ beforeEach(() => { MainQueue.clear(); HttpUtils.cancelPendingRequests(); PersistedRequests.clear(); + requestThrottle.clear(); NetworkStore.checkRequiredData(); // Wait for any Log command to finish and Onyx to fully clear @@ -242,7 +244,7 @@ describe('APITests', () => { // We let the SequentialQueue process again after its wait time return new Promise((resolve) => { - setTimeout(resolve, RequestThrottle.getLastRequestWaitTime()); + setTimeout(resolve, requestThrottle.getLastRequestWaitTime()); }); }) .then(() => { @@ -255,7 +257,7 @@ describe('APITests', () => { // We let the SequentialQueue process again after its wait time return new Promise((resolve) => { - setTimeout(resolve, RequestThrottle.getLastRequestWaitTime()); + setTimeout(resolve, requestThrottle.getLastRequestWaitTime()); }).then(waitForBatchedUpdates); }) .then(() => { From 2a031a04d73bbb8b9e0b421358a7a76a5fbf2720 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 8 Nov 2024 22:04:11 +0530 Subject: [PATCH 021/104] Feature: Per Diem Rates Settings Page --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../CategorySelectorModal.tsx | 0 .../CategorySelector/index.tsx | 0 src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/libs/API/types.ts | 2 + .../ModalStackNavigators/index.tsx | 1 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + src/libs/actions/Policy/Category.ts | 55 +++++++++ src/libs/actions/Policy/PerDiem.ts | 13 ++- .../PolicyDistanceRatesSettingsPage.tsx | 4 +- .../perDiem/WorkspacePerDiemPage.tsx | 5 +- .../perDiem/WorkspacePerDiemSettingsPage.tsx | 104 ++++++++++++++++++ 16 files changed, 192 insertions(+), 8 deletions(-) rename src/{pages/workspace/distanceRates => components}/CategorySelector/CategorySelectorModal.tsx (100%) rename src/{pages/workspace/distanceRates => components}/CategorySelector/index.tsx (100%) create mode 100644 src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cd94035e0fff..da1f489f6164 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1277,6 +1277,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/per-diem', getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem` as const, }, + WORKSPACE_PER_DIEM_SETTINGS: { + route: 'settings/workspaces/:policyID/per-diem/settings', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem/settings` as const, + }, RULES_CUSTOM_NAME: { route: 'settings/workspaces/:policyID/rules/name', getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/name` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9b8fe54111cf..061903e0a876 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -543,6 +543,7 @@ const SCREENS = { RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age', RULES_BILLABLE_DEFAULT: 'Rules_Billable_Default', PER_DIEM: 'Per_Diem', + PER_DIEM_SETTINGS: 'Per_Diem_Settings', }, EDIT_REQUEST: { diff --git a/src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx b/src/components/CategorySelector/CategorySelectorModal.tsx similarity index 100% rename from src/pages/workspace/distanceRates/CategorySelector/CategorySelectorModal.tsx rename to src/components/CategorySelector/CategorySelectorModal.tsx diff --git a/src/pages/workspace/distanceRates/CategorySelector/index.tsx b/src/components/CategorySelector/index.tsx similarity index 100% rename from src/pages/workspace/distanceRates/CategorySelector/index.tsx rename to src/components/CategorySelector/index.tsx diff --git a/src/languages/en.ts b/src/languages/en.ts index 221b718a7699..35a81fa89cc8 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2430,6 +2430,7 @@ const translations = { return 'Member'; } }, + defaultCategory: 'Default category', }, perDiem: { subtitle: 'Set per diem rates to control daily employee spend. ', @@ -3973,7 +3974,6 @@ const translations = { unit: 'Unit', taxFeatureNotEnabledMessage: 'Taxes must be enabled on the workspace to use this feature. Head over to ', changePromptMessage: ' to make that change.', - defaultCategory: 'Default category', deleteDistanceRate: 'Delete distance rate', areYouSureDelete: () => ({ one: 'Are you sure you want to delete this rate?', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1db2cd23011e..343634b2815d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2453,6 +2453,7 @@ const translations = { return 'Miembro'; } }, + defaultCategory: 'Categoría predeterminada', }, perDiem: { subtitle: 'Establece las tasas per diem para controlar los gastos diarios de los empleados. ', @@ -4017,7 +4018,6 @@ const translations = { unit: 'Unidad', taxFeatureNotEnabledMessage: 'Los impuestos deben estar activados en el área de trabajo para poder utilizar esta función. Dirígete a ', changePromptMessage: ' para hacer ese cambio.', - defaultCategory: 'Categoría predeterminada', deleteDistanceRate: 'Eliminar tasa de distancia', areYouSureDelete: () => ({ one: '¿Estás seguro de que quieres eliminar esta tasa?', diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index b8b4bb749701..a8847545fbb3 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -288,6 +288,7 @@ const WRITE_COMMANDS = { ADD_BILLING_CARD_AND_REQUEST_WORKSPACE_OWNER_CHANGE: 'AddBillingCardAndRequestPolicyOwnerChange', SET_POLICY_DISTANCE_RATES_UNIT: 'SetPolicyDistanceRatesUnit', SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY: 'SetPolicyDistanceRatesDefaultCategory', + SET_POLICY_PER_DIEM_RATES_DEFAULT_CATEGORY: 'SetPolicyPerDiemRatesDefaultCategory', ENABLE_DISTANCE_REQUEST_TAX: 'EnableDistanceRequestTax', UPDATE_POLICY_DISTANCE_RATE_VALUE: 'UpdatePolicyDistanceRateValue', UPDATE_POLICY_DISTANCE_TAX_RATE_VALUE: 'UpdateDistanceTaxRate', @@ -692,6 +693,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_POLICY_TAX_CODE]: Parameters.UpdatePolicyTaxCodeParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_UNIT]: Parameters.SetPolicyDistanceRatesUnitParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; + [WRITE_COMMANDS.SET_POLICY_PER_DIEM_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; [WRITE_COMMANDS.ENABLE_DISTANCE_REQUEST_TAX]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; [WRITE_COMMANDS.REPORT_EXPORT]: Parameters.ReportExportParams; [WRITE_COMMANDS.MARK_AS_EXPORTED]: Parameters.MarkAsExportedParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8a64424c8f7d..fb8178cc0bd3 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -566,6 +566,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/rules/RulesMaxExpenseAmountPage').default, [SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AGE]: () => require('../../../../pages/workspace/rules/RulesMaxExpenseAgePage').default, [SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: () => require('../../../../pages/workspace/rules/RulesBillableDefaultPage').default, + [SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemSettingsPage').default, }); const EnablePaymentsStackNavigator = createModalStackNavigator({ 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 d282bab770c6..c8309ba70ea9 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -243,6 +243,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AGE, SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT, ], + [SCREENS.WORKSPACE.PER_DIEM]: [SCREENS.WORKSPACE.PER_DIEM_SETTINGS], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 7a5b31489764..e789c2cce4f3 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -935,6 +935,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: { path: ROUTES.RULES_BILLABLE_DEFAULT.route, }, + [SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: { + path: ROUTES.WORKSPACE_PER_DIEM_SETTINGS.route, + }, }, }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index ba859efff944..4ec8787c696a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -896,6 +896,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: { policyID: string; }; + [SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: { + policyID: string; + }; } & ReimbursementAccountNavigatorParamList; type NewChatNavigatorParamList = { diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index dced49976c5a..63aee4bb1e46 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1069,6 +1069,60 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData}); } +function setPolicyPerDiemRatesDefaultCategory(policyID: string, currentCustomUnit: CustomUnit, newCustomUnit: CustomUnit) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [newCustomUnit.customUnitID]: { + ...newCustomUnit, + pendingFields: {defaultCategory: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + }, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [newCustomUnit.customUnitID]: { + pendingFields: {defaultCategory: null}, + }, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [currentCustomUnit.customUnitID]: { + ...currentCustomUnit, + errorFields: {defaultCategory: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, + pendingFields: {defaultCategory: null}, + }, + }, + }, + }, + ]; + + const params: SetPolicyDistanceRatesDefaultCategoryParams = { + policyID, + customUnit: JSON.stringify(removePendingFieldsFromCustomUnit(newCustomUnit)), + }; + + API.write(WRITE_COMMANDS.SET_POLICY_PER_DIEM_RATES_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData}); +} + function downloadCategoriesCSV(policyID: string, onDownloadFailed: () => void) { const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_CATEGORIES_CSV, { policyID, @@ -1365,6 +1419,7 @@ export { clearCategoryErrors, enablePolicyCategories, setPolicyDistanceRatesDefaultCategory, + setPolicyPerDiemRatesDefaultCategory, deleteWorkspaceCategories, buildOptimisticPolicyCategories, setPolicyCategoryReceiptsRequired, diff --git a/src/libs/actions/Policy/PerDiem.ts b/src/libs/actions/Policy/PerDiem.ts index 2ce31fd4c921..a480b1a35a9e 100644 --- a/src/libs/actions/Policy/PerDiem.ts +++ b/src/libs/actions/Policy/PerDiem.ts @@ -9,6 +9,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, Report} from '@src/types/onyx'; +import type {ErrorFields} from '@src/types/onyx/OnyxCommon'; import type {OnyxData} from '@src/types/onyx/Request'; const allPolicies: OnyxCollection = {}; @@ -119,4 +120,14 @@ function openPolicyPerDiemPage(policyID?: string) { API.read(READ_COMMANDS.OPEN_POLICY_PER_DIEM_RATES_PAGE, params); } -export {enablePerDiem, openPolicyPerDiemPage}; +function clearPolicyPerDiemRatesErrorFields(policyID: string, customUnitID: string, updatedErrorFields: ErrorFields) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + customUnits: { + [customUnitID]: { + errorFields: updatedErrorFields, + }, + }, + }); +} + +export {enablePerDiem, openPolicyPerDiemPage, clearPolicyPerDiemRatesErrorFields}; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx index eed24a4ea13f..bf8b28d2580a 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -23,12 +23,12 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Category from '@userActions/Policy/Category'; import * as DistanceRate from '@userActions/Policy/DistanceRate'; import * as Policy from '@userActions/Policy/Policy'; +import CategorySelector from '@src/components/CategorySelector'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {CustomUnit} from '@src/types/onyx/Policy'; -import CategorySelector from './CategorySelector'; import UnitSelector from './UnitSelector'; type PolicyDistanceRatesSettingsPageProps = StackScreenProps; @@ -125,7 +125,7 @@ function PolicyDistanceRatesSettingsPage({route}: PolicyDistanceRatesSettingsPag > { - // TODO: Uncomment this when the import feature is ready - // Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_RATES_SETTINGS.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_SETTINGS.getRoute(policyID)); }; // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx new file mode 100644 index 000000000000..89a83f805e6b --- /dev/null +++ b/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx @@ -0,0 +1,104 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useState} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import type {ListItem} from '@components/SelectionList/types'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clearPolicyPerDiemRatesErrorFields} from '@libs/actions/Policy/PerDiem'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Category from '@userActions/Policy/Category'; +import CategorySelector from '@src/components/CategorySelector'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {CustomUnit} from '@src/types/onyx/Policy'; + +type WorkspacePerDiemSettingsPageProps = StackScreenProps; + +function WorkspacePerDiemSettingsPage({route}: WorkspacePerDiemSettingsPageProps) { + const policyID = route.params.policyID; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + + const styles = useThemeStyles(); + const [isCategoryPickerVisible, setIsCategoryPickerVisible] = useState(false); + const {translate} = useLocalize(); + const customUnit = getPerDiemCustomUnit(policy); + const customUnitID = customUnit?.customUnitID ?? ''; + + const defaultCategory = customUnit?.defaultCategory; + const errorFields = customUnit?.errorFields; + + const FullPageBlockingView = !customUnit ? FullPageOfflineBlockingView : View; + + const setNewCategory = (category: ListItem) => { + if (!category.searchText || !customUnit) { + return; + } + + Category.setPolicyPerDiemRatesDefaultCategory(policyID, customUnit, { + ...customUnit, + defaultCategory: defaultCategory === category.searchText ? '' : category.searchText, + }); + }; + + const clearErrorFields = (fieldName: keyof CustomUnit) => { + clearPolicyPerDiemRatesErrorFields(policyID, customUnitID, {...errorFields, [fieldName]: null}); + }; + + return ( + + + + + + {!!policy?.areCategoriesEnabled && OptionsListUtils.hasEnabledOptions(policyCategories ?? {}) && ( + clearErrorFields('defaultCategory')} + > + setIsCategoryPickerVisible(true)} + hidePickerModal={() => setIsCategoryPickerVisible(false)} + /> + + )} + + + + + ); +} + +WorkspacePerDiemSettingsPage.displayName = 'WorkspacePerDiemSettingsPage'; + +export default WorkspacePerDiemSettingsPage; From 92302b405790d17c3a09dde61eaba5d864cd311a Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 8 Nov 2024 22:12:35 +0530 Subject: [PATCH 022/104] Fix lint --- .../workspace/categories/WorkspaceCategoriesSettingsPage.tsx | 2 +- .../workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx | 2 +- src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index aea6f596badd..12502787d9df 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -1,6 +1,7 @@ import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import CategorySelectorModal from '@components/CategorySelector/CategorySelectorModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -12,7 +13,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import CategorySelectorModal from '@pages/workspace/distanceRates/CategorySelector/CategorySelectorModal'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx index bf8b28d2580a..2d25f066023c 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -3,6 +3,7 @@ import React, {useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import CategorySelector from '@components/CategorySelector'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -23,7 +24,6 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Category from '@userActions/Policy/Category'; import * as DistanceRate from '@userActions/Policy/DistanceRate'; import * as Policy from '@userActions/Policy/Policy'; -import CategorySelector from '@src/components/CategorySelector'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx index 89a83f805e6b..c620c811184c 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx @@ -3,6 +3,7 @@ import React, {useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import CategorySelector from '@components/CategorySelector'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -17,7 +18,6 @@ import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Category from '@userActions/Policy/Category'; -import CategorySelector from '@src/components/CategorySelector'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; From f5da09521939008c1974b8943dea06dcc75e74ed Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 8 Nov 2024 10:45:28 -0800 Subject: [PATCH 023/104] Try advancing timers past throttle --- tests/unit/NetworkTest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index ea638aab4cf7..bb2738416440 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -111,7 +111,8 @@ describe('NetworkTests', () => { expect(isOffline).toBe(false); // Advance the network request queue by 1 second so that it can realize it's back online - jest.advanceTimersByTime(CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); + // And advance past the retry delay + jest.advanceTimersByTime(CONST.NETWORK.PROCESS_REQUEST_DELAY_MS + CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS); return waitForBatchedUpdates(); }) .then(() => { From 225ad109708674a3d5722be799b5f9020fccce57 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 8 Nov 2024 13:47:43 -0800 Subject: [PATCH 024/104] Comment out shit causing a circular dependency --- src/libs/Network/index.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/libs/Network/index.ts b/src/libs/Network/index.ts index 2a600d5d51de..668a7038e706 100644 --- a/src/libs/Network/index.ts +++ b/src/libs/Network/index.ts @@ -1,6 +1,6 @@ import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; -import {reauthenticate} from '@libs/Middleware/Reauthentication'; +// import {reauthenticate} from '@libs/Middleware/Reauthentication'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Request} from '@src/types/onyx'; @@ -17,15 +17,15 @@ ActiveClientManager.isReady().then(() => { setInterval(MainQueue.process, CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); // If a reauthentication request is set make sure it is processed - Onyx.connect({ - key: ONYXKEYS.REAUTHENTICATION_REQUEST, - callback: (request) => { - if (!request) { - return; - } - reauthenticate(request.commandName); - }, - }); + // Onyx.connect({ + // key: ONYXKEYS.REAUTHENTICATION_REQUEST, + // callback: (request) => { + // if (!request) { + // return; + // } + // // reauthenticate(request.commandName); + // }, + // }); }); /** From 117a8e8c9db9cbbf7e17fcb02c2e057efaf6a90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Sat, 9 Nov 2024 09:50:03 +0100 Subject: [PATCH 025/104] fix test naming --- tests/unit/CategoryOptionListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/CategoryOptionListUtils.ts b/tests/unit/CategoryOptionListUtils.ts index 21f5e6533e77..2537094511ce 100644 --- a/tests/unit/CategoryOptionListUtils.ts +++ b/tests/unit/CategoryOptionListUtils.ts @@ -4,7 +4,7 @@ import type {PolicyCategories} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; describe('CategoryOptionListUtils', () => { - it('getFilteredOptions() for categories', () => { + it('getCategoryListSections()', () => { const search = 'Food'; const emptySearch = ''; const wrongSearch = 'bla bla'; From 33c65fc54563e395ac0f67a6ef3b994626f80cfb Mon Sep 17 00:00:00 2001 From: Hans Date: Mon, 11 Nov 2024 18:21:58 +0700 Subject: [PATCH 026/104] addressing comment --- .../settings/Wallet/Card/BaseGetPhysicalCard.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 2b92058caedc..ee1875b14276 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -102,7 +102,7 @@ function BaseGetPhysicalCard({ const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); const cardID = cardToBeIssued?.cardID.toString() ?? '-1'; - const [currentCardId, setCurrentCardId] = useState(cardID); + const [currentCardID, setCurrentCardID] = useState(cardID); const errorMessage = ErrorUtils.getLatestErrorMessageField(cardToBeIssued); useEffect(() => { @@ -137,16 +137,18 @@ function BaseGetPhysicalCard({ }, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, cardToBeIssued, privatePersonalDetails]); useEffect(() => { - if (!isConfirmation || !!cardToBeIssued || !currentCardId) { + // If that's not the confirmation route, or if there's a value for cardToBeIssued, + // It means the current card is not issued and we still need to stay on this screen. + if (!isConfirmation || !!cardToBeIssued || !currentCardID) { return; } // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); - Wallet.clearPhysicalCardError(currentCardId); - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(currentCardId.toString())); - setCurrentCardId(undefined); - }, [currentCardId, isConfirmation, cardToBeIssued]); + Wallet.clearPhysicalCardError(currentCardID); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(currentCardID.toString())); + setCurrentCardID(undefined); + }, [currentCardID, isConfirmation, cardToBeIssued]); const onSubmit = useCallback(() => { const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); @@ -159,7 +161,7 @@ function BaseGetPhysicalCard({ const handleIssuePhysicalCard = useCallback( (validateCode: string) => { - setCurrentCardId(cardToBeIssued?.cardID.toString()); + setCurrentCardID(cardToBeIssued?.cardID.toString()); const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); // If the current step of the get physical card flow is the confirmation page Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? -1, session?.authToken ?? '', updatedPrivatePersonalDetails, validateCode); From 14652ea57b6477b87e02ffae72fd0e90c084a399 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 12 Nov 2024 09:45:56 +0700 Subject: [PATCH 027/104] add code request status --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index ee1875b14276..fa1593cf41fb 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -95,6 +95,7 @@ function BaseGetPhysicalCard({ const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST); const [session] = useOnyx(ONYXKEYS.SESSION); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [validateCodeAction] = useOnyx(ONYXKEYS.VALIDATE_ACTION_CODE); const [draftValues] = useOnyx(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [isActionCodeModalVisible, setActionCodeModalVisible] = useState(false); @@ -183,6 +184,7 @@ function BaseGetPhysicalCard({ {renderContent({onSubmit, submitButtonText, children, onValidate})} User.requestValidateCodeAction()} clearError={() => Wallet.clearPhysicalCardError(cardID)} From 982abf03c4f30b43ba29bfde446ce85f26f9a8de Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 12 Nov 2024 10:11:50 +0700 Subject: [PATCH 028/104] Update src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx Co-authored-by: Dominic <165644294+dominictb@users.noreply.github.com> --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index fa1593cf41fb..16b0afcb0c6d 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -164,7 +164,6 @@ function BaseGetPhysicalCard({ (validateCode: string) => { setCurrentCardID(cardToBeIssued?.cardID.toString()); const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues, privatePersonalDetails); - // If the current step of the get physical card flow is the confirmation page Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? -1, session?.authToken ?? '', updatedPrivatePersonalDetails, validateCode); }, [cardToBeIssued?.cardID, draftValues, session?.authToken, privatePersonalDetails], From 55c4a0c7ab8e7b92e3d8c70ecb99b6fdf87ac115 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 12 Nov 2024 10:12:01 +0700 Subject: [PATCH 029/104] Update src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx Co-authored-by: Dominic <165644294+dominictb@users.noreply.github.com> --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 16b0afcb0c6d..bce0a8a928b6 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -138,8 +138,8 @@ function BaseGetPhysicalCard({ }, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, cardToBeIssued, privatePersonalDetails]); useEffect(() => { - // If that's not the confirmation route, or if there's a value for cardToBeIssued, - // It means the current card is not issued and we still need to stay on this screen. + // Current step of the get physical card flow should be the confirmation page; and + // Card has NOT_ACTIVATED state when successfully being issued so cardToBeIssued should be undefined if (!isConfirmation || !!cardToBeIssued || !currentCardID) { return; } From 66742e61b03f872c17483dfe469414fd6bea70cc Mon Sep 17 00:00:00 2001 From: Wildan M Date: Tue, 12 Nov 2024 12:44:34 +0700 Subject: [PATCH 030/104] Fix gesture in modal for android --- .../ValidateCodeActionModal/ValidateCodeForm/index.android.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/index.android.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/index.android.tsx index 704405f93a2c..2d36965e7412 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/index.android.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/index.android.tsx @@ -1,6 +1,7 @@ import React, {forwardRef} from 'react'; import BaseValidateCodeForm from './BaseValidateCodeForm'; import type {ValidateCodeFormHandle, ValidateCodeFormProps} from './BaseValidateCodeForm'; +import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; const ValidateCodeForm = forwardRef((props, ref) => ( )); -export default ValidateCodeForm; +export default gestureHandlerRootHOC(ValidateCodeForm); From 1776b6860cd5b64ae9c8acefa620524b79cadf31 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 12 Nov 2024 11:24:39 +0530 Subject: [PATCH 031/104] Use newer command to change default category --- .../EnableDistanceRequestTaxParams.ts | 6 ++ .../SetCustomUnitDefaultCategoryParams.ts | 7 ++ ...olicyDistanceRatesDefaultCategoryParams.ts | 6 -- src/libs/API/parameters/index.ts | 3 +- src/libs/API/types.ts | 8 +- src/libs/actions/Policy/Category.ts | 81 +++---------------- .../PolicyDistanceRatesSettingsPage.tsx | 5 +- .../perDiem/WorkspacePerDiemSettingsPage.tsx | 5 +- 8 files changed, 33 insertions(+), 88 deletions(-) create mode 100644 src/libs/API/parameters/EnableDistanceRequestTaxParams.ts create mode 100644 src/libs/API/parameters/SetCustomUnitDefaultCategoryParams.ts delete mode 100644 src/libs/API/parameters/SetPolicyDistanceRatesDefaultCategoryParams.ts diff --git a/src/libs/API/parameters/EnableDistanceRequestTaxParams.ts b/src/libs/API/parameters/EnableDistanceRequestTaxParams.ts new file mode 100644 index 000000000000..0d42d9ba84b4 --- /dev/null +++ b/src/libs/API/parameters/EnableDistanceRequestTaxParams.ts @@ -0,0 +1,6 @@ +type EnableDistanceRequestTaxParams = { + policyID: string; + customUnit: string; +}; + +export default EnableDistanceRequestTaxParams; diff --git a/src/libs/API/parameters/SetCustomUnitDefaultCategoryParams.ts b/src/libs/API/parameters/SetCustomUnitDefaultCategoryParams.ts new file mode 100644 index 000000000000..53eac3110af7 --- /dev/null +++ b/src/libs/API/parameters/SetCustomUnitDefaultCategoryParams.ts @@ -0,0 +1,7 @@ +type SetCustomUnitDefaultCategoryParams = { + policyID: string; + customUnitID: string; + category: string; +}; + +export default SetCustomUnitDefaultCategoryParams; diff --git a/src/libs/API/parameters/SetPolicyDistanceRatesDefaultCategoryParams.ts b/src/libs/API/parameters/SetPolicyDistanceRatesDefaultCategoryParams.ts deleted file mode 100644 index d2d11993a172..000000000000 --- a/src/libs/API/parameters/SetPolicyDistanceRatesDefaultCategoryParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -type SetPolicyDistanceRatesDefaultCategoryParams = { - policyID: string; - customUnit: string; -}; - -export default SetPolicyDistanceRatesDefaultCategoryParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index fb5558fb0350..55714480762f 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -206,7 +206,8 @@ export type {default as EnablePolicyTaxesParams} from './EnablePolicyTaxesParams export type {default as OpenPolicyMoreFeaturesPageParams} from './OpenPolicyMoreFeaturesPageParams'; export type {default as CreatePolicyDistanceRateParams} from './CreatePolicyDistanceRateParams'; export type {default as SetPolicyDistanceRatesUnitParams} from './SetPolicyDistanceRatesUnitParams'; -export type {default as SetPolicyDistanceRatesDefaultCategoryParams} from './SetPolicyDistanceRatesDefaultCategoryParams'; +export type {default as EnableDistanceRequestTaxParams} from './EnableDistanceRequestTaxParams'; +export type {default as SetCustomUnitDefaultCategoryParams} from './SetCustomUnitDefaultCategoryParams'; export type {default as UpdatePolicyDistanceRateValueParams} from './UpdatePolicyDistanceRateValueParams'; export type {default as SetPolicyDistanceRatesEnabledParams} from './SetPolicyDistanceRatesEnabledParams'; export type {default as DeletePolicyDistanceRatesParams} from './DeletePolicyDistanceRatesParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index a8847545fbb3..d730819d876c 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -287,8 +287,7 @@ const WRITE_COMMANDS = { REQUEST_WORKSPACE_OWNER_CHANGE: 'RequestWorkspaceOwnerChange', ADD_BILLING_CARD_AND_REQUEST_WORKSPACE_OWNER_CHANGE: 'AddBillingCardAndRequestPolicyOwnerChange', SET_POLICY_DISTANCE_RATES_UNIT: 'SetPolicyDistanceRatesUnit', - SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY: 'SetPolicyDistanceRatesDefaultCategory', - SET_POLICY_PER_DIEM_RATES_DEFAULT_CATEGORY: 'SetPolicyPerDiemRatesDefaultCategory', + SET_CUSTOM_UNIT_DEFAULT_CATEGORY: 'SetCustomUnitDefaultCategory', ENABLE_DISTANCE_REQUEST_TAX: 'EnableDistanceRequestTax', UPDATE_POLICY_DISTANCE_RATE_VALUE: 'UpdatePolicyDistanceRateValue', UPDATE_POLICY_DISTANCE_TAX_RATE_VALUE: 'UpdateDistanceTaxRate', @@ -692,9 +691,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.RENAME_POLICY_TAX]: Parameters.RenamePolicyTaxParams; [WRITE_COMMANDS.UPDATE_POLICY_TAX_CODE]: Parameters.UpdatePolicyTaxCodeParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_UNIT]: Parameters.SetPolicyDistanceRatesUnitParams; - [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; - [WRITE_COMMANDS.SET_POLICY_PER_DIEM_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; - [WRITE_COMMANDS.ENABLE_DISTANCE_REQUEST_TAX]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; + [WRITE_COMMANDS.SET_CUSTOM_UNIT_DEFAULT_CATEGORY]: Parameters.SetCustomUnitDefaultCategoryParams; + [WRITE_COMMANDS.ENABLE_DISTANCE_REQUEST_TAX]: Parameters.EnableDistanceRequestTaxParams; [WRITE_COMMANDS.REPORT_EXPORT]: Parameters.ReportExportParams; [WRITE_COMMANDS.MARK_AS_EXPORTED]: Parameters.MarkAsExportedParams; [WRITE_COMMANDS.REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE]: Parameters.RequestExpensifyCardLimitIncreaseParams; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 63aee4bb1e46..cb9a39612b09 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -13,7 +13,6 @@ import type { SetPolicyCategoryMaxAmountParams, SetPolicyCategoryReceiptsRequiredParams, SetPolicyCategoryTaxParams, - SetPolicyDistanceRatesDefaultCategoryParams, SetWorkspaceCategoryDescriptionHintParams, UpdatePolicyCategoryGLCodeParams, } from '@libs/API/parameters'; @@ -28,13 +27,13 @@ import {translateLocal} from '@libs/Localize'; import Log from '@libs/Log'; import enhanceParameters from '@libs/Network/enhanceParameters'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import {navigateWhenEnableFeature, removePendingFieldsFromCustomUnit} from '@libs/PolicyUtils'; +import {navigateWhenEnableFeature} from '@libs/PolicyUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyCategory, RecentlyUsedCategories, Report} from '@src/types/onyx'; -import type {ApprovalRule, CustomUnit, ExpenseRule} from '@src/types/onyx/Policy'; +import type {ApprovalRule, ExpenseRule} from '@src/types/onyx/Policy'; import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -1015,15 +1014,15 @@ function enablePolicyCategories(policyID: string, enabled: boolean) { } } -function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUnit: CustomUnit, newCustomUnit: CustomUnit) { +function setPolicyCustomUnitDefaultCategory(policyID: string, customUnitID: string, oldCatrgory: string | undefined, category: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { customUnits: { - [newCustomUnit.customUnitID]: { - ...newCustomUnit, + [customUnitID]: { + defaultCategory: category, pendingFields: {defaultCategory: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, }, }, @@ -1037,7 +1036,7 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { customUnits: { - [newCustomUnit.customUnitID]: { + [customUnitID]: { pendingFields: {defaultCategory: null}, }, }, @@ -1051,8 +1050,8 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { customUnits: { - [currentCustomUnit.customUnitID]: { - ...currentCustomUnit, + [customUnitID]: { + defaultCategory: oldCatrgory, errorFields: {defaultCategory: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, pendingFields: {defaultCategory: null}, }, @@ -1061,66 +1060,13 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn }, ]; - const params: SetPolicyDistanceRatesDefaultCategoryParams = { + const params = { policyID, - customUnit: JSON.stringify(removePendingFieldsFromCustomUnit(newCustomUnit)), + customUnitID, + category, }; - API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData}); -} - -function setPolicyPerDiemRatesDefaultCategory(policyID: string, currentCustomUnit: CustomUnit, newCustomUnit: CustomUnit) { - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - customUnits: { - [newCustomUnit.customUnitID]: { - ...newCustomUnit, - pendingFields: {defaultCategory: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, - }, - }, - }, - }, - ]; - - const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - customUnits: { - [newCustomUnit.customUnitID]: { - pendingFields: {defaultCategory: null}, - }, - }, - }, - }, - ]; - - const failureData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - customUnits: { - [currentCustomUnit.customUnitID]: { - ...currentCustomUnit, - errorFields: {defaultCategory: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, - pendingFields: {defaultCategory: null}, - }, - }, - }, - }, - ]; - - const params: SetPolicyDistanceRatesDefaultCategoryParams = { - policyID, - customUnit: JSON.stringify(removePendingFieldsFromCustomUnit(newCustomUnit)), - }; - - API.write(WRITE_COMMANDS.SET_POLICY_PER_DIEM_RATES_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData}); + API.write(WRITE_COMMANDS.SET_CUSTOM_UNIT_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData}); } function downloadCategoriesCSV(policyID: string, onDownloadFailed: () => void) { @@ -1418,8 +1364,7 @@ export { setPolicyCategoryGLCode, clearCategoryErrors, enablePolicyCategories, - setPolicyDistanceRatesDefaultCategory, - setPolicyPerDiemRatesDefaultCategory, + setPolicyCustomUnitDefaultCategory, deleteWorkspaceCategories, buildOptimisticPolicyCategories, setPolicyCategoryReceiptsRequired, diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx index 2d25f066023c..d971b42b45eb 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -65,10 +65,7 @@ function PolicyDistanceRatesSettingsPage({route}: PolicyDistanceRatesSettingsPag return; } - Category.setPolicyDistanceRatesDefaultCategory(policyID, customUnit, { - ...customUnit, - defaultCategory: defaultCategory === category.searchText ? '' : category.searchText, - }); + Category.setPolicyCustomUnitDefaultCategory(policyID, customUnitID, customUnit.defaultCategory, defaultCategory === category.searchText ? '' : category.searchText); }; const clearErrorFields = (fieldName: keyof CustomUnit) => { diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx index c620c811184c..4ac57ed296b5 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx @@ -46,10 +46,7 @@ function WorkspacePerDiemSettingsPage({route}: WorkspacePerDiemSettingsPageProps return; } - Category.setPolicyPerDiemRatesDefaultCategory(policyID, customUnit, { - ...customUnit, - defaultCategory: defaultCategory === category.searchText ? '' : category.searchText, - }); + Category.setPolicyCustomUnitDefaultCategory(policyID, customUnitID, customUnit.defaultCategory, defaultCategory === category.searchText ? '' : category.searchText); }; const clearErrorFields = (fieldName: keyof CustomUnit) => { From cbf6a2b940b256319fbdb5e84a5fb004af3f18a4 Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 13 Nov 2024 11:12:53 +0700 Subject: [PATCH 032/104] address comment --- .../ValidateCodeForm/BaseValidateCodeForm.tsx | 2 +- src/components/ValidateCodeActionModal/type.ts | 2 +- src/libs/actions/Wallet.ts | 6 +++--- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx index 5c918128e662..5418f7e5b3c1 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -66,7 +66,7 @@ type ValidateCodeFormProps = { /** Function is called when validate code modal is mounted and on magic code resend */ sendValidateCode: () => void; - /** Wheather the form is loading or not */ + /** Whether the form is loading or not */ isLoading?: boolean; }; diff --git a/src/components/ValidateCodeActionModal/type.ts b/src/components/ValidateCodeActionModal/type.ts index 1fda266ec47d..fbee1a3e0bf6 100644 --- a/src/components/ValidateCodeActionModal/type.ts +++ b/src/components/ValidateCodeActionModal/type.ts @@ -38,7 +38,7 @@ type ValidateCodeActionModalProps = { /** If the magic code has been resent previously */ hasMagicCodeBeenSent?: boolean; - /** Wheather the form is loading or not */ + /** Whether the form is loading or not */ isLoading?: boolean; }; diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index d5b3862b62b5..2ea60d1a453f 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -352,12 +352,12 @@ function resetWalletAdditionalDetailsDraft() { /** * Clear the error of specific card - * @param cardId The card id of the card that you want to clear the errors. + * @param cardID The card id of the card that you want to clear the errors. */ -function clearPhysicalCardError(cardId: string) { +function clearPhysicalCardError(cardID: string) { FormActions.clearErrors(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM); Onyx.merge(ONYXKEYS.CARD_LIST, { - [cardId]: { + [cardID]: { errors: null, }, }); diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index bce0a8a928b6..6c347c1d9293 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -143,6 +143,7 @@ function BaseGetPhysicalCard({ if (!isConfirmation || !!cardToBeIssued || !currentCardID) { return; } + // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); From 92e7e90017b9bbfc55688b8b7decbe4d940307d1 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal <58412969+shubham1206agra@users.noreply.github.com> Date: Thu, 14 Nov 2024 00:07:15 +0530 Subject: [PATCH 033/104] Apply suggestions from code review Co-authored-by: c3024 <102477862+c3024@users.noreply.github.com> --- src/libs/actions/Policy/Category.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index cb9a39612b09..2f663ac204d2 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1014,7 +1014,7 @@ function enablePolicyCategories(policyID: string, enabled: boolean) { } } -function setPolicyCustomUnitDefaultCategory(policyID: string, customUnitID: string, oldCatrgory: string | undefined, category: string) { +function setPolicyCustomUnitDefaultCategory(policyID: string, customUnitID: string, oldCategory: string | undefined, category: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -1051,7 +1051,7 @@ function setPolicyCustomUnitDefaultCategory(policyID: string, customUnitID: stri value: { customUnits: { [customUnitID]: { - defaultCategory: oldCatrgory, + defaultCategory: oldCategory, errorFields: {defaultCategory: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}, pendingFields: {defaultCategory: null}, }, From 01a1fe8477d7daad82173eb20c3d9beed8c407f2 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 14 Nov 2024 00:12:01 +0530 Subject: [PATCH 034/104] Fix usage of function --- .../distanceRates/PolicyDistanceRatesSettingsPage.tsx | 4 ++-- src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx index d971b42b45eb..fbbdf5ee382f 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -61,11 +61,11 @@ function PolicyDistanceRatesSettingsPage({route}: PolicyDistanceRatesSettingsPag }; const setNewCategory = (category: ListItem) => { - if (!category.searchText || !customUnit) { + if (!category.searchText || !customUnit || defaultCategory === category.searchText) { return; } - Category.setPolicyCustomUnitDefaultCategory(policyID, customUnitID, customUnit.defaultCategory, defaultCategory === category.searchText ? '' : category.searchText); + Category.setPolicyCustomUnitDefaultCategory(policyID, customUnitID, customUnit.defaultCategory, category.searchText); }; const clearErrorFields = (fieldName: keyof CustomUnit) => { diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx index 4ac57ed296b5..1fd28b4ff75a 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemSettingsPage.tsx @@ -42,11 +42,11 @@ function WorkspacePerDiemSettingsPage({route}: WorkspacePerDiemSettingsPageProps const FullPageBlockingView = !customUnit ? FullPageOfflineBlockingView : View; const setNewCategory = (category: ListItem) => { - if (!category.searchText || !customUnit) { + if (!category.searchText || !customUnit || defaultCategory === category.searchText) { return; } - Category.setPolicyCustomUnitDefaultCategory(policyID, customUnitID, customUnit.defaultCategory, defaultCategory === category.searchText ? '' : category.searchText); + Category.setPolicyCustomUnitDefaultCategory(policyID, customUnitID, customUnit.defaultCategory, category.searchText); }; const clearErrorFields = (fieldName: keyof CustomUnit) => { From 0855d78a912e970388a48fa234f55d7172a8164e Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 14 Nov 2024 17:43:24 +0700 Subject: [PATCH 035/104] remove dupe props --- src/components/ValidateCodeActionModal/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ValidateCodeActionModal/index.tsx b/src/components/ValidateCodeActionModal/index.tsx index 6e90f3460925..81e4f3092125 100644 --- a/src/components/ValidateCodeActionModal/index.tsx +++ b/src/components/ValidateCodeActionModal/index.tsx @@ -85,7 +85,6 @@ function ValidateCodeActionModal({ buttonStyles={[themeStyles.justifyContentEnd, themeStyles.flex1, safePaddingBottomStyle]} ref={validateCodeFormRef} hasMagicCodeBeenSent={hasMagicCodeBeenSent} - isLoading={isLoading} /> {footer?.()} From 5c75531b6cf352f2b15209d3030e0b9b515001e8 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 14 Nov 2024 17:51:13 +0700 Subject: [PATCH 036/104] deprecate description props --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 6c347c1d9293..8fe75c7c7c3c 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -192,7 +192,7 @@ function BaseGetPhysicalCard({ handleSubmitForm={handleIssuePhysicalCard} title={translate('cardPage.validateCardTitle')} onClose={() => setActionCodeModalVisible(false)} - description={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} + descriptionPrimary={translate('cardPage.enterMagicCode', {contactMethod: account?.primaryLogin ?? ''})} /> ); From fb7ee30ad820d3903071e27ae561a73ff798da9d Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 14 Nov 2024 18:12:15 +0700 Subject: [PATCH 037/104] address comment --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 8fe75c7c7c3c..a8aafa38503f 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -140,7 +140,8 @@ function BaseGetPhysicalCard({ useEffect(() => { // Current step of the get physical card flow should be the confirmation page; and // Card has NOT_ACTIVATED state when successfully being issued so cardToBeIssued should be undefined - if (!isConfirmation || !!cardToBeIssued || !currentCardID) { + // -1 is not a valid cardID, we don't need to clean up the form value in that case. + if (!isConfirmation || !!cardToBeIssued || !currentCardID || currentCardID === '-1') { return; } From 5256c4e05ed7fe5c8b76c2e7f8fe23abff99c105 Mon Sep 17 00:00:00 2001 From: Shahidullah Muffakir Date: Fri, 15 Nov 2024 06:56:47 +0530 Subject: [PATCH 038/104] ensure consistent profile picture shape in shared QR codes --- src/pages/workspace/WorkspaceProfileSharePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx index 16a076205ad3..58c4564cc842 100644 --- a/src/pages/workspace/WorkspaceProfileSharePage.tsx +++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx @@ -86,7 +86,7 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) { if (!adminRoom?.reportID) { return; } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminRoom.reportID)); + Navigation.dismissModal(adminRoom.reportID); }} > {CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS} From ceca20c00a7a8794d6dfcfc6b5f80f891765117d Mon Sep 17 00:00:00 2001 From: Hans Date: Fri, 15 Nov 2024 16:01:35 +0700 Subject: [PATCH 039/104] fix -1 value appears inside cardList --- src/libs/actions/Wallet.ts | 20 ++++++++++++++++++- .../Wallet/Card/BaseGetPhysicalCard.tsx | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index 2ea60d1a453f..ea7e86ef49b7 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -299,6 +299,13 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private errors: null, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.VALIDATE_ACTION_CODE, + value: { + validateCodeSent: false, + }, + }, ]; const successData: OnyxUpdate[] = [ @@ -320,6 +327,13 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private errors: null, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.VALIDATE_ACTION_CODE, + value: { + validateCodeSent: false, + }, + }, ]; const failureData: OnyxUpdate[] = [ @@ -354,7 +368,11 @@ function resetWalletAdditionalDetailsDraft() { * Clear the error of specific card * @param cardID The card id of the card that you want to clear the errors. */ -function clearPhysicalCardError(cardID: string) { +function clearPhysicalCardError(cardID?: string) { + if (!cardID) { + return; + } + FormActions.clearErrors(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM); Onyx.merge(ONYXKEYS.CARD_LIST, { [cardID]: { diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index a8aafa38503f..f7c8ff60be36 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -188,7 +188,7 @@ function BaseGetPhysicalCard({ hasMagicCodeBeenSent={validateCodeAction?.validateCodeSent} isVisible={isActionCodeModalVisible} sendValidateCode={() => User.requestValidateCodeAction()} - clearError={() => Wallet.clearPhysicalCardError(cardID)} + clearError={() => Wallet.clearPhysicalCardError(currentCardID)} validateError={!isEmptyObject(formData?.errors) ? formData?.errors : errorMessage} handleSubmitForm={handleIssuePhysicalCard} title={translate('cardPage.validateCardTitle')} From f8c25d3951f37604ff5e7ab4c99d3f07189fc2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 18 Nov 2024 10:20:44 +0100 Subject: [PATCH 040/104] fix use correct const --- src/libs/CategoryOptionListUtils.ts | 2 +- ...ategoryOptionListUtils.ts => CategoryOptionListUtilsTest.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/unit/{CategoryOptionListUtils.ts => CategoryOptionListUtilsTest.ts} (100%) diff --git a/src/libs/CategoryOptionListUtils.ts b/src/libs/CategoryOptionListUtils.ts index d370da441110..a385962358c1 100644 --- a/src/libs/CategoryOptionListUtils.ts +++ b/src/libs/CategoryOptionListUtils.ts @@ -163,7 +163,7 @@ function getCategoryListSections({ const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); - if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) { + if (numberOfEnabledCategories < CONST.STANDARD_LIST_ITEM_LIMIT) { const data = getCategoryOptionTree(filteredCategories, false, selectedOptionsWithDisabledState); categorySections.push({ // "All" section when items amount less than the threshold diff --git a/tests/unit/CategoryOptionListUtils.ts b/tests/unit/CategoryOptionListUtilsTest.ts similarity index 100% rename from tests/unit/CategoryOptionListUtils.ts rename to tests/unit/CategoryOptionListUtilsTest.ts From b7df377ffe9689531eb10cc13daefb523278fbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 18 Nov 2024 10:22:35 +0100 Subject: [PATCH 041/104] remove todo --- src/components/CategoryPicker.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 34455ca5905c..19bb98bff58e 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -38,7 +38,6 @@ function CategoryPicker({selectedCategory, policyID, onSubmit}: CategoryPickerPr { name: selectedCategory, isSelected: true, - // TODO: i added this enabled property, is true the correct default? before it was just "as" casted... enabled: true, }, ]; From 67a0c78adf4bb25a3568be38059bfd842aa29be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 18 Nov 2024 10:28:28 +0100 Subject: [PATCH 042/104] use correct type --- tests/unit/TagsOptionsListUtilsTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/TagsOptionsListUtilsTest.ts b/tests/unit/TagsOptionsListUtilsTest.ts index f3051c63be6a..191854ec4c21 100644 --- a/tests/unit/TagsOptionsListUtilsTest.ts +++ b/tests/unit/TagsOptionsListUtilsTest.ts @@ -92,7 +92,7 @@ describe('TagsOptionsListUtils', () => { ], }, ]; - const smallWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ + const smallWrongSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, @@ -285,7 +285,7 @@ describe('TagsOptionsListUtils', () => { ], }, ]; - const largeWrongSearchResultList: OptionsListUtils.CategoryTreeSection[] = [ + const largeWrongSearchResultList: OptionsListUtils.CategorySection[] = [ { title: '', shouldShow: true, From 895831e4208c5b26c79739b363b441a83f4b9be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 18 Nov 2024 13:23:35 +0100 Subject: [PATCH 043/104] remove default options --- src/components/Search/SearchFiltersChatsSelector.tsx | 1 - src/components/Search/SearchFiltersParticipantsSelector.tsx | 1 - src/components/Search/SearchRouter/SearchRouter.tsx | 2 +- src/pages/RoomInvitePage.tsx | 3 +-- src/pages/iou/request/MoneyRequestAttendeeSelector.tsx | 1 - src/pages/iou/request/MoneyRequestParticipantsSelector.tsx | 2 -- .../settings/AboutPage/ShareLogList/BaseShareLogList.tsx | 1 - src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx | 1 - src/pages/tasks/TaskAssigneeSelectorModal.tsx | 1 - src/pages/tasks/TaskShareDestinationSelectorModal.tsx | 2 -- src/pages/workspace/WorkspaceInvitePage.tsx | 4 ++-- 11 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 36b56867b99f..2e8e1009be1f 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -23,7 +23,6 @@ const defaultListOptions = { personalDetails: [], userToInvite: null, currentUserOption: null, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], headerMessage: '', diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index e7a60a5dc212..8566dbf657bd 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -25,7 +25,6 @@ const defaultListOptions = { personalDetails: [], currentUserOption: null, headerMessage: '', - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index e65b12deb64b..1a88e88291b9 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -72,7 +72,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { const {options, areOptionsInitialized} = useOptionsList(); const searchOptions = useMemo(() => { if (!areOptionsInitialized) { - return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; + return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, tagOptions: [], taxRatesOptions: []}; } return OptionsListUtils.getSearchOptions(options, '', betas ?? []); }, [areOptionsInitialized, betas, options]); diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index c833fdb68ae6..29fd4405733d 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -72,7 +72,7 @@ function RoomInvitePage({ const defaultOptions = useMemo(() => { if (!areOptionsInitialized) { - return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; + return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, tagOptions: [], taxRatesOptions: []}; } const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], '', excludedUsers); @@ -95,7 +95,6 @@ function RoomInvitePage({ selectedOptions: newSelectedOptions, recentReports: [], currentUserOption: null, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index 8732efd2e72d..91f1bcb29dcd 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -113,7 +113,6 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde personalDetails: [], currentUserOption: null, headerMessage: '', - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 4478951555ef..26d157da2344 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -102,7 +102,6 @@ function MoneyRequestParticipantsSelector({ personalDetails: [], currentUserOption: null, headerMessage: '', - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; @@ -139,7 +138,6 @@ function MoneyRequestParticipantsSelector({ personalDetails: [], currentUserOption: null, headerMessage: '', - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 3f5db6cf5613..9364d9072533 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -34,7 +34,6 @@ function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { personalDetails: [], userToInvite: null, currentUserOption: null, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], headerMessage: '', diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index a6ed5ca1b53e..bb3594124ae8 100644 --- a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx @@ -50,7 +50,6 @@ function useOptions() { personalDetails, currentUserOption, headerMessage, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 59ebe08e41a4..a7fbd0af60d4 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -62,7 +62,6 @@ function useOptions() { personalDetails, currentUserOption, headerMessage, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], }; diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 8cd38a54f7f9..3584c5fadbf4 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -63,7 +63,6 @@ function TaskShareDestinationSelectorModal() { personalDetails: [], userToInvite: null, currentUserOption: null, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], header: '', @@ -77,7 +76,6 @@ function TaskShareDestinationSelectorModal() { personalDetails: [], userToInvite: null, currentUserOption: null, - categoryOptions: [], tagOptions: [], taxRatesOptions: [], header, diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 3e63ae7cbe79..8f4861a05bf0 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -87,12 +87,12 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { const defaultOptions = useMemo(() => { if (!areOptionsInitialized) { - return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; + return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, tagOptions: [], taxRatesOptions: []}; } const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], '', excludedUsers, true); - return {...inviteOptions, recentReports: [], currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; + return {...inviteOptions, recentReports: [], currentUserOption: null, tagOptions: [], taxRatesOptions: []}; }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails]); const inviteOptions = useMemo( From cf6a298c2094db00049c53111e5b55932369c489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 18 Nov 2024 13:25:45 +0100 Subject: [PATCH 044/104] renamed categorysection to section --- src/components/Search/SearchFiltersChatsSelector.tsx | 2 +- .../Search/SearchFiltersParticipantsSelector.tsx | 2 +- src/libs/CategoryOptionListUtils.ts | 6 +++--- src/libs/OptionsListUtils.ts | 10 +++++----- src/pages/NewChatPage.tsx | 2 +- .../SearchFiltersCardPage.tsx | 4 ++-- .../iou/request/MoneyRequestAttendeeSelector.tsx | 2 +- .../iou/request/MoneyRequestParticipantsSelector.tsx | 2 +- tests/unit/OptionsListUtilsTest.ts | 6 +++--- tests/unit/TagsOptionsListUtilsTest.ts | 12 ++++++------ 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 2e8e1009be1f..b25ccbbcc1a1 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -76,7 +76,7 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen }, [defaultOptions, cleanSearchTerm, selectedOptions]); const {sections, headerMessage} = useMemo(() => { - const newSections: OptionsListUtils.CategorySection[] = []; + const newSections: OptionsListUtils.Section[] = []; if (!areOptionsInitialized) { return {sections: [], headerMessage: undefined}; } diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index 8566dbf657bd..f052d94a8a5f 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -74,7 +74,7 @@ function SearchFiltersParticipantsSelector({initialAccountIDs, onFiltersUpdate}: }, [defaultOptions, cleanSearchTerm, selectedOptions]); const {sections, headerMessage} = useMemo(() => { - const newSections: OptionsListUtils.CategorySection[] = []; + const newSections: OptionsListUtils.Section[] = []; if (!areOptionsInitialized) { return {sections: [], headerMessage: undefined}; } diff --git a/src/libs/CategoryOptionListUtils.ts b/src/libs/CategoryOptionListUtils.ts index a385962358c1..5e3feed05af3 100644 --- a/src/libs/CategoryOptionListUtils.ts +++ b/src/libs/CategoryOptionListUtils.ts @@ -7,9 +7,9 @@ import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import * as Localize from './Localize'; -import type {CategorySectionBase, OptionTree} from './OptionsListUtils'; +import type {OptionTree, SectionBase} from './OptionsListUtils'; -type CategoryTreeSection = CategorySectionBase & { +type CategoryTreeSection = SectionBase & { data: OptionTree[]; indexOffset?: number; }; @@ -279,4 +279,4 @@ function sortCategories(categories: Record): Category[] { export {getCategoryListSections, getCategoryOptionTree, sortCategories}; -export type {Category, CategorySectionBase, CategoryTreeSection, Hierarchy}; +export type {Category, SectionBase as CategorySectionBase, CategoryTreeSection, Hierarchy}; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 7f40b46baf5a..d4e9855c6667 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -166,24 +166,24 @@ type MemberForList = { reportID: string; }; -type CategorySectionBase = { +type SectionBase = { title: string | undefined; shouldShow: boolean; }; -type CategorySection = CategorySectionBase & { +type Section = SectionBase & { data: Option[]; }; type SectionForSearchTerm = { - section: CategorySection; + section: Section; }; type Options = { recentReports: ReportUtils.OptionData[]; personalDetails: ReportUtils.OptionData[]; userToInvite: ReportUtils.OptionData | null; currentUserOption: ReportUtils.OptionData | null | undefined; - taxRatesOptions: CategorySection[]; + taxRatesOptions: Section[]; }; type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; @@ -2067,4 +2067,4 @@ export { hasReportErrors, }; -export type {CategorySection, CategorySectionBase, MemberForList, Options, OptionList, SearchOption, PayeePersonalDetails, Tax, TaxRatesOption, Option, OptionTree}; +export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, PayeePersonalDetails, Tax, TaxRatesOption, Option, OptionTree}; diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index c28290e353e7..8ab5e209a904 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -139,7 +139,7 @@ function NewChatPage() { useOptions(); const [sections, firstKeyForList] = useMemo(() => { - const sectionsList: OptionsListUtils.CategorySection[] = []; + const sectionsList: OptionsListUtils.Section[] = []; let firstKey = ''; const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(debouncedSearchTerm, selectedOptions, recentReports, personalDetails); diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx index 67c8a6c9e09e..b5e50d54db25 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx @@ -10,7 +10,7 @@ import SelectionList from '@components/SelectionList'; import CardListItem from '@components/SelectionList/CardListItem'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {CategorySection} from '@libs/OptionsListUtils'; +import type {Section} from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; import * as SearchActions from '@userActions/Search'; @@ -27,7 +27,7 @@ function SearchFiltersCardPage() { const [newCards, setNewCards] = useState(currentCards ?? []); const sections = useMemo(() => { - const newSections: CategorySection[] = []; + const newSections: Section[] = []; const cards = Object.values(cardList ?? {}) .sort((a, b) => a.bank.localeCompare(b.bank)) .map((card) => { diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index 91f1bcb29dcd..17e3357e1317 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -130,7 +130,7 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde * Returns the sections needed for the OptionsSelector */ const [sections, header] = useMemo(() => { - const newSections: OptionsListUtils.CategorySection[] = []; + const newSections: OptionsListUtils.Section[] = []; if (!areOptionsInitialized || !didScreenTransitionEnd) { return [newSections, '']; } diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 26d157da2344..3f08fddfdf5c 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -160,7 +160,7 @@ function MoneyRequestParticipantsSelector({ * @returns {Array} */ const [sections, header] = useMemo(() => { - const newSections: OptionsListUtils.CategorySection[] = []; + const newSections: OptionsListUtils.Section[] = []; if (!areOptionsInitialized || !didScreenTransitionEnd) { return [newSections, '']; } diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 9de8b9f33f8d..02141ff2f1b9 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -679,7 +679,7 @@ describe('OptionsListUtils', () => { taxCode: 'CODE1', } as Transaction; - const resultList: OptionsListUtils.CategorySection[] = [ + const resultList: OptionsListUtils.Section[] = [ { data: [ { @@ -718,7 +718,7 @@ describe('OptionsListUtils', () => { }, ]; - const searchResultList: OptionsListUtils.CategorySection[] = [ + const searchResultList: OptionsListUtils.Section[] = [ { data: [ { @@ -737,7 +737,7 @@ describe('OptionsListUtils', () => { }, ]; - const wrongSearchResultList: OptionsListUtils.CategorySection[] = [ + const wrongSearchResultList: OptionsListUtils.Section[] = [ { data: [], shouldShow: true, diff --git a/tests/unit/TagsOptionsListUtilsTest.ts b/tests/unit/TagsOptionsListUtilsTest.ts index 191854ec4c21..57ebebc218fd 100644 --- a/tests/unit/TagsOptionsListUtilsTest.ts +++ b/tests/unit/TagsOptionsListUtilsTest.ts @@ -39,7 +39,7 @@ describe('TagsOptionsListUtils', () => { pendingAction: 'delete', }, }; - const smallResultList: OptionsListUtils.CategorySection[] = [ + const smallResultList: OptionsListUtils.Section[] = [ { title: '', shouldShow: false, @@ -75,7 +75,7 @@ describe('TagsOptionsListUtils', () => { ], }, ]; - const smallSearchResultList: OptionsListUtils.CategorySection[] = [ + const smallSearchResultList: OptionsListUtils.Section[] = [ { title: '', shouldShow: true, @@ -92,7 +92,7 @@ describe('TagsOptionsListUtils', () => { ], }, ]; - const smallWrongSearchResultList: OptionsListUtils.CategorySection[] = [ + const smallWrongSearchResultList: OptionsListUtils.Section[] = [ { title: '', shouldShow: true, @@ -157,7 +157,7 @@ describe('TagsOptionsListUtils', () => { accountID: undefined, }, }; - const largeResultList: OptionsListUtils.CategorySection[] = [ + const largeResultList: OptionsListUtils.Section[] = [ { title: '', shouldShow: true, @@ -259,7 +259,7 @@ describe('TagsOptionsListUtils', () => { ], }, ]; - const largeSearchResultList: OptionsListUtils.CategorySection[] = [ + const largeSearchResultList: OptionsListUtils.Section[] = [ { title: '', shouldShow: true, @@ -285,7 +285,7 @@ describe('TagsOptionsListUtils', () => { ], }, ]; - const largeWrongSearchResultList: OptionsListUtils.CategorySection[] = [ + const largeWrongSearchResultList: OptionsListUtils.Section[] = [ { title: '', shouldShow: true, From 876ff5a6b0319292b9ccce64678dc19e24b40ce1 Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Mon, 18 Nov 2024 22:41:43 +0200 Subject: [PATCH 045/104] revert reauth test --- tests/unit/NetworkTest.ts | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index bb2738416440..e482cc3261d4 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -67,24 +67,34 @@ describe('NetworkTests', () => { }, }); - // And given they are signed in + // Given a test user login and account ID return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN).then(() => { expect(isOffline).toBe(false); - // Set up mocks for the requests + // Mock fetch() so that it throws a TypeError to simulate a bad network connection + global.fetch = jest.fn().mockRejectedValue(new TypeError(CONST.ERROR.FAILED_TO_FETCH)); + + const actualXhr = HttpUtils.xhr; + const mockedXhr = jest.fn(); mockedXhr .mockImplementationOnce(() => - // Given the first request is made with an expired authToken Promise.resolve({ jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, }), ) - // And the call to re-authenticate fails to fetch - .mockImplementationOnce(() => Promise.reject(new Error('Failed to fetch'))) + // Fail the call to re-authenticate + .mockImplementationOnce(actualXhr) + + // The next call should still be using the old authToken + .mockImplementationOnce(() => + Promise.resolve({ + jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, + }), + ) - // And there's another request to Authenticate and it succeeds + // Succeed the call to set a new authToken .mockImplementationOnce(() => Promise.resolve({ jsonCode: CONST.JSON_CODE.SUCCESS, @@ -92,7 +102,7 @@ describe('NetworkTests', () => { }), ) - // And all remaining requests should succeed + // All remaining requests should succeed .mockImplementation(() => Promise.resolve({ jsonCode: CONST.JSON_CODE.SUCCESS, @@ -101,28 +111,24 @@ describe('NetworkTests', () => { HttpUtils.xhr = mockedXhr; - // When the user opens their public profile page + // This should first trigger re-authentication and then a Failed to fetch PersonalDetails.openPublicProfilePage(TEST_USER_ACCOUNT_ID); - - // And the network is back online to process the requests return waitForBatchedUpdates() .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) .then(() => { expect(isOffline).toBe(false); // Advance the network request queue by 1 second so that it can realize it's back online - // And advance past the retry delay - jest.advanceTimersByTime(CONST.NETWORK.PROCESS_REQUEST_DELAY_MS + CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS); + jest.advanceTimersByTime(CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); return waitForBatchedUpdates(); }) .then(() => { - // Then there will have been 2 calls to Authenticate, one for the failed re-authentication and one retry that succeeds + // Then we will eventually have 1 call to OpenPublicProfilePage and 1 calls to Authenticate + const callsToOpenPublicProfilePage = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'OpenPublicProfilePage'); const callsToAuthenticate = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'Authenticate'); - expect(callsToAuthenticate.length).toBe(2); - // And two calls to openPublicProfilePage, one with the expired token and one after re-authentication - const callsToOpenPublicProfilePage = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'OpenPublicProfilePage'); expect(callsToOpenPublicProfilePage.length).toBe(1); + expect(callsToAuthenticate.length).toBe(1); }); }); }); From 80d634b4a2f10c4063ea7f5002c7eedda210a12b Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Mon, 18 Nov 2024 22:56:07 +0200 Subject: [PATCH 046/104] revert RequestThrottle to be function based --- src/libs/Middleware/Reauthentication.ts | 7 +-- src/libs/Network/SequentialQueue.ts | 12 ++--- src/libs/RequestThrottle.ts | 65 ++++++++++++------------- tests/unit/APITest.ts | 9 ++-- 4 files changed, 42 insertions(+), 51 deletions(-) diff --git a/src/libs/Middleware/Reauthentication.ts b/src/libs/Middleware/Reauthentication.ts index 859dfa01697a..87f1d3d68034 100644 --- a/src/libs/Middleware/Reauthentication.ts +++ b/src/libs/Middleware/Reauthentication.ts @@ -7,16 +7,14 @@ import * as NetworkStore from '@libs/Network/NetworkStore'; import type {RequestError} from '@libs/Network/SequentialQueue'; import NetworkConnection from '@libs/NetworkConnection'; import * as Request from '@libs/Request'; -import RequestThrottle from '@libs/RequestThrottle'; import CONST from '@src/CONST'; +import * as RequestThrottle from '@src/libs/RequestThrottle'; import ONYXKEYS from '@src/ONYXKEYS'; import type Middleware from './types'; // We store a reference to the active authentication request so that we are only ever making one request to authenticate at a time. let isAuthenticating: Promise | null = null; -const reauthThrottle = new RequestThrottle(); - function reauthenticate(commandName?: string): Promise { if (isAuthenticating) { return isAuthenticating; @@ -46,8 +44,7 @@ function reauthenticate(commandName?: string): Promise { function retryReauthenticate(commandName?: string): Promise { return Authentication.reauthenticate(commandName).catch((error: RequestError) => { - return reauthThrottle - .sleep(error, 'Authenticate') + return RequestThrottle.sleep(error, 'Authenticate') .then(() => retryReauthenticate(commandName)) .catch(() => { NetworkStore.setIsAuthenticating(false); diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index ec07d315a608..cdf9b075ec44 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -2,10 +2,10 @@ import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import Log from '@libs/Log'; import * as Request from '@libs/Request'; -import RequestThrottle from '@libs/RequestThrottle'; import * as PersistedRequests from '@userActions/PersistedRequests'; import * as QueuedOnyxUpdates from '@userActions/QueuedOnyxUpdates'; import CONST from '@src/CONST'; +import * as RequestThrottle from '@src/libs/RequestThrottle'; import ONYXKEYS from '@src/ONYXKEYS'; import type OnyxRequest from '@src/types/onyx/Request'; import type {ConflictData} from '@src/types/onyx/Request'; @@ -28,7 +28,6 @@ resolveIsReadyPromise?.(); let isSequentialQueueRunning = false; let currentRequestPromise: Promise | null = null; let isQueuePaused = false; -const requestThrottle = new RequestThrottle(); /** * Puts the queue into a paused state so that no requests will be processed @@ -100,7 +99,7 @@ function process(): Promise { Log.info('[SequentialQueue] Removing persisted request because it was processed successfully.', false, {request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - requestThrottle.clear(); + RequestThrottle.clear(); return process(); }) .catch((error: RequestError) => { @@ -109,18 +108,17 @@ function process(): Promise { if (error.name === CONST.ERROR.REQUEST_CANCELLED || error.message === CONST.ERROR.DUPLICATE_RECORD) { Log.info("[SequentialQueue] Removing persisted request because it failed and doesn't need to be retried.", false, {error, request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - requestThrottle.clear(); + RequestThrottle.clear(); return process(); } PersistedRequests.rollbackOngoingRequest(); - return requestThrottle - .sleep(error, requestToProcess.command) + return RequestThrottle.sleep(error, requestToProcess.command) .then(process) .catch(() => { Onyx.update(requestToProcess.failureData ?? []); Log.info('[SequentialQueue] Removing persisted request because it failed too many times.', false, {error, request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - requestThrottle.clear(); + RequestThrottle.clear(); return process(); }); }); diff --git a/src/libs/RequestThrottle.ts b/src/libs/RequestThrottle.ts index 8a6673c22a92..3bbc82ff5b45 100644 --- a/src/libs/RequestThrottle.ts +++ b/src/libs/RequestThrottle.ts @@ -3,44 +3,41 @@ import Log from './Log'; import type {RequestError} from './Network/SequentialQueue'; import {generateRandomInt} from './NumberUtils'; -class RequestThrottle { - private requestWaitTime = 0; +let requestWaitTime = 0; +let requestRetryCount = 0; - private requestRetryCount = 0; +function clear() { + requestWaitTime = 0; + requestRetryCount = 0; + Log.info(`[RequestThrottle] in clear()`); +} - clear() { - this.requestWaitTime = 0; - this.requestRetryCount = 0; - Log.info(`[RequestThrottle] in clear()`); - } - - getRequestWaitTime() { - if (this.requestWaitTime) { - this.requestWaitTime = Math.min(this.requestWaitTime * 2, CONST.NETWORK.MAX_RETRY_WAIT_TIME_MS); - } else { - this.requestWaitTime = generateRandomInt(CONST.NETWORK.MIN_RETRY_WAIT_TIME_MS, CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS); - } - return this.requestWaitTime; +function getRequestWaitTime() { + if (requestWaitTime) { + requestWaitTime = Math.min(requestWaitTime * 2, CONST.NETWORK.MAX_RETRY_WAIT_TIME_MS); + } else { + requestWaitTime = generateRandomInt(CONST.NETWORK.MIN_RETRY_WAIT_TIME_MS, CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS); } + return requestWaitTime; +} - getLastRequestWaitTime() { - return this.requestWaitTime; - } +function getLastRequestWaitTime() { + return requestWaitTime; +} - sleep(error: RequestError, command: string): Promise { - this.requestRetryCount++; - return new Promise((resolve, reject) => { - if (this.requestRetryCount <= CONST.NETWORK.MAX_REQUEST_RETRIES) { - const currentRequestWaitTime = this.getRequestWaitTime(); - Log.info( - `[RequestThrottle] Retrying request after error: '${error.name}', '${error.message}', '${error.status}'. Command: ${command}. Retry count: ${this.requestRetryCount}. Wait time: ${currentRequestWaitTime}`, - ); - setTimeout(resolve, currentRequestWaitTime); - } else { - reject(); - } - }); - } +function sleep(error: RequestError, command: string): Promise { + requestRetryCount++; + return new Promise((resolve, reject) => { + if (requestRetryCount <= CONST.NETWORK.MAX_REQUEST_RETRIES) { + const currentRequestWaitTime = getRequestWaitTime(); + Log.info( + `[RequestThrottle] Retrying request after error: '${error.name}', '${error.message}', '${error.status}'. Command: ${command}. Retry count: ${requestRetryCount}. Wait time: ${currentRequestWaitTime}`, + ); + setTimeout(resolve, currentRequestWaitTime); + return; + } + reject(); + }); } -export default RequestThrottle; +export {clear, getRequestWaitTime, sleep, getLastRequestWaitTime}; diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index bc4b650fb6e5..5b6947861dbe 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -9,7 +9,7 @@ import * as MainQueue from '@src/libs/Network/MainQueue'; import * as NetworkStore from '@src/libs/Network/NetworkStore'; import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; import * as Request from '@src/libs/Request'; -import RequestThrottle from '@src/libs/RequestThrottle'; +import * as RequestThrottle from '@src/libs/RequestThrottle'; import ONYXKEYS from '@src/ONYXKEYS'; import type ReactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; @@ -39,7 +39,6 @@ type XhrCalls = Array<{ }>; const originalXHR = HttpUtils.xhr; -const requestThrottle = new RequestThrottle(); beforeEach(() => { global.fetch = TestHelper.getGlobalFetchMock(); @@ -48,7 +47,7 @@ beforeEach(() => { MainQueue.clear(); HttpUtils.cancelPendingRequests(); PersistedRequests.clear(); - requestThrottle.clear(); + RequestThrottle.clear(); NetworkStore.checkRequiredData(); // Wait for any Log command to finish and Onyx to fully clear @@ -244,7 +243,7 @@ describe('APITests', () => { // We let the SequentialQueue process again after its wait time return new Promise((resolve) => { - setTimeout(resolve, requestThrottle.getLastRequestWaitTime()); + setTimeout(resolve, RequestThrottle.getLastRequestWaitTime()); }); }) .then(() => { @@ -257,7 +256,7 @@ describe('APITests', () => { // We let the SequentialQueue process again after its wait time return new Promise((resolve) => { - setTimeout(resolve, requestThrottle.getLastRequestWaitTime()); + setTimeout(resolve, RequestThrottle.getLastRequestWaitTime()); }).then(waitForBatchedUpdates); }) .then(() => { From ff16617bf4b322d0719c592f9671e388276f4fa1 Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Mon, 18 Nov 2024 23:24:58 +0200 Subject: [PATCH 047/104] fix flaky network tests --- src/libs/Middleware/Reauthentication.ts | 6 ++++- src/libs/Network/SequentialQueue.ts | 12 +++++++++- src/libs/Network/index.ts | 30 ++++++++++--------------- tests/unit/NetworkTest.ts | 26 ++++++++++++++++----- 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/libs/Middleware/Reauthentication.ts b/src/libs/Middleware/Reauthentication.ts index 87f1d3d68034..29df3b7f0b50 100644 --- a/src/libs/Middleware/Reauthentication.ts +++ b/src/libs/Middleware/Reauthentication.ts @@ -54,6 +54,10 @@ function retryReauthenticate(commandName?: string): Promise { }); } +function resetReauthentication(): void { + isAuthenticating = null; +} + const Reauthentication: Middleware = (response, request, isFromSequentialQueue) => response .then((data) => { @@ -144,4 +148,4 @@ const Reauthentication: Middleware = (response, request, isFromSequentialQueue) }); export default Reauthentication; -export {reauthenticate}; +export {reauthenticate, resetReauthentication}; diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index cdf9b075ec44..68c1be0f32a5 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -271,5 +271,15 @@ function waitForIdle(): Promise { return isReadyPromise; } -export {flush, getCurrentRequest, isRunning, isPaused, push, waitForIdle, pause, unpause, process}; +function resetQueue(): void { + isSequentialQueueRunning = false; + currentRequestPromise = null; + isQueuePaused = false; + isReadyPromise = new Promise((resolve) => { + resolveIsReadyPromise = resolve; + }); + resolveIsReadyPromise?.(); +} + +export {flush, getCurrentRequest, isRunning, isPaused, push, waitForIdle, pause, unpause, process, resetQueue}; export type {RequestError}; diff --git a/src/libs/Network/index.ts b/src/libs/Network/index.ts index 668a7038e706..0ad415b74048 100644 --- a/src/libs/Network/index.ts +++ b/src/libs/Network/index.ts @@ -1,33 +1,30 @@ -import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; // import {reauthenticate} from '@libs/Middleware/Reauthentication'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {Request} from '@src/types/onyx'; import type Response from '@src/types/onyx/Response'; import pkg from '../../../package.json'; import * as MainQueue from './MainQueue'; import * as SequentialQueue from './SequentialQueue'; +let processQueueInterval: NodeJS.Timer; + // We must wait until the ActiveClientManager is ready so that we ensure only the "leader" tab processes any persisted requests ActiveClientManager.isReady().then(() => { SequentialQueue.flush(); // Start main queue and process once every n ms delay - setInterval(MainQueue.process, CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); - - // If a reauthentication request is set make sure it is processed - // Onyx.connect({ - // key: ONYXKEYS.REAUTHENTICATION_REQUEST, - // callback: (request) => { - // if (!request) { - // return; - // } - // // reauthenticate(request.commandName); - // }, - // }); + processQueueInterval = setInterval(MainQueue.process, CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); }); +// Clear interval +function clearProcessQueueInterval() { + if (!processQueueInterval) { + return; + } + clearInterval(processQueueInterval as unknown as number); +} + /** * Perform a queued post request */ @@ -69,7 +66,4 @@ function post(command: string, data: Record = {}, type = CONST. }); } -export { - // eslint-disable-next-line import/prefer-default-export - post, -}; +export {post, clearProcessQueueInterval}; diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index e482cc3261d4..b403e2f274e6 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -2,6 +2,7 @@ import type {Mock} from 'jest-mock'; import type {OnyxEntry} from 'react-native-onyx'; import MockedOnyx from 'react-native-onyx'; import * as App from '@libs/actions/App'; +import {resetReauthentication} from '@libs/Middleware/Reauthentication'; import CONST from '@src/CONST'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import * as PersistedRequests from '@src/libs/actions/PersistedRequests'; @@ -12,6 +13,7 @@ import Log from '@src/libs/Log'; import * as Network from '@src/libs/Network'; import * as MainQueue from '@src/libs/Network/MainQueue'; import * as NetworkStore from '@src/libs/Network/NetworkStore'; +import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; import NetworkConnection from '@src/libs/NetworkConnection'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Session as OnyxSession} from '@src/types/onyx'; @@ -35,15 +37,27 @@ const originalXHR = HttpUtils.xhr; beforeEach(() => { global.fetch = TestHelper.getGlobalFetchMock(); HttpUtils.xhr = originalXHR; + + // Reset any pending requests MainQueue.clear(); HttpUtils.cancelPendingRequests(); NetworkStore.checkRequiredData(); - - // Wait for any Log command to finish and Onyx to fully clear - return waitForBatchedUpdates() - .then(() => PersistedRequests.clear()) - .then(() => Onyx.clear()) - .then(waitForBatchedUpdates); + NetworkStore.setIsAuthenticating(false); + resetReauthentication(); + Network.clearProcessQueueInterval(); + SequentialQueue.resetQueue(); + + return Promise.all([SequentialQueue.waitForIdle(), waitForBatchedUpdates(), PersistedRequests.clear(), Onyx.clear()]) + .then(() => { + return waitForBatchedUpdates(); + }) + .then( + () => + // Wait for all async operations to complete + new Promise((resolve) => { + setTimeout(resolve, 100); + }), + ); }); afterEach(() => { From c9cb455fd60ed449728431f7ed3b0ee75cab5961 Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Mon, 18 Nov 2024 23:34:02 +0200 Subject: [PATCH 048/104] add improved reauth while offline test --- tests/unit/NetworkTest.ts | 93 +++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index b403e2f274e6..9607487b9e93 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -17,6 +17,7 @@ import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; import NetworkConnection from '@src/libs/NetworkConnection'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Session as OnyxSession} from '@src/types/onyx'; +import type OnyxNetwork from '@src/types/onyx/Network'; import type ReactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -148,77 +149,91 @@ describe('NetworkTests', () => { }); test('failing to reauthenticate while offline should not log out user', async () => { + // 1. Setup Phase - Initialize test user and state variables const TEST_USER_LOGIN = 'test@testguy.com'; const TEST_USER_ACCOUNT_ID = 1; + const defaultTimeout = 1000; - let session: OnyxEntry; + let sessionState: OnyxEntry; + let networkState: OnyxEntry; + + // Set up listeners for session and network state changes Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => (session = val), + callback: (val) => (sessionState = val), }); Onyx.connect({ key: ONYXKEYS.NETWORK, + callback: (val) => (networkState = val), }); + // Sign in test user and wait for updates await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); await waitForBatchedUpdates(); - expect(session?.authToken).not.toBeUndefined(); - - // Turn off the network - await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + const initialAuthToken = sessionState?.authToken; + expect(initialAuthToken).toBeDefined(); - const mockedXhr = jest.fn(); - mockedXhr - // Call ReconnectApp with an expired token + // 2. Mock Setup Phase - Configure XHR mocks for the test sequence + const mockedXhr = jest + .fn() + // First mock: ReconnectApp will fail with NOT_AUTHENTICATED .mockImplementationOnce(() => Promise.resolve({ jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, }), ) - // Call Authenticate - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.SUCCESS, - authToken: 'newAuthToken', - }), - ) - // Call ReconnectApp again, it should connect with a new token - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.SUCCESS, - }), - ); + // Second mock: Authenticate with network check and delay + .mockImplementationOnce(() => { + // Check network state immediately + if (networkState?.isOffline) { + return Promise.reject(new Error('Network request failed')); + } + + // create a delayed response. Timeout will expire after the second App.reconnectApp(); + return new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Network request failed')); + }, defaultTimeout); + }); + }); HttpUtils.xhr = mockedXhr; - // Initiate the requests + // 3. Test Execution Phase - Start with online network + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); + + // Trigger reconnect which will fail due to expired token App.confirmReadyToOpenApp(); App.reconnectApp(); await waitForBatchedUpdates(); - // Turn the network back online - await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - - // Filter requests results by request name - const reconnectResults = (HttpUtils.xhr as Mock).mock.results.filter((_, index) => (HttpUtils.xhr as Mock)?.mock?.calls?.at(index)?.[0] === 'ReconnectApp'); - const authenticateResults = (HttpUtils.xhr as Mock).mock.results.filter((_, index) => (HttpUtils.xhr as Mock)?.mock?.calls?.at(index)?.[0] === 'Authenticate'); + // 4. First API Call Verification - Check ReconnectApp + const firstCall = mockedXhr.mock.calls.at(0) as [string, Record]; + expect(firstCall[0]).toBe('ReconnectApp'); - // Get the response code of Authenticate call - const authenticateResponse = await (authenticateResults?.at(0)?.value as Promise<{jsonCode: string}>); + // 5. Authentication Start - Verify authenticate was triggered + await waitForBatchedUpdates(); + const secondCall = mockedXhr.mock.calls.at(1) as [string, Record]; + expect(secondCall[0]).toBe('Authenticate'); - // Get the response code of the second Reconnect call - const reconnectResponse = await (reconnectResults?.at(1)?.value as Promise<{jsonCode: string}>); + // 6. Network State Change - Set offline and back online while authenticate is pending + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - // Authenticate request should return 200 - expect(authenticateResponse.jsonCode).toBe(CONST.JSON_CODE.SUCCESS); + // Trigger another reconnect due to network change + App.confirmReadyToOpenApp(); + App.reconnectApp(); + await waitForBatchedUpdates(); - // The second ReconnectApp should return 200 - expect(reconnectResponse.jsonCode).toBe(CONST.JSON_CODE.SUCCESS); + // 7. Wait and Verify - Wait for authenticate timeout and verify session + await new Promise((resolve) => { + setTimeout(resolve, defaultTimeout + 100); + }); - // check if the user is still logged in - expect(session?.authToken).not.toBeUndefined(); + // Verify the session remained intact and wasn't cleared + expect(sessionState?.authToken).toBe(initialAuthToken); }); test('consecutive API calls eventually succeed when authToken is expired', () => { From db950710410ab8da968da496c0a87ad988070110 Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Mon, 18 Nov 2024 23:43:45 +0200 Subject: [PATCH 049/104] cleanup code --- src/libs/Network/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/Network/index.ts b/src/libs/Network/index.ts index 0ad415b74048..d91ea8c02553 100644 --- a/src/libs/Network/index.ts +++ b/src/libs/Network/index.ts @@ -1,5 +1,4 @@ import * as ActiveClientManager from '@libs/ActiveClientManager'; -// import {reauthenticate} from '@libs/Middleware/Reauthentication'; import CONST from '@src/CONST'; import type {Request} from '@src/types/onyx'; import type Response from '@src/types/onyx/Response'; From 7b876d3ed171a7bfc25d5739f4f59ad13aa7ed18 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 19 Nov 2024 10:19:57 +0700 Subject: [PATCH 050/104] address comment --- .../settings/Wallet/Card/BaseGetPhysicalCard.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index f7c8ff60be36..269f0b1c52d7 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -102,8 +102,7 @@ function BaseGetPhysicalCard({ const [formData] = useOnyx(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM); const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); - const cardID = cardToBeIssued?.cardID.toString() ?? '-1'; - const [currentCardID, setCurrentCardID] = useState(cardID); + const [currentCardID, setCurrentCardID] = useState(cardToBeIssued?.cardID.toString() ?? '-1'); const errorMessage = ErrorUtils.getLatestErrorMessageField(cardToBeIssued); useEffect(() => { @@ -140,8 +139,7 @@ function BaseGetPhysicalCard({ useEffect(() => { // Current step of the get physical card flow should be the confirmation page; and // Card has NOT_ACTIVATED state when successfully being issued so cardToBeIssued should be undefined - // -1 is not a valid cardID, we don't need to clean up the form value in that case. - if (!isConfirmation || !!cardToBeIssued || !currentCardID || currentCardID === '-1') { + if (!isConfirmation || !!cardToBeIssued || !currentCardID) { return; } @@ -149,7 +147,7 @@ function BaseGetPhysicalCard({ // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); Wallet.clearPhysicalCardError(currentCardID); - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(currentCardID.toString())); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(currentCardID)); setCurrentCardID(undefined); }, [currentCardID, isConfirmation, cardToBeIssued]); @@ -179,7 +177,12 @@ function BaseGetPhysicalCard({ > Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID))} + onBackButtonPress={() => { + if (currentCardID) { + Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(currentCardID)); + } + Navigation.goBack(); + }} /> {headline} {renderContent({onSubmit, submitButtonText, children, onValidate})} From 4d963845af22163a841c6fc271154bc11491f77b Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 19 Nov 2024 10:20:59 +0700 Subject: [PATCH 051/104] remove -1 --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 269f0b1c52d7..eef5024180e7 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -102,7 +102,7 @@ function BaseGetPhysicalCard({ const [formData] = useOnyx(ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM); const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); - const [currentCardID, setCurrentCardID] = useState(cardToBeIssued?.cardID.toString() ?? '-1'); + const [currentCardID, setCurrentCardID] = useState(cardToBeIssued?.cardID.toString()); const errorMessage = ErrorUtils.getLatestErrorMessageField(cardToBeIssued); useEffect(() => { From 2a656a8caa4cdc913bfba6c87a94efa4ac399cc6 Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Tue, 19 Nov 2024 13:21:23 +0200 Subject: [PATCH 052/104] fix typo --- tests/actions/ReportTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index c279079b995b..1cd17e33829d 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -55,7 +55,7 @@ describe('actions/Report', () => { const promise = Onyx.clear().then(jest.useRealTimers); if (getIsUsingFakeTimers()) { // flushing pending timers - // Onyx.clear() promise is resolved in batch which happends after the current microtasks cycle + // Onyx.clear() promise is resolved in batch which happens after the current microtasks cycle setImmediate(jest.runOnlyPendingTimers); } From cc5162440b8ad0c86edc76bc5a26bfc354b6ea90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 19 Nov 2024 16:45:54 +0100 Subject: [PATCH 053/104] use Section --- tests/unit/TaxOptionsListUtilsTest.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/TaxOptionsListUtilsTest.ts b/tests/unit/TaxOptionsListUtilsTest.ts index 255cedd7c7d5..f0e1eac4826a 100644 --- a/tests/unit/TaxOptionsListUtilsTest.ts +++ b/tests/unit/TaxOptionsListUtilsTest.ts @@ -1,4 +1,4 @@ -import type {CategorySection} from '@libs/OptionsListUtils'; +import type {Section} from '@libs/OptionsListUtils'; import * as TaxOptionsListUtils from '@libs/TaxOptionsListUtils'; import type {Policy, TaxRatesWithDefault, Transaction} from '@src/types/onyx'; @@ -45,7 +45,7 @@ describe('TaxOptionsListUtils', () => { taxCode: 'CODE1', } as Transaction; - const resultList: CategorySection[] = [ + const resultList: Section[] = [ { data: [ { @@ -84,7 +84,7 @@ describe('TaxOptionsListUtils', () => { }, ]; - const searchResultList: CategorySection[] = [ + const searchResultList: Section[] = [ { data: [ { @@ -103,7 +103,7 @@ describe('TaxOptionsListUtils', () => { }, ]; - const wrongSearchResultList: CategorySection[] = [ + const wrongSearchResultList: Section[] = [ { data: [], shouldShow: true, From b343200916919c64ec6156634c21b0bd4d578e4e Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 20 Nov 2024 09:05:05 +0700 Subject: [PATCH 054/104] prettier --- .../ValidateCodeActionModal/ValidateCodeForm/index.android.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/index.android.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/index.android.tsx index 2d36965e7412..08c2b087b7d5 100644 --- a/src/components/ValidateCodeActionModal/ValidateCodeForm/index.android.tsx +++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/index.android.tsx @@ -1,7 +1,7 @@ import React, {forwardRef} from 'react'; +import {gestureHandlerRootHOC} from 'react-native-gesture-handler'; import BaseValidateCodeForm from './BaseValidateCodeForm'; import type {ValidateCodeFormHandle, ValidateCodeFormProps} from './BaseValidateCodeForm'; -import { gestureHandlerRootHOC } from 'react-native-gesture-handler'; const ValidateCodeForm = forwardRef((props, ref) => ( Date: Wed, 20 Nov 2024 12:30:56 +0700 Subject: [PATCH 055/104] resolve conflict --- tests/actions/IOUTest.ts | 345 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 28004d3dc7a5..7a8284b4c3b8 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -1683,6 +1683,351 @@ describe('actions/IOU', () => { }); }); + describe('edit expense', () => { + const amount = 10000; + const comment = '💸💸💸💸'; + const merchant = 'NASDAQ'; + + afterEach(() => { + mockFetch?.resume?.(); + }); + + it('updates the IOU request and IOU report when offline', () => { + let thread: OptimisticChatReport; + let iouReport: OnyxEntry; + let iouAction: OnyxEntry>; + let transaction: OnyxEntry; + + mockFetch?.pause?.(); + IOU.requestMoney( + { + report: {reportID: ''}, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant, + comment, + }, + ); + return waitForBatchedUpdates() + .then(() => { + Onyx.set(ONYXKEYS.SESSION, {email: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connection); + iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); + + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connection); + + [iouAction] = Object.values(reportActionsForIOUReport ?? {}).filter( + (reportAction): reportAction is OnyxTypes.ReportAction => ReportActionsUtils.isMoneyRequestAction(reportAction), + ); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connection); + + transaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); + resolve(); + }, + }); + }), + ) + .then(() => { + thread = ReportUtils.buildTransactionThread(iouAction, iouReport) ?? null; + Onyx.set(`report_${thread?.reportID ?? '-1'}`, thread); + return waitForBatchedUpdates(); + }) + .then(() => { + if (transaction) { + IOU.editMoneyRequest( + transaction, + thread.reportID, + {amount: 20000, comment: 'Double the amount!'}, + { + id: '123', + role: 'user', + type: CONST.POLICY.TYPE.TEAM, + name: '', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, + }, + {}, + {}, + ); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connection); + + const updatedTransaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); + expect(updatedTransaction?.modifiedAmount).toBe(20000); + expect(updatedTransaction?.comment).toMatchObject({comment: 'Double the amount!'}); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (allActions) => { + Onyx.disconnect(connection); + const updatedAction = Object.values(allActions ?? {}).find((reportAction) => !isEmptyObject(reportAction)); + expect(updatedAction?.actionName).toEqual('MODIFIEDEXPENSE'); + expect(updatedAction && ReportActionsUtils.getOriginalMessage(updatedAction)).toEqual( + expect.objectContaining({amount: 20000, newComment: 'Double the amount!', oldAmount: amount, oldComment: comment}), + ); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connection); + const updatedIOUReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); + const updatedChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === iouReport?.chatReportID); + expect(updatedIOUReport).toEqual( + expect.objectContaining({ + total: 20000, + cachedTotal: '$200.00', + lastMessageHtml: 'submitted $200.00', + lastMessageText: 'submitted $200.00', + }), + ); + expect(updatedChatReport).toEqual( + expect.objectContaining({ + lastMessageHtml: `${CARLOS_EMAIL} owes $200.00`, + lastMessageText: `${CARLOS_EMAIL} owes $200.00`, + }), + ); + resolve(); + }, + }); + }), + ) + .then(() => { + mockFetch?.resume?.(); + }); + }); + + it('resets the IOU request and IOU report when api returns an error', () => { + let thread: OptimisticChatReport; + let iouReport: OnyxEntry; + let iouAction: OnyxEntry>; + let transaction: OnyxEntry; + + IOU.requestMoney( + { + report: {reportID: ''}, + payeeEmail: RORY_EMAIL, + payeeAccountID: RORY_ACCOUNT_ID, + participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, + }, + { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant, + comment, + }, + ); + return waitForBatchedUpdates() + .then(() => { + Onyx.set(ONYXKEYS.SESSION, {email: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}); + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connection); + [iouReport] = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.IOU); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForIOUReport) => { + Onyx.disconnect(connection); + + [iouAction] = Object.values(reportActionsForIOUReport ?? {}).filter( + (reportAction): reportAction is OnyxTypes.ReportAction => ReportActionsUtils.isMoneyRequestAction(reportAction), + ); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connection); + + transaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); + resolve(); + }, + }); + }), + ) + .then(() => { + thread = ReportUtils.buildTransactionThread(iouAction, iouReport); + Onyx.set(`report_${thread.reportID}`, thread); + return waitForBatchedUpdates(); + }) + .then(() => { + mockFetch?.fail?.(); + + if (transaction) { + IOU.editMoneyRequest( + transaction, + thread.reportID, + {amount: 20000, comment: 'Double the amount!'}, + { + id: '123', + role: 'user', + type: CONST.POLICY.TYPE.TEAM, + name: '', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, + }, + {}, + {}, + ); + } + return waitForBatchedUpdates(); + }) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connection); + + const updatedTransaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); + expect(updatedTransaction?.modifiedAmount).toBe(undefined); + expect(updatedTransaction?.amount).toBe(10000); + expect(updatedTransaction?.comment).toMatchObject({comment}); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (allActions) => { + Onyx.disconnect(connection); + const updatedAction = Object.values(allActions ?? {}).find((reportAction) => !isEmptyObject(reportAction)); + expect(updatedAction?.actionName).toEqual('MODIFIEDEXPENSE'); + expect(Object.values(updatedAction?.errors ?? {}).at(0)).toEqual(Localize.translateLocal('iou.error.genericEditFailureMessage')); + resolve(); + }, + }); + }), + ) + .then( + () => + new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connection); + const updatedIOUReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); + const updatedChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === iouReport?.chatReportID); + expect(updatedIOUReport).toEqual( + expect.objectContaining({ + total: 10000, + cachedTotal: '$100.00', + lastMessageHtml: `submitted $${amount / 100}.00 for ${comment}`, + lastMessageText: `submitted $${amount / 100}.00 for ${comment}`, + }), + ); + expect(updatedChatReport).toEqual( + expect.objectContaining({ + lastMessageHtml: '', + }), + ); + resolve(); + }, + }); + }), + ); + }); + }); + describe('pay expense report via ACH', () => { const amount = 10000; const comment = '💸💸💸💸'; From b5ad2c19f9176ac7022d198c7f622304907c28d0 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 20 Nov 2024 12:35:47 +0700 Subject: [PATCH 056/104] remove change --- tests/actions/IOUTest.ts | 345 --------------------------------------- 1 file changed, 345 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 7a8284b4c3b8..28004d3dc7a5 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -1683,351 +1683,6 @@ describe('actions/IOU', () => { }); }); - describe('edit expense', () => { - const amount = 10000; - const comment = '💸💸💸💸'; - const merchant = 'NASDAQ'; - - afterEach(() => { - mockFetch?.resume?.(); - }); - - it('updates the IOU request and IOU report when offline', () => { - let thread: OptimisticChatReport; - let iouReport: OnyxEntry; - let iouAction: OnyxEntry>; - let transaction: OnyxEntry; - - mockFetch?.pause?.(); - IOU.requestMoney( - { - report: {reportID: ''}, - payeeEmail: RORY_EMAIL, - payeeAccountID: RORY_ACCOUNT_ID, - participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, - }, - { - amount, - attendees: [], - currency: CONST.CURRENCY.USD, - created: '', - merchant, - comment, - }, - ); - return waitForBatchedUpdates() - .then(() => { - Onyx.set(ONYXKEYS.SESSION, {email: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connection); - iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); - - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForIOUReport) => { - Onyx.disconnect(connection); - - [iouAction] = Object.values(reportActionsForIOUReport ?? {}).filter( - (reportAction): reportAction is OnyxTypes.ReportAction => ReportActionsUtils.isMoneyRequestAction(reportAction), - ); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connection); - - transaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); - resolve(); - }, - }); - }), - ) - .then(() => { - thread = ReportUtils.buildTransactionThread(iouAction, iouReport) ?? null; - Onyx.set(`report_${thread?.reportID ?? '-1'}`, thread); - return waitForBatchedUpdates(); - }) - .then(() => { - if (transaction) { - IOU.editMoneyRequest( - transaction, - thread.reportID, - {amount: 20000, comment: 'Double the amount!'}, - { - id: '123', - role: 'user', - type: CONST.POLICY.TYPE.TEAM, - name: '', - owner: '', - outputCurrency: '', - isPolicyExpenseChatEnabled: false, - }, - {}, - {}, - ); - } - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connection); - - const updatedTransaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); - expect(updatedTransaction?.modifiedAmount).toBe(20000); - expect(updatedTransaction?.comment).toMatchObject({comment: 'Double the amount!'}); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, - waitForCollectionCallback: false, - callback: (allActions) => { - Onyx.disconnect(connection); - const updatedAction = Object.values(allActions ?? {}).find((reportAction) => !isEmptyObject(reportAction)); - expect(updatedAction?.actionName).toEqual('MODIFIEDEXPENSE'); - expect(updatedAction && ReportActionsUtils.getOriginalMessage(updatedAction)).toEqual( - expect.objectContaining({amount: 20000, newComment: 'Double the amount!', oldAmount: amount, oldComment: comment}), - ); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connection); - const updatedIOUReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); - const updatedChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === iouReport?.chatReportID); - expect(updatedIOUReport).toEqual( - expect.objectContaining({ - total: 20000, - cachedTotal: '$200.00', - lastMessageHtml: 'submitted $200.00', - lastMessageText: 'submitted $200.00', - }), - ); - expect(updatedChatReport).toEqual( - expect.objectContaining({ - lastMessageHtml: `${CARLOS_EMAIL} owes $200.00`, - lastMessageText: `${CARLOS_EMAIL} owes $200.00`, - }), - ); - resolve(); - }, - }); - }), - ) - .then(() => { - mockFetch?.resume?.(); - }); - }); - - it('resets the IOU request and IOU report when api returns an error', () => { - let thread: OptimisticChatReport; - let iouReport: OnyxEntry; - let iouAction: OnyxEntry>; - let transaction: OnyxEntry; - - IOU.requestMoney( - { - report: {reportID: ''}, - payeeEmail: RORY_EMAIL, - payeeAccountID: RORY_ACCOUNT_ID, - participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, - }, - { - amount, - attendees: [], - currency: CONST.CURRENCY.USD, - created: '', - merchant, - comment, - }, - ); - return waitForBatchedUpdates() - .then(() => { - Onyx.set(ONYXKEYS.SESSION, {email: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}); - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connection); - [iouReport] = Object.values(allReports ?? {}).filter((report) => report?.type === CONST.REPORT.TYPE.IOU); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForIOUReport) => { - Onyx.disconnect(connection); - - [iouAction] = Object.values(reportActionsForIOUReport ?? {}).filter( - (reportAction): reportAction is OnyxTypes.ReportAction => ReportActionsUtils.isMoneyRequestAction(reportAction), - ); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connection); - - transaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); - resolve(); - }, - }); - }), - ) - .then(() => { - thread = ReportUtils.buildTransactionThread(iouAction, iouReport); - Onyx.set(`report_${thread.reportID}`, thread); - return waitForBatchedUpdates(); - }) - .then(() => { - mockFetch?.fail?.(); - - if (transaction) { - IOU.editMoneyRequest( - transaction, - thread.reportID, - {amount: 20000, comment: 'Double the amount!'}, - { - id: '123', - role: 'user', - type: CONST.POLICY.TYPE.TEAM, - name: '', - owner: '', - outputCurrency: '', - isPolicyExpenseChatEnabled: false, - }, - {}, - {}, - ); - } - return waitForBatchedUpdates(); - }) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (allTransactions) => { - Onyx.disconnect(connection); - - const updatedTransaction = Object.values(allTransactions ?? {}).find((t) => !isEmptyObject(t)); - expect(updatedTransaction?.modifiedAmount).toBe(undefined); - expect(updatedTransaction?.amount).toBe(10000); - expect(updatedTransaction?.comment).toMatchObject({comment}); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, - waitForCollectionCallback: false, - callback: (allActions) => { - Onyx.disconnect(connection); - const updatedAction = Object.values(allActions ?? {}).find((reportAction) => !isEmptyObject(reportAction)); - expect(updatedAction?.actionName).toEqual('MODIFIEDEXPENSE'); - expect(Object.values(updatedAction?.errors ?? {}).at(0)).toEqual(Localize.translateLocal('iou.error.genericEditFailureMessage')); - resolve(); - }, - }); - }), - ) - .then( - () => - new Promise((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (allReports) => { - Onyx.disconnect(connection); - const updatedIOUReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); - const updatedChatReport = Object.values(allReports ?? {}).find((report) => report?.reportID === iouReport?.chatReportID); - expect(updatedIOUReport).toEqual( - expect.objectContaining({ - total: 10000, - cachedTotal: '$100.00', - lastMessageHtml: `submitted $${amount / 100}.00 for ${comment}`, - lastMessageText: `submitted $${amount / 100}.00 for ${comment}`, - }), - ); - expect(updatedChatReport).toEqual( - expect.objectContaining({ - lastMessageHtml: '', - }), - ); - resolve(); - }, - }); - }), - ); - }); - }); - describe('pay expense report via ACH', () => { const amount = 10000; const comment = '💸💸💸💸'; From d5ace1f07c5c9ee61518cda384074e0a54861462 Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Wed, 20 Nov 2024 11:58:55 +0200 Subject: [PATCH 057/104] remove redundant code --- tests/unit/NetworkTest.ts | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index 9607487b9e93..96a7f08e92f1 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -17,7 +17,6 @@ import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; import NetworkConnection from '@src/libs/NetworkConnection'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Session as OnyxSession} from '@src/types/onyx'; -import type OnyxNetwork from '@src/types/onyx/Network'; import type ReactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -48,17 +47,9 @@ beforeEach(() => { Network.clearProcessQueueInterval(); SequentialQueue.resetQueue(); - return Promise.all([SequentialQueue.waitForIdle(), waitForBatchedUpdates(), PersistedRequests.clear(), Onyx.clear()]) - .then(() => { - return waitForBatchedUpdates(); - }) - .then( - () => - // Wait for all async operations to complete - new Promise((resolve) => { - setTimeout(resolve, 100); - }), - ); + return Promise.all([SequentialQueue.waitForIdle(), waitForBatchedUpdates(), PersistedRequests.clear(), Onyx.clear()]).then(() => { + return waitForBatchedUpdates(); + }); }); afterEach(() => { @@ -155,7 +146,6 @@ describe('NetworkTests', () => { const defaultTimeout = 1000; let sessionState: OnyxEntry; - let networkState: OnyxEntry; // Set up listeners for session and network state changes Onyx.connect({ @@ -163,11 +153,6 @@ describe('NetworkTests', () => { callback: (val) => (sessionState = val), }); - Onyx.connect({ - key: ONYXKEYS.NETWORK, - callback: (val) => (networkState = val), - }); - // Sign in test user and wait for updates await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); await waitForBatchedUpdates(); @@ -186,11 +171,6 @@ describe('NetworkTests', () => { ) // Second mock: Authenticate with network check and delay .mockImplementationOnce(() => { - // Check network state immediately - if (networkState?.isOffline) { - return Promise.reject(new Error('Network request failed')); - } - // create a delayed response. Timeout will expire after the second App.reconnectApp(); return new Promise((_, reject) => { setTimeout(() => { From 1ce059b4d41346c6e1acf716cab2d114b1800125 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 20 Nov 2024 19:25:25 +0800 Subject: [PATCH 058/104] fix new message shows when creating a new expense --- src/libs/ReportUtils.ts | 1 + src/pages/home/report/ReportActionsList.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 373a861f7c2e..d40cdf8aea8a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5172,6 +5172,7 @@ function buildOptimisticReportPreview( actorAccountID: hasReceipt ? currentUserAccountID : reportActorAccountID, childReportID: childReportID ?? iouReport?.reportID, childMoneyRequestCount: 1, + childLastActorAccountID: currentUserAccountID, childLastMoneyRequestComment: comment, childRecentReceiptTransactionIDs: hasReceipt && !isEmptyObject(transaction) ? {[transaction?.transactionID ?? '-1']: created} : undefined, }; diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 8ddea8e7e940..100585b6f818 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -281,7 +281,7 @@ function ReportActionsList({ } // If no unread marker exists, don't set an unread marker for newly added messages from the current user. - const isFromCurrentUser = accountID === (ReportActionsUtils.isReportPreviewAction(message) ? !message.childLastActorAccountID : message.actorAccountID); + const isFromCurrentUser = accountID === (ReportActionsUtils.isReportPreviewAction(message) ? message.childLastActorAccountID : message.actorAccountID); const isNewMessage = !prevSortedVisibleReportActionsObjects[message.reportActionID]; // The unread marker will show if the action's `created` time is later than `unreadMarkerTime`. From c17d32ee29f2b1fdc444bbcf5940990867053d4a Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Wed, 20 Nov 2024 15:50:04 +0200 Subject: [PATCH 059/104] Revert "revert RequestThrottle to be function based" This reverts commit 80d634b4a2f10c4063ea7f5002c7eedda210a12b. --- src/libs/Middleware/Reauthentication.ts | 7 ++- src/libs/Network/SequentialQueue.ts | 12 +++-- src/libs/RequestThrottle.ts | 65 +++++++++++++------------ tests/unit/APITest.ts | 9 ++-- 4 files changed, 51 insertions(+), 42 deletions(-) diff --git a/src/libs/Middleware/Reauthentication.ts b/src/libs/Middleware/Reauthentication.ts index 29df3b7f0b50..e9a176005be7 100644 --- a/src/libs/Middleware/Reauthentication.ts +++ b/src/libs/Middleware/Reauthentication.ts @@ -7,14 +7,16 @@ import * as NetworkStore from '@libs/Network/NetworkStore'; import type {RequestError} from '@libs/Network/SequentialQueue'; import NetworkConnection from '@libs/NetworkConnection'; import * as Request from '@libs/Request'; +import RequestThrottle from '@libs/RequestThrottle'; import CONST from '@src/CONST'; -import * as RequestThrottle from '@src/libs/RequestThrottle'; import ONYXKEYS from '@src/ONYXKEYS'; import type Middleware from './types'; // We store a reference to the active authentication request so that we are only ever making one request to authenticate at a time. let isAuthenticating: Promise | null = null; +const reauthThrottle = new RequestThrottle(); + function reauthenticate(commandName?: string): Promise { if (isAuthenticating) { return isAuthenticating; @@ -44,7 +46,8 @@ function reauthenticate(commandName?: string): Promise { function retryReauthenticate(commandName?: string): Promise { return Authentication.reauthenticate(commandName).catch((error: RequestError) => { - return RequestThrottle.sleep(error, 'Authenticate') + return reauthThrottle + .sleep(error, 'Authenticate') .then(() => retryReauthenticate(commandName)) .catch(() => { NetworkStore.setIsAuthenticating(false); diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 68c1be0f32a5..2ca2b043b737 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -2,10 +2,10 @@ import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import Log from '@libs/Log'; import * as Request from '@libs/Request'; +import RequestThrottle from '@libs/RequestThrottle'; import * as PersistedRequests from '@userActions/PersistedRequests'; import * as QueuedOnyxUpdates from '@userActions/QueuedOnyxUpdates'; import CONST from '@src/CONST'; -import * as RequestThrottle from '@src/libs/RequestThrottle'; import ONYXKEYS from '@src/ONYXKEYS'; import type OnyxRequest from '@src/types/onyx/Request'; import type {ConflictData} from '@src/types/onyx/Request'; @@ -28,6 +28,7 @@ resolveIsReadyPromise?.(); let isSequentialQueueRunning = false; let currentRequestPromise: Promise | null = null; let isQueuePaused = false; +const requestThrottle = new RequestThrottle(); /** * Puts the queue into a paused state so that no requests will be processed @@ -99,7 +100,7 @@ function process(): Promise { Log.info('[SequentialQueue] Removing persisted request because it was processed successfully.', false, {request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - RequestThrottle.clear(); + requestThrottle.clear(); return process(); }) .catch((error: RequestError) => { @@ -108,17 +109,18 @@ function process(): Promise { if (error.name === CONST.ERROR.REQUEST_CANCELLED || error.message === CONST.ERROR.DUPLICATE_RECORD) { Log.info("[SequentialQueue] Removing persisted request because it failed and doesn't need to be retried.", false, {error, request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - RequestThrottle.clear(); + requestThrottle.clear(); return process(); } PersistedRequests.rollbackOngoingRequest(); - return RequestThrottle.sleep(error, requestToProcess.command) + return requestThrottle + .sleep(error, requestToProcess.command) .then(process) .catch(() => { Onyx.update(requestToProcess.failureData ?? []); Log.info('[SequentialQueue] Removing persisted request because it failed too many times.', false, {error, request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - RequestThrottle.clear(); + requestThrottle.clear(); return process(); }); }); diff --git a/src/libs/RequestThrottle.ts b/src/libs/RequestThrottle.ts index 3bbc82ff5b45..8a6673c22a92 100644 --- a/src/libs/RequestThrottle.ts +++ b/src/libs/RequestThrottle.ts @@ -3,41 +3,44 @@ import Log from './Log'; import type {RequestError} from './Network/SequentialQueue'; import {generateRandomInt} from './NumberUtils'; -let requestWaitTime = 0; -let requestRetryCount = 0; +class RequestThrottle { + private requestWaitTime = 0; -function clear() { - requestWaitTime = 0; - requestRetryCount = 0; - Log.info(`[RequestThrottle] in clear()`); -} + private requestRetryCount = 0; -function getRequestWaitTime() { - if (requestWaitTime) { - requestWaitTime = Math.min(requestWaitTime * 2, CONST.NETWORK.MAX_RETRY_WAIT_TIME_MS); - } else { - requestWaitTime = generateRandomInt(CONST.NETWORK.MIN_RETRY_WAIT_TIME_MS, CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS); + clear() { + this.requestWaitTime = 0; + this.requestRetryCount = 0; + Log.info(`[RequestThrottle] in clear()`); } - return requestWaitTime; -} -function getLastRequestWaitTime() { - return requestWaitTime; -} - -function sleep(error: RequestError, command: string): Promise { - requestRetryCount++; - return new Promise((resolve, reject) => { - if (requestRetryCount <= CONST.NETWORK.MAX_REQUEST_RETRIES) { - const currentRequestWaitTime = getRequestWaitTime(); - Log.info( - `[RequestThrottle] Retrying request after error: '${error.name}', '${error.message}', '${error.status}'. Command: ${command}. Retry count: ${requestRetryCount}. Wait time: ${currentRequestWaitTime}`, - ); - setTimeout(resolve, currentRequestWaitTime); - return; + getRequestWaitTime() { + if (this.requestWaitTime) { + this.requestWaitTime = Math.min(this.requestWaitTime * 2, CONST.NETWORK.MAX_RETRY_WAIT_TIME_MS); + } else { + this.requestWaitTime = generateRandomInt(CONST.NETWORK.MIN_RETRY_WAIT_TIME_MS, CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME_MS); } - reject(); - }); + return this.requestWaitTime; + } + + getLastRequestWaitTime() { + return this.requestWaitTime; + } + + sleep(error: RequestError, command: string): Promise { + this.requestRetryCount++; + return new Promise((resolve, reject) => { + if (this.requestRetryCount <= CONST.NETWORK.MAX_REQUEST_RETRIES) { + const currentRequestWaitTime = this.getRequestWaitTime(); + Log.info( + `[RequestThrottle] Retrying request after error: '${error.name}', '${error.message}', '${error.status}'. Command: ${command}. Retry count: ${this.requestRetryCount}. Wait time: ${currentRequestWaitTime}`, + ); + setTimeout(resolve, currentRequestWaitTime); + } else { + reject(); + } + }); + } } -export {clear, getRequestWaitTime, sleep, getLastRequestWaitTime}; +export default RequestThrottle; diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index 5b6947861dbe..bc4b650fb6e5 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -9,7 +9,7 @@ import * as MainQueue from '@src/libs/Network/MainQueue'; import * as NetworkStore from '@src/libs/Network/NetworkStore'; import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; import * as Request from '@src/libs/Request'; -import * as RequestThrottle from '@src/libs/RequestThrottle'; +import RequestThrottle from '@src/libs/RequestThrottle'; import ONYXKEYS from '@src/ONYXKEYS'; import type ReactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; @@ -39,6 +39,7 @@ type XhrCalls = Array<{ }>; const originalXHR = HttpUtils.xhr; +const requestThrottle = new RequestThrottle(); beforeEach(() => { global.fetch = TestHelper.getGlobalFetchMock(); @@ -47,7 +48,7 @@ beforeEach(() => { MainQueue.clear(); HttpUtils.cancelPendingRequests(); PersistedRequests.clear(); - RequestThrottle.clear(); + requestThrottle.clear(); NetworkStore.checkRequiredData(); // Wait for any Log command to finish and Onyx to fully clear @@ -243,7 +244,7 @@ describe('APITests', () => { // We let the SequentialQueue process again after its wait time return new Promise((resolve) => { - setTimeout(resolve, RequestThrottle.getLastRequestWaitTime()); + setTimeout(resolve, requestThrottle.getLastRequestWaitTime()); }); }) .then(() => { @@ -256,7 +257,7 @@ describe('APITests', () => { // We let the SequentialQueue process again after its wait time return new Promise((resolve) => { - setTimeout(resolve, RequestThrottle.getLastRequestWaitTime()); + setTimeout(resolve, requestThrottle.getLastRequestWaitTime()); }).then(waitForBatchedUpdates); }) .then(() => { From 008ec5645d082aecc072a795a1cf4a001d116468 Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Wed, 20 Nov 2024 17:08:15 +0200 Subject: [PATCH 060/104] use sequentialQueueRequestThrottle in APITest --- src/libs/Network/SequentialQueue.ts | 12 ++++++------ tests/unit/APITest.ts | 9 ++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 2ca2b043b737..3b4dd7591d31 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -28,7 +28,7 @@ resolveIsReadyPromise?.(); let isSequentialQueueRunning = false; let currentRequestPromise: Promise | null = null; let isQueuePaused = false; -const requestThrottle = new RequestThrottle(); +const sequentialQueueRequestThrottle = new RequestThrottle(); /** * Puts the queue into a paused state so that no requests will be processed @@ -100,7 +100,7 @@ function process(): Promise { Log.info('[SequentialQueue] Removing persisted request because it was processed successfully.', false, {request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - requestThrottle.clear(); + sequentialQueueRequestThrottle.clear(); return process(); }) .catch((error: RequestError) => { @@ -109,18 +109,18 @@ function process(): Promise { if (error.name === CONST.ERROR.REQUEST_CANCELLED || error.message === CONST.ERROR.DUPLICATE_RECORD) { Log.info("[SequentialQueue] Removing persisted request because it failed and doesn't need to be retried.", false, {error, request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - requestThrottle.clear(); + sequentialQueueRequestThrottle.clear(); return process(); } PersistedRequests.rollbackOngoingRequest(); - return requestThrottle + return sequentialQueueRequestThrottle .sleep(error, requestToProcess.command) .then(process) .catch(() => { Onyx.update(requestToProcess.failureData ?? []); Log.info('[SequentialQueue] Removing persisted request because it failed too many times.', false, {error, request: requestToProcess}); PersistedRequests.endRequestAndRemoveFromQueue(requestToProcess); - requestThrottle.clear(); + sequentialQueueRequestThrottle.clear(); return process(); }); }); @@ -283,5 +283,5 @@ function resetQueue(): void { resolveIsReadyPromise?.(); } -export {flush, getCurrentRequest, isRunning, isPaused, push, waitForIdle, pause, unpause, process, resetQueue}; +export {flush, getCurrentRequest, isRunning, isPaused, push, waitForIdle, pause, unpause, process, resetQueue, sequentialQueueRequestThrottle}; export type {RequestError}; diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index bc4b650fb6e5..ced9d5e68c4b 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -8,8 +8,8 @@ import HttpUtils from '@src/libs/HttpUtils'; import * as MainQueue from '@src/libs/Network/MainQueue'; import * as NetworkStore from '@src/libs/Network/NetworkStore'; import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; +import {sequentialQueueRequestThrottle} from '@src/libs/Network/SequentialQueue'; import * as Request from '@src/libs/Request'; -import RequestThrottle from '@src/libs/RequestThrottle'; import ONYXKEYS from '@src/ONYXKEYS'; import type ReactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; @@ -39,7 +39,6 @@ type XhrCalls = Array<{ }>; const originalXHR = HttpUtils.xhr; -const requestThrottle = new RequestThrottle(); beforeEach(() => { global.fetch = TestHelper.getGlobalFetchMock(); @@ -48,7 +47,7 @@ beforeEach(() => { MainQueue.clear(); HttpUtils.cancelPendingRequests(); PersistedRequests.clear(); - requestThrottle.clear(); + sequentialQueueRequestThrottle.clear(); NetworkStore.checkRequiredData(); // Wait for any Log command to finish and Onyx to fully clear @@ -244,7 +243,7 @@ describe('APITests', () => { // We let the SequentialQueue process again after its wait time return new Promise((resolve) => { - setTimeout(resolve, requestThrottle.getLastRequestWaitTime()); + setTimeout(resolve, sequentialQueueRequestThrottle.getLastRequestWaitTime()); }); }) .then(() => { @@ -257,7 +256,7 @@ describe('APITests', () => { // We let the SequentialQueue process again after its wait time return new Promise((resolve) => { - setTimeout(resolve, requestThrottle.getLastRequestWaitTime()); + setTimeout(resolve, sequentialQueueRequestThrottle.getLastRequestWaitTime()); }).then(waitForBatchedUpdates); }) .then(() => { From 79249f3053573e22b52c2c69627cac4f1625440d Mon Sep 17 00:00:00 2001 From: James Dean Date: Wed, 20 Nov 2024 16:40:36 -0800 Subject: [PATCH 061/104] Update en.ts Minor copy improvement --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 591f7eb0ed42..5f61402e5182 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1411,7 +1411,7 @@ const translations = { enableWalletToSendAndReceiveMoney: 'Enable your wallet to send and receive money with friends.', walletEnabledToSendAndReceiveMoney: 'Your wallet has been enabled to send and receive money with friends.', enableWallet: 'Enable wallet', - addBankAccountToSendAndReceive: 'Add a bank account to get paid back for expenses you submit to a workspace.', + addBankAccountToSendAndReceive: 'Get paid back for expenses you submit to a workspace.', addBankAccount: 'Add bank account', assignedCards: 'Assigned cards', assignedCardsDescription: 'These are cards assigned by a workspace admin to manage company spend.', From fd029a880b93fcee56509cf76065edd8b8a6a249 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 20 Nov 2024 21:35:58 +0700 Subject: [PATCH 062/104] fix: remove unreachable return --- .../workspace/companyCards/assignCard/AssignCardFeedPage.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index 20c51b882054..1e75ee15abce 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -52,8 +52,6 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { default: return ; } - - return ; } export default withPolicyAndFullscreenLoading(AssignCardFeedPage); From 1cd6d197734009e6744a24740b03c7991aaa1f0a Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 21 Nov 2024 15:40:32 +0700 Subject: [PATCH 063/104] feat: skip assignee step when only one workspace member --- ...WorkspaceCompanyCardsListHeaderButtons.tsx | 22 ++++++++++++++++++- .../assignCard/AssignCardFeedPage.tsx | 7 +----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index 031ac309e155..b2e707170370 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -16,9 +16,12 @@ import * as CardUtils from '@libs/CardUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; +import * as CompanyCards from '@userActions/CompanyCards'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {CompanyCardFeed} from '@src/types/onyx'; +import type {AssignCardData, AssignCardStep} from '@src/types/onyx/AssignCard'; type WorkspaceCompanyCardsListHeaderButtonsProps = { /** Current policy id */ @@ -41,6 +44,23 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp const isCustomFeed = CardUtils.isCustomFeed(selectedFeed); const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds); const currentFeedData = companyFeeds?.[selectedFeed]; + const policy = PolicyUtils.getPolicy(policyID); + + const handleAssignCard = () => { + const data: Partial = { + bankName: selectedFeed, + }; + + let currentStep: AssignCardStep = CONST.COMPANY_CARD.STEP.ASSIGNEE; + + if (Object.keys(policy?.employeeList ?? {}).length === 1) { + data.email = Object.keys(policy?.employeeList ?? {}).at(0); + currentStep = CONST.COMPANY_CARD.STEP.CARD; + } + + CompanyCards.setAssignCardStepAndData({data, currentStep}); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed)); + }; return ( Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed))} + onPress={handleAssignCard} icon={Expensicons.Plus} text={translate('workspace.companyCards.assignCard')} style={shouldChangeLayout && styles.flex1} diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index 1e75ee15abce..ad1109f665f0 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -1,10 +1,9 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect} from 'react'; +import React from 'react'; import {useOnyx} from 'react-native-onyx'; import type {SettingsNavigatorParamList} from '@navigation/types'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; -import * as CompanyCards from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -24,10 +23,6 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { const backTo = route.params?.backTo; const policyID = policy?.id ?? '-1'; - useEffect(() => { - CompanyCards.setAssignCardStepAndData({data: {bankName: feed}}); - }, [feed]); - switch (currentStep) { case CONST.COMPANY_CARD.STEP.ASSIGNEE: return ; From 2396fc07d6d977f2dc8cd2fe2d46a1e8a07ddf90 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 21 Nov 2024 15:48:39 +0700 Subject: [PATCH 064/104] feat: skip the card selection step if only one card available --- .../companyCards/assignCard/AssigneeStep.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index e8e8c81cba07..d09d8a51b301 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -35,6 +35,14 @@ function AssigneeStep({policy}: AssigneeStepProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); + const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policy?.id ?? '-1'); + + const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${assignCard?.data?.bankName ?? ''}`); + const {cardList, ...cards} = list ?? {}; + // We need to filter out cards which already has been assigned + const filteredCardList = Object.fromEntries( + Object.entries(cardList ?? {}).filter(([cardNumber]) => !Object.values(cards).find((card) => card.lastFourPAN && cardNumber.endsWith(card.lastFourPAN))), + ); const isEditing = assignCard?.isEditing; @@ -57,8 +65,10 @@ function AssigneeStep({policy}: AssigneeStepProps) { const personalDetail = PersonalDetailsUtils.getPersonalDetailByEmail(selectedMember); const memberName = personalDetail?.firstName ? personalDetail.firstName : personalDetail?.login; + const nextStep = Object.keys(filteredCardList).length === 1 ? CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE : CONST.COMPANY_CARD.STEP.CARD; + CompanyCards.setAssignCardStepAndData({ - currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.CARD, + currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : nextStep, data: { email: selectedMember, cardName: `${memberName}'s card`, @@ -69,7 +79,10 @@ function AssigneeStep({policy}: AssigneeStepProps) { const handleBackButtonPress = () => { if (isEditing) { - CompanyCards.setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, isEditing: false}); + CompanyCards.setAssignCardStepAndData({ + currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, + isEditing: false, + }); return; } Navigation.goBack(); From d1b6ef5dd4ade301e6828a65968788f75d3de1c4 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 21 Nov 2024 16:19:57 +0700 Subject: [PATCH 065/104] feat: skip card selection step when only one card --- ...WorkspaceCompanyCardsListHeaderButtons.tsx | 13 ++++++++++ .../members/WorkspaceMemberNewCardPage.tsx | 24 +++++++++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index b2e707170370..0f6f93b63508 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -46,6 +46,13 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp const currentFeedData = companyFeeds?.[selectedFeed]; const policy = PolicyUtils.getPolicy(policyID); + const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`); + const {cardList, ...cards} = list ?? {}; + // We need to filter out cards which already has been assigned + const filteredCardList = Object.fromEntries( + Object.entries(cardList ?? {}).filter(([cardNumber]) => !Object.values(cards).find((card) => card.lastFourPAN && cardNumber.endsWith(card.lastFourPAN))), + ); + const handleAssignCard = () => { const data: Partial = { bankName: selectedFeed, @@ -58,6 +65,12 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp currentStep = CONST.COMPANY_CARD.STEP.CARD; } + if (Object.keys(filteredCardList).length === 1) { + currentStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE; + data.cardNumber = Object.keys(filteredCardList).at(0); + data.encryptedCardNumber = Object.values(filteredCardList).at(0); + } + CompanyCards.setAssignCardStepAndData({data, currentStep}); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed)); }; diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx index 79dcdd37ce1f..4b1ab84ce732 100644 --- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx @@ -26,6 +26,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeed} from '@src/types/onyx'; +import type {AssignCardData, AssignCardStep} from '@src/types/onyx/AssignCard'; type CardFeedListItem = ListItem & { /** Card feed value */ @@ -49,6 +50,13 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew const memberLogin = personalDetails?.[accountID]?.login ?? ''; const availableCompanyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds); + const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`); + const {cardList, ...cards} = list ?? {}; + // We need to filter out cards which already has been assigned + const filteredCardList = Object.fromEntries( + Object.entries(cardList ?? {}).filter(([cardNumber]) => !Object.values(cards).find((card) => card.lastFourPAN && cardNumber.endsWith(card.lastFourPAN))), + ); + const handleSubmit = () => { if (!selectedFeed) { setShouldShowError(true); @@ -64,11 +72,19 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew }); Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.getRoute(policyID, ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, accountID))); } else { + const data: Partial = { + email: memberLogin, + }; + let currentStep: AssignCardStep = CONST.COMPANY_CARD.STEP.CARD; + + if (Object.keys(filteredCardList).length === 1) { + currentStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE; + data.cardNumber = Object.keys(filteredCardList).at(0); + data.encryptedCardNumber = Object.values(filteredCardList).at(0); + } CompanyCards.setAssignCardStepAndData({ - currentStep: CONST.COMPANY_CARD.STEP.CARD, - data: { - email: memberLogin, - }, + currentStep, + data, isEditing: false, }); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed, ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, accountID))); From 47a9f1239714678e0403baaf7dccdc4b3f9714fc Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 21 Nov 2024 16:40:36 +0700 Subject: [PATCH 066/104] chore: code cleanup --- src/libs/CardUtils.ts | 7 +++++++ .../WorkspaceCompanyCardsListHeaderButtons.tsx | 6 +----- .../workspace/companyCards/assignCard/AssigneeStep.tsx | 7 ++----- .../companyCards/assignCard/CardSelectionStep.tsx | 7 ++----- src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx | 6 +----- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 77aeb8e0ecc3..00437bd76248 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -352,6 +352,12 @@ function getSelectedFeed(lastSelectedFeed: OnyxEntry, cardFeeds return lastSelectedFeed ?? defaultFeed; } +function getFilteredCardList(list?: WorkspaceCardsList) { + const {cardList, ...cards} = list ?? {}; + // We need to filter out cards which already has been assigned + return Object.fromEntries(Object.entries(cardList ?? {}).filter(([cardNumber]) => !Object.values(cards).find((card) => card.lastFourPAN && cardNumber.endsWith(card.lastFourPAN)))); +} + export { isExpensifyCard, isCorporateCard, @@ -378,4 +384,5 @@ export { getCorrectStepForSelectedBank, getCustomOrFormattedFeedName, removeExpensifyCardFromCompanyCards, + getFilteredCardList, }; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index 0f6f93b63508..3e778c870137 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -47,11 +47,7 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp const policy = PolicyUtils.getPolicy(policyID); const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`); - const {cardList, ...cards} = list ?? {}; - // We need to filter out cards which already has been assigned - const filteredCardList = Object.fromEntries( - Object.entries(cardList ?? {}).filter(([cardNumber]) => !Object.values(cards).find((card) => card.lastFourPAN && cardNumber.endsWith(card.lastFourPAN))), - ); + const filteredCardList = CardUtils.getFilteredCardList(list); const handleAssignCard = () => { const data: Partial = { diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index d09d8a51b301..0bcdbdfaeb29 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -13,6 +13,7 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CardUtils from '@libs/CardUtils'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -38,11 +39,7 @@ function AssigneeStep({policy}: AssigneeStepProps) { const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policy?.id ?? '-1'); const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${assignCard?.data?.bankName ?? ''}`); - const {cardList, ...cards} = list ?? {}; - // We need to filter out cards which already has been assigned - const filteredCardList = Object.fromEntries( - Object.entries(cardList ?? {}).filter(([cardNumber]) => !Object.values(cards).find((card) => card.lastFourPAN && cardNumber.endsWith(card.lastFourPAN))), - ); + const filteredCardList = CardUtils.getFilteredCardList(list); const isEditing = assignCard?.isEditing; diff --git a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx index 4b07e7a220b8..47bcbbd3ed6d 100644 --- a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx @@ -45,11 +45,8 @@ function CardSelectionStep({feed, policyID}: CardSelectionStepProps) { const isEditing = assignCard?.isEditing; const assigneeDisplayName = PersonalDetailsUtils.getPersonalDetailByEmail(assignCard?.data?.email ?? '')?.displayName ?? ''; - const {cardList, ...cards} = list ?? {}; - // We need to filter out cards which already has been assigned - const filteredCardList = Object.fromEntries( - Object.entries(cardList ?? {}).filter(([cardNumber]) => !Object.values(cards).find((card) => card.lastFourPAN && cardNumber.endsWith(card.lastFourPAN))), - ); + const filteredCardList = CardUtils.getFilteredCardList(list); + const [cardSelected, setCardSelected] = useState(assignCard?.data?.encryptedCardNumber ?? ''); const [shouldShowError, setShouldShowError] = useState(false); diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx index 4b1ab84ce732..ab2914d32600 100644 --- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx @@ -51,11 +51,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew const availableCompanyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds); const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`); - const {cardList, ...cards} = list ?? {}; - // We need to filter out cards which already has been assigned - const filteredCardList = Object.fromEntries( - Object.entries(cardList ?? {}).filter(([cardNumber]) => !Object.values(cards).find((card) => card.lastFourPAN && cardNumber.endsWith(card.lastFourPAN))), - ); + const filteredCardList = CardUtils.getFilteredCardList(list); const handleSubmit = () => { if (!selectedFeed) { From 6ee355137fdb61183eff7b26df53c6ee4def400e Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 21 Nov 2024 17:05:43 +0700 Subject: [PATCH 067/104] feat: create util functions --- src/libs/CardUtils.ts | 6 ++++++ .../companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx | 2 +- .../workspace/companyCards/assignCard/AssigneeStep.tsx | 2 +- src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx | 2 +- src/types/onyx/Card.ts | 5 ++++- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 00437bd76248..f1f28ad348c8 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -9,6 +9,7 @@ import type {TranslationPaths} from '@src/languages/types'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {BankAccountList, Card, CardFeeds, CardList, CompanyCardFeed, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx'; +import type {FilteredCardList} from '@src/types/onyx/Card'; import type {CompanyCardNicknames, CompanyFeeds} from '@src/types/onyx/CardFeeds'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -358,6 +359,10 @@ function getFilteredCardList(list?: WorkspaceCardsList) { return Object.fromEntries(Object.entries(cardList ?? {}).filter(([cardNumber]) => !Object.values(cards).find((card) => card.lastFourPAN && cardNumber.endsWith(card.lastFourPAN)))); } +function hasOnlyOneCardToAssign(list: FilteredCardList) { + return !!(Object.keys(list).length === 1); +} + export { isExpensifyCard, isCorporateCard, @@ -385,4 +390,5 @@ export { getCustomOrFormattedFeedName, removeExpensifyCardFromCompanyCards, getFilteredCardList, + hasOnlyOneCardToAssign, }; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index 3e778c870137..c220511dff8a 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -61,7 +61,7 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp currentStep = CONST.COMPANY_CARD.STEP.CARD; } - if (Object.keys(filteredCardList).length === 1) { + if (CardUtils.hasOnlyOneCardToAssign(filteredCardList)) { currentStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE; data.cardNumber = Object.keys(filteredCardList).at(0); data.encryptedCardNumber = Object.values(filteredCardList).at(0); diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 0bcdbdfaeb29..1b2819fc380c 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -62,7 +62,7 @@ function AssigneeStep({policy}: AssigneeStepProps) { const personalDetail = PersonalDetailsUtils.getPersonalDetailByEmail(selectedMember); const memberName = personalDetail?.firstName ? personalDetail.firstName : personalDetail?.login; - const nextStep = Object.keys(filteredCardList).length === 1 ? CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE : CONST.COMPANY_CARD.STEP.CARD; + const nextStep = CardUtils.hasOnlyOneCardToAssign(filteredCardList) ? CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE : CONST.COMPANY_CARD.STEP.CARD; CompanyCards.setAssignCardStepAndData({ currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : nextStep, diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx index ab2914d32600..e9ecf11a4dcd 100644 --- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx @@ -73,7 +73,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew }; let currentStep: AssignCardStep = CONST.COMPANY_CARD.STEP.CARD; - if (Object.keys(filteredCardList).length === 1) { + if (CardUtils.hasOnlyOneCardToAssign(filteredCardList)) { currentStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE; data.cardNumber = Object.keys(filteredCardList).at(0); data.encryptedCardNumber = Object.values(filteredCardList).at(0); diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index 8894db2723d1..7d3d252dd86b 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -180,5 +180,8 @@ type WorkspaceCardsList = Record & { cardList?: Record; }; +/** Card list with only available card */ +type FilteredCardList = Record; + export default Card; -export type {ExpensifyCardDetails, CardList, IssueNewCard, IssueNewCardStep, IssueNewCardData, WorkspaceCardsList, CardLimitType}; +export type {ExpensifyCardDetails, CardList, IssueNewCard, IssueNewCardStep, IssueNewCardData, WorkspaceCardsList, CardLimitType, FilteredCardList}; From 824ad5b5f44f96d28e86662042313371013735fc Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 21 Nov 2024 17:15:53 +0700 Subject: [PATCH 068/104] fix: minor fix --- src/libs/CardUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index f1f28ad348c8..46461edb5525 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -360,7 +360,7 @@ function getFilteredCardList(list?: WorkspaceCardsList) { } function hasOnlyOneCardToAssign(list: FilteredCardList) { - return !!(Object.keys(list).length === 1); + return Object.keys(list).length === 1; } export { From d71e7f1cd473598d5470fda5d9b1d12e335ae15e Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 21 Nov 2024 17:22:47 +0700 Subject: [PATCH 069/104] fix: minor fix --- .../WorkspaceCompanyCardsListHeaderButtons.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index c220511dff8a..1e36881a68a0 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -59,12 +59,12 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp if (Object.keys(policy?.employeeList ?? {}).length === 1) { data.email = Object.keys(policy?.employeeList ?? {}).at(0); currentStep = CONST.COMPANY_CARD.STEP.CARD; - } - if (CardUtils.hasOnlyOneCardToAssign(filteredCardList)) { - currentStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE; - data.cardNumber = Object.keys(filteredCardList).at(0); - data.encryptedCardNumber = Object.values(filteredCardList).at(0); + if (CardUtils.hasOnlyOneCardToAssign(filteredCardList)) { + currentStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE; + data.cardNumber = Object.keys(filteredCardList).at(0); + data.encryptedCardNumber = Object.values(filteredCardList).at(0); + } } CompanyCards.setAssignCardStepAndData({data, currentStep}); From ed7a9e396eb9e242549fca4c0b349ca76766ad6e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 21 Nov 2024 13:03:15 +0100 Subject: [PATCH 070/104] Navigate to some other existing feed after removing one --- src/libs/actions/CompanyCards.ts | 36 +++++++++++-------- .../WorkspaceCompanyCardsSettingsPage.tsx | 4 ++- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index 4a102ab9bb72..8e83b9192a71 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -151,27 +151,33 @@ function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, API.write(WRITE_COMMANDS.SET_COMPANY_CARD_TRANSACTION_LIABILITY, parameters, onyxData); } -function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: number, bankName: CompanyCardFeed) { +function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: number, bankName: CompanyCardFeed, feedToOpen?: CompanyCardFeed) { const authToken = NetworkStore.getAuthToken(); const isCustomFeed = CardUtils.isCustomFeed(bankName); const feedUpdates = {[bankName]: null}; - const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, - value: { - settings: { - ...(isCustomFeed ? {companyCards: feedUpdates} : {oAuthAccountDetails: feedUpdates}), - companyCardNicknames: { - [bankName]: null, - }, + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, + value: { + settings: { + ...(isCustomFeed ? {companyCards: feedUpdates} : {oAuthAccountDetails: feedUpdates}), + companyCardNicknames: { + [bankName]: null, }, }, }, - ], - }; + }, + ]; + + if (feedToOpen) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, + value: feedToOpen, + }); + } const parameters = { authToken, @@ -179,7 +185,7 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu bankName, }; - API.write(WRITE_COMMANDS.DELETE_COMPANY_CARD_FEED, parameters, onyxData); + API.write(WRITE_COMMANDS.DELETE_COMPANY_CARD_FEED, parameters, {optimisticData}); } function assignWorkspaceCompanyCard(policyID: string, data?: Partial) { diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx index 41f698f61dab..2fb232360594 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx @@ -23,6 +23,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import type {CompanyCardFeed} from '@src/types/onyx'; type WorkspaceCompanyCardsSettingsPageProps = StackScreenProps; @@ -51,7 +52,8 @@ function WorkspaceCompanyCardsSettingsPage({ const deleteCompanyCardFeed = () => { if (selectedFeed) { - CompanyCards.deleteWorkspaceCompanyCardFeed(policyID, workspaceAccountID, selectedFeed); + const feedToOpen = (Object.keys(companyFeeds) as CompanyCardFeed[]).filter((feed) => feed !== selectedFeed).at(0); + CompanyCards.deleteWorkspaceCompanyCardFeed(policyID, workspaceAccountID, selectedFeed, feedToOpen); } setDeleteCompanyCardConfirmModalVisible(false); Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack); From 68a64fa52f5c727148df1ecb99c451bce5c74359 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 21 Nov 2024 14:29:55 +0100 Subject: [PATCH 071/104] Remove getCompanyCardNumber function --- src/libs/CardUtils.ts | 9 --------- .../workspace/companyCards/WorkspaceCompanyCardsList.tsx | 3 +-- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 77aeb8e0ecc3..b52ea975994b 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -212,14 +212,6 @@ function sortCardsByCardholderName(cardsList: OnyxEntry, per }); } -function getCompanyCardNumber(cardList: Record, lastFourPAN?: string, cardName = ''): string { - if (!lastFourPAN) { - return ''; - } - - return Object.keys(cardList).find((card) => card.endsWith(lastFourPAN)) ?? cardName; -} - function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD.BANK): IconAsset { const feedIcons = { [CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: Illustrations.VisaCompanyCardDetailLarge, @@ -368,7 +360,6 @@ export { getTranslationKeyForLimitType, getEligibleBankAccountsForCard, sortCardsByCardholderName, - getCompanyCardNumber, getCardFeedIcon, getCardFeedName, getCompanyFeeds, diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx index 75b4f44fc843..7959879609e7 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx @@ -36,7 +36,6 @@ function WorkspaceCompanyCardsList({cardsList, policyID}: WorkspaceCompanyCardsL const renderItem = useCallback( ({item, index}: ListRenderItemInfo) => { const cardID = Object.keys(cardsList ?? {}).find((id) => cardsList?.[id].cardID === item.cardID); - const cardName = CardUtils.getCompanyCardNumber(cardsList?.cardList ?? {}, item.lastFourPAN); const isCardDeleted = item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; return ( From 146fc097e0a5e777f104d5b14865d9c1b21905aa Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Thu, 21 Nov 2024 17:45:01 +0200 Subject: [PATCH 072/104] fix reauth online test, fix RequestThrottle.clear --- src/libs/Middleware/Reauthentication.ts | 3 +- src/libs/RequestThrottle.ts | 9 +- tests/unit/NetworkTest.ts | 120 ++++++++++-------------- 3 files changed, 59 insertions(+), 73 deletions(-) diff --git a/src/libs/Middleware/Reauthentication.ts b/src/libs/Middleware/Reauthentication.ts index e9a176005be7..8f1c9a3dc9b6 100644 --- a/src/libs/Middleware/Reauthentication.ts +++ b/src/libs/Middleware/Reauthentication.ts @@ -59,6 +59,7 @@ function retryReauthenticate(commandName?: string): Promise { function resetReauthentication(): void { isAuthenticating = null; + reauthThrottle.clear(); } const Reauthentication: Middleware = (response, request, isFromSequentialQueue) => @@ -151,4 +152,4 @@ const Reauthentication: Middleware = (response, request, isFromSequentialQueue) }); export default Reauthentication; -export {reauthenticate, resetReauthentication}; +export {reauthenticate, resetReauthentication, reauthThrottle}; diff --git a/src/libs/RequestThrottle.ts b/src/libs/RequestThrottle.ts index 8a6673c22a92..4190e3845d72 100644 --- a/src/libs/RequestThrottle.ts +++ b/src/libs/RequestThrottle.ts @@ -8,9 +8,16 @@ class RequestThrottle { private requestRetryCount = 0; + private timeoutID?: NodeJS.Timeout; + clear() { this.requestWaitTime = 0; this.requestRetryCount = 0; + if (this.timeoutID) { + Log.info(`[RequestThrottle] clearing timeoutID: ${String(this.timeoutID)}`); + clearTimeout(this.timeoutID); + this.timeoutID = undefined; + } Log.info(`[RequestThrottle] in clear()`); } @@ -35,7 +42,7 @@ class RequestThrottle { Log.info( `[RequestThrottle] Retrying request after error: '${error.name}', '${error.message}', '${error.status}'. Command: ${command}. Retry count: ${this.requestRetryCount}. Wait time: ${currentRequestWaitTime}`, ); - setTimeout(resolve, currentRequestWaitTime); + this.timeoutID = setTimeout(resolve, currentRequestWaitTime); } else { reject(); } diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index 96a7f08e92f1..d9771736d4de 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -56,87 +56,65 @@ afterEach(() => { NetworkStore.resetHasReadRequiredDataFromStorage(); Onyx.addDelayToConnectCallback(0); jest.clearAllMocks(); + jest.clearAllTimers(); + jest.useRealTimers(); }); describe('NetworkTests', () => { test('failing to reauthenticate should not log out user', () => { - // Given a test user login and account ID + // Use fake timers to control timing in the test + jest.useFakeTimers(); + const TEST_USER_LOGIN = 'test@testguy.com'; const TEST_USER_ACCOUNT_ID = 1; + const NEW_AUTH_TOKEN = 'qwerty12345'; - let isOffline: boolean; - + let sessionState: OnyxEntry; Onyx.connect({ - key: ONYXKEYS.NETWORK, - callback: (val) => { - isOffline = !!val?.isOffline; - }, + key: ONYXKEYS.SESSION, + callback: (val) => (sessionState = val), }); - // Given a test user login and account ID - return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN).then(() => { - expect(isOffline).toBe(false); - - // Mock fetch() so that it throws a TypeError to simulate a bad network connection - global.fetch = jest.fn().mockRejectedValue(new TypeError(CONST.ERROR.FAILED_TO_FETCH)); - - const actualXhr = HttpUtils.xhr; - - const mockedXhr = jest.fn(); - mockedXhr - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, - }), - ) - - // Fail the call to re-authenticate - .mockImplementationOnce(actualXhr) - - // The next call should still be using the old authToken - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, - }), - ) - - // Succeed the call to set a new authToken - .mockImplementationOnce(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.SUCCESS, - authToken: 'qwerty12345', - }), - ) - - // All remaining requests should succeed - .mockImplementation(() => - Promise.resolve({ - jsonCode: CONST.JSON_CODE.SUCCESS, - }), - ); - - HttpUtils.xhr = mockedXhr; - - // This should first trigger re-authentication and then a Failed to fetch - PersonalDetails.openPublicProfilePage(TEST_USER_ACCOUNT_ID); - return waitForBatchedUpdates() - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) - .then(() => { - expect(isOffline).toBe(false); - - // Advance the network request queue by 1 second so that it can realize it's back online - jest.advanceTimersByTime(CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); - return waitForBatchedUpdates(); - }) - .then(() => { - // Then we will eventually have 1 call to OpenPublicProfilePage and 1 calls to Authenticate - const callsToOpenPublicProfilePage = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'OpenPublicProfilePage'); - const callsToAuthenticate = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'Authenticate'); - - expect(callsToOpenPublicProfilePage.length).toBe(1); - expect(callsToAuthenticate.length).toBe(1); - }); - }); + return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) + .then(() => { + // Mock XHR with a sequence of responses: + // 1. First call fails with NOT_AUTHENTICATED + // 2. Second call fails with network error + // 3. Third call succeeds with new auth token + const mockedXhr = jest + .fn() + .mockImplementationOnce(() => + Promise.resolve({ + jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, + }), + ) + .mockImplementationOnce(() => Promise.reject(new Error(CONST.ERROR.FAILED_TO_FETCH))) + .mockImplementationOnce(() => + Promise.resolve({ + jsonCode: CONST.JSON_CODE.SUCCESS, + authToken: NEW_AUTH_TOKEN, + }), + ); + + HttpUtils.xhr = mockedXhr; + + // Trigger an API call that will cause reauthentication flow + PersonalDetails.openPublicProfilePage(TEST_USER_ACCOUNT_ID); + return waitForBatchedUpdates(); + }) + .then(() => { + // Process pending retry request + jest.runAllTimers(); + return waitForBatchedUpdates(); + }) + .then(() => { + // Verify: + // 1. We attempted to authenticate twice (first failed, retry succeeded) + // 2. The session has the new auth token (user wasn't logged out) + const callsToAuthenticate = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'Authenticate'); + expect(callsToAuthenticate.length).toBe(2); + expect(sessionState?.authToken).toBe(NEW_AUTH_TOKEN); + }); }); test('failing to reauthenticate while offline should not log out user', async () => { From dfb863c81d2fbe4a11c48f0b5ada970bbdd61a9f Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Thu, 21 Nov 2024 17:47:07 +0200 Subject: [PATCH 073/104] fix race condition in reauth offline test --- tests/unit/NetworkTest.ts | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index d9771736d4de..2998aa0e8a25 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -121,7 +121,6 @@ describe('NetworkTests', () => { // 1. Setup Phase - Initialize test user and state variables const TEST_USER_LOGIN = 'test@testguy.com'; const TEST_USER_ACCOUNT_ID = 1; - const defaultTimeout = 1000; let sessionState: OnyxEntry; @@ -138,24 +137,23 @@ describe('NetworkTests', () => { const initialAuthToken = sessionState?.authToken; expect(initialAuthToken).toBeDefined(); + // Create a promise that we can resolve later to control the timing + let resolveAuthRequest: (value: unknown) => void = () => {}; + const pendingAuthRequest = new Promise((resolve) => { + resolveAuthRequest = resolve; + }); + // 2. Mock Setup Phase - Configure XHR mocks for the test sequence const mockedXhr = jest .fn() - // First mock: ReconnectApp will fail with NOT_AUTHENTICATED + // First call: Return NOT_AUTHENTICATED to trigger reauthentication .mockImplementationOnce(() => Promise.resolve({ jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, }), ) - // Second mock: Authenticate with network check and delay - .mockImplementationOnce(() => { - // create a delayed response. Timeout will expire after the second App.reconnectApp(); - return new Promise((_, reject) => { - setTimeout(() => { - reject(new Error('Network request failed')); - }, defaultTimeout); - }); - }); + // Second call: Return a pending promise that we'll resolve later + .mockImplementationOnce(() => pendingAuthRequest); HttpUtils.xhr = mockedXhr; @@ -180,17 +178,15 @@ describe('NetworkTests', () => { await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - // Trigger another reconnect due to network change + // 7.Trigger another reconnect due to network change App.confirmReadyToOpenApp(); App.reconnectApp(); - await waitForBatchedUpdates(); - // 7. Wait and Verify - Wait for authenticate timeout and verify session - await new Promise((resolve) => { - setTimeout(resolve, defaultTimeout + 100); - }); + // 8. Now fail the pending authentication request + resolveAuthRequest(Promise.reject(new Error('Network request failed'))); + await waitForBatchedUpdates(); // Now we wait for all updates after the auth request fails - // Verify the session remained intact and wasn't cleared + // 9. Verify the session remained intact and wasn't cleared expect(sessionState?.authToken).toBe(initialAuthToken); }); From 8d99484a22b27bfde3993848d7d2305e0fb678b5 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 22 Nov 2024 01:00:15 +0700 Subject: [PATCH 074/104] merge param into one param --- src/libs/actions/IOU.ts | 43 ++++--- .../iou/request/step/IOURequestStepAmount.tsx | 10 +- .../step/IOURequestStepConfirmation.tsx | 16 ++- .../step/IOURequestStepScan/index.native.tsx | 30 +++-- .../request/step/IOURequestStepScan/index.tsx | 30 +++-- tests/actions/IOUTest.ts | 120 +++++++++--------- 6 files changed, 133 insertions(+), 116 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7809b9ac0303..5c1affb89299 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -165,19 +165,6 @@ type GPSPoint = { long: number; }; -type RequestMoneyInformation = { - report: OnyxEntry; - payeeEmail: string | undefined; - payeeAccountID: number; - participant: Participant; - policy?: OnyxEntry; - policyTagList?: OnyxEntry; - policyCategories?: OnyxEntry; - gpsPoints?: GPSPoint; - action?: IOUAction; - reimbursible?: boolean; -}; - type RequestMoneyTransactionData = { attendees: Attendee[] | undefined; amount: number; @@ -196,6 +183,28 @@ type RequestMoneyTransactionData = { linkedTrackedExpenseReportID?: string; }; +type RequestMoneyPolicyParams = { + policy?: OnyxEntry; + policyTagList?: OnyxEntry; + policyCategories?: OnyxEntry; +}; + +type RequestMoneyParticipantParams = { + payeeEmail: string | undefined; + payeeAccountID: number; + participant: Participant; +}; + +type RequestMoneyInformation = { + report: OnyxEntry; + participantData: RequestMoneyParticipantParams; + policyData?: RequestMoneyPolicyParams; + gpsPoints?: GPSPoint; + action?: IOUAction; + reimbursible?: boolean; + transactionData: RequestMoneyTransactionData; +}; + let allPersonalDetails: OnyxTypes.PersonalDetailsList = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -3555,8 +3564,10 @@ function shareTrackedExpense( /** * Submit expense to another user */ -function requestMoney(requestMoneyInformation: RequestMoneyInformation, requestMoneyTransactionData: RequestMoneyTransactionData) { - const {report, payeeEmail, payeeAccountID, participant, policy, policyTagList, policyCategories, gpsPoints, action, reimbursible} = requestMoneyInformation; +function requestMoney(requestMoneyInformation: RequestMoneyInformation) { + const {report, participantData, policyData = {}, transactionData, gpsPoints, action, reimbursible} = requestMoneyInformation; + const {participant, payeeAccountID, payeeEmail} = participantData; + const {policy, policyCategories, policyTagList} = policyData; const { amount, currency, @@ -3573,7 +3584,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation, requestM actionableWhisperReportActionID, linkedTrackedExpenseReportAction, linkedTrackedExpenseReportID, - } = requestMoneyTransactionData; + } = transactionData; // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 327b325d989f..83cdfa194777 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -210,21 +210,21 @@ function IOURequestStepAmount({ } if (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.REQUEST) { playSound(SOUNDS.DONE); - IOU.requestMoney( - { - report, + IOU.requestMoney({ + report, + participantData: { participant: participants.at(0) ?? {}, payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, }, - { + transactionData: { amount: backendAmount, currency, created: transaction?.created ?? '', merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, attendees: transaction?.attendees, }, - ); + }); return; } if (iouType === CONST.IOU.TYPE.TRACK) { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index d91ed84313d2..7df544e967f9 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -238,19 +238,21 @@ function IOURequestStepConfirmation({ if (!participant) { return; } - IOU.requestMoney( - { - report, + IOU.requestMoney({ + report, + participantData: { payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant, + }, + policyData: { policy, policyTagList: policyTags, policyCategories, - gpsPoints, - action, }, - { + gpsPoints, + action, + transactionData: { amount: transaction.amount, attendees: transaction.attendees, currency: transaction.currency, @@ -267,7 +269,7 @@ function IOURequestStepConfirmation({ linkedTrackedExpenseReportAction: transaction.linkedTrackedExpenseReportAction, linkedTrackedExpenseReportID: transaction.linkedTrackedExpenseReportID, }, - ); + }); }, [report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, policy, policyTags, policyCategories, action], ); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index e22e550d3eb8..0416315bbafc 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -255,14 +255,14 @@ function IOURequestStepScan({ receipt, ); } else { - IOU.requestMoney( - { - report, + IOU.requestMoney({ + report, + participantData: { payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant, }, - { + transactionData: { amount: 0, attendees: transaction?.attendees, currency: transaction?.currency ?? 'USD', @@ -270,7 +270,7 @@ function IOURequestStepScan({ merchant: '', receipt, }, - ); + }); } }, [currentUserPersonalDetails.accountID, currentUserPersonalDetails.login, iouType, report, transaction?.attendees, transaction?.created, transaction?.currency], @@ -355,19 +355,21 @@ function IOURequestStepScan({ }, ); } else { - IOU.requestMoney( - { - report, + IOU.requestMoney({ + report, + participantData: { payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant, + }, + policyData: { policy, - gpsPoints: { - lat: successData.coords.latitude, - long: successData.coords.longitude, - }, }, - { + gpsPoints: { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }, + transactionData: { amount: 0, attendees: transaction?.attendees, currency: transaction?.currency ?? 'USD', @@ -376,7 +378,7 @@ function IOURequestStepScan({ receipt, billable: false, }, - ); + }); } }, (errorData) => { diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 4ae8c0e72688..e646583bb67b 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -284,14 +284,14 @@ function IOURequestStepScan({ receipt, ); } else { - IOU.requestMoney( - { - report, + IOU.requestMoney({ + report, + participantData: { payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant, }, - { + transactionData: { amount: 0, attendees: transaction?.attendees, currency: transaction?.currency ?? 'USD', @@ -299,7 +299,7 @@ function IOURequestStepScan({ merchant: '', receipt, }, - ); + }); } }, [currentUserPersonalDetails.accountID, currentUserPersonalDetails.login, iouType, report, transaction?.attendees, transaction?.created, transaction?.currency], @@ -385,19 +385,21 @@ function IOURequestStepScan({ }, ); } else { - IOU.requestMoney( - { - report, + IOU.requestMoney({ + report, + participantData: { payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant, + }, + policyData: { policy, - gpsPoints: { - lat: successData.coords.latitude, - long: successData.coords.longitude, - }, }, - { + gpsPoints: { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }, + transactionData: { amount: 0, attendees: transaction?.attendees, currency: transaction?.currency ?? 'USD', @@ -406,7 +408,7 @@ function IOURequestStepScan({ receipt, billable: false, }, - ); + }); } }, (errorData) => { diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 28004d3dc7a5..909ec1b2068a 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -80,14 +80,14 @@ describe('actions/IOU', () => { let transactionThread: OnyxEntry; let transactionThreadCreatedAction: OnyxEntry; mockFetch?.pause?.(); - IOU.requestMoney( - { - report: {reportID: ''}, + IOU.requestMoney({ + report: {reportID: ''}, + participantData: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -95,7 +95,7 @@ describe('actions/IOU', () => { merchant, comment, }, - ); + }); return waitForBatchedUpdates() .then( () => @@ -294,14 +294,14 @@ describe('actions/IOU', () => { }), ) .then(() => { - IOU.requestMoney( - { - report: chatReport, + IOU.requestMoney({ + report: chatReport, + participantData: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -309,7 +309,7 @@ describe('actions/IOU', () => { merchant: '', comment, }, - ); + }); return waitForBatchedUpdates(); }) .then( @@ -513,14 +513,14 @@ describe('actions/IOU', () => { .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${existingTransaction.transactionID}`, existingTransaction)) .then(() => { if (chatReport) { - IOU.requestMoney( - { - report: chatReport, + IOU.requestMoney({ + report: chatReport, + participantData: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -528,7 +528,7 @@ describe('actions/IOU', () => { merchant: '', comment, }, - ); + }); } return waitForBatchedUpdates(); }) @@ -668,14 +668,14 @@ describe('actions/IOU', () => { let transactionThreadReport: OnyxEntry; let transactionThreadAction: OnyxEntry; mockFetch?.pause?.(); - IOU.requestMoney( - { - report: {reportID: ''}, + IOU.requestMoney({ + report: {reportID: ''}, + participantData: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -683,7 +683,7 @@ describe('actions/IOU', () => { merchant: '', comment, }, - ); + }); return ( waitForBatchedUpdates() .then( @@ -1484,14 +1484,14 @@ describe('actions/IOU', () => { let createIOUAction: OnyxEntry>; let payIOUAction: OnyxEntry; let transaction: OnyxEntry; - IOU.requestMoney( - { - report: {reportID: ''}, + IOU.requestMoney({ + report: {reportID: ''}, + participantData: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -1499,7 +1499,7 @@ describe('actions/IOU', () => { merchant: '', comment, }, - ); + }); return waitForBatchedUpdates() .then( () => @@ -1720,14 +1720,14 @@ describe('actions/IOU', () => { ) .then(() => { if (chatReport) { - IOU.requestMoney( - { - report: chatReport, + IOU.requestMoney({ + report: chatReport, + participantData: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -1735,7 +1735,7 @@ describe('actions/IOU', () => { merchant, comment, }, - ); + }); } return waitForBatchedUpdates(); }) @@ -1848,14 +1848,14 @@ describe('actions/IOU', () => { ) .then(() => { if (chatReport) { - IOU.requestMoney( - { - report: chatReport, + IOU.requestMoney({ + report: chatReport, + participantData: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -1863,7 +1863,7 @@ describe('actions/IOU', () => { merchant, comment, }, - ); + }); } return waitForBatchedUpdates(); }) @@ -1949,14 +1949,14 @@ describe('actions/IOU', () => { await TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); // When a submit IOU expense is made - IOU.requestMoney( - { - report: {reportID: ''}, + IOU.requestMoney({ + report: chatReport, + participantData: { payeeEmail: TEST_USER_LOGIN, payeeAccountID: TEST_USER_ACCOUNT_ID, participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -1964,7 +1964,7 @@ describe('actions/IOU', () => { merchant: '', comment, }, - ); + }); await waitForBatchedUpdates(); // When fetching all reports from Onyx @@ -2698,14 +2698,14 @@ describe('actions/IOU', () => { const amount2 = 20000; const comment2 = 'Send me money please 2'; if (chatReport) { - IOU.requestMoney( - { - report: chatReport, + IOU.requestMoney({ + report: chatReport, + participantData: { payeeEmail: TEST_USER_LOGIN, payeeAccountID: TEST_USER_ACCOUNT_ID, participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, }, - { + transactionData: { amount: amount2, attendees: [], currency: CONST.CURRENCY.USD, @@ -2713,7 +2713,7 @@ describe('actions/IOU', () => { merchant: '', comment: comment2, }, - ); + }); } await waitForBatchedUpdates(); @@ -2916,14 +2916,14 @@ describe('actions/IOU', () => { ) .then(() => { if (chatReport) { - IOU.requestMoney( - { - report: chatReport, + IOU.requestMoney({ + report: chatReport, + participantData: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -2931,7 +2931,7 @@ describe('actions/IOU', () => { merchant, comment, }, - ); + }); } return waitForBatchedUpdates(); }) @@ -3023,14 +3023,14 @@ describe('actions/IOU', () => { ) .then(() => { if (chatReport) { - IOU.requestMoney( - { - report: chatReport, + IOU.requestMoney({ + report: chatReport, + participantData: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -3038,7 +3038,7 @@ describe('actions/IOU', () => { merchant, comment, }, - ); + }); } return waitForBatchedUpdates(); }) @@ -3131,14 +3131,14 @@ describe('actions/IOU', () => { ) .then(() => { if (chatReport) { - IOU.requestMoney( - { - report: chatReport, + IOU.requestMoney({ + report: chatReport, + participantData: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, }, - { + transactionData: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -3146,7 +3146,7 @@ describe('actions/IOU', () => { merchant, comment, }, - ); + }); } return waitForBatchedUpdates(); }) From 1196f626493bc12144df713cc88c98f75305fe2e Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Thu, 21 Nov 2024 21:00:29 +0200 Subject: [PATCH 075/104] add comments to cleanup functions, fix interval type --- src/libs/Network/SequentialQueue.ts | 4 ++++ src/libs/Network/index.ts | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 3b4dd7591d31..be78fcb0338d 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -273,6 +273,10 @@ function waitForIdle(): Promise { return isReadyPromise; } +/** + * Clear any pending requests during test runs + * This is to prevent previous requests interfering with other tests + */ function resetQueue(): void { isSequentialQueueRunning = false; currentRequestPromise = null; diff --git a/src/libs/Network/index.ts b/src/libs/Network/index.ts index d91ea8c02553..4d27f75ab1a7 100644 --- a/src/libs/Network/index.ts +++ b/src/libs/Network/index.ts @@ -6,7 +6,8 @@ import pkg from '../../../package.json'; import * as MainQueue from './MainQueue'; import * as SequentialQueue from './SequentialQueue'; -let processQueueInterval: NodeJS.Timer; +// React Native uses a number for the timer id, but Web/NodeJS uses a Timeout object +let processQueueInterval: NodeJS.Timeout | number; // We must wait until the ActiveClientManager is ready so that we ensure only the "leader" tab processes any persisted requests ActiveClientManager.isReady().then(() => { @@ -16,12 +17,15 @@ ActiveClientManager.isReady().then(() => { processQueueInterval = setInterval(MainQueue.process, CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); }); -// Clear interval +/** + * Clear any existing intervals during test runs + * This is to prevent previous intervals interfering with other tests + */ function clearProcessQueueInterval() { if (!processQueueInterval) { return; } - clearInterval(processQueueInterval as unknown as number); + clearInterval(processQueueInterval); } /** From 9ce47533ec94a0b8a2b9af16fa2e297c3eed4ca4 Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Thu, 21 Nov 2024 21:05:27 +0200 Subject: [PATCH 076/104] add name param to RequestThrottle --- src/libs/Middleware/Reauthentication.ts | 2 +- src/libs/Network/SequentialQueue.ts | 2 +- src/libs/RequestThrottle.ts | 12 +++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/libs/Middleware/Reauthentication.ts b/src/libs/Middleware/Reauthentication.ts index 8f1c9a3dc9b6..db68ab60381b 100644 --- a/src/libs/Middleware/Reauthentication.ts +++ b/src/libs/Middleware/Reauthentication.ts @@ -15,7 +15,7 @@ import type Middleware from './types'; // We store a reference to the active authentication request so that we are only ever making one request to authenticate at a time. let isAuthenticating: Promise | null = null; -const reauthThrottle = new RequestThrottle(); +const reauthThrottle = new RequestThrottle('Re-authentication'); function reauthenticate(commandName?: string): Promise { if (isAuthenticating) { diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index be78fcb0338d..5cde21bf3a31 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -28,7 +28,7 @@ resolveIsReadyPromise?.(); let isSequentialQueueRunning = false; let currentRequestPromise: Promise | null = null; let isQueuePaused = false; -const sequentialQueueRequestThrottle = new RequestThrottle(); +const sequentialQueueRequestThrottle = new RequestThrottle('SequentialQueue'); /** * Puts the queue into a paused state so that no requests will be processed diff --git a/src/libs/RequestThrottle.ts b/src/libs/RequestThrottle.ts index 4190e3845d72..4507a7032564 100644 --- a/src/libs/RequestThrottle.ts +++ b/src/libs/RequestThrottle.ts @@ -10,15 +10,21 @@ class RequestThrottle { private timeoutID?: NodeJS.Timeout; + private name: string; + + constructor(name: string) { + this.name = name; + } + clear() { this.requestWaitTime = 0; this.requestRetryCount = 0; if (this.timeoutID) { - Log.info(`[RequestThrottle] clearing timeoutID: ${String(this.timeoutID)}`); + Log.info(`[RequestThrottle - ${this.name}] clearing timeoutID: ${String(this.timeoutID)}`); clearTimeout(this.timeoutID); this.timeoutID = undefined; } - Log.info(`[RequestThrottle] in clear()`); + Log.info(`[RequestThrottle - ${this.name}] in clear()`); } getRequestWaitTime() { @@ -40,7 +46,7 @@ class RequestThrottle { if (this.requestRetryCount <= CONST.NETWORK.MAX_REQUEST_RETRIES) { const currentRequestWaitTime = this.getRequestWaitTime(); Log.info( - `[RequestThrottle] Retrying request after error: '${error.name}', '${error.message}', '${error.status}'. Command: ${command}. Retry count: ${this.requestRetryCount}. Wait time: ${currentRequestWaitTime}`, + `[RequestThrottle - ${this.name}] Retrying request after error: '${error.name}', '${error.message}', '${error.status}'. Command: ${command}. Retry count: ${this.requestRetryCount}. Wait time: ${currentRequestWaitTime}`, ); this.timeoutID = setTimeout(resolve, currentRequestWaitTime); } else { From bf399e6291065fd23e2f81ee0a517d43fb216639 Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Thu, 21 Nov 2024 21:16:00 +0200 Subject: [PATCH 077/104] remove reauth onyx key --- src/ONYXKEYS.ts | 4 ---- src/libs/Middleware/Reauthentication.ts | 9 --------- 2 files changed, 13 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 08feab508556..49dd42fa8281 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -36,9 +36,6 @@ const ONYXKEYS = { PERSISTED_REQUESTS: 'networkRequestQueue', PERSISTED_ONGOING_REQUESTS: 'networkOngoingRequestQueue', - /** The re-authentication request to be retried as needed */ - REAUTHENTICATION_REQUEST: 'reauthenticationRequest', - /** Stores current date */ CURRENT_DATE: 'currentDate', @@ -891,7 +888,6 @@ type OnyxValuesMapping = { [ONYXKEYS.IS_SIDEBAR_LOADED]: boolean; [ONYXKEYS.PERSISTED_REQUESTS]: OnyxTypes.Request[]; [ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: OnyxTypes.Request; - [ONYXKEYS.REAUTHENTICATION_REQUEST]: OnyxTypes.Request; [ONYXKEYS.CURRENT_DATE]: string; [ONYXKEYS.CREDENTIALS]: OnyxTypes.Credentials; [ONYXKEYS.STASHED_CREDENTIALS]: OnyxTypes.Credentials; diff --git a/src/libs/Middleware/Reauthentication.ts b/src/libs/Middleware/Reauthentication.ts index db68ab60381b..d1f2c48da75e 100644 --- a/src/libs/Middleware/Reauthentication.ts +++ b/src/libs/Middleware/Reauthentication.ts @@ -9,7 +9,6 @@ import NetworkConnection from '@libs/NetworkConnection'; import * as Request from '@libs/Request'; import RequestThrottle from '@libs/RequestThrottle'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type Middleware from './types'; // We store a reference to the active authentication request so that we are only ever making one request to authenticate at a time. @@ -22,12 +21,6 @@ function reauthenticate(commandName?: string): Promise { return isAuthenticating; } - const reauthRequest = { - commandName, - }; - // eslint-disable-next-line rulesdir/prefer-actions-set-data - Onyx.set(ONYXKEYS.REAUTHENTICATION_REQUEST, reauthRequest); - isAuthenticating = retryReauthenticate(commandName) .then((response) => { return response; @@ -36,8 +29,6 @@ function reauthenticate(commandName?: string): Promise { throw error; }) .finally(() => { - // eslint-disable-next-line rulesdir/prefer-actions-set-data - Onyx.set(ONYXKEYS.REAUTHENTICATION_REQUEST, null); isAuthenticating = null; }); From 463fbe34c6a7b3dd5df08b72815104b5d0e18d79 Mon Sep 17 00:00:00 2001 From: Povilas Zirgulis Date: Thu, 21 Nov 2024 21:31:49 +0200 Subject: [PATCH 078/104] improve comments, remove unused import --- src/libs/Middleware/Reauthentication.ts | 5 ++++- src/libs/RequestThrottle.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/Middleware/Reauthentication.ts b/src/libs/Middleware/Reauthentication.ts index d1f2c48da75e..9d95fa8af873 100644 --- a/src/libs/Middleware/Reauthentication.ts +++ b/src/libs/Middleware/Reauthentication.ts @@ -1,4 +1,3 @@ -import Onyx from 'react-native-onyx'; import redirectToSignIn from '@libs/actions/SignInRedirect'; import * as Authentication from '@libs/Authentication'; import Log from '@libs/Log'; @@ -48,8 +47,12 @@ function retryReauthenticate(commandName?: string): Promise { }); } +// Used in tests to reset the reauthentication state function resetReauthentication(): void { + // Resets the authentication state flag to allow new reauthentication flows to start fresh isAuthenticating = null; + + // Clears any pending reauth timeouts set by reauthThrottle.sleep() reauthThrottle.clear(); } diff --git a/src/libs/RequestThrottle.ts b/src/libs/RequestThrottle.ts index 4507a7032564..c4589bb07afa 100644 --- a/src/libs/RequestThrottle.ts +++ b/src/libs/RequestThrottle.ts @@ -24,7 +24,7 @@ class RequestThrottle { clearTimeout(this.timeoutID); this.timeoutID = undefined; } - Log.info(`[RequestThrottle - ${this.name}] in clear()`); + Log.info(`[RequestThrottle - ${this.name}] cleared`); } getRequestWaitTime() { From 684b827cfdab6acbae6e6ce98f2296d3bc5cc09b Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Thu, 21 Nov 2024 17:19:15 -0300 Subject: [PATCH 079/104] Fix spanish translation for borrar gastos --- src/languages/es.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 7c3a99694b0e..1fac1a35537b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -642,8 +642,8 @@ const translations = { copyEmailToClipboard: 'Copiar email al portapapeles', markAsUnread: 'Marcar como no leído', markAsRead: 'Marcar como leído', - editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gastos' : 'comentario'}`, - deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gastos' : 'comentario'}`, + editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gasto' : 'comentario'}`, + deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gasto' : 'comentario'}`, deleteConfirmation: ({action}: DeleteConfirmationParams) => `¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'gasto' : 'comentario'}?`, onlyVisible: 'Visible sólo para', From 70b5c0c760b33007014654834bc5f6a619d0120e Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Thu, 21 Nov 2024 15:41:16 -0500 Subject: [PATCH 080/104] log when thirdPartyScripts loads for debugging --- web/thirdPartyScripts.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/thirdPartyScripts.js b/web/thirdPartyScripts.js index 15e77dbd490e..c45defd89e69 100644 --- a/web/thirdPartyScripts.js +++ b/web/thirdPartyScripts.js @@ -153,3 +153,5 @@ window['_fs_namespace'] = 'FS'; }), (g._v = '2.0.0')); })(window, document, window._fs_namespace, 'script', window._fs_script); + +console.log('thirdPartyScripts.js loaded'); From 1908fd9b0147e1aec4e692be0d2544de7b356bc3 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Thu, 21 Nov 2024 15:56:40 -0500 Subject: [PATCH 081/104] fix MIME type --- web/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/index.html b/web/index.html index 12d2c6c67782..d155de77bd3b 100644 --- a/web/index.html +++ b/web/index.html @@ -133,7 +133,7 @@ <% if (htmlWebpackPlugin.options.useThirdPartyScripts) { %> - + <% } %> From 4fcab2c9580a4a14a9cb21784ef1bb3a68814b7f Mon Sep 17 00:00:00 2001 From: James Dean Date: Thu, 21 Nov 2024 13:03:23 -0800 Subject: [PATCH 082/104] Update es.ts --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index ac071dd2a48f..0dd2dba9a943 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1412,7 +1412,7 @@ const translations = { enableWalletToSendAndReceiveMoney: 'Habilita tu Billetera Expensify para comenzar a enviar y recibir dinero con amigos.', walletEnabledToSendAndReceiveMoney: 'Tu billetera ha sido habilitada para enviar y recibir dinero con amigos.', enableWallet: 'Habilitar billetera', - addBankAccountToSendAndReceive: 'Añade una cuenta bancaria para recibir reembolsos por los gastos que envíes a un espacio de trabajo.', + addBankAccountToSendAndReceive: 'Recibe el reembolso de los gastos que envíes a un espacio de trabajo.', addBankAccount: 'Añadir cuenta bancaria', assignedCards: 'Tarjetas asignadas', assignedCardsDescription: 'Son tarjetas asignadas por un administrador del espacio de trabajo para gestionar los gastos de la empresa.', From bec24d7d28b65aeb6ce74979e32bbbd1de173241 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Thu, 21 Nov 2024 16:42:03 -0500 Subject: [PATCH 083/104] fix script path --- web/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/index.html b/web/index.html index d155de77bd3b..1dc0f7836fa4 100644 --- a/web/index.html +++ b/web/index.html @@ -133,7 +133,7 @@ <% if (htmlWebpackPlugin.options.useThirdPartyScripts) { %> - + <% } %> From d530da581bbe180bc793c94c4b862bee154d715a Mon Sep 17 00:00:00 2001 From: Sheena Trepanier Date: Thu, 21 Nov 2024 15:45:17 -0800 Subject: [PATCH 084/104] Update Netsuite-Troubleshooting.md Add NS troubleshooting for New Expensify --- .../netsuite/Netsuite-Troubleshooting.md | 440 +++++++++++++++++- 1 file changed, 438 insertions(+), 2 deletions(-) diff --git a/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md b/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md index 2ac1aaadbef4..221079c18156 100644 --- a/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md +++ b/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md @@ -1,6 +1,442 @@ --- title: Netsuite Troubleshooting -description: Coming soon +description: Troubleshoot common NetSuite sync and export errors. --- -# Coming soon +Synchronizing and exporting data between Expensify and NetSuite can streamline your financial processes, but occasionally, users may encounter errors that prevent a smooth integration. These errors often arise from discrepancies in settings, missing data, or configuration issues within NetSuite or Expensify. + +This troubleshooting guide aims to help you identify and resolve common sync and export errors, ensuring a seamless connection between your financial management systems. By following the step-by-step solutions provided for each specific error, you can quickly address issues and maintain accurate and efficient expense reporting and data management. + +# ExpensiError NS0005: Please enter value(s) for Department, Location or Class + +**Why does this happen?** + +This error occurs when the classification (like Location) is required at the header level of your transaction form in NetSuite. + +For expense reports and journal entries, NetSuite uses classifications from the employee record default. Expensify only exports this information at the line item level. + +For vendor bills, these classifications can't be mandatory because we use the vendor record instead of the employee record, and vendor records don’t have default classifications. + +## How to fix it for vendor bills + +Note: When exporting as a Vendor Bill, we pull from the vendor record, not the employee. Therefore, employee defaults don’t apply at the header ("main") level. This error appears if your NetSuite transaction form requires those fields. + +1. Go to **Customization > Forms > Transaction Forms**. +2. Click **"Edit"** on your preferred vendor bill form. +3. Go to **Screen Fields > Main**. +4. Uncheck both **"Show"** and **"Mandatory"** for the listed fields in your error message. +5. Sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) +6. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + +## How to fix it for journal entries and expense reports + +Note: If you see this error when exporting a Journal Entry or Expense Report, it might be because the report submitter doesn’t have default settings for Departments, Classes, or Locations. + +1. Go to **Lists > Employees** in NetSuite. +2. Click **"Edit"** next to the employee's name who submitted the report. +3. Scroll down to the **Classification** section. +4. Select a default **Department**, **Class**, and **Location** for the employee. +5. Click **Save**. +6. Sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) +7. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + + +# ExpensiError NS0012: Currency Does Not Exist In NetSuite + +**Why does this happen? (scenario 1)** + +When dealing with foreign transactions, Expensify sends the conversion rate and currency of the original expense to NetSuite. If the currency isn't listed in your NetSuite subsidiary, you'll see an error message saying the currency does not exist in NetSuite. + +## How to fix it + +1. Ensure the currency in Expensify matches what's in your NetSuite subsidiary. +2. If you see an error saying 'The currency X does not exist in NetSuite', re-sync your connection to NetSuite through the workspace admin section in Expensify. +3. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + +**Why does this happen? (scenario 2)** + +This error can happen if you’re using a non-OneWorld NetSuite instance and exporting a currency other than EUR, GBP, USD, or CAD. + +## How to fix it + +1. Head to NetSuite. +2. Go to **Setup > Enable Features**. +3. Check the **Multiple Currencies** box. + +Once you've done this, you can add the offending currency by searching **New Currencies** in the NetSuite global search. + +# ExpensiError NS0021: Invalid tax code reference key + +**Why does this happen?** + +This error usually indicates an issue with the Tax Group settings in NetSuite, which can arise from several sources. + +## How to fix it + +If a Tax Code on Sales Transactions is mapped to a Tax Group, an error will occur. To fix this, the Tax Code must be mapped to a Tax Code on Purchase Transactions instead. + +To verify if a Tax Code is for Sales or Purchase transactions, view the relevant Tax Code(s). + +**For Australian Taxes:** + +Ensure your Tax Groups are mapped correctly: +- **GST 10%** to **NCT-AU** (not the Sales Transaction Tax Code TS-AU) +- **No GST 0%** to **NCF-AU** (not the Sales Transaction Tax Code TFS-AU) + +### Tax Group Type +Tax Groups can represent different types of taxes. For compatibility with Expensify, ensure the tax type is set to GST/VAT. + +### Enable Tax Groups +Some subsidiaries require you to enable Tax Groups. Go to **Set Up Taxes** for the subsidiary's country and ensure the Tax Code lists include both Tax Codes and Tax Groups. + +# ExpensiError NS0023: Employee Does Not Exist in NetSuite (Invalid Employee) + +**Why does this happen?** + +This can happen if the employee’s subsidiary in NetSuite doesn’t match the subsidiary selected for the connection in Expensify. + +## How to fix it + +1. **Check the Employee's Subsidiary** + - Go to the employee record in NetSuite. + - Confirm the employee's subsidiary matches what’s listed as the subsidiary at the workspace level. + - To find this in Expensify navigate to **Settings > Workspaces > click workspace name > Accounting > Subsidiary**. + - If the subsidiaries don’t match, update the subsidiary in Expensify to match what’s listed in NetSuite. + - Sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) +2. **Verify Access Restrictions:** + - Go to **Lists > Employees > Employees > [Select Employee] > Edit > Access**. + - Uncheck **Restrict Access to Expensify**. +3. **Additional Checks:** + - Ensure the email on the employee record in NetSuite matches the email address of the report submitter in Expensify. + - In NetSuite, make sure the employee's hire date is in the past and/or the termination date is in the future. +4. **Currency Match for Journal Entries:** + - If exporting as Journal Entries, ensure the currency for the NetSuite employee record, NetSuite subsidiary, and Expensify workspace all match. + - In NetSuite, go to the **Human Resources** tab > **Expense Report Currencies**, and add the subsidiary/policy currency if necessary. + +# ExpensiError NS0085: Expense Does Not Have Appropriate Permissions for Settings an Exchange Rate in NetSuite + +**Why does this happen?** + +This error occurs when the exchange rate settings in NetSuite aren't updated correctly. + +## How to fix it + +1. In NetSuite, go to Customization > Forms > Transaction Forms. +2. Search for the form type that the report is being exported as (Expense Report, Journal Entry, or Vendor Bill) and click Edit next to the form that has the Preferred checkbox checked. + - **For Expense Reports:** + - Go to Screen Fields > Expenses (the Expenses tab farthest to the right). + - Ensure the Exchange Rate field under the Description column has the Show checkbox checked. + - **For Vendor Bills:** + - Go to Screen Fields > Main. + - Ensure the Exchange Rate field under the Description column has the Show checkbox checked. + - **For Journal Entries:** + - Go to Screen Fields > Lines. + - Ensure the Exchange Rate field under the Description column has the Show checkbox checked. + - Go to Screen Fields > Main and ensure the Show checkbox is checked in the Exchange Rate field under the Description column. +3. Sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) +4. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + +# ExpensiError NS0079: The Transaction Date is Not Within the Date Range of Your Accounting Period + +**Why does this happen?** + +The transaction date you specified is not within the date range of your accounting period. When the posting period settings in NetSuite are not configured to allow a transaction date outside the posting period, you can't export a report to the next open period, which is why you’ll run into this error. + +## How to fix it + +1. In NetSuite, navigate to Setup > Accounting > Accounting Preferences. +2. Under the General Ledger section, ensure the field Allow Transaction Date Outside of the Posting Period is set to Warn. +3. Then, choose whether to export your reports to the First Open Period or the Current Period. + +**Additionally, ensure the Export to Next Open Period feature is enabled within Expensify:** +1. Navigate to **Settings > Workspaces > Workspace Name > Accounting > Export. +2. Scroll down and confirm that the toggle for **Export to next open period** is enabled. + +If any configuration settings are updated on the NetSuite connection, be sure to sync the connection before trying the export again. + +# ExpensiError NS0055: The Vendor You are Trying to Export to Does Not Have Access to the Currency X + +**Why does this happen?** + +This error occurs when a vendor tied to a report in Expensify does not have access to a currency on the report in NetSuite. The vendor used in NetSuite depends on the type of expenses on the report you're exporting. + +- For **reimbursable** (out-of-pocket) expenses, this is the employee who submitted the report. +- For **non-reimbursable** (e.g., company card) expenses, this is the default vendor set via the Settings > Workspaces > click workspace name > Accounting > Export settings. + +## How to fix it + +To fix this, the vendor needs to be given access to the applicable currency: +1. In NetSuite, navigate to Lists > Relationships > Vendors to access the list of Vendors. +2. Click Edit next to the Vendor tied to the report: + - For reimbursable (out-of-pocket) expenses, this is the report's submitter. + - For non-reimbursable (e.g., company card) expenses, this is the default vendor set via **Settings > Workspaces > click workspace name > Accounting > Export > click Export company card expenses as > Default vendor.** +3. Navigate to the Financial tab. +4. Scroll down to the Currencies section and add all the currencies that are on the report you are trying to export. +5. Click Save. + +# ExpensiError NS0068: You do not have permission to set a value for element - “Created From” + +**Why does this happen?** + +This error typically occurs due to insufficient permissions or misconfigured settings in NetSuite on the preferred transaction form for your export type. + +## How to fix it + +1. In NetSuite, go to Customization > Forms > Transaction Forms. +2. Search for the form type that the report is being exported as in NetSuite (Expense Report, Journal Entry, Vendor Bill, or if the report total is negative, Vendor Credit). +3. Click Edit next to the form that has the Preferred checkbox checked. +4. Go to Screen Fields > Main and ensure the field Created From has the Show checkbox checked. +5. Sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) +6. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + +## ExpensiError NS0068: Reports with Expensify Card expenses + +**Why does this happen?** + +Expensify Card expenses export as Journal Entries. If you encounter this error when exporting a report with Expensify Card non-reimbursable expenses, ensure the field Created From has the Show checkbox checked for Journal Entries in NetSuite. + +## How to fix it +1. In NetSuite, go to Customization > Forms > Transaction Forms. +2. Click Edit next to the journal entry form that has the Preferred checkbox checked. +3. Ensure the field Created From has the Show checkbox checked. +4. Sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) +5. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + +# ExpensiError NS0037: You do not have permission to set a value for element - “Receipt URL” + +**Why does this happen?** + +This error typically occurs due to insufficient permissions or misconfigured settings in NetSuite on the preferred transaction form for your export type. + +## How to fix it + +1. In NetSuite, go to Customization > Forms > Transaction Forms. +2. Search for the form type that the report is being exported as in NetSuite (Expense Report, Journal Entry, or Vendor Bill). +3. Click Edit next to the form that has the Preferred checkbox checked. + - If the report is being exported as an Expense Report: + - Go to Screen Fields > Expenses (the Expenses tab farthest to the right). + - Ensure the field ReceiptURL has the Show checkbox checked. + - If the report is being exported as a Journal Entry: + - Go to Screen Fields > Lines. + - Ensure the field ReceiptURL has the Show checkbox checked. + - If the report is being exported as a Vendor Bill: + - Go to Screen Fields > Main. + - Ensure the field ReceiptURL has the Show checkbox checked. +4. Sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) +5. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + +# ExpensiError NS0042: Error creating vendor - this entity already exists + +**Why does this happen?** + +This error occurs when a vendor record already exists in NetSuite, but Expensify is still attempting to create a new one. This typically means that Expensify cannot find the existing vendor during export. +- The vendor record already exists in NetSuite, but there may be discrepancies preventing Expensify from recognizing it. +- The email on the NetSuite vendor record does not match the email of the report submitter in Expensify. +- The vendor record might not be associated with the correct subsidiary in NetSuite. + +## How to fix it + +1. **Check Email Matching:** + - Ensure the email on the NetSuite vendor record matches the email of the report submitter in Expensify. + - If it doesn’t match update the existing vendor record in NetSuite to match the report submitter's email and name. + - If there is no email listed, add the email address of the report’s submitter to the existing vendor record in NetSuite. +2. **Check Subsidiary Association:** + - Ensure the vendor record is associated with the same subsidiary selected in the connection configurations + - You can review this under **Settings > Workspaces > click workspace name > Accounting > Subsidiary.** +3. **Automatic Vendor Creation:** + - If you want Expensify to automatically create vendors, ensure the "Auto-create employees/vendors" option is enabled under **Settings > Workspaces > click workspace name > Accounting > Advanced.** + - If appropriate, delete the existing vendor record in NetSuite to allow Expensify to create a new one. +4. After making the necessary changes, sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) +5. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + +# ExpensiError NS0109: Failed to login to NetSuite, please verify your credentials + +**Why does this happen?** + +This error indicates a problem with the tokens created for the connection between Expensify and NetSuite. The error message will say, "Login Error. Please check your credentials." + +## How to fix it + +1. Review the [Connect to NetSuite](https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite) guide and follow steps 1 and 2 exactly as outlined. +2. If you're using an existing token and encounter a problem, you may need to create a new token. + +# ExpensiError NS0123 Login Error: Please make sure that the Expensify integration is enabled + +**Why does this happen?** + +This error indicates that the Expensify integration is not enabled in NetSuite. + +## How to fix it + +1. **Enable the Expensify Integration:** + - In NetSuite, navigate to Setup > Integrations > Manage Integrations. + - Ensure that the Expensify Integration is listed and that the State is Enabled. +2. **If you can't find the Expensify integration:** + - Click "Show Inactives" to see if Expensify is listed as inactive. + - If Expensify is listed, update its state to Enabled. +3. Once the Expensify integration is enabled, sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) + +# ExpensiError NS0045: Expenses Not Categorized with a NetSuite Account + +**Why does this happen?** + +This happens when approved expenses are categorized with an option that didn’t import from NetSuite. For NetSuite to accept expense coding, it must first exits and be imported into Expensify from NetSuite. + +## How to fix it + +1. Log into NetSuite +2. Do a global search for the missing record. + - Ensure the expense category is active and correctly named. + - Ensure the category is associated with the correct subsidiary that the Expensify workspace is linked to. +3. Sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) +4. Go back to the report, click on the offending expense(s), and re-apply the category in question. +5. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + + +# ExpensiError NS0061: Please Enter Value(s) for: Tax Code + +**Why does this happen?** + +This error typically occurs when attempting to export expense reports to a Canadian subsidiary in NetSuite for the first time and/or if your subsidiary in NetSuite has Tax enabled. + +## How to fix it + +To fix this, you need to enable Tax in the NetSuite configuration settings. + +1. Go to **Settings > Workspaces > click workspace name > Accounting > Export**. + - Select a Journal Entry tax posting account if you plan on exporting any expenses with taxes. +2. Wait for the connection to sync, it will automatically do so after you make a change. +3. Attempt the export again. + +**Note:** Expenses created before Tax was enabled might need to have the newly imported taxes applied to them retroactively to be exported. + +# Error creating employee: Your current role does not have permission to access this record. + +**Why does this happen?** + +This error indicates that the credentials or role used to connect NetSuite to Expensify do not have the necessary permissions within NetSuite. You can find setup instructions for configuring permissions in NetSuite [here](https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite). + +## How to fix it + +1. If permissions are configured correctly, confirm the report submitter exists in the subsidiary set for the workspace connection and that their Expensify email address matches the email on the NetSuite Employee Record. +2. If the above is true, try toggling off _Auto create employees/vendors_ under the **Settings > Workspaces > Group > click workspace name > Accounting > Advanced tab of the NetSuite configuration window. +3. Sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) +4. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + +# Elimination Settings for X Do Not Match + +**Why does this happen?** + +This error occurs when an Intercompany Payable account is set as the default in the Default Payable Account field in the NetSuite subsidiary preferences, and the Accounting Approval option is enabled for Expense Reports. + +## How to fix it + +Set the Default Payable Account for Expense Reports on each subsidiary in NetSuite to ensure the correct payable account is active. + +1. Navigate to Subsidiaries: + - Go to Setup > Company > Subsidiaries. +2. Edit Subsidiary Preferences: + - Click Edit for the desired subsidiary. + - Go to the Preferences tab. +3. Set Default Payable Account: + - Choose the preferred account for Default Payable Account for Expense Reports. + +Repeat these steps for each subsidiary to ensure the settings are correct, and then sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) + +# ExpensiError NS0046: Billable Expenses Not Coded with a NetSuite Customer or Billable Project + +**Why does this happen?** + +NetSuite requires billable expenses to be assigned to a Customer or a Project that is configured as billable to a Customer. If this is not set up correctly in NetSuite, this error can occur. + +## How to fix it + +1. Check the billable expenses and confirm that a Customer or Project tag is selected. +2. Make any necessary adjustments to the billable expense. +3. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. +Click on the report in question and it will open in the right-hand panel. +Click on Export to NetSuite to try to export again. + +{% include faq-begin.md %} +## Why are reports exporting as `Accounting Approved` instead of `Paid in Full`? + +**This can occur for two reasons:** +- Missing Locations, Classes, or Departments in the Bill Payment Form +- Incorrect Settings in Expensify Workspace Configuration + +**Missing Locations, Classes, or Departments in Bill Payment Form:** If locations, classes, or departments are required in your accounting classifications but are not marked as 'Show' on the preferred bill payment form, this error can occur, and you will need to update the bill payment form in NetSuite: + +1. Go to Customization > Forms > Transaction Forms. +2. Find your preferred (checkmarked) Bill Payment form. +3. Click Edit or Customize. +4. Under the Screen Fields > Main tab, check 'Show' near the department, class, and location options. + +**Incorrect Settings in Expensify Workspace Configuration:** To fix this, you'll want to confirm the NetSuite connection settings are set up correctly in Expensify: + +1. Head to **Settings > Workspaces > click workspace name > Accounting > Advanced.** +2. **Ensure the following settings are correct:** + - Sync Reimbursed Reports: Enabled and payment account chosen. + - Journal Entry Approval Level: Approved for Posting. + - A/P Approval Account: This must match the current account being used for bill payment. +3. **Verify A/P Approval Account:** + - To ensure the A/P Approval Account matches the account in NetSuite: + - Go to your bill/expense report causing the error. + - Click Make Payment. + - This account needs to match the account selected in your Expensify configuration. +4. **Check Expense Report List:** + - Make sure this is also the account selected on the expense report by looking at the expense report list. + +Following these steps will help ensure that reports are exported as "Paid in Full" instead of "Accounting Approved." + +## Why are reports exporting as `Pending Approval`? +If reports are exporting as "Pending Approval" instead of "Approved," you'll need to adjust the approval preferences in NetSuite. + +**Exporting as Journal Entries/Vendor Bills:** +1. In NetSuite, go to Setup > Accounting > Accounting Preferences. +2. On the **General** tab, uncheck **Require Approvals on Journal Entries**. +3. On the **Approval Routing** tab, uncheck Journal Entries/Vendor Bills to remove the approval requirement for Journal Entries created in NetSuite. + +**Note:** This change affects all Journal Entries, not just those created by Expensify. + +**Exporting as Expense Reports:** +1. In NetSuite, navigate to Setup > Company > Enable Features. +2. On the "Employee" tab, uncheck "Approval Routing" to remove the approval requirement for Expense Reports created in NetSuite. Please note that this setting also applies to purchase orders. + +## How do I Change the Default Payable Account for Reimbursable Expenses in NetSuite? + +NetSuite is set up with a default payable account that is credited each time reimbursable expenses are exported as Expense Reports to NetSuite (once approved by the supervisor and accounting). If you need to change this to credit a different account, follow the below steps: + +**For OneWorld Accounts:** +1. Navigate to Setup > Company > Subsidiaries in NetSuite. +2. Next to the subsidiary you want to update, click Edit. +3. Click the Preferences tab. +4. In the Default Payable Account for Expense Reports field, select the desired payable account. +5. Click Save. + +**For Non-OneWorld Accounts:** +1. Navigate to Setup > Accounting > Accounting Preferences in NetSuite. +2. Click the Time & Expenses tab. +3. Under the Expenses section, locate the Default Payable Account for Expense Reports field and choose the preferred account. +4. Click Save. + +{% include faq-end.md %} From cb5b0a4a93ef2382a6ff467f9a23b11521fced31 Mon Sep 17 00:00:00 2001 From: Sheena Trepanier Date: Thu, 21 Nov 2024 15:56:13 -0800 Subject: [PATCH 085/104] Update Netsuite-Troubleshooting.md Fixed some minor formatting errors. --- .../connections/netsuite/Netsuite-Troubleshooting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md b/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md index 221079c18156..15a74cf925fa 100644 --- a/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md +++ b/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md @@ -378,7 +378,7 @@ Click on the report in question and it will open in the right-hand panel. Click on Export to NetSuite to try to export again. {% include faq-begin.md %} -## Why are reports exporting as `Accounting Approved` instead of `Paid in Full`? +## Why are reports exporting as _Accounting Approved_ instead of _Paid in Full_? **This can occur for two reasons:** - Missing Locations, Classes, or Departments in the Bill Payment Form @@ -408,7 +408,7 @@ Click on Export to NetSuite to try to export again. Following these steps will help ensure that reports are exported as "Paid in Full" instead of "Accounting Approved." -## Why are reports exporting as `Pending Approval`? +## Why are reports exporting as _Pending Approval_? If reports are exporting as "Pending Approval" instead of "Approved," you'll need to adjust the approval preferences in NetSuite. **Exporting as Journal Entries/Vendor Bills:** From 5c82d2c0cdb6dfdb0ed30c2a98a810054459d1a3 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 22 Nov 2024 10:21:00 +0700 Subject: [PATCH 086/104] fix: clear the data when closing the flow --- .../companyCards/assignCard/AssignCardFeedPage.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index ad1109f665f0..2fe757c4e36f 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -1,9 +1,10 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React from 'react'; +import React, {useEffect} from 'react'; import {useOnyx} from 'react-native-onyx'; import type {SettingsNavigatorParamList} from '@navigation/types'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import * as CompanyCards from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -23,6 +24,12 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { const backTo = route.params?.backTo; const policyID = policy?.id ?? '-1'; + useEffect(() => { + return () => { + CompanyCards.clearAssignCardStepAndData(); + }; + }, []); + switch (currentStep) { case CONST.COMPANY_CARD.STEP.ASSIGNEE: return ; From d9dd7a3f5895fd090c30a9d67626839076f05c46 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 22 Nov 2024 10:36:40 +0700 Subject: [PATCH 087/104] fix: add missing bank name --- src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx index e9ecf11a4dcd..e8c634a30f93 100644 --- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx @@ -70,6 +70,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew } else { const data: Partial = { email: memberLogin, + bankName: selectedFeed, }; let currentStep: AssignCardStep = CONST.COMPANY_CARD.STEP.CARD; From 4137f2fcc4d0fce90b29437dcd9b304fc69376e8 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 22 Nov 2024 11:18:25 +0700 Subject: [PATCH 088/104] fix: add card name --- .../WorkspaceCompanyCardsListHeaderButtons.tsx | 7 ++++++- src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index 1e36881a68a0..b2f6a993a18b 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -13,6 +13,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CardUtils from '@libs/CardUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; @@ -57,7 +58,11 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp let currentStep: AssignCardStep = CONST.COMPANY_CARD.STEP.ASSIGNEE; if (Object.keys(policy?.employeeList ?? {}).length === 1) { - data.email = Object.keys(policy?.employeeList ?? {}).at(0); + const userEmail = Object.keys(policy?.employeeList ?? {}).at(0) ?? ''; + data.email = userEmail; + const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(userEmail); + const memberName = personalDetails?.firstName ? personalDetails.firstName : personalDetails?.login; + data.cardName = `${memberName}'s card`; currentStep = CONST.COMPANY_CARD.STEP.CARD; if (CardUtils.hasOnlyOneCardToAssign(filteredCardList)) { diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx index e8c634a30f93..c5415521cd5c 100644 --- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx @@ -48,6 +48,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew const accountID = Number(route.params.accountID); const memberLogin = personalDetails?.[accountID]?.login ?? ''; + const memberName = personalDetails?.[accountID]?.firstName ? personalDetails?.[accountID]?.firstName : personalDetails?.[accountID]?.login; const availableCompanyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds); const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`); @@ -71,6 +72,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew const data: Partial = { email: memberLogin, bankName: selectedFeed, + cardName: `${memberName}'s card`, }; let currentStep: AssignCardStep = CONST.COMPANY_CARD.STEP.CARD; From 2570632ce5be0391db55a9b1c9f55a074361564f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 22 Nov 2024 11:52:22 +0700 Subject: [PATCH 089/104] fix: delay navigation --- .../companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx | 2 +- src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index b2f6a993a18b..22b17496040e 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -73,7 +73,7 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: Worksp } CompanyCards.setAssignCardStepAndData({data, currentStep}); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed)); + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed))); }; return ( diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx index c5415521cd5c..21e45516c95d 100644 --- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx @@ -86,7 +86,9 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew data, isEditing: false, }); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed, ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, accountID))); + Navigation.setNavigationActionToMicrotaskQueue(() => + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed, ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, accountID))), + ); } }; From 6e2e36e042d41f652f1e878443c32916670708d4 Mon Sep 17 00:00:00 2001 From: Prakash Baskaran Date: Fri, 22 Nov 2024 11:52:25 +0530 Subject: [PATCH 090/104] Update copy on the app preferences page --- src/languages/en.ts | 2 -- src/languages/es.ts | 2 -- src/pages/settings/Preferences/PreferencesPage.tsx | 8 -------- 3 files changed, 12 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 9bea1261ddbd..ea9a294cb3ed 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -316,7 +316,6 @@ const translations = { owner: 'Owner', dateFormat: 'YYYY-MM-DD', send: 'Send', - notifications: 'Notifications', na: 'N/A', noResultsFound: 'No results found', recentDestinations: 'Recent destinations', @@ -1617,7 +1616,6 @@ const translations = { preferencesPage: { appSection: { title: 'App preferences', - subtitle: 'Customize your Expensify account.', }, testSection: { title: 'Test preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index 7e6f8efc897a..5dfdf34ad73e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -307,7 +307,6 @@ const translations = { owner: 'Dueño', dateFormat: 'AAAA-MM-DD', send: 'Enviar', - notifications: 'Notificaciones', na: 'N/A', noResultsFound: 'No se han encontrado resultados', recentDestinations: 'Destinos recientes', @@ -1619,7 +1618,6 @@ const translations = { preferencesPage: { appSection: { title: 'Preferencias de la aplicación', - subtitle: 'Personaliza tu cuenta de Expensify.', }, testSection: { title: 'Preferencias para tests', diff --git a/src/pages/settings/Preferences/PreferencesPage.tsx b/src/pages/settings/Preferences/PreferencesPage.tsx index 6616d342aa3c..9a7f1872b79a 100755 --- a/src/pages/settings/Preferences/PreferencesPage.tsx +++ b/src/pages/settings/Preferences/PreferencesPage.tsx @@ -52,19 +52,11 @@ function PreferencesPage() {
- - {translate('common.notifications')} - {translate('preferencesPage.receiveRelevantFeatureUpdatesAndExpensifyNews')} From 1faf95cc1d1075a172164e1b97e5770796341b03 Mon Sep 17 00:00:00 2001 From: Christina Dobrzynski <51066321+Christinadobrzyn@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:58:21 +0800 Subject: [PATCH 091/104] Update and rename Create-and-Pay-Bills.md to Receive-and-Pay-Bills.md Tracking article https://github.com/Expensify/Expensify/issues/444051 Updating the Create a Bill article so it's about how to Receive a bill --- .../payments/Create-and-Pay-Bills.md | 79 ------------- .../payments/Receive-and-Pay-Bills.md | 111 ++++++++++++++++++ 2 files changed, 111 insertions(+), 79 deletions(-) delete mode 100644 docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md create mode 100644 docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md deleted file mode 100644 index b231984f61e2..000000000000 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Create and Pay Bills -description: Expensify bill management and payment methods. ---- -Streamline your operations by receiving and paying vendor or supplier bills directly in Expensify. Vendors can send bills even if they don't have an Expensify account, and you can manage payments seamlessly. - -# Receive Bills in Expensify -You can receive bills in three ways: -- Directly from Vendors: Provide your Expensify billing email to vendors. -- Forwarding Emails: Forward bills received in your email to Expensify. -- Manual Upload: For physical bills, create a Bill in Expensify from the Reports page. - -# Bill Pay Workflow -1. When a vendor or supplier sends a bill to Expensify, the document is automatically SmartScanned, and a Bill is created. This Bill is managed by the primary domain contact, who can view it on the Reports page within their default group workspace. - -2. Once the Bill is ready for processing, it follows the established approval workflow. As each person approves it, the Bill appears in the next approver’s Inbox. The final approver will pay the Bill using one of the available payment methods. - -3. During this process, the Bill is coded with the appropriate GL codes from your connected accounting software. After completing the approval workflow, the Bill can be exported back to your accounting system. - -# Payment Methods -There are multiple ways to pay Bills in Expensify. Let’s go over each method below. - -## ACH bank-to-bank transfer - -To use this payment method, you must have a [business bank account connected to your Expensify account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account). - -**To pay with an ACH bank-to-bank transfer:** -1. Sign in to your [Expensify web account](www.expensify.com). -2. Go to the Home or Reports page and locate the Bill that needs to be paid. -3. Click the Pay button to be redirected to the Bill. -4. Choose the ACH option from the drop-down list. - -**Fees:** None - -## Credit or Debit Card -This option is available to all US and International customers receiving a bill from a US vendor with a US business bank account. - -**To pay with a credit or debit card:** -1. Sign in to your [Expensify web account](www.expensify.com). -2. Click on the Bill you’d like to pay to see the details. -3. Click the Pay button. -4. Enter your credit card or debit card details. - -**Fees:** 2.9% of the total amount paid. - -## Venmo -If both you and the vendor must have Venmo connected to Expensify, you can pay the bill by following the steps outlined [here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments#setting-up-third-party-payments). - -**Fees:** Venmo charges a 3% sender’s fee. - - -## Pay outside of Expensify -If you are unable to pay using one of the above methods, you can still mark the Bill as paid. This will update its status to indicate that the payment was made outside Expensify. - -**To mark a Bill as paid outside of Expensify:** -1. Sign in to your [Expensify web account](www.expensify.com). -2. Click on the Bill you’d like to pay to see the details. -3. Click the Reimburse button. -4. Choose **I’ll do it manually**. - -**Fees:** None. - -{% include faq-begin.md %} - -## Who receives vendor bills in Expensify? -Bills are sent to the Primary Contact listed under **Settings > Domains > [Domain Name] > Domain Admins**. - -## Who can view and pay a Bill? -Only the primary domain contact can view and pay a Bill. - -## How can others access Bills? -The primary contact can share Bills or grant Copilot access for others to manage payments. - -## Is Bill Pay supported internationally? -Currently, payments are only supported in USD. - -## What's the difference between a Bill and an Invoice in Expensify? -A Bill represents a payable amount owed to a vendor, while an Invoice is a receivable amount owed to you. -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md new file mode 100644 index 000000000000..15aa3c0bc493 --- /dev/null +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md @@ -0,0 +1,111 @@ +--- +title: Receive and Pay Bills +description: Expensify bill management and payment methods. +--- + +Easily receive and pay vendor or supplier bills directly in Expensify. Your vendors don’t even need an Expensify account! Manage everything seamlessly in one place. + +# Receiving Bills + +Expensify makes it easy to receive bills in three simple ways: + +### 1. Directly from Vendors +Share your Expensify billing email with vendors to receive bills automatically. + +- Set a Primary Contact under **Settings > Domains > Domain Admins**. +- Ask vendors to email bills to your billing address: `domainname@expensify.cash` (e.g., for *expensify.com*, use `expensify@expensify.cash`). +- Once emailed, the bill is automatically created in Expensify, ready for payment. + +### 2. Forwarding Emails +Received a bill in your email? Forward it to Expensify. + +- Ensure your Primary Contact is set under **Settings > Domains > Domain Admins**. +- Forward bills to `domainname@expensify.cash`. +- Expensify will create a bill automatically, ready for payment. + +### 3. Manual Upload +Got a paper bill? Create a bill manually in [Expensify](https://www.expensify.com/): + +1. Log in to [Expensify](https://www.expensify.com). +2. Go to **Reports > New Report > Bill**. +3. Enter the invoice details: sender’s email, merchant name, amount, and date. +4. Upload the invoice as a receipt. + + +# Paying Bills in Expensify + +Expensify makes it easy to manage and pay vendor bills with a straightforward workflow and flexible payment options. Here’s how it works: + +## Bill Pay Workflow + +1. **SmartScan & Create**: When a vendor sends a bill, Expensify automatically SmartScans the document and creates a bill. +2. **Submission to Primary Contact**: The bill is submitted to the primary contact, who can review it on the Reports page under their default group policy. +3. **Communication**: If the approver needs clarification, they can communicate directly with the sender via the invoice linked to the bill. +4. **Approval Workflow**: Once reviewed, the bill follows your workspace’s approval process. The final approver handles the payment. +5. **Accounting Integration**: During approval, the bill is coded with the correct GL codes from your connected accounting software. Once approved, it can be exported back to your accounting system. + +## Payment Methods + +Expensify offers several ways to pay bills. Choose the method that works best for you: + +### 1. ACH Bank-to-Bank Transfer + +Fast and fee-free, this method requires a connected [[business bank account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account)]. + +**How to Pay via ACH:** +1. Log in to your [[Expensify web account](https://www.expensify.com/)]. +2. Find the bill on the Home or Reports page. +3. Click **Pay** and select the ACH option. + +**Fees:** None. + +--- + +### 2. Credit or Debit Card + +Pay vendors using a credit or debit card. This option is available for US and international customers paying US vendors with a US business bank account. + +**How to Pay with a Card:** +1. Log in to your [[Expensify web account](https://www.expensify.com/)]. +2. Open the bill details and click **Pay**. +3. Enter your card information to complete the payment. + +**Fees:** 2.9% of the total amount paid. + +--- + +### 3. Venmo + +If both you and the vendor have Venmo accounts connected to Expensify, you can pay through Venmo. Learn how to set up Venmo [[here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments#setting-up-third-party-payments)]. + +**Fees:** Venmo charges a 3% sender’s fee. + +--- + +### 4. Pay Outside Expensify + +If you prefer to pay outside Expensify, you can still track the payment within the platform. + +**How to Mark as Paid Outside Expensify:** +1. Log in to your [[Expensify web account](https://www.expensify.com/)]. +2. Open the bill details and click **Pay**. +3. Select **Mark as Paid** to update its status. + +**Fees:** None. +{% include faq-begin.md %} + +## Who receives vendor bills in Expensify? +bills are sent to the Primary Contact listed under **Settings > Domains > [Domain Name] > Domain Admins**. + +## Who can view and pay a bill? +Only the primary domain contact can view and pay a bill. + +## How can others access bills? +The primary contact can share bills or grant Copilot access for others to manage payments. + +## Is bill Pay supported internationally? +Currently, payments are only supported in USD. + +## What's the difference between a bill and an Invoice in Expensify? +A bill represents a payable amount owed to a vendor, while an Invoice is a receivable amount owed to you. +{% include faq-end.md %} From e15205c6a4747de9bfcfa3ea6d8181cf96aedb6c Mon Sep 17 00:00:00 2001 From: Christina Dobrzynski <51066321+Christinadobrzyn@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:07:43 +0800 Subject: [PATCH 092/104] Update redirects.csv Updating the redirect to the new article title Receive-and-Pay-Bills --- docs/redirects.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index 5c83d510ccb8..e1c0e12eb070 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -603,3 +603,4 @@ https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-T https://help.expensify.com/articles/expensify-classic/spending-insights/Default-Export-Templates,https://help.expensify.com/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports/ https://help.expensify.com/articles/expensify-classic/spending-insights/Other-Export-Options,https://help.expensify.com/articles/expensify-classic/spending-insights/Export-Expenses-And-Reports/ https://help.expensify.com/articles/expensify-classic/travel/Edit-or-cancel-travel-arrangements,https://help.expensify.com/articles/expensify-classic/travel/Book-with-Expensify-Travel +https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills From 4904e4dc9fd90085cabbf698b62c30bf70ac0d5a Mon Sep 17 00:00:00 2001 From: Christina Dobrzynski <51066321+Christinadobrzyn@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:15:49 +0800 Subject: [PATCH 093/104] Update Receive-and-Pay-Bills.md Made a few changes to the webpage --- .../payments/Receive-and-Pay-Bills.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md index 15aa3c0bc493..328b7f2051bc 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Receive-and-Pay-Bills.md @@ -20,7 +20,7 @@ Share your Expensify billing email with vendors to receive bills automatically. Received a bill in your email? Forward it to Expensify. - Ensure your Primary Contact is set under **Settings > Domains > Domain Admins**. -- Forward bills to `domainname@expensify.cash`. +- Forward bills to `domainname@expensify.cash`. Example: `domainname@expensify.cash` (e.g., for *expensify.com*, use `expensify@expensify.cash`). - Expensify will create a bill automatically, ready for payment. ### 3. Manual Upload @@ -50,10 +50,10 @@ Expensify offers several ways to pay bills. Choose the method that works best fo ### 1. ACH Bank-to-Bank Transfer -Fast and fee-free, this method requires a connected [[business bank account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account)]. +Fast and fee-free, this method requires a connected [business bank account](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Connect-US-Business-Bank-Account). **How to Pay via ACH:** -1. Log in to your [[Expensify web account](https://www.expensify.com/)]. +1. Log in to your [Expensify web account](https://www.expensify.com/). 2. Find the bill on the Home or Reports page. 3. Click **Pay** and select the ACH option. @@ -66,7 +66,7 @@ Fast and fee-free, this method requires a connected [[business bank account](htt Pay vendors using a credit or debit card. This option is available for US and international customers paying US vendors with a US business bank account. **How to Pay with a Card:** -1. Log in to your [[Expensify web account](https://www.expensify.com/)]. +1. Log in to your [Expensify web account](https://www.expensify.com/). 2. Open the bill details and click **Pay**. 3. Enter your card information to complete the payment. @@ -76,7 +76,7 @@ Pay vendors using a credit or debit card. This option is available for US and in ### 3. Venmo -If both you and the vendor have Venmo accounts connected to Expensify, you can pay through Venmo. Learn how to set up Venmo [[here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments#setting-up-third-party-payments)]. +If both you and the vendor have Venmo accounts connected to Expensify, you can pay through Venmo. Learn how to set up Venmo [here](https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/Third-Party-Payments#setting-up-third-party-payments). **Fees:** Venmo charges a 3% sender’s fee. @@ -87,7 +87,7 @@ If both you and the vendor have Venmo accounts connected to Expensify, you can p If you prefer to pay outside Expensify, you can still track the payment within the platform. **How to Mark as Paid Outside Expensify:** -1. Log in to your [[Expensify web account](https://www.expensify.com/)]. +1. Log in to your [Expensify web account](https://www.expensify.com/). 2. Open the bill details and click **Pay**. 3. Select **Mark as Paid** to update its status. From fb7a68b236ddb24cb5a6701c85760a65457d3fac Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 22 Nov 2024 09:30:09 +0100 Subject: [PATCH 094/104] Update card display on the members page --- src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index b0066187e6f1..3ffe224dd50a 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -326,7 +326,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM > Date: Fri, 22 Nov 2024 16:34:46 +0800 Subject: [PATCH 095/104] Update Receive-and-Pay-Bills.md made some changes to the linked articles From 2e28afd170c180518454050de82a287e60d15b1e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 22 Nov 2024 11:46:41 +0100 Subject: [PATCH 096/104] Fix minor bug --- .../companyCards/WorkspaceCompanyCardsSettingsPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx index 2fb232360594..8215a3d0bf40 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx @@ -1,5 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useState} from 'react'; +import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; @@ -40,7 +40,8 @@ function WorkspaceCompanyCardsSettingsPage({ const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`); - const selectedFeed = CardUtils.getSelectedFeed(lastSelectedFeed, cardFeeds); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we want to run the hook only once to escape unexpected feed change + const selectedFeed = useMemo(() => CardUtils.getSelectedFeed(lastSelectedFeed, cardFeeds), []); const feedName = CardUtils.getCustomOrFormattedFeedName(selectedFeed, cardFeeds?.settings?.companyCardNicknames); const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds); const liabilityType = selectedFeed && companyFeeds[selectedFeed]?.liabilityType; From 53a4d1fe465a23a9e299b7db448bf0ea7fc858b7 Mon Sep 17 00:00:00 2001 From: Pujan Date: Fri, 22 Nov 2024 17:25:34 +0530 Subject: [PATCH 097/104] workspace delete message fix in profile --- src/pages/workspace/WorkspaceProfilePage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 979df0099d82..482d66846e66 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -61,7 +61,9 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policy?.id ?? '-1'); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); - const hasCardFeedOrExpensifyCard = !isEmptyObject(cardFeeds) || !isEmptyObject(cardsList); + const hasCardFeedOrExpensifyCard = + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + !isEmptyObject(cardFeeds) || !isEmptyObject(cardsList) || ((policy?.areExpensifyCardsEnabled || policy?.areCompanyCardsEnabled) && policy?.workspaceAccountID); const [street1, street2] = (policy?.address?.addressStreet ?? '').split('\n'); const formattedAddress = From 1af3f9dc3e6f1cd11e0724b3f6268cdc041b1d1a Mon Sep 17 00:00:00 2001 From: Jules Rosser Date: Fri, 22 Nov 2024 15:02:31 +0000 Subject: [PATCH 098/104] remove . to align sentence with others in list --- help/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/help/README.md b/help/README.md index d62513f07f53..c0fb4dbf524a 100644 --- a/help/README.md +++ b/help/README.md @@ -48,7 +48,7 @@ Every PR pushed by an authorized Expensify employee or representative will autom 3. Install Ruby and Jekyll 4. Build the entire site using Jekyll 5. Create a "preview" of the newly built site in Cloudflare -6. Record a link to that preview in the PR. +6. Record a link to that preview in the PR ## How to deploy the site for real Whenever a PR that touches the `/help` directory is merged, it will re-run the build just like before. However, it will detect that this build is being run from the `main` branch, and thus push the changes to the `production` Cloudflare environment -- meaning, it will replace the contents hosted at https://newhelp.expensify.com From 2be41e22f0f4652193fb52cac1a475846960cc00 Mon Sep 17 00:00:00 2001 From: John Schuster Date: Fri, 22 Nov 2024 09:12:45 -0600 Subject: [PATCH 099/104] Update Claim-And-Verify-A-Domain.md deleting the `space` to fix the markdown formatting. --- .../expensify-classic/domains/Claim-And-Verify-A-Domain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md b/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md index ed74224c622e..ecb0b938aa8e 100644 --- a/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md +++ b/docs/articles/expensify-classic/domains/Claim-And-Verify-A-Domain.md @@ -46,6 +46,6 @@ After successful verification, an email will be sent to all members of the Expen # Add another domain -To add an additional domain, you’ll have to first add your email address that is connected with your domain as your [primary or secondary email] (https://help.expensify.com/articles/expensify-classic/settings/account-settings/Change-or-add-email-address) (for example, if your domain is yourcompany.com, then you want to add and verify your email address @yourcompany.com as your primary or secondary email address). Then you can complete the steps above to add the domain. +To add an additional domain, you’ll have to first add your email address that is connected with your domain as your [primary or secondary email](https://help.expensify.com/articles/expensify-classic/settings/account-settings/Change-or-add-email-address) (for example, if your domain is yourcompany.com, then you want to add and verify your email address @yourcompany.com as your primary or secondary email address). Then you can complete the steps above to add the domain. From f6387b6692d948e90471703b46d7192795a5a94a Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 22 Nov 2024 15:15:10 +0000 Subject: [PATCH 100/104] Update version to 9.0.65-5 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ce5927fc2ad9..ce5c92aec37f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009006504 - versionName "9.0.65-4" + versionCode 1009006505 + versionName "9.0.65-5" // 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/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 91cb6a7f745b..48ab1740093e 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.65.4 + 9.0.65.5 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 38b36ea381f6..c283e62e44af 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.65.4 + 9.0.65.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index def1f5fd29dd..7db594f06494 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.65 CFBundleVersion - 9.0.65.4 + 9.0.65.5 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 90868fbb3a55..318b08eaf217 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.65-4", + "version": "9.0.65-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.65-4", + "version": "9.0.65-5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5be8ade75636..57de821f096e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.65-4", + "version": "9.0.65-5", "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.", From d0918fe29b8597d3ef834ce33c0b87d65c57c5ff Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 22 Nov 2024 23:12:10 +0700 Subject: [PATCH 101/104] rename param --- src/libs/actions/IOU.ts | 10 ++++---- .../iou/request/step/IOURequestStepAmount.tsx | 2 +- .../step/IOURequestStepConfirmation.tsx | 4 ++-- .../step/IOURequestStepScan/index.native.tsx | 6 ++--- .../request/step/IOURequestStepScan/index.tsx | 6 ++--- tests/actions/IOUTest.ts | 24 +++++++++---------- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index b72b95a0c44f..a28c4490c7c9 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -197,8 +197,8 @@ type RequestMoneyParticipantParams = { type RequestMoneyInformation = { report: OnyxEntry; - participantData: RequestMoneyParticipantParams; - policyData?: RequestMoneyPolicyParams; + participantParams: RequestMoneyParticipantParams; + policyParams?: RequestMoneyPolicyParams; gpsPoints?: GPSPoint; action?: IOUAction; reimbursible?: boolean; @@ -3565,9 +3565,9 @@ function shareTrackedExpense( * Submit expense to another user */ function requestMoney(requestMoneyInformation: RequestMoneyInformation) { - const {report, participantData, policyData = {}, transactionData, gpsPoints, action, reimbursible} = requestMoneyInformation; - const {participant, payeeAccountID, payeeEmail} = participantData; - const {policy, policyCategories, policyTagList} = policyData; + const {report, participantParams, policyParams = {}, transactionData, gpsPoints, action, reimbursible} = requestMoneyInformation; + const {participant, payeeAccountID, payeeEmail} = participantParams; + const {policy, policyCategories, policyTagList} = policyParams; const { amount, currency, diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 83cdfa194777..5a9fa9358355 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -212,7 +212,7 @@ function IOURequestStepAmount({ playSound(SOUNDS.DONE); IOU.requestMoney({ report, - participantData: { + participantParams: { participant: participants.at(0) ?? {}, payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 7df544e967f9..1c3a34bfff42 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -240,12 +240,12 @@ function IOURequestStepConfirmation({ } IOU.requestMoney({ report, - participantData: { + participantParams: { payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant, }, - policyData: { + policyParams: { policy, policyTagList: policyTags, policyCategories, diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 0416315bbafc..29dccaa0f9f1 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -257,7 +257,7 @@ function IOURequestStepScan({ } else { IOU.requestMoney({ report, - participantData: { + participantParams: { payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant, @@ -357,12 +357,12 @@ function IOURequestStepScan({ } else { IOU.requestMoney({ report, - participantData: { + participantParams: { payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant, }, - policyData: { + policyParams: { policy, }, gpsPoints: { diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index e646583bb67b..eb40fba34ddf 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -286,7 +286,7 @@ function IOURequestStepScan({ } else { IOU.requestMoney({ report, - participantData: { + participantParams: { payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant, @@ -387,12 +387,12 @@ function IOURequestStepScan({ } else { IOU.requestMoney({ report, - participantData: { + participantParams: { payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, participant, }, - policyData: { + policyParams: { policy, }, gpsPoints: { diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 909ec1b2068a..1928ed694228 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -82,7 +82,7 @@ describe('actions/IOU', () => { mockFetch?.pause?.(); IOU.requestMoney({ report: {reportID: ''}, - participantData: { + participantParams: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, @@ -296,7 +296,7 @@ describe('actions/IOU', () => { .then(() => { IOU.requestMoney({ report: chatReport, - participantData: { + participantParams: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, @@ -515,7 +515,7 @@ describe('actions/IOU', () => { if (chatReport) { IOU.requestMoney({ report: chatReport, - participantData: { + participantParams: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, @@ -670,7 +670,7 @@ describe('actions/IOU', () => { mockFetch?.pause?.(); IOU.requestMoney({ report: {reportID: ''}, - participantData: { + participantParams: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, @@ -1486,7 +1486,7 @@ describe('actions/IOU', () => { let transaction: OnyxEntry; IOU.requestMoney({ report: {reportID: ''}, - participantData: { + participantParams: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, @@ -1722,7 +1722,7 @@ describe('actions/IOU', () => { if (chatReport) { IOU.requestMoney({ report: chatReport, - participantData: { + participantParams: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, @@ -1850,7 +1850,7 @@ describe('actions/IOU', () => { if (chatReport) { IOU.requestMoney({ report: chatReport, - participantData: { + participantParams: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, @@ -1951,7 +1951,7 @@ describe('actions/IOU', () => { // When a submit IOU expense is made IOU.requestMoney({ report: chatReport, - participantData: { + participantParams: { payeeEmail: TEST_USER_LOGIN, payeeAccountID: TEST_USER_ACCOUNT_ID, participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, @@ -2700,7 +2700,7 @@ describe('actions/IOU', () => { if (chatReport) { IOU.requestMoney({ report: chatReport, - participantData: { + participantParams: { payeeEmail: TEST_USER_LOGIN, payeeAccountID: TEST_USER_ACCOUNT_ID, participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, @@ -2918,7 +2918,7 @@ describe('actions/IOU', () => { if (chatReport) { IOU.requestMoney({ report: chatReport, - participantData: { + participantParams: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, @@ -3025,7 +3025,7 @@ describe('actions/IOU', () => { if (chatReport) { IOU.requestMoney({ report: chatReport, - participantData: { + participantParams: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, @@ -3133,7 +3133,7 @@ describe('actions/IOU', () => { if (chatReport) { IOU.requestMoney({ report: chatReport, - participantData: { + participantParams: { payeeEmail: RORY_EMAIL, payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, From e08c53a2ea6a9a2aa3eba94c0f03453c80471f80 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 22 Nov 2024 23:23:50 +0700 Subject: [PATCH 102/104] rename transactionData --- src/libs/actions/IOU.ts | 8 +++---- .../iou/request/step/IOURequestStepAmount.tsx | 2 +- .../step/IOURequestStepConfirmation.tsx | 2 +- .../step/IOURequestStepScan/index.native.tsx | 4 ++-- .../request/step/IOURequestStepScan/index.tsx | 4 ++-- tests/actions/IOUTest.ts | 24 +++++++++---------- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index a28c4490c7c9..10eee66428e8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -165,7 +165,7 @@ type GPSPoint = { long: number; }; -type RequestMoneyTransactionData = { +type RequestMoneyTransactionParams = { attendees: Attendee[] | undefined; amount: number; currency: string; @@ -202,7 +202,7 @@ type RequestMoneyInformation = { gpsPoints?: GPSPoint; action?: IOUAction; reimbursible?: boolean; - transactionData: RequestMoneyTransactionData; + transactionParams: RequestMoneyTransactionParams; }; let allPersonalDetails: OnyxTypes.PersonalDetailsList = {}; @@ -3565,7 +3565,7 @@ function shareTrackedExpense( * Submit expense to another user */ function requestMoney(requestMoneyInformation: RequestMoneyInformation) { - const {report, participantParams, policyParams = {}, transactionData, gpsPoints, action, reimbursible} = requestMoneyInformation; + const {report, participantParams, policyParams = {}, transactionParams, gpsPoints, action, reimbursible} = requestMoneyInformation; const {participant, payeeAccountID, payeeEmail} = participantParams; const {policy, policyCategories, policyTagList} = policyParams; const { @@ -3584,7 +3584,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation) { actionableWhisperReportActionID, linkedTrackedExpenseReportAction, linkedTrackedExpenseReportID, - } = transactionData; + } = transactionParams; // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 5a9fa9358355..72a931bf359c 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -217,7 +217,7 @@ function IOURequestStepAmount({ payeeEmail: currentUserPersonalDetails.login, payeeAccountID: currentUserPersonalDetails.accountID, }, - transactionData: { + transactionParams: { amount: backendAmount, currency, created: transaction?.created ?? '', diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 1c3a34bfff42..d8c48dc3d587 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -252,7 +252,7 @@ function IOURequestStepConfirmation({ }, gpsPoints, action, - transactionData: { + transactionParams: { amount: transaction.amount, attendees: transaction.attendees, currency: transaction.currency, diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 29dccaa0f9f1..d491c196e9c2 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -262,7 +262,7 @@ function IOURequestStepScan({ payeeAccountID: currentUserPersonalDetails.accountID, participant, }, - transactionData: { + transactionParams: { amount: 0, attendees: transaction?.attendees, currency: transaction?.currency ?? 'USD', @@ -369,7 +369,7 @@ function IOURequestStepScan({ lat: successData.coords.latitude, long: successData.coords.longitude, }, - transactionData: { + transactionParams: { amount: 0, attendees: transaction?.attendees, currency: transaction?.currency ?? 'USD', diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index eb40fba34ddf..bc8622072226 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -291,7 +291,7 @@ function IOURequestStepScan({ payeeAccountID: currentUserPersonalDetails.accountID, participant, }, - transactionData: { + transactionParams: { amount: 0, attendees: transaction?.attendees, currency: transaction?.currency ?? 'USD', @@ -399,7 +399,7 @@ function IOURequestStepScan({ lat: successData.coords.latitude, long: successData.coords.longitude, }, - transactionData: { + transactionParams: { amount: 0, attendees: transaction?.attendees, currency: transaction?.currency ?? 'USD', diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 1928ed694228..4430ec0ce052 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -87,7 +87,7 @@ describe('actions/IOU', () => { payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -301,7 +301,7 @@ describe('actions/IOU', () => { payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -520,7 +520,7 @@ describe('actions/IOU', () => { payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -675,7 +675,7 @@ describe('actions/IOU', () => { payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -1491,7 +1491,7 @@ describe('actions/IOU', () => { payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -1727,7 +1727,7 @@ describe('actions/IOU', () => { payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -1855,7 +1855,7 @@ describe('actions/IOU', () => { payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -1956,7 +1956,7 @@ describe('actions/IOU', () => { payeeAccountID: TEST_USER_ACCOUNT_ID, participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -2705,7 +2705,7 @@ describe('actions/IOU', () => { payeeAccountID: TEST_USER_ACCOUNT_ID, participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, }, - transactionData: { + transactionParams: { amount: amount2, attendees: [], currency: CONST.CURRENCY.USD, @@ -2923,7 +2923,7 @@ describe('actions/IOU', () => { payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -3030,7 +3030,7 @@ describe('actions/IOU', () => { payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, @@ -3138,7 +3138,7 @@ describe('actions/IOU', () => { payeeAccountID: RORY_ACCOUNT_ID, participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport.reportID}, }, - transactionData: { + transactionParams: { amount, attendees: [], currency: CONST.CURRENCY.USD, From 999d8372f0c05d61ff9e22305f134741e2db9c2d Mon Sep 17 00:00:00 2001 From: Jack Nam <30609178+thienlnam@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:50:37 -0800 Subject: [PATCH 103/104] Revert "fix: Submit button hang in middle page" --- .../MoneyRequestConfirmationList.tsx | 22 ++--------- src/components/withKeyboardState.tsx | 8 +--- .../index.ts | 37 ------------------- 3 files changed, 5 insertions(+), 62 deletions(-) delete mode 100644 src/hooks/useIsWindowHeightReducedByKeyboard/index.ts diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 2f1d459e369a..a0143f87e789 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,6 +1,6 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; -import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; @@ -46,7 +46,6 @@ import type {SectionListDataType} from './SelectionList/types'; import UserListItem from './SelectionList/UserListItem'; import SettlementButton from './SettlementButton'; import Text from './Text'; -import {KeyboardStateContext} from './withKeyboardState'; type MoneyRequestConfirmationListProps = { /** Callback to inform parent modal of success */ @@ -195,7 +194,7 @@ function MoneyRequestConfirmationList({ const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {isKeyboardShown, isWindowHeightReducedByKeyboard} = useContext(KeyboardStateContext); + const isTypeRequest = iouType === CONST.IOU.TYPE.SUBMIT; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.PAY; @@ -825,7 +824,7 @@ function MoneyRequestConfirmationList({ }, [routeError, isTypeSplit, shouldShowReadOnlySplits, debouncedFormError, formError, translate]); const footerContent = useMemo(() => { - if (isReadOnly || isKeyboardShown || isWindowHeightReducedByKeyboard) { + if (isReadOnly) { return; } @@ -877,20 +876,7 @@ function MoneyRequestConfirmationList({ {button} ); - }, [ - isReadOnly, - iouType, - confirm, - bankAccountRoute, - iouCurrencyCode, - policyID, - splitOrRequestOptions, - styles.ph1, - styles.mb2, - errorMessage, - isKeyboardShown, - isWindowHeightReducedByKeyboard, - ]); + }, [isReadOnly, iouType, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, styles.ph1, styles.mb2, errorMessage]); const listFooterContent = ( ({ @@ -23,7 +19,6 @@ const KeyboardStateContext = createContext({ function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null { const [keyboardHeight, setKeyboardHeight] = useState(0); - const isWindowHeightReducedByKeyboard = useIsWindowHeightReducedByKeyboard(); useEffect(() => { const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', (e) => { @@ -43,9 +38,8 @@ function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null { () => ({ keyboardHeight, isKeyboardShown: keyboardHeight !== 0, - isWindowHeightReducedByKeyboard, }), - [keyboardHeight, isWindowHeightReducedByKeyboard], + [keyboardHeight], ); return {children}; } diff --git a/src/hooks/useIsWindowHeightReducedByKeyboard/index.ts b/src/hooks/useIsWindowHeightReducedByKeyboard/index.ts deleted file mode 100644 index 7895c7209115..000000000000 --- a/src/hooks/useIsWindowHeightReducedByKeyboard/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {useCallback, useEffect, useState} from 'react'; -import usePrevious from '@hooks/usePrevious'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useWindowDimensions from '@hooks/useWindowDimensions'; - -const useIsWindowHeightReducedByKeyboard = () => { - const [isWindowHeightReducedByKeyboard, setIsWindowHeightReducedByKeyboard] = useState(false); - const {windowHeight} = useWindowDimensions(); - const prevWindowHeight = usePrevious(windowHeight); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const toggleKeyboardOnSmallScreens = useCallback( - (isKBOpen: boolean) => { - if (!shouldUseNarrowLayout) { - return; - } - setIsWindowHeightReducedByKeyboard(isKBOpen); - }, - [shouldUseNarrowLayout], - ); - useEffect(() => { - // Use window height changes to toggle the keyboard. To maintain keyboard state - // on all platforms we also use focus/blur events. So we need to make sure here - // that we avoid redundant keyboard toggling. - // Minus 100px is needed to make sure that when the internet connection is - // disabled in android chrome and a small 'No internet connection' text box appears, - // we do not take it as a sign to open the keyboard - if (!isWindowHeightReducedByKeyboard && windowHeight < prevWindowHeight - 100) { - toggleKeyboardOnSmallScreens(true); - } else if (isWindowHeightReducedByKeyboard && windowHeight > prevWindowHeight) { - toggleKeyboardOnSmallScreens(false); - } - }, [isWindowHeightReducedByKeyboard, prevWindowHeight, toggleKeyboardOnSmallScreens, windowHeight]); - - return isWindowHeightReducedByKeyboard; -}; - -export default useIsWindowHeightReducedByKeyboard; From ef0d341c7d15ce1b636cf5bb70b1f389293af297 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 22 Nov 2024 19:31:38 +0000 Subject: [PATCH 104/104] Update version to 9.0.66-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- ios/NotificationServiceExtension/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index ce5c92aec37f..907813d56e2e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009006505 - versionName "9.0.65-5" + versionCode 1009006600 + versionName "9.0.66-0" // 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/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 48ab1740093e..0feab9ddaced 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.65 + 9.0.66 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.65.5 + 9.0.66.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index c283e62e44af..d1921d0b1b65 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.65 + 9.0.66 CFBundleSignature ???? CFBundleVersion - 9.0.65.5 + 9.0.66.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 7db594f06494..111c5363813a 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.65 + 9.0.66 CFBundleVersion - 9.0.65.5 + 9.0.66.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 318b08eaf217..83d0c122ab2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.65-5", + "version": "9.0.66-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.65-5", + "version": "9.0.66-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 57de821f096e..4df25219e277 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.65-5", + "version": "9.0.66-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",