diff --git a/android/app/build.gradle b/android/app/build.gradle
index 528fd8f176de..792a1c62c2f6 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -106,8 +106,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001031511
- versionName "1.3.15-11"
+ versionCode 1001031512
+ versionName "1.3.15-12"
}
splits {
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index fc315b4e5814..8544fbaec3d0 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -30,7 +30,7 @@
CFBundleVersion
- 1.3.15.11
+ 1.3.15.12
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index bf63eb349f2e..b7c24aa58815 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.3.15.11
+ 1.3.15.12
diff --git a/package-lock.json b/package-lock.json
index d40a7929d66d..2ef23d288b2e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.15-11",
+ "version": "1.3.15-12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.15-11",
+ "version": "1.3.15-12",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index ff1e20e20816..8e53f69f0993 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.15-11",
+ "version": "1.3.15-12",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/CONST.js b/src/CONST.js
index 47fb10ec576a..988ded81a73d 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -559,6 +559,7 @@ const CONST = {
},
STATE: {
SUBMITTED: 'SUBMITTED',
+ PROCESSING: 'PROCESSING',
},
STATE_NUM: {
OPEN: 0,
@@ -960,6 +961,7 @@ const CONST = {
ELSEWHERE: 'Elsewhere',
EXPENSIFY: 'Expensify',
PAYPAL_ME: 'PayPal.me',
+ VBBA: 'ACH',
},
MONEY_REQUEST_TYPE: {
SEND: 'send',
diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index a5e3c97740da..b901e1f2ea8a 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -91,6 +91,9 @@ export default {
// A unique identifier that each user has that's used to send notifications
NVP_PRIVATE_PUSH_NOTIFICATION_ID: 'private_pushNotificationID',
+ // The NVP with the last payment method used per policy
+ NVP_LAST_PAYMENT_METHOD: 'nvp_lastPaymentMethod',
+
// Does this user have push notifications enabled for this device?
PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled',
diff --git a/src/ROUTES.js b/src/ROUTES.js
index ec4730289e06..c67bef713fa1 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -19,6 +19,7 @@ const SETTINGS_CONTACT_METHODS = 'settings/profile/contact-methods';
export default {
BANK_ACCOUNT: 'bank-account',
+ BANK_ACCOUNT_NEW: 'bank-account/new',
BANK_ACCOUNT_WITH_STEP_TO_OPEN: 'bank-account/:stepToOpen?',
BANK_ACCOUNT_PERSONAL: 'bank-account/personal',
getBankAccountRoute: (stepToOpen = '', policyID = '') => `bank-account/${stepToOpen}?policyID=${policyID}`,
diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js
index 61927aefb868..2581bde85b4e 100644
--- a/src/components/AddPaymentMethodMenu.js
+++ b/src/components/AddPaymentMethodMenu.js
@@ -12,11 +12,16 @@ import PopoverMenu from './PopoverMenu';
import paypalMeDataPropTypes from './paypalMeDataPropTypes';
const propTypes = {
+ /** Should the component be visible? */
isVisible: PropTypes.bool.isRequired,
+
+ /** Callback to execute when the component closes. */
onClose: PropTypes.func.isRequired,
+
+ /** Anchor position for the AddPaymentMenu. */
anchorPosition: PropTypes.shape({
- top: PropTypes.number,
- left: PropTypes.number,
+ horizontal: PropTypes.number,
+ vertical: PropTypes.number,
}),
/** Account details for PayPal.Me */
@@ -43,7 +48,7 @@ const AddPaymentMethodMenu = (props) => (
isVisible={props.isVisible}
onClose={props.onClose}
anchorPosition={props.anchorPosition}
- onItemSelected={() => props.onClose()}
+ onItemSelected={props.onClose}
menuItems={[
{
text: props.translate('common.bankAccount'),
diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js
index f65696ec3541..29424233891d 100644
--- a/src/components/AvatarWithImagePicker.js
+++ b/src/components/AvatarWithImagePicker.js
@@ -294,6 +294,7 @@ class AvatarWithImagePicker extends React.Component {
onItemSelected={() => this.setState({isMenuVisible: false})}
menuItems={this.createMenuItems(openPicker)}
anchorPosition={this.props.anchorPosition}
+ anchorAlignment={this.props.anchorAlignment}
/>
>
)}
diff --git a/src/components/Button/index.js b/src/components/Button/index.js
index 65107cfe9392..f9200d085b28 100644
--- a/src/components/Button/index.js
+++ b/src/components/Button/index.js
@@ -109,6 +109,9 @@ const propTypes = {
/** Accessibility label for the component */
accessibilityLabel: PropTypes.string,
+
+ /** A ref to forward the button */
+ forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.oneOfType([PropTypes.instanceOf(React.Component), PropTypes.func])})]),
};
const defaultProps = {
@@ -141,6 +144,7 @@ const defaultProps = {
shouldEnableHapticFeedback: false,
nativeID: '',
accessibilityLabel: '',
+ forwardedRef: undefined,
};
class Button extends Component {
@@ -240,6 +244,7 @@ class Button extends Component {
render() {
return (
{
if (e && e.type === 'click') {
e.currentTarget.blur();
@@ -303,4 +308,15 @@ class Button extends Component {
Button.propTypes = propTypes;
Button.defaultProps = defaultProps;
-export default compose(withNavigationFallback, withNavigationFocus)(Button);
+export default compose(
+ withNavigationFallback,
+ withNavigationFocus,
+)(
+ React.forwardRef((props, ref) => (
+
+ )),
+);
diff --git a/src/components/ButtonWithDropdown.js b/src/components/ButtonWithDropdown.js
deleted file mode 100644
index 61b578488e36..000000000000
--- a/src/components/ButtonWithDropdown.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {View} from 'react-native';
-import styles from '../styles/styles';
-import Button from './Button';
-import Icon from './Icon';
-import * as Expensicons from './Icon/Expensicons';
-import themeColors from '../styles/themes/default';
-
-const propTypes = {
- /** Text to display for the main button */
- buttonText: PropTypes.string.isRequired,
-
- /** Callback to execute when the main button is pressed */
- onButtonPress: PropTypes.func,
-
- /** Callback to execute when the dropdown element is pressed */
- onDropdownPress: PropTypes.func,
-
- /** Whether we should show a loading state for the main button */
- isLoading: PropTypes.bool,
-
- /** Should the button be disabled */
- isDisabled: PropTypes.bool,
-};
-
-const defaultProps = {
- onButtonPress: () => {},
- onDropdownPress: () => {},
- isDisabled: false,
- isLoading: false,
-};
-
-const ButtonWithDropdown = (props) => (
-
-
-
-
-
-);
-
-ButtonWithDropdown.propTypes = propTypes;
-ButtonWithDropdown.defaultProps = defaultProps;
-ButtonWithDropdown.displayName = 'ButtonWithDropdown';
-export default ButtonWithDropdown;
diff --git a/src/components/ButtonWithDropdownMenu.js b/src/components/ButtonWithDropdownMenu.js
new file mode 100644
index 000000000000..a518ce647c75
--- /dev/null
+++ b/src/components/ButtonWithDropdownMenu.js
@@ -0,0 +1,139 @@
+import React, {useState, useRef, useEffect} from 'react';
+import PropTypes from 'prop-types';
+import {View} from 'react-native';
+import _ from 'underscore';
+import useWindowDimensions from '../hooks/useWindowDimensions';
+import styles from '../styles/styles';
+import Button from './Button';
+import PopoverMenu from './PopoverMenu';
+import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
+import themeColors from '../styles/themes/default';
+import CONST from '../CONST';
+
+const propTypes = {
+ /** Text to display for the menu header */
+ menuHeaderText: PropTypes.string,
+
+ /** Callback to execute when the main button is pressed */
+ onPress: PropTypes.func.isRequired,
+
+ /** Whether we should show a loading state for the main button */
+ isLoading: PropTypes.bool,
+
+ /** Should the confirmation button be disabled? */
+ isDisabled: PropTypes.bool,
+
+ /** Additional styles to add to the component */
+ style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
+
+ /** Menu options to display */
+ /** e.g. [{text: 'Pay with Expensify', icon: Wallet}, {text: 'PayPal', icon: PayPal}] */
+ options: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.string.isRequired,
+ text: PropTypes.string.isRequired,
+ icon: PropTypes.elementType,
+ iconWidth: PropTypes.number,
+ iconHeight: PropTypes.number,
+ iconDescription: PropTypes.string,
+ }),
+ ).isRequired,
+};
+
+const defaultProps = {
+ isLoading: false,
+ isDisabled: false,
+ menuHeaderText: '',
+ style: [],
+};
+
+const ButtonWithDropdownMenu = (props) => {
+ const [selectedItemIndex, setSelectedItemIndex] = useState(0);
+ const [isMenuVisible, setIsMenuVisible] = useState(false);
+ const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null);
+ const {width: windowWidth, height: windowHeight} = useWindowDimensions();
+ const caretButton = useRef(null);
+ useEffect(() => {
+ if (!caretButton.current) {
+ return;
+ }
+ caretButton.current.measureInWindow((x, y, w, h) => {
+ setPopoverAnchorPosition({
+ horizontal: x + w,
+ vertical: y + h,
+ });
+ });
+ }, [windowWidth, windowHeight]);
+
+ const selectedItem = props.options[selectedItemIndex];
+ return (
+
+ {props.options.length > 1 ? (
+
+
+ ) : (
+ props.onPress(event, props.options[0].value)}
+ pressOnEnter
+ />
+ )}
+ {props.options.length > 1 && !_.isEmpty(popoverAnchorPosition) && (
+ setIsMenuVisible(false)}
+ onItemSelected={() => setIsMenuVisible(false)}
+ anchorPosition={popoverAnchorPosition}
+ anchorAlignment={{
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
+ }}
+ headerText={props.menuHeaderText}
+ menuItems={_.map(props.options, (item, index) => ({
+ ...item,
+ onSelected: () => {
+ setSelectedItemIndex(index);
+ },
+ }))}
+ />
+ )}
+
+ );
+};
+
+ButtonWithDropdownMenu.propTypes = propTypes;
+ButtonWithDropdownMenu.defaultProps = defaultProps;
+ButtonWithDropdownMenu.displayName = 'ButtonWithDropdownMenu';
+
+export default React.memo(ButtonWithDropdownMenu);
diff --git a/src/components/ButtonWithMenu.js b/src/components/ButtonWithMenu.js
deleted file mode 100644
index a1576ad5f551..000000000000
--- a/src/components/ButtonWithMenu.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import React, {PureComponent} from 'react';
-import PropTypes from 'prop-types';
-import {View} from 'react-native';
-import _ from 'underscore';
-import styles from '../styles/styles';
-import Button from './Button';
-import ButtonWithDropdown from './ButtonWithDropdown';
-import PopoverMenu from './PopoverMenu';
-
-const propTypes = {
- /** Text to display for the menu header */
- menuHeaderText: PropTypes.string,
-
- /** Callback to execute when the main button is pressed */
- onPress: PropTypes.func.isRequired,
-
- /** Whether we should show a loading state for the main button */
- isLoading: PropTypes.bool,
-
- /** Should the confirmation button be disabled? */
- isDisabled: PropTypes.bool,
-
- /** Menu options to display */
- /** e.g. [{text: 'Pay with Expensify', icon: Wallet}, {text: 'PayPal', icon: PayPal}] */
- options: PropTypes.arrayOf(
- PropTypes.shape({
- value: PropTypes.string.isRequired,
- text: PropTypes.string.isRequired,
- icon: PropTypes.elementType,
- iconWidth: PropTypes.number,
- iconHeight: PropTypes.number,
- iconDescription: PropTypes.string,
- }),
- ).isRequired,
-};
-
-const defaultProps = {
- isLoading: false,
- isDisabled: false,
- menuHeaderText: '',
-};
-
-class ButtonWithMenu extends PureComponent {
- constructor(props) {
- super(props);
-
- this.state = {
- selectedItemIndex: 0,
- isMenuVisible: false,
- };
- }
-
- setMenuVisibility(isMenuVisible) {
- this.setState({isMenuVisible});
- }
-
- render() {
- const selectedItem = this.props.options[this.state.selectedItemIndex];
- return (
-
- {this.props.options.length > 1 ? (
- this.props.onPress(event, selectedItem.value)}
- onDropdownPress={() => {
- this.setMenuVisibility(true);
- }}
- />
- ) : (
- this.props.onPress(event, this.props.options[0].value)}
- pressOnEnter
- />
- )}
- {this.props.options.length > 1 && (
- this.setMenuVisibility(false)}
- onItemSelected={() => this.setMenuVisibility(false)}
- anchorPosition={styles.createMenuPositionRightSidepane}
- headerText={this.props.menuHeaderText}
- menuItems={_.map(this.props.options, (item, index) => ({
- ...item,
- onSelected: () => {
- this.setState({selectedItemIndex: index});
- },
- }))}
- />
- )}
-
- );
- }
-}
-
-ButtonWithMenu.propTypes = propTypes;
-ButtonWithMenu.defaultProps = defaultProps;
-
-export default ButtonWithMenu;
diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js
index ae9578f68a4b..1e15e9e20bf3 100644
--- a/src/components/EmojiPicker/EmojiPicker.js
+++ b/src/components/EmojiPicker/EmojiPicker.js
@@ -20,7 +20,6 @@ class EmojiPicker extends React.Component {
this.measureEmojiPopoverAnchorPosition = this.measureEmojiPopoverAnchorPosition.bind(this);
this.measureEmojiPopoverAnchorPositionAndUpdateState = this.measureEmojiPopoverAnchorPositionAndUpdateState.bind(this);
this.focusEmojiSearchInput = this.focusEmojiSearchInput.bind(this);
- this.measureContent = this.measureContent.bind(this);
this.onModalHide = () => {};
this.onEmojiSelected = () => {};
@@ -131,20 +130,6 @@ class EmojiPicker extends React.Component {
});
}
- /**
- * Used to calculate the EmojiPicker Dimensions
- *
- * @returns {JSX}
- */
- measureContent() {
- return (
- (this.emojiSearchInput = el)}
- />
- );
- }
-
/**
* Focus the search input in the emoji picker.
*/
@@ -176,8 +161,7 @@ class EmojiPicker extends React.Component {
width: CONST.EMOJI_PICKER_SIZE.WIDTH,
height: CONST.EMOJI_PICKER_SIZE.HEIGHT,
}}
- anchorOrigin={this.state.emojiPopoverAnchorOrigin}
- measureContent={this.measureContent}
+ anchorAlignment={this.state.emojiPopoverAnchorOrigin}
>
this.setState({shouldShowAddPaymentMenu: false})}
anchorPosition={{
- top: this.state.anchorPositionTop,
- left: this.state.anchorPositionLeft,
+ vertical: this.state.anchorPositionVertical,
+ horizontal: this.state.anchorPositionHorizontal,
}}
shouldShowPaypal={false}
onItemSelected={(item) => {
@@ -159,4 +166,10 @@ export default withOnyx({
bankAccountList: {
key: ONYXKEYS.BANK_ACCOUNT_LIST,
},
+ reimbursementAccount: {
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ },
+ chatReport: {
+ key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`,
+ },
})(KYCWall);
diff --git a/src/components/KYCWall/kycWallPropTypes.js b/src/components/KYCWall/kycWallPropTypes.js
index 4558d56ceb25..4f23d10174a2 100644
--- a/src/components/KYCWall/kycWallPropTypes.js
+++ b/src/components/KYCWall/kycWallPropTypes.js
@@ -2,13 +2,15 @@ import PropTypes from 'prop-types';
import userWalletPropTypes from '../../pages/EnablePayments/userWalletPropTypes';
import bankAccountPropTypes from '../bankAccountPropTypes';
import cardPropTypes from '../cardPropTypes';
+import iouReportPropTypes from '../../pages/iouReportPropTypes';
+import reimbursementAccountPropTypes from '../../pages/ReimbursementAccount/ReimbursementAccountDraftPropTypes';
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,
+ addDebitCardRoute: PropTypes.string,
/** Route for the KYC enable payments screen for a given navigation stack */
enablePaymentsRoute: PropTypes.string.isRequired,
@@ -33,6 +35,15 @@ const propTypes = {
/** List of bank accounts */
bankAccountList: PropTypes.objectOf(bankAccountPropTypes),
+
+ /** The chat report this report is linked to */
+ chatReport: iouReportPropTypes,
+
+ /** The IOU/Expense report we are paying */
+ iouReport: iouReportPropTypes,
+
+ /** The reimbursement account linked to the Workspace */
+ reimbursementAccount: reimbursementAccountPropTypes,
};
const defaultProps = {
@@ -43,6 +54,10 @@ const defaultProps = {
chatReportID: '',
bankAccountList: {},
cardList: {},
+ chatReport: null,
+ reimbursementAccount: {},
+ addDebitCardRoute: '',
+ iouReport: {},
};
export {propTypes, defaultProps};
diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js
index 51325f619f1a..710743bb0edb 100644
--- a/src/components/Modal/BaseModal.js
+++ b/src/components/Modal/BaseModal.js
@@ -138,6 +138,7 @@ class BaseModal extends PureComponent {
animationInTiming={this.props.animationInTiming}
animationOutTiming={this.props.animationOutTiming}
statusBarTranslucent={this.props.statusBarTranslucent}
+ onLayout={this.props.onLayout}
>
{(insets) => {
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index 916ed12219b3..8b6a43f47215 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -10,7 +10,7 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize';
import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
import compose from '../libs/compose';
import CONST from '../CONST';
-import ButtonWithMenu from './ButtonWithMenu';
+import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
import Log from '../libs/Log';
import SettlementButton from './SettlementButton';
import ROUTES from '../ROUTES';
@@ -67,6 +67,9 @@ const propTypes = {
/** Callback function to navigate to a provided step in the MoneyRequestModal flow */
navigateToStep: PropTypes.func.isRequired,
+
+ /** The policyID of the request */
+ policyID: PropTypes.string.isRequired,
};
const defaultProps = {
@@ -288,9 +291,10 @@ class MoneyRequestConfirmationList extends Component {
addBankAccountRoute={ROUTES.IOU_SEND_ADD_BANK_ACCOUNT}
addDebitCardRoute={ROUTES.IOU_SEND_ADD_DEBIT_CARD}
currency={this.props.iou.selectedCurrencyCode}
+ policyID={this.props.policyID}
/>
) : (
- this.confirm(value)}
options={this.getSplitOrRequestOptions()}
diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js
index 66d1898307e5..0a14315fbbd8 100644
--- a/src/components/MoneyRequestHeader.js
+++ b/src/components/MoneyRequestHeader.js
@@ -1,8 +1,8 @@
import React from 'react';
+import {withOnyx} from 'react-native-onyx';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
-import {withOnyx} from 'react-native-onyx';
import HeaderWithCloseButton from './HeaderWithCloseButton';
import iouReportPropTypes from '../pages/iouReportPropTypes';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
@@ -19,10 +19,14 @@ import compose from '../libs/compose';
import Navigation from '../libs/Navigation/Navigation';
import ROUTES from '../ROUTES';
import Icon from './Icon';
+import SettlementButton from './SettlementButton';
+import * as Policy from '../libs/actions/Policy';
+import ONYXKEYS from '../ONYXKEYS';
+import * as IOU from '../libs/actions/IOU';
import * as CurrencyUtils from '../libs/CurrencyUtils';
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
import DateUtils from '../libs/DateUtils';
-import ONYXKEYS from '../ONYXKEYS';
+import reportPropTypes from '../pages/reportPropTypes';
const propTypes = {
/** The report currently being looked at */
@@ -37,17 +41,30 @@ const propTypes = {
name: PropTypes.string,
}).isRequired,
+ /** The chat report this report is linked to */
+ chatReport: reportPropTypes,
+
/** Personal details so we can get the ones for the report participants */
personalDetails: PropTypes.objectOf(participantPropTypes).isRequired,
/** Whether we're viewing a report with a single transaction in it */
isSingleTransactionView: PropTypes.bool,
+ /** Session info for the currently logged in user. */
+ session: PropTypes.shape({
+ /** Currently logged in user email */
+ email: PropTypes.string,
+ }),
+
...withLocalizePropTypes,
};
const defaultProps = {
isSingleTransactionView: false,
+ chatReport: {},
+ session: {
+ email: null,
+ },
parentReport: {},
};
@@ -59,7 +76,6 @@ const MoneyRequestHeader = (props) => {
const formattedTransactionDate = DateUtils.getDateStringFromISOTimestamp(transactionDate);
const formattedAmount = CurrencyUtils.convertToDisplayString(ReportUtils.getMoneyRequestTotal(props.report), props.report.currency);
-
const moneyRequestReport = props.isSingleTransactionView ? props.parentReport : props.report;
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
const isExpenseReport = ReportUtils.isExpenseReport(moneyRequestReport);
@@ -67,6 +83,9 @@ const MoneyRequestHeader = (props) => {
const payeeAvatar = isExpenseReport
? ReportUtils.getWorkspaceAvatar(moneyRequestReport)
: ReportUtils.getAvatar(lodashGet(props.personalDetails, [moneyRequestReport.managerEmail, 'avatar']), moneyRequestReport.managerEmail);
+ const policy = props.policies[`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`];
+ const shouldShowSettlementButton =
+ !isSettled && (Policy.isAdminOfFreePolicy([policy]) || (ReportUtils.isMoneyRequestReport(props.report) && lodashGet(props.session, 'email', null) === props.report.managerEmail));
return (
{
onSelected: () => {},
},
]}
- threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton}
+ threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(props.windowWidth)}
report={moneyRequestReport}
policies={props.policies}
personalDetails={props.personalDetails}
@@ -114,7 +133,7 @@ const MoneyRequestHeader = (props) => {
)}
-
+
{!props.isSingleTransactionView && {formattedAmount}}
{isSettled && (
@@ -124,8 +143,36 @@ const MoneyRequestHeader = (props) => {
/>
)}
+ {shouldShowSettlementButton && !props.isSmallScreenWidth && (
+
+ IOU.payMoneyRequest(paymentType, props.chatReport, props.report)}
+ enablePaymentsRoute={ROUTES.BANK_ACCOUNT_NEW}
+ addBankAccountRoute={ROUTES.IOU_DETAILS_ADD_BANK_ACCOUNT}
+ shouldShowPaymentOptions
+ />
+
+ )}
+ {shouldShowSettlementButton && props.isSmallScreenWidth && (
+ IOU.payMoneyRequest(paymentType, props.chatReport, props.report)}
+ enablePaymentsRoute={ROUTES.BANK_ACCOUNT_NEW}
+ addBankAccountRoute={ROUTES.IOU_DETAILS_ADD_BANK_ACCOUNT}
+ shouldShowPaymentOptions
+ />
+ )}
{props.isSingleTransactionView && (
<>
@@ -156,6 +203,12 @@ export default compose(
withWindowDimensions,
withLocalize,
withOnyx({
+ chatReport: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
parentReport: {
key: (props) => `${ONYXKEYS.COLLECTION.REPORT}${props.report.parentReportID}`,
},
diff --git a/src/components/Popover/index.js b/src/components/Popover/index.js
index 031d9703d2c4..2c3adf263cc5 100644
--- a/src/components/Popover/index.js
+++ b/src/components/Popover/index.js
@@ -20,6 +20,7 @@ const Popover = (props) => {
animationInTiming={props.disableAnimation ? 1 : props.animationInTiming}
animationOutTiming={props.disableAnimation ? 1 : props.animationOutTiming}
shouldCloseOnOutsideClick
+ onLayout={props.onLayout}
/>,
document.body,
);
@@ -33,6 +34,7 @@ const Popover = (props) => {
fullscreen={props.isSmallScreenWidth ? true : props.fullscreen}
animationInTiming={props.disableAnimation && !props.isSmallScreenWidth ? 1 : props.animationInTiming}
animationOutTiming={props.disableAnimation && !props.isSmallScreenWidth ? 1 : props.animationOutTiming}
+ onLayout={props.onLayout}
/>
);
};
diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js
index 76dfc7e30e80..5b5eadffcdce 100644
--- a/src/components/PopoverMenu/index.js
+++ b/src/components/PopoverMenu/index.js
@@ -1,135 +1,99 @@
import _ from 'underscore';
-import React, {PureComponent} from 'react';
+import React from 'react';
+import PropTypes from 'prop-types';
import {View} from 'react-native';
-import Popover from '../Popover';
+import PopoverWithMeasuredContent from '../PopoverWithMeasuredContent';
import styles from '../../styles/styles';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
import MenuItem from '../MenuItem';
-import {propTypes as createMenuPropTypes, defaultProps} from './popoverMenuPropTypes';
-import ArrowKeyFocusManager from '../ArrowKeyFocusManager';
+import {propTypes as createMenuPropTypes, defaultProps as createMenuDefaultProps} from './popoverMenuPropTypes';
import Text from '../Text';
-import KeyboardShortcut from '../../libs/KeyboardShortcut';
import CONST from '../../CONST';
+import useArrowKeyFocusManager from '../../hooks/useArrowKeyFocusManager';
+import useKeyboardShortcut from '../../hooks/useKeyboardShortcut';
+import useWindowDimensions from '../../hooks/useWindowDimensions';
const propTypes = {
...createMenuPropTypes,
...windowDimensionsPropTypes,
-};
-
-class PopoverMenu extends PureComponent {
- constructor(props) {
- super(props);
- this.state = {
- focusedIndex: -1,
- };
- this.resetFocusAndHideModal = this.resetFocusAndHideModal.bind(this);
- this.removeKeyboardListener = this.removeKeyboardListener.bind(this);
- this.attachKeyboardListener = this.attachKeyboardListener.bind(this);
- this.selectedItem = null;
- }
-
- componentDidUpdate(prevProps) {
- if (this.props.isVisible === prevProps.isVisible) {
- return;
- }
- if (this.props.isVisible) {
- this.attachKeyboardListener();
- } else {
- this.removeKeyboardListener();
- }
- }
+ /** The horizontal and vertical anchors points for the popover */
+ anchorPosition: PropTypes.shape({
+ horizontal: PropTypes.number.isRequired,
+ vertical: PropTypes.number.isRequired,
+ }).isRequired,
- componentWillUnmount() {
- this.removeKeyboardListener();
- }
+ /** Where the popover should be positioned relative to the anchor points. */
+ anchorAlignment: PropTypes.shape({
+ horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
+ vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
+ }),
+};
- /**
- * Set item to local variable to fire `onSelected` action after the menu popup closes
- * @param {Object} item
- */
- selectItem(item) {
- this.selectedItem = item;
- this.props.onItemSelected(item);
- }
+const defaultProps = {
+ ...createMenuDefaultProps,
+ anchorAlignment: {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
+ },
+};
- attachKeyboardListener() {
- const shortcutConfig = CONST.KEYBOARD_SHORTCUTS.ENTER;
- this.unsubscribeEnterKey = KeyboardShortcut.subscribe(
- shortcutConfig.shortcutKey,
- () => {
- if (this.state.focusedIndex === -1) {
- return;
- }
- this.selectItem(this.props.menuItems[this.state.focusedIndex]);
- this.setState({focusedIndex: -1}); // Reset the focusedIndex on selecting any menu
- },
- shortcutConfig.descriptionKey,
- shortcutConfig.modifiers,
- true,
- );
- }
+const PopoverMenu = (props) => {
+ const {isSmallScreenWidth} = useWindowDimensions();
- removeKeyboardListener() {
- if (!this.unsubscribeEnterKey) {
- return;
- }
- this.unsubscribeEnterKey();
- }
+ const selectItem = (index) => {
+ const selectedItem = props.menuItems[index];
+ props.onItemSelected(selectedItem);
+ selectedItem.onSelected();
+ };
- resetFocusAndHideModal() {
- this.setState({focusedIndex: -1}); // Reset the focusedIndex on modal hide
- this.removeKeyboardListener();
- if (this.selectedItem) {
- this.selectedItem.onSelected();
- this.selectedItem = null;
- }
- }
+ const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: props.menuItems.length - 1});
+ useKeyboardShortcut(
+ CONST.KEYBOARD_SHORTCUTS.ENTER,
+ () => {
+ if (focusedIndex === -1) {
+ return;
+ }
+ selectItem(focusedIndex);
+ setFocusedIndex(-1); // Reset the focusedIndex on selecting any menu
+ },
+ {isActive: props.isVisible},
+ );
- render() {
- return (
-
-
- {!_.isEmpty(this.props.headerText) && (
-
- {this.props.headerText}
-
- )}
- this.setState({focusedIndex: index})}
- >
- {_.map(this.props.menuItems, (item, menuIndex) => (
-
-
-
- );
- }
-}
+ return (
+ setFocusedIndex(-1)}
+ animationIn={props.animationIn}
+ animationOut={props.animationOut}
+ animationInTiming={props.animationInTiming}
+ disableAnimation={props.disableAnimation}
+ fromSidebarMediumScreen={props.fromSidebarMediumScreen}
+ >
+
+ {!_.isEmpty(props.headerText) && {props.headerText}}
+ {_.map(props.menuItems, (item, menuIndex) => (
+
+
+ );
+};
PopoverMenu.propTypes = propTypes;
PopoverMenu.defaultProps = defaultProps;
+PopoverMenu.displayName = 'PopoverMenu';
-export default withWindowDimensions(PopoverMenu);
+export default React.memo(withWindowDimensions(PopoverMenu));
diff --git a/src/components/PopoverWithMeasuredContent.js b/src/components/PopoverWithMeasuredContent.js
index 7452738c355c..438e6cce010e 100644
--- a/src/components/PopoverWithMeasuredContent.js
+++ b/src/components/PopoverWithMeasuredContent.js
@@ -21,16 +21,13 @@ const propTypes = {
vertical: PropTypes.number.isRequired,
}).isRequired,
- /** Where the popover should be positioned relative to the anchor points. */
- anchorOrigin: PropTypes.shape({
+ /** How the popover should be aligned. The value you passed will is the part of the component that will be aligned to the
+ * anchorPosition. ie: vertical:top means the top of the menu will be positioned in the anchorPosition */
+ anchorAlignment: PropTypes.shape({
horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
}),
- /** A function with content to measure. This component will use this.props.children by default,
- but in the case the children are not displayed, the measurement will not work. */
- measureContent: PropTypes.func.isRequired,
-
/** Static dimensions for the popover.
* Note: When passed, it will skip dimensions measuring of the popover, and provided dimensions will be used to calculate the anchor position.
*/
@@ -46,7 +43,7 @@ const defaultProps = {
...defaultPopoverProps,
// Default positioning of the popover
- anchorOrigin: {
+ anchorAlignment: {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
},
@@ -125,7 +122,7 @@ class PopoverWithMeasuredContent extends Component {
*/
calculateAdjustedAnchorPosition() {
let horizontalConstraint;
- switch (this.props.anchorOrigin.horizontal) {
+ switch (this.props.anchorAlignment.horizontal) {
case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT:
horizontalConstraint = {left: this.props.anchorPosition.horizontal - this.popoverWidth};
break;
@@ -140,7 +137,7 @@ class PopoverWithMeasuredContent extends Component {
}
let verticalConstraint;
- switch (this.props.anchorOrigin.vertical) {
+ switch (this.props.anchorAlignment.vertical) {
case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM:
verticalConstraint = {top: this.props.anchorPosition.vertical - this.popoverHeight};
break;
@@ -164,7 +161,7 @@ class PopoverWithMeasuredContent extends Component {
const adjustedAnchorPosition = this.calculateAdjustedAnchorPosition();
const horizontalShift = computeHorizontalShift(adjustedAnchorPosition.left, this.popoverWidth, this.props.windowWidth);
const verticalShift = computeVerticalShift(adjustedAnchorPosition.top, this.popoverHeight, this.props.windowHeight);
- const shifedAnchorPosition = {
+ const shiftedAnchorPosition = {
left: adjustedAnchorPosition.left + horizontalShift,
top: adjustedAnchorPosition.top + verticalShift,
};
@@ -172,17 +169,17 @@ class PopoverWithMeasuredContent extends Component {
- {this.props.measureContent()}
+ {this.props.children}
) : (
/*
- This is an invisible view used to measure the size of the popover,
- before it ever needs to be displayed.
- We do this because we need to know its dimensions in order to correctly animate the popover,
- but we can't measure its dimensions without first rendering it.
- */
+ This is an invisible view used to measure the size of the popover,
+ before it ever needs to be displayed.
+ We do this because we need to know its dimensions in order to correctly animate the popover,
+ but we can't measure its dimensions without first rendering it.
+ */
{
/>
))}
- {isCurrentUserManager && props.iouReport.stateNum === CONST.REPORT.STATE_NUM.PROCESSING && (
- IOU.payMoneyRequest(paymentType, props.chatReport, props.iouReport)}
+ enablePaymentsRoute={ROUTES.BANK_ACCOUNT_NEW}
+ addBankAccountRoute={ROUTES.IOU_DETAILS_ADD_BANK_ACCOUNT}
style={[styles.requestPreviewBox]}
- onPress={() => {
- Navigation.navigate(ROUTES.getIouDetailsRoute(props.chatReportID, props.iouReportID));
- }}
- onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
- onPressOut={() => ControlSelection.unblock()}
- text={props.translate('iou.pay')}
- success
- medium
/>
)}
diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js
index 2308fe99751c..1c9c5ed3bcd7 100644
--- a/src/components/SettlementButton.js
+++ b/src/components/SettlementButton.js
@@ -2,7 +2,7 @@ import _ from 'underscore';
import React from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
-import ButtonWithMenu from './ButtonWithMenu';
+import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
import * as Expensicons from './Icon/Expensicons';
import Permissions from '../libs/Permissions';
import ONYXKEYS from '../ONYXKEYS';
@@ -14,6 +14,8 @@ import KYCWall from './KYCWall';
import withNavigation from './withNavigation';
import {withNetwork} from './OnyxProvider';
import networkPropTypes from './networkPropTypes';
+import iouReportPropTypes from '../pages/iouReportPropTypes';
+import * as ReportUtils from '../libs/ReportUtils';
const propTypes = {
/** Callback to execute when this button is pressed. Receives a single payment type argument. */
@@ -31,9 +33,27 @@ const propTypes = {
/** When the button is opened via an IOU, ID for the chatReport that the IOU is linked to */
chatReportID: PropTypes.string,
+ /** The IOU/Expense report we are paying */
+ iouReport: iouReportPropTypes,
+
/** List of betas available to current user */
betas: PropTypes.arrayOf(PropTypes.string),
+ /** The route to redirect if user does not have a payment method setup */
+ enablePaymentsRoute: PropTypes.string.isRequired,
+
+ /** Should we show the payment options? */
+ shouldShowPaymentOptions: PropTypes.bool,
+
+ /** The last payment method used per policy */
+ nvp_lastPaymentMethod: PropTypes.objectOf(PropTypes.string),
+
+ /** The policyID of the report we are paying */
+ policyID: PropTypes.string.isRequired,
+
+ /** Additional styles to add to the component */
+ style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
+
...withLocalizePropTypes,
};
@@ -42,6 +62,10 @@ const defaultProps = {
shouldShowPaypal: false,
chatReportID: '',
betas: [],
+ shouldShowPaymentOptions: false,
+ nvp_lastPaymentMethod: {},
+ style: [],
+ iouReport: {},
};
class SettlementButton extends React.Component {
@@ -51,55 +75,61 @@ class SettlementButton extends React.Component {
getButtonOptionsFromProps() {
const buttonOptions = [];
-
- if (this.props.currency === CONST.CURRENCY.USD && Permissions.canUsePayWithExpensify(this.props.betas) && Permissions.canUseWallet(this.props.betas)) {
- buttonOptions.push({
+ const paymentMethods = {
+ [CONST.IOU.PAYMENT_TYPE.EXPENSIFY]: {
text: this.props.translate('iou.settleExpensify'),
icon: Expensicons.Wallet,
- value: CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
- });
- }
-
- if (this.props.shouldShowPaypal && _.includes(CONST.PAYPAL_SUPPORTED_CURRENCIES, this.props.currency)) {
- buttonOptions.push({
+ value: ReportUtils.isExpenseReport(this.props.iouReport) ? CONST.IOU.PAYMENT_TYPE.VBBA : CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
+ },
+ [CONST.IOU.PAYMENT_TYPE.PAYPAL_ME]: {
text: this.props.translate('iou.settlePaypalMe'),
icon: Expensicons.PayPal,
value: CONST.IOU.PAYMENT_TYPE.PAYPAL_ME,
- });
+ },
+ [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: {
+ text: this.props.translate('iou.settleElsewhere'),
+ icon: Expensicons.Cash,
+ value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
+ },
+ };
+ if (!this.props.shouldShowPaymentOptions && this.props.nvp_lastPaymentMethod[this.props.policyID]) {
+ return [paymentMethods[this.props.nvp_lastPaymentMethod[this.props.policyID]]];
}
-
- buttonOptions.push({
- text: this.props.translate('iou.settleElsewhere'),
- icon: Expensicons.Cash,
- value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
- });
-
+ if (this.props.currency === CONST.CURRENCY.USD && Permissions.canUsePayWithExpensify(this.props.betas) && Permissions.canUseWallet(this.props.betas)) {
+ buttonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.EXPENSIFY]);
+ }
+ if (this.props.shouldShowPaypal && _.includes(CONST.PAYPAL_SUPPORTED_CURRENCIES, this.props.currency)) {
+ buttonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.PAYPAL_ME]);
+ }
+ buttonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]);
return buttonOptions;
}
render() {
return (
this.props.onPress(CONST.IOU.PAYMENT_TYPE.EXPENSIFY)}
+ onSuccessfulKYC={(iouPaymentType) => this.props.onPress(iouPaymentType)}
enablePaymentsRoute={this.props.enablePaymentsRoute}
addBankAccountRoute={this.props.addBankAccountRoute}
addDebitCardRoute={this.props.addDebitCardRoute}
isDisabled={this.props.network.isOffline}
chatReportID={this.props.chatReportID}
+ iouReport={this.props.iouReport}
>
{(triggerKYCFlow) => (
- {
- if (iouPaymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) {
- triggerKYCFlow(event);
+ if (iouPaymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || iouPaymentType === CONST.IOU.PAYMENT_TYPE.VBBA) {
+ triggerKYCFlow(event, iouPaymentType);
return;
}
this.props.onPress(iouPaymentType);
}}
options={this.getButtonOptionsFromProps()}
+ style={this.props.style}
/>
)}
@@ -118,5 +148,8 @@ export default compose(
betas: {
key: ONYXKEYS.BETAS,
},
+ nvp_lastPaymentMethod: {
+ key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD,
+ },
}),
)(SettlementButton);
diff --git a/src/components/withNavigationFallback.js b/src/components/withNavigationFallback.js
index 56e4018fb24c..4bd5cfdeb5d8 100644
--- a/src/components/withNavigationFallback.js
+++ b/src/components/withNavigationFallback.js
@@ -36,7 +36,7 @@ export default function (WrappedComponent) {
WithNavigationFallback.contextType = NavigationContext;
WithNavigationFallback.displayName = `WithNavigationFocusWithFallback(${getComponentDisplayName(WrappedComponent)})`;
WithNavigationFallback.propTypes = {
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+ forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.oneOfType([PropTypes.instanceOf(React.Component), PropTypes.func])})]),
};
WithNavigationFallback.defaultProps = {
forwardedRef: undefined,
diff --git a/src/components/withNavigationFocus.js b/src/components/withNavigationFocus.js
index 45b97472d2ac..0ece1b658c65 100644
--- a/src/components/withNavigationFocus.js
+++ b/src/components/withNavigationFocus.js
@@ -22,7 +22,7 @@ export default function withNavigationFocus(WrappedComponent) {
WithNavigationFocus.displayName = `withNavigationFocus(${getComponentDisplayName(WrappedComponent)})`;
WithNavigationFocus.propTypes = {
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+ forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.oneOfType([PropTypes.instanceOf(React.Component), PropTypes.func])})]),
};
WithNavigationFocus.defaultProps = {
forwardedRef: undefined,
diff --git a/src/hooks/useArrowKeyFocusManager.js b/src/hooks/useArrowKeyFocusManager.js
new file mode 100644
index 000000000000..09c3f03d488b
--- /dev/null
+++ b/src/hooks/useArrowKeyFocusManager.js
@@ -0,0 +1,72 @@
+import {useState, useEffect} from 'react';
+import useKeyboardShortcut from './useKeyboardShortcut';
+import CONST from '../CONST';
+
+/**
+ * A hook that makes it easy to use the arrow keys to manage focus of items in a list
+ *
+ * @param {Object} config
+ * @param {Number} config.maxIndex – typically the number of items in your list
+ * @param {Function} [config.onFocusedIndexChange] – optional callback to execute when focusedIndex changes
+ * @param {Number} [config.initialFocusedIndex] – where to start in the list
+ * @param {Array} [config.disabledIndexes] – An array of indexes to disable + skip over
+ * @param {Boolean} [config.shouldExcludeTextAreaNodes] – Whether arrow keys should have any effect when a TextArea node is focused
+ * @returns {Array}
+ */
+export default function useArrowKeyFocusManager({maxIndex, onFocusedIndexChange = () => {}, initialFocusedIndex = 0, disabledIndexes = [], shouldExcludeTextAreaNodes = true}) {
+ const [focusedIndex, setFocusedIndex] = useState(initialFocusedIndex);
+ useEffect(() => onFocusedIndexChange(focusedIndex), [focusedIndex, onFocusedIndexChange]);
+
+ useKeyboardShortcut(
+ CONST.KEYBOARD_SHORTCUTS.ARROW_UP,
+ () => {
+ if (maxIndex < 0) {
+ return;
+ }
+
+ const currentFocusedIndex = focusedIndex > 0 ? focusedIndex - 1 : maxIndex;
+ let newFocusedIndex = currentFocusedIndex;
+
+ while (disabledIndexes.includes(newFocusedIndex)) {
+ newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : maxIndex;
+ if (newFocusedIndex === currentFocusedIndex) {
+ // all indexes are disabled
+ return; // no-op
+ }
+ }
+
+ setFocusedIndex(newFocusedIndex);
+ },
+ {
+ excludedNodes: shouldExcludeTextAreaNodes ? ['TEXTAREA'] : [],
+ },
+ );
+
+ useKeyboardShortcut(
+ CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN,
+ () => {
+ if (maxIndex < 0) {
+ return;
+ }
+
+ const currentFocusedIndex = focusedIndex < maxIndex ? focusedIndex + 1 : 0;
+ let newFocusedIndex = currentFocusedIndex;
+
+ while (disabledIndexes.includes(newFocusedIndex)) {
+ newFocusedIndex = newFocusedIndex < maxIndex ? newFocusedIndex + 1 : 0;
+ if (newFocusedIndex === currentFocusedIndex) {
+ // all indexes are disabled
+ return; // no-op
+ }
+ }
+
+ setFocusedIndex(newFocusedIndex);
+ },
+ {
+ excludedNodes: shouldExcludeTextAreaNodes ? ['TEXTAREA'] : [],
+ },
+ );
+
+ // Note: you don't need to manually manage focusedIndex in the parent. setFocusedIndex is only exposed in case you want to reset focusedIndex or focus a specific item
+ return [focusedIndex, setFocusedIndex];
+}
diff --git a/src/hooks/useKeyboardShortcut.js b/src/hooks/useKeyboardShortcut.js
new file mode 100644
index 000000000000..434daeda1921
--- /dev/null
+++ b/src/hooks/useKeyboardShortcut.js
@@ -0,0 +1,37 @@
+import {useEffect, useRef, useCallback} from 'react';
+import KeyboardShortcut from '../libs/KeyboardShortcut';
+
+/**
+ * Register a keyboard shortcut handler.
+ *
+ * @param {Object} shortcut
+ * @param {Function} callback
+ * @param {Object} [config]
+ */
+export default function useKeyboardShortcut(shortcut, callback, config = {}) {
+ const {captureOnInputs = true, shouldBubble = false, priority = 0, shouldPreventDefault = true, excludedNodes = [], isActive = true} = config;
+
+ const subscription = useRef(null);
+ const subscribe = useCallback(
+ () =>
+ KeyboardShortcut.subscribe(
+ shortcut.shortcutKey,
+ callback,
+ shortcut.descriptionKey,
+ shortcut.modifiers,
+ captureOnInputs,
+ shouldBubble,
+ priority,
+ shouldPreventDefault,
+ excludedNodes,
+ ),
+ [callback, captureOnInputs, excludedNodes, priority, shortcut.descriptionKey, shortcut.modifiers, shortcut.shortcutKey, shouldBubble, shouldPreventDefault],
+ );
+
+ useEffect(() => {
+ const unsubscribe = subscription.current || (() => {});
+ unsubscribe();
+ subscription.current = isActive ? subscribe() : null;
+ return isActive ? subscription.current : () => {};
+ }, [isActive, subscribe]);
+}
diff --git a/src/hooks/useWindowDimensions.js b/src/hooks/useWindowDimensions.js
new file mode 100644
index 000000000000..ea46be38246a
--- /dev/null
+++ b/src/hooks/useWindowDimensions.js
@@ -0,0 +1,20 @@
+import {useWindowDimensions} from 'react-native';
+import variables from '../styles/variables';
+
+/**
+ * A convenience wrapper around React Native's useWindowDimensions hook that also provides booleans for our breakpoints.
+ * @returns {Object}
+ */
+export default function () {
+ const {width: windowWidth, height: windowHeight} = useWindowDimensions();
+ const isSmallScreenWidth = windowWidth <= variables.mobileResponsiveWidthBreakpoint;
+ const isMediumScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint;
+ const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint;
+ return {
+ windowWidth,
+ windowHeight,
+ isSmallScreenWidth,
+ isMediumScreenWidth,
+ isLargeScreenWidth,
+ };
+}
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index 0b429a6d098b..33af1acd67c5 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -338,7 +338,7 @@ function requestMoney(report, amount, currency, payeeEmail, participant, comment
);
let isNewReportPreviewAction = false;
- let reportPreviewAction = ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID);
+ let reportPreviewAction = isNewIOUReport ? null : ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID);
if (!reportPreviewAction) {
isNewReportPreviewAction = true;
reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport.reportID, iouReport.reportID);
@@ -1014,6 +1014,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType
* @returns {Object}
*/
function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMethodType) {
+ const reportPreviewAction = ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID);
const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(iouReport.total, iouReport.currency, iouReport.reportID);
const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction(
CONST.IOU.REPORT_ACTION_TYPE.PAY,
@@ -1037,6 +1038,17 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho
lastVisibleActionCreated: optimisticIOUReportAction.created,
hasOutstandingIOU: false,
iouReportID: null,
+ lastMessageText: optimisticIOUReportAction.message[0].text,
+ lastMessageHtml: optimisticIOUReportAction.message[0].html,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`,
+ value: {
+ [reportPreviewAction.reportActionID]: {
+ created: DateUtils.getDBTime(),
+ },
},
},
{
@@ -1065,6 +1077,11 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`,
value: optimisticTransaction,
},
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD,
+ value: {[iouReport.policyID]: paymentMethodType},
+ },
];
const successData = [
@@ -1087,6 +1104,15 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho
];
const failureData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`,
+ value: {
+ [reportPreviewAction.reportActionID]: {
+ created: reportPreviewAction.created,
+ },
+ },
+ },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
@@ -1173,44 +1199,23 @@ function sendMoneyViaPaypal(report, amount, currency, comment, managerEmail, rec
}
/**
+ * @param {String} paymentType
* @param {Object} chatReport
* @param {Object} iouReport
- * @param {Object} recipient
+ * @param {String} reimbursementBankAccountState
*/
-function payMoneyRequestElsewhere(chatReport, iouReport, recipient) {
- const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, CONST.IOU.PAYMENT_TYPE.ELSEWHERE);
-
- API.write('PayMoneyRequestElsewhere', params, {optimisticData, successData, failureData});
-
- Navigation.navigate(ROUTES.getReportRoute(chatReport.reportID));
-}
-
-/**
- * @param {Object} chatReport
- * @param {Object} iouReport
- * @param {Object} recipient
- */
-function payMoneyRequestWithWallet(chatReport, iouReport, recipient) {
- const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, CONST.IOU.PAYMENT_TYPE.EXPENSIFY);
-
- API.write('PayMoneyRequestWithWallet', params, {optimisticData, successData, failureData});
-
- Navigation.navigate(ROUTES.getReportRoute(chatReport.reportID));
-}
-
-/**
- * @param {Object} chatReport
- * @param {Object} iouReport
- * @param {Object} recipient
- */
-function payMoneyRequestViaPaypal(chatReport, iouReport, recipient) {
- const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, CONST.IOU.PAYMENT_TYPE.PAYPAL_ME);
-
- API.write('PayMoneyRequestViaPaypal', params, {optimisticData, successData, failureData});
+function payMoneyRequest(paymentType, chatReport, iouReport) {
+ const recipient = {
+ login: iouReport.ownerEmail,
+ payPalMeAddress: iouReport.submitterPayPalMeAddress,
+ };
+ const {params, optimisticData, successData, failureData} = getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentType);
+ API.write('PayMoneyRequest', params, {optimisticData, successData, failureData});
Navigation.navigate(ROUTES.getReportRoute(chatReport.reportID));
-
- asyncOpenURL(Promise.resolve(), buildPayPalPaymentUrl(iouReport.total, recipient.payPalMeAddress, iouReport.currency));
+ if (paymentType === CONST.IOU.PAYMENT_TYPE.PAYPAL_ME) {
+ asyncOpenURL(Promise.resolve(), buildPayPalPaymentUrl(iouReport.total, recipient.payPalMeAddress, iouReport.currency));
+ }
}
export {
@@ -1220,10 +1225,8 @@ export {
requestMoney,
sendMoneyElsewhere,
sendMoneyViaPaypal,
- payMoneyRequestElsewhere,
- payMoneyRequestViaPaypal,
+ payMoneyRequest,
setIOUSelectedCurrency,
setMoneyRequestDescription,
sendMoneyWithWallet,
- payMoneyRequestWithWallet,
};
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index 427c8a0d8003..bf63309b52dd 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -207,7 +207,7 @@ const HeaderView = (props) => {
{shouldShowThreeDotsButton && (
)}
diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js
index dfdf559e79c7..2d255868c539 100644
--- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js
+++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js
@@ -46,7 +46,6 @@ class PopoverReportActionContextMenu extends React.Component {
this.contextMenuAnchor = undefined;
this.showContextMenu = this.showContextMenu.bind(this);
this.hideContextMenu = this.hideContextMenu.bind(this);
- this.measureContent = this.measureContent.bind(this);
this.measureContextMenuAnchorPosition = this.measureContextMenuAnchorPosition.bind(this);
this.confirmDeleteAndHideModal = this.confirmDeleteAndHideModal.bind(this);
this.hideDeleteModal = this.hideDeleteModal.bind(this);
@@ -218,27 +217,6 @@ class PopoverReportActionContextMenu extends React.Component {
});
}
- /**
- * Used to calculate the Context Menu Dimensions
- *
- * @returns {JSX}
- */
- measureContent() {
- return (
-
- );
- }
-
/**
* Run the callback and return a noop function to reset it
* @param {Function} callback
@@ -302,7 +280,6 @@ class PopoverReportActionContextMenu extends React.Component {
animationIn="fadeIn"
disableAnimation={false}
animationOutTiming={1}
- measureContent={this.measureContent}
shouldSetModalVisibility={false}
fullscreen
>
@@ -312,6 +289,7 @@ class PopoverReportActionContextMenu extends React.Component {
reportID={this.state.reportID}
reportAction={this.state.reportAction}
draftMessage={this.state.reportActionDraftMessage}
+ selection={this.state.selection}
isArchivedRoom={this.state.isArchivedRoom}
isChronosReport={this.state.isChronosReport}
anchor={this.contextMenuTargetNode}
diff --git a/src/pages/home/report/ReactionList/PopoverReactionList.js b/src/pages/home/report/ReactionList/PopoverReactionList.js
index 3642cce69e8f..8a413163056d 100644
--- a/src/pages/home/report/ReactionList/PopoverReactionList.js
+++ b/src/pages/home/report/ReactionList/PopoverReactionList.js
@@ -39,7 +39,6 @@ class PopoverReactionList extends React.Component {
this.showReactionList = this.showReactionList.bind(this);
this.hideReactionList = this.hideReactionList.bind(this);
- this.measureContent = this.measureContent.bind(this);
this.measureReactionListPosition = this.measureReactionListPosition.bind(this);
this.getReactionListMeasuredLocation = this.getReactionListMeasuredLocation.bind(this);
@@ -147,26 +146,6 @@ class PopoverReactionList extends React.Component {
});
}
- /**
- * Used to calculate the PopoverReactionList Dimensions
- *
- * @returns {JSX}
- */
- measureContent() {
- return (
-
- );
- }
-
render() {
return (
<>
@@ -177,7 +156,6 @@ class PopoverReactionList extends React.Component {
animationIn="fadeIn"
disableAnimation={false}
animationOutTiming={1}
- measureContent={this.measureContent}
shouldSetModalVisibility={false}
fullscreen
>
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index b3ec4dabc0b8..b60c2d4d1938 100644
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -981,7 +981,8 @@ class ReportActionCompose extends React.Component {
isVisible={this.state.isMenuVisible}
onClose={() => this.setMenuVisibility(false)}
onItemSelected={() => this.setMenuVisibility(false)}
- anchorPosition={styles.createMenuPositionReportActionCompose}
+ anchorPosition={styles.createMenuPositionReportActionCompose(this.props.windowHeight)}
+ anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM}}
menuItems={[
...this.getMoneyRequestOptions(reportParticipants),
...this.getTaskOption(reportParticipants),
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
index cded77bb2f2c..6c0dd2cf6cd5 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
@@ -171,7 +171,7 @@ class FloatingActionButtonAndPopover extends React.Component {
this.payMoneyRequest(paymentMethodType)}
+ onPress={(paymentMethodType) => IOU.payMoneyRequest(paymentMethodType, this.props.chatReport, this.props.iouReport)}
shouldShowPaypal={Boolean(lodashGet(this.props, 'iouReport.submitterPayPalMeAddress'))}
currency={lodashGet(this.props, 'iouReport.currency')}
enablePaymentsRoute={ROUTES.IOU_DETAILS_ENABLE_PAYMENTS}
addBankAccountRoute={ROUTES.IOU_DETAILS_ADD_BANK_ACCOUNT}
addDebitCardRoute={ROUTES.IOU_DETAILS_ADD_DEBIT_CARD}
chatReportID={this.props.route.params.chatReportID}
+ policyID={this.props.iouReport.policyID}
/>
)}
diff --git a/src/pages/iou/MoneyRequestModal.js b/src/pages/iou/MoneyRequestModal.js
index 02e31ede0527..a55ecec665da 100644
--- a/src/pages/iou/MoneyRequestModal.js
+++ b/src/pages/iou/MoneyRequestModal.js
@@ -367,6 +367,7 @@ const MoneyRequestModal = (props) => {
// the floating-action-button (since it is something that exists outside the context of a report).
canModifyParticipants={!_.isEmpty(reportID)}
navigateToStep={navigateToStep}
+ policyID={props.report.policyID}
/>
)}
diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js
index d4726cf8b644..025d22b33b37 100644
--- a/src/pages/iou/steps/MoneyRequestConfirmPage.js
+++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js
@@ -28,6 +28,9 @@ const propTypes = {
/** Function to navigate to a given step in the parent MoneyRequestModal */
navigateToStep: PropTypes.func.isRequired,
+
+ /** The policyID of the request */
+ policyID: PropTypes.string.isRequired,
};
const defaultProps = {
@@ -45,6 +48,7 @@ const MoneyRequestConfirmPage = (props) => (
iouType={props.iouType}
canModifyParticipants={props.canModifyParticipants}
navigateToStep={props.navigateToStep}
+ policyID={props.policyID}
/>
);
diff --git a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
index 2389d3a3de6b..4b7459ea7cfa 100644
--- a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
+++ b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
@@ -49,8 +49,9 @@ class BasePaymentsPage extends React.Component {
title: '',
},
selectedPaymentMethodType: null,
+ anchorPositionHorizontal: 0,
+ anchorPositionVertical: 0,
anchorPositionTop: 0,
- anchorPositionBottom: 0,
anchorPositionRight: 0,
addPaymentMethodButton: null,
methodID: null,
@@ -152,10 +153,11 @@ class BasePaymentsPage extends React.Component {
setPositionAddPaymentMenu(position) {
this.setState({
anchorPositionTop: position.top + position.height + variables.addPaymentPopoverTopSpacing,
- anchorPositionBottom: this.props.windowHeight - position.top,
// We want the position to be 13px to the right of the left border
anchorPositionRight: this.props.windowWidth - position.right + variables.addPaymentPopoverRightSpacing,
+ anchorPositionHorizontal: position.x,
+ anchorPositionVertical: position.y,
});
}
@@ -417,8 +419,8 @@ class BasePaymentsPage extends React.Component {
isVisible={this.state.shouldShowAddPaymentMenu}
onClose={this.hideAddPaymentMenu}
anchorPosition={{
- bottom: this.state.anchorPositionBottom,
- right: this.state.anchorPositionRight - 10,
+ horizontal: this.state.anchorPositionHorizontal,
+ vertical: this.state.anchorPositionVertical - 10,
}}
onItemSelected={(method) => this.addPaymentMethodTypePressed(method)}
/>
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js
index 88890bcd0f99..1a216fd0aacb 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.js
@@ -23,6 +23,7 @@ import styles from '../../../styles/styles';
import * as Expensicons from '../../../components/Icon/Expensicons';
import ONYXKEYS from '../../../ONYXKEYS';
import * as UserUtils from '../../../libs/UserUtils';
+import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
const propTypes = {
/* Onyx Props */
@@ -37,6 +38,7 @@ const propTypes = {
}),
...withLocalizePropTypes,
+ ...windowDimensionsPropTypes,
...withCurrentUserPersonalDetailsPropTypes,
};
@@ -100,7 +102,8 @@ const ProfilePage = (props) => {
source={ReportUtils.getAvatar(lodashGet(currentUserDetails, 'avatar', ''), lodashGet(currentUserDetails, 'login', ''))}
onImageSelected={PersonalDetails.updateAvatar}
onImageRemoved={PersonalDetails.deleteAvatar}
- anchorPosition={styles.createMenuPositionProfile}
+ anchorPosition={styles.createMenuPositionProfile(props.windowWidth)}
+ anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
size={CONST.AVATAR_SIZE.LARGE}
/>
@@ -133,6 +136,7 @@ ProfilePage.displayName = 'ProfilePage';
export default compose(
withLocalize,
+ withWindowDimensions,
withCurrentUserPersonalDetails,
withOnyx({
loginList: {
diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js
index bc4d6274534c..01a434fa5ae5 100644
--- a/src/pages/workspace/WorkspaceInitialPage.js
+++ b/src/pages/workspace/WorkspaceInitialPage.js
@@ -28,6 +28,7 @@ import ONYXKEYS from '../../ONYXKEYS';
import OfflineWithFeedback from '../../components/OfflineWithFeedback';
import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursementAccountPropTypes';
import * as ReportUtils from '../../libs/ReportUtils';
+import withWindowDimensions from '../../components/withWindowDimensions';
const propTypes = {
...policyPropTypes,
@@ -149,7 +150,7 @@ const WorkspaceInitialPage = (props) => {
onSelected: () => setIsDeleteModalOpen(true),
},
]}
- threeDotsAnchorPosition={styles.threeDotsPopoverOffset}
+ threeDotsAnchorPosition={styles.threeDotsPopoverOffset(props.windowWidth)}
/>
Policy.updateWorkspaceAvatar(lodashGet(props.policy, 'id', ''), file)}
onImageRemoved={() => Policy.deleteWorkspaceAvatar(lodashGet(props.policy, 'id', ''))}
@@ -158,6 +161,7 @@ WorkspaceSettingsPage.displayName = 'WorkspaceSettingsPage';
export default compose(
withPolicy,
+ withWindowDimensions,
withOnyx({
currencyList: {key: ONYXKEYS.CURRENCY_LIST},
}),
diff --git a/src/stories/ButtonWithDropdown.stories.js b/src/stories/ButtonWithDropdownMenu.stories.js
similarity index 64%
rename from src/stories/ButtonWithDropdown.stories.js
rename to src/stories/ButtonWithDropdownMenu.stories.js
index ac716f44f7ec..54084be5b018 100644
--- a/src/stories/ButtonWithDropdown.stories.js
+++ b/src/stories/ButtonWithDropdownMenu.stories.js
@@ -1,5 +1,5 @@
import React from 'react';
-import ButtonWithDropdown from '../components/ButtonWithDropdown';
+import ButtonWithDropdownMenu from '../components/ButtonWithDropdownMenu';
/**
* We use the Component Story Format for writing stories. Follow the docs here:
@@ -7,18 +7,22 @@ import ButtonWithDropdown from '../components/ButtonWithDropdown';
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
const story = {
- title: 'Components/ButtonWithDropdown',
- component: ButtonWithDropdown,
+ title: 'Components/ButtonWithDropdownMenu',
+ component: ButtonWithDropdownMenu,
};
// eslint-disable-next-line react/jsx-props-no-spreading
-const Template = (args) => ;
+const Template = (args) => ;
// Arguments can be passed to the component by binding
// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Default = Template.bind({});
Default.args = {
buttonText: 'Pay with PayPal.me',
+ options: [
+ {value: 1, text: 'One'},
+ {value: 2, text: 'Two'},
+ ],
};
export default story;
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 8763ecc765dd..62cecda4ca89 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -1209,20 +1209,20 @@ const styles = {
lineHeight: variables.fontSizeOnlyEmojisHeight,
},
- createMenuPositionSidebar: {
- left: 18,
- bottom: 100,
- },
+ createMenuPositionSidebar: (windowHeight) => ({
+ horizontal: 18,
+ vertical: windowHeight - 100,
+ }),
- createMenuPositionProfile: {
- right: 18,
- top: 180,
- },
+ createMenuPositionProfile: (windowWidth) => ({
+ horizontal: windowWidth - 355,
+ vertical: 250,
+ }),
- createMenuPositionReportActionCompose: {
- left: 18 + variables.sideBarWidth,
- bottom: 75,
- },
+ createMenuPositionReportActionCompose: (windowHeight) => ({
+ horizontal: 18 + variables.sideBarWidth,
+ vertical: windowHeight - 75,
+ }),
createMenuPositionRightSidepane: {
right: 18,
@@ -2824,15 +2824,15 @@ const styles = {
flex: 1,
},
- threeDotsPopoverOffset: {
- top: 50,
- right: 60,
- },
+ threeDotsPopoverOffset: (windowWidth) => ({
+ vertical: 50,
+ horizontal: windowWidth - 60,
+ }),
- threeDotsPopoverOffsetNoCloseButton: {
- top: 50,
- right: 10,
- },
+ threeDotsPopoverOffsetNoCloseButton: (windowWidth) => ({
+ vertical: 50,
+ horizontal: windowWidth - 10,
+ }),
invert: {
// It's important to invert the Y AND X axis to prevent a react native issue that can lead to ANRs on android 13
diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js
index ade3f9617edd..74b1b6b9c3ab 100644
--- a/tests/actions/IOUTest.js
+++ b/tests/actions/IOUTest.js
@@ -1230,7 +1230,7 @@ describe('actions/IOU', () => {
)
.then(() => {
fetch.pause();
- IOU.payMoneyRequestElsewhere(chatReport, iouReport, {login: iouReport.ownerEmail});
+ IOU.payMoneyRequest(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, chatReport, iouReport);
return waitForPromisesToResolve();
})
.then(