Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate KYCWall class component to functional #28798

Merged
merged 11 commits into from
Oct 20, 2023
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
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
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
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
Loading