Skip to content

Commit

Permalink
Merge pull request #54444 from callstack-internal/feat/54298
Browse files Browse the repository at this point in the history
Feat: Handle the direct feed credentials expiration
  • Loading branch information
mountiny authored Jan 7, 2025
2 parents 89a18c6 + 8d82f12 commit 5c22754
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 4 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2754,6 +2754,7 @@ const CONST = {
},
STEP_NAMES: ['1', '2', '3', '4'],
STEP: {
BANK_CONNECTION: 'BankConnection',
ASSIGNEE: 'Assignee',
CARD: 'Card',
CARD_NAME: 'CardName',
Expand Down
10 changes: 10 additions & 0 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 @@ -350,6 +351,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 @@ -401,6 +410,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 = !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,92 @@
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 = {
/** ID of the policy */
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}`);
// 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 = () => <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,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 = (
<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

0 comments on commit 5c22754

Please sign in to comment.