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

Company Cards - Bank UI #50716

Merged
merged 7 commits into from
Oct 21, 2024
Merged
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
263 changes: 263 additions & 0 deletions assets/images/companyCards/pending-bank.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2603,6 +2603,14 @@ const CONST = {
WELLS_FARGO: 'Wells Fargo',
OTHER: 'Other',
},
BANK_CONNECTIONS: {
WELLS_FARGO: 'wellsfargo',
CHASE: 'chase',
BREX: 'brex',
CAPITAL_ONE: 'capitalone',
CITI_BANK: 'citibank',
AMEX: 'americanexpressfdx',
},
AMEX_CUSTOM_FEED: {
CORPORATE: 'American Express Corporate Cards',
BUSINESS: 'American Express Business Cards',
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const PUBLIC_SCREENS_ROUTES = {
ROOT: '',
TRANSITION_BETWEEN_APPS: 'transition',
CONNECTION_COMPLETE: 'connection-complete',
BANK_CONNECTION_COMPLETE: 'bank-connection-complete',
VALIDATE_LOGIN: 'v/:accountID/:validateCode',
UNLINK_LOGIN: 'u/:accountID/:validateCode',
APPLE_SIGN_IN: 'sign-in-with-apple',
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Illustrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import WellsFargoCompanyCardDetail from '@assets/images/companyCards/card-wellsf
import OtherCompanyCardDetail from '@assets/images/companyCards/card=-generic.svg';
import CompanyCardsEmptyState from '@assets/images/companyCards/emptystate__card-pos.svg';
import MasterCardCompanyCards from '@assets/images/companyCards/mastercard.svg';
import PendingBank from '@assets/images/companyCards/pending-bank.svg';
import CompanyCardsPendingState from '@assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg';
import VisaCompanyCards from '@assets/images/companyCards/visa.svg';
import EmptyCardState from '@assets/images/emptystate__expensifycard.svg';
Expand Down Expand Up @@ -207,6 +208,7 @@ export {
Approval,
WalletAlt,
Workflows,
PendingBank,
ThreeLeggedLaptopWoman,
House,
Alert,
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
ChangeTypeParams,
CharacterLengthLimitParams,
CharacterLimitParams,
CompanyCardBankName,
CompanyCardFeedNameParams,
ConfirmThatParams,
ConnectionNameParams,
Expand Down Expand Up @@ -3313,6 +3314,9 @@ const translations = {
emptyAddedFeedDescription: 'Get started by assigning your first card to a member.',
pendingFeedTitle: `We're reviewing your request...`,
pendingFeedDescription: `We're currently reviewing your feed details. Once that's done we'll reach out to you via`,
pendingBankTitle: 'Check your browser window',
pendingBankDescription: ({bankName}: CompanyCardBankName) => `Please connect to ${bankName} via your browser window that just opened. If one didn’t open, `,
pendingBankLink: 'please click here.',
giveItNameInstruction: 'Give the card a name that sets it apart from the others.',
updating: 'Updating...',
noAccountsFound: 'No accounts found',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
ChangeTypeParams,
CharacterLengthLimitParams,
CharacterLimitParams,
CompanyCardBankName,
CompanyCardFeedNameParams,
ConfirmThatParams,
ConnectionNameParams,
Expand Down Expand Up @@ -3355,6 +3356,9 @@ const translations = {
emptyAddedFeedDescription: 'Comienza asignando tu primera tarjeta a un miembro.',
pendingFeedTitle: `Estamos revisando tu solicitud...`,
pendingFeedDescription: `Actualmente estamos revisando los detalles de tu feed. Una vez hecho esto, nos pondremos en contacto contigo a través de`,
pendingBankTitle: 'Comprueba la ventana de tu navegador',
pendingBankDescription: ({bankName}: CompanyCardBankName) => `Conéctese a ${bankName} a través de la ventana del navegador que acaba de abrir. Si no se abrió, `,
pendingBankLink: 'por favor haga clic aquí.',
giveItNameInstruction: 'Nombra la tarjeta para distingirla de las demás.',
updating: 'Actualizando...',
noAccountsFound: 'No se han encontrado cuentas',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,10 @@ type ImportedTypesParams = {
importedTypes: string[];
};

type CompanyCardBankName = {
bankName: string;
};

export type {
AuthenticationErrorParams,
ImportMembersSuccessfullDescriptionParams,
Expand Down Expand Up @@ -729,6 +733,7 @@ export type {
DateParams,
FiltersAmountBetweenParams,
StatementPageTitleParams,
CompanyCardBankName,
DisconnectPromptParams,
DisconnectTitleParams,
CharacterLengthLimitParams,
Expand Down
34 changes: 34 additions & 0 deletions src/libs/actions/getCompanyCardBankConnection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {getApiRoot} from '@libs/ApiUtils';
import * as NetworkStore from '@libs/Network/NetworkStore';
import CONST from '@src/CONST';

type CompanyCardBankConnection = {
authToken: string;
domainName: string;
scrapeMinDate: string;
isCorporate: string;
};

// TODO remove this when BE will support bank UI callbacks
const bankUrl = 'https://secure.chase.com/web/auth/#/logon/logon/chaseOnline?redirect_url=';
narefyev91 marked this conversation as resolved.
Show resolved Hide resolved

export default function getCompanyCardBankConnection(bankName?: string, domainName?: string, scrapeMinDate?: string) {
const bankConnection = Object.keys(CONST.COMPANY_CARDS.BANKS).find((key) => CONST.COMPANY_CARDS.BANKS[key as keyof typeof CONST.COMPANY_CARDS.BANKS] === bankName);

// TODO remove this when BE will support bank UI callbacks
if (!domainName) {
return bankUrl;
}

if (!bankName || !bankConnection) {
return null;
}
const authToken = NetworkStore.getAuthToken();
const params: CompanyCardBankConnection = {authToken: authToken ?? '', domainName: domainName ?? '', isCorporate: 'true', scrapeMinDate: scrapeMinDate ?? ''};
const commandURL = getApiRoot({
shouldSkipWebProxy: true,
command: '',
});
const bank = CONST.COMPANY_CARDS.BANK_CONNECTIONS[bankConnection as keyof typeof CONST.COMPANY_CARDS.BANK_CONNECTIONS];
return `${commandURL}partners/banks/${bank}/oauth_callback.php?${new URLSearchParams(params).toString()}`;
}
3 changes: 3 additions & 0 deletions src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPol
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import AmexCustomFeed from './AmexCustomFeed';
import BankConnection from './BankConnection';
import CardInstructionsStep from './CardInstructionsStep';
import CardNameStep from './CardNameStep';
import CardTypeStep from './CardTypeStep';
Expand All @@ -28,6 +29,8 @@ function AddNewCardPage({policy}: WithPolicyAndFullscreenLoadingProps) {
return <SelectFeedType />;
case CONST.COMPANY_CARDS.STEP.CARD_TYPE:
return <CardTypeStep />;
case CONST.COMPANY_CARDS.STEP.BANK_CONNECTION:
return <BankConnection />;
case CONST.COMPANY_CARDS.STEP.CARD_INSTRUCTIONS:
return <CardInstructionsStep policyID={policyID} />;
case CONST.COMPANY_CARDS.STEP.CARD_NAME:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, {useEffect, useRef, useState} from 'react';
import {useOnyx} from 'react-native-onyx';
import {WebView} from 'react-native-webview';
import type {ValueOf} from 'type-fest';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import useLocalize from '@hooks/useLocalize';
import getUAForWebView from '@libs/getUAForWebView';
import * as CompanyCards from '@userActions/CompanyCards';
import getCompanyCardBankConnection from '@userActions/getCompanyCardBankConnection';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

function BankConnection() {
const {translate} = useLocalize();
const webViewRef = useRef<WebView>(null);
const [isWebViewOpen, setWebViewOpen] = useState(false);
const [session] = useOnyx(ONYXKEYS.SESSION);
const authToken = session?.authToken ?? null;
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
const bankName: ValueOf<typeof CONST.COMPANY_CARDS.BANKS> | undefined = addNewCard?.data?.selectedBank;
const url = getCompanyCardBankConnection(bankName);

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

const handleBackButtonPress = () => {
setWebViewOpen(false);
if (bankName === CONST.COMPANY_CARDS.BANKS.BREX) {
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK});
return;
}
if (bankName === CONST.COMPANY_CARDS.BANKS.AMEX) {
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.AMEX_CUSTOM_FEED});
return;
}
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_FEED_TYPE});
};

useEffect(() => {
setWebViewOpen(true);
}, []);

return (
<Modal
onClose={handleBackButtonPress}
fullscreen
isVisible={isWebViewOpen}
type={CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE}
>
<HeaderWithBackButton
title={translate('workspace.companyCards.addCardFeed')}
onBackButtonPress={handleBackButtonPress}
/>
<FullPageOfflineBlockingView>
{url && (
<WebView
ref={webViewRef}
source={{
uri: url,
headers: {
Cookie: `authToken=${authToken}`,
},
}}
userAgent={getUAForWebView()}
incognito
startInLoadingState
renderLoading={renderLoading}
/>
)}
</FullPageOfflineBlockingView>
</Modal>
);
}

BankConnection.displayName = 'BankConnection';

export default BankConnection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, {useCallback, useEffect} from 'react';
import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
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 getCurrentUrl from '@navigation/currentUrl';
import * as CompanyCards from '@userActions/CompanyCards';
import getCompanyCardBankConnection from '@userActions/getCompanyCardBankConnection';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import openBankConnection from './openBankConnection';

let customWindow: Window | null = null;

function BankConnection() {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
const bankName: ValueOf<typeof CONST.COMPANY_CARDS.BANKS> | undefined = addNewCard?.data?.selectedBank;
const currentUrl = getCurrentUrl();
const isBankConnectionCompleteRoute = currentUrl.includes(ROUTES.BANK_CONNECTION_COMPLETE);
const url = getCompanyCardBankConnection(bankName);

const onOpenBankConnectionFlow = useCallback(() => {
if (!url) {
return;
}
customWindow = openBankConnection(url);
}, [url]);

const handleBackButtonPress = () => {
customWindow?.close();
if (bankName === CONST.COMPANY_CARDS.BANKS.BREX) {
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK});
return;
}
if (bankName === CONST.COMPANY_CARDS.BANKS.AMEX) {
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.AMEX_CUSTOM_FEED});
return;
}
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_FEED_TYPE});
};

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();
return;
}
customWindow = openBankConnection(url);
}, [isBankConnectionCompleteRoute, url]);

return (
<ScreenWrapper testID={BankConnection.displayName}>
<HeaderWithBackButton
title={translate('workspace.companyCards.addCardFeed')}
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 @@ -28,7 +28,7 @@ function SelectFeedType() {
return;
}
CompanyCards.setAddNewCompanyCardStepAndData({
step: typeSelected === CONST.COMPANY_CARDS.FEED_TYPE.DIRECT ? CONST.COMPANY_CARDS.STEP.SELECT_BANK : CONST.COMPANY_CARDS.STEP.CARD_TYPE,
step: typeSelected === CONST.COMPANY_CARDS.FEED_TYPE.DIRECT ? CONST.COMPANY_CARDS.STEP.BANK_CONNECTION : CONST.COMPANY_CARDS.STEP.CARD_TYPE,
data: {selectedFeedType: typeSelected},
});
};
Expand Down
5 changes: 5 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5157,6 +5157,11 @@ const styles = (theme: ThemeColors) =>
height: 188,
},

pendingBankCardIllustration: {
width: 217,
height: 150,
},

cardIcon: {
overflow: 'hidden',
borderRadius: variables.cardBorderRadius,
Expand Down
Loading