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

Fix: Add payment dynamic #7566

Merged
merged 6 commits into from
Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -1,39 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import {ActivityIndicator} from 'react-native';
import themeColors from '../styles/themes/default';
import CONST from '../CONST';
import Navigation from '../libs/Navigation/Navigation';
import AddPaymentMethodMenu from './AddPaymentMethodMenu';
import getClickedElementLocation from '../libs/getClickedElementLocation';
import * as PaymentUtils from '../libs/PaymentUtils';
import * as PaymentMethods from '../libs/actions/PaymentMethods';
import ONYXKEYS from '../ONYXKEYS';
import userWalletPropTypes from '../pages/EnablePayments/userWalletPropTypes';
import Log from '../libs/Log';

const propTypes = {
/** Route for the Add Bank Account screen for a given navigation stack */
addBankAccountRoute: PropTypes.string.isRequired,

/** Route for the Add Debit Card screen for a given navigation stack */
addDebitCardRoute: PropTypes.string.isRequired,

/** Route for the KYC enable payments screen for a given navigation stack */
enablePaymentsRoute: PropTypes.string.isRequired,

/** Where to place the popover */
popoverPlacement: PropTypes.string,

...userWalletPropTypes,
};

const defaultProps = {
// eslint-disable-next-line react/default-props-match-prop-types
userWallet: {},
popoverPlacement: 'top',
};
import themeColors from '../../styles/themes/default';
import CONST from '../../CONST';
import Navigation from '../../libs/Navigation/Navigation';
import AddPaymentMethodMenu from '../AddPaymentMethodMenu';
import getClickedElementLocation from '../../libs/getClickedElementLocation';
import * as PaymentUtils from '../../libs/PaymentUtils';
import * as PaymentMethods from '../../libs/actions/PaymentMethods';
import ONYXKEYS from '../../ONYXKEYS';
import Log from '../../libs/Log';
import {propTypes, defaultProps} from './kycWallPropTypes';

// 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
Expand All @@ -57,6 +34,9 @@ class KYCWall extends React.Component {
}

componentWillUnmount() {
if (this.props.listenResize) {
window.removeEventListener('resize', null);
}
PaymentMethods.kycWallRef.current = null;
}

Expand All @@ -78,6 +58,19 @@ class KYCWall extends React.Component {
};
}

/**
* Set position of the transfer payment menu
*
* @param {Object} position
*/

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove line break

setPositionAddPaymentMenu(position) {
this.setState({
anchorPositionTop: position.anchorPositionTop,
anchorPositionLeft: position.anchorPositionLeft,
});
}

/**
* 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.
Expand All @@ -89,13 +82,19 @@ 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 (!PaymentUtils.hasExpensifyPaymentMethod(this.props.cardList, this.props.bankAccountList)) {
Log.info('[KYC Wallet] User does not have valid payment method');
const clickedElementLocation = getClickedElementLocation(event.nativeEvent);
const {anchorPositionTop, anchorPositionLeft} = this.getAnchorPosition(clickedElementLocation);
let clickedElementLocation = getClickedElementLocation(event.nativeEvent);
let position = this.getAnchorPosition(clickedElementLocation);
if (this.props.listenResize) {
window.addEventListener('resize', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, hmm, this is gonna bind a new event listener each time. That can have unintended consequences. The best practice would be to set an event listener once on component init and then clean it up on unmount...

Here's an example of what that looks like using a class component

https://reactjs.org/docs/hooks-effect.html#example-using-classes-1

cc @rushatgabhane @deetergp

Copy link
Contributor Author

@phivh phivh Feb 16, 2022

Choose a reason for hiding this comment

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

hmm, we already added a remove listener when the modal close. The scenario is add listener when the modal appears and remove listener when the modal closes.

Copy link
Contributor

@marcaaron marcaaron Feb 16, 2022

Choose a reason for hiding this comment

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

Please fix this. Here are the instructions...

  1. add event listeners in componentDidMount()
  2. remove event listeners in componentWillUnmount()
  3. bind the callbacks they use i.e. don't use an arrow function

Generally speaking even if we are focusing on the web side for consistency we should use the Dimensions utility. React native web is able to use this.

https://reactnative.dev/docs/0.66/dimensions#addeventlistener

Copy link
Member

Choose a reason for hiding this comment

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

@phivh right now, we're adding a event listener each time the window is resized.
So yeah, let's create a follow-up PR to fix this. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, let me take into this one

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@marcaaron @deetergp @rushatgabhane created follow-up PR for this one. Please take a look when you guys have a chance.
#7847

clickedElementLocation = getClickedElementLocation(event.nativeEvent);
position = this.getAnchorPosition(clickedElementLocation);
this.setPositionAddPaymentMenu(position);
});
}
this.setState({
shouldShowAddPaymentMenu: true,
anchorPositionTop,
anchorPositionLeft,
});
this.setPositionAddPaymentMenu(position);
return;
}

Expand Down
14 changes: 14 additions & 0 deletions src/components/KYCWall/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import {propTypes, defaultProps} from './kycWallPropTypes';
import BaseKYCWall from './BaseKYCWall';

const KYCWall = props => (
// eslint-disable-next-line react/jsx-props-no-spreading
<BaseKYCWall {...props} listenResize />
);

KYCWall.propTypes = propTypes;
KYCWall.defaultProps = defaultProps;
KYCWall.displayName = 'KYCWall';

export default KYCWall;
Copy link
Member

Choose a reason for hiding this comment

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

New line above export please

3 changes: 3 additions & 0 deletions src/components/KYCWall/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import BaseKYCWall from './BaseKYCWall';

export default BaseKYCWall;
30 changes: 30 additions & 0 deletions src/components/KYCWall/kycWallPropTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import PropTypes from 'prop-types';
import userWalletPropTypes from '../../pages/EnablePayments/userWalletPropTypes';

const propTypes = {
/** Route for the Add Bank Account screen for a given navigation stack */
addBankAccountRoute: PropTypes.string.isRequired,

