diff --git a/src/CONST.ts b/src/CONST.ts index 67ad4c941822..1460d74af149 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1069,6 +1069,12 @@ const CONST = { }, }, + KYC_WALL_SOURCE: { + REPORT: 'REPORT', // The user attempted to pay a money request + ENABLE_WALLET: 'ENABLE_WALLET', // The user clicked on the `Enable wallet` button on the Wallet page + TRANSFER_BALANCE: 'TRANSFER_BALANCE', // The user attempted to transfer their wallet balance to their bank account or debit card + }, + OS: { WINDOWS: 'Windows', MAC_OS: PLATFORM_OS_MACOS, diff --git a/src/components/KYCWall/BaseKYCWall.js b/src/components/KYCWall/BaseKYCWall.js index db3c85ef818c..ccee8bc4e6a0 100644 --- a/src/components/KYCWall/BaseKYCWall.js +++ b/src/components/KYCWall/BaseKYCWall.js @@ -24,6 +24,7 @@ class KYCWall extends React.Component { this.continue = this.continue.bind(this); this.setMenuPosition = this.setMenuPosition.bind(this); + this.selectPaymentMethod = this.selectPaymentMethod.bind(this); this.anchorRef = React.createRef(null); this.state = { @@ -39,7 +40,6 @@ class KYCWall extends React.Component { if (this.props.shouldListenForResize) { this.dimensionsSubscription = Dimensions.addEventListener('change', this.setMenuPosition); } - Wallet.setKYCWallSourceChatReportID(this.props.chatReportID); } componentWillUnmount() { @@ -88,6 +88,18 @@ class KYCWall extends React.Component { }); } + /** + * @param {String} paymentMethod + */ + selectPaymentMethod(paymentMethod) { + this.props.onSelectPaymentMethod(paymentMethod); + if (paymentMethod === CONST.PAYMENT_METHODS.BANK_ACCOUNT) { + Navigation.navigate(this.props.addBankAccountRoute); + } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) { + Navigation.navigate(this.props.addDebitCardRoute); + } + } + /** * Take the position of the button that calls this method and show the Add Payment method menu when the user has no valid payment method. * If they do have a valid payment method they are navigated to the "enable payments" route to complete KYC checks. @@ -97,6 +109,14 @@ class KYCWall extends React.Component { * @param {String} iouPaymentType */ continue(event, iouPaymentType) { + const currentSource = lodashGet(this.props.walletTerms, 'source', this.props.source); + + /** + * Set the source, so we can tailor the process according to how we got here. + * We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold. + */ + Wallet.setKYCWallSource(this.props.source, this.props.chatReportID); + if (this.state.shouldShowAddPaymentMenu) { this.setState({shouldShowAddPaymentMenu: false}); return; @@ -111,9 +131,13 @@ class KYCWall extends React.Component { // Check to see if user has a valid payment method on file and display the add payment popover if they don't if ( (isExpenseReport && lodashGet(this.props.reimbursementAccount, 'achData.state', '') !== CONST.BANK_ACCOUNT.STATE.OPEN) || - (!isExpenseReport && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, this.props.bankAccountList)) + (!isExpenseReport && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, this.props.bankAccountList, this.props.shouldIncludeDebitCard)) ) { Log.info('[KYC Wallet] User does not have valid payment method'); + if (!this.props.shouldIncludeDebitCard) { + this.selectPaymentMethod(CONST.PAYMENT_METHODS.BANK_ACCOUNT); + return; + } const clickedElementLocation = getClickedTargetLocation(targetElement); const position = this.getAnchorPosition(clickedElementLocation); this.setPositionAddPaymentMenu(position); @@ -132,7 +156,7 @@ class KYCWall extends React.Component { } } Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them'); - this.props.onSuccessfulKYC(iouPaymentType); + this.props.onSuccessfulKYC(iouPaymentType, currentSource); } render() { @@ -149,11 +173,7 @@ class KYCWall extends React.Component { anchorAlignment={this.props.anchorAlignment} onItemSelected={(item) => { this.setState({shouldShowAddPaymentMenu: false}); - if (item === CONST.PAYMENT_METHODS.BANK_ACCOUNT) { - Navigation.navigate(this.props.addBankAccountRoute); - } else if (item === CONST.PAYMENT_METHODS.DEBIT_CARD) { - Navigation.navigate(this.props.addDebitCardRoute); - } + this.selectPaymentMethod(item); }} /> {this.props.children(this.continue, this.anchorRef)} @@ -169,6 +189,9 @@ export default withOnyx({ userWallet: { key: ONYXKEYS.USER_WALLET, }, + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, fundList: { key: ONYXKEYS.FUND_LIST, }, diff --git a/src/components/KYCWall/kycWallPropTypes.js b/src/components/KYCWall/kycWallPropTypes.js index 6c117eb67f5b..b585535784dc 100644 --- a/src/components/KYCWall/kycWallPropTypes.js +++ b/src/components/KYCWall/kycWallPropTypes.js @@ -5,6 +5,7 @@ import bankAccountPropTypes from '../bankAccountPropTypes'; import cardPropTypes from '../cardPropTypes'; import iouReportPropTypes from '../../pages/iouReportPropTypes'; import reimbursementAccountPropTypes from '../../pages/ReimbursementAccount/ReimbursementAccountDraftPropTypes'; +import walletTermsPropTypes from '../../pages/EnablePayments/walletTermsPropTypes'; import CONST from '../../CONST'; const propTypes = { @@ -26,6 +27,12 @@ const propTypes = { /** The user's wallet */ userWallet: userWalletPropTypes, + /** Information related to the last step of the wallet activation flow */ + walletTerms: walletTermsPropTypes, + + /** The source that triggered the KYC wall */ + source: PropTypes.oneOf(_.values(CONST.KYC_WALL_SOURCE)).isRequired, + /** When the button is opened via an IOU, ID for the chatReport that the IOU is linked to */ chatReportID: PropTypes.string, @@ -49,10 +56,17 @@ const propTypes = { horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), }), + + /** Whether the option to add a debit card should be included */ + shouldIncludeDebitCard: PropTypes.bool, + + /** Callback for when a payment method has been selected */ + onSelectPaymentMethod: PropTypes.func, }; const defaultProps = { userWallet: {}, + walletTerms: {}, shouldListenForResize: false, isDisabled: false, chatReportID: '', @@ -66,6 +80,8 @@ const defaultProps = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }, + shouldIncludeDebitCard: true, + onSelectPaymentMethod: () => {}, }; export {propTypes, defaultProps}; diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js index 67673d664ac3..287f3210b14d 100644 --- a/src/components/SettlementButton.js +++ b/src/components/SettlementButton.js @@ -207,6 +207,7 @@ function SettlementButton({ addBankAccountRoute={addBankAccountRoute} addDebitCardRoute={addDebitCardRoute} isDisabled={isOffline} + source={CONST.KYC_WALL_SOURCE.REPORT} chatReportID={chatReportID} iouReport={iouReport} anchorAlignment={anchorAlignment} diff --git a/src/languages/en.ts b/src/languages/en.ts index 0e8512fb254f..e110d91c58a5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -857,6 +857,8 @@ export default { receiveMoney: 'Receive money in your local currency', expensifyWallet: 'Expensify Wallet', sendAndReceiveMoney: 'Send and receive money from your Expensify Wallet.', + enableWalletToSendAndReceiveMoney: 'Enable your Expensify Wallet to start sending and receiving money with friends!', + enableWallet: 'Enable wallet', bankAccounts: 'Bank accounts', addBankAccountToSendAndReceive: 'Add a bank account to send and receive payments directly in the app.', addBankAccount: 'Add bank account', @@ -1219,7 +1221,7 @@ export default { }, additionalDetailsStep: { headerTitle: 'Additional details', - helpText: 'We need to confirm the following information before we can process this payment.', + helpText: 'We need to confirm the following information before you can send and receive money from your Wallet.', helpTextIdologyQuestions: 'We need to ask you just a few more questions to finish validating your identity.', helpLink: 'Learn more about why we need this.', legalFirstNameLabel: 'Legal first name', diff --git a/src/languages/es.ts b/src/languages/es.ts index a8ee93e35282..ef106576212a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -853,6 +853,8 @@ export default { receiveMoney: 'Recibe dinero en tu moneda local', expensifyWallet: 'Billetera Expensify', sendAndReceiveMoney: 'Envía y recibe dinero desde tu Billetera Expensify.', + enableWalletToSendAndReceiveMoney: 'Habilita tu Billetera Expensify para comenzar a enviar y recibir dinero con amigos', + enableWallet: 'Habilitar Billetera', bankAccounts: 'Cuentas bancarias', addBankAccountToSendAndReceive: 'Añade una cuenta bancaria para enviar y recibir pagos directamente en la aplicación.', addBankAccount: 'Agregar cuenta bancaria', @@ -1238,7 +1240,7 @@ export default { }, additionalDetailsStep: { headerTitle: 'Detalles adicionales', - helpText: 'Necesitamos confirmar la siguiente información antes de que podamos procesar el pago.', + helpText: 'Necesitamos confirmar la siguiente información antes de que puedas enviar y recibir dinero desde tu Billetera.', helpTextIdologyQuestions: 'Tenemos que preguntarte unas preguntas más para terminar de verificar tu identidad', helpLink: 'Obtén más información sobre por qué necesitamos esto.', legalFirstNameLabel: 'Primer nombre legal', diff --git a/src/libs/PaymentUtils.ts b/src/libs/PaymentUtils.ts index 7ea935577fb1..90ec2b3ccc52 100644 --- a/src/libs/PaymentUtils.ts +++ b/src/libs/PaymentUtils.ts @@ -11,16 +11,16 @@ type AccountType = BankAccount['accountType'] | Fund['accountType']; /** * Check to see if user has either a debit card or personal bank account added */ -function hasExpensifyPaymentMethod(fundList: Record, bankAccountList: Record): boolean { +function hasExpensifyPaymentMethod(fundList: Record, bankAccountList: Record, shouldIncludeDebitCard = true): boolean { const validBankAccount = Object.values(bankAccountList).some((bankAccountJSON) => { const bankAccount = new BankAccountModel(bankAccountJSON); - return bankAccount.isDefaultCredit(); + return bankAccount.getPendingAction() !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && bankAccount.isDefaultCredit(); }); // Hide any billing cards that are not P2P debit cards for now because you cannot make them your default method, or delete them const validDebitCard = Object.values(fundList).some((card) => card?.accountData?.additionalData?.isP2PDebitCard ?? false); - return validBankAccount || validDebitCard; + return validBankAccount || (shouldIncludeDebitCard && validDebitCard); } function getPaymentMethodDescription(accountType: AccountType, account: BankAccount['accountData'] | Fund['accountData']): string { diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index a0d035292773..bf4f170f1ba7 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -56,7 +56,7 @@ function openPersonalBankAccountSetupView(exitReportID: string) { } /** - * Whether after adding a bank account we should continue with the KYC flow + * Whether after adding a bank account we should continue with the KYC flow. If so, we must specify the fallback route. */ function setPersonalBankAccountContinueKYCOnSuccess(onSuccessFallbackRoute: string) { Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {onSuccessFallbackRoute}); diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 38ca49e4836a..183920eccf21 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -69,12 +69,13 @@ function setAdditionalDetailsErrorMessage(additionalErrorMessage) { } /** - * Save the ID of the chat whose IOU triggered showing the KYC wall. + * Save the source that triggered the KYC wall and optionally the chat report ID associated with the IOU * + * @param {String} source * @param {String} chatReportID */ -function setKYCWallSourceChatReportID(chatReportID) { - Onyx.merge(ONYXKEYS.WALLET_TERMS, {chatReportID}); +function setKYCWallSource(source, chatReportID = '') { + Onyx.merge(ONYXKEYS.WALLET_TERMS, {source, chatReportID}); } /** @@ -333,5 +334,5 @@ export { updatePersonalDetails, verifyIdentity, acceptWalletTerms, - setKYCWallSourceChatReportID, + setKYCWallSource, }; diff --git a/src/libs/models/BankAccount.js b/src/libs/models/BankAccount.js index c0dee2a672ff..ec75e92ab27e 100644 --- a/src/libs/models/BankAccount.js +++ b/src/libs/models/BankAccount.js @@ -222,6 +222,14 @@ class BankAccount { return this.json.accountData.additionalData || {}; } + /** + * Get the pending action of the bank account + * @returns {String} + */ + getPendingAction() { + return lodashGet(this.json, 'pendingAction', ''); + } + /** * Return a map needed to setup a withdrawal account * @returns {Object} diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js index a560a467bc7b..1eda16ad841a 100644 --- a/src/pages/AddPersonalBankAccountPage.js +++ b/src/pages/AddPersonalBankAccountPage.js @@ -7,6 +7,7 @@ import HeaderWithBackButton from '../components/HeaderWithBackButton'; import ScreenWrapper from '../components/ScreenWrapper'; import Navigation from '../libs/Navigation/Navigation'; import * as BankAccounts from '../libs/actions/BankAccounts'; +import * as PaymentMethods from '../libs/actions/PaymentMethods'; import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import AddPlaidBankAccount from '../components/AddPlaidBankAccount'; import getPlaidOAuthReceivedRedirectURI from '../libs/getPlaidOAuthReceivedRedirectURI'; @@ -17,7 +18,6 @@ import Form from '../components/Form'; import ROUTES from '../ROUTES'; import * as PlaidDataProps from './ReimbursementAccount/plaidDataPropTypes'; import ConfirmationPage from '../components/ConfirmationPage'; -import * as PaymentMethods from '../libs/actions/PaymentMethods'; const propTypes = { ...withLocalizePropTypes, @@ -36,6 +36,9 @@ const propTypes = { /** Any reportID we should redirect to at the end of the flow */ exitReportID: PropTypes.string, + /** Whether we should continue with KYC at the end of the flow */ + shouldContinueKYCOnSuccess: PropTypes.bool, + /** Whether the form is loading */ isLoading: PropTypes.bool, @@ -52,6 +55,7 @@ const defaultProps = { isLoading: false, plaidAccountID: '', exitReportID: '', + shouldContinueKYCOnSuccess: false, }, }; diff --git a/src/pages/EnablePayments/ActivateStep.js b/src/pages/EnablePayments/ActivateStep.js index 2d23f39d25e5..2badb5595f3b 100644 --- a/src/pages/EnablePayments/ActivateStep.js +++ b/src/pages/EnablePayments/ActivateStep.js @@ -25,6 +25,7 @@ const propTypes = { const defaultProps = { userWallet: {}, walletTerms: { + source: '', chatReportID: 0, }, }; @@ -32,7 +33,15 @@ const defaultProps = { function ActivateStep(props) { const isActivatedWallet = _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], props.userWallet.tierName); const animation = isActivatedWallet ? LottieAnimations.Fireworks : LottieAnimations.ReviewingBankInfo; - const continueButtonText = props.walletTerms.chatReportID ? props.translate('activateStep.continueToPayment') : props.translate('activateStep.continueToTransfer'); + let continueButtonText = ''; + + if (props.walletTerms.chatReportID) { + continueButtonText = props.translate('activateStep.continueToPayment'); + } else if (props.walletTerms.source === CONST.KYC_WALL_SOURCE.ENABLE_WALLET) { + continueButtonText = props.translate('common.continue'); + } else { + continueButtonText = props.translate('activateStep.continueToTransfer'); + } return ( <> @@ -43,7 +52,7 @@ function ActivateStep(props) { description={props.translate(`activateStep.${isActivatedWallet ? 'activated' : 'checkBackLater'}Message`)} shouldShowButton={isActivatedWallet} buttonText={continueButtonText} - onButtonPress={PaymentMethods.continueSetup} + onButtonPress={() => PaymentMethods.continueSetup()} /> ); diff --git a/src/pages/EnablePayments/walletTermsPropTypes.js b/src/pages/EnablePayments/walletTermsPropTypes.js index c5f19cd3a9f2..4dadd9946149 100644 --- a/src/pages/EnablePayments/walletTermsPropTypes.js +++ b/src/pages/EnablePayments/walletTermsPropTypes.js @@ -1,10 +1,15 @@ import PropTypes from 'prop-types'; +import _ from 'underscore'; +import CONST from '../../CONST'; /** Prop types related to the Terms step of KYC flow */ export default PropTypes.shape({ /** Any error message to show */ errors: PropTypes.objectOf(PropTypes.string), + /** The source that triggered the KYC wall */ + source: PropTypes.oneOf(_.values(CONST.KYC_WALL_SOURCE)), + /** When the user accepts the Wallet's terms in order to pay an IOU, this is the ID of the chatReport the IOU is linked to */ chatReportID: PropTypes.string, }); diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index ec9ff537189e..37bb49952984 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -62,6 +62,7 @@ function WalletPage({bankAccountList, betas, cardList, fundList, isLoadingPaymen const hasBankAccount = !_.isEmpty(bankAccountList) || !_.isEmpty(fundList); const hasWallet = userWallet.walletLinkedAccountID > 0; + const hasActivatedWallet = _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], userWallet.tierName); const hasAssignedCard = !_.isEmpty(cardList); const shouldShowEmptyState = !hasBankAccount && !hasWallet && !hasAssignedCard; @@ -241,8 +242,13 @@ function WalletPage({bankAccountList, betas, cardList, fundList, isLoadingPaymen } }, [paymentMethod.selectedPaymentMethod.bankAccountID, paymentMethod.selectedPaymentMethod.fundID, paymentMethod.selectedPaymentMethodType]); - const navigateToTransferBalancePage = () => { - Navigation.navigate(ROUTES.SETTINGS_WALLET_TRANSFER_BALANCE); + /** + * Navigate to the appropriate page after completing the KYC flow, depending on what initiated it + * + * @param {String} source + */ + const navigateToWalletOrTransferBalancePage = (source) => { + Navigation.navigate(source === CONST.KYC_WALL_SOURCE.ENABLE_WALLET ? ROUTES.SETTINGS_WALLET : ROUTES.SETTINGS_WALLET_TRANSFER_BALANCE); }; useEffect(() => { @@ -330,7 +336,7 @@ function WalletPage({bankAccountList, betas, cardList, fundList, isLoadingPaymen {hasWallet && ( <> @@ -352,23 +358,44 @@ function WalletPage({bankAccountList, betas, cardList, fundList, isLoadingPaymen )} navigateToWalletOrTransferBalancePage(source)} + onSelectPaymentMethod={(selectedPaymentMethod) => { + if (hasActivatedWallet || selectedPaymentMethod !== CONST.PAYMENT_METHODS.BANK_ACCOUNT) { + return; + } + // To allow upgrading to a gold wallet, continue with the KYC flow after adding a bank account + BankAccounts.setPersonalBankAccountContinueKYCOnSuccess(ROUTES.SETTINGS_WALLET); + }} enablePaymentsRoute={ROUTES.SETTINGS_ENABLE_PAYMENTS} addBankAccountRoute={ROUTES.SETTINGS_ADD_BANK_ACCOUNT} addDebitCardRoute={ROUTES.SETTINGS_ADD_DEBIT_CARD} popoverPlacement="bottom" + source={hasActivatedWallet ? CONST.KYC_WALL_SOURCE.TRANSFER_BALANCE : CONST.KYC_WALL_SOURCE.ENABLE_WALLET} + shouldIncludeDebitCard={hasActivatedWallet} > - {(triggerKYCFlow, buttonRef) => ( - - )} + {(triggerKYCFlow, buttonRef) => + hasActivatedWallet ? ( + + ) : ( +