diff --git a/src/CONST.ts b/src/CONST.ts index af1544e877cc..df691b8c4504 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2754,6 +2754,7 @@ const CONST = { }, STEP_NAMES: ['1', '2', '3', '4'], STEP: { + BANK_CONNECTION: 'BankConnection', ASSIGNEE: 'Assignee', CARD: 'Card', CARD_NAME: 'CardName', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index e26618fd3a6d..7bf8c84771d1 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'; @@ -350,6 +351,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 ?? {}; @@ -401,6 +410,7 @@ export { getEligibleBankAccountsForCard, sortCardsByCardholderName, getCardFeedIcon, + isSelectedFeedExpired, getCardFeedName, getCompanyFeeds, isCustomFeed, diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index ea3a0e0f7071..635ff33dbda6 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 = !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))); }; diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index 1aaace9e37fa..7d6d41506524 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'; @@ -24,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(() => { @@ -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 ( (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); + 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 = () => ; + + 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..b714229752cf --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/BankConnection/index.tsx @@ -0,0 +1,93 @@ +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 = { + /** ID of the policy */ + 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; 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..a5fdbb6d1433 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; @@ -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();