/** Route for the Add Debit Card screen for a given navigation stack */
addDebitCardRoute: PropTypes.string.isRequired,

/** Route for the KYC enable payments screen for a given navigation stack */
enablePaymentsRoute: PropTypes.string.isRequired,

/** Where to place the popover */
popoverPlacement: PropTypes.string,

/** Listen for window resize event on web and desktop */
listenResize: PropTypes.bool,

...userWalletPropTypes,
};

const defaultProps = {
// eslint-disable-next-line react/default-props-match-prop-types
userWallet: {},
popoverPlacement: 'top',
listenResize: false,
};

export {propTypes, defaultProps};
Original file line number Diff line number Diff line change
@@ -1,59 +1,35 @@
import React from 'react';
import {View, TouchableOpacity} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import PaymentMethodList from './PaymentMethodList';
import ROUTES from '../../../ROUTES';
import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton';
import PasswordPopover from '../../../components/PasswordPopover';
import ScreenWrapper from '../../../components/ScreenWrapper';
import Navigation from '../../../libs/Navigation/Navigation';
import styles from '../../../styles/styles';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import compose from '../../../libs/compose';
import KeyboardAvoidingView from '../../../components/KeyboardAvoidingView/index';
import * as BankAccounts from '../../../libs/actions/BankAccounts';
import Popover from '../../../components/Popover';
import MenuItem from '../../../components/MenuItem';
import Text from '../../../components/Text';
import * as PaymentMethods from '../../../libs/actions/PaymentMethods';
import getClickedElementLocation from '../../../libs/getClickedElementLocation';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import CurrentWalletBalance from '../../../components/CurrentWalletBalance';
import ONYXKEYS from '../../../ONYXKEYS';
import Permissions from '../../../libs/Permissions';
import ConfirmPopover from '../../../components/ConfirmPopover';
import AddPaymentMethodMenu from '../../../components/AddPaymentMethodMenu';
import CONST from '../../../CONST';
import * as Expensicons from '../../../components/Icon/Expensicons';
import walletTransferPropTypes from './walletTransferPropTypes';
import ConfirmModal from '../../../components/ConfirmModal';
import KYCWall from '../../../components/KYCWall';
import PaymentMethodList from '../PaymentMethodList';
import ROUTES from '../../../../ROUTES';
import HeaderWithCloseButton from '../../../../components/HeaderWithCloseButton';
import PasswordPopover from '../../../../components/PasswordPopover';
import ScreenWrapper from '../../../../components/ScreenWrapper';
import Navigation from '../../../../libs/Navigation/Navigation';
import styles from '../../../../styles/styles';
import withLocalize from '../../../../components/withLocalize';
import compose from '../../../../libs/compose';
import KeyboardAvoidingView from '../../../../components/KeyboardAvoidingView/index';
import * as BankAccounts from '../../../../libs/actions/BankAccounts';
import Popover from '../../../../components/Popover';
import MenuItem from '../../../../components/MenuItem';
import Text from '../../../../components/Text';
import * as PaymentMethods from '../../../../libs/actions/PaymentMethods';
import getClickedElementLocation from '../../../../libs/getClickedElementLocation';
import withWindowDimensions from '../../../../components/withWindowDimensions';
import CurrentWalletBalance from '../../../../components/CurrentWalletBalance';
import ONYXKEYS from '../../../../ONYXKEYS';
import Permissions from '../../../../libs/Permissions';
import ConfirmPopover from '../../../../components/ConfirmPopover';
import AddPaymentMethodMenu from '../../../../components/AddPaymentMethodMenu';
import CONST from '../../../../CONST';
import * as Expensicons from '../../../../components/Icon/Expensicons';
import ConfirmModal from '../../../../components/ConfirmModal';
import KYCWall from '../../../../components/KYCWall';
import {propTypes, defaultProps} from './paymentsPagePropTypes';

const propTypes = {
/** Wallet balance transfer props */
walletTransfer: walletTransferPropTypes,

/** List of betas available to current user */
betas: PropTypes.arrayOf(PropTypes.string),

/** Are we loading payment methods? */
isLoadingPaymentMethods: PropTypes.bool,

...withLocalizePropTypes,

...windowDimensionsPropTypes,
};

