diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5cd60c1f2d4f..947cfba1851d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,13 +1,13 @@
-# Contributing to Expensify.cash
-Welcome! Thanks for checking out Expensify.cash and for taking the time to contribute!
+# Contributing to Expensify
+Welcome! Thanks for checking out the new Expensify app and for taking the time to contribute!
## Getting Started
-If you would like to become an Expensify.cash contributor, the first step is to read this document in it's entirety. The second step is to review the README guidelines [here](https://github.com/Expensify/Expensify.cash/blob/main/README.md) for a general overview of the code repository (i.e. how to run the app locally, testing, storage, etc). Please read both documents before asking questions, as it may be covered within the documentation.
+If you would like to become an Expensify contributor, the first step is to read this document in its entirety. The second step is to review the README guidelines [here](https://github.com/Expensify/Expensify.cash/blob/main/README.md) for a general overview of the code repository (i.e. how to run the app locally, testing, storage, etc). Please read both documents before asking questions, as it may be covered within the documentation.
#### Test Accounts
-You can create as many accounts as needed in order to test your changes directly from [new.expensify.com](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be invited by entering a valid email or phone in the "Find or start a chat" input then tapping the avatar.
+You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be invited by entering a valid email or phone in the "Find or start a chat" input then tapping the avatar.
-**Note**: When testing chat functionality in Expensify Cash please do this between accounts you or your fellow contributors own - **do not test chatting with Concierge**, as this diverts to our customer support team. Thank you.
+**Note**: When testing chat functionality in the app please do this between accounts you or your fellow contributors own - **do not test chatting with Concierge**, as this diverts to our customer support team. Thank you.
## Code of Conduct
This project and everyone participating in it is governed by the Expensify [Code of Conduct](https://github.com/Expensify/Expensify.cash/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [contributors@expensify.com](mailto:contributors@expensify.com).
@@ -24,21 +24,21 @@ If you are hired for an Upwork job and have any job-specific questions, please a
If you've found a vulnerability, please email security@expensify.com with the subject `Vulnerability Report` instead of creating an issue.
## Payment for Contributions
-We hire and pay external contributors via Upwork.com. If you'd like to be paid for contributing, please create an Upwork account and apply for a job in the [Upwork issue list](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2). Payment for your contributions will be made no less than 7 days after the pull request is merged to allow for regression testing. We hire one contributor for each Upwork job. New Expensify.cash contributors are limited to working on one job at a time, however experienced contributors may work on numerous jobs simultaneously. If you have not received payment after 8 days of the PR being deployed to production, please email contributors@expensify.com referencing the GH issue and your GH handle.
+We hire and pay external contributors via Upwork.com. If you'd like to be paid for contributing, please create an Upwork account and apply for a job in the [Upwork issue list](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2). Payment for your contributions will be made no less than 7 days after the pull request is merged to allow for regression testing. We hire one contributor for each Upwork job. New contributors are limited to working on one job at a time, however experienced contributors may work on numerous jobs simultaneously. If you have not received payment after 8 days of the PR being deployed to production, please email contributors@expensify.com referencing the GH issue and your GH handle.
-## Finding Expensify.cash Jobs
-There are two ways you can find an Expensify.cash job that you can contribute to:
+## Finding Jobs
+There are two ways you can find a job that you can contribute to:
#### Finding a job that Expensify posted
-This is the most common scenario for contributors. The Expensify team posts Expensify.cash jobs to the Upwork job list [here](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow.
+This is the most common scenario for contributors. The Expensify team posts new jobs to the Upwork job list [here](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2). Each job in Upwork has a corresponding GitHub issue, which will include instructions to follow.
#### Proposing a job that Expensify hasn’t posted
In this scenario, it’s possible that you found a bug or enhancement that we haven’t posted to the [Upwork job list](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2) or [Github repository](https://github.com/Expensify/Expensify.cash/issues?q=is%3Aissue). This is an opportunity to propose a job, and (optionally) a solution. If it's a valid job proposal, we will compensate you for the solution and give an additional bonus of $150 for proactively proposing the job. In this case, please take the following steps:
- 1. Check to ensure an issue does not already exist in the Expensify.cash Issue list or Upwork job list. Please use your best judgement to search for similar titles and issue descriptions.
+ 1. Check to ensure an issue does not already exist in the New Expensify Issue list or Upwork job list. Please use your best judgement to search for similar titles and issue descriptions.
2. If your bug or enhancement matches an existing issue, please feel free to comment on that GitHub issue with your findings if you think it’ll help solve a problem.
- 3. If there is no existing issue or Upwork job, create a new GitHub issue in the Expensify.cash repo.
+ 3. If there is no existing issue or Upwork job, create a new GitHub issue in the Expensify/App repo.
4. Make sure to fill out all the required information fields in the issue template.
5. Add the `AutoAssignerTriage` label to your issue.
6. Before starting your PR to solve the bug or enhancement that you are proposing, please add a comment on your issue with a solution proposal.
@@ -50,7 +50,7 @@ In this scenario, it’s possible that you found a bug or enhancement that we ha
>
>**Solution:** Start up time will perceptibly decrease by 1042ms if we prevent the unnecessary re-renders of this component.
-## Working on Expensify.cash Jobs
+## Working on Expensify Jobs
*Reminder: For technical guidance please refer to the [README](https://github.com/Expensify/Expensify.cash/blob/main/README.md)*.
#### Express interest for the job on Upwork.com
diff --git a/README.md b/README.md
index bc68c0e2295e..da5d751538a8 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
@@ -19,14 +19,14 @@
* [Deploying](#deploying)
#### Additional Reading
-* [Contributing to Expensify.cash](./CONTRIBUTING.md)
+* [Contributing to Expensify](./CONTRIBUTING.md)
* [Expensify Code of Conduct](./CODE_OF_CONDUCT.md)
* [Contributor License Agreement](./CLA.md)
----
# Local development
-These instructions should get you set up ready to work on Expensify.cash 🙌
+These instructions should get you set up ready to work on New Expensify 🙌
## Getting Started
1. Install `node` & `npm`: `brew install node`
diff --git a/android/app/build.gradle b/android/app/build.gradle
index f74b1ca34dc5..50b887ce5ae7 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -149,8 +149,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001007802
- versionName "1.0.78-2"
+ versionCode 1001007803
+ versionName "1.0.78-3"
}
splits {
abi {
diff --git a/ios/ExpensifyCash/Info.plist b/ios/ExpensifyCash/Info.plist
index fa814ed0007d..940db60b1553 100644
--- a/ios/ExpensifyCash/Info.plist
+++ b/ios/ExpensifyCash/Info.plist
@@ -30,7 +30,7 @@
CFBundleVersion
- 1.0.78.2
+ 1.0.78.3
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/ExpensifyCashTests/Info.plist b/ios/ExpensifyCashTests/Info.plist
index 445926092867..b22fbd0c97f3 100644
--- a/ios/ExpensifyCashTests/Info.plist
+++ b/ios/ExpensifyCashTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.0.78.2
+ 1.0.78.3
diff --git a/package-lock.json b/package-lock.json
index 3d9db7c1b4ca..f3b5c2b8ac33 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "expensify.cash",
- "version": "1.0.78-2",
+ "version": "1.0.78-3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index cb1a3e760e85..000fcd3b12c3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "expensify.cash",
- "version": "1.0.78-2",
+ "version": "1.0.78-3",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "Expensify.cash is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index b9fd1a2068aa..dfd81620d98a 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -130,6 +130,7 @@ class AttachmentModal extends PureComponent {
textStyles={[styles.buttonConfirmText]}
text={this.props.translate('common.send')}
onPress={this.submitAndClose}
+ pressOnEnter
/>
)}
diff --git a/src/components/Button.js b/src/components/Button.js
index 1b26159706ad..be1f13ac5131 100644
--- a/src/components/Button.js
+++ b/src/components/Button.js
@@ -1,13 +1,12 @@
import _ from 'underscore';
-import React from 'react';
+import React, {Component} from 'react';
+import {Pressable, ActivityIndicator} from 'react-native';
import PropTypes from 'prop-types';
-import {
- Pressable, ActivityIndicator,
-} from 'react-native';
import styles from '../styles/styles';
import themeColors from '../styles/themes/default';
import OpacityView from './OpacityView';
import Text from './Text';
+import KeyboardShortcut from '../libs/KeyboardShortcut';
const propTypes = {
/** The text for the button label */
@@ -28,6 +27,9 @@ const propTypes = {
/** A function that is called when the button is clicked on */
onPress: PropTypes.func,
+ /** Call the onPress function when Enter key is pressed */
+ pressOnEnter: PropTypes.bool,
+
/** Additional styles to add after local styles */
style: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.object),
@@ -60,6 +62,7 @@ const defaultProps = {
small: false,
large: false,
onPress: () => {},
+ pressOnEnter: false,
style: [],
textStyles: [],
success: false,
@@ -69,16 +72,42 @@ const defaultProps = {
shouldRemoveLeftBorderRadius: false,
};
-const Button = (props) => {
- const additionalStyles = _.isArray(props.style) ? props.style : [props.style];
+class Button extends Component {
+ constructor(props) {
+ super(props);
+ this.additionalStyles = _.isArray(this.props.style) ? this.props.style : [this.props.style];
+
+ this.renderContent = this.renderContent.bind(this);
+ }
+
+ componentDidMount() {
+ if (!this.props.pressOnEnter) {
+ return;
+ }
+
+ // Setup and attach keypress handler for pressing the button with Enter key
+ this.unsubscribe = KeyboardShortcut.subscribe('Enter', () => {
+ if (!this.props.isDisabled && !this.props.isLoading) {
+ this.props.onPress();
+ }
+ }, [], true);
+ }
- function renderContent() {
- const {ContentComponent} = props;
+ componentWillUnmount() {
+ // Cleanup event listeners
+ if (!this.unsubscribe) {
+ return;
+ }
+ this.unsubscribe();
+ }
+
+ renderContent() {
+ const {ContentComponent} = this.props;
if (ContentComponent) {
return ;
}
- return props.isLoading
+ return this.props.isLoading
? (
) : (
@@ -86,49 +115,52 @@ const Button = (props) => {
selectable={false}
style={[
styles.buttonText,
- props.small && styles.buttonSmallText,
- props.large && styles.buttonLargeText,
- props.success && styles.buttonSuccessText,
- props.danger && styles.buttonDangerText,
- ...props.textStyles,
+ this.props.small && styles.buttonSmallText,
+ this.props.large && styles.buttonLargeText,
+ this.props.success && styles.buttonSuccessText,
+ this.props.danger && styles.buttonDangerText,
+ ...this.props.textStyles,
]}
>
- {props.text}
+ {this.props.text}
);
}
- return (
-
- {({pressed, hovered}) => (
-
- {renderContent()}
-
- )}
-
- );
-};
+ render() {
+ return (
+
+ {({pressed, hovered}) => (
+
+ {this.renderContent()}
+
+ )}
+
+ );
+ }
+}
Button.propTypes = propTypes;
Button.defaultProps = defaultProps;
diff --git a/src/components/ConfirmModal.js b/src/components/ConfirmModal.js
index c7529f974c86..8ddd05768696 100755
--- a/src/components/ConfirmModal.js
+++ b/src/components/ConfirmModal.js
@@ -71,6 +71,7 @@ const ConfirmModal = props => (
danger={props.danger}
style={[styles.mt4]}
onPress={props.onConfirm}
+ pressOnEnter
text={props.confirmText || props.translate('common.yes')}
/>
this.props.onConfirm(this.getSplits())}
+ pressOnEnter
/>
>
diff --git a/src/components/InvertedFlatList/index.android.js b/src/components/InvertedFlatList/index.android.js
index 4ecfe10415bd..8f9062e570bf 100644
--- a/src/components/InvertedFlatList/index.android.js
+++ b/src/components/InvertedFlatList/index.android.js
@@ -6,6 +6,7 @@ export default forwardRef((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
+ initialNumToRender={20}
// Remove clipped subviews helps prevent avatars and attachments from eating up excess memory on Android. When
// we run out of memory images stop appearing without any warning.
diff --git a/src/components/InvertedFlatList/index.ios.js b/src/components/InvertedFlatList/index.ios.js
index a98fa587721b..9a9ae537e0cd 100644
--- a/src/components/InvertedFlatList/index.ios.js
+++ b/src/components/InvertedFlatList/index.ios.js
@@ -1,4 +1,11 @@
+import React, {forwardRef} from 'react';
import BaseInvertedFlatList from './BaseInvertedFlatList';
-BaseInvertedFlatList.displayName = 'InvertedFlatList';
-export default BaseInvertedFlatList;
+export default forwardRef((props, ref) => (
+
+));
diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js
index a2dd20b2c999..3dab8062b6ac 100644
--- a/src/components/InvertedFlatList/index.js
+++ b/src/components/InvertedFlatList/index.js
@@ -61,6 +61,7 @@ class InvertedFlatList extends React.Component {
{...this.props}
ref={el => this.list = el}
shouldMeasureItems
+ initialNumToRender={25}
/>
);
}
diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js
index bfe3da6fe6f8..38d0d580e011 100644
--- a/src/components/Modal/BaseModal.js
+++ b/src/components/Modal/BaseModal.js
@@ -3,8 +3,6 @@ import {View} from 'react-native';
import PropTypes from 'prop-types';
import ReactNativeModal from 'react-native-modal';
import {SafeAreaInsetsContext} from 'react-native-safe-area-context';
-
-import KeyboardShortcut from '../../libs/KeyboardShortcut';
import styles, {getModalPaddingStyles, getSafeAreaPadding} from '../../styles/styles';
import themeColors from '../../styles/themes/default';
import {propTypes as modalPropTypes, defaultProps as modalDefaultProps} from './ModalPropTypes';
@@ -27,22 +25,19 @@ class BaseModal extends PureComponent {
constructor(props) {
super(props);
- this.hideModalAndRemoveEventListeners = this.hideModalAndRemoveEventListeners.bind(this);
- this.subscribeToKeyEvents = this.subscribeToKeyEvents.bind(this);
- this.unsubscribeFromKeyEvents = this.unsubscribeFromKeyEvents.bind(this);
+ this.hideModal = this.hideModal.bind(this);
}
componentWillUnmount() {
// we don't want to call the onModalHide on unmount
- this.hideModalAndRemoveEventListeners(this.props.isVisible);
+ this.hideModal(this.props.isVisible);
}
/**
- * Hides modal and unsubscribes from key event listeners
+ * Hides modal
* @param {Boolean} [callHideCallback=true] Should we call the onModalHide callback
*/
- hideModalAndRemoveEventListeners(callHideCallback = true) {
- this.unsubscribeFromKeyEvents();
+ hideModal(callHideCallback = true) {
if (this.props.shouldSetModalVisibility) {
setModalVisibility(false);
}
@@ -51,20 +46,6 @@ class BaseModal extends PureComponent {
}
}
- /**
- * Listens to specific keyboard keys when the modal has been opened
- */
- subscribeToKeyEvents() {
- KeyboardShortcut.subscribe('Enter', this.props.onSubmit, [], true);
- }
-
- /**
- * Stops listening to keyboard keys when modal has been closed
- */
- unsubscribeFromKeyEvents() {
- KeyboardShortcut.unsubscribe('Enter');
- }
-
render() {
const {
modalStyle,
@@ -97,14 +78,13 @@ class BaseModal extends PureComponent {
// eslint-disable-next-line react/jsx-props-no-multi-spaces
onBackButtonPress={this.props.onClose}
onModalShow={() => {
- this.subscribeToKeyEvents();
if (this.props.shouldSetModalVisibility) {
setModalVisibility(true);
}
this.props.onModalShow();
}}
propagateSwipe={this.props.propagateSwipe}
- onModalHide={this.hideModalAndRemoveEventListeners}
+ onModalHide={this.hideModal}
onSwipeComplete={this.props.onClose}
swipeDirection={swipeDirection}
isVisible={this.props.isVisible}
diff --git a/src/components/RenderHTML/index.js b/src/components/RenderHTML/index.js
index 4e03dae203a7..405d2621154b 100755
--- a/src/components/RenderHTML/index.js
+++ b/src/components/RenderHTML/index.js
@@ -5,10 +5,11 @@ import {
propTypes,
defaultProps,
} from './renderHTMLPropTypes';
+import canUseTouchScreen from '../../libs/canUseTouchscreen';
const RenderHTML = ({html, debug, isSmallScreenWidth}) => (
diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js
index 53239f2e6d01..c81dae2f1e33 100644
--- a/src/components/StatePicker.js
+++ b/src/components/StatePicker.js
@@ -9,6 +9,13 @@ const STATES = _.map(CONST.STATES, ({stateISO}) => ({
label: stateISO,
}));
+
+// Add a blank state so users are sure to actively choose a state instead accidentally going with the default choice
+STATES.unshift({
+ value: '',
+ label: '-',
+});
+
const propTypes = {
/** A callback method that is called when the value changes and it received the selected value as an argument */
onChange: PropTypes.func.isRequired,
diff --git a/src/languages/en.js b/src/languages/en.js
index ff4f68b69a9c..2c13b708c404 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -197,7 +197,7 @@ export default {
initialSettingsPage: {
about: 'About',
aboutPage: {
- description: 'Expensify.cash is built by a community of open source developers from around the world. Come help us build the next generation of Expensify.',
+ description: 'The New Expensify is built by a community of open source developers from around the world. Come help us build the next generation of Expensify.',
appDownloadLinks: 'App download links',
viewTheCode: 'View the code',
viewOpenJobs: 'View open jobs',
@@ -342,6 +342,7 @@ export default {
website: 'Please enter a valid website',
zipCode: 'Please enter a valid zip code',
addressStreet: 'Please enter a valid address street that is not a PO Box',
+ addressState: 'Please select a valid state',
incorporationDate: 'Please enter a valid incorporation date',
incorporationState: 'Please enter a valid Incorporation State',
industryCode: 'Please enter a valid industry classification code',
diff --git a/src/languages/es.js b/src/languages/es.js
index 825010eff0da..de5b415a1fdf 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -190,7 +190,7 @@ export default {
initialSettingsPage: {
about: 'Acerca de',
aboutPage: {
- description: 'Expensify.cash está desarrollado por una comunidad de desarrolladores open source de todo el mundo. Ayúdanos a construir la próxima generación de Expensify.',
+ description: 'El nuevo Expensify está creado por una comunidad de desarrolladores de código abierto de todo el mundo. Ven y ayúdanos a construir la próxima generación de Expensify.',
appDownloadLinks: 'Enlaces para descargar la App',
viewTheCode: 'Ver codigo',
viewOpenJobs: 'Ver trabajos disponibles',
@@ -321,6 +321,7 @@ export default {
website: 'Ingrese un sitio web válido',
zipCode: 'Ingrese un código postal válido',
addressStreet: 'Ingrese una calle de dirección válida que no sea un apartado postal',
+ addressState: 'Por favor, selecciona un estado',
incorporationDate: 'Ingrese una fecha de incorporación válida',
incorporationState: 'Ingrese un estado de incorporación válido',
industryCode: 'Ingrese un código de clasificación de industria válido',
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js
index ba720e3f789a..ff370c7ef42e 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.js
@@ -96,6 +96,22 @@ const modalScreenListeners = {
},
};
+let hasLoadedPolicies = false;
+
+/**
+ * We want to only load policy info if you are in the freePlan beta.
+ * @param {Array} betas
+ */
+function loadPoliciesBehindBeta(betas) {
+ // When removing the freePlan beta, simply load the policyList and the policySummaries in componentDidMount().
+ // Policy info loading should not be blocked behind the defaultRooms beta alone.
+ if (!hasLoadedPolicies && (Permissions.canUseFreePlan(betas) || Permissions.canUseDefaultRooms(betas))) {
+ getPolicyList();
+ getPolicySummaries();
+ hasLoadedPolicies = true;
+ }
+}
+
const propTypes = {
/** Information about the network */
network: PropTypes.shape({
@@ -142,10 +158,7 @@ class AuthScreens extends React.Component {
fetchCountryCodeByRequestIP();
UnreadIndicatorUpdater.listenForReportChanges();
- if (Permissions.canUseFreePlan(this.props.betas) || Permissions.canUseDefaultRooms(this.props.betas)) {
- getPolicyList();
- getPolicySummaries();
- }
+ loadPoliciesBehindBeta(this.props.betas);
// Refresh the personal details, timezone and betas every 30 minutes
// There is no pusher event that sends updated personal details data yet
@@ -186,9 +199,17 @@ class AuthScreens extends React.Component {
return true;
}
+ if (nextProps.betas !== this.props.betas) {
+ return true;
+ }
+
return false;
}
+ componentDidUpdate() {
+ loadPoliciesBehindBeta(this.props.betas);
+ }
+
componentWillUnmount() {
KeyboardShortcut.unsubscribe('K');
NetworkConnection.stopListeningForReconnect();
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index 7a827d18a6e8..56a2eaa54e9d 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -23,6 +23,12 @@ Onyx.connect({
callback: val => currentUserLogin = val && val.email,
});
+let currentUser;
+Onyx.connect({
+ key: ONYXKEYS.USER,
+ callback: val => currentUser = val,
+});
+
let countryCodeByIP;
Onyx.connect({
key: ONYXKEYS.COUNTRY_CODE,
@@ -231,7 +237,8 @@ function createOption(personalDetailList, report, draftComments, {
// It doesn't make sense to provide a login in the case of a report with multiple participants since
// there isn't any one single login to refer to for a report.
- login: !hasMultipleParticipants ? personalDetail.login : null,
+ // If single login is a mobile number, appending SMS domain
+ login: !hasMultipleParticipants ? addSMSDomainIfPhoneNumber(personalDetail.login) : null,
reportID: report ? report.reportID : null,
isUnread: report ? report.unreadActionCount > 0 : null,
hasDraftComment: _.size(reportDraftComment) > 0,
@@ -706,6 +713,36 @@ function getReportIcons(report, personalDetails) {
.map(item => item.avatar);
}
+/**
+ * Returns the given userDetails is currentUser or not.
+ * @param {Object} userDetails
+ * @returns {Bool}
+ */
+
+function isCurrentUser(userDetails) {
+ if (!userDetails) {
+ // If userDetails is null or undefined
+ return false;
+ }
+
+ // If user login is mobile number, append sms domain if not appended already just a fail safe.
+ const userDetailsLogin = addSMSDomainIfPhoneNumber(userDetails.login);
+
+ // Initial check with currentUserLogin
+ let result = currentUserLogin.toLowerCase() === userDetailsLogin.toLowerCase();
+ const {loginList} = currentUser;
+ let index = 0;
+
+ // Checking userDetailsLogin against to current user login options.
+ while (index < loginList.length && !result) {
+ if (loginList[index].partnerUserID.toLowerCase() === userDetailsLogin.toLowerCase()) {
+ result = true;
+ }
+ index++;
+ }
+ return result;
+}
+
export {
getSearchOptions,
getNewChatOptions,
@@ -718,4 +755,5 @@ export {
getIOUConfirmationOptionsFromParticipants,
getDefaultAvatar,
getReportIcons,
+ isCurrentUser,
};
diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js
index df3aa71cfa2c..28b3075e45ab 100644
--- a/src/libs/ValidationUtils.js
+++ b/src/libs/ValidationUtils.js
@@ -78,6 +78,11 @@ function isValidIdentity(identity) {
return false;
}
+ if (identity.state === '') {
+ Growl.error(translateLocal('bankAccount.error.addressState'));
+ return false;
+ }
+
if (!isValidZipCode(identity.zipCode)) {
Growl.error(translateLocal('bankAccount.error.zipCode'));
return false;
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index 33a8a9ac7832..75c82c8a3eb5 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -1132,6 +1132,11 @@ function updateLastReadActionID(reportID, sequenceNumber) {
// action). If 1 isn't subtracted then the "New" marker appears one row below the action (the first unread action)
const lastReadSequenceNumber = (sequenceNumber - 1) || reportMaxSequenceNumbers[reportID];
+ // We call this method in many cases where there's nothing to update because we already updated it, so we avoid
+ // doing an unnecessary server call if the last read is the same one we had already
+ if (lastReadSequenceNumbers[reportID] === lastReadSequenceNumber) {
+ return;
+ }
setLocalLastRead(reportID, lastReadSequenceNumber);
// Mark the report as not having any unread items
diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js
index 6e785629e6c9..4bf663aca62f 100644
--- a/src/libs/actions/User.js
+++ b/src/libs/actions/User.js
@@ -64,7 +64,7 @@ function getBetas() {
function getUserDetails() {
API.Get({
returnValueList: 'account, loginList, nameValuePairs',
- nvpNames: CONST.NVP.BLOCKED_FROM_CONCIERGE,
+ nvpNames: [CONST.NVP.BLOCKED_FROM_CONCIERGE, CONST.NVP.PAYPAL_ME_ADDRESS].join(','),
})
.then((response) => {
// Update the User onyx key
diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js
index de561fa37249..5f8732b881ef 100644
--- a/src/pages/ReimbursementAccount/CompanyStep.js
+++ b/src/pages/ReimbursementAccount/CompanyStep.js
@@ -33,14 +33,14 @@ class CompanyStep extends React.Component {
companyName: lodashGet(props, ['achData', 'companyName'], ''),
addressStreet: lodashGet(props, ['achData', 'addressStreet'], ''),
addressCity: lodashGet(props, ['achData', 'addressCity'], ''),
- addressState: lodashGet(props, ['achData', 'addressState']) || 'AK',
+ addressState: lodashGet(props, ['achData', 'addressState']) || '',
addressZipCode: lodashGet(props, ['achData', 'addressZipCode'], ''),
companyPhone: lodashGet(props, ['achData', 'companyPhone'], ''),
website: lodashGet(props, ['achData', 'website'], 'https://'),
companyTaxID: lodashGet(props, ['achData', 'companyTaxID'], ''),
incorporationType: lodashGet(props, ['achData', 'incorporationType'], ''),
incorporationDate: lodashGet(props, ['achData', 'incorporationDate'], ''),
- incorporationState: lodashGet(props, ['achData', 'incorporationState']) || 'AK',
+ incorporationState: lodashGet(props, ['achData', 'incorporationState']) || '',
industryCode: lodashGet(props, ['achData', 'industryCode'], ''),
hasNoConnectionToCannabis: lodashGet(props, ['achData', 'hasNoConnectionToCannabis'], false),
password: '',
@@ -61,6 +61,11 @@ class CompanyStep extends React.Component {
return false;
}
+ if (this.state.addressState === '') {
+ Growl.error(this.props.translate('bankAccount.error.addressState'));
+ return false;
+ }
+
if (!isValidZipCode(this.state.addressZipCode)) {
Growl.error(this.props.translate('bankAccount.error.zipCode'));
return false;
diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js
index 812926397c50..2287724daa9b 100644
--- a/src/pages/ReimbursementAccount/RequestorStep.js
+++ b/src/pages/ReimbursementAccount/RequestorStep.js
@@ -28,7 +28,7 @@ class RequestorStep extends React.Component {
lastName: lodashGet(props, ['achData', 'lastName'], ''),
requestorAddressStreet: lodashGet(props, ['achData', 'requestorAddressStreet'], ''),
requestorAddressCity: lodashGet(props, ['achData', 'requestorAddressCity'], ''),
- requestorAddressState: lodashGet(props, ['achData', 'requestorAddressState']) || 'AK',
+ requestorAddressState: lodashGet(props, ['achData', 'requestorAddressState']) || '',
requestorAddressZipCode: lodashGet(props, ['achData', 'requestorAddressZipCode'], ''),
dob: lodashGet(props, ['achData', 'dob'], ''),
ssnLast4: lodashGet(props, ['achData', 'ssnLast4'], ''),
@@ -60,6 +60,7 @@ class RequestorStep extends React.Component {
if (!isValidIdentity({
street: this.state.requestorAddressStreet,
+ state: this.state.requestorAddressState,
zipCode: this.state.requestorAddressZipCode,
dob: this.state.dob,
ssnLast4: this.state.ssnLast4,
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 50a6a8d65777..5c0d8748ca17 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -26,6 +26,7 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal
import {deleteReportComment} from '../../../libs/actions/Report';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import ControlSelection from '../../../libs/ControlSelection';
+import canUseTouchScreen from '../../../libs/canUseTouchscreen';
const propTypes = {
/** The ID of the report this action is on. */
@@ -272,7 +273,7 @@ class ReportActionItem extends Component {
<>
this.popoverAnchor = el}
- onPressIn={() => this.props.isSmallScreenWidth && ControlSelection.block()}
+ onPressIn={() => this.props.isSmallScreenWidth && canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
onSecondaryInteraction={this.showPopover}
>
diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js
index 0dda5b83372b..15241841116b 100644
--- a/src/pages/home/report/ReportActionItemFragment.js
+++ b/src/pages/home/report/ReportActionItemFragment.js
@@ -11,6 +11,7 @@ import Text from '../../../components/Text';
import Tooltip from '../../../components/Tooltip';
import {isSingleEmoji} from '../../../libs/ValidationUtils';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
+import canUseTouchScreen from '../../../libs/canUseTouchscreen';
const propTypes = {
/** The message fragment needing to be displayed */
@@ -65,7 +66,7 @@ class ReportActionItemFragment extends React.PureComponent {
/>
) : (
{Str.htmlDecode(fragment.text)}
diff --git a/src/pages/iou/steps/IOUAmountPage.js b/src/pages/iou/steps/IOUAmountPage.js
index 9cdb9c414abd..ff94c9b35998 100755
--- a/src/pages/iou/steps/IOUAmountPage.js
+++ b/src/pages/iou/steps/IOUAmountPage.js
@@ -17,7 +17,6 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal
import compose from '../../../libs/compose';
import Button from '../../../components/Button';
import Text from '../../../components/Text';
-import KeyboardShortcut from '../../../libs/KeyboardShortcut';
const propTypes = {
// Whether or not this IOU has multiple participants
@@ -77,15 +76,8 @@ class IOUAmountPage extends React.Component {
componentDidMount() {
// Component is not initialized yet due to navigation transitions
- // Wait until interactions are complete before trying to focus or attach listener
+ // Wait until interactions are complete before trying to focus
InteractionManager.runAfterInteractions(() => {
- // Setup and attach keypress handler for navigating to the next screen
- this.unsubscribe = KeyboardShortcut.subscribe('Enter', () => {
- if (this.state.amount !== '') {
- this.props.onStepComplete(this.state.amount);
- }
- }, [], true);
-
// Focus text input
if (this.textInput) {
this.textInput.focus();
@@ -93,15 +85,6 @@ class IOUAmountPage extends React.Component {
});
}
- componentWillUnmount() {
- // Cleanup all keydown event listeners that we've set up
- if (!this.unsubscribe) {
- return;
- }
-
- this.unsubscribe();
- }
-
/**
* Check if amount is a decimal upto 3 digits
*
@@ -197,6 +180,7 @@ class IOUAmountPage extends React.Component {
success
style={[styles.w100, styles.mt5]}
onPress={() => this.props.onStepComplete(this.state.amount)}
+ pressOnEnter
isDisabled={!this.state.amount.length || parseFloat(this.state.amount) < 0.01}
text={this.props.translate('common.next')}
/>
diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js
index 2b671c1f0325..6b3ee1b552bd 100755
--- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js
+++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
-import {getNewChatOptions} from '../../../../libs/OptionsListUtils';
+import {getNewChatOptions, isCurrentUser} from '../../../../libs/OptionsListUtils';
import OptionsSelector from '../../../../components/OptionsSelector';
import ONYXKEYS from '../../../../ONYXKEYS';
import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
@@ -90,7 +90,7 @@ class IOUParticipantsRequest extends Component {
indexOffset: 0,
});
- if (this.state.userToInvite) {
+ if (this.state.userToInvite && !isCurrentUser(this.state.userToInvite)) {
sections.push({
undefined,
data: [this.state.userToInvite],
diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
index b2a312a7a000..6b9c6e75dc46 100755
--- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
+++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
@@ -6,7 +6,7 @@ import {withOnyx} from 'react-native-onyx';
import ONYXKEYS from '../../../../ONYXKEYS';
import styles from '../../../../styles/styles';
import OptionsSelector from '../../../../components/OptionsSelector';
-import {getNewGroupOptions} from '../../../../libs/OptionsListUtils';
+import {getNewGroupOptions, isCurrentUser} from '../../../../libs/OptionsListUtils';
import CONST from '../../../../CONST';
import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
import compose from '../../../../libs/compose';
@@ -133,7 +133,7 @@ class IOUParticipantsSplit extends Component {
indexOffset: sections.reduce((prev, {data}) => prev + data.length, 0),
});
- if (this.state.userToInvite) {
+ if (this.state.userToInvite && !isCurrentUser(this.state.userToInvite)) {
sections.push(({
undefined,
data: [this.state.userToInvite],
diff --git a/src/pages/settings/Payments/AddPayPalMePage.js b/src/pages/settings/Payments/AddPayPalMePage.js
index e6601581b0bc..46928e99cd7b 100644
--- a/src/pages/settings/Payments/AddPayPalMePage.js
+++ b/src/pages/settings/Payments/AddPayPalMePage.js
@@ -92,7 +92,6 @@ class AddPayPalMePage extends React.Component {
value={this.state.payPalMeUsername}
placeholder={this.props.translate('addPayPalMePage.yourPayPalUsername')}
onChangeText={text => this.setState({payPalMeUsername: text})}
- editable={!this.props.payPalMeUsername}
returnKeyType="done"
/>
@@ -100,8 +99,8 @@ class AddPayPalMePage extends React.Component {
diff --git a/src/pages/settings/Payments/PaymentMethodList.js b/src/pages/settings/Payments/PaymentMethodList.js
index 8b6ea1d4d01b..0994ca80a31e 100644
--- a/src/pages/settings/Payments/PaymentMethodList.js
+++ b/src/pages/settings/Payments/PaymentMethodList.js
@@ -110,12 +110,17 @@ class PaymentMethodList extends Component {
});
}
- combinedPaymentMethods.push({
- title: this.props.translate('paymentMethodList.addPaymentMethod'),
- icon: Plus,
- onPress: e => this.props.onPress(e),
- key: 'addPaymentMethodButton',
- });
+ // Don't show Add Payment Method button if user provided details for all possible payment methods.
+ // Right now only available method is Paypal.me
+ // When there is a new payment method, it needs to be added to following if condition.
+ if (!this.props.payPalMeUsername) {
+ combinedPaymentMethods.push({
+ title: this.props.translate('paymentMethodList.addPaymentMethod'),
+ icon: Plus,
+ onPress: e => this.props.onPress(e),
+ key: 'addPaymentMethodButton',
+ });
+ }
return combinedPaymentMethods;
}
diff --git a/src/pages/settings/Payments/PaymentsPage.js b/src/pages/settings/Payments/PaymentsPage.js
index a3121567f8bf..7f2dc9deb8ad 100644
--- a/src/pages/settings/Payments/PaymentsPage.js
+++ b/src/pages/settings/Payments/PaymentsPage.js
@@ -1,5 +1,8 @@
import React from 'react';
import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import {withOnyx} from 'react-native-onyx';
+import ONYXKEYS from '../../../ONYXKEYS';
import PaymentMethodList from './PaymentMethodList';
import ROUTES from '../../../ROUTES';
import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton';
@@ -18,9 +21,16 @@ import getClickedElementLocation from '../../../libs/getClickedElementLocation';
const PAYPAL = 'payPalMe';
const propTypes = {
+ /** User's paypal.me username if they have one */
+ payPalMeUsername: PropTypes.string,
+
...withLocalizePropTypes,
};
+const defaultProps = {
+ payPalMeUsername: '',
+};
+
class PaymentsPage extends React.Component {
constructor(props) {
super(props);
@@ -48,7 +58,9 @@ class PaymentsPage extends React.Component {
*/
paymentMethodPressed(nativeEvent, account) {
if (account) {
- // TODO: Show the make default/delete popover
+ if (account === PAYPAL) {
+ Navigation.navigate(ROUTES.SETTINGS_ADD_PAYPAL_ME);
+ }
} else {
const position = getClickedElementLocation(nativeEvent);
this.setState({
@@ -104,11 +116,13 @@ class PaymentsPage extends React.Component {
left: this.state.anchorPositionLeft,
}}
>
- this.addPaymentMethodTypePressed(PAYPAL)}
- />
+ {!this.props.payPalMeUsername && (
+ this.addPaymentMethodTypePressed(PAYPAL)}
+ />
+ )}
@@ -117,8 +131,14 @@ class PaymentsPage extends React.Component {
}
PaymentsPage.propTypes = propTypes;
+PaymentsPage.defaultProps = defaultProps;
PaymentsPage.displayName = 'PaymentsPage';
export default compose(
withLocalize,
+ withOnyx({
+ payPalMeUsername: {
+ key: ONYXKEYS.NVP_PAYPAL_ME_ADDRESS,
+ },
+ }),
)(PaymentsPage);
diff --git a/src/pages/workspace/NewWorkspacePage.js b/src/pages/workspace/NewWorkspacePage.js
index 20d6739a70f9..c817cab3d155 100644
--- a/src/pages/workspace/NewWorkspacePage.js
+++ b/src/pages/workspace/NewWorkspacePage.js
@@ -64,6 +64,7 @@ class NewWorkspacePage extends React.Component {
label={this.props.translate('workspace.new.chooseAName')}
value={this.state.name}
onChangeText={name => this.setState({name})}
+ onSubmitEditting={this.submit}
/>
{this.props.translate('workspace.new.helpText')}
@@ -73,6 +74,7 @@ class NewWorkspacePage extends React.Component {
style={[styles.w100]}
text={this.props.translate('workspace.new.getStarted')}
onPress={this.submit}
+ pressOnEnter
/>
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js
index efb326e64db2..c4d96005e949 100644
--- a/src/pages/workspace/WorkspaceInvitePage.js
+++ b/src/pages/workspace/WorkspaceInvitePage.js
@@ -160,6 +160,7 @@ class WorkspaceInvitePage extends React.Component {
isDisabled={!this.state.emailOrPhone}
text={this.props.translate('common.invite')}
onPress={this.inviteUser}
+ pressOnEnter
/>