Skip to content

Commit

Permalink
Merge pull request #50716 from narefyev91/company-cards-bank-ui
Browse files Browse the repository at this point in the history
Company Cards - Bank UI
  • Loading branch information
mountiny authored Oct 21, 2024
2 parents 4575341 + be97e7d commit b2fc5c9
Show file tree
Hide file tree
Showing 15 changed files with 519 additions and 1 deletion.
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 @@ -2623,6 +2623,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 @@ -3314,6 +3315,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 @@ -3359,6 +3360,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=';

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;
91 changes: 91 additions & 0 deletions src/pages/workspace/companyCards/addNew/BankConnection/index.tsx
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;
2 changes: 1 addition & 1 deletion src/pages/workspace/companyCards/addNew/SelectFeedType.tsx
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

0 comments on commit b2fc5c9

Please sign in to comment.