const defaultProps = {
walletTransfer: {
shouldShowConfirmModal: false,
},
betas: [],
isLoadingPaymentMethods: true,
};

class PaymentsPage extends React.Component {
class BasePaymentsPage extends React.Component {
constructor(props) {
super(props);

Expand Down Expand Up @@ -94,6 +70,20 @@ class PaymentsPage extends React.Component {
}
}

/**
* Set position of the payment menu
*
* @param {Object} position
*/
setPositionAddPaymentMenu(position) {
this.setState({
anchorPositionTop: position.bottom,

// We want the position to be 13px to the right of the left border
anchorPositionLeft: position.left + 13,
});
}

/**
* Display the delete/default menu, or the add payment method menu
*
Expand All @@ -102,7 +92,13 @@ class PaymentsPage extends React.Component {
* @param {String} account
*/
paymentMethodPressed(nativeEvent, accountType, account) {
const position = getClickedElementLocation(nativeEvent);
let position = getClickedElementLocation(nativeEvent);
if (this.props.listenResize) {
window.addEventListener('resize', () => {
position = getClickedElementLocation(nativeEvent);
this.setPositionAddPaymentMenu(position);
});
}
if (accountType) {
let formattedSelectedPaymentMethod;
if (accountType === CONST.PAYMENT_METHODS.PAYPAL) {
Expand Down Expand Up @@ -131,21 +127,15 @@ class PaymentsPage extends React.Component {
shouldShowDefaultDeleteMenu: true,
selectedPaymentMethod: account,
selectedPaymentMethodType: accountType,
anchorPositionTop: position.bottom,

// We want the position to be 13px to the right of the left border
anchorPositionLeft: position.left + 13,
formattedSelectedPaymentMethod,
});
} else {
this.setState({
shouldShowAddPaymentMenu: true,
anchorPositionTop: position.bottom,

// We want the position to be 13px to the right of the left border
anchorPositionLeft: position.left + 13,
});
this.setPositionAddPaymentMenu(position);
return;
}
this.setState({
shouldShowAddPaymentMenu: true,
});
this.setPositionAddPaymentMenu(position);
}

/**
Expand Down Expand Up @@ -178,13 +168,19 @@ class PaymentsPage extends React.Component {
* Hide the add payment modal
*/
hideAddPaymentMenu() {
if (this.props.listenResize) {
window.removeEventListener('resize', null);
Copy link
Member

Choose a reason for hiding this comment

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

Remove eventListener in hideDefaultDeleteMenu() too.

}
this.setState({shouldShowAddPaymentMenu: false});
}

/**
* Hide the default / delete modal
*/
hideDefaultDeleteMenu() {
if (this.props.listenResize) {
window.removeEventListener('resize', null);
}
this.setState({shouldShowDefaultDeleteMenu: false});
}

Expand Down Expand Up @@ -390,8 +386,8 @@ class PaymentsPage extends React.Component {
}
}

PaymentsPage.propTypes = propTypes;
PaymentsPage.defaultProps = defaultProps;
BasePaymentsPage.propTypes = propTypes;
BasePaymentsPage.defaultProps = defaultProps;

export default compose(
withWindowDimensions,
Expand All @@ -408,4 +404,4 @@ export default compose(
initWithStoredValues: false,
},
}),
)(PaymentsPage);
)(BasePaymentsPage);
10 changes: 10 additions & 0 deletions src/pages/settings/Payments/PaymentsPage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import BasePaymentsPage from './BasePaymentsPage';

const PaymentsPage = () => (
<BasePaymentsPage listenResize />
Copy link
Contributor

Choose a reason for hiding this comment

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

Not going to leave a full review here, but please see this draft PR which explains proper usage of boolean props to enable or disable features...

#7611

@rushatgabhane please 👀 and thank you 🙇

Copy link
Contributor

Choose a reason for hiding this comment

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

And applied this would be changed to something like shouldListenForResize.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed. Let's change it.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, I agree!

);

PaymentsPage.displayName = 'PaymentsPage';

export default PaymentsPage;
3 changes: 3 additions & 0 deletions src/pages/settings/Payments/PaymentsPage/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import BasePaymentsPage from './BasePaymentsPage';

export default BasePaymentsPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import PropTypes from 'prop-types';
import walletTransferPropTypes from '../walletTransferPropTypes';
import {withLocalizePropTypes} from '../../../../components/withLocalize';
import {windowDimensionsPropTypes} from '../../../../components/withWindowDimensions';

const propTypes = {
/** Wallet balance transfer props */
walletTransfer: walletTransferPropTypes,

/** List of betas available to current user */
betas: PropTypes.arrayOf(PropTypes.string),

/** Are we loading payment methods? */
isLoadingPaymentMethods: PropTypes.bool,

/** Listen for window resize event on web and desktop. */
listenResize: PropTypes.bool,

...withLocalizePropTypes,

...windowDimensionsPropTypes,
};

const defaultProps = {
walletTransfer: {
shouldShowConfirmModal: false,
},
betas: [],
isLoadingPaymentMethods: true,
listenResize: false,
};

export {propTypes, defaultProps};