From e4194a128f89a1a44b27bf17583dd0b56dea059b Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 13 Aug 2024 16:32:48 +0200 Subject: [PATCH 1/4] Implement OpenCardDetailsPage api call --- .../API/parameters/OpenCardDetailsPageParams.ts | 6 ++++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ src/libs/actions/Card.ts | 17 +++++++++++++++++ .../WorkspaceExpensifyCardDetailsPage.tsx | 7 ++++++- 5 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/libs/API/parameters/OpenCardDetailsPageParams.ts diff --git a/src/libs/API/parameters/OpenCardDetailsPageParams.ts b/src/libs/API/parameters/OpenCardDetailsPageParams.ts new file mode 100644 index 000000000000..e279326c8a82 --- /dev/null +++ b/src/libs/API/parameters/OpenCardDetailsPageParams.ts @@ -0,0 +1,6 @@ +type OpenCardDetailsPageParams = { + authToken: string; + cardID: number; +}; + +export default OpenCardDetailsPageParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 81c6c8887c3f..8e8c46c0b82c 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -272,3 +272,4 @@ export type {CreateWorkspaceApprovalParams, UpdateWorkspaceApprovalParams, Remov export type {default as StartIssueNewCardFlowParams} from './StartIssueNewCardFlowParams'; export type {default as ConfigureExpensifyCardsForPolicyParams} from './ConfigureExpensifyCardsForPolicyParams'; export type {default as CreateExpensifyCardParams} from './CreateExpensifyCardParams'; +export type {default as OpenCardDetailsPageParams} from './OpenCardDetailsPageParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 9e8c8995c09f..f78ff6efb393 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -716,6 +716,7 @@ const READ_COMMANDS = { OPEN_SUBSCRIPTION_PAGE: 'OpenSubscriptionPage', OPEN_DRAFT_DISTANCE_EXPENSE: 'OpenDraftDistanceExpense', START_ISSUE_NEW_CARD_FLOW: 'StartIssueNewCardFlow', + OPEN_CARD_DETAILS_PAGE: 'OpenCardDetailsPage', } as const; type ReadCommand = ValueOf; @@ -772,6 +773,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE]: null; [READ_COMMANDS.OPEN_DRAFT_DISTANCE_EXPENSE]: null; [READ_COMMANDS.START_ISSUE_NEW_CARD_FLOW]: Parameters.StartIssueNewCardFlowParams; + [READ_COMMANDS.OPEN_CARD_DETAILS_PAGE]: Parameters.OpenCardDetailsPageParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 52e028e3909a..26d871381300 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -4,6 +4,7 @@ import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import type { ActivatePhysicalExpensifyCardParams, + OpenCardDetailsPageParams, ReportVirtualExpensifyCardFraudParams, RequestReplacementExpensifyCardParams, RevealExpensifyCardDetailsParams, @@ -418,6 +419,21 @@ function issueExpensifyCard(policyID: string, feedCountry: string, data?: IssueN API.write(WRITE_COMMANDS.CREATE_ADMIN_ISSUED_VIRTUAL_CARD, parameters); } +function openCardDetailsPage(cardID: number) { + const authToken = NetworkStore.getAuthToken(); + + if (!authToken) { + return; + } + + const parameters: OpenCardDetailsPageParams = { + authToken, + cardID, + }; + + API.read(READ_COMMANDS.OPEN_CARD_DETAILS_PAGE, parameters); +} + export { requestReplacementExpensifyCard, activatePhysicalExpensifyCard, @@ -432,5 +448,6 @@ export { startIssueNewCardFlow, configureExpensifyCardsForPolicy, issueExpensifyCard, + openCardDetailsPage, }; export type {ReplacementReason}; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx index 7c5b26ff180a..6e0a211a14ee 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -1,5 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import ExpensifyCardImage from '@assets/images/expensify-card.svg'; @@ -24,6 +24,7 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; +import * as Card from '@userActions/Card'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -52,6 +53,10 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(cardholder); const translationForLimitType = CardUtils.getTranslationKeyForLimitType(card?.nameValuePairs?.limitType); + useEffect(() => { + Card.openCardDetailsPage(Number(cardID)); + }, []); + const deactivateCard = () => { setIsDeactivateModalVisible(false); From f79957ced1bffcaf298eb7687c8e7d3d0c59b478 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 13 Aug 2024 16:52:33 +0200 Subject: [PATCH 2/4] Add sortCardsByCardholderName method. Turn currentBalance, remainingLimit and earnedCashback fields into optional. --- src/libs/CardUtils.ts | 17 ++++++++++++++++- .../WorkspaceExpensifyCardListPage.tsx | 15 ++------------- src/types/onyx/ExpensifyCardSettings.ts | 6 +++--- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index a5dfe69bf40e..516b9b610c04 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -2,11 +2,13 @@ import lodash from 'lodash'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import localeCompare from '@libs/LocaleCompare'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {BankAccountList, Card, CardList} from '@src/types/onyx'; +import type {BankAccountList, Card, CardList, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as Localize from './Localize'; @@ -166,6 +168,18 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry bankAccount?.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && bankAccount?.accountData?.allowDebit); } +function sortCardsByCardholderName(cardsList: OnyxEntry, personalDetails: OnyxEntry): Card[] { + return Object.values(cardsList ?? {}).sort((cardA: Card, cardB: Card) => { + const userA = personalDetails?.[cardA.accountID ?? '-1'] ?? {}; + const userB = personalDetails?.[cardB.accountID ?? '-1'] ?? {}; + + const aName = PersonalDetailsUtils.getDisplayNameOrDefault(userA); + const bName = PersonalDetailsUtils.getDisplayNameOrDefault(userB); + + return localeCompare(aName, bName); + }); +} + export { isExpensifyCard, isCorporateCard, @@ -180,4 +194,5 @@ export { getMCardNumberString, getTranslationKeyForLimitType, getEligibleBankAccountsForCard, + sortCardsByCardholderName, }; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx index 769e94c4ecc4..c27d5a7168ba 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx @@ -15,8 +15,7 @@ import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import localeCompare from '@libs/LocaleCompare'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import * as CardUtils from '@libs/CardUtils'; import Navigation from '@navigation/Navigation'; import type {FullScreenNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; @@ -48,17 +47,7 @@ function WorkspaceExpensifyCardListPage({route, cardsList}: WorkspaceExpensifyCa const policyCurrency = useMemo(() => policy?.outputCurrency ?? CONST.CURRENCY.USD, [policy]); - const sortedCards = useMemo( - () => - Object.values(cardsList ?? {}).sort((cardA: Card, cardB: Card) => { - const userA = personalDetails?.[cardA.accountID ?? '-1'] ?? {}; - const userB = personalDetails?.[cardB.accountID ?? '-1'] ?? {}; - const aName = PersonalDetailsUtils.getDisplayNameOrDefault(userA); - const bName = PersonalDetailsUtils.getDisplayNameOrDefault(userB); - return localeCompare(aName, bName); - }), - [cardsList, personalDetails], - ); + const sortedCards = useMemo(() => CardUtils.sortCardsByCardholderName(cardsList, personalDetails), [cardsList, personalDetails]); const getHeaderButtons = () => ( diff --git a/src/types/onyx/ExpensifyCardSettings.ts b/src/types/onyx/ExpensifyCardSettings.ts index e5846e1bcab6..05c6667e7c05 100644 --- a/src/types/onyx/ExpensifyCardSettings.ts +++ b/src/types/onyx/ExpensifyCardSettings.ts @@ -3,13 +3,13 @@ import type * as OnyxCommon from './OnyxCommon'; /** Model of Expensify card settings for a workspace */ type ExpensifyCardSettings = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Sum of all posted Expensify Card transactions */ - currentBalance: number; + currentBalance?: number; /** Remaining limit for Expensify Cards on the workspace */ - remainingLimit: number; + remainingLimit?: number; /** The total amount of cash back earned thus far */ - earnedCashback: number; + earnedCashback?: number; /** The date of the last settlement */ monthlySettlementDate: Date; From 684cfeb6202e9bab0eee49d76c750ca8a62120ba Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 14 Aug 2024 12:38:31 +0200 Subject: [PATCH 3/4] Lint fixes --- src/libs/CardUtils.ts | 4 ++-- .../expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 516b9b610c04..e0041dde9934 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -2,15 +2,15 @@ import lodash from 'lodash'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import localeCompare from '@libs/LocaleCompare'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {BankAccountList, Card, CardList, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import localeCompare from './LocaleCompare'; import * as Localize from './Localize'; +import * as PersonalDetailsUtils from './PersonalDetailsUtils'; let allCards: OnyxValues[typeof ONYXKEYS.CARD_LIST] = {}; Onyx.connect({ diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx index 6e0a211a14ee..0a1abe4442e8 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -55,7 +55,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail useEffect(() => { Card.openCardDetailsPage(Number(cardID)); - }, []); + }, [cardID]); const deactivateCard = () => { setIsDeactivateModalVisible(false); From d692b677e11f4f9b672bc7b4603010ba121815e9 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 14 Aug 2024 14:58:56 +0200 Subject: [PATCH 4/4] Update availableSpend value optimistically --- src/libs/actions/Card.ts | 4 +++- .../WorkspaceEditCardLimitPage.tsx | 21 ++++++++++++------- .../WorkspaceExpensifyCardDetailsPage.tsx | 11 +++++++--- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 1ebc9cfadbf3..2356f125ff67 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -315,7 +315,7 @@ function clearIssueNewCardFlow() { }); } -function updateExpensifyCardLimit(workspaceAccountID: number, cardID: number, newLimit: number, oldLimit?: number) { +function updateExpensifyCardLimit(workspaceAccountID: number, cardID: number, newLimit: number, newAvailableSpend: number, oldLimit?: number, oldAvailableSpend?: number) { const authToken = NetworkStore.getAuthToken(); if (!authToken) { @@ -328,6 +328,7 @@ function updateExpensifyCardLimit(workspaceAccountID: number, cardID: number, ne key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`, value: { [cardID]: { + availableSpend: newAvailableSpend, nameValuePairs: { unapprovedExpenseLimit: newLimit, }, @@ -356,6 +357,7 @@ function updateExpensifyCardLimit(workspaceAccountID: number, cardID: number, ne key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`, value: { [cardID]: { + availableSpend: oldAvailableSpend, nameValuePairs: { unapprovedExpenseLimit: oldLimit, }, diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index c63b5c13d270..3b44654b754a 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -52,26 +52,33 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { } }, [card?.nameValuePairs?.limitType]); - const updateCardLimit = (newLimit: string) => { + const getNewAvailableSpend = (newLimit: number) => { + const currentLimit = card?.nameValuePairs?.unapprovedExpenseLimit ?? 0; + const currentSpend = currentLimit - (card?.availableSpend ?? 0); + + return newLimit - currentSpend; + }; + + const updateCardLimit = (newLimit: number) => { + const newAvailableSpend = getNewAvailableSpend(newLimit); + setIsConfirmModalVisible(false); - Card.updateExpensifyCardLimit(workspaceAccountID, Number(cardID), Number(newLimit) * 100, card?.nameValuePairs?.unapprovedExpenseLimit); + Card.updateExpensifyCardLimit(workspaceAccountID, Number(cardID), newLimit, newAvailableSpend, card?.nameValuePairs?.unapprovedExpenseLimit, card?.availableSpend); Navigation.goBack(); }; const submit = (values: FormOnyxValues) => { - const currentLimit = card?.nameValuePairs?.unapprovedExpenseLimit ?? 0; - const currentSpend = currentLimit - (card?.availableSpend ?? 0); const newLimit = Number(values[INPUT_IDS.LIMIT]) * 100; - const newAvailableSpend = newLimit - currentSpend; + const newAvailableSpend = getNewAvailableSpend(newLimit); if (newAvailableSpend <= 0) { setIsConfirmModalVisible(true); return; } - updateCardLimit(values[INPUT_IDS.LIMIT]); + updateCardLimit(newLimit); }; const validate = useCallback( @@ -126,7 +133,7 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { updateCardLimit(inputValues[INPUT_IDS.LIMIT])} + onConfirm={() => updateCardLimit(Number(inputValues[INPUT_IDS.LIMIT]) * 100)} onCancel={() => setIsConfirmModalVisible(false)} prompt={translate(getPromptTextKey, CurrencyUtils.convertToDisplayString(Number(inputValues[INPUT_IDS.LIMIT]) * 100, CONST.CURRENCY.USD))} confirmText={translate('workspace.expensifyCard.changeLimit')} diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx index 0a1abe4442e8..510a7a778100 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -1,5 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import ExpensifyCardImage from '@assets/images/expensify-card.svg'; @@ -14,6 +14,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CardUtils from '@libs/CardUtils'; @@ -44,7 +45,6 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); - // TODO: add an API call to load the card details data: https://github.com/Expensify/App/issues/47231 const card = cardsList?.[cardID]; const cardholder = personalDetails?.[card?.accountID ?? -1]; const isVirtual = !!card?.nameValuePairs?.isVirtual; @@ -53,10 +53,14 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(cardholder); const translationForLimitType = CardUtils.getTranslationKeyForLimitType(card?.nameValuePairs?.limitType); - useEffect(() => { + const fetchCardDetails = useCallback(() => { Card.openCardDetailsPage(Number(cardID)); }, [cardID]); + const {isOffline} = useNetwork({onReconnect: fetchCardDetails}); + + useEffect(() => fetchCardDetails(), [fetchCardDetails]); + const deactivateCard = () => { setIsDeactivateModalVisible(false); @@ -116,6 +120,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail title={formattedAvailableSpendAmount} interactive={false} titleStyle={styles.newKansasLarge} + containerStyle={isOffline ? styles.buttonOpacityDisabled : null} />