diff --git a/assets/images/companyCards/pending-bank.svg b/assets/images/companyCards/pending-bank.svg new file mode 100644 index 000000000000..dc265466d53f --- /dev/null +++ b/assets/images/companyCards/pending-bank.svg @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CONST.ts b/src/CONST.ts index c51864208eca..2de265de53ce 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -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', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 30c7196f19b4..cf15013fed9b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -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', diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index bae8f6af1ab2..18ae1792686f 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -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'; @@ -207,6 +208,7 @@ export { Approval, WalletAlt, Workflows, + PendingBank, ThreeLeggedLaptopWoman, House, Alert, diff --git a/src/languages/en.ts b/src/languages/en.ts index 5798f7fe48e9..ae9d199d423e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -39,6 +39,7 @@ import type { ChangeTypeParams, CharacterLengthLimitParams, CharacterLimitParams, + CompanyCardBankName, CompanyCardFeedNameParams, ConfirmThatParams, ConnectionNameParams, @@ -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', diff --git a/src/languages/es.ts b/src/languages/es.ts index 84c03d5d9bf3..9e182d99a94a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -37,6 +37,7 @@ import type { ChangeTypeParams, CharacterLengthLimitParams, CharacterLimitParams, + CompanyCardBankName, CompanyCardFeedNameParams, ConfirmThatParams, ConnectionNameParams, @@ -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', diff --git a/src/languages/params.ts b/src/languages/params.ts index 02dafa76a46d..9341b914d1d0 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -538,6 +538,10 @@ type ImportedTypesParams = { importedTypes: string[]; }; +type CompanyCardBankName = { + bankName: string; +}; + export type { AuthenticationErrorParams, ImportMembersSuccessfullDescriptionParams, @@ -729,6 +733,7 @@ export type { DateParams, FiltersAmountBetweenParams, StatementPageTitleParams, + CompanyCardBankName, DisconnectPromptParams, DisconnectTitleParams, CharacterLengthLimitParams, diff --git a/src/libs/actions/getCompanyCardBankConnection/index.tsx b/src/libs/actions/getCompanyCardBankConnection/index.tsx new file mode 100644 index 000000000000..935c5d297cb0 --- /dev/null +++ b/src/libs/actions/getCompanyCardBankConnection/index.tsx @@ -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()}`; +} diff --git a/src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx b/src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx index 480bd5d538fe..105c7107548d 100644 --- a/src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx +++ b/src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx @@ -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'; @@ -28,6 +29,8 @@ function AddNewCardPage({policy}: WithPolicyAndFullscreenLoadingProps) { return ; case CONST.COMPANY_CARDS.STEP.CARD_TYPE: return ; + case CONST.COMPANY_CARDS.STEP.BANK_CONNECTION: + return ; case CONST.COMPANY_CARDS.STEP.CARD_INSTRUCTIONS: return ; case CONST.COMPANY_CARDS.STEP.CARD_NAME: diff --git a/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx new file mode 100644 index 000000000000..8de2b9baba9a --- /dev/null +++ b/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx @@ -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(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 | undefined = addNewCard?.data?.selectedBank; + const url = getCompanyCardBankConnection(bankName); + + const renderLoading = () => ; + + 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 ( + + + + {url && ( + + )} + + + ); +} + +BankConnection.displayName = 'BankConnection'; + +export default BankConnection; diff --git a/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx b/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx new file mode 100644 index 000000000000..2b4d86f2e43b --- /dev/null +++ b/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx @@ -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 | 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 = ( + + {bankName && translate(`workspace.moreFeatures.companyCards.pendingBankDescription`, {bankName})} + {translate('workspace.moreFeatures.companyCards.pendingBankLink')} + + ); + + useEffect(() => { + if (!url) { + return; + } + if (isBankConnectionCompleteRoute) { + customWindow?.close(); + return; + } + customWindow = openBankConnection(url); + }, [isBankConnectionCompleteRoute, url]); + + return ( + + + + + ); +} + +BankConnection.displayName = 'BankConnection'; + +export default BankConnection; diff --git a/src/pages/workspace/companyCards/addNew/BankConnection/openBankConnection/index.tsx b/src/pages/workspace/companyCards/addNew/BankConnection/openBankConnection/index.tsx new file mode 100644 index 000000000000..91a81bdbd6c6 --- /dev/null +++ b/src/pages/workspace/companyCards/addNew/BankConnection/openBankConnection/index.tsx @@ -0,0 +1,5 @@ +const handleOpenBankConnectionFlow = (url: string) => { + return window.open(url, '_blank'); +}; + +export default handleOpenBankConnectionFlow; diff --git a/src/pages/workspace/companyCards/addNew/BankConnection/openBankConnection/index.website.tsx b/src/pages/workspace/companyCards/addNew/BankConnection/openBankConnection/index.website.tsx new file mode 100644 index 000000000000..220404cee0e7 --- /dev/null +++ b/src/pages/workspace/companyCards/addNew/BankConnection/openBankConnection/index.website.tsx @@ -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; diff --git a/src/pages/workspace/companyCards/addNew/SelectFeedType.tsx b/src/pages/workspace/companyCards/addNew/SelectFeedType.tsx index fd4fe021185c..959e7c10f3aa 100644 --- a/src/pages/workspace/companyCards/addNew/SelectFeedType.tsx +++ b/src/pages/workspace/companyCards/addNew/SelectFeedType.tsx @@ -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}, }); }; diff --git a/src/styles/index.ts b/src/styles/index.ts index 71e8e0259bab..3ea14bb14515 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5157,6 +5157,11 @@ const styles = (theme: ThemeColors) => height: 188, }, + pendingBankCardIllustration: { + width: 217, + height: 150, + }, + cardIcon: { overflow: 'hidden', borderRadius: variables.cardBorderRadius,