Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Handle the direct feed credentials expiration #54444

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2768,6 +2768,7 @@ const CONST = {
},
STEP_NAMES: ['1', '2', '3', '4'],
STEP: {
BANK_CONNECTION: 'BankConnection',
ASSIGNEE: 'Assignee',
CARD: 'Card',
CARD_NAME: 'CardName',
Expand Down
14 changes: 12 additions & 2 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -203,8 +204,8 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry<BankAccountL
function sortCardsByCardholderName(cardsList: OnyxEntry<WorkspaceCardsList>, personalDetails: OnyxEntry<PersonalDetailsList>): 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);
Expand Down Expand Up @@ -352,6 +353,14 @@ function getSelectedFeed(lastSelectedFeed: OnyxEntry<CompanyCardFeed>, 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 ?? {};
Expand Down Expand Up @@ -403,6 +412,7 @@ export {
getEligibleBankAccountsForCard,
sortCardsByCardholderName,
getCardFeedIcon,
isSelectedFeedExpired,
getCardFeedName,
getCompanyFeeds,
isCustomFeed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(() => {
Expand All @@ -46,6 +47,13 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) {
}

switch (currentStep) {
case CONST.COMPANY_CARD.STEP.BANK_CONNECTION:
return (
<BankConnection
policyID={policyID}
feed={feed}
/>
);
case CONST.COMPANY_CARD.STEP.ASSIGNEE:
return (
<AssigneeStep
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WebView>(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}`);
const isFeedExpired = CardUtils.isSelectedFeedExpired(cardFeeds?.settings?.oAuthAccountDetails?.[feed]);

const renderLoading = () => <FullScreenLoadingIndicator />;

const handleBackButtonPress = () => {
Navigation.goBack();
};

useEffect(() => {
if (!url) {
return;
}
if (!isFeedExpired) {
CompanyCards.setAssignCardStepAndData({
currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE,
isEditing: false,
});
}
}, [isFeedExpired, url]);

return (
<ScreenWrapper
testID={BankConnection.displayName}
shouldShowOfflineIndicator={false}
includeSafeAreaPaddingBottom={false}
shouldEnablePickerAvoiding={false}
shouldEnableMaxHeight
>
<HeaderWithBackButton
title={translate('workspace.companyCards.assignCard')}
onBackButtonPress={handleBackButtonPress}
/>
<FullPageOfflineBlockingView>
{!!url && (
<WebView
ref={webViewRef}
source={{
uri: url,
headers: {
Cookie: `authToken=${authToken}`,
},
}}
userAgent={getUAForWebView()}
incognito
startInLoadingState
renderLoading={renderLoading}
/>
)}
</FullPageOfflineBlockingView>
</ScreenWrapper>
);
}

BankConnection.displayName = 'BankConnection';

export default BankConnection;
Original file line number Diff line number Diff line change
@@ -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 = (
<Text style={[styles.textAlignCenter, styles.textSupporting]}>
{bankName && translate(`workspace.moreFeatures.companyCards.pendingBankDescription`, {bankName})}
<TextLink onPress={onOpenBankConnectionFlow}>{translate('workspace.moreFeatures.companyCards.pendingBankLink')}</TextLink>
</Text>
);

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 (
<ScreenWrapper testID={BankConnection.displayName}>
<HeaderWithBackButton
title={translate('workspace.companyCards.assignCard')}
onBackButtonPress={handleBackButtonPress}
/>
<BlockingView
icon={Illustrations.PendingBank}
iconWidth={styles.pendingBankCardIllustration.width}
iconHeight={styles.pendingBankCardIllustration.height}
title={translate('workspace.moreFeatures.companyCards.pendingBankTitle')}
linkKey="workspace.moreFeatures.companyCards.pendingBankLink"
CustomSubtitle={CustomSubtitle}
shouldShowLink
onLinkPress={onOpenBankConnectionFlow}
/>
</ScreenWrapper>
);
}

BankConnection.displayName = 'BankConnection';

export default BankConnection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const handleOpenBankConnectionFlow = (url: string) => {
return window.open(url, '_blank');
};

export default handleOpenBankConnectionFlow;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type CardSelectionStepProps = {
feed: CompanyCardFeed;

/** Current policy id */
policyID: string;
policyID: string | undefined;
};

function CardSelectionStep({feed, policyID}: CardSelectionStepProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
Loading