From c50a2ecdf70541f60daf8a3634e725c3c7daf92c Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Sat, 21 Dec 2024 03:16:38 +0100 Subject: [PATCH 1/8] add new step --- src/CONST.ts | 1 + .../companyCards/assignCard/AssignCardFeedPage.tsx | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index e317c19d96d2..b9df636e5437 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2764,6 +2764,7 @@ const CONST = { }, STEP_NAMES: ['1', '2', '3', '4'], STEP: { + BANK_CONNECTION: 'BankConnection', ASSIGNEE: 'Assignee', CARD: 'Card', CARD_NAME: 'CardName', diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index 1aaace9e37fa..21340bb4b63c 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -11,6 +11,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import AssigneeStep from './AssigneeStep'; +import BankConnection from './BankConnection'; import CardNameStep from './CardNameStep'; import CardSelectionStep from './CardSelectionStep'; import ConfirmationStep from './ConfirmationStep'; @@ -46,6 +47,13 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { } switch (currentStep) { + case CONST.COMPANY_CARD.STEP.BANK_CONNECTION: + return ( + + ); case CONST.COMPANY_CARD.STEP.ASSIGNEE: return ( Date: Sat, 21 Dec 2024 03:16:55 +0100 Subject: [PATCH 2/8] add new function isSelectedFeedExpired --- src/libs/CardUtils.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 9a71480019a6..8c5cd2e5c6c0 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1,3 +1,4 @@ +import {fromUnixTime, isBefore} from 'date-fns'; import groupBy from 'lodash/groupBy'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -352,6 +353,14 @@ function getSelectedFeed(lastSelectedFeed: OnyxEntry, cardFeeds return lastSelectedFeed ?? defaultFeed; } +function isSelectedFeedExpired(directFeed: DirectCardFeedData | undefined): boolean { + if (!directFeed) { + return false; + } + + return isBefore(fromUnixTime(directFeed.expiration), new Date()); +} + /** Returns list of cards which can be assigned */ function getFilteredCardList(list: WorkspaceCardsList | undefined, directFeed: DirectCardFeedData | undefined) { const {cardList: customFeedCardsToAssign, ...cards} = list ?? {}; @@ -403,6 +412,7 @@ export { getEligibleBankAccountsForCard, sortCardsByCardholderName, getCardFeedIcon, + isSelectedFeedExpired, getCardFeedName, getCompanyFeeds, isCustomFeed, From 3819d84a916a71a51482ce861ca0ad0f663e3325 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Sat, 21 Dec 2024 03:17:12 +0100 Subject: [PATCH 3/8] udpate navigation --- .../workspace/companyCards/WorkspaceCompanyCardsPage.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 392138a2d8d1..31d324e62ca4 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -54,6 +54,7 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) { const isNoFeed = isEmptyObject(companyCards) && !selectedFeedData; const isPending = !!selectedFeedData?.pending; const isFeedAdded = !isPending && !isNoFeed; + const isFeedExpired = CardUtils.isSelectedFeedExpired(selectedFeed ? cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed] : undefined); const fetchCompanyCards = useCallback(() => { CompanyCards.openPolicyCompanyCardsPage(policyID, workspaceAccountID); @@ -102,6 +103,10 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) { } } + if (isFeedExpired) { + currentStep = CONST.COMPANY_CARD.STEP.BANK_CONNECTION; + } + CompanyCards.setAssignCardStepAndData({data, currentStep}); Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed))); }; From 78548a85bb008d68f594127ac6736239252e47de Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Sat, 21 Dec 2024 03:18:40 +0100 Subject: [PATCH 4/8] add new step --- .../BankConnection/index.native.tsx | 90 ++++++++++++++++++ .../assignCard/BankConnection/index.tsx | 92 +++++++++++++++++++ .../openBankConnection/index.tsx | 5 + .../openBankConnection/index.website.tsx | 14 +++ 4 files changed, 201 insertions(+) create mode 100644 src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx create mode 100644 src/pages/workspace/companyCards/assignCard/BankConnection/index.tsx create mode 100644 src/pages/workspace/companyCards/assignCard/BankConnection/openBankConnection/index.tsx create mode 100644 src/pages/workspace/companyCards/assignCard/BankConnection/openBankConnection/index.website.tsx diff --git a/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx new file mode 100644 index 000000000000..d9e622ebc08d --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx @@ -0,0 +1,90 @@ +import React, {useEffect, useRef} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import {WebView} from 'react-native-webview'; +import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import * as CardUtils from '@libs/CardUtils'; +import getUAForWebView from '@libs/getUAForWebView'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as CompanyCards from '@userActions/CompanyCards'; +import getCompanyCardBankConnection from '@userActions/getCompanyCardBankConnection'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {CompanyCardFeed} from '@src/types/onyx'; + +type BankConnectionStepProps = { + policyID?: string; + + /** Selected feed */ + feed: CompanyCardFeed; +}; + +function BankConnection({policyID, feed}: BankConnectionStepProps) { + const {translate} = useLocalize(); + const webViewRef = useRef(null); + const [session] = useOnyx(ONYXKEYS.SESSION); + const authToken = session?.authToken ?? null; + const bankName = CardUtils.getCardFeedName(feed); + const url = getCompanyCardBankConnection(policyID, bankName); + const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID ?? '-1'); + const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); + const isFeedExpired = CardUtils.isSelectedFeedExpired(cardFeeds?.settings?.oAuthAccountDetails?.[feed]); + + const renderLoading = () => ; + + const handleBackButtonPress = () => { + Navigation.goBack(); + }; + + useEffect(() => { + if (!url) { + return; + } + if (!isFeedExpired) { + CompanyCards.setAssignCardStepAndData({ + currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE, + isEditing: false, + }); + } + }, [isFeedExpired, url]); + + return ( + + + + {!!url && ( + + )} + + + ); +} + +BankConnection.displayName = 'BankConnection'; + +export default BankConnection; diff --git a/src/pages/workspace/companyCards/assignCard/BankConnection/index.tsx b/src/pages/workspace/companyCards/assignCard/BankConnection/index.tsx new file mode 100644 index 000000000000..c29f24437791 --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/BankConnection/index.tsx @@ -0,0 +1,92 @@ +import React, {useCallback, useEffect} from 'react'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CardUtils from '@libs/CardUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import getCurrentUrl from '@navigation/currentUrl'; +import * as CompanyCards from '@userActions/CompanyCards'; +import getCompanyCardBankConnection from '@userActions/getCompanyCardBankConnection'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {CompanyCardFeed} from '@src/types/onyx'; +import openBankConnection from './openBankConnection'; + +let customWindow: Window | null = null; + +type BankConnectionStepProps = { + policyID?: string; + + /** Selected feed */ + feed: CompanyCardFeed; +}; + +function BankConnection({policyID, feed}: BankConnectionStepProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const bankName = CardUtils.getCardFeedName(feed); + const currentUrl = getCurrentUrl(); + const isBankConnectionCompleteRoute = currentUrl.includes(ROUTES.BANK_CONNECTION_COMPLETE); + const url = getCompanyCardBankConnection(policyID, bankName); + + const onOpenBankConnectionFlow = useCallback(() => { + if (!url) { + return; + } + customWindow = openBankConnection(url); + }, [url]); + + const handleBackButtonPress = () => { + Navigation.goBack(); + }; + + const CustomSubtitle = ( + + {bankName && translate(`workspace.moreFeatures.companyCards.pendingBankDescription`, {bankName})} + {translate('workspace.moreFeatures.companyCards.pendingBankLink')} + + ); + + useEffect(() => { + if (!url) { + return; + } + if (isBankConnectionCompleteRoute) { + customWindow?.close(); + CompanyCards.setAssignCardStepAndData({ + currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE, + isEditing: false, + }); + return; + } + customWindow = openBankConnection(url); + }, [isBankConnectionCompleteRoute, policyID, url]); + + return ( + + + + + ); +} + +BankConnection.displayName = 'BankConnection'; + +export default BankConnection; diff --git a/src/pages/workspace/companyCards/assignCard/BankConnection/openBankConnection/index.tsx b/src/pages/workspace/companyCards/assignCard/BankConnection/openBankConnection/index.tsx new file mode 100644 index 000000000000..91a81bdbd6c6 --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/BankConnection/openBankConnection/index.tsx @@ -0,0 +1,5 @@ +const handleOpenBankConnectionFlow = (url: string) => { + return window.open(url, '_blank'); +}; + +export default handleOpenBankConnectionFlow; diff --git a/src/pages/workspace/companyCards/assignCard/BankConnection/openBankConnection/index.website.tsx b/src/pages/workspace/companyCards/assignCard/BankConnection/openBankConnection/index.website.tsx new file mode 100644 index 000000000000..220404cee0e7 --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/BankConnection/openBankConnection/index.website.tsx @@ -0,0 +1,14 @@ +const WINDOW_WIDTH = 700; +const WINDOW_HEIGHT = 600; + +const handleOpenBankConnectionFlow = (url: string) => { + const screenWidth = window.screen.width; + const screenHeight = window.screen.height; + const left = (screenWidth - WINDOW_WIDTH) / 2; + const top = (screenHeight - WINDOW_HEIGHT) / 2; + const popupFeatures = `width=${WINDOW_WIDTH},height=${WINDOW_HEIGHT},left=${left},top=${top},scrollbars=yes,resizable=yes`; + + return window.open(url, 'popupWindow', popupFeatures); +}; + +export default handleOpenBankConnectionFlow; From 5d6f921a1f5a0f2d39bcfc90a3aa57e34333b2e2 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 24 Dec 2024 01:18:40 +0100 Subject: [PATCH 5/8] fix new linter --- src/libs/CardUtils.ts | 4 ++-- .../workspace/companyCards/assignCard/AssignCardFeedPage.tsx | 2 +- .../companyCards/assignCard/BankConnection/index.native.tsx | 2 +- src/pages/workspace/companyCards/assignCard/CardNameStep.tsx | 2 +- .../workspace/companyCards/assignCard/CardSelectionStep.tsx | 2 +- .../workspace/companyCards/assignCard/ConfirmationStep.tsx | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 8c5cd2e5c6c0..5d17576c1979 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -204,8 +204,8 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry, personalDetails: OnyxEntry): Card[] { const {cardList, ...cards} = cardsList ?? {}; return Object.values(cards).sort((cardA: Card, cardB: Card) => { - const userA = personalDetails?.[cardA.accountID ?? '-1'] ?? {}; - const userB = personalDetails?.[cardB.accountID ?? '-1'] ?? {}; + const userA = cardA.accountID ? personalDetails?.[cardA.accountID] : {}; + const userB = cardB.accountID ? personalDetails?.[cardB.accountID] : {}; const aName = PersonalDetailsUtils.getDisplayNameOrDefault(userA); const bName = PersonalDetailsUtils.getDisplayNameOrDefault(userB); diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index 21340bb4b63c..7d6d41506524 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -25,7 +25,7 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { const feed = route.params?.feed; const backTo = route.params?.backTo; - const policyID = policy?.id ?? '-1'; + const policyID = policy?.id; const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => !!account?.delegatedAccess?.delegate}); useEffect(() => { diff --git a/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx index d9e622ebc08d..f7c8555f57dd 100644 --- a/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx @@ -30,7 +30,7 @@ function BankConnection({policyID, feed}: BankConnectionStepProps) { const authToken = session?.authToken ?? null; const bankName = CardUtils.getCardFeedName(feed); const url = getCompanyCardBankConnection(policyID, bankName); - const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID ?? '-1'); + const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); const isFeedExpired = CardUtils.isSelectedFeedExpired(cardFeeds?.settings?.oAuthAccountDetails?.[feed]); diff --git a/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx b/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx index d96d15761ed6..8bc88744e5b8 100644 --- a/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx @@ -20,7 +20,7 @@ import INPUT_IDS from '@src/types/form/EditExpensifyCardNameForm'; type CardNameStepProps = { /** Current policy id */ - policyID: string; + policyID: string | undefined; }; function CardNameStep({policyID}: CardNameStepProps) { diff --git a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx index e537bbc3a625..827fd4f7bdb7 100644 --- a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx @@ -28,7 +28,7 @@ type CardSelectionStepProps = { feed: CompanyCardFeed; /** Current policy id */ - policyID: string; + policyID: string | undefined; }; function CardSelectionStep({feed, policyID}: CardSelectionStepProps) { diff --git a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx index c583fa8e2d70..0366b4b52f7f 100644 --- a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx @@ -21,7 +21,7 @@ import type {AssignCardStep} from '@src/types/onyx/AssignCard'; type ConfirmationStepProps = { /** Current policy id */ - policyID: string; + policyID: string | undefined; /** Route to go back to */ backTo?: Route; From df5ccf16a0b864ccf4aa87311307c53030576879 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 24 Dec 2024 01:22:50 +0100 Subject: [PATCH 6/8] fix ts --- .../workspace/companyCards/assignCard/ConfirmationStep.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx index 0366b4b52f7f..a5fdbb6d1433 100644 --- a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx @@ -38,6 +38,9 @@ function ConfirmationStep({policyID, backTo}: ConfirmationStepProps) { const cardholderName = PersonalDetailsUtils.getPersonalDetailByEmail(data?.email ?? '')?.displayName ?? ''; const submit = () => { + if (!policyID) { + return; + } CompanyCards.assignWorkspaceCompanyCard(policyID, data); Navigation.navigate(backTo ?? ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); CompanyCards.clearAssignCardStepAndData(); From e289024b7a5eb039cd25ddce617ddfa2d352b234 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 7 Jan 2025 14:32:42 +0100 Subject: [PATCH 7/8] add doc --- .../companyCards/assignCard/BankConnection/index.native.tsx | 1 + .../workspace/companyCards/assignCard/BankConnection/index.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx index f7c8555f57dd..ade3ab5552cb 100644 --- a/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx @@ -17,6 +17,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {CompanyCardFeed} from '@src/types/onyx'; type BankConnectionStepProps = { + /** ID of the policy */ policyID?: string; /** Selected feed */ diff --git a/src/pages/workspace/companyCards/assignCard/BankConnection/index.tsx b/src/pages/workspace/companyCards/assignCard/BankConnection/index.tsx index c29f24437791..b714229752cf 100644 --- a/src/pages/workspace/companyCards/assignCard/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/assignCard/BankConnection/index.tsx @@ -20,6 +20,7 @@ import openBankConnection from './openBankConnection'; let customWindow: Window | null = null; type BankConnectionStepProps = { + /** ID of the policy */ policyID?: string; /** Selected feed */ From 8d82f12301fbe89443e8dd3877a72e9585dd51df Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 7 Jan 2025 17:38:41 +0100 Subject: [PATCH 8/8] add comment --- .../companyCards/assignCard/BankConnection/index.native.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx index ade3ab5552cb..32e9c03ea8d0 100644 --- a/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/assignCard/BankConnection/index.native.tsx @@ -33,6 +33,7 @@ function BankConnection({policyID, feed}: BankConnectionStepProps) { const url = getCompanyCardBankConnection(policyID, bankName); const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); + // This does not apply for custom feeds, this is used to check if the feed is expired to push user to reauthenticate const isFeedExpired = CardUtils.isSelectedFeedExpired(cardFeeds?.settings?.oAuthAccountDetails?.[feed]); const renderLoading = () => ;