Skip to content

Commit b2fc5c9

Browse files
authored
Merge pull request Expensify#50716 from narefyev91/company-cards-bank-ui
Company Cards - Bank UI
2 parents 4575341 + be97e7d commit b2fc5c9

File tree

15 files changed

+519
-1
lines changed

15 files changed

+519
-1
lines changed

assets/images/companyCards/pending-bank.svg

+263
Loading

src/CONST.ts

+8
Original file line numberDiff line numberDiff line change
@@ -2623,6 +2623,14 @@ const CONST = {
26232623
WELLS_FARGO: 'Wells Fargo',
26242624
OTHER: 'Other',
26252625
},
2626+
BANK_CONNECTIONS: {
2627+
WELLS_FARGO: 'wellsfargo',
2628+
CHASE: 'chase',
2629+
BREX: 'brex',
2630+
CAPITAL_ONE: 'capitalone',
2631+
CITI_BANK: 'citibank',
2632+
AMEX: 'americanexpressfdx',
2633+
},
26262634
AMEX_CUSTOM_FEED: {
26272635
CORPORATE: 'American Express Corporate Cards',
26282636
BUSINESS: 'American Express Business Cards',

src/ROUTES.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const PUBLIC_SCREENS_ROUTES = {
2121
ROOT: '',
2222
TRANSITION_BETWEEN_APPS: 'transition',
2323
CONNECTION_COMPLETE: 'connection-complete',
24+
BANK_CONNECTION_COMPLETE: 'bank-connection-complete',
2425
VALIDATE_LOGIN: 'v/:accountID/:validateCode',
2526
UNLINK_LOGIN: 'u/:accountID/:validateCode',
2627
APPLE_SIGN_IN: 'sign-in-with-apple',

src/components/Icon/Illustrations.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import WellsFargoCompanyCardDetail from '@assets/images/companyCards/card-wellsf
1212
import OtherCompanyCardDetail from '@assets/images/companyCards/card=-generic.svg';
1313
import CompanyCardsEmptyState from '@assets/images/companyCards/emptystate__card-pos.svg';
1414
import MasterCardCompanyCards from '@assets/images/companyCards/mastercard.svg';
15+
import PendingBank from '@assets/images/companyCards/pending-bank.svg';
1516
import CompanyCardsPendingState from '@assets/images/companyCards/pendingstate_laptop-with-hourglass-and-cards.svg';
1617
import VisaCompanyCards from '@assets/images/companyCards/visa.svg';
1718
import EmptyCardState from '@assets/images/emptystate__expensifycard.svg';
@@ -207,6 +208,7 @@ export {
207208
Approval,
208209
WalletAlt,
209210
Workflows,
211+
PendingBank,
210212
ThreeLeggedLaptopWoman,
211213
House,
212214
Alert,

src/languages/en.ts

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import type {
3939
ChangeTypeParams,
4040
CharacterLengthLimitParams,
4141
CharacterLimitParams,
42+
CompanyCardBankName,
4243
CompanyCardFeedNameParams,
4344
ConfirmThatParams,
4445
ConnectionNameParams,
@@ -3314,6 +3315,9 @@ const translations = {
33143315
emptyAddedFeedDescription: 'Get started by assigning your first card to a member.',
33153316
pendingFeedTitle: `We're reviewing your request...`,
33163317
pendingFeedDescription: `We're currently reviewing your feed details. Once that's done we'll reach out to you via`,
3318+
pendingBankTitle: 'Check your browser window',
3319+
pendingBankDescription: ({bankName}: CompanyCardBankName) => `Please connect to ${bankName} via your browser window that just opened. If one didn’t open, `,
3320+
pendingBankLink: 'please click here.',
33173321
giveItNameInstruction: 'Give the card a name that sets it apart from the others.',
33183322
updating: 'Updating...',
33193323
noAccountsFound: 'No accounts found',

src/languages/es.ts

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import type {
3737
ChangeTypeParams,
3838
CharacterLengthLimitParams,
3939
CharacterLimitParams,
40+
CompanyCardBankName,
4041
CompanyCardFeedNameParams,
4142
ConfirmThatParams,
4243
ConnectionNameParams,
@@ -3359,6 +3360,9 @@ const translations = {
33593360
emptyAddedFeedDescription: 'Comienza asignando tu primera tarjeta a un miembro.',
33603361
pendingFeedTitle: `Estamos revisando tu solicitud...`,
33613362
pendingFeedDescription: `Actualmente estamos revisando los detalles de tu feed. Una vez hecho esto, nos pondremos en contacto contigo a través de`,
3363+
pendingBankTitle: 'Comprueba la ventana de tu navegador',
3364+
pendingBankDescription: ({bankName}: CompanyCardBankName) => `Conéctese a ${bankName} a través de la ventana del navegador que acaba de abrir. Si no se abrió, `,
3365+
pendingBankLink: 'por favor haga clic aquí.',
33623366
giveItNameInstruction: 'Nombra la tarjeta para distingirla de las demás.',
33633367
updating: 'Actualizando...',
33643368
noAccountsFound: 'No se han encontrado cuentas',

src/languages/params.ts

+5
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,10 @@ type ImportedTypesParams = {
538538
importedTypes: string[];
539539
};
540540

541+
type CompanyCardBankName = {
542+
bankName: string;
543+
};
544+
541545
export type {
542546
AuthenticationErrorParams,
543547
ImportMembersSuccessfullDescriptionParams,
@@ -729,6 +733,7 @@ export type {
729733
DateParams,
730734
FiltersAmountBetweenParams,
731735
StatementPageTitleParams,
736+
CompanyCardBankName,
732737
DisconnectPromptParams,
733738
DisconnectTitleParams,
734739
CharacterLengthLimitParams,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {getApiRoot} from '@libs/ApiUtils';
2+
import * as NetworkStore from '@libs/Network/NetworkStore';
3+
import CONST from '@src/CONST';
4+
5+
type CompanyCardBankConnection = {
6+
authToken: string;
7+
domainName: string;
8+
scrapeMinDate: string;
9+
isCorporate: string;
10+
};
11+
12+
// TODO remove this when BE will support bank UI callbacks
13+
const bankUrl = 'https://secure.chase.com/web/auth/#/logon/logon/chaseOnline?redirect_url=';
14+
15+
export default function getCompanyCardBankConnection(bankName?: string, domainName?: string, scrapeMinDate?: string) {
16+
const bankConnection = Object.keys(CONST.COMPANY_CARDS.BANKS).find((key) => CONST.COMPANY_CARDS.BANKS[key as keyof typeof CONST.COMPANY_CARDS.BANKS] === bankName);
17+
18+
// TODO remove this when BE will support bank UI callbacks
19+
if (!domainName) {
20+
return bankUrl;
21+
}
22+
23+
if (!bankName || !bankConnection) {
24+
return null;
25+
}
26+
const authToken = NetworkStore.getAuthToken();
27+
const params: CompanyCardBankConnection = {authToken: authToken ?? '', domainName: domainName ?? '', isCorporate: 'true', scrapeMinDate: scrapeMinDate ?? ''};
28+
const commandURL = getApiRoot({
29+
shouldSkipWebProxy: true,
30+
command: '',
31+
});
32+
const bank = CONST.COMPANY_CARDS.BANK_CONNECTIONS[bankConnection as keyof typeof CONST.COMPANY_CARDS.BANK_CONNECTIONS];
33+
return `${commandURL}partners/banks/${bank}/oauth_callback.php?${new URLSearchParams(params).toString()}`;
34+
}

src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPol
66
import CONST from '@src/CONST';
77
import ONYXKEYS from '@src/ONYXKEYS';
88
import AmexCustomFeed from './AmexCustomFeed';
9+
import BankConnection from './BankConnection';
910
import CardInstructionsStep from './CardInstructionsStep';
1011
import CardNameStep from './CardNameStep';
1112
import CardTypeStep from './CardTypeStep';
@@ -28,6 +29,8 @@ function AddNewCardPage({policy}: WithPolicyAndFullscreenLoadingProps) {
2829
return <SelectFeedType />;
2930
case CONST.COMPANY_CARDS.STEP.CARD_TYPE:
3031
return <CardTypeStep />;
32+
case CONST.COMPANY_CARDS.STEP.BANK_CONNECTION:
33+
return <BankConnection />;
3134
case CONST.COMPANY_CARDS.STEP.CARD_INSTRUCTIONS:
3235
return <CardInstructionsStep policyID={policyID} />;
3336
case CONST.COMPANY_CARDS.STEP.CARD_NAME:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, {useEffect, useRef, useState} from 'react';
2+
import {useOnyx} from 'react-native-onyx';
3+
import {WebView} from 'react-native-webview';
4+
import type {ValueOf} from 'type-fest';
5+
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
6+
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
7+
import HeaderWithBackButton from '@components/HeaderWithBackButton';
8+
import Modal from '@components/Modal';
9+
import useLocalize from '@hooks/useLocalize';
10+
import getUAForWebView from '@libs/getUAForWebView';
11+
import * as CompanyCards from '@userActions/CompanyCards';
12+
import getCompanyCardBankConnection from '@userActions/getCompanyCardBankConnection';
13+
import CONST from '@src/CONST';
14+
import ONYXKEYS from '@src/ONYXKEYS';
15+
16+
function BankConnection() {
17+
const {translate} = useLocalize();
18+
const webViewRef = useRef<WebView>(null);
19+
const [isWebViewOpen, setWebViewOpen] = useState(false);
20+
const [session] = useOnyx(ONYXKEYS.SESSION);
21+
const authToken = session?.authToken ?? null;
22+
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
23+
const bankName: ValueOf<typeof CONST.COMPANY_CARDS.BANKS> | undefined = addNewCard?.data?.selectedBank;
24+
const url = getCompanyCardBankConnection(bankName);
25+
26+
const renderLoading = () => <FullScreenLoadingIndicator />;
27+
28+
const handleBackButtonPress = () => {
29+
setWebViewOpen(false);
30+
if (bankName === CONST.COMPANY_CARDS.BANKS.BREX) {
31+
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK});
32+
return;
33+
}
34+
if (bankName === CONST.COMPANY_CARDS.BANKS.AMEX) {
35+
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.AMEX_CUSTOM_FEED});
36+
return;
37+
}
38+
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_FEED_TYPE});
39+
};
40+
41+
useEffect(() => {
42+
setWebViewOpen(true);
43+
}, []);
44+
45+
return (
46+
<Modal
47+
onClose={handleBackButtonPress}
48+
fullscreen
49+
isVisible={isWebViewOpen}
50+
type={CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE}
51+
>
52+
<HeaderWithBackButton
53+
title={translate('workspace.companyCards.addCardFeed')}
54+
onBackButtonPress={handleBackButtonPress}
55+
/>
56+
<FullPageOfflineBlockingView>
57+
{url && (
58+
<WebView
59+
ref={webViewRef}
60+
source={{
61+
uri: url,
62+
headers: {
63+
Cookie: `authToken=${authToken}`,
64+
},
65+
}}
66+
userAgent={getUAForWebView()}
67+
incognito
68+
startInLoadingState
69+
renderLoading={renderLoading}
70+
/>
71+
)}
72+
</FullPageOfflineBlockingView>
73+
</Modal>
74+
);
75+
}
76+
77+
BankConnection.displayName = 'BankConnection';
78+
79+
export default BankConnection;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, {useCallback, useEffect} from 'react';
2+
import {useOnyx} from 'react-native-onyx';
3+
import type {ValueOf} from 'type-fest';
4+
import BlockingView from '@components/BlockingViews/BlockingView';
5+
import HeaderWithBackButton from '@components/HeaderWithBackButton';
6+
import * as Illustrations from '@components/Icon/Illustrations';
7+
import ScreenWrapper from '@components/ScreenWrapper';
8+
import Text from '@components/Text';
9+
import TextLink from '@components/TextLink';
10+
import useLocalize from '@hooks/useLocalize';
11+
import useThemeStyles from '@hooks/useThemeStyles';
12+
import getCurrentUrl from '@navigation/currentUrl';
13+
import * as CompanyCards from '@userActions/CompanyCards';
14+
import getCompanyCardBankConnection from '@userActions/getCompanyCardBankConnection';
15+
import CONST from '@src/CONST';
16+
import ONYXKEYS from '@src/ONYXKEYS';
17+
import ROUTES from '@src/ROUTES';
18+
import openBankConnection from './openBankConnection';
19+
20+
let customWindow: Window | null = null;
21+
22+
function BankConnection() {
23+
const styles = useThemeStyles();
24+
const {translate} = useLocalize();
25+
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
26+
const bankName: ValueOf<typeof CONST.COMPANY_CARDS.BANKS> | undefined = addNewCard?.data?.selectedBank;
27+
const currentUrl = getCurrentUrl();
28+
const isBankConnectionCompleteRoute = currentUrl.includes(ROUTES.BANK_CONNECTION_COMPLETE);
29+
const url = getCompanyCardBankConnection(bankName);
30+
31+
const onOpenBankConnectionFlow = useCallback(() => {
32+
if (!url) {
33+
return;
34+
}
35+
customWindow = openBankConnection(url);
36+
}, [url]);
37+
38+
const handleBackButtonPress = () => {
39+
customWindow?.close();
40+
if (bankName === CONST.COMPANY_CARDS.BANKS.BREX) {
41+
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK});
42+
return;
43+
}
44+
if (bankName === CONST.COMPANY_CARDS.BANKS.AMEX) {
45+
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.AMEX_CUSTOM_FEED});
46+
return;
47+
}
48+
CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_FEED_TYPE});
49+
};
50+
51+
const CustomSubtitle = (
52+
<Text style={[styles.textAlignCenter, styles.textSupporting]}>
53+
{bankName && translate(`workspace.moreFeatures.companyCards.pendingBankDescription`, {bankName})}
54+
<TextLink onPress={onOpenBankConnectionFlow}>{translate('workspace.moreFeatures.companyCards.pendingBankLink')}</TextLink>
55+
</Text>
56+
);
57+
58+
useEffect(() => {
59+
if (!url) {
60+
return;
61+
}
62+
if (isBankConnectionCompleteRoute) {
63+
customWindow?.close();
64+
return;
65+
}
66+
customWindow = openBankConnection(url);
67+
}, [isBankConnectionCompleteRoute, url]);
68+
69+
return (
70+
<ScreenWrapper testID={BankConnection.displayName}>
71+
<HeaderWithBackButton
72+
title={translate('workspace.companyCards.addCardFeed')}
73+
onBackButtonPress={handleBackButtonPress}
74+
/>
75+
<BlockingView
76+
icon={Illustrations.PendingBank}
77+
iconWidth={styles.pendingBankCardIllustration.width}
78+
iconHeight={styles.pendingBankCardIllustration.height}
79+
title={translate('workspace.moreFeatures.companyCards.pendingBankTitle')}
80+
linkKey="workspace.moreFeatures.companyCards.pendingBankLink"
81+
CustomSubtitle={CustomSubtitle}
82+
shouldShowLink
83+
onLinkPress={onOpenBankConnectionFlow}
84+
/>
85+
</ScreenWrapper>
86+
);
87+
}
88+
89+
BankConnection.displayName = 'BankConnection';
90+
91+
export default BankConnection;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const handleOpenBankConnectionFlow = (url: string) => {
2+
return window.open(url, '_blank');
3+
};
4+
5+
export default handleOpenBankConnectionFlow;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const WINDOW_WIDTH = 700;
2+
const WINDOW_HEIGHT = 600;
3+
4+
const handleOpenBankConnectionFlow = (url: string) => {
5+
const screenWidth = window.screen.width;
6+
const screenHeight = window.screen.height;
7+
const left = (screenWidth - WINDOW_WIDTH) / 2;
8+
const top = (screenHeight - WINDOW_HEIGHT) / 2;
9+
const popupFeatures = `width=${WINDOW_WIDTH},height=${WINDOW_HEIGHT},left=${left},top=${top},scrollbars=yes,resizable=yes`;
10+
11+
return window.open(url, 'popupWindow', popupFeatures);
12+
};
13+
14+
export default handleOpenBankConnectionFlow;

src/pages/workspace/companyCards/addNew/SelectFeedType.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function SelectFeedType() {
2828
return;
2929
}
3030
CompanyCards.setAddNewCompanyCardStepAndData({
31-
step: typeSelected === CONST.COMPANY_CARDS.FEED_TYPE.DIRECT ? CONST.COMPANY_CARDS.STEP.SELECT_BANK : CONST.COMPANY_CARDS.STEP.CARD_TYPE,
31+
step: typeSelected === CONST.COMPANY_CARDS.FEED_TYPE.DIRECT ? CONST.COMPANY_CARDS.STEP.BANK_CONNECTION : CONST.COMPANY_CARDS.STEP.CARD_TYPE,
3232
data: {selectedFeedType: typeSelected},
3333
});
3434
};

src/styles/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -5157,6 +5157,11 @@ const styles = (theme: ThemeColors) =>
51575157
height: 188,
51585158
},
51595159

5160+
pendingBankCardIllustration: {
5161+
width: 217,
5162+
height: 150,
5163+
},
5164+
51605165
cardIcon: {
51615166
overflow: 'hidden',
51625167
borderRadius: variables.cardBorderRadius,

0 commit comments

Comments
 (0)