From fe0b583ca2e041e1d041e787b727c3debad65a5a Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 26 Aug 2024 08:48:34 +0200 Subject: [PATCH 1/5] Implements Card_Deactivate API call --- .../API/parameters/CardDeactivateParams.ts | 6 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/Card.ts | 42 +++++++++++++++++++ .../WorkspaceExpensifyCardDetailsPage.tsx | 9 ++-- 5 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 src/libs/API/parameters/CardDeactivateParams.ts diff --git a/src/libs/API/parameters/CardDeactivateParams.ts b/src/libs/API/parameters/CardDeactivateParams.ts new file mode 100644 index 000000000000..e329d02a0c9e --- /dev/null +++ b/src/libs/API/parameters/CardDeactivateParams.ts @@ -0,0 +1,6 @@ +type CardDeactivateParams = { + authToken: string; + cardID: number; +}; + +export default CardDeactivateParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index a72220c3d943..2028692866b1 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -278,4 +278,5 @@ export type {default as CreateExpensifyCardParams} from './CreateExpensifyCardPa export type {default as UpdateExpensifyCardTitleParams} from './UpdateExpensifyCardTitleParams'; export type {default as OpenCardDetailsPageParams} from './OpenCardDetailsPageParams'; export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams'; +export type {default as CardDeactivateParams} from './CardDeactivateParams'; export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index de63ed032afe..23ac0fddba39 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -34,6 +34,7 @@ const WRITE_COMMANDS = { UPDATE_EXPENSIFY_CARD_LIMIT: 'UpdateExpensifyCardLimit', UPDATE_EXPENSIFY_CARD_TITLE: 'UpdateExpensifyCardTitle', UPDATE_EXPENSIFY_CARD_LIMIT_TYPE: 'UpdateExpensifyCardLimitType', + CARD_DEACTIVATE: 'Card_Deactivate', CHRONOS_REMOVE_OOO_EVENT: 'Chronos_RemoveOOOEvent', MAKE_DEFAULT_PAYMENT_METHOD: 'MakeDefaultPaymentMethod', ADD_PAYMENT_CARD: 'AddPaymentCard', @@ -360,6 +361,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT]: Parameters.UpdateExpensifyCardLimitParams; [WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_TITLE]: Parameters.UpdateExpensifyCardTitleParams; [WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT_TYPE]: Parameters.UpdateExpensifyCardLimitTypeParams; + [WRITE_COMMANDS.CARD_DEACTIVATE]: Parameters.CardDeactivateParams; [WRITE_COMMANDS.MAKE_DEFAULT_PAYMENT_METHOD]: Parameters.MakeDefaultPaymentMethodParams; [WRITE_COMMANDS.ADD_PAYMENT_CARD]: Parameters.AddPaymentCardParams; [WRITE_COMMANDS.ADD_PAYMENT_CARD_GBP]: Parameters.AddPaymentCardParams; diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 747df5d3998e..bcac1e22432f 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, + CardDeactivateParams, OpenCardDetailsPageParams, ReportVirtualExpensifyCardFraudParams, RequestReplacementExpensifyCardParams, @@ -499,6 +500,46 @@ function updateExpensifyCardLimitType(workspaceAccountID: number, cardID: number API.write(WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT_TYPE, parameters, {optimisticData, successData, failureData}); } +function deactivateCard(workspaceAccountID: number, cardID: number, oldCardState?: ValueOf) { + const authToken = NetworkStore.getAuthToken(); + + if (!authToken) { + return; + } + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`, + value: { + [cardID]: { + state: CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`, + value: { + [cardID]: { + state: oldCardState, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + ]; + + const parameters: CardDeactivateParams = { + authToken, + cardID, + }; + + API.write(WRITE_COMMANDS.CARD_DEACTIVATE, parameters, {optimisticData, failureData}); +} + function startIssueNewCardFlow(policyID: string) { const parameters: StartIssueNewCardFlowParams = { policyID, @@ -635,5 +676,6 @@ export { openCardDetailsPage, toggleContinuousReconciliation, updateExpensifyCardLimitType, + deactivateCard, }; export type {ReplacementReason}; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx index 510a7a778100..0cc5a46f9fd8 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -23,6 +23,7 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; import * as Card from '@userActions/Card'; @@ -63,12 +64,14 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail const deactivateCard = () => { setIsDeactivateModalVisible(false); - - // TODO: add API call when it's supported https://github.com/Expensify/Expensify/issues/407841 - + Card.deactivateCard(workspaceAccountID, Number(cardID), card?.state); Navigation.goBack(); }; + if (card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED) { + return ; + } + return ( Date: Mon, 26 Aug 2024 09:35:12 +0200 Subject: [PATCH 2/5] Hide deactivated cards --- src/libs/CardUtils.ts | 7 ++++++- src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index e0041dde9934..1d4d8d104446 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -168,8 +168,12 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry bankAccount?.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && bankAccount?.accountData?.allowDebit); } +function filterDeactivatedCards(cardsList: OnyxEntry): Card[] { + return Object.values(cardsList ?? {}).filter((card) => card.state !== CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED); +} + function sortCardsByCardholderName(cardsList: OnyxEntry, personalDetails: OnyxEntry): Card[] { - return Object.values(cardsList ?? {}).sort((cardA: Card, cardB: Card) => { + return filterDeactivatedCards(cardsList).sort((cardA: Card, cardB: Card) => { const userA = personalDetails?.[cardA.accountID ?? '-1'] ?? {}; const userB = personalDetails?.[cardB.accountID ?? '-1'] ?? {}; @@ -195,4 +199,5 @@ export { getTranslationKeyForLimitType, getEligibleBankAccountsForCard, sortCardsByCardholderName, + filterDeactivatedCards, }; diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 2742033430ba..f949b88491ff 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -22,6 +22,7 @@ import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; @@ -75,6 +76,8 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? -1] ?? ({} as PersonalDetails); const policyOwnerDisplayName = ownerDetails.displayName ?? policy?.owner ?? ''; + const filteredCardList = useMemo(() => CardUtils.filterDeactivatedCards(cardsList), [cardsList]); + const confirmModalPrompt = useMemo(() => { const isApprover = Member.isApprover(policy, accountID); if (!isApprover) { @@ -259,7 +262,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM {translate('walletPage.assignedCards')} - {Object.values(cardsList ?? {}).map((card) => ( + {filteredCardList.map((card) => ( Date: Tue, 27 Aug 2024 11:54:33 +0200 Subject: [PATCH 3/5] Code improvements --- src/libs/CardUtils.ts | 7 +------ src/libs/actions/Card.ts | 9 ++++----- .../expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx | 7 +------ 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 1d4d8d104446..e0041dde9934 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -168,12 +168,8 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry bankAccount?.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && bankAccount?.accountData?.allowDebit); } -function filterDeactivatedCards(cardsList: OnyxEntry): Card[] { - return Object.values(cardsList ?? {}).filter((card) => card.state !== CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED); -} - function sortCardsByCardholderName(cardsList: OnyxEntry, personalDetails: OnyxEntry): Card[] { - return filterDeactivatedCards(cardsList).sort((cardA: Card, cardB: Card) => { + return Object.values(cardsList ?? {}).sort((cardA: Card, cardB: Card) => { const userA = personalDetails?.[cardA.accountID ?? '-1'] ?? {}; const userB = personalDetails?.[cardB.accountID ?? '-1'] ?? {}; @@ -199,5 +195,4 @@ export { getTranslationKeyForLimitType, getEligibleBankAccountsForCard, sortCardsByCardholderName, - filterDeactivatedCards, }; diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index bcac1e22432f..dd187c991b5f 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -19,6 +19,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import * as NetworkStore from '@libs/Network/NetworkStore'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Card} from '@src/types/onyx'; import type {CardLimitType, ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep} from '@src/types/onyx/Card'; import type {ConnectionName} from '@src/types/onyx/Policy'; @@ -500,7 +501,7 @@ function updateExpensifyCardLimitType(workspaceAccountID: number, cardID: number API.write(WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT_TYPE, parameters, {optimisticData, successData, failureData}); } -function deactivateCard(workspaceAccountID: number, cardID: number, oldCardState?: ValueOf) { +function deactivateCard(workspaceAccountID: number, cardID: number, card?: Card) { const authToken = NetworkStore.getAuthToken(); if (!authToken) { @@ -512,9 +513,7 @@ function deactivateCard(workspaceAccountID: number, cardID: number, oldCardState onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`, value: { - [cardID]: { - state: CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED, - }, + [cardID]: null, }, }, ]; @@ -525,7 +524,7 @@ function deactivateCard(workspaceAccountID: number, cardID: number, oldCardState key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`, value: { [cardID]: { - state: oldCardState, + ...card, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), }, }, diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx index 0cc5a46f9fd8..3dea375173f0 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -23,7 +23,6 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; -import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; import * as Card from '@userActions/Card'; @@ -64,14 +63,10 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail const deactivateCard = () => { setIsDeactivateModalVisible(false); - Card.deactivateCard(workspaceAccountID, Number(cardID), card?.state); + Card.deactivateCard(workspaceAccountID, Number(cardID), card); Navigation.goBack(); }; - if (card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED) { - return ; - } - return ( Date: Tue, 27 Aug 2024 14:54:57 +0200 Subject: [PATCH 4/5] Add offline modal --- .../WorkspaceExpensifyCardDetailsPage.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx index 3dea375173f0..34a591a83073 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -5,6 +5,7 @@ import {useOnyx} from 'react-native-onyx'; import ExpensifyCardImage from '@assets/images/expensify-card.svg'; import Badge from '@components/Badge'; import ConfirmModal from '@components/ConfirmModal'; +import DecisionModal from '@components/DecisionModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {FallbackAvatar} from '@components/Icon/Expensicons'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -17,6 +18,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -38,7 +40,9 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID); const [isDeactivateModalVisible, setIsDeactivateModalVisible] = useState(false); + const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); const theme = useTheme(); @@ -143,7 +147,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail iconFill={theme.icon} title={translate('workspace.expensifyCard.deactivate')} style={styles.mv1} - onPress={() => setIsDeactivateModalVisible(true)} + onPress={() => (isOffline ? setIsOfflineModalVisible(true) : setIsDeactivateModalVisible(true))} /> + setIsOfflineModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={isOfflineModalVisible} + onClose={() => setIsOfflineModalVisible(false)} + /> )} From 89b6e18ae4c249e1469fff025f0dc430de8d5ddc Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 28 Aug 2024 12:14:44 +0200 Subject: [PATCH 5/5] Minor improvement --- src/libs/actions/Card.ts | 3 ++- .../expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 5903739d86ff..d9229cf4bae2 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -489,8 +489,9 @@ function updateExpensifyCardLimitType(workspaceAccountID: number, cardID: number API.write(WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT_TYPE, parameters, {optimisticData, successData, failureData}); } -function deactivateCard(workspaceAccountID: number, cardID: number, card?: Card) { +function deactivateCard(workspaceAccountID: number, card?: Card) { const authToken = NetworkStore.getAuthToken(); + const cardID = card?.cardID ?? -1; if (!authToken) { return; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx index 34a591a83073..f3e1472820c8 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -67,7 +67,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail const deactivateCard = () => { setIsDeactivateModalVisible(false); - Card.deactivateCard(workspaceAccountID, Number(cardID), card); + Card.deactivateCard(workspaceAccountID, card); Navigation.goBack(); };