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
226 changes: 127 additions & 99 deletions src/components/KYCWall/BaseKYCWall.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, {useEffect, useState, useRef, useCallback} from 'react';
import {withOnyx} from 'react-native-onyx';
import {Dimensions} from 'react-native';
import lodashGet from 'lodash/get';
Expand All @@ -14,78 +14,97 @@ 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.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);
}
Wallet.setKYCWallSourceChatReportID(this.props.chatReportID);
}
function KYCWall({
shouldListenForResize,
chatReportID,
iouReport,
fundList,
reimbursementAccount,
bankAccountList,
userWallet,
enablePaymentsRoute,
onSuccessfulKYC,
addBankAccountRoute,
addDebitCardRoute,
anchorAlignment,
children,
}) {
const anchorRef = useRef(null);
const transferBalanceButtonRef = useRef(null);

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);
}
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]);

/**
* 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 @@ -95,74 +114,83 @@ class KYCWall extends React.Component {
* @param {Event} event
* @param {String} iouPaymentType
*/
continue(event, iouPaymentType) {
if (this.state.shouldShowAddPaymentMenu) {
this.setState({shouldShowAddPaymentMenu: false});
const continueAction = (event, iouPaymentType) => {
if (shouldShowAddPaymentMenu) {
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

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

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 hasGoldWallet = this.props.userWallet.tierName && this.props.userWallet.tierName === CONST.WALLET.TIER_NAME.GOLD;
const hasGoldWallet = userWallet.tierName && userWallet.tierName === CONST.WALLET.TIER_NAME.GOLD;

if (!hasGoldWallet) {
Log.info('[KYC Wallet] User does not have gold 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);
}

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});
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.props.children(this.continue, this.anchorRef)}
</>
);
}
onSuccessfulKYC(iouPaymentType);
};

const handleItemSelected = (item) => {
setShouldShowAddPaymentMenu(false);

if (item === CONST.PAYMENT_METHODS.BANK_ACCOUNT) {
Navigation.navigate(addBankAccountRoute);
} else if (item === CONST.PAYMENT_METHODS.DEBIT_CARD) {
Navigation.navigate(addDebitCardRoute);
}
};

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

KYCWall.propTypes = propTypes;
KYCWall.defaultProps = defaultProps;
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.displayName = 'BaseKYCWall';

export default withOnyx({
userWallet: {
Expand Down
4 changes: 2 additions & 2 deletions src/libs/actions/PaymentMethods.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ const kycWallRef = createRef();
* 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() {
if (!kycWallRef.current || !kycWallRef.current.continue) {
if (!kycWallRef.current || !kycWallRef.current.continueAction) {
Navigation.goBack(ROUTES.HOME);
return;
}

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

function openWalletPage() {
Expand Down
Loading