Skip to content
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
238 changes: 133 additions & 105 deletions src/components/KYCWall/BaseKYCWall.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useEffect, useState, useRef, useCallback} from 'react';
import _ from 'underscore';
import React from 'react';
import {withOnyx} from 'react-native-onyx';
import {Dimensions} from 'react-native';
import lodashGet from 'lodash/get';
Expand All @@ -15,90 +15,114 @@ import {propTypes, defaultProps} from './kycWallPropTypes';
import * as Wallet from '../../libs/actions/Wallet';
import * as ReportUtils from '../../libs/ReportUtils';

const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20;

// This component allows us to block various actions by forcing the user to first add a default payment method and successfully make it through our Know Your Customer flow
// before continuing to take whatever action they originally intended to take. It requires a button as a child and a native event so we can get the coordinates and use it
// to render the AddPaymentMethodMenu in the correct location.
class KYCWall extends React.Component {
constructor(props) {
super(props);

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 = {
shouldShowAddPaymentMenu: false,
anchorPositionVertical: 0,
anchorPositionHorizontal: 0,
transferBalanceButton: null,
};
}

componentDidMount() {
PaymentMethods.kycWallRef.current = this;
if (this.props.shouldListenForResize) {
this.dimensionsSubscription = Dimensions.addEventListener('change', this.setMenuPosition);
}
}

componentWillUnmount() {
if (this.props.shouldListenForResize && this.dimensionsSubscription) {
this.dimensionsSubscription.remove();
}
PaymentMethods.kycWallRef.current = null;
}

setMenuPosition() {
if (!this.state.transferBalanceButton) {
return;
}
const buttonPosition = getClickedTargetLocation(this.state.transferBalanceButton);
const position = this.getAnchorPosition(buttonPosition);
this.setPositionAddPaymentMenu(position);
}
function KYCWall({
addBankAccountRoute,
addDebitCardRoute,
anchorAlignment,
bankAccountList,
chatReportID,
children,
enablePaymentsRoute,
fundList,
iouReport,
onSelectPaymentMethod,
onSuccessfulKYC,
reimbursementAccount,
shouldIncludeDebitCard,
shouldListenForResize,
source,
userWallet,
walletTerms,
}) {
const anchorRef = useRef(null);
const transferBalanceButtonRef = useRef(null);

const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false);
const [anchorPosition, setAnchorPosition] = useState({
anchorPositionVertical: 0,
anchorPositionHorizontal: 0,
});

/**
* @param {DOMRect} domRect
* @returns {Object}
*/
getAnchorPosition(domRect) {
if (this.props.anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) {
const getAnchorPosition = useCallback(
(domRect) => {
if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) {
return {
anchorPositionVertical: domRect.top + domRect.height + CONST.MODAL.POPOVER_MENU_PADDING,
anchorPositionHorizontal: domRect.left + POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET,
};
}

return {
anchorPositionVertical: domRect.top + domRect.height + CONST.MODAL.POPOVER_MENU_PADDING,
anchorPositionHorizontal: domRect.left + 20,
anchorPositionVertical: domRect.top - CONST.MODAL.POPOVER_MENU_PADDING,
anchorPositionHorizontal: domRect.left,
};
}

return {
anchorPositionVertical: domRect.top - CONST.MODAL.POPOVER_MENU_PADDING,
anchorPositionHorizontal: domRect.left,
};
}
},
[anchorAlignment.vertical],
);

/**
* Set position of the transfer payment menu
*
* @param {Object} position
*/
setPositionAddPaymentMenu(position) {
this.setState({
anchorPositionVertical: position.anchorPositionVertical,
anchorPositionHorizontal: position.anchorPositionHorizontal,
const setPositionAddPaymentMenu = ({anchorPositionVertical, anchorPositionHorizontal}) => {
setAnchorPosition({
anchorPositionVertical,
anchorPositionHorizontal,
});
}
};

const setMenuPosition = useCallback(() => {
if (!transferBalanceButtonRef.current) {
return;
}
const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current);
const position = getAnchorPosition(buttonPosition);

setPositionAddPaymentMenu(position);
}, [getAnchorPosition]);

useEffect(() => {
let dimensionsSubscription = null;

PaymentMethods.kycWallRef.current = this;

if (shouldListenForResize) {
dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition);
}

Wallet.setKYCWallSourceChatReportID(chatReportID);

return () => {
if (shouldListenForResize && dimensionsSubscription) {
dimensionsSubscription.remove();
}

PaymentMethods.kycWallRef.current = null;
};
}, [chatReportID, setMenuPosition, shouldListenForResize]);

/**
* @param {String} paymentMethod
*/
selectPaymentMethod(paymentMethod) {
this.props.onSelectPaymentMethod(paymentMethod);
const selectPaymentMethod = (paymentMethod) => {
onSelectPaymentMethod(paymentMethod);

if (paymentMethod === CONST.PAYMENT_METHODS.BANK_ACCOUNT) {
Navigation.navigate(this.props.addBankAccountRoute);
Navigation.navigate(addBankAccountRoute);
} else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) {
Navigation.navigate(this.props.addDebitCardRoute);
Navigation.navigate(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.
Expand All @@ -108,82 +132,86 @@ class KYCWall extends React.Component {
* @param {Event} event
* @param {String} iouPaymentType
*/
continue(event, iouPaymentType) {
const currentSource = lodashGet(this.props.walletTerms, 'source', this.props.source);
const continueAction = (event, iouPaymentType) => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw had to change the name as continue is a forbidden keyword

const currentSource = lodashGet(walletTerms, 'source', 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);
Wallet.setKYCWallSource(source, chatReportID);

if (shouldShowAddPaymentMenu) {
setShouldShowAddPaymentMenu(false);

if (this.state.shouldShowAddPaymentMenu) {
this.setState({shouldShowAddPaymentMenu: false});
return;
}

// Use event target as fallback if anchorRef is null for safety
const targetElement = this.anchorRef.current || event.nativeEvent.target;
this.setState({transferBalanceButton: targetElement});
const isExpenseReport = ReportUtils.isExpenseReport(this.props.iouReport);
const paymentCardList = this.props.fundList || {};
const targetElement = anchorRef.current || event.nativeEvent.target;

transferBalanceButtonRef.current = targetElement;
const isExpenseReport = ReportUtils.isExpenseReport(iouReport);
const paymentCardList = fundList || {};

// 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, this.props.shouldIncludeDebitCard))
(isExpenseReport && lodashGet(reimbursementAccount, 'achData.state', '') !== CONST.BANK_ACCOUNT.STATE.OPEN) ||
(!isExpenseReport && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard))
) {
Log.info('[KYC Wallet] User does not have valid payment method');
if (!this.props.shouldIncludeDebitCard) {
this.selectPaymentMethod(CONST.PAYMENT_METHODS.BANK_ACCOUNT);
if (!shouldIncludeDebitCard) {
selectPaymentMethod(CONST.PAYMENT_METHODS.BANK_ACCOUNT);
return;
}

const clickedElementLocation = getClickedTargetLocation(targetElement);
const position = this.getAnchorPosition(clickedElementLocation);
this.setPositionAddPaymentMenu(position);
this.setState({
shouldShowAddPaymentMenu: true,
});
const position = getAnchorPosition(clickedElementLocation);

setPositionAddPaymentMenu(position);
setShouldShowAddPaymentMenu(true);

return;
}

if (!isExpenseReport) {
// Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks
const hasActivatedWallet = this.props.userWallet.tierName && _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], this.props.userWallet.tierName);
const hasActivatedWallet = userWallet.tierName && _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], userWallet.tierName);
if (!hasActivatedWallet) {
Log.info('[KYC Wallet] User does not have active wallet');
Navigation.navigate(this.props.enablePaymentsRoute);
Navigation.navigate(enablePaymentsRoute);
return;
}
}

Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them');
this.props.onSuccessfulKYC(iouPaymentType, currentSource);
}

render() {
return (
<>
<AddPaymentMethodMenu
isVisible={this.state.shouldShowAddPaymentMenu}
onClose={() => this.setState({shouldShowAddPaymentMenu: false})}
anchorRef={this.anchorRef}
anchorPosition={{
vertical: this.state.anchorPositionVertical,
horizontal: this.state.anchorPositionHorizontal,
}}
anchorAlignment={this.props.anchorAlignment}
onItemSelected={(item) => {
this.setState({shouldShowAddPaymentMenu: false});
this.selectPaymentMethod(item);
}}
/>
{this.props.children(this.continue, this.anchorRef)}
</>
);
}
onSuccessfulKYC(iouPaymentType, currentSource);
};

return (
<>
<AddPaymentMethodMenu
isVisible={shouldShowAddPaymentMenu}
onClose={() => setShouldShowAddPaymentMenu(false)}
anchorRef={anchorRef}
anchorAlignment={anchorAlignment}
anchorPosition={{
vertical: anchorPosition.anchorPositionVertical,
horizontal: anchorPosition.anchorPositionHorizontal,
}}
onItemSelected={(item) => {
setShouldShowAddPaymentMenu(false);
selectPaymentMethod(item);
}}
/>
{children(continueAction, anchorRef)}
</>
);
}

KYCWall.propTypes = propTypes;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional components are expected to have a displayName value

Copy link
Copy Markdown
Contributor Author

@Swor71 Swor71 Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abdulrahuman5196 I am aware of this, however this component is exported from the index.js file which has the displayName https://github.com/Expensify/App/blob/main/src/components/KYCWall/index.js#L17

Would you like me to add that line to this component as well?

EDIT: I've actually added the displayName for the base component as well

KYCWall.defaultProps = defaultProps;
KYCWall.displayName = 'BaseKYCWall';

export default withOnyx({
userWallet: {
Expand Down
6 changes: 3 additions & 3 deletions src/libs/actions/PaymentMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {FilterMethodPaymentType} from '../../types/onyx/WalletTransfer';
import PaymentMethod from '../../types/onyx/PaymentMethod';

type KYCWallRef = {
continue?: () => void;
continueAction?: () => void;
};

/**
Expand All @@ -23,14 +23,14 @@ const kycWallRef = createRef<KYCWallRef>();
* When we successfully add a payment method or pass the KYC checks we will continue with our setup action if we have one set.
*/
function continueSetup(fallbackRoute = ROUTES.HOME) {
if (!kycWallRef.current?.continue) {
if (!kycWallRef.current?.continueAction) {
Navigation.goBack(fallbackRoute);
return;
}

// Close the screen (Add Debit Card, Add Bank Account, or Enable Payments) on success and continue with setup
Navigation.goBack(fallbackRoute);
kycWallRef.current.continue();
kycWallRef.current.continueAction();
}

function openWalletPage() {
Expand Down