From aa35fa835c73839acf68c6b578bb86cfc504340c Mon Sep 17 00:00:00 2001
From: Katsuki <1313124+K4tsuki@users.noreply.github.com>
Date: Mon, 14 Feb 2022 14:30:37 +0700
Subject: [PATCH 1/2] Fix sync indicator
---
src/pages/home/sidebar/SidebarLinks.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index d31e1f9d0fd4..40c36ad8f850 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -235,7 +235,7 @@ class SidebarLinks extends React.Component {
@@ -295,6 +295,7 @@ export default compose(
},
isSyncingData: {
key: ONYXKEYS.IS_LOADING_AFTER_RECONNECT,
+ initWithStoredValues: false,
},
betas: {
key: ONYXKEYS.BETAS,
From 19fc3fcce532310fe27e78292606e1a631eb0ae5 Mon Sep 17 00:00:00 2001
From: Katsuki <1313124+K4tsuki@users.noreply.github.com>
Date: Thu, 3 Mar 2022 09:32:13 +0700
Subject: [PATCH 2/2] fix sync icon & sign commits
---
.env.production | 2 +-
.env.staging | 2 +-
.github/ISSUE_TEMPLATE/Standard.md | 13 +-
.github/PULL_REQUEST_TEMPLATE.md | 36 ++
.../actions/awaitStagingDeploys/action.yml | 3 +
.../awaitStagingDeploys.js | 18 +-
.github/actions/awaitStagingDeploys/index.js | 64 +++-
.../actions/getDeployPullRequestList/index.js | 17 +
.../actions/getPullRequestDetails/index.js | 17 +
.github/actions/getReleaseBody/index.js | 17 +
.../markPullRequestsAsDeployed/index.js | 17 +
.../actions/triggerWorkflowAndWait/index.js | 17 +
.github/libs/ActionUtils.js | 17 +
.../scripts/validateActionsAndWorkflows.sh | 31 +-
.github/workflows/cherryPick.yml | 8 +-
.github/workflows/createNewVersion.yml | 4 +-
.github/workflows/deploy.yml | 8 +-
.github/workflows/deployBlocker.yml | 8 +-
.github/workflows/finishReleaseCycle.yml | 2 +-
.github/workflows/lockDeploys.yml | 2 +-
.github/workflows/platformDeploy.yml | 14 +-
.github/workflows/preDeploy.yml | 55 ++-
.github/workflows/updateProtectedBranch.yml | 14 +-
.gitignore | 3 +
PR_REVIEW_GUIDELINES.md | 72 ++++
android/app/build.gradle | 4 +-
android/app/src/main/AndroidManifest.xml | 1 +
.../com/expensify/chat/MainApplication.java | 3 +
android/settings.gradle | 6 +
assets/images/key.svg | 8 +
config/electron.config.js | 34 --
.../electronBuilder/electronBuilder.config.js | 44 +++
config/webpack/productionConfig.js | 29 --
config/webpack/webpack.common.js | 43 ++-
config/webpack/webpack.desktop.js | 56 +++
config/webpack/webpack.dev.js | 27 +-
config/webpack/webpack.prod.js | 9 -
config/webpack/webpack.staging.js | 9 -
desktop/ELECTRON_ENVIRONMENT.js | 30 --
desktop/README.md | 21 +-
desktop/main.js | 63 ++--
desktop/notarize.js | 2 +-
desktop/package-lock.json | 320 ++++++++++++++++++
desktop/package.json | 15 +
desktop/start.js | 14 +-
.../xcshareddata/IDEWorkspaceChecks.plist | 8 +
ios/NewExpensify/Info.plist | 4 +-
ios/NewExpensifyTests/Info.plist | 4 +-
ios/Podfile | 2 +
ios/Podfile.lock | 16 +-
package-lock.json | 181 ++--------
package.json | 21 +-
scripts/build-desktop.sh | 27 ++
{build => scripts}/react-native-web.sh | 0
{tests/utils => scripts}/shellUtils.sh | 0
src/{CONST/index.js => CONST.js} | 28 +-
src/CONST/ENVIRONMENT.js | 5 -
src/ROUTES.js | 2 +
src/components/AddressSearch.js | 52 ++-
src/components/AttachmentModal.js | 2 +-
src/components/Avatar.js | 5 +
src/components/BigNumberPad.js | 1 +
src/components/Button.js | 19 +-
.../CustomStatusBar/index.android.js | 1 +
.../EmojiPicker/EmojiPickerButton.js | 51 +++
.../EmojiPicker/EmojiPickerMenu/index.js | 42 ++-
.../EmojiPickerMenu/index.native.js | 46 ++-
src/components/EmojiPicker/index.js | 187 ++++------
src/components/EnvironmentBadge.js | 8 +-
.../ErrorBoundary/BaseErrorBoundary.js | 2 +-
src/components/ErrorBoundary/index.native.js | 2 +-
src/components/FullNameInputRow.js | 2 +
src/components/Hoverable/index.native.js | 2 +-
src/components/Icon/Expensicons.js | 2 +
src/components/ImageWithSizeCalculation.js | 40 ++-
src/components/MultipleAvatars.js | 61 ++--
src/components/PopoverWithMeasuredContent.js | 23 +-
.../index.android.js | 60 ----
.../{index.ios.js => index.native.js} | 6 +-
src/components/RNTextInput.js | 41 +++
src/components/ReportActionItem/IOUPreview.js | 4 +-
src/components/ReportWelcomeText.js | 2 +-
src/components/RoomNameInput.js | 97 ++----
src/components/Text.js | 5 +-
src/components/TextInput/BaseTextInput.js | 8 +-
.../TextInput/baseTextInputPropTypes.js | 2 +-
src/components/TextInput/index.js | 8 +-
.../TextInputFocusable/index.android.js | 5 +-
.../TextInputFocusable/index.ios.js | 5 +-
src/components/TextInputFocusable/index.js | 6 +-
src/components/TextInputWithFocusStyles.js | 90 -----
src/components/TextInputWithName/index.js | 41 ---
.../TextInputWithName/index.native.js | 21 --
.../textInputWithNamepropTypes.js | 19 --
.../TextInputWithPrefix/index.android.js | 6 +-
src/components/TextInputWithPrefix/index.js | 6 +-
src/components/Tooltip/index.js | 4 +
src/components/Tooltip/tooltipPropTypes.js | 3 +-
.../WalletStatementModalPropTypes.js | 25 ++
src/components/WalletStatementModal/index.js | 54 +++
.../WalletStatementModal/index.native.js | 39 +++
src/languages/en.js | 9 +-
src/languages/es.js | 23 +-
src/libs/API.js | 8 +-
src/libs/FormUtils.js | 8 +-
src/libs/HapticFeedback/index.android.js | 18 +
src/libs/HapticFeedback/index.ios.js | 12 +
src/libs/HapticFeedback/index.js | 6 +
src/libs/HttpUtils.js | 23 +-
src/libs/Log.js | 3 +-
.../Navigation/AppNavigator/AuthScreens.js | 6 +
.../AppNavigator/ModalStackNavigators.js | 7 +
src/libs/Navigation/CustomActions.js | 52 +--
src/libs/Navigation/linkingConfig.js | 5 +
src/libs/Network.js | 21 +-
src/libs/ValidationUtils.js | 29 ++
src/libs/actions/App.js | 2 +-
src/libs/actions/EmojiPickerAction.js | 31 ++
src/libs/actions/PaymentMethods.js | 2 +-
src/libs/actions/PersonalDetails.js | 8 +-
src/libs/actions/Report.js | 19 +-
src/libs/actions/Session/index.js | 5 +-
src/libs/actions/SignInRedirect.js | 7 +-
src/libs/actions/WelcomeActions.js | 84 +++++
src/libs/checkForUpdates.js | 4 +-
src/libs/reportUtils.js | 11 +
.../EnablePayments/AdditionalDetailsStep.js | 25 +-
src/pages/ReportDetailsPage.js | 2 +-
src/pages/ReportSettingsPage.js | 84 ++++-
src/pages/RequestCallPage.js | 87 +++--
src/pages/home/HeaderView.js | 5 +-
src/pages/home/report/ReportActionCompose.js | 8 +-
.../home/report/ReportActionItemSingle.js | 11 +-
src/pages/home/report/ReportActionsView.js | 3 +
src/pages/home/sidebar/OptionRow.js | 3 +
src/pages/home/sidebar/SidebarLinks.js | 2 +-
src/pages/home/sidebar/SidebarScreen.js | 40 +--
src/pages/iou/IOUCurrencySelection.js | 8 +-
.../settings/Payments/PaymentMethodList.js | 38 ++-
.../Payments/PaymentsPage/BasePaymentsPage.js | 9 +-
src/pages/settings/Profile/ProfilePage.js | 68 ++--
.../settings/Security/CloseAccountPage.js | 9 +-
.../settings/Security/SecuritySettingsPage.js | 2 +-
src/pages/signin/SignInPageLayout/index.js | 23 +-
src/pages/wallet/WalletStatementPage.js | 62 ++++
src/pages/workspace/WorkspaceNewRoomPage.js | 78 ++++-
src/setup/index.js | 6 +
src/stories/AddressSearch.stories.js | 42 +++
src/stories/Form.stories.js | 9 +-
src/styles/StyleUtils.js | 15 +-
src/styles/colors.js | 1 +
src/styles/styles.js | 7 +
tests/unit/awaitStagingDeploysTest.js | 114 ++++++-
.../unit/getPullRequestsMergedBetweenTest.sh | 3 +-
154 files changed, 2529 insertions(+), 1240 deletions(-)
create mode 100644 PR_REVIEW_GUIDELINES.md
create mode 100644 assets/images/key.svg
delete mode 100644 config/electron.config.js
create mode 100644 config/electronBuilder/electronBuilder.config.js
delete mode 100644 config/webpack/productionConfig.js
create mode 100644 config/webpack/webpack.desktop.js
delete mode 100644 config/webpack/webpack.prod.js
delete mode 100644 config/webpack/webpack.staging.js
delete mode 100644 desktop/ELECTRON_ENVIRONMENT.js
create mode 100644 desktop/package-lock.json
create mode 100644 desktop/package.json
create mode 100644 ios/ExpensifyCash.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
create mode 100755 scripts/build-desktop.sh
rename {build => scripts}/react-native-web.sh (100%)
rename {tests/utils => scripts}/shellUtils.sh (100%)
rename src/{CONST/index.js => CONST.js} (97%)
delete mode 100644 src/CONST/ENVIRONMENT.js
create mode 100644 src/components/EmojiPicker/EmojiPickerButton.js
delete mode 100644 src/components/PressableWithSecondaryInteraction/index.android.js
rename src/components/PressableWithSecondaryInteraction/{index.ios.js => index.native.js} (88%)
create mode 100644 src/components/RNTextInput.js
delete mode 100644 src/components/TextInputWithFocusStyles.js
delete mode 100755 src/components/TextInputWithName/index.js
delete mode 100644 src/components/TextInputWithName/index.native.js
delete mode 100644 src/components/TextInputWithName/textInputWithNamepropTypes.js
create mode 100644 src/components/WalletStatementModal/WalletStatementModalPropTypes.js
create mode 100644 src/components/WalletStatementModal/index.js
create mode 100644 src/components/WalletStatementModal/index.native.js
create mode 100644 src/libs/HapticFeedback/index.android.js
create mode 100644 src/libs/HapticFeedback/index.ios.js
create mode 100644 src/libs/HapticFeedback/index.js
create mode 100644 src/libs/actions/EmojiPickerAction.js
create mode 100644 src/libs/actions/WelcomeActions.js
create mode 100644 src/pages/wallet/WalletStatementPage.js
create mode 100644 src/stories/AddressSearch.stories.js
diff --git a/.env.production b/.env.production
index 85c73146dd13..568ec690acfd 100644
--- a/.env.production
+++ b/.env.production
@@ -5,4 +5,4 @@ EXPENSIFY_PARTNER_NAME=chat-expensify-com
EXPENSIFY_PARTNER_PASSWORD=e21965746fd75f82bb66
PUSHER_APP_KEY=268df511a204fbb60884
USE_WEB_PROXY=false
-ENVIRONMENT=PROD
+ENVIRONMENT=production
diff --git a/.env.staging b/.env.staging
index 36e04bc26778..bd6b03805d56 100644
--- a/.env.staging
+++ b/.env.staging
@@ -5,4 +5,4 @@ EXPENSIFY_PARTNER_NAME=chat-expensify-com
EXPENSIFY_PARTNER_PASSWORD=e21965746fd75f82bb66
PUSHER_APP_KEY=268df511a204fbb60884
USE_WEB_PROXY=false
-ENVIRONMENT=STG
+ENVIRONMENT=staging
diff --git a/.github/ISSUE_TEMPLATE/Standard.md b/.github/ISSUE_TEMPLATE/Standard.md
index e753c1079a5e..932e77753a9a 100644
--- a/.github/ISSUE_TEMPLATE/Standard.md
+++ b/.github/ISSUE_TEMPLATE/Standard.md
@@ -20,7 +20,7 @@ Describe what actually happened
Can the user still use Expensify without this being fixed? Have you informed them of the workaround?
## Platform:
-
Where is this issue occurring?
@@ -31,13 +31,14 @@ Where is this issue occurring?
- Desktop App
- Mobile Web
-**Version Number:**
-**Reproducible in staging?:**
-**Reproducible in production?:**
+**Version Number:**
+**Reproducible in staging?:**
+**Reproducible in production?:**
+**Email or phone of affected tester (no customers):**
**Logs:** https://stackoverflow.com/c/expensify/questions/4856
**Notes/Photos/Videos:** Any additional supporting documentation
-**Expensify/Expensify Issue URL:**
-**Issue reported by:**
+**Expensify/Expensify Issue URL:**
+**Issue reported by:**
**Slack conversation:**
[View all open jobs on GitHub](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22)
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 7857da1475eb..8b930c692841 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -31,6 +31,42 @@ For example:
- [ ] Verify that no errors appear in the JS console
+### PR Review Checklist
+
+#### Contributor (PR Author) Checklist
+- [ ] I made sure to pull `main` before submitting my PR for review
+- [ ] I linked the correct issue in the `### Fixed Issues` section above
+- [ ] I wrote clear testing steps that cover the changes made in this PR
+ - [ ] I clearly indicated the environment tests should be run in (Staging vs Production)
+- [ ] I wrote testing steps that cover success & fail scenarios (if applicable)
+- [ ] I ran the tests & they passed on **all platforms**
+- [ ] I included screenshots or videos for tests on [all platforms](https://github.com/Expensify/App/blob/main/CONTRIBUTING.md#make-sure-you-can-test-on-all-platforms)
+- [ ] I verified there are no console errors related to changes in this PR
+- [ ] I followed proper code patterns (see [Reviewing the code](https://github.com/Expensify/App/blob/main/PR_REVIEW_GUIDELINES.md#reviewing-the-code))
+ - [ ] I added comments when the code was not self explanatory
+ - [ ] I put all copy / text shown in the product in all `src/languages/*` files (if applicable)
+ - [ ] I followed proper naming convention for platform-specific files (if applicable)
+ - [ ] I followed style guidelines (in [`Styling.md`](https://github.com/Expensify/App/blob/main/STYLING.md)) for all style edits I made
+- [ ] I followed the guidelines as stated in the [Review Guidelines](https://github.com/Expensify/App/blob/main/PR_REVIEW_GUIDELINES.md)
+
+#### PR Reviewer Checklist
+- [ ] I verified the Author pulled `main` before submitting the PR
+- [ ] I verified the correct issue is linked in the `### Fixed Issues` section above
+- [ ] I verified testing steps are clear and they cover the changes made in this PR
+ - [ ] I verified the testing environment is mentioned in the test steps
+- [ ] I verified testing steps cover success & fail scenarios (if applicable)
+- [ ] I verified tests pass on **all platforms** & I tested again on all platforms
+- [ ] I checked that screenshots or videos are included for tests on [all platforms](https://github.com/Expensify/App/blob/main/CONTRIBUTING.md#make-sure-you-can-test-on-all-platforms)
+- [ ] I verified there are no console errors related to changes in this PR
+- [ ] I verified proper code patterns were followed (see [Reviewing the code](https://github.com/Expensify/App/blob/main/PR_REVIEW_GUIDELINES.md#reviewing-the-code))
+ - [ ] I verified comments were added when the code was not self explanatory
+ - [ ] I verified any copy / text shown in the product was added in all `src/languages/*` files (if applicable)
+ - [ ] I verified proper naming convention for platform-specific files was followed (if applicable)
+ - [ ] I verified [style guidelines](https://github.com/Expensify/App/blob/main/STYLING.md) were followed
+- [ ] I verified that this PR follows the guidelines as stated in the [Review Guidelines](https://github.com/Expensify/App/blob/main/PR_REVIEW_GUIDELINES.md)
+
### QA Steps
+
diff --git a/config/electron.config.js b/config/electron.config.js
deleted file mode 100644
index 14079e6e9a36..000000000000
--- a/config/electron.config.js
+++ /dev/null
@@ -1,34 +0,0 @@
-const ENVIRONMENT = require('../src/CONST/ENVIRONMENT');
-
-module.exports = {
- appId: 'com.expensifyreactnative.chat',
- productName: 'New Expensify',
- extraMetadata: {
- main: './desktop/main.js',
- electronEnvironment: process.env.SHOULD_DEPLOY_PRODUCTION ? ENVIRONMENT.PRODUCTION : ENVIRONMENT.STAGING,
- },
- mac: {
- category: 'public.app-category.finance',
- icon: process.env.SHOULD_DEPLOY_PRODUCTION === 'true' ? './desktop/icon.png' : './desktop/icon-stg.png',
- hardenedRuntime: true,
- entitlements: 'desktop/entitlements.mac.plist',
- entitlementsInherit: 'desktop/entitlements.mac.plist',
- type: 'distribution',
- },
- dmg: {
- title: 'New Expensify',
- artifactName: 'NewExpensify.dmg',
- internetEnabled: true,
- },
- publish: [{
- provider: 's3',
- bucket: process.env.SHOULD_DEPLOY_PRODUCTION === 'true' ? 'expensify-cash' : 'staging-expensify-cash',
- channel: 'latest',
- }],
- files: [
- './dist/**/*',
- './desktop/*.js',
- './src/libs/checkForUpdates.js',
- './src/CONST/ENVIRONMENT.js',
- ],
-};
diff --git a/config/electronBuilder/electronBuilder.config.js b/config/electronBuilder/electronBuilder.config.js
new file mode 100644
index 000000000000..f2455ad0428d
--- /dev/null
+++ b/config/electronBuilder/electronBuilder.config.js
@@ -0,0 +1,44 @@
+const {version} = require('../../package.json');
+
+const isStaging = process.env.ELECTRON_ENV === 'staging';
+const isPublishing = process.argv.includes('--publish');
+
+/**
+ * The configuration for the production and staging Electron builds.
+ * It can be used to create local builds of the same, by omitting the `--publish` flag
+ */
+module.exports = {
+ appId: 'com.expensifyreactnative.chat',
+ productName: 'New Expensify',
+ extraMetadata: {
+ version,
+ },
+ mac: {
+ category: 'public.app-category.finance',
+ target: [
+ {target: 'dmg', arch: ['x64', 'arm64', 'universal']},
+ ],
+ icon: isStaging ? './desktop/icon-stg.png' : './desktop/icon.png',
+ hardenedRuntime: true,
+ entitlements: 'desktop/entitlements.mac.plist',
+ entitlementsInherit: 'desktop/entitlements.mac.plist',
+ type: 'distribution',
+ },
+ dmg: {
+ internetEnabled: true,
+ },
+ publish: [{
+ provider: 's3',
+ bucket: isStaging ? 'staging-expensify-cash' : 'expensify-cash',
+ channel: 'latest',
+ }],
+ afterSign: isPublishing ? './desktop/notarize.js' : undefined,
+ files: [
+ 'dist',
+ '!dist/www/{.well-known,favicon*}',
+ ],
+ directories: {
+ app: 'desktop',
+ output: 'desktop-build',
+ },
+};
diff --git a/config/webpack/productionConfig.js b/config/webpack/productionConfig.js
deleted file mode 100644
index 594d616ac423..000000000000
--- a/config/webpack/productionConfig.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const webpack = require('webpack');
-const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
-
-/**
- * Get the production webpack configuration, given an environment object.
- *
- * @param {Object} env
- * @returns {Object}
- */
-function getProductionConfig(env) {
- return ({
- mode: 'production',
- devtool: 'source-map',
- plugins: [
- // This allows us to interactively inspect JS bundle contents
- ...(process.env.ANALYZE_BUNDLE === 'true' ? [new BundleAnalyzerPlugin()] : []),
- new webpack.DefinePlugin({
- __REACT_WEB_CONFIG__: JSON.stringify(env),
-
- // React Native JavaScript environment requires the global __DEV__ variable to be accessible.
- // react-native-render-html uses variable to log exclusively during development.
- // See https://reactnative.dev/docs/javascript-environment
- __DEV__: false,
- }),
- ],
- });
-}
-
-module.exports = getProductionConfig;
diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js
index e2d3ca0974af..1b856a3039c7 100644
--- a/config/webpack/webpack.common.js
+++ b/config/webpack/webpack.common.js
@@ -1,22 +1,18 @@
-const _ = require('underscore');
const path = require('path');
-const {IgnorePlugin} = require('webpack');
+const {IgnorePlugin, DefinePlugin} = require('webpack');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
+const dotenv = require('dotenv');
+const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
const CustomVersionFilePlugin = require('./CustomVersionFilePlugin');
-// Check for a --platform command line argument (default to 'web')
-// If it is 'web', we want to ignore .desktop.js files, and if it is 'desktop', we want to ignore .website.js files.
-const platformIndex = _.findIndex(process.argv, arg => arg === '--platform');
-const platform = (platformIndex > 0) ? process.argv[platformIndex + 1] : 'web';
-const platformExclude = platform === 'web' ? new RegExp(/\.desktop\.js$/) : new RegExp(/\.website\.js$/);
-
const includeModules = [
'react-native-animatable',
'react-native-reanimated',
'react-native-picker-select',
'react-native-web',
+ 'react-native-webview',
'@react-native-picker',
'react-native-modal',
'react-native-onyx',
@@ -25,7 +21,16 @@ const includeModules = [
'react-native-google-places-autocomplete',
].join('|');
-const webpackConfig = {
+/**
+ * Get a production grade config for web or desktop
+ * @param {Object} env
+ * @param {String} env.envFile path to the env file to be used
+ * @param {'web'|'desktop'} env.platform
+ * @returns {Configuration}
+ */
+const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({
+ mode: 'production',
+ devtool: 'source-map',
entry: {
app: './index.js',
},
@@ -59,7 +64,20 @@ const webpackConfig = {
],
}),
new IgnorePlugin(/^\.\/locale$/, /moment$/),
- new CustomVersionFilePlugin(),
+ ...(platform === 'web' ? [new CustomVersionFilePlugin()] : []),
+ new DefinePlugin({
+ __REACT_WEB_CONFIG__: JSON.stringify(
+ dotenv.config({path: envFile}).parsed,
+ ),
+
+ // React Native JavaScript environment requires the global __DEV__ variable to be accessible.
+ // react-native-render-html uses variable to log exclusively during development.
+ // See https://reactnative.dev/docs/javascript-environment
+ __DEV__: /staging|prod/.test(envFile) === false,
+ }),
+
+ // This allows us to interactively inspect JS bundle contents
+ ...(process.env.ANALYZE_BUNDLE === 'true' ? [new BundleAnalyzerPlugin()] : []),
],
module: {
rules: [
@@ -78,7 +96,6 @@ const webpackConfig = {
*/
exclude: [
new RegExp(`node_modules/(?!(${includeModules})/).*|.native.js$`),
- platformExclude,
],
},
{
@@ -86,7 +103,6 @@ const webpackConfig = {
loader: 'eslint-loader',
exclude: [
/node_modules|\.native\.js$/,
- platformExclude,
],
options: {
cache: false,
@@ -142,8 +158,9 @@ const webpackConfig = {
// without this, web will try to use native implementations and break in not very obvious ways.
// This is also why we have to use .website.js for our own web-specific files...
// Because desktop also relies on "web-specific" module implementations
+ // This also skips packing web only dependencies to desktop and vice versa
extensions: ['.web.js', (platform === 'web') ? '.website.js' : '.desktop.js', '.js', '.jsx'],
},
-};
+});
module.exports = webpackConfig;
diff --git a/config/webpack/webpack.desktop.js b/config/webpack/webpack.desktop.js
new file mode 100644
index 000000000000..298741eed498
--- /dev/null
+++ b/config/webpack/webpack.desktop.js
@@ -0,0 +1,56 @@
+const path = require('path');
+const webpack = require('webpack');
+const _ = require('underscore');
+
+const desktopDependencies = require('../../desktop/package.json').dependencies;
+const getCommonConfig = require('./webpack.common');
+
+/**
+ * Desktop creates 2 configurations in parallel
+ * 1. electron-main - the core that serves the app content
+ * 2. web - the app content that would be rendered in electron
+ * Everything is placed in desktop/dist and ready for packaging
+ * @param {Object} env
+ * @returns {webpack.Configuration[]}
+ */
+module.exports = (env) => {
+ const rendererConfig = getCommonConfig({...env, platform: 'desktop'});
+ const outputPath = path.resolve(__dirname, '../../desktop/dist');
+
+ rendererConfig.name = 'renderer';
+ rendererConfig.output.path = path.join(outputPath, 'www');
+
+ // Expose react-native-config to desktop-main
+ const definePlugin = _.find(rendererConfig.plugins, plugin => plugin.constructor === webpack.DefinePlugin);
+
+ const mainProcessConfig = {
+ mode: 'production',
+ name: 'desktop-main',
+ target: 'electron-main',
+ entry: {
+ main: './desktop/main.js',
+ contextBridge: './desktop/contextBridge.js',
+ },
+ output: {
+ filename: '[name].js',
+ path: outputPath,
+ libraryTarget: 'commonjs2',
+ },
+ resolve: rendererConfig.resolve,
+ plugins: [definePlugin],
+ externals: [
+ ..._.keys(desktopDependencies),
+ 'fsevents',
+ ],
+ node: {
+ /**
+ * Disables webpack processing of __dirname and __filename, so it works like in node
+ * https://github.com/webpack/webpack/issues/2010
+ */
+ __dirname: false,
+ __filename: false,
+ },
+ };
+
+ return [mainProcessConfig, rendererConfig];
+};
diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.js
index e32304af8aac..0c46975435cc 100644
--- a/config/webpack/webpack.dev.js
+++ b/config/webpack/webpack.dev.js
@@ -1,12 +1,13 @@
const path = require('path');
-const webpack = require('webpack');
const {merge} = require('webpack-merge');
-const dotenv = require('dotenv');
-const common = require('./webpack.common');
+const getCommonConfig = require('./webpack.common');
-const env = dotenv.config({path: path.resolve(__dirname, '../../.env')}).parsed;
-
-module.exports = () => {
+/**
+ * Configuration for the local dev server
+ * @param {Object} env
+ * @returns {Configuration}
+ */
+module.exports = (env = {}) => {
// Check if the USE_WEB_PROXY variable has been provided
// and rewrite any requests to the local proxy server
const proxySettings = process.env.USE_WEB_PROXY === 'false'
@@ -18,7 +19,9 @@ module.exports = () => {
},
};
- return merge(common, {
+ const baseConfig = getCommonConfig(env);
+
+ return merge(baseConfig, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
@@ -27,15 +30,5 @@ module.exports = () => {
...proxySettings,
historyApiFallback: true,
},
- plugins: [
- new webpack.DefinePlugin({
- __REACT_WEB_CONFIG__: JSON.stringify(env),
-
- // React Native JavaScript environment requires the global __DEV__ variable to be accessible.
- // react-native-render-html uses variable to log exclusively during development.
- // See https://reactnative.dev/docs/javascript-environment
- __DEV__: true,
- }),
- ],
});
};
diff --git a/config/webpack/webpack.prod.js b/config/webpack/webpack.prod.js
deleted file mode 100644
index dfd7c852ab99..000000000000
--- a/config/webpack/webpack.prod.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const path = require('path');
-const {merge} = require('webpack-merge');
-const dotenv = require('dotenv');
-const common = require('./webpack.common');
-const getProductionConfig = require('./productionConfig');
-
-const env = dotenv.config({path: path.resolve(__dirname, '../../.env.production')}).parsed;
-
-module.exports = merge(common, getProductionConfig(env));
diff --git a/config/webpack/webpack.staging.js b/config/webpack/webpack.staging.js
deleted file mode 100644
index 24f706d90731..000000000000
--- a/config/webpack/webpack.staging.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const path = require('path');
-const {merge} = require('webpack-merge');
-const dotenv = require('dotenv');
-const common = require('./webpack.common');
-const getProductionConfig = require('./productionConfig');
-
-const env = dotenv.config({path: path.resolve(__dirname, '../../.env.staging')}).parsed;
-
-module.exports = merge(common, getProductionConfig(env));
diff --git a/desktop/ELECTRON_ENVIRONMENT.js b/desktop/ELECTRON_ENVIRONMENT.js
deleted file mode 100644
index 698e14b43078..000000000000
--- a/desktop/ELECTRON_ENVIRONMENT.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// This variable is injected into package.json by electron-builder via the extraMetadata field (specified in electron.config.js)
-// It will be `PROD` on production, `STG` on staging, and `undefined` on dev (because dev doesn't use electron-builder)
-const {electronEnvironment} = require('../package.json');
-const ENVIRONMENT = require('../src/CONST/ENVIRONMENT');
-
-/**
- * @returns {String} – One of ['PROD', 'STG', 'DEV']
- */
-function getEnvironment() {
- // If we are on dev, then the NODE_ENV environment variable will be present (set by the executing shell in start.js)
- if (process.env.NODE_ENV === 'development') {
- return ENVIRONMENT.DEV;
- }
-
- // Otherwise, use the environment injected into package.json by electron-builder
- return electronEnvironment;
-}
-
-function isDev() {
- return getEnvironment() === ENVIRONMENT.DEV;
-}
-
-function isProd() {
- return getEnvironment() === ENVIRONMENT.PRODUCTION;
-}
-
-module.exports = {
- isDev,
- isProd,
-};
diff --git a/desktop/README.md b/desktop/README.md
index 85d2d7273028..87ca4dc36b24 100644
--- a/desktop/README.md
+++ b/desktop/README.md
@@ -23,6 +23,7 @@
* [Architecture](#architecture)
* [Testing Electron Auto-Update](#testing-electron-auto-update)
+* [Packaging](#packaging)
# Architecture
The New Expensify desktop app is built using [Electron.js](https://www.electronjs.org/). We try our best to maintain Electron best practices, particularly when it comes to [security](https://www.electronjs.org/docs/latest/tutorial/security).
@@ -87,8 +88,7 @@ mc policy set public electron-builder/electron-builder
Once you have Min.IO setup and running, the next step is to temporarily revert some changes from https://github.com/Expensify/App/commit/b640b3010fd7a40783d1c04faf4489836e98038d, specifically
1. Update the `desktop-build` command in package.json to add `--publish always` at the end
-2. Update electron.config.js to re-add `afterSign: 'desktop/notarize.js',`
-3. Update electron.config.js to replace the `publish` value with the following:
+2. Update electronBuilder.config.js to replace the `publish` value with the following:
```
publish: [{
provider: 's3',
@@ -122,3 +122,20 @@ AWS_ACCESS_KEY_ID=RootUserKey AWS_SECRET_ACCESS_KEY=RootPassKey APPLE_ID=YOUR_AP
This command will create a build, notarize it, and push your build to the server. Note that it can take around 10 minutes for the command to complete.
Once the command finishes, revert the version update in `package.json`, remove `--publish always` from the `desktop-build` command, and again run the `npm run desktop-build` command above **including the args**. After the build is done, you'll find `NewExpensify.dmg` in the `dist/` folder in the root of the project. Open the `.dmg` and install the app. Your app will attempt to auto-update in the background.
+
+# Packaging
+To avoid bundling unnecessary `node_modules` we use a [2 package structure](https://www.electron.build/tutorials/two-package-structure)
+The root [package.json](../package.json) serves for `devDependencies` and shared (renderer) `dependencies`
+The [desktop/package.json](./package.json) serves for desktop (electron-main) specific dependencies
+We use Webpack with a [desktop specific config](../config/webpack/webpack.desktop.js) to bundle our js code
+Half of the config takes care of packaging root package dependencies - everything related to rendering App in the Electron window. Packaged under `dist/www`
+The other half is about bundling the `main.js` script which initializes Electron and renders `www`
+
+## See what is getting packaged in the app
+If you suspect unnecessary items might be getting packaged you can inspect the package content in `desktop-build/`
+The app content (`dist/www`) is archived under `/New\ Expensify.app/Contents/Resources/app.asar`
+To see the actual `app.asar` content run the following script
+```shell
+npx asar extract desktop-build/mac/New\ Expensify.app/Contents/Resources/app.asar ./unpacked-asar
+```
+The expected size of `app.asar` = `desktop/dist/www/` + `desktop/node_modules/`;
diff --git a/desktop/main.js b/desktop/main.js
index 43962026e227..fc1bd8ff10ea 100644
--- a/desktop/main.js
+++ b/desktop/main.js
@@ -11,9 +11,9 @@ const serve = require('electron-serve');
const contextMenu = require('electron-context-menu');
const {autoUpdater} = require('electron-updater');
const log = require('electron-log');
-const ELECTRON_ENVIRONMENT = require('./ELECTRON_ENVIRONMENT');
const ELECTRON_EVENTS = require('./ELECTRON_EVENTS');
const checkForUpdates = require('../src/libs/checkForUpdates');
+const CONFIG = require('../src/CONFIG').default;
const port = process.env.PORT || 8080;
@@ -40,17 +40,6 @@ autoUpdater.logger.transports.file.level = 'info';
// See https://www.npmjs.com/package/electron-log
_.assign(console, log.functions);
-// setup Hot reload
-if (ELECTRON_ENVIRONMENT.isDev()) {
- try {
- require('electron-reloader')(module, {
- watchRenderer: false,
- ignore: [/^(desktop)/],
- });
- // eslint-disable-next-line no-empty
- } catch {}
-}
-
// This sets up the command line arguments used to manage the update. When
// the --expected-update-version flag is set, the app will open pre-hidden
// until it detects that it has been upgraded to the correct version.
@@ -124,13 +113,14 @@ const electronUpdater = browserWindow => ({
});
const mainWindow = (() => {
- const loadURL = ELECTRON_ENVIRONMENT.isDev()
+ const loadURL = __DEV__
? win => win.loadURL(`http://localhost:${port}`)
- : serve({directory: `${__dirname}/../dist`});
+ : serve({directory: `${__dirname}/www`});
// Prod and staging set the icon in the electron-builder config, so only update it here for dev
- if (ELECTRON_ENVIRONMENT.isDev()) {
- app.dock.setIcon(`${__dirname}/icon-dev.png`);
+ if (__DEV__) {
+ console.debug('CONFIG: ', CONFIG);
+ app.dock.setIcon(`${__dirname}/../icon-dev.png`);
app.setName('New Expensify');
}
@@ -157,43 +147,32 @@ const mainWindow = (() => {
* 1. Modify headers on any outgoing requests to match the origin of our corresponding web environment (not necessary in case of web proxy, because it already does that)
* 2. Modify the Access-Control-Allow-Origin header of the response to match the "real" origin of our Electron app.
*/
+ const webRequest = browserWindow.webContents.session.webRequest;
const validDestinationFilters = {urls: ['https://*.expensify.com/*']};
- if (!ELECTRON_ENVIRONMENT.isDev()) {
- const newDotURL = ELECTRON_ENVIRONMENT.isProd() ? 'https://new.expensify.com' : 'https://staging.new.expensify.com';
-
+ /* eslint-disable no-param-reassign */
+ if (!__DEV__) {
// Modify the origin and referer for requests sent to our API
- browserWindow.webContents.session.webRequest.onBeforeSendHeaders(validDestinationFilters, (details, callback) => {
- // eslint-disable-next-line no-param-reassign
- details.requestHeaders.origin = newDotURL;
- // eslint-disable-next-line no-param-reassign
- details.requestHeaders.referer = newDotURL;
+ webRequest.onBeforeSendHeaders(validDestinationFilters, (details, callback) => {
+ details.requestHeaders.origin = CONFIG.EXPENSIFY.URL_EXPENSIFY_CASH;
+ details.requestHeaders.referer = CONFIG.EXPENSIFY.URL_EXPENSIFY_CASH;
callback({requestHeaders: details.requestHeaders});
});
// Modify access-control-allow-origin header for the response
- browserWindow.webContents.session.webRequest.onHeadersReceived(validDestinationFilters, (details, callback) => {
- // eslint-disable-next-line no-param-reassign
+ webRequest.onHeadersReceived(validDestinationFilters, (details, callback) => {
details.responseHeaders['access-control-allow-origin'] = ['app://-'];
callback({responseHeaders: details.responseHeaders});
});
+ } else {
+ webRequest.onHeadersReceived(validDestinationFilters, (details, callback) => {
+ details.responseHeaders['access-control-allow-origin'] = [`http://localhost:${process.env.PORT}`];
+ callback({responseHeaders: details.responseHeaders});
+ });
}
-
- if (ELECTRON_ENVIRONMENT.isDev()) {
- const dotenv = require('dotenv');
- const path = require('path');
- const devEnvConfig = dotenv.config({path: path.resolve(__dirname, '../.env')}).parsed;
-
- if (devEnvConfig.USE_WEB_PROXY === 'true') {
- browserWindow.webContents.session.webRequest.onHeadersReceived(validDestinationFilters, (details, callback) => {
- // eslint-disable-next-line no-param-reassign
- details.responseHeaders['access-control-allow-origin'] = ['http://localhost:8080'];
- callback({responseHeaders: details.responseHeaders});
- });
- }
- }
+ /* eslint-enable */
// Prod and staging overwrite the app name in the electron-builder config, so only update it here for dev
- if (ELECTRON_ENVIRONMENT.isDev()) {
+ if (__DEV__) {
browserWindow.setTitle('New Expensify');
}
@@ -331,7 +310,7 @@ const mainWindow = (() => {
// Start checking for JS updates
.then((browserWindow) => {
- if (ELECTRON_ENVIRONMENT.isDev()) {
+ if (__DEV__) {
return;
}
diff --git a/desktop/notarize.js b/desktop/notarize.js
index 20897a4fc7ac..70ee4a893356 100644
--- a/desktop/notarize.js
+++ b/desktop/notarize.js
@@ -1,5 +1,5 @@
const {notarize} = require('electron-notarize');
-const electron = require('../config/electron.config');
+const electron = require('../config/electronBuilder/electronBuilder.config');
exports.default = function notarizing(context) {
const {electronPlatformName, appOutDir} = context;
diff --git a/desktop/package-lock.json b/desktop/package-lock.json
new file mode 100644
index 000000000000..99cc9074ef19
--- /dev/null
+++ b/desktop/package-lock.json
@@ -0,0 +1,320 @@
+{
+ "name": "new.expensify.desktop",
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "@types/semver": {
+ "version": "7.3.9",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
+ "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ=="
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ },
+ "astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="
+ },
+ "builder-util-runtime": {
+ "version": "8.9.2",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.9.2.tgz",
+ "integrity": "sha512-rhuKm5vh7E0aAmT6i8aoSfEjxzdYEFX7zDApK+eNgOhjofnWb74d9SRJv0H/8nsgOkos0TZ4zxW0P8J4N7xQ2A==",
+ "requires": {
+ "debug": "^4.3.2",
+ "sax": "^1.2.4"
+ }
+ },
+ "cli-truncate": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
+ "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
+ "requires": {
+ "slice-ansi": "^3.0.0",
+ "string-width": "^4.2.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "debug": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+ "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "electron-context-menu": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.5.2.tgz",
+ "integrity": "sha512-1cEQR6fA9ktFsRBc+eXPwvrOgAPytUD7rUV4iBAA5zTrLAPKokJ23xeMjcK2fjrDPrlFRBxcLz0KP+GUhMrSCQ==",
+ "requires": {
+ "cli-truncate": "^2.1.0",
+ "electron-dl": "^3.1.0",
+ "electron-is-dev": "^1.2.0"
+ }
+ },
+ "electron-dl": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.3.0.tgz",
+ "integrity": "sha512-Zwaz/OMGPIfBLV2SQH4sTsdDOs/U4y5AOHfremMBXEpjIxX+SiTx845DZAvJJwgb5hfowyWOBLiJhd/emBNLLQ==",
+ "requires": {
+ "ext-name": "^5.0.0",
+ "pupa": "^2.0.1",
+ "unused-filename": "^2.1.0"
+ }
+ },
+ "electron-is-dev": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-1.2.0.tgz",
+ "integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw=="
+ },
+ "electron-log": {
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.6.tgz",
+ "integrity": "sha512-nirYgRdY+F+vclr8ijdwy2vW03IzFpDHTaKNWu76dEN21Y76+smcES5knS7cgHUUB0qNLOi8vZO36taakjbSXA=="
+ },
+ "electron-serve": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/electron-serve/-/electron-serve-1.1.0.tgz",
+ "integrity": "sha512-tQJBCbXKoKCfkBC143QCqnEtT1s8dNE2V+b/82NF6lxnGO/2Q3a3GSLHtKl3iEDQgdzTf9pH7p418xq2rXbz1Q=="
+ },
+ "electron-updater": {
+ "version": "4.6.5",
+ "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.6.5.tgz",
+ "integrity": "sha512-kdTly8O9mSZfm9fslc1mnCY+mYOeaYRy7ERa2Fed240u01BKll3aiupzkd07qKw69KvhBSzuHroIW3mF0D8DWA==",
+ "requires": {
+ "@types/semver": "^7.3.6",
+ "builder-util-runtime": "8.9.2",
+ "fs-extra": "^10.0.0",
+ "js-yaml": "^4.1.0",
+ "lazy-val": "^1.0.5",
+ "lodash.escaperegexp": "^4.1.2",
+ "lodash.isequal": "^4.5.0",
+ "semver": "^7.3.5"
+ }
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "escape-goat": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
+ "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q=="
+ },
+ "ext-list": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
+ "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==",
+ "requires": {
+ "mime-db": "^1.28.0"
+ }
+ },
+ "ext-name": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz",
+ "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==",
+ "requires": {
+ "ext-list": "^2.0.0",
+ "sort-keys-length": "^1.0.0"
+ }
+ },
+ "fs-extra": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
+ "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==",
+ "requires": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.9",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
+ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+ },
+ "is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
+ },
+ "js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "requires": {
+ "argparse": "^2.0.1"
+ }
+ },
+ "jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "requires": {
+ "graceful-fs": "^4.1.6",
+ "universalify": "^2.0.0"
+ }
+ },
+ "lazy-val": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
+ "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="
+ },
+ "lodash.escaperegexp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+ "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c="
+ },
+ "lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "mime-db": {
+ "version": "1.51.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+ "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
+ },
+ "modify-filename": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
+ "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE="
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
+ },
+ "pupa": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
+ "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
+ "requires": {
+ "escape-goat": "^2.0.0"
+ }
+ },
+ "sax": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "slice-ansi": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
+ "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ }
+ },
+ "sort-keys": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
+ "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
+ "requires": {
+ "is-plain-obj": "^1.0.0"
+ }
+ },
+ "sort-keys-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
+ "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=",
+ "requires": {
+ "sort-keys": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
+ },
+ "unused-filename": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-2.1.0.tgz",
+ "integrity": "sha512-BMiNwJbuWmqCpAM1FqxCTD7lXF97AvfQC8Kr/DIeA6VtvhJaMDupZ82+inbjl5yVP44PcxOuCSxye1QMS0wZyg==",
+ "requires": {
+ "modify-filename": "^1.1.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ }
+ }
+}
diff --git a/desktop/package.json b/desktop/package.json
new file mode 100644
index 000000000000..926e39b5ce95
--- /dev/null
+++ b/desktop/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "new.expensify.desktop",
+ "description": "Desktop package native dependencies",
+ "main": "dist/main.js",
+ "scripts": {},
+ "dependencies": {
+ "electron-context-menu": "^2.3.0",
+ "electron-log": "^4.3.5",
+ "electron-serve": "^1.0.0",
+ "electron-updater": "^4.3.4"
+ },
+ "author": "Expensify, Inc.",
+ "license": "MIT",
+ "private": true
+}
diff --git a/desktop/start.js b/desktop/start.js
index cf28e08ed310..d3d26d4de08a 100644
--- a/desktop/start.js
+++ b/desktop/start.js
@@ -1,24 +1,30 @@
#!/usr/bin/env node
const portfinder = require('portfinder');
const concurrently = require('concurrently');
+require('dotenv').config();
const basePort = 8080;
portfinder.getPortPromise({
port: basePort,
}).then((port) => {
+ const devServer = `webpack-dev-server --config config/webpack/webpack.dev.js --port ${port} --env.platform desktop`;
+ const buildMain = 'webpack --config config/webpack/webpack.desktop.js --config-name desktop-main --mode=development';
+
const processes = [
{
- command: `webpack-dev-server --config config/webpack/webpack.dev.js --port ${port} --platform desktop`,
+ command: devServer,
name: 'Renderer',
prefixColor: 'red.dim',
-
},
{
- command: `wait-port localhost:${port} && export NODE_ENV=development PORT=${port} \
- && electron desktop/main.js`,
+ command: `${buildMain} && wait-port localhost:${port} && electron desktop/dist/main.js`,
name: 'Main',
prefixColor: 'cyan.dim',
+ env: {
+ PORT: port,
+ NODE_ENV: 'development',
+ },
},
];
concurrently(processes, {
diff --git a/ios/ExpensifyCash.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/ExpensifyCash.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 000000000000..18d981003d68
--- /dev/null
+++ b/ios/ExpensifyCash.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 8912f6cecde7..62faebbc0b09 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.1.38
+ 1.1.40
CFBundleSignature
????
CFBundleURLTypes
@@ -31,7 +31,7 @@
CFBundleVersion
- 1.1.38.3
+ 1.1.40.0
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index d4d030ed5edf..0f0f5e6358dc 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.1.38
+ 1.1.40
CFBundleSignature
????
CFBundleVersion
- 1.1.38.3
+ 1.1.40.0
diff --git a/ios/Podfile b/ios/Podfile
index e2c5f53e6b95..6ddfb790f587 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -17,6 +17,8 @@ target 'NewExpensify' do
:hermes_enabled => true
)
+ pod 'react-native-webview', :path => '../node_modules/react-native-webview'
+
target 'NewExpensifyTests' do
inherit! :complete
# Pods for testing
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 6939287b873f..fe5225951dcc 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -460,6 +460,8 @@ PODS:
- React
- react-native-safe-area-context (3.1.8):
- React-Core
+ - react-native-webview (11.17.2):
+ - React-Core
- React-perflogger (0.66.4)
- React-RCTActionSheet (0.66.4):
- React-Core/RCTActionSheetHeaders (= 0.66.4)
@@ -668,6 +670,7 @@ DEPENDENCIES:
- "react-native-progress-bar-android (from `../node_modules/@react-native-community/progress-bar-android`)"
- "react-native-progress-view (from `../node_modules/@react-native-community/progress-view`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
+ - react-native-webview (from `../node_modules/react-native-webview`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
@@ -806,6 +809,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-community/progress-view"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
+ react-native-webview:
+ :path: "../node_modules/react-native-webview"
React-perflogger:
:path: "../node_modules/react-native/ReactCommon/reactperflogger"
React-RCTActionSheet:
@@ -926,9 +931,9 @@ SPEC CHECKSUMS:
React-jsinspector: d0374f7509d407d2264168b6d0fad0b54e300b85
React-logger: 933f80c97c633ee8965d609876848148e3fef438
react-native-config: 6502b1879f97ed5ac570a029961fc35ea606cd14
- react-native-document-picker: 429972f7ece4463aa5bcdd789622b3a674a3c5d1
- react-native-flipper: 169e8ba429b73ad637ce007337ce4b415e783799
- react-native-image-picker: 4089335b89b625d4e34d53fb249c48a7a791b3ea
+ react-native-document-picker: 772d04a4bc5c35da9abe27b08ac271420ae3f9ef
+ react-native-flipper: cd9eabd8917104c1bbdca2621717cdca3b2addef
+ react-native-image-picker: f45729c43d4f854508ab25c0d0f0f711a2a8a267
react-native-netinfo: 3a61a486f2329f5884753fd5bab21450a535d97c
react-native-pdf: 4b5a9e4465a6a3b399e91dc4838eb44ddf716d1f
react-native-performance: 8edfa2bbc9a2af4a02f01d342118e413a95145e0
@@ -936,6 +941,7 @@ SPEC CHECKSUMS:
react-native-progress-bar-android: ce95a69f11ac580799021633071368d08aaf9ad8
react-native-progress-view: 5816e8a6be812c2b122c6225a2a3db82d9008640
react-native-safe-area-context: 01158a92c300895d79dee447e980672dc3fb85a6
+ react-native-webview: 77ee909f73e1fcab76380f7dcc3344771fe61bd8
React-perflogger: 93075d8931c32cd1fce8a98c15d2d5ccc4d891bd
React-RCTActionSheet: 7d3041e6761b4f3044a37079ddcb156575fb6d89
React-RCTAnimation: 743e88b55ac62511ae5c2e22803d4f503f2a3a13
@@ -971,6 +977,6 @@ SPEC CHECKSUMS:
Yoga: e7dc4e71caba6472ff48ad7d234389b91dadc280
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
-PODFILE CHECKSUM: 46dab5ec14a7bc81989e2f3e8cd4bba211deddbe
+PODFILE CHECKSUM: 1199d07d3ccd1ea23300ad9e27bc9287716223bf
-COCOAPODS: 1.11.2
+COCOAPODS: 1.10.1
diff --git a/package-lock.json b/package-lock.json
index 713cd80a22e1..fda48d0e4030 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.1.38-3",
+ "version": "1.1.40-0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -15283,11 +15283,6 @@
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
- "@types/semver": {
- "version": "7.3.4",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz",
- "integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ=="
- },
"@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
@@ -16947,11 +16942,6 @@
"integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=",
"dev": true
},
- "astral-regex": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
- "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="
- },
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
@@ -16985,7 +16975,8 @@
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
- "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true
},
"atob": {
"version": "2.1.2",
@@ -18687,15 +18678,6 @@
}
}
},
- "builder-util-runtime": {
- "version": "8.7.2",
- "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.7.2.tgz",
- "integrity": "sha512-xBqv+8bg6cfnzAQK1k3OGpfaHg+QkPgIgpEkXNhouZ0WiUkyZCftuRc2LYzQrLucFywpa14Xbc6+hTbpq83yRA==",
- "requires": {
- "debug": "^4.1.1",
- "sax": "^1.2.4"
- }
- },
"builtin-status-codes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
@@ -19346,15 +19328,6 @@
"string-width": "^4.2.0"
}
},
- "cli-truncate": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
- "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
- "requires": {
- "slice-ansi": "^3.0.0",
- "string-width": "^4.2.0"
- }
- },
"cli-width": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
@@ -21928,35 +21901,11 @@
}
}
},
- "electron-context-menu": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.3.0.tgz",
- "integrity": "sha512-XYsYkNY+jvX4C5o09qMuZoKL6e9frnQzBFehZSIiKp6zK0u3XYowJYDyK3vDKKZxYsOIGiE/Gbx40jERC03Ctw==",
- "requires": {
- "cli-truncate": "^2.0.0",
- "electron-dl": "^3.0.0",
- "electron-is-dev": "^1.0.1"
- }
- },
- "electron-dl": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.0.2.tgz",
- "integrity": "sha512-pRgE9Jbhoo5z6Vk3qi+vIrfpMDlCp2oB1UeR96SMnsfz073jj0AZGQwp69EdIcEvlUlwBSGyJK8Jt6OB6JLn+g==",
- "requires": {
- "ext-name": "^5.0.0",
- "pupa": "^2.0.1",
- "unused-filename": "^2.1.0"
- }
- },
"electron-is-dev": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-1.2.0.tgz",
- "integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw=="
- },
- "electron-log": {
- "version": "4.3.5",
- "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.3.5.tgz",
- "integrity": "sha512-J5Ew3axdk7W4jzzxKLSAi1sqbcAoo9CzHuBVsG0tT47j256xKulNrWFf3lZmHJ1KDXOQUcuwOngQF0jjmpEdpw=="
+ "integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==",
+ "dev": true
},
"electron-notarize": {
"version": "1.0.0",
@@ -22216,31 +22165,12 @@
}
}
},
- "electron-serve": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/electron-serve/-/electron-serve-1.0.0.tgz",
- "integrity": "sha512-Rsm4tjj1eK7NUWKgGw6NjHkjfB+bIXZh0ztybUYzqmwCm1wzb7zv95LERbwricDZfCsKHB0V57NgVvHdi2OOAQ=="
- },
"electron-to-chromium": {
"version": "1.3.820",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.820.tgz",
"integrity": "sha512-5cFwDmo2yzEA9hn55KZ9+cX/b6DSFvpKz8Hb2fiDmriXWB+DBoXKXmncQwNRFBBTlUdsvPHCoy594OoMLAO0Tg==",
"dev": true
},
- "electron-updater": {
- "version": "4.3.5",
- "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.3.5.tgz",
- "integrity": "sha512-5jjN7ebvfj1cLI0VZMdCnJk6aC4bP+dy7ryBf21vArR0JzpRVk0OZHA2QBD+H5rm6ZSeDYHOY6+8PrMEqJ4wlQ==",
- "requires": {
- "@types/semver": "^7.3.1",
- "builder-util-runtime": "8.7.2",
- "fs-extra": "^9.0.1",
- "js-yaml": "^3.14.0",
- "lazy-val": "^1.0.4",
- "lodash.isequal": "^4.5.0",
- "semver": "^7.3.2"
- }
- },
"element-resize-detector": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.2.4.tgz",
@@ -22538,7 +22468,8 @@
"escape-goat": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
- "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q=="
+ "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
+ "dev": true
},
"escape-html": {
"version": "1.0.3",
@@ -24123,8 +24054,8 @@
}
},
"expensify-common": {
- "version": "git+https://github.com/Expensify/expensify-common.git#2d0f8f2f3424fea79854ea10d050824b5bd0c27b",
- "from": "git+https://github.com/Expensify/expensify-common.git#2d0f8f2f3424fea79854ea10d050824b5bd0c27b",
+ "version": "git+https://github.com/Expensify/expensify-common.git#f77bb4710c13d01153716df7fb087b637ba3b8bd",
+ "from": "git+https://github.com/Expensify/expensify-common.git#f77bb4710c13d01153716df7fb087b637ba3b8bd",
"requires": {
"classnames": "2.3.1",
"clipboard": "2.0.4",
@@ -24255,23 +24186,6 @@
}
}
},
- "ext-list": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
- "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==",
- "requires": {
- "mime-db": "^1.28.0"
- }
- },
- "ext-name": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz",
- "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==",
- "requires": {
- "ext-list": "^2.0.0",
- "sort-keys-length": "^1.0.0"
- }
- },
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -25076,6 +24990,7 @@
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz",
"integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==",
+ "dev": true,
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
@@ -26891,11 +26806,6 @@
"path-is-inside": "^1.0.2"
}
},
- "is-plain-obj": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
- "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
- },
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -30757,6 +30667,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz",
"integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==",
+ "dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^1.0.0"
@@ -30872,7 +30783,8 @@
"lazy-val": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.4.tgz",
- "integrity": "sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q=="
+ "integrity": "sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q==",
+ "dev": true
},
"lcid": {
"version": "1.0.0",
@@ -33793,11 +33705,6 @@
"resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz",
"integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ=="
},
- "modify-filename": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
- "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE="
- },
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
@@ -36039,14 +35946,6 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
- "pupa": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz",
- "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==",
- "requires": {
- "escape-goat": "^2.0.0"
- }
- },
"pusher-js": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-7.0.0.tgz",
@@ -37347,6 +37246,22 @@
"version": "git+https://github.com/Expensify/react-native-web.git#3cd81b5af5916dca1dbdfef6b4e9ad261772afe6",
"from": "git+https://github.com/Expensify/react-native-web.git#3cd81b5af5916dca1dbdfef6b4e9ad261772afe6"
},
+ "react-native-webview": {
+ "version": "11.17.2",
+ "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-11.17.2.tgz",
+ "integrity": "sha512-7Sac02xq11qFACJmSUuCnH0aUFtSWUvSRC09EZ2qwNXq4IvT05xlX6978nlKUXf2ljw/0qZIzqbKzuXnu6Wq8Q==",
+ "requires": {
+ "escape-string-regexp": "2.0.0",
+ "invariant": "2.2.4"
+ },
+ "dependencies": {
+ "escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="
+ }
+ }
+ },
"react-pdf": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.2.0.tgz",
@@ -39288,16 +39203,6 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="
},
- "slice-ansi": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
- "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
- "requires": {
- "ansi-styles": "^4.0.0",
- "astral-regex": "^2.0.0",
- "is-fullwidth-code-point": "^3.0.0"
- }
- },
"smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@@ -39507,22 +39412,6 @@
}
}
},
- "sort-keys": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
- "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
- "requires": {
- "is-plain-obj": "^1.0.0"
- }
- },
- "sort-keys-length": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
- "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=",
- "requires": {
- "sort-keys": "^1.0.0"
- }
- },
"source-list-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -41748,7 +41637,8 @@
"universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
- "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug=="
+ "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
+ "dev": true
},
"unpipe": {
"version": "1.0.0",
@@ -41797,15 +41687,6 @@
}
}
},
- "unused-filename": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-2.1.0.tgz",
- "integrity": "sha512-BMiNwJbuWmqCpAM1FqxCTD7lXF97AvfQC8Kr/DIeA6VtvhJaMDupZ82+inbjl5yVP44PcxOuCSxye1QMS0wZyg==",
- "requires": {
- "modify-filename": "^1.1.0",
- "path-exists": "^4.0.0"
- }
- },
"upath": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
diff --git a/package.json b/package.json
index af2ff72d806d..ce25848df9c3 100644
--- a/package.json
+++ b/package.json
@@ -1,25 +1,25 @@
{
"name": "new.expensify",
- "version": "1.1.38-3",
+ "version": "1.1.40-0",
"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.",
"license": "MIT",
"private": true,
"scripts": {
- "postinstall": "./build/react-native-web.sh",
+ "postinstall": "scripts/react-native-web.sh && cd desktop && npm install",
"clean": "react-native clean-project-auto",
"android": "npm run check-metro-bundler-port && react-native run-android",
"ios": "npm run check-metro-bundler-port && react-native run-ios",
"ipad": "npm run check-metro-bundler-port && react-native run-ios --simulator=\"iPad Pro (12.9-inch) (4th generation)\"",
"ipad-sm": "npm run check-metro-bundler-port && react-native run-ios --simulator=\"iPad Pro (9.7-inch)\"",
- "desktop": "node desktop/start.js",
"start": "react-native start",
"web": "node web/proxy.js & webpack-dev-server --open --config config/webpack/webpack.dev.js",
- "build": "webpack --config config/webpack/webpack.prod.js",
- "build-staging": "webpack --config config/webpack/webpack.staging.js",
- "desktop-build": "webpack --config config/webpack/webpack.prod.js --platform desktop && electron-builder --config config/electron.config.js",
- "desktop-build-staging": "webpack --config config/webpack/webpack.staging.js --platform desktop && electron-builder --config config/electron.config.js",
+ "build": "webpack --config config/webpack/webpack.common.js --env.envFile=.env.production",
+ "build-staging": "webpack --config config/webpack/webpack.common.js --env.envFile=.env.staging",
+ "desktop": "node desktop/start.js",
+ "desktop-build": "scripts/build-desktop.sh production",
+ "desktop-build-staging": "scripts/build-desktop.sh staging",
"ios-build": "fastlane ios build",
"android-build": "fastlane android build",
"test": "jest",
@@ -61,11 +61,7 @@
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
"dotenv": "^8.2.0",
- "electron-context-menu": "^2.3.0",
- "electron-log": "^4.3.5",
- "electron-serve": "^1.0.0",
- "electron-updater": "^4.3.4",
- "expensify-common": "git+https://github.com/Expensify/expensify-common.git#2d0f8f2f3424fea79854ea10d050824b5bd0c27b",
+ "expensify-common": "git+https://github.com/Expensify/expensify-common.git#f77bb4710c13d01153716df7fb087b637ba3b8bd",
"fbjs": "^3.0.2",
"file-loader": "^6.0.0",
"html-entities": "^1.3.1",
@@ -106,6 +102,7 @@
"react-native-screens": "^3.10.1",
"react-native-svg": "^12.1.0",
"react-native-web": "git+https://github.com/Expensify/react-native-web.git#3cd81b5af5916dca1dbdfef6b4e9ad261772afe6",
+ "react-native-webview": "^11.17.2",
"react-pdf": "^5.2.0",
"react-plaid-link": "^3.2.0",
"react-web-config": "^1.0.0",
diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh
new file mode 100755
index 000000000000..caacef993025
--- /dev/null
+++ b/scripts/build-desktop.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+set -e
+
+ELECTRON_ENV=${1:-development}
+
+if [[ "$ELECTRON_ENV" == "staging" ]]; then
+ ENV_FILE=".env.staging"
+elif [[ "$ELECTRON_ENV" == "production" ]]; then
+ ENV_FILE=".env.production"
+else
+ ENV_FILE=".env"
+fi
+
+SCRIPTS_DIR=$(dirname "${BASH_SOURCE[0]}")
+LOCAL_PACKAGES=$(npm bin)
+source "$SCRIPTS_DIR/shellUtils.sh";
+
+title "Bundling Desktop js Bundle Using Webpack"
+info " • ELECTRON_ENV: $ELECTRON_ENV"
+info " • ENV file: $ENV_FILE"
+info ""
+"$LOCAL_PACKAGES/webpack" --config config/webpack/webpack.desktop.js --env.envFile=$ENV_FILE
+
+title "Building Desktop App Archive Using Electron"
+info ""
+shift 1
+"$LOCAL_PACKAGES/electron-builder" --config config/electronBuilder/electronBuilder.config.js "$@"
diff --git a/build/react-native-web.sh b/scripts/react-native-web.sh
similarity index 100%
rename from build/react-native-web.sh
rename to scripts/react-native-web.sh
diff --git a/tests/utils/shellUtils.sh b/scripts/shellUtils.sh
similarity index 100%
rename from tests/utils/shellUtils.sh
rename to scripts/shellUtils.sh
diff --git a/src/CONST/index.js b/src/CONST.js
similarity index 97%
rename from src/CONST/index.js
rename to src/CONST.js
index 8d401264cb8d..d169485906e4 100755
--- a/src/CONST/index.js
+++ b/src/CONST.js
@@ -1,9 +1,9 @@
import lodashGet from 'lodash/get';
import Config from 'react-native-config';
-import ENVIRONMENT from './ENVIRONMENT';
-import * as Url from '../libs/Url';
+import * as Url from './libs/Url';
const CLOUDFRONT_URL = 'https://d2k5nsl2zxldvw.cloudfront.net';
+const USE_EXPENSIFY_URL = 'https://use.expensify.com';
const ACTIVE_ENVIRONMENT_NEW_EXPENSIFY_URL = Url.addTrailingForwardSlash(lodashGet(Config, 'EXPENSIFY_URL_CASH', 'https://new.expensify.com'));
const PLATFORM_OS_MACOS = 'Mac OS';
const ANDROID_PACKAGE_NAME = 'com.expensify.chat';
@@ -185,6 +185,7 @@ const CONST = {
},
CONCIERGE_CHAT_NAME: 'Concierge',
CLOUDFRONT_URL,
+ USE_EXPENSIFY_URL,
NEW_ZOOM_MEETING_URL: 'https://zoom.us/start/videomeeting',
NEW_GOOGLE_MEET_MEETING_URL: 'https://meet.google.com/new',
DEEPLINK_BASE_URL: 'new-expensify://',
@@ -192,13 +193,13 @@ const CONST = {
EXPENSIFY_ICON_URL: `${CLOUDFRONT_URL}/images/favicon-2019.png`,
UPWORK_URL: 'https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22',
GITHUB_URL: 'https://github.com/Expensify/App',
- TERMS_URL: 'https://use.expensify.com/terms',
- PRIVACY_URL: 'https://use.expensify.com/privacy',
- LICENSES_URL: 'https://use.expensify.com/licenses',
+ TERMS_URL: `${USE_EXPENSIFY_URL}/terms`,
+ PRIVACY_URL: `${USE_EXPENSIFY_URL}/privacy`,
+ LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`,
PLAY_STORE_URL: `https://play.google.com/store/apps/details?id=${ANDROID_PACKAGE_NAME}&hl=en`,
ADD_SECONDARY_LOGIN_URL: encodeURI('settings?param={"section":"account","openModal":"secondaryLogin"}'),
MANAGE_CARDS_URL: 'domain_companycards',
- FEES_URL: 'https://use.expensify.com/fees',
+ FEES_URL: `${USE_EXPENSIFY_URL}/fees`,
CFPB_PREPAID_URL: 'https://cfpb.gov/prepaid',
STAGING_SECURE_URL: 'https://staging-secure.expensify.com/',
NEWDOT: 'new.expensify.com',
@@ -297,6 +298,7 @@ const CONST = {
ERROR: {
API_OFFLINE: 'session.offlineMessageRetry',
UNKNOWN_ERROR: 'Unknown error',
+ REQUEST_CANCELLED: 'AbortError',
},
NETWORK: {
METHOD: {
@@ -366,7 +368,10 @@ const CONST = {
ADD_PAYMENT_MENU_POSITION_Y: 226,
ADD_PAYMENT_MENU_POSITION_X: 356,
- EMOJI_PICKER_SIZE: 320,
+ EMOJI_PICKER_SIZE: {
+ WIDTH: 320,
+ HEIGHT: 400,
+ },
NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 300,
EMOJI_PICKER_ITEM_HEIGHT: 40,
EMOJI_PICKER_HEADER_HEIGHT: 38,
@@ -391,6 +396,12 @@ const CONST = {
ADMIN: 'admin@expensify.com',
},
+ ENVIRONMENT: {
+ DEV: 'development',
+ STAGING: 'staging',
+ PRODUCTION: 'production',
+ },
+
// Used to delay the initial fetching of reportActions when the app first inits or reconnects (e.g. returning
// from backgound). The times are based on how long it generally seems to take for the app to become interactive
// in each scenario.
@@ -526,6 +537,7 @@ const CONST = {
ROLE: {
ADMIN: 'admin',
},
+ ROOM_PREFIX: '#',
},
TERMS: {
@@ -609,6 +621,4 @@ const CONST = {
},
};
-CONST.ENVIRONMENT = ENVIRONMENT;
-
export default CONST;
diff --git a/src/CONST/ENVIRONMENT.js b/src/CONST/ENVIRONMENT.js
deleted file mode 100644
index dd742b0f8245..000000000000
--- a/src/CONST/ENVIRONMENT.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- DEV: 'DEV',
- STAGING: 'STG',
- PRODUCTION: 'PROD',
-};
diff --git a/src/ROUTES.js b/src/ROUTES.js
index c71b311767cd..54b27b273347 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -89,6 +89,8 @@ export default {
// This is a special validation URL that will take the user to /workspace/new after validation. This is used
// when linking users from e.com in order to share a session in this app.
ENABLE_PAYMENTS: 'enable-payments',
+ WALLET_STATEMENT_WITH_DATE: 'statements/:yearMonth',
+ getWalletStatementWithDateRoute: yearMonth => `statements/${yearMonth}`,
WORKSPACE_NEW: 'workspace/new',
WORKSPACE_INITIAL: 'workspace/:policyID',
WORKSPACE_INVITE: 'workspace/:policyID/invite',
diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch.js
index 2cb3df16232f..dac2f8cd7c8d 100644
--- a/src/components/AddressSearch.js
+++ b/src/components/AddressSearch.js
@@ -9,6 +9,7 @@ import styles from '../styles/styles';
import TextInput from './TextInput';
import Log from '../libs/Log';
import * as GooglePlacesUtils from '../libs/GooglePlacesUtils';
+import * as FormUtils from '../libs/FormUtils';
// The error that's being thrown below will be ignored until we fork the
// react-native-google-places-autocomplete repo and replace the
@@ -16,6 +17,26 @@ import * as GooglePlacesUtils from '../libs/GooglePlacesUtils';
LogBox.ignoreLogs(['VirtualizedLists should never be nested']);
const propTypes = {
+ /** Indicates that the input is being used with the Form component */
+ isFormInput: PropTypes.bool,
+
+ /**
+ * The ID used to uniquely identify the input
+ *
+ * @param {Object} props - props passed to the input
+ * @returns {Object} - returns an Error object if isFormInput is supplied but inputID is falsey or not a string
+ */
+ inputID: props => FormUtils.validateInputIDProps(props),
+
+ /** Saves a draft of the input value when used in a form */
+ shouldSaveDraft: PropTypes.bool,
+
+ /** Callback that is called when the text input is blurred */
+ onBlur: PropTypes.func,
+
+ /** Error text to display */
+ errorText: PropTypes.string,
+
/** The label to display for the field */
label: PropTypes.string.isRequired,
@@ -32,7 +53,12 @@ const propTypes = {
};
const defaultProps = {
- value: '',
+ isFormInput: false,
+ inputID: undefined,
+ shouldSaveDraft: false,
+ onBlur: () => {},
+ errorText: '',
+ value: undefined,
containerStyles: [],
};
@@ -66,7 +92,7 @@ const AddressSearch = (props) => {
const state = GooglePlacesUtils.getAddressComponent(addressComponents, 'administrative_area_level_1', 'short_name');
const values = {};
- if (street && street.length > props.value.trim().length) {
+ if (street && props.value && street.length > props.value.trim().length) {
// We are only passing the street number and name if the combined length is longer than the value
// that was initially passed to the autocomplete component. Google Places can truncate details
// like Apt # and this is the best way we have to tell that the new value it's giving us is less
@@ -111,10 +137,27 @@ const AddressSearch = (props) => {
}}
textInputProps={{
InputComp: TextInput,
+ ref: (node) => {
+ if (!props.innerRef) {
+ return;
+ }
+
+ if (_.isFunction(props.innerRef)) {
+ props.innerRef(node);
+ return;
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ props.innerRef.current = node;
+ },
label: props.label,
containerStyles: props.containerStyles,
errorText: props.errorText,
value: props.value,
+ isFormInput: props.isFormInput,
+ inputID: props.inputID,
+ shouldSaveDraft: props.shouldSaveDraft,
+ onBlur: props.onBlur,
onChangeText: (text) => {
if (skippedFirstOnChangeTextRef.current) {
props.onChange({street: text});
@@ -160,4 +203,7 @@ const AddressSearch = (props) => {
AddressSearch.propTypes = propTypes;
AddressSearch.defaultProps = defaultProps;
-export default withLocalize(AddressSearch);
+export default withLocalize(React.forwardRef((props, ref) => (
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+)));
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index 1fc8d10c5022..0d0b80417a93 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -139,7 +139,7 @@ class AttachmentModal extends PureComponent {
? addEncryptedAuthTokenToURL(this.state.sourceURL)
: this.state.sourceURL;
- const attachmentViewStyles = this.props.isSmallScreenWidth
+ const attachmentViewStyles = this.props.isSmallScreenWidth || this.props.isMediumScreenWidth
? [styles.imageModalImageCenterContainer]
: [styles.imageModalImageCenterContainer, styles.p5];
diff --git a/src/components/Avatar.js b/src/components/Avatar.js
index 228ed6fb23a4..0e6aa6a381e6 100644
--- a/src/components/Avatar.js
+++ b/src/components/Avatar.js
@@ -2,6 +2,7 @@ import React, {PureComponent} from 'react';
import {Image, View} from 'react-native';
import PropTypes from 'prop-types';
import styles from '../styles/styles';
+import themeColors from '../styles/themes/default';
import RoomAvatar from './RoomAvatar';
import stylePropTypes from '../styles/stylePropTypes';
@@ -42,8 +43,12 @@ class Avatar extends PureComponent {
const imageStyle = [
this.props.size === 'small' ? styles.avatarSmall : styles.avatarNormal,
+
+ // Background color isn't added for room avatar because it changes it's shape to a square
+ this.props.isChatRoom ? {} : {backgroundColor: themeColors.icon},
...this.props.imageStyles,
];
+
return (
{this.props.isChatRoom
diff --git a/src/components/BigNumberPad.js b/src/components/BigNumberPad.js
index 604eab5b0662..0ebb6d86ed24 100644
--- a/src/components/BigNumberPad.js
+++ b/src/components/BigNumberPad.js
@@ -59,6 +59,7 @@ class BigNumberPad extends React.Component {
return (
@@ -476,6 +496,14 @@ EmojiPickerMenu.defaultProps = defaultProps;
export default compose(
withWindowDimensions,
withLocalize,
+ withOnyx({
+ preferredSkinTone: {
+ key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
+ },
+ frequentlyUsedEmojis: {
+ key: ONYXKEYS.FREQUENTLY_USED_EMOJIS,
+ },
+ }),
)(React.forwardRef((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
index 1a8d1017af8d..7b69f0e114f1 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.native.js
@@ -1,9 +1,11 @@
import React, {Component} from 'react';
import {View, FlatList} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import compose from '../../../libs/compose';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../withWindowDimensions';
import CONST from '../../../CONST';
+import ONYXKEYS from '../../../ONYXKEYS';
import styles from '../../../styles/styles';
import emojis from '../../../../assets/emojis';
import EmojiPickerMenuItem from '../EmojiPickerMenuItem';
@@ -11,6 +13,7 @@ import Text from '../../Text';
import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
import EmojiSkinToneList from '../EmojiSkinToneList';
import * as EmojiUtils from '../../../libs/EmojiUtils';
+import * as User from '../../../libs/actions/User';
const propTypes = {
/** Function to add the selected emoji to the main compose text input */
@@ -19,9 +22,6 @@ const propTypes = {
/** Stores user's preferred skin tone */
preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- /** Function to sync the selected skin tone with parent, onyx and nvp */
- updatePreferredSkinTone: PropTypes.func,
-
/** User's frequently used emojis */
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
@@ -36,6 +36,10 @@ const propTypes = {
...withLocalizePropTypes,
};
+const defaultProps = {
+ preferredSkinTone: undefined,
+};
+
class EmojiPickerMenu extends Component {
constructor(props) {
super(props);
@@ -56,8 +60,17 @@ class EmojiPickerMenu extends Component {
this.renderItem = this.renderItem.bind(this);
this.isMobileLandscape = this.isMobileLandscape.bind(this);
+ this.updatePreferredSkinTone = this.updatePreferredSkinTone.bind(this);
}
+ /**
+ * @param {String} emoji
+ * @param {Object} emojiObject
+ */
+ addToFrequentAndSelectEmoji(emoji, emojiObject) {
+ EmojiUtils.addToFrequentlyUsedEmojis(this.props.frequentlyUsedEmojis, emojiObject);
+ this.props.onEmojiSelected(emoji);
+ }
/**
* Check if its a landscape mode of mobile device
@@ -68,6 +81,16 @@ class EmojiPickerMenu extends Component {
return this.props.windowWidth >= this.props.windowHeight;
}
+ /**
+ * @param {Number} skinTone
+ */
+ updatePreferredSkinTone(skinTone) {
+ if (this.props.preferredSkinTone === skinTone) {
+ return;
+ }
+
+ User.setPreferredSkinTone(skinTone);
+ }
/**
* Given an emoji item object, render a component based on its type.
@@ -98,7 +121,7 @@ class EmojiPickerMenu extends Component {
return (
this.props.onEmojiSelected(emoji, item)}
+ onPress={emoji => this.addToFrequentAndSelectEmoji(emoji, item)}
emoji={emojiCode}
/>
);
@@ -120,7 +143,7 @@ class EmojiPickerMenu extends Component {
stickyHeaderIndices={this.unfilteredHeaderIndices}
/>
@@ -129,14 +152,19 @@ class EmojiPickerMenu extends Component {
}
EmojiPickerMenu.propTypes = propTypes;
-EmojiPickerMenu.defaultProps = {
- preferredSkinTone: undefined,
- updatePreferredSkinTone: undefined,
-};
+EmojiPickerMenu.defaultProps = defaultProps;
export default compose(
withWindowDimensions,
withLocalize,
+ withOnyx({
+ preferredSkinTone: {
+ key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
+ },
+ frequentlyUsedEmojis: {
+ key: ONYXKEYS.FREQUENTLY_USED_EMOJIS,
+ },
+ }),
)(React.forwardRef((props, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
diff --git a/src/components/EmojiPicker/index.js b/src/components/EmojiPicker/index.js
index b7811bc14a53..8331d5842300 100644
--- a/src/components/EmojiPicker/index.js
+++ b/src/components/EmojiPicker/index.js
@@ -1,45 +1,9 @@
import React from 'react';
-import PropTypes from 'prop-types';
-import {Pressable, Dimensions} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
+import {Dimensions} from 'react-native';
import _ from 'underscore';
-import compose from '../../libs/compose';
-import Popover from '../Popover';
-import Tooltip from '../Tooltip';
-import Icon from '../Icon';
-import ONYXKEYS from '../../ONYXKEYS';
import EmojiPickerMenu from './EmojiPickerMenu';
-import * as StyleUtils from '../../styles/StyleUtils';
-import * as Expensicons from '../Icon/Expensicons';
-import * as User from '../../libs/actions/User';
-import * as EmojiUtils from '../../libs/EmojiUtils';
-import getButtonState from '../../libs/getButtonState';
-import styles from '../../styles/styles';
import CONST from '../../CONST';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
-import withLocalize, {withLocalizePropTypes} from '../withLocalize';
-
-const propTypes = {
- /** Flag to disable the emoji picker button */
- isDisabled: PropTypes.bool,
-
- /** Callback on emoji popover hide */
- onModalHide: PropTypes.func,
-
- /** Callback on before showing emoji picker */
- onBeforeShowEmojiPicker: PropTypes.func,
-
- /** Callback on emoji selection */
- onEmojiSelected: PropTypes.func.isRequired,
- ...windowDimensionsPropTypes,
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- isDisabled: false,
- onModalHide: () => {},
- onBeforeShowEmojiPicker: () => {},
-};
+import PopoverWithMeasuredContent from '../PopoverWithMeasuredContent';
class EmojiPicker extends React.Component {
constructor(props) {
@@ -49,8 +13,10 @@ class EmojiPicker extends React.Component {
this.showEmojiPicker = this.showEmojiPicker.bind(this);
this.selectEmoji = this.selectEmoji.bind(this);
this.measureEmojiPopoverAnchorPosition = this.measureEmojiPopoverAnchorPosition.bind(this);
- this.setPreferredSkinTone = this.setPreferredSkinTone.bind(this);
this.focusEmojiSearchInput = this.focusEmojiSearchInput.bind(this);
+ this.measureContent = this.measureContent.bind(this);
+ this.onModalHide = () => {};
+ this.onEmojiSelected = () => {};
this.state = {
isEmojiPickerVisible: false,
@@ -74,39 +40,36 @@ class EmojiPicker extends React.Component {
this.emojiPopoverDimensionListener.remove();
}
- /**
- * Update preferredSkinTone and sync with Onyx, NVP.
- * @param {Number|String} skinTone
- */
- setPreferredSkinTone(skinTone) {
- if (skinTone === this.props.preferredSkinTone) {
- return;
- }
-
- User.setPreferredSkinTone(skinTone);
- }
-
/**
* Callback for the emoji picker to add whatever emoji is chosen into the main input
*
* @param {String} emoji
- * @param {Object} emojiObject
*/
- selectEmoji(emoji, emojiObject) {
- EmojiUtils.addToFrequentlyUsedEmojis(this.props.frequentlyUsedEmojis, emojiObject);
+ selectEmoji(emoji) {
this.hideEmojiPicker();
- this.props.onEmojiSelected(emoji);
+ if (_.isFunction(this.onEmojiSelected)) {
+ this.onEmojiSelected(emoji);
+ }
}
hideEmojiPicker() {
this.setState({isEmojiPickerVisible: false});
}
- showEmojiPicker() {
- if (_.isFunction(this.props.onBeforeShowEmojiPicker)) {
- this.props.onBeforeShowEmojiPicker();
- }
+ /**
+ * Show the ReportActionContextMenu modal popover.
+ *
+ * @param {Function} [onModalHide=() => {}] - Run a callback when Modal hides.
+ * @param {Function} [onEmojiSelected=() => {}] - Run a callback when Emoji selected.
+ * @param {Element} emojiPopoverAnchor - Element to which Popover is anchored
+ */
+ showEmojiPicker(onModalHide, onEmojiSelected, emojiPopoverAnchor) {
+ this.onModalHide = onModalHide;
+ this.onEmojiSelected = onEmojiSelected;
+ this.emojiPopoverAnchor = emojiPopoverAnchor;
+
this.setState({isEmojiPickerVisible: true});
+ this.measureEmojiPopoverAnchorPosition();
}
measureEmojiPopoverAnchorPosition() {
@@ -119,6 +82,20 @@ 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.
*/
@@ -130,70 +107,38 @@ class EmojiPicker extends React.Component {
}
render() {
+ // There is no way to disable animations and they are really laggy, because there are so many
+ // emojis. The best alternative is to set it to 1ms so it just "pops" in and out
return (
- <>
- {
-
- // There is no way to disable animations and they are really laggy, because there are so many
- // emojis. The best alternative is to set it to 1ms so it just "pops" in and out
- }
-
- this.emojiSearchInput = el}
- preferredSkinTone={this.props.preferredSkinTone}
- updatePreferredSkinTone={this.setPreferredSkinTone}
- frequentlyUsedEmojis={this.props.frequentlyUsedEmojis}
- />
-
- ([
- styles.chatItemEmojiButton,
- StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed)),
- ])}
- ref={el => this.emojiPopoverAnchor = el}
- onLayout={this.measureEmojiPopoverAnchorPosition}
- onPress={this.showEmojiPicker}
- disabled={this.props.isDisabled}
- >
- {({hovered, pressed}) => (
-
-
-
- )}
-
- >
+
+ this.emojiSearchInput = el}
+ />
+
);
}
}
-EmojiPicker.propTypes = propTypes;
-EmojiPicker.defaultProps = defaultProps;
-
-export default compose(
- withWindowDimensions,
- withLocalize,
- withOnyx({
- preferredSkinTone: {
- key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
- },
- frequentlyUsedEmojis: {
- key: ONYXKEYS.FREQUENTLY_USED_EMOJIS,
- },
- }),
-)(EmojiPicker);
+export default EmojiPicker;
diff --git a/src/components/EnvironmentBadge.js b/src/components/EnvironmentBadge.js
index 122a0e47d87a..12cc13255366 100644
--- a/src/components/EnvironmentBadge.js
+++ b/src/components/EnvironmentBadge.js
@@ -3,6 +3,12 @@ import CONST from '../CONST';
import withEnvironment, {environmentPropTypes} from './withEnvironment';
import Badge from './Badge';
+const ENVIRONMENT_SHORT_FORM = {
+ [CONST.ENVIRONMENT.DEV]: 'DEV',
+ [CONST.ENVIRONMENT.STAGING]: 'STG',
+ [CONST.ENVIRONMENT.PRODUCTION]: 'PROD',
+};
+
const EnvironmentBadge = (props) => {
// If we are on production, don't show any badge
if (props.environment === CONST.ENVIRONMENT.PRODUCTION) {
@@ -13,7 +19,7 @@ const EnvironmentBadge = (props) => {
);
};
diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.js b/src/components/ErrorBoundary/BaseErrorBoundary.js
index 062dcb5a2175..e479b04f7ade 100644
--- a/src/components/ErrorBoundary/BaseErrorBoundary.js
+++ b/src/components/ErrorBoundary/BaseErrorBoundary.js
@@ -36,7 +36,7 @@ class BaseErrorBoundary extends React.Component {
}
componentDidCatch(error, errorInfo) {
- this.props.logError(this.props.errorMessage, error, errorInfo);
+ this.props.logError(this.props.errorMessage, error, JSON.stringify(errorInfo));
// We hide the splash screen since the error might happened during app init
BootSplash.hide();
diff --git a/src/components/ErrorBoundary/index.native.js b/src/components/ErrorBoundary/index.native.js
index ae295a9d0c4d..7792f58edbb0 100644
--- a/src/components/ErrorBoundary/index.native.js
+++ b/src/components/ErrorBoundary/index.native.js
@@ -9,7 +9,7 @@ BaseErrorBoundary.defaultProps.logError = (errorMessage, error, errorInfo) => {
/* On native we also log the error to crashlytics
* Since the error was handled we need to manually tell crashlytics about it */
- crashlytics().log(`errorInfo: ${JSON.stringify(errorInfo)}`);
+ crashlytics().log(`errorInfo: ${errorInfo}`);
crashlytics().recordError(error);
};
diff --git a/src/components/FullNameInputRow.js b/src/components/FullNameInputRow.js
index 1e93c4b429d0..d58fb2a4a855 100644
--- a/src/components/FullNameInputRow.js
+++ b/src/components/FullNameInputRow.js
@@ -48,6 +48,7 @@ const FullNameInputRow = (props) => {
{
{
const childrenWithHoverState = _.isFunction(props.children)
? props.children(false)
: props.children;
- return {childrenWithHoverState};
+ return {childrenWithHoverState};
};
Hoverable.propTypes = propTypes;
diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js
index 8d242e77e02c..340e8e5ad25e 100644
--- a/src/components/Icon/Expensicons.js
+++ b/src/components/Icon/Expensicons.js
@@ -31,6 +31,7 @@ import Gear from '../../../assets/images/gear.svg';
import Hashtag from '../../../assets/images/hashtag.svg';
import Info from '../../../assets/images/info.svg';
import Invoice from '../../../assets/images/invoice.svg';
+import Key from '../../../assets/images/key.svg';
import Keyboard from '../../../assets/images/keyboard.svg';
import Link from '../../../assets/images/link.svg';
import LinkCopy from '../../../assets/images/link-copy.svg';
@@ -104,6 +105,7 @@ export {
Hashtag,
Info,
Invoice,
+ Key,
Keyboard,
Link,
LinkCopy,
diff --git a/src/components/ImageWithSizeCalculation.js b/src/components/ImageWithSizeCalculation.js
index 698da27c610e..003fb3247ef5 100644
--- a/src/components/ImageWithSizeCalculation.js
+++ b/src/components/ImageWithSizeCalculation.js
@@ -1,9 +1,10 @@
import React, {PureComponent} from 'react';
-import {Image} from 'react-native';
+import {Image, ActivityIndicator} from 'react-native';
import PropTypes from 'prop-types';
import Log from '../libs/Log';
import styles from '../styles/styles';
import makeCancellablePromise from '../libs/MakeCancellablePromise';
+import themeColors from '../styles/themes/default';
const propTypes = {
/** Url for image to display */
@@ -29,6 +30,14 @@ const defaultProps = {
* it can be appropriately resized.
*/
class ImageWithSizeCalculation extends PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isLoading: true,
+ };
+ }
+
componentDidMount() {
this.calculateImageSize();
}
@@ -85,15 +94,26 @@ class ImageWithSizeCalculation extends PureComponent {
render() {
return (
-
+ <>
+ this.setState({isLoading: false})}
+ />
+ {this.state.isLoading && (
+
+ )}
+ >
);
}
}
diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js
index 957f1c19a64f..1d65a5469481 100644
--- a/src/components/MultipleAvatars.js
+++ b/src/components/MultipleAvatars.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import {Image, View} from 'react-native';
import styles from '../styles/styles';
import Avatar from './Avatar';
+import Tooltip from './Tooltip';
import Text from './Text';
const propTypes = {
@@ -21,6 +22,9 @@ const propTypes = {
/** Whether this avatar is for an archived room */
isArchivedRoom: PropTypes.bool,
+
+ /** Tooltip for the Avatar */
+ avatarTooltips: PropTypes.arrayOf(PropTypes.string),
};
const defaultProps = {
@@ -29,6 +33,7 @@ const defaultProps = {
secondAvatarStyle: [styles.secondAvatarHovered],
isChatRoom: false,
isArchivedRoom: false,
+ avatarTooltips: [],
};
const MultipleAvatars = (props) => {
@@ -46,44 +51,52 @@ const MultipleAvatars = (props) => {
if (props.avatarImageURLs.length === 1) {
return (
-
+
+
+
);
}
return (
-
+
-
+
+
+
{props.avatarImageURLs.length === 2 ? (
-
+
+
+
) : (
-
-
+
- {`+${props.avatarImageURLs.length - 1}`}
-
-
+
+ {`+${props.avatarImageURLs.length - 1}`}
+
+
+
)}
diff --git a/src/components/PopoverWithMeasuredContent.js b/src/components/PopoverWithMeasuredContent.js
index bb9a201519e0..2285f857968a 100644
--- a/src/components/PopoverWithMeasuredContent.js
+++ b/src/components/PopoverWithMeasuredContent.js
@@ -2,6 +2,7 @@ import _ from 'underscore';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
+import lodashGet from 'lodash/get';
import Popover from './Popover';
import {propTypes as popoverPropTypes, defaultProps as defaultPopoverProps} from './Popover/popoverPropTypes';
import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
@@ -30,6 +31,14 @@ const propTypes = {
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.
+ */
+ popoverDimensions: PropTypes.shape({
+ height: PropTypes.number,
+ width: PropTypes.number,
+ }),
+
...windowDimensionsPropTypes,
};
@@ -41,6 +50,10 @@ const defaultProps = {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
},
+ popoverDimensions: {
+ height: 0,
+ width: 0,
+ },
};
/**
@@ -53,14 +66,14 @@ class PopoverWithMeasuredContent extends Component {
constructor(props) {
super(props);
+ this.popoverWidth = lodashGet(this.props, 'popoverDimensions.width', 0);
+ this.popoverHeight = lodashGet(this.props, 'popoverDimensions.height', 0);
+
this.state = {
- isContentMeasured: false,
+ isContentMeasured: this.popoverWidth > 0 && this.popoverHeight > 0,
isVisible: false,
};
- this.popoverWidth = 0;
- this.popoverHeight = 0;
-
this.measurePopover = this.measurePopover.bind(this);
}
@@ -76,7 +89,7 @@ class PopoverWithMeasuredContent extends Component {
static getDerivedStateFromProps(props, state) {
// When Popover is shown recalculate
if (!state.isVisible && props.isVisible) {
- return {isContentMeasured: false, isVisible: true};
+ return {isContentMeasured: lodashGet(props, 'popoverDimensions.width', 0) > 0 && lodashGet(props, 'popoverDimensions.height', 0) > 0, isVisible: true};
}
if (!props.isVisible) {
return {isVisible: false};
diff --git a/src/components/PressableWithSecondaryInteraction/index.android.js b/src/components/PressableWithSecondaryInteraction/index.android.js
deleted file mode 100644
index 0537a79657b1..000000000000
--- a/src/components/PressableWithSecondaryInteraction/index.android.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import _ from 'underscore';
-import React, {forwardRef} from 'react';
-import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
-import {Pressable, Platform} from 'react-native';
-import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes';
-import Text from '../Text';
-
-/**
- * Triggers haptic feedback, and calls onSecondaryInteraction
- *
- * @param {GestureResponderEvent} event
- * @param {Object} props
- */
-function handleLongPress(event, props) {
- event.preventDefault();
- props.onSecondaryInteraction(event);
-
- // The constant effectHeavyClick is added in API level 29.
- // Docs: https://developer.android.com/reference/android/os/VibrationEffect#EFFECT_HEAVY_CLICK
- // We use keyboardTap added in API level 8 as a fallback.
- // Docs: https://developer.android.com/reference/android/view/HapticFeedbackConstants#KEYBOARD_TAP
- if (Platform.Version >= 29) {
- ReactNativeHapticFeedback.trigger('effectHeavyClick');
- return;
- }
- ReactNativeHapticFeedback.trigger('keyboardTap');
-}
-
-/**
- * This is a special Pressable that calls onSecondaryInteraction when LongPressed.
- *
- * @param {Object} props
- * @returns {React.Component}
- */
-const PressableWithSecondaryInteraction = (props) => {
- // Use Text node for inline mode to prevent content overflow.
- const Node = props.inline ? Text : Pressable;
- return (
- handleLongPress(event, props)}
- onPressOut={props.onPressOut}
- // eslint-disable-next-line react/jsx-props-no-spreading
- {...(_.omit(props, 'onLongPress'))}
- >
- {props.children}
-
- );
-};
-
-PressableWithSecondaryInteraction.propTypes = pressableWithSecondaryInteractionPropTypes.propTypes;
-PressableWithSecondaryInteraction.defaultProps = pressableWithSecondaryInteractionPropTypes.defaultProps;
-PressableWithSecondaryInteraction.displayName = 'PressableWithSecondaryInteraction';
-
-export default forwardRef((props, ref) => (
- // eslint-disable-next-line react/jsx-props-no-spreading
-
-));
diff --git a/src/components/PressableWithSecondaryInteraction/index.ios.js b/src/components/PressableWithSecondaryInteraction/index.native.js
similarity index 88%
rename from src/components/PressableWithSecondaryInteraction/index.ios.js
rename to src/components/PressableWithSecondaryInteraction/index.native.js
index 60b8ffb83748..650333e2c460 100644
--- a/src/components/PressableWithSecondaryInteraction/index.ios.js
+++ b/src/components/PressableWithSecondaryInteraction/index.native.js
@@ -1,9 +1,9 @@
import _ from 'underscore';
import React, {forwardRef} from 'react';
-import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
import {Pressable} from 'react-native';
import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes';
import Text from '../Text';
+import HapticFeedback from '../../libs/HapticFeedback';
/**
* This is a special Pressable that calls onSecondaryInteraction when LongPressed.
@@ -21,9 +21,7 @@ const PressableWithSecondaryInteraction = (props) => {
onPressIn={props.onPressIn}
onLongPress={(e) => {
e.preventDefault();
- ReactNativeHapticFeedback.trigger('selection', {
- enableVibrateFallback: true,
- });
+ HapticFeedback.trigger();
props.onSecondaryInteraction(e);
}}
onPressOut={props.onPressOut}
diff --git a/src/components/RNTextInput.js b/src/components/RNTextInput.js
new file mode 100644
index 000000000000..75e14b0966e5
--- /dev/null
+++ b/src/components/RNTextInput.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import _ from 'underscore';
+// eslint-disable-next-line no-restricted-imports
+import {TextInput} from 'react-native';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ /** A ref to forward to the text input */
+ forwardedRef: PropTypes.func,
+};
+
+const defaultProps = {
+ forwardedRef: () => {},
+};
+
+const RNTextInput = props => (
+ {
+ if (!_.isFunction(props.forwardedRef)) {
+ return;
+ }
+ props.forwardedRef(ref);
+ }}
+
+ // By default, align input to the left to override right alignment in RTL mode which is not yet supported in the App.
+ // eslint-disable-next-line react/jsx-props-no-multi-spaces
+ textAlign="left"
+
+ // eslint-disable-next-line
+ {...props}
+ />
+);
+
+RNTextInput.propTypes = propTypes;
+RNTextInput.defaultProps = defaultProps;
+RNTextInput.displayName = 'RNTextInput';
+
+export default React.forwardRef((props, ref) => (
+ /* eslint-disable-next-line react/jsx-props-no-spreading */
+
+));
diff --git a/src/components/ReportActionItem/IOUPreview.js b/src/components/ReportActionItem/IOUPreview.js
index a0679ca3fc40..4e4792c9086c 100644
--- a/src/components/ReportActionItem/IOUPreview.js
+++ b/src/components/ReportActionItem/IOUPreview.js
@@ -113,9 +113,10 @@ const IOUPreview = (props) => {
const ownerAvatar = lodashGet(props.personalDetails, [ownerEmail, 'avatar'], '');
const cachedTotal = props.iouReport.total && props.iouReport.currency
? props.numberFormat(
- props.iouReport.total / 100,
+ Math.abs(props.iouReport.total) / 100,
{style: 'currency', currency: props.iouReport.currency},
) : '';
+ const avatarTooltip = [Str.removeSMSDomain(managerEmail), Str.removeSMSDomain(ownerEmail)];
return (
@@ -140,6 +141,7 @@ const IOUPreview = (props) => {
diff --git a/src/components/ReportWelcomeText.js b/src/components/ReportWelcomeText.js
index 0d30b2f1ed6a..90bed15c3b42 100644
--- a/src/components/ReportWelcomeText.js
+++ b/src/components/ReportWelcomeText.js
@@ -70,7 +70,7 @@ const ReportWelcomeText = (props) => {
const isResctrictedRoom = lodashGet(props, 'report.visibility', '') === CONST.REPORT.VISIBILITY.RESTRICTED;
return (
-
+
{!props.shouldIncludeParticipants
? (
<>
diff --git a/src/components/RoomNameInput.js b/src/components/RoomNameInput.js
index c9ba86fbae4d..430572844a20 100644
--- a/src/components/RoomNameInput.js
+++ b/src/components/RoomNameInput.js
@@ -1,6 +1,5 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
-import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
import CONST from '../CONST';
import ONYXKEYS from '../ONYXKEYS';
@@ -14,17 +13,14 @@ const propTypes = {
/** Callback to execute when the text input is modified correctly */
onChangeText: PropTypes.func,
- /** Callback to execute when an error gets found/cleared/modified */
- onChangeError: PropTypes.func,
-
/** Initial room name to show in input field. This should include the '#' already prefixed to the name */
initialValue: PropTypes.string,
/** Whether we should show the input as disabled */
disabled: PropTypes.bool,
- /** ID of policy whose room names we should be checking for duplicates */
- policyID: PropTypes.string,
+ /** Error text to show */
+ errorText: PropTypes.string,
...withLocalizePropTypes,
...fullPolicyPropTypes,
@@ -52,10 +48,9 @@ const propTypes = {
const defaultProps = {
onChangeText: () => {},
- onChangeError: () => {},
initialValue: '',
disabled: false,
- policyID: '',
+ errorText: '',
...fullPolicyDefaultProps,
};
@@ -64,94 +59,48 @@ class RoomNameInput extends Component {
super(props);
this.state = {
roomName: props.initialValue,
- error: '',
};
- this.originalRoomName = props.initialValue;
-
- this.checkAndModifyRoomName = this.checkAndModifyRoomName.bind(this);
- this.checkExistingRoomName = this.checkExistingRoomName.bind(this);
- }
-
- componentDidUpdate(prevProps, prevState) {
- // As we are modifying the text input, we'll bubble up any changes/errors so the parent component can see it
- if (prevState.roomName !== this.state.roomName) {
- this.props.onChangeText(this.state.roomName);
- }
- if (prevState.error !== this.state.error) {
- this.props.onChangeError(this.state.error);
- }
-
- // If the selected policyID has changed we need to check if the room name already exists on this new policy.
- if (prevProps.policyID !== this.props.policyID) {
- this.checkExistingRoomName(this.state.roomName);
- }
+ this.setModifiedRoomName = this.setModifiedRoomName.bind(this);
}
/**
- * Modifies the room name to follow our conventions:
- * - Max length 80 characters
- * - Cannot not include space or special characters, and we automatically apply an underscore for spaces
- * - Must be lowercase
- * Also checks to see if this room name already exists, and displays an error message if so.
+ * Sets the modified room name in the state and calls the onChangeText callback
* @param {Event} event
- *
*/
- checkAndModifyRoomName(event) {
+ setModifiedRoomName(event) {
const nativeEvent = event.nativeEvent;
const roomName = nativeEvent.text;
const target = nativeEvent.target;
const selection = target.selectionStart;
-
- const modifiedRoomNameWithoutHash = roomName
- .replace(/ /g, '_')
- .replace(/[^a-zA-Z\d_]/g, '')
- .substring(0, CONST.REPORT.MAX_ROOM_NAME_LENGTH)
- .toLowerCase();
- const finalRoomName = `#${modifiedRoomNameWithoutHash}`;
-
- this.checkExistingRoomName(finalRoomName);
-
- this.setState({
- roomName: finalRoomName,
- }, () => {
+ const modifiedRoomName = this.modifyRoomName(roomName);
+ this.setState({roomName: modifiedRoomName}, () => {
if (!selection) {
return;
}
target.selectionEnd = selection;
});
+ this.props.onChangeText(modifiedRoomName);
}
/**
- * Checks to see if this room name already exists, and displays an error message if so.
+ * Modifies the room name to follow our conventions:
+ * - Max length 80 characters
+ * - Cannot not include space or special characters, and we automatically apply an underscore for spaces
+ * - Must be lowercase
* @param {String} roomName
- *
+ * @returns {String}
*/
- checkExistingRoomName(roomName) {
- const isExistingRoomName = _.some(
- _.values(this.props.reports),
- report => report && report.policyID === this.props.policyID && report.reportName === roomName,
- );
-
- let error = '';
-
- // We error if the room name already exists. We don't care if it matches the original name provided in this
- // component because then we are not changing the room's name.
- if (isExistingRoomName && roomName !== this.originalRoomName) {
- error = this.props.translate('newRoomPage.roomAlreadyExistsError');
- }
-
- // Certain names are reserved for default rooms and should not be used for policy rooms.
- if (_.contains(CONST.REPORT.RESERVED_ROOM_NAMES, roomName)) {
- error = this.props.translate('newRoomPage.roomNameReservedError');
- }
+ modifyRoomName(roomName) {
+ const modifiedRoomNameWithoutHash = roomName
+ .replace(/ /g, '_')
+ .replace(/[^a-zA-Z\d_]/g, '')
+ .substr(0, CONST.REPORT.MAX_ROOM_NAME_LENGTH)
+ .toLowerCase();
- this.setState({
- error,
- });
+ return `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`;
}
-
render() {
return (
);
diff --git a/src/components/Text.js b/src/components/Text.js
index 63e992eca3cd..84a78d3e37c5 100644
--- a/src/components/Text.js
+++ b/src/components/Text.js
@@ -15,8 +15,7 @@ const propTypes = {
fontSize: PropTypes.number,
/** The alignment of the text */
- // eslint-disable-next-line react/forbid-prop-types
- textAlign: PropTypes.any,
+ textAlign: PropTypes.string,
/** Any children to display */
children: PropTypes.node,
@@ -32,7 +31,7 @@ const defaultProps = {
color: themeColors.text,
fontSize: variables.fontSizeNormal,
family: 'GTA',
- textAlign: null,
+ textAlign: 'left',
children: null,
style: {},
};
diff --git a/src/components/TextInput/BaseTextInput.js b/src/components/TextInput/BaseTextInput.js
index 330a39ed0587..2aa61153e42c 100644
--- a/src/components/TextInput/BaseTextInput.js
+++ b/src/components/TextInput/BaseTextInput.js
@@ -4,6 +4,7 @@ import {
Animated, View, TouchableWithoutFeedback, Pressable, AppState, Keyboard,
} from 'react-native';
import Str from 'expensify-common/lib/str';
+import RNTextInput from '../RNTextInput';
import TextInputLabel from './TextInputLabel';
import * as baseTextInputPropTypes from './baseTextInputPropTypes';
import themeColors from '../../styles/themes/default';
@@ -12,7 +13,6 @@ import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
import Text from '../Text';
import * as styleConst from './styleConst';
-import TextInputWithName from '../TextInputWithName';
import * as StyleUtils from '../../styles/StyleUtils';
class BaseTextInput extends Component {
@@ -58,7 +58,8 @@ class BaseTextInput extends Component {
componentDidUpdate() {
// Activate or deactivate the label when value is changed programmatically from outside
- if (this.value === this.props.value) {
+ // Only update when value prop is provided
+ if (this.props.value === undefined || this.value === this.props.value) {
return;
}
@@ -215,7 +216,7 @@ class BaseTextInput extends Component {
>
) : null}
- {
if (typeof this.props.innerRef === 'function') { this.props.innerRef(ref); }
this.input = ref;
@@ -240,7 +241,6 @@ class BaseTextInput extends Component {
onChangeText={this.setValue}
secureTextEntry={this.state.passwordHidden}
onPressOut={this.props.onPress}
- name={this.props.name}
showSoftInputOnFocus={!this.props.disableKeyboard}
/>
{this.props.secureTextEntry && (
diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js
index 2f94e5668059..1454049c495a 100644
--- a/src/components/TextInput/baseTextInputPropTypes.js
+++ b/src/components/TextInput/baseTextInputPropTypes.js
@@ -56,7 +56,7 @@ const propTypes = {
* @param {Object} props - props passed to the input
* @returns {Object} - returns an Error object if isFormInput is supplied but inputID is falsey or not a string
*/
- inputID: props => FormUtils.getInputIDPropTypes(props),
+ inputID: props => FormUtils.validateInputIDProps(props),
/** Saves a draft of the input value when used in a form */
shouldSaveDraft: PropTypes.bool,
diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.js
index 7bb389dd0442..30a00e89b76d 100644
--- a/src/components/TextInput/index.js
+++ b/src/components/TextInput/index.js
@@ -6,11 +6,13 @@ import * as baseTextInputPropTypes from './baseTextInputPropTypes';
class TextInput extends React.Component {
componentDidMount() {
- if (!this.props.disableKeyboard) {
- return;
+ if (this.props.disableKeyboard) {
+ this.textInput.setNativeProps({inputmode: 'none'});
}
- this.textInput.setNativeProps({inputmode: 'none'});
+ if (this.props.name) {
+ this.textInput.setNativeProps({name: this.props.name});
+ }
}
render() {
diff --git a/src/components/TextInputFocusable/index.android.js b/src/components/TextInputFocusable/index.android.js
index 691eea55b3a5..749a27d899d3 100644
--- a/src/components/TextInputFocusable/index.android.js
+++ b/src/components/TextInputFocusable/index.android.js
@@ -1,8 +1,7 @@
import React from 'react';
-// eslint-disable-next-line no-restricted-imports
-import {TextInput} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
+import RNTextInput from '../RNTextInput';
import themeColors from '../../styles/themes/default';
import CONST from '../../CONST';
@@ -64,7 +63,7 @@ class TextInputFocusable extends React.Component {
render() {
return (
- this.textInput = el}
maxHeight={CONST.COMPOSER_MAX_HEIGHT}
diff --git a/src/components/TextInputFocusable/index.ios.js b/src/components/TextInputFocusable/index.ios.js
index 2c1a7e0ad028..6955e6813bc5 100644
--- a/src/components/TextInputFocusable/index.ios.js
+++ b/src/components/TextInputFocusable/index.ios.js
@@ -1,8 +1,7 @@
import React from 'react';
-// eslint-disable-next-line no-restricted-imports
-import {TextInput} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
+import RNTextInput from '../RNTextInput';
import themeColors from '../../styles/themes/default';
import CONST from '../../CONST';
@@ -76,7 +75,7 @@ class TextInputFocusable extends React.Component {
// Selection Property not worked in IOS properly, So removed from props.
const propsToPass = _.omit(this.props, 'selection');
return (
- this.textInput = el}
maxHeight={CONST.COMPOSER_MAX_HEIGHT}
diff --git a/src/components/TextInputFocusable/index.js b/src/components/TextInputFocusable/index.js
index bbb6801eeef1..d7d4344faae2 100755
--- a/src/components/TextInputFocusable/index.js
+++ b/src/components/TextInputFocusable/index.js
@@ -1,9 +1,9 @@
import React from 'react';
-// eslint-disable-next-line no-restricted-imports
-import {TextInput, StyleSheet} from 'react-native';
+import {StyleSheet} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import RNTextInput from '../RNTextInput';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import Growl from '../../libs/Growl';
import themeColors from '../../styles/themes/default';
@@ -348,7 +348,7 @@ class TextInputFocusable extends React.Component {
propStyles.outline = 'none';
const propsWithoutStyles = _.omit(this.props, 'style');
return (
- this.textInput = el}
selection={this.state.selection}
diff --git a/src/components/TextInputWithFocusStyles.js b/src/components/TextInputWithFocusStyles.js
deleted file mode 100644
index 47fa1e3a585b..000000000000
--- a/src/components/TextInputWithFocusStyles.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import React from 'react';
-// eslint-disable-next-line no-restricted-imports
-import {TextInput, StyleSheet} from 'react-native';
-import PropTypes from 'prop-types';
-import _ from 'underscore';
-
-const propTypes = {
- /** A ref to forward to the text input */
- forwardedRef: PropTypes.func.isRequired,
-
- /** Styles to apply to the text input when it has focus */
- // eslint-disable-next-line react/forbid-prop-types
- styleFocusIn: PropTypes.any,
-
- /** Styles to apply to the text input when it does not have focus */
- // eslint-disable-next-line react/forbid-prop-types
- styleFocusOut: PropTypes.any,
-
- /** General styles to apply to the text input */
- // eslint-disable-next-line react/forbid-prop-types
- style: PropTypes.any,
-
- /** A function to call when the input has been blurred */
- onBlur: PropTypes.func,
-
- /** A function to call when the input has gotten focus */
- onFocus: PropTypes.func,
-};
-const defaultProps = {
- styleFocusIn: null,
- styleFocusOut: null,
- style: null,
- onBlur: () => {},
- onFocus: () => {},
-};
-
-class TextInputWithFocusStyles extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- isFocused: false,
- };
- }
-
- render() {
- // Make full objects out of both the style coming from props, and the style we have in the state
- const propStyles = StyleSheet.flatten(this.props.style);
- const focusedStyle = this.state.isFocused
- ? StyleSheet.flatten(this.props.styleFocusIn)
- : StyleSheet.flatten(this.props.styleFocusOut);
-
- // Merge the two styles together
- const style = StyleSheet.compose(propStyles, focusedStyle);
-
- // Omit the props that are used in this intermediary component and only pass down the props that
- // are necessary
- const propsToPassToTextInput = _.omit(this.props, [
- 'focusInStyle',
- 'focusOutStyle',
- 'onFocus',
- 'onBlur',
- 'style',
- ]);
- return (
- {
- this.setState({isFocused: true});
- this.props.onFocus();
- }}
- onBlur={() => {
- this.setState({isFocused: false});
- this.props.onBlur();
- }}
- /* eslint-disable-next-line react/jsx-props-no-spreading */
- {...propsToPassToTextInput}
- />
- );
- }
-}
-
-TextInputWithFocusStyles.propTypes = propTypes;
-TextInputWithFocusStyles.defaultProps = defaultProps;
-
-export default React.forwardRef((props, ref) => (
- /* eslint-disable-next-line react/jsx-props-no-spreading */
-
-));
diff --git a/src/components/TextInputWithName/index.js b/src/components/TextInputWithName/index.js
deleted file mode 100755
index 19cc05d44696..000000000000
--- a/src/components/TextInputWithName/index.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import _ from 'underscore';
-import React from 'react';
-// eslint-disable-next-line no-restricted-imports
-import {TextInput} from 'react-native';
-import textInputWithNamepropTypes from './textInputWithNamepropTypes';
-
-/**
- * On web we need to set the native attribute name for accessiblity.
- */
-class TextInputWithName extends React.Component {
- componentDidMount() {
- if (!this.textInput) {
- return;
- }
- if (_.isFunction(this.props.forwardedRef)) {
- this.props.forwardedRef(this.textInput);
- }
-
- if (this.props.name) {
- this.textInput.setNativeProps({name: this.props.name});
- }
- }
-
- render() {
- return (
- this.textInput = el}
- // eslint-disable-next-line react/jsx-props-no-spreading
- {...this.props}
- />
- );
- }
-}
-
-TextInputWithName.propTypes = textInputWithNamepropTypes.propTypes;
-TextInputWithName.defaultProps = textInputWithNamepropTypes.defaultProps;
-
-export default React.forwardRef((props, ref) => (
- /* eslint-disable-next-line react/jsx-props-no-spreading */
-
-));
diff --git a/src/components/TextInputWithName/index.native.js b/src/components/TextInputWithName/index.native.js
deleted file mode 100644
index e5f0163e446c..000000000000
--- a/src/components/TextInputWithName/index.native.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-// eslint-disable-next-line no-restricted-imports
-import {TextInput} from 'react-native';
-import textInputWithNamepropTypes from './textInputWithNamepropTypes';
-
-const TextInputWithName = props => (
-
-);
-
-TextInputWithName.propTypes = textInputWithNamepropTypes.propTypes;
-TextInputWithName.defaultProps = textInputWithNamepropTypes.defaultProps;
-TextInputWithName.displayName = 'TextInputWithName';
-
-export default React.forwardRef((props, ref) => (
- /* eslint-disable-next-line react/jsx-props-no-spreading */
-
-));
diff --git a/src/components/TextInputWithName/textInputWithNamepropTypes.js b/src/components/TextInputWithName/textInputWithNamepropTypes.js
deleted file mode 100644
index 902ff2289d68..000000000000
--- a/src/components/TextInputWithName/textInputWithNamepropTypes.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import PropTypes from 'prop-types';
-
-const propTypes = {
- /** Name attribute for the input */
- name: PropTypes.string,
-
- /** A ref to forward to the text input */
- forwardedRef: PropTypes.func,
-};
-
-const defaultProps = {
- name: '',
- forwardedRef: () => {},
-};
-
-export default {
- propTypes,
- defaultProps,
-};
diff --git a/src/components/TextInputWithPrefix/index.android.js b/src/components/TextInputWithPrefix/index.android.js
index e9b3944289fc..b04f68bd1490 100644
--- a/src/components/TextInputWithPrefix/index.android.js
+++ b/src/components/TextInputWithPrefix/index.android.js
@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
-// eslint-disable-next-line no-restricted-imports
-import {TextInput, View} from 'react-native';
+import {View} from 'react-native';
import _ from 'underscore';
import React from 'react';
+import RNTextInput from '../RNTextInput';
import Text from '../Text';
import styles from '../../styles/styles';
import InlineErrorText from '../InlineErrorText';
@@ -35,7 +35,7 @@ const TextInputWithPrefix = props => (
]}
>
{props.prefixCharacter}
- (
]}
>
{props.prefixCharacter}
- this.wrapperView = el}
diff --git a/src/components/Tooltip/tooltipPropTypes.js b/src/components/Tooltip/tooltipPropTypes.js
index 9d850f131b73..c575dc468828 100644
--- a/src/components/Tooltip/tooltipPropTypes.js
+++ b/src/components/Tooltip/tooltipPropTypes.js
@@ -6,7 +6,7 @@ const propTypes = {
absolute: PropTypes.bool,
/** The text to display in the tooltip. */
- text: PropTypes.string.isRequired,
+ text: PropTypes.string,
/** Styles to be assigned to the Tooltip wrapper views */
containerStyles: PropTypes.arrayOf(PropTypes.object),
@@ -31,6 +31,7 @@ const defaultProps = {
shiftHorizontal: 0,
shiftVertical: 0,
containerStyles: [],
+ text: '',
};
export {
diff --git a/src/components/WalletStatementModal/WalletStatementModalPropTypes.js b/src/components/WalletStatementModal/WalletStatementModalPropTypes.js
new file mode 100644
index 000000000000..38fca6fe9f81
--- /dev/null
+++ b/src/components/WalletStatementModal/WalletStatementModalPropTypes.js
@@ -0,0 +1,25 @@
+import PropTypes from 'prop-types';
+
+const walletStatementPropTypes = {
+ /* Onyx Props */
+ /** Session info for the currently logged in user. */
+ session: PropTypes.shape({
+
+ /** Currently logged in user authToken */
+ authToken: PropTypes.string,
+ }),
+
+ /** URL for oldDot (expensify.com) statements page to display */
+ statementPageURL: PropTypes.string,
+};
+
+const walletStatementDefaultProps = {
+ session: {
+ authToken: null,
+ },
+};
+
+export {
+ walletStatementPropTypes,
+ walletStatementDefaultProps,
+};
diff --git a/src/components/WalletStatementModal/index.js b/src/components/WalletStatementModal/index.js
new file mode 100644
index 000000000000..31783da04e89
--- /dev/null
+++ b/src/components/WalletStatementModal/index.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import {withOnyx} from 'react-native-onyx';
+import lodashGet from 'lodash/get';
+import {View} from 'react-native';
+import compose from '../../libs/compose';
+import withLocalize from '../withLocalize';
+import ONYXKEYS from '../../ONYXKEYS';
+import {walletStatementPropTypes, walletStatementDefaultProps} from './WalletStatementModalPropTypes';
+import styles from '../../styles/styles';
+import FullScreenLoadingIndicator from '../FullscreenLoadingIndicator';
+
+class WalletStatementModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isLoading: true,
+ };
+ }
+
+ render() {
+ const authToken = lodashGet(this.props, 'session.authToken', null);
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+}
+
+WalletStatementModal.propTypes = walletStatementPropTypes;
+WalletStatementModal.defaultProps = walletStatementDefaultProps;
+WalletStatementModal.displayName = 'WalletStatementModal';
+export default compose(
+ withLocalize,
+ withOnyx({
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ }),
+)(WalletStatementModal);
diff --git a/src/components/WalletStatementModal/index.native.js b/src/components/WalletStatementModal/index.native.js
new file mode 100644
index 000000000000..42784e4b2671
--- /dev/null
+++ b/src/components/WalletStatementModal/index.native.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import {WebView} from 'react-native-webview';
+import lodashGet from 'lodash/get';
+import {withOnyx} from 'react-native-onyx';
+import withLocalize from '../withLocalize';
+import ONYXKEYS from '../../ONYXKEYS';
+import compose from '../../libs/compose';
+import {walletStatementPropTypes, walletStatementDefaultProps} from './WalletStatementModalPropTypes';
+import FullScreenLoadingIndicator from '../FullscreenLoadingIndicator';
+
+const WalletStatementModal = (props) => {
+ const authToken = lodashGet(props, 'session.authToken', null);
+ return (
+ }
+ />
+ );
+};
+
+WalletStatementModal.propTypes = walletStatementPropTypes;
+WalletStatementModal.defaultProps = walletStatementDefaultProps;
+WalletStatementModal.displayName = 'WalletStatementModal';
+export default compose(
+ withLocalize,
+ withOnyx({
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ }),
+)(WalletStatementModal);
diff --git a/src/languages/en.js b/src/languages/en.js
index cc56939abce1..20be3907e42b 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -303,7 +303,8 @@ export default {
closeAccountSuccess: 'Account closed successfully',
closeAccountActionRequired: 'Looks like you need to complete some actions before closing your account. Check out the guide',
closeAccountTryAgainAfter: 'and try again after.',
- typeToConfirm: ({emailOrPhone}) => `Enter ${emailOrPhone} to confirm`,
+ enterDefaultContact: 'Enter your default contact method',
+ defaultContact: 'Default contact method:',
okayGotIt: 'Okay, Got it',
},
passwordPage: {
@@ -712,7 +713,7 @@ export default {
},
workspace: {
common: {
- card: 'Issue corporate cards',
+ card: 'Issue cards',
workspace: 'Workspace',
edit: 'Edit workspace',
delete: 'Delete Workspace',
@@ -895,7 +896,9 @@ export default {
createRoom: 'Create Room',
policyRoomRenamed: 'Policy room renamed!',
roomAlreadyExistsError: 'A room with this name already exists',
- roomNameReservedError: 'This name is reserved and cannot be used',
+ roomNameReservedError: 'A room on this workspace already uses this name',
+ pleaseEnterRoomName: 'Please enter a room name',
+ pleaseSelectWorkspace: 'Please select a workspace',
renamedRoomAction: ({oldName, newName}) => ` renamed this room from ${oldName} to ${newName}`,
social: 'social',
selectAWorkspace: 'Select a workspace',
diff --git a/src/languages/es.js b/src/languages/es.js
index 07832d488a15..a1d9bc7272f8 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -303,7 +303,8 @@ export default {
closeAccountSuccess: 'Cuenta cerrada exitosamente',
closeAccountActionRequired: 'Parece que necesitas completar algunas acciones antes de cerrar tu cuenta. Mira la guĂa',
closeAccountTryAgainAfter: 'e intenta nuevamente',
- typeToConfirm: ({emailOrPhone}) => `Ingresa ${emailOrPhone} para confirmar`,
+ enterDefaultContact: 'Introduce tu método de contacto predeterminado',
+ defaultContact: 'MĂ©todo de contacto predeterminado:',
okayGotIt: 'Ok, entendido',
},
passwordPage: {
@@ -714,12 +715,12 @@ export default {
},
workspace: {
common: {
- card: 'Emitir tarjetas corporativas',
+ card: 'Emitir tarjetas',
workspace: 'Espacio de trabajo',
edit: 'Editar espacio de trabajo',
delete: 'Eliminar espacio de trabajo',
settings: 'ConfiguraciĂłn general',
- reimburse: 'Reembolsar recibos',
+ reimburse: 'Reembolsar gastos',
bills: 'Pagar facturas',
invoices: 'Enviar facturas',
travel: 'Reservar viaje',
@@ -763,14 +764,14 @@ export default {
reimburse: {
captureReceipts: 'Captura recibos',
fastReimbursementsHappyMembers: '¡Reembolsos rápidos = miembros felices!',
- kilometers: '',
- miles: '',
+ kilometers: 'KilĂłmetros',
+ miles: 'Millas',
viewAllReceipts: 'Ver todos los recibos',
reimburseReceipts: 'Reembolsar recibos',
- trackDistance: '',
- trackDistanceCopy: '',
- trackDistanceRate: '',
- trackDistanceUnit: '',
+ trackDistance: 'Medir distancia',
+ trackDistanceCopy: 'Configura la tarifa y unidad usadas para medir distancias.',
+ trackDistanceRate: 'Tarifa',
+ trackDistanceUnit: 'Unidad',
unlockNextDayReimbursements: 'Desbloquea reembolsos diarios',
captureNoVBACopyBeforeEmail: 'Pide a los miembros de tu espacio de trabajo que envĂen recibos a ',
captureNoVBACopyAfterEmail: ' y descarga la App de Expensify para controlar tus gastos en efectivo sobre la marcha.',
@@ -897,7 +898,9 @@ export default {
createRoom: 'Crea una sala de chat',
policyRoomRenamed: '¡Espacio de trabajo renombrado!',
roomAlreadyExistsError: 'Ya existe una sala con este nombre',
- roomNameReservedError: 'Este nombre está reservado y no puede usarse',
+ roomNameReservedError: 'Una sala en este espacio de trabajo ya usa este nombre',
+ pleaseEnterRoomName: 'Por favor escribe el nombre de una sala',
+ pleaseSelectWorkspace: 'Por favor, selecciona un espacio de trabajo',
renamedRoomAction: ({oldName, newName}) => ` cambiĂł el nombre de la sala de ${oldName} a ${newName}`,
social: 'social',
selectAWorkspace: 'Seleccionar un espacio de trabajo',
diff --git a/src/libs/API.js b/src/libs/API.js
index 7306e6d3f099..ce55e3de5ed9 100644
--- a/src/libs/API.js
+++ b/src/libs/API.js
@@ -190,6 +190,10 @@ Network.registerResponseHandler((queuedRequest, response) => {
});
Network.registerErrorHandler((queuedRequest, error) => {
+ if (error.name === CONST.ERROR.REQUEST_CANCELLED) {
+ Log.info('[API] request canceled', false, queuedRequest);
+ return;
+ }
if (queuedRequest.command !== 'Log') {
Log.hmmm('[API] Handled error when making request', error);
} else {
@@ -449,7 +453,9 @@ function DeleteLogin(parameters) {
const commandName = 'DeleteLogin';
requireParameters(['partnerUserID', 'partnerName', 'partnerPassword', 'shouldRetry'],
parameters, commandName);
- return Network.post(commandName, parameters);
+
+ // Non-cancellable request: during logout, when requests are cancelled, we don't want to cancel the actual logout request
+ return Network.post(commandName, {...parameters, canCancel: false});
}
/**
diff --git a/src/libs/FormUtils.js b/src/libs/FormUtils.js
index c9a3ec49546b..afc55f8c2981 100644
--- a/src/libs/FormUtils.js
+++ b/src/libs/FormUtils.js
@@ -1,15 +1,15 @@
/**
- * Gets the prop type for inputID
+ * Custom prop validator that enforces inputID as a required string if isFormInput is true
*
* @param {Object} props - props passed to the input component
* @returns {Object} returns an Error object if isFormInput is supplied but inputID is falsey or not a string
*/
-function getInputIDPropTypes(props) {
+function validateInputIDProps(props) {
if (!props.isFormInput) {
return;
}
if (!props.inputID) {
- return new Error('InputID is required if isFormInput prop is supplied.');
+ return new Error('inputID is required if isFormInput is true');
}
if (typeof props.inputID !== 'string') {
return new Error(`Invalid prop type ${typeof props.inputID} supplied to inputID. Expecting string.`);
@@ -18,5 +18,5 @@ function getInputIDPropTypes(props) {
export {
// eslint-disable-next-line import/prefer-default-export
- getInputIDPropTypes,
+ validateInputIDProps,
};
diff --git a/src/libs/HapticFeedback/index.android.js b/src/libs/HapticFeedback/index.android.js
new file mode 100644
index 000000000000..e0e077a3513b
--- /dev/null
+++ b/src/libs/HapticFeedback/index.android.js
@@ -0,0 +1,18 @@
+import {Platform} from 'react-native';
+import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
+
+function trigger() {
+ // The constant effectHeavyClick is added in API level 29.
+ // Docs: https://developer.android.com/reference/android/os/VibrationEffect#EFFECT_HEAVY_CLICK
+ // We use keyboardTap added in API level 8 as a fallback.
+ // Docs: https://developer.android.com/reference/android/view/HapticFeedbackConstants#KEYBOARD_TAP
+ if (Platform.Version >= 29) {
+ ReactNativeHapticFeedback.trigger('effectHeavyClick');
+ return;
+ }
+ ReactNativeHapticFeedback.trigger('keyboardTap');
+}
+
+export default {
+ trigger,
+};
diff --git a/src/libs/HapticFeedback/index.ios.js b/src/libs/HapticFeedback/index.ios.js
new file mode 100644
index 000000000000..d9b002621eb7
--- /dev/null
+++ b/src/libs/HapticFeedback/index.ios.js
@@ -0,0 +1,12 @@
+
+import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
+
+function trigger() {
+ ReactNativeHapticFeedback.trigger('selection', {
+ enableVibrateFallback: true,
+ });
+}
+
+export default {
+ trigger,
+};
diff --git a/src/libs/HapticFeedback/index.js b/src/libs/HapticFeedback/index.js
new file mode 100644
index 000000000000..39dbfb3c17aa
--- /dev/null
+++ b/src/libs/HapticFeedback/index.js
@@ -0,0 +1,6 @@
+/**
+ * Web does not support Haptic feedback
+ */
+export default {
+ trigger: () => {},
+};
diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js
index f9a33ea4be05..ee2828c87d8d 100644
--- a/src/libs/HttpUtils.js
+++ b/src/libs/HttpUtils.js
@@ -10,17 +10,23 @@ Onyx.connect({
callback: val => shouldUseSecureStaging = (val && _.isBoolean(val.shouldUseSecureStaging)) ? val.shouldUseSecureStaging : false,
});
+// We use the AbortController API to terminate pending request in `cancelPendingRequests`
+let cancellationController = new AbortController();
+
/**
* Send an HTTP request, and attempt to resolve the json response.
* If there is a network error, we'll set the application offline.
*
* @param {String} url
- * @param {String} method
- * @param {Object} body
+ * @param {String} [method]
+ * @param {Object} [body]
+ * @param {Boolean} [canCancel]
* @returns {Promise}
*/
-function processHTTPRequest(url, method = 'get', body = null) {
+function processHTTPRequest(url, method = 'get', body = null, canCancel = true) {
return fetch(url, {
+ // We hook requests to the same Controller signal, so we can cancel them all at once
+ signal: canCancel ? cancellationController.signal : undefined,
method,
body,
})
@@ -44,7 +50,7 @@ function xhr(command, data, type = CONST.NETWORK.METHOD.POST, shouldUseSecure =
apiRoot = CONST.STAGING_SECURE_URL;
}
- return processHTTPRequest(`${apiRoot}api?command=${command}`, type, formData);
+ return processHTTPRequest(`${apiRoot}api?command=${command}`, type, formData, data.canCancel);
}
/**
@@ -64,7 +70,16 @@ function download(relativePath) {
return processHTTPRequest(`${siteRoot}${strippedRelativePath}`);
}
+function cancelPendingRequests() {
+ cancellationController.abort();
+
+ // We create a new instance because once `abort()` is called any future requests using the same controller would
+ // automatically get rejected: https://dom.spec.whatwg.org/#abortcontroller-api-integration
+ cancellationController = new AbortController();
+}
+
export default {
download,
xhr,
+ cancelPendingRequests,
};
diff --git a/src/libs/Log.js b/src/libs/Log.js
index 28e4978c6ad4..6a0ccc41f899 100644
--- a/src/libs/Log.js
+++ b/src/libs/Log.js
@@ -21,7 +21,8 @@ function LogCommand(parameters) {
parameters, commandName);
// Note: We are forcing Log to run since it requires no authToken and should only be queued when we are offline.
- return Network.post(commandName, {...parameters, forceNetworkRequest: true});
+ // Non-cancellable request: during logout, when requests are cancelled, we don't want to cancel any remaining logs
+ return Network.post(commandName, {...parameters, forceNetworkRequest: true, canCancel: false});
}
/**
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js
index c8502a911f77..5b5d72713217 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.js
@@ -357,6 +357,12 @@ class AuthScreens extends React.Component {
component={ModalStackNavigators.IOUSendModalStackNavigator}
listeners={modalScreenListeners}
/>
+
);
}
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index 071e6c29da11..f0f362ef2eda 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -30,6 +30,7 @@ import WorkspaceInvitePage from '../../../pages/workspace/WorkspaceInvitePage';
import ReimbursementAccountPage from '../../../pages/ReimbursementAccount/ReimbursementAccountPage';
import RequestCallPage from '../../../pages/RequestCallPage';
import ReportDetailsPage from '../../../pages/ReportDetailsPage';
+import WalletStatementPage from '../../../pages/wallet/WalletStatementPage';
import WorkspaceSettingsPage from '../../../pages/workspace/WorkspaceSettingsPage';
import WorkspaceInitialPage from '../../../pages/workspace/WorkspaceInitialPage';
import WorkspaceCardPage from '../../../pages/workspace/card/WorkspaceCardPage';
@@ -313,6 +314,11 @@ const RequestCallModalStackNavigator = createModalStackNavigator([{
name: 'RequestCall_Root',
}]);
+const WalletStatementStackNavigator = createModalStackNavigator([{
+ Component: WalletStatementPage,
+ name: 'WalletStatement_Root',
+}]);
+
export {
IOUBillStackNavigator,
IOURequestModalStackNavigator,
@@ -330,4 +336,5 @@ export {
AddPersonalBankAccountModalStackNavigator,
ReimbursementAccountModalStackNavigator,
RequestCallModalStackNavigator,
+ WalletStatementStackNavigator,
};
diff --git a/src/libs/Navigation/CustomActions.js b/src/libs/Navigation/CustomActions.js
index 245d5d7340ba..aa3083c573d9 100644
--- a/src/libs/Navigation/CustomActions.js
+++ b/src/libs/Navigation/CustomActions.js
@@ -6,40 +6,28 @@ import lodashGet from 'lodash/get';
import linkingConfig from './linkingConfig';
import navigationRef from './navigationRef';
+/**
+ * @returns {Object}
+ */
+function getActiveState() {
+ // We use our RootState as the dispatch's state is relative to the active navigator and might not contain our active screen.
+ return navigationRef.current.getRootState();
+}
+
/**
* Go back to the Main Drawer
* @param {Object} navigationRef
*/
function navigateBackToRootDrawer() {
- let isLeavingNestedDrawerNavigator = false;
-
- // This should take us to the first view of the modal's stack navigator
- navigationRef.current.dispatch((state) => {
- // If this is a nested drawer navigator then we pop the screen and
- // prevent calling goBack() as it's default behavior is to toggle open the active drawer
- if (state.type === 'drawer') {
- isLeavingNestedDrawerNavigator = true;
- return StackActions.pop();
- }
-
- // If there are multiple routes then we can pop back to the first route
- if (state.routes.length > 1) {
- return StackActions.popToTop();
- }
-
- // Otherwise, we are already on the last page of a modal so just do nothing here as goBack() will navigate us
- // back to the screen we were on before we opened the modal.
- return StackActions.pop(0);
+ const activeState = getActiveState();
+
+ // To navigate to the main drawer Route, pop to the first route on the Root Stack Navigator as the main drawer is always the first route that is activated.
+ // It will pop all fullscreen and RHN modals that are over the main drawer.
+ // It won't work when the main drawer is not the first route of the Root Stack Navigator which is not the case ATM.
+ navigationRef.current.dispatch({
+ ...StackActions.popToTop(),
+ target: activeState.key,
});
-
- if (isLeavingNestedDrawerNavigator) {
- return;
- }
-
- // Navigate back to where we were before we launched the modal
- if (navigationRef.current.canGoBack()) {
- navigationRef.current.goBack();
- }
}
/**
@@ -69,14 +57,6 @@ function getScreenNameFromState(state) {
return getRouteFromState(state).name || '';
}
-/**
- * @returns {Object}
- */
-function getActiveState() {
- // We use our RootState as the dispatch's state is relative to the active navigator and might not contain our active screen.
- return navigationRef.current.getRootState();
-}
-
/**
* Special accomodation must be made for navigating to a screen inside a DrawerNavigator (e.g. our ReportScreen). The web/mWeb default behavior when
* calling "navigate()" does not give us the browser history we would expect for a typical web paradigm (e.g. that navigating from one screen another
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index eb396beae293..59aea4001b4c 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -218,6 +218,11 @@ export default {
RequestCall_Root: ROUTES.REQUEST_CALL,
},
},
+ Wallet_Statement: {
+ screens: {
+ WalletStatement_Root: ROUTES.WALLET_STATEMENT_WITH_DATE,
+ },
+ },
},
},
};
diff --git a/src/libs/Network.js b/src/libs/Network.js
index bec4bc711cc1..3c6ee338a36b 100644
--- a/src/libs/Network.js
+++ b/src/libs/Network.js
@@ -233,7 +233,7 @@ function processNetworkRequestQueue() {
.catch((error) => {
// When the request did not reach its destination add it back the queue to be retried
const shouldRetry = lodashGet(queuedRequest, 'data.shouldRetry');
- if (shouldRetry) {
+ if (shouldRetry && error.name !== CONST.ERROR.REQUEST_CANCELLED) {
const retryCount = NetworkRequestQueue.incrementRetries(queuedRequest);
getLogger().info('A retrieable request failed', false, {
retryCount,
@@ -246,7 +246,7 @@ function processNetworkRequestQueue() {
return;
}
- getLogger().alert('Request was retried too many times with no success. No more retries left');
+ getLogger().info('Request was retried too many times with no success. No more retries left');
}
onError(queuedRequest, error);
@@ -296,10 +296,13 @@ function post(command, data = {}, type = CONST.NETWORK.METHOD.POST, shouldUseSec
shouldUseSecure,
};
- // All requests should be retried by default
- if (_.isUndefined(request.data.shouldRetry)) {
- request.data.shouldRetry = true;
- }
+ // By default, request are retry-able and cancellable
+ // (e.g. any requests currently happening when the user logs out are cancelled)
+ request.data = {
+ ...data,
+ shouldRetry: lodashGet(data, 'shouldRetry', true),
+ canCancel: lodashGet(data, 'canCancel', true),
+ };
// Add the request to a queue of actions to perform
networkRequestQueue.push(request);
@@ -339,10 +342,12 @@ function registerParameterEnhancer(callback) {
}
/**
- * Clear the queue so all pending requests will be cancelled
+ * Clear the queue and cancels all pending requests
+ * Non-cancellable requests like Log would not be cleared
*/
function clearRequestQueue() {
- networkRequestQueue = [];
+ networkRequestQueue = _.filter(networkRequestQueue, r => !r.data.canCancel);
+ HttpUtils.cancelPendingRequests();
}
export {
diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js
index c052fcd0bf43..a8dcd55919c9 100644
--- a/src/libs/ValidationUtils.js
+++ b/src/libs/ValidationUtils.js
@@ -321,6 +321,33 @@ function doesFailCharacterLimit(maxLength, valuesToBeValidated) {
return _.map(valuesToBeValidated, value => value.length > maxLength);
}
+/**
+ * Checks if is one of the certain names which are reserved for default rooms
+ * and should not be used for policy rooms.
+ *
+ * @param {String} roomName
+ * @returns {Boolean}
+ */
+function isReservedRoomName(roomName) {
+ return _.contains(CONST.REPORT.RESERVED_ROOM_NAMES, roomName);
+}
+
+/**
+ * Checks if the room name already exists.
+ *
+ * @param {String} roomName
+ * @param {Object} reports
+ * @param {String} policyID
+ * @returns {Boolean}
+ */
+function isExistingRoomName(roomName, reports, policyID) {
+ return _.some(
+ reports,
+ report => report && report.policyID === policyID
+ && report.reportName === roomName,
+ );
+}
+
export {
meetsAgeRequirements,
isValidAddress,
@@ -344,4 +371,6 @@ export {
isValidRoutingNumber,
isValidSSNLastFour,
doesFailCharacterLimit,
+ isReservedRoomName,
+ isExistingRoomName,
};
diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js
index 422f2b6881c6..dc149fa6926c 100644
--- a/src/libs/actions/App.js
+++ b/src/libs/actions/App.js
@@ -80,7 +80,7 @@ AppState.addEventListener('change', (nextAppState) => {
});
function triggerUpdateAvailable() {
- Onyx.merge(ONYXKEYS.UPDATE_AVAILABLE, true);
+ Onyx.set(ONYXKEYS.UPDATE_AVAILABLE, true);
}
export {
diff --git a/src/libs/actions/EmojiPickerAction.js b/src/libs/actions/EmojiPickerAction.js
new file mode 100644
index 000000000000..2a355b65580c
--- /dev/null
+++ b/src/libs/actions/EmojiPickerAction.js
@@ -0,0 +1,31 @@
+import React from 'react';
+
+const emojiPickerRef = React.createRef();
+
+/**
+ * Show the ReportActionContextMenu modal popover.
+ *
+ * @param {Function} [onModalHide=() => {}] - Run a callback when Modal hides.
+ * @param {Function} [onEmojiSelected=() => {}] - Run a callback when Emoji selected.
+ * @param {Element} emojiPopoverAnchor - Element on which EmojiPicker is anchored
+ */
+function showEmojiPicker(
+ onModalHide = () => {},
+ onEmojiSelected = () => {},
+ emojiPopoverAnchor,
+) {
+ if (!emojiPickerRef.current) {
+ return;
+ }
+
+ emojiPickerRef.current.showEmojiPicker(
+ onModalHide,
+ onEmojiSelected,
+ emojiPopoverAnchor,
+ );
+}
+
+export {
+ emojiPickerRef,
+ showEmojiPicker,
+};
diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js
index 94a1b6cedb6f..fd9fa25bed38 100644
--- a/src/libs/actions/PaymentMethods.js
+++ b/src/libs/actions/PaymentMethods.js
@@ -200,7 +200,7 @@ function transferWalletBalance(paymentMethod) {
if (response.jsonCode !== 200) {
throw new Error(response.message);
}
- Onyx.merge(ONYXKEYS.USER_WALLET, {balance: 0});
+ Onyx.merge(ONYXKEYS.USER_WALLET, {currentBalance: 0});
Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {shouldShowConfirmModal: true, loading: false});
Navigation.navigate(ROUTES.SETTINGS_PAYMENTS);
}).catch(() => {
diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js
index faf7e126c09d..f7a82ce8699d 100644
--- a/src/libs/actions/PersonalDetails.js
+++ b/src/libs/actions/PersonalDetails.js
@@ -326,15 +326,15 @@ function setAvatar(file) {
}
/**
- * Deletes the user's avatar image
+ * Replaces the user's avatar image with a default avatar
*
- * @param {String} login
+ * @param {String} defaultAvatarURL
*/
-function deleteAvatar(login) {
+function deleteAvatar(defaultAvatarURL) {
// We don't want to save the default avatar URL in the backend since we don't want to allow
// users the option of removing the default avatar, instead we'll save an empty string
API.PersonalDetails_Update({details: JSON.stringify({avatar: ''})});
- mergeLocalPersonalDetails({avatar: OptionsListUtils.getDefaultAvatar(login)});
+ mergeLocalPersonalDetails({avatar: defaultAvatarURL});
}
// When the app reconnects from being offline, fetch all of the personal details
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index 1ec091423f34..6da07c7211fe 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -121,9 +121,9 @@ function getUnreadActionCount(report) {
* @param {Object} report
* @return {String[]}
*/
-function getParticipantEmailsFromReport({sharedReportList}) {
+function getParticipantEmailsFromReport({sharedReportList, reportNameValuePairs}) {
const emailArray = _.map(sharedReportList, participant => participant.email);
- return _.without(emailArray, currentUserEmail);
+ return ReportUtils.isChatRoom(reportNameValuePairs) ? emailArray : _.without(emailArray, currentUserEmail);
}
/**
@@ -144,8 +144,8 @@ function getChatReportName(fullReport, chatType) {
: '')}`;
}
- // For a basic policy room, return its original name
- if (ReportUtils.isUserCreatedPolicyRoom({chatType})) {
+ // For a basic policy room or a Policy Expense chat, return its original name
+ if (ReportUtils.isUserCreatedPolicyRoom({chatType}) || ReportUtils.isPolicyExpenseChat({chatType})) {
return fullReport.reportName;
}
@@ -224,6 +224,7 @@ function getSimplifiedReportObject(report) {
statusNum: report.status,
oldPolicyName,
visibility,
+ isOwnPolicyExpenseChat: lodashGet(report, ['isOwnPolicyExpenseChat'], false),
};
}
@@ -828,8 +829,13 @@ function subscribeToReportCommentPushNotifications() {
// Open correct report when push notification is clicked
PushNotification.onSelected(PushNotification.TYPE.REPORT_COMMENT, ({reportID}) => {
- Navigation.setDidTapNotification();
- Linking.openURL(`${CONST.DEEPLINK_BASE_URL}${ROUTES.getReportRoute(reportID)}`);
+ if (Navigation.isReady()) {
+ Navigation.navigate(ROUTES.getReportRoute(reportID));
+ } else {
+ // Navigation container is not yet ready, use deep linking to open the correct report instead
+ Navigation.setDidTapNotification();
+ Linking.openURL(`${CONST.DEEPLINK_BASE_URL}${ROUTES.getReportRoute(reportID)}`);
+ }
});
}
@@ -1504,7 +1510,6 @@ function navigateToConciergeChat() {
}
Navigation.navigate(ROUTES.getReportRoute(conciergeChatReportID));
- Navigation.closeDrawer();
}
/**
diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js
index bb58c7983ff9..9c1d5516fadc 100644
--- a/src/libs/actions/Session/index.js
+++ b/src/libs/actions/Session/index.js
@@ -136,7 +136,6 @@ function fetchAccountDetails(login) {
});
Onyx.merge(ONYXKEYS.ACCOUNT, {
accountExists: response.accountExists,
- requiresTwoFactorAuth: response.requiresTwoFactorAuth,
validated: response.validated,
closed: response.isClosed,
forgotPassword: false,
@@ -250,6 +249,10 @@ function signIn(password, twoFactorAuthCode) {
createTemporaryLogin(authToken, email);
})
.catch((error) => {
+ if (error.message === 'passwordForm.error.twoFactorAuthenticationEnabled') {
+ Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, loading: false});
+ return;
+ }
Onyx.merge(ONYXKEYS.ACCOUNT, {error: Localize.translateLocal(error.message), loading: false});
});
}
diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js
index be838e817111..c9116f48e34e 100644
--- a/src/libs/actions/SignInRedirect.js
+++ b/src/libs/actions/SignInRedirect.js
@@ -1,5 +1,6 @@
import Onyx from 'react-native-onyx';
import ONYXKEYS from '../../ONYXKEYS';
+import * as Network from '../Network';
let currentActiveClients;
Onyx.connect({
@@ -38,12 +39,16 @@ function clearStorageAndRedirect(errorMessage) {
}
/**
- * Clears the Onyx store and redirects to the sign in page.
+ * Cleanup actions resulting in the user being redirected to the Sign-in page
+ * - Clears the Onyx store - removing the authToken redirects the user to the Sign-in page
+ * - Cancels pending network calls - any lingering requests are discarded to prevent unwanted storage writes
+ *
* Normally this method would live in Session.js, but that would cause a circular dependency with Network.js.
*
* @param {String} [errorMessage] error message to be displayed on the sign in page
*/
function redirectToSignIn(errorMessage) {
+ Network.clearRequestQueue();
clearStorageAndRedirect(errorMessage);
}
diff --git a/src/libs/actions/WelcomeActions.js b/src/libs/actions/WelcomeActions.js
new file mode 100644
index 000000000000..c3b6c7f18d1f
--- /dev/null
+++ b/src/libs/actions/WelcomeActions.js
@@ -0,0 +1,84 @@
+import Onyx from 'react-native-onyx';
+import _ from 'underscore';
+import lodashGet from 'lodash/get';
+import Navigation from '../Navigation/Navigation';
+import * as ReportUtils from '../reportUtils';
+import ROUTES from '../../ROUTES';
+import * as Policy from './Policy';
+import ONYXKEYS from '../../ONYXKEYS';
+import NameValuePair from './NameValuePair';
+import CONST from '../../CONST';
+
+/* Flag for new users used to show welcome actions on first load */
+let isFirstTimeNewExpensifyUser = false;
+Onyx.connect({
+ key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
+ callback: val => isFirstTimeNewExpensifyUser = val,
+});
+
+const allReports = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ callback: (val, key) => {
+ if (!val || !key) {
+ return;
+ }
+
+ allReports[key] = {...allReports[key], ...val};
+ },
+});
+
+const allPolicies = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ callback: (val, key) => {
+ if (!val || !key) {
+ return;
+ }
+
+ allPolicies[key] = {...allPolicies[key], ...val};
+ },
+});
+
+/**
+ * Shows a welcome action on first login
+ *
+ * @param {Object} params
+ * @param {Object} params.routes
+ * @param {Function} params.toggleCreateMenu
+ */
+function show({routes, toggleCreateMenu}) {
+ // NOTE: This setTimeout is required due to a bug in react-navigation where modals do not display properly in a drawerContent
+ // This is a short-term workaround, see this issue for updates on a long-term solution: https://github.com/Expensify/App/issues/5296
+ setTimeout(() => {
+ if (!isFirstTimeNewExpensifyUser) {
+ return;
+ }
+
+ // Set the NVP back to false so we don't automatically run welcome actions again
+ NameValuePair.set(CONST.NVP.IS_FIRST_TIME_NEW_EXPENSIFY_USER, false, ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER);
+
+ // We want to display the Workspace chat first since that means a user is already in a Workspace and doesn't need to create another one
+ const workspaceChatReport = _.find(allReports, report => ReportUtils.isPolicyExpenseChat(report));
+ if (workspaceChatReport) {
+ Navigation.navigate(ROUTES.getReportRoute(workspaceChatReport.reportID));
+ return;
+ }
+
+ // If we are rendering the SidebarScreen at the same time as a workspace route that means we've already created a workspace via workspace/new and should not open the global
+ // create menu right now.
+ const topRouteName = lodashGet(_.last(routes), 'name', '');
+ const isDisplayingWorkspaceRoute = topRouteName.toLowerCase().includes('workspace');
+
+ // It's also possible that we already have a workspace policy. In either case we will not toggle the menu but do still want to set the NVP in this case
+ // since the user does not need to create a workspace.
+ if (!Policy.isAdminOfFreePolicy(allPolicies) && !isDisplayingWorkspaceRoute) {
+ toggleCreateMenu();
+ }
+ }, 1500);
+}
+
+export {
+ // eslint-disable-next-line import/prefer-default-export
+ show,
+};
diff --git a/src/libs/checkForUpdates.js b/src/libs/checkForUpdates.js
index 2f915212a0bc..fbf7ee84a8a7 100644
--- a/src/libs/checkForUpdates.js
+++ b/src/libs/checkForUpdates.js
@@ -1,9 +1,9 @@
const _ = require('underscore');
-const UPDATE_INTERVAL = 1000 * 60 * 60;
+const UPDATE_INTERVAL = 1000 * 60 * 60 * 8;
/**
- * Check for updates every hour and perform and platform-specific update
+ * Check for updates every 8 hours and perform and platform-specific update
*
* @param {Object} platformSpecificUpdater
* @param {Function} platformSpecificUpdater.update
diff --git a/src/libs/reportUtils.js b/src/libs/reportUtils.js
index 0180b260b16b..509275bb420b 100644
--- a/src/libs/reportUtils.js
+++ b/src/libs/reportUtils.js
@@ -102,6 +102,16 @@ function isUserCreatedPolicyRoom(report) {
return lodashGet(report, ['chatType'], '') === CONST.REPORT.CHAT_TYPE.POLICY_ROOM;
}
+/**
+ * Whether the provided report is a Policy Expense chat.
+ * @param {Object} report
+ * @param {String} report.chatType
+ * @returns {Boolean}
+ */
+function isPolicyExpenseChat(report) {
+ return lodashGet(report, ['chatType'], '') === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT;
+}
+
/**
* Whether the provided report is a chat room
* @param {Object} report
@@ -257,4 +267,5 @@ export {
canShowReportRecipientLocalTime,
formatReportLastMessageText,
chatIncludesConcierge,
+ isPolicyExpenseChat,
};
diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js
index ba3dc07f410a..238ff4835a06 100644
--- a/src/pages/EnablePayments/AdditionalDetailsStep.js
+++ b/src/pages/EnablePayments/AdditionalDetailsStep.js
@@ -70,7 +70,6 @@ class AdditionalDetailsStep extends React.Component {
this.fieldNameTranslationKeys = {
legalFirstName: 'additionalDetailsStep.legalFirstNameLabel',
- legalMiddleName: 'additionalDetailsStep.legalMiddleNameLabel',
legalLastName: 'additionalDetailsStep.legalLastNameLabel',
addressStreet: 'common.personalAddress',
addressCity: 'common.city',
@@ -83,7 +82,6 @@ class AdditionalDetailsStep extends React.Component {
this.state = {
legalFirstName: lodashGet(props.walletAdditionalDetailsDraft, 'legalFirstName', ''),
- legalMiddleName: lodashGet(props.walletAdditionalDetailsDraft, 'legalMiddleName', ''),
legalLastName: lodashGet(props.walletAdditionalDetailsDraft, 'legalLastName', ''),
addressStreet: lodashGet(props.walletAdditionalDetailsDraft, 'addressStreet', ''),
addressCity: lodashGet(props.walletAdditionalDetailsDraft, 'addressCity', ''),
@@ -94,13 +92,17 @@ class AdditionalDetailsStep extends React.Component {
ssn: lodashGet(props.walletAdditionalDetailsDraft, 'ssn', ''),
};
- const formHelper = new FormHelper({
+ this.formHelper = new FormHelper({
errorPath: 'walletAdditionalDetails.errorFields',
setErrors: Wallet.setAdditionalDetailsErrors,
});
+ }
- this.getErrors = () => formHelper.getErrors(props);
- this.clearError = path => formHelper.clearError(props, path);
+ /**
+ * @returns {Object}
+ */
+ getErrors() {
+ return this.formHelper.getErrors(this.props);
}
/**
@@ -115,6 +117,13 @@ class AdditionalDetailsStep extends React.Component {
return `${this.props.translate(this.fieldNameTranslationKeys[fieldName])} ${this.props.translate('common.isRequiredField')}.`;
}
+ /**
+ * @param {String} path
+ */
+ clearError(path) {
+ this.formHelper.clearError(this.props, path);
+ }
+
/**
* @returns {Boolean}
*/
@@ -197,12 +206,6 @@ class AdditionalDetailsStep extends React.Component {
value={this.state.legalFirstName}
errorText={this.getErrorText('legalFirstName')}
/>
- this.clearErrorAndSetValue('legalMiddleName', val)}
- value={this.state.legalMiddleName}
- />
diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js
index 19d43a40807d..ab9b6e7130fa 100644
--- a/src/pages/ReportSettingsPage.js
+++ b/src/pages/ReportSettingsPage.js
@@ -18,6 +18,8 @@ import Button from '../components/Button';
import RoomNameInput from '../components/RoomNameInput';
import Picker from '../components/Picker';
import withFullPolicy, {fullPolicyDefaultProps, fullPolicyPropTypes} from './workspace/withFullPolicy';
+import * as ValidationUtils from '../libs/ValidationUtils';
+import Growl from '../libs/Growl';
const propTypes = {
@@ -49,6 +51,18 @@ const propTypes = {
notificationPreference: PropTypes.string,
}).isRequired,
+ /** All reports shared with the user */
+ reports: PropTypes.shape({
+ /** The report name */
+ reportName: PropTypes.string,
+
+ /** The report type */
+ type: PropTypes.string,
+
+ /** ID of the policy */
+ policyID: PropTypes.string,
+ }).isRequired,
+
/** The policies which the user has access to and which the report could be tied to */
policies: PropTypes.shape({
/** The policy name */
@@ -84,12 +98,61 @@ class ReportSettingsPage extends Component {
this.state = {
newRoomName: this.props.report.reportName,
- error: '',
+ errors: {},
};
+
+ this.validateAndRenameReport = this.validateAndRenameReport.bind(this);
+ }
+
+ validateAndRenameReport() {
+ if (!this.validate()) {
+ return;
+ }
+ if (this.props.report.reportName === this.state.newRoomName) {
+ Growl.success(this.props.translate('newRoomPage.policyRoomRenamed'));
+ return;
+ }
+ Report.renameReport(this.props.report.reportID, this.state.newRoomName);
+ }
+
+ validate() {
+ const errors = {};
+
+ // We error if the user doesn't enter a room name or left blank
+ if (!this.state.newRoomName || this.state.newRoomName === CONST.POLICY.ROOM_PREFIX) {
+ errors.newRoomName = this.props.translate('newRoomPage.pleaseEnterRoomName');
+ }
+
+ // We error if the room name already exists. We don't error if the room name matches same as previous.
+ if (ValidationUtils.isExistingRoomName(this.state.newRoomName, this.props.reports, this.props.report.policyID) && this.state.newRoomName !== this.props.report.reportName) {
+ errors.newRoomName = this.props.translate('newRoomPage.roomAlreadyExistsError');
+ }
+
+ // Certain names are reserved for default rooms and should not be used for policy rooms.
+ if (ValidationUtils.isReservedRoomName(this.state.newRoomName)) {
+ errors.newRoomName = this.props.translate('newRoomPage.roomNameReservedError');
+ }
+
+ this.setState({errors});
+ return _.isEmpty(errors);
+ }
+
+ /**
+ * @param {String} inputKey
+ * @param {String} value
+ */
+ clearErrorAndSetValue(inputKey, value) {
+ this.setState(prevState => ({
+ [inputKey]: value,
+ errors: {
+ ...prevState.errors,
+ [inputKey]: '',
+ },
+ }));
}
render() {
- const shouldDisableRename = ReportUtils.isDefaultRoom(this.props.report) || ReportUtils.isArchivedRoom(this.props.report) || this.props.isLoadingRenamePolicyRoom;
+ const shouldDisableRename = ReportUtils.isDefaultRoom(this.props.report) || ReportUtils.isArchivedRoom(this.props.report);
const linkedWorkspace = _.find(this.props.policies, policy => policy.id === this.props.report.policyID);
return (
@@ -128,26 +191,22 @@ class ReportSettingsPage extends Component {
this.setState({newRoomName})}
- onChangeError={error => this.setState({error})}
initialValue={this.state.newRoomName}
- disabled={shouldDisableRename}
policyID={linkedWorkspace && linkedWorkspace.id}
+ errorText={this.state.errors.newRoomName}
+ onChangeText={newRoomName => this.clearErrorAndSetValue('newRoomName', newRoomName)}
+ disabled={shouldDisableRename}
/>
@@ -199,5 +258,8 @@ export default compose(
isLoadingRenamePolicyRoom: {
key: ONYXKEYS.IS_LOADING_RENAME_POLICY_ROOM,
},
+ reports: {
+ key: ONYXKEYS.COLLECTION.REPORT,
+ },
}),
)(ReportSettingsPage);
diff --git a/src/pages/RequestCallPage.js b/src/pages/RequestCallPage.js
index 3606bd7a77db..6f73e70fc248 100644
--- a/src/pages/RequestCallPage.js
+++ b/src/pages/RequestCallPage.js
@@ -200,30 +200,34 @@ class RequestCallPage extends Component {
* so we return empty strings instead.
* @param {String} login
* @param {String} displayName
+ * @param {String} firstName
+ * @param {String} lastName
*
* @returns {Object}
*/
- getFirstAndLastName({login, displayName}) {
- let firstName;
- let lastName;
-
+ getFirstAndLastName({
+ login,
+ displayName,
+ firstName,
+ lastName,
+ }) {
+ if (firstName || lastName) {
+ return {firstName: firstName || '', lastName: lastName || ''};
+ }
if (Str.removeSMSDomain(login) === displayName) {
- firstName = '';
- lastName = '';
- } else {
- const firstSpaceIndex = displayName.indexOf(' ');
- const lastSpaceIndex = displayName.lastIndexOf(' ');
-
- if (firstSpaceIndex === -1) {
- firstName = displayName;
- lastName = '';
- } else {
- firstName = displayName.substring(0, firstSpaceIndex);
- lastName = displayName.substring(lastSpaceIndex);
- }
+ return {firstName: '', lastName: ''};
+ }
+
+ const firstSpaceIndex = displayName.indexOf(' ');
+ const lastSpaceIndex = displayName.lastIndexOf(' ');
+ if (firstSpaceIndex === -1) {
+ return {firstName: displayName, lastName: ''};
}
- return {firstName, lastName};
+ return {
+ firstName: displayName.substring(0, firstSpaceIndex).trim(),
+ lastName: displayName.substring(lastSpaceIndex).trim(),
+ };
}
getWaitTimeMessageKey(minutes) {
@@ -313,32 +317,27 @@ class RequestCallPage extends Component {
onChangeLastName={lastName => this.setState({lastName})}
style={[styles.mv4]}
/>
-
-
- this.setState({phoneNumber})}
- />
-
-
- this.setState({phoneExtension})}
- />
-
-
+ this.setState({phoneNumber})}
+ />
+ this.setState({phoneExtension})}
+ containerStyles={[styles.mt4]}
+ />
{this.getWaitTimeMessage()}
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index d2ded8b80ef4..14234980fa89 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -98,6 +98,8 @@ const HeaderView = (props) => {
// these users via alternative means. It is possible to request a call with Concierge so we leave the option for them.
const shouldShowCallButton = isConcierge || !isAutomatedExpensifyAccount;
+ const avatarTooltip = isChatRoom ? undefined : _.pluck(displayNamesWithTooltips, 'tooltip');
+
return (
@@ -111,7 +113,7 @@ const HeaderView = (props) => {
)}
- {props.report && props.report.reportName && (
+ {Boolean(props.report && props.report.reportName) && (
{
secondAvatarStyle={[styles.secondAvatarHovered]}
isChatRoom={isChatRoom}
isArchivedRoom={ReportUtils.isArchivedRoom(props.report)}
+ avatarTooltips={avatarTooltip}
/>
this.setIsFocused(true)}
onBlur={() => this.setIsFocused(false)}
onPasteFile={file => displayFileInModal({file})}
@@ -566,11 +565,10 @@ class ReportActionCompose extends React.Component {
>
)}
- this.focus(true)}
onEmojiSelected={this.addEmojiToTextBox}
- onBeforeShowEmojiPicker={() => this.textInput.blur()}
/>
diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js
index 234afa196196..82d98997e2c3 100644
--- a/src/pages/home/report/ReportActionItemSingle.js
+++ b/src/pages/home/report/ReportActionItemSingle.js
@@ -15,6 +15,7 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal
import Navigation from '../../../libs/Navigation/Navigation';
import ROUTES from '../../../ROUTES';
import {withPersonalDetails} from '../../../components/OnyxProvider';
+import Tooltip from '../../../components/Tooltip';
const propTypes = {
/** All the data of the action */
@@ -63,10 +64,12 @@ const ReportActionItemSingle = (props) => {
return (
showUserDetails(props.action.actorEmail)}>
-
+
+
+
{props.showHeader ? (
diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js
index b7da95451c17..0e8e924b02d0 100755
--- a/src/pages/home/report/ReportActionsView.js
+++ b/src/pages/home/report/ReportActionsView.js
@@ -38,6 +38,8 @@ import ONYXKEYS from '../../../ONYXKEYS';
import {withPersonalDetails} from '../../../components/OnyxProvider';
import currentUserPersonalDetailsPropsTypes from '../../settings/Profile/currentUserPersonalDetailsPropsTypes';
import {participantPropTypes} from '../sidebar/optionPropTypes';
+import EmojiPicker from '../../../components/EmojiPicker';
+import * as EmojiPickerAction from '../../../libs/actions/EmojiPickerAction';
const propTypes = {
/** The ID of the report actions will be created for */
@@ -578,6 +580,7 @@ class ReportActionsView extends React.Component {
extraData={extraData}
/>
+
>
);
}
diff --git a/src/pages/home/sidebar/OptionRow.js b/src/pages/home/sidebar/OptionRow.js
index 383542cc9347..6141fc8d7e29 100644
--- a/src/pages/home/sidebar/OptionRow.js
+++ b/src/pages/home/sidebar/OptionRow.js
@@ -129,6 +129,8 @@ const OptionRow = (props) => {
},
);
+ const avatarTooltips = props.showTitleTooltip && !props.option.isChatRoom && !props.option.isArchivedRoom ? _.pluck(displayNamesWithTooltips, 'tooltip') : undefined;
+
return (
{hovered => (
@@ -175,6 +177,7 @@ const OptionRow = (props) => {
]}
isChatRoom={props.option.isChatRoom}
isArchivedRoom={props.option.isArchivedRoom}
+ avatarTooltips={avatarTooltips}
/>
)
}
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index 40c36ad8f850..5a88503a660d 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -235,7 +235,7 @@ class SidebarLinks extends React.Component {
diff --git a/src/pages/home/sidebar/SidebarScreen.js b/src/pages/home/sidebar/SidebarScreen.js
index e98ada2bbc02..3d3d06ee5115 100755
--- a/src/pages/home/sidebar/SidebarScreen.js
+++ b/src/pages/home/sidebar/SidebarScreen.js
@@ -1,5 +1,5 @@
-import _ from 'underscore';
import lodashGet from 'lodash/get';
+import _ from 'underscore';
import React, {Component} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
@@ -22,15 +22,12 @@ import Permissions from '../../../libs/Permissions';
import ONYXKEYS from '../../../ONYXKEYS';
import * as Policy from '../../../libs/actions/Policy';
import Performance from '../../../libs/Performance';
-import NameValuePair from '../../../libs/actions/NameValuePair';
+import * as WelcomeAction from '../../../libs/actions/WelcomeActions';
const propTypes = {
/* Beta features list */
betas: PropTypes.arrayOf(PropTypes.string).isRequired,
- /* Flag for new users used to open the Global Create menu on first load */
- isFirstTimeNewExpensifyUser: PropTypes.bool,
-
/* Is workspace is being created by the user? */
isCreatingWorkspace: PropTypes.bool,
@@ -39,7 +36,6 @@ const propTypes = {
...withLocalizePropTypes,
};
const defaultProps = {
- isFirstTimeNewExpensifyUser: false,
isCreatingWorkspace: false,
};
@@ -61,29 +57,8 @@ class SidebarScreen extends Component {
Performance.markStart(CONST.TIMING.SIDEBAR_LOADED);
Timing.start(CONST.TIMING.SIDEBAR_LOADED, true);
- // NOTE: This setTimeout is required due to a bug in react-navigation where modals do not display properly in a drawerContent
- // This is a short-term workaround, see this issue for updates on a long-term solution: https://github.com/Expensify/App/issues/5296
- setTimeout(() => {
- if (!this.props.isFirstTimeNewExpensifyUser) {
- return;
- }
-
- // If we are rendering the SidebarScreen at the same time as a workspace route that means we've already created a workspace via workspace/new and should not open the global
- // create menu right now.
- const routes = lodashGet(this.props.navigation.getState(), 'routes', []);
- const topRouteName = lodashGet(_.last(routes), 'name', '');
- const isDisplayingWorkspaceRoute = topRouteName.toLowerCase().includes('workspace');
-
- // It's also possible that we already have a workspace policy. In either case we will not toggle the menu but do still want to set the NVP in this case since the user does
- // not need to create a workspace.
- if (!Policy.isAdminOfFreePolicy(this.props.allPolicies) && !isDisplayingWorkspaceRoute) {
- this.toggleCreateMenu();
- }
-
- // Set the NVP back to false so we don't automatically open the menu again
- // Note: this may need to be moved if this NVP is used for anything else later
- NameValuePair.set(CONST.NVP.IS_FIRST_TIME_NEW_EXPENSIFY_USER, false, ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER);
- }, 1500);
+ const routes = lodashGet(this.props.navigation.getState(), 'routes', []);
+ WelcomeAction.show({routes, toggleCreateMenu: this.toggleCreateMenu});
}
/**
@@ -122,6 +97,8 @@ class SidebarScreen extends Component {
}
render() {
+ // Workspaces are policies with type === 'free'
+ const workspaces = _.filter(this.props.allPolicies, policy => policy && policy.type === CONST.POLICY.TYPE.FREE);
return (
Navigation.navigate(ROUTES.NEW_GROUP),
},
- ...(Permissions.canUsePolicyRooms(this.props.betas) ? [
+ ...(Permissions.canUsePolicyRooms(this.props.betas) && workspaces.length ? [
{
icon: Expensicons.Hashtag,
text: this.props.translate('sidebarScreen.newRoom'),
@@ -221,9 +198,6 @@ export default compose(
betas: {
key: ONYXKEYS.BETAS,
},
- isFirstTimeNewExpensifyUser: {
- key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
- },
isCreatingWorkspace: {
key: ONYXKEYS.IS_CREATING_WORKSPACE,
},
diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js
index aa81512d876f..933583d1210c 100644
--- a/src/pages/iou/IOUCurrencySelection.js
+++ b/src/pages/iou/IOUCurrencySelection.js
@@ -9,8 +9,7 @@ import ONYXKEYS from '../../ONYXKEYS';
import * as OptionsListUtils from '../../libs/OptionsListUtils';
import Text from '../../components/Text';
import OptionRow from '../home/sidebar/OptionRow';
-import themeColors from '../../styles/themes/default';
-import TextInputWithFocusStyles from '../../components/TextInputWithFocusStyles';
+import TextInput from '../../components/TextInput';
import Navigation from '../../libs/Navigation/Navigation';
import ScreenWrapper from '../../components/ScreenWrapper';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
@@ -174,14 +173,11 @@ class IOUCurrencySelection extends Component {
- this.textInput = el}
- style={[styles.textInput]}
value={this.state.searchValue}
onChangeText={this.changeSearchValue}
placeholder={this.props.translate('common.search')}
- placeholderTextColor={themeColors.placeholderText}
/>
diff --git a/src/pages/settings/Payments/PaymentMethodList.js b/src/pages/settings/Payments/PaymentMethodList.js
index f4b4c3caff23..9d01deefe9a6 100644
--- a/src/pages/settings/Payments/PaymentMethodList.js
+++ b/src/pages/settings/Payments/PaymentMethodList.js
@@ -97,11 +97,30 @@ class PaymentMethodList extends Component {
}
/**
- * Take all of the different payment methods and create a list that can be easily digested by renderItem
- *
+ * @param {Boolean} isDefault
+ * @returns {*}
+ */
+ getDefaultBadgeText(isDefault = false) {
+ if (!isDefault) {
+ return null;
+ }
+
+ const defaultablePaymentMethodCount = _.reduce(this.getFilteredPaymentMethods(), (count, method) => (
+ (method.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT || method.accountType === CONST.PAYMENT_METHODS.DEBIT_CARD)
+ ? count + 1
+ : count
+ ), 0);
+ if (defaultablePaymentMethodCount <= 1) {
+ return null;
+ }
+
+ return this.props.translate('paymentMethodList.defaultPaymentMethod');
+ }
+
+ /**
* @returns {Array}
*/
- createPaymentMethodList() {
+ getFilteredPaymentMethods() {
let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(this.props.bankAccountList, this.props.cardList, this.props.payPalMeUsername, this.props.userWallet);
if (!_.isEmpty(this.props.filterType)) {
@@ -116,6 +135,17 @@ class PaymentMethodList extends Component {
wrapperStyle: this.isPaymentMethodActive(paymentMethod) ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] : null,
}));
+ return combinedPaymentMethods;
+ }
+
+ /**
+ * Take all of the different payment methods and create a list that can be easily digested by renderItem
+ *
+ * @returns {Array}
+ */
+ createPaymentMethodList() {
+ const combinedPaymentMethods = this.getFilteredPaymentMethods();
+
// If we have not added any payment methods, show a default empty state
if (_.isEmpty(combinedPaymentMethods)) {
combinedPaymentMethods.push({
@@ -172,7 +202,7 @@ class PaymentMethodList extends Component {
iconFill={item.iconFill}
iconHeight={item.iconSize}
iconWidth={item.iconSize}
- badgeText={item.isDefault ? this.props.translate('paymentMethodList.defaultPaymentMethod') : null}
+ badgeText={this.getDefaultBadgeText(item.isDefault)}
wrapperStyle={item.wrapperStyle}
shouldShowSelectedState={this.props.shouldShowSelectedState}
isSelected={this.props.selectedMethodID === item.methodID}
diff --git a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
index 4dc677977f3a..53afcf8253fb 100644
--- a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
+++ b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js
@@ -299,7 +299,7 @@ class BasePaymentsPage extends React.Component {
wrapperStyle={[styles.pv0, styles.ph0, styles.mb4]}
/>
)}
- {Permissions.canUseWallet(this.props.betas) && (
+ {Permissions.canUseWallet(this.props.betas) && !isPayPalMeSelected && (
{
this.setState({
@@ -308,10 +308,9 @@ class BasePaymentsPage extends React.Component {
passwordButtonText: this.props.translate('paymentsPage.setDefaultConfirmation'),
});
}}
- style={[styles.button, isPayPalMeSelected && styles.buttonDisable, styles.alignSelfCenter, styles.w100]}
- disabled={isPayPalMeSelected}
+ style={[styles.button, styles.alignSelfCenter, styles.w100]}
>
-
+
{this.props.translate('paymentsPage.setDefaultConfirmation')}
@@ -326,7 +325,7 @@ class BasePaymentsPage extends React.Component {
style={[
styles.button,
styles.buttonDanger,
- Permissions.canUseWallet(this.props.betas) && styles.mt4,
+ Permissions.canUseWallet(this.props.betas) && !isPayPalMeSelected && styles.mt4,
styles.alignSelfCenter,
styles.w100,
]}
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js
index d0569f6b7777..f208d28b5d1b 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.js
@@ -70,6 +70,9 @@ const timezones = _.map(moment.tz.names(), timezone => ({
class ProfilePage extends Component {
constructor(props) {
super(props);
+
+ this.defaultAvatar = OptionsListUtils.getDefaultAvatar(this.props.myPersonalDetails.login);
+
this.state = {
firstName: props.myPersonalDetails.firstName,
hasFirstNameError: false,
@@ -81,17 +84,15 @@ class ProfilePage extends Component {
selectedTimezone: lodashGet(props.myPersonalDetails.timezone, 'selected', CONST.DEFAULT_TIME_ZONE.selected),
isAutomaticTimezone: lodashGet(props.myPersonalDetails.timezone, 'automatic', CONST.DEFAULT_TIME_ZONE.automatic),
logins: this.getLogins(props.user.loginList),
- avatarImage: null,
- avatarPreviewURL: lodashGet(this.props.myPersonalDetails, 'avatar', OptionsListUtils.getDefaultAvatar(this.props.myPersonalDetails.login)),
- isAvatarUpdated: false,
+ avatar: {uri: lodashGet(this.props.myPersonalDetails, 'avatar', OptionsListUtils.getDefaultAvatar(this.props.myPersonalDetails.login))},
+ isAvatarChanged: false,
};
this.getLogins = this.getLogins.bind(this);
this.setAutomaticTimezone = this.setAutomaticTimezone.bind(this);
this.updatePersonalDetails = this.updatePersonalDetails.bind(this);
this.validateInputs = this.validateInputs.bind(this);
- this.setAvatar = this.setAvatar.bind(this);
- this.removeAvatar = this.removeAvatar.bind(this);
+ this.updateAvatar = this.updateAvatar.bind(this);
}
componentDidUpdate(prevProps) {
@@ -102,10 +103,6 @@ class ProfilePage extends Component {
stateToUpdate = {...stateToUpdate, logins: this.getLogins(this.props.user.loginList)};
}
- if (this.props.myPersonalDetails.avatar !== this.state.avatarPreviewURL && this.props.myPersonalDetails.avatar !== prevProps.myPersonalDetails.avatar) {
- stateToUpdate = {...stateToUpdate, isAvatarUpdated: true};
- }
-
if (_.isEmpty(stateToUpdate)) {
return;
}
@@ -157,22 +154,11 @@ class ProfilePage extends Component {
}
/**
- * Set avatar image
- * @param {Object} img
- */
- setAvatar(img) {
- this.setState({avatarImage: img, avatarPreviewURL: img.uri, isAvatarUpdated: false});
- }
-
- /**
- * Remove the avatar image
+ * Updates the user's avatar image.
+ * @param {Object} avatar
*/
- removeAvatar() {
- this.setState({
- avatarPreviewURL: OptionsListUtils.getDefaultAvatar(this.props.myPersonalDetails.login),
- avatarImage: null,
- isAvatarUpdated: false,
- });
+ updateAvatar(avatar) {
+ this.setState({avatar: _.isUndefined(avatar) ? {uri: OptionsListUtils.getDefaultAvatar(this.props.myPersonalDetails.login)} : avatar, isAvatarChanged: true});
}
/**
@@ -183,14 +169,17 @@ class ProfilePage extends Component {
return;
}
- if (this.state.avatarImage) {
- PersonalDetails.setAvatar(this.state.avatarImage);
- }
+ // Check if the user has modified their avatar
+ if ((this.props.myPersonalDetails.avatar !== this.state.avatar.uri) && this.state.isAvatarChanged) {
+ // If the user removed their profile photo, replace it accordingly with the default avatar
+ if (this.state.avatar.uri.includes('/images/avatars/avatar')) {
+ PersonalDetails.deleteAvatar(this.state.avatar.uri);
+ } else {
+ PersonalDetails.setAvatar(this.state.avatar);
+ }
- // Checks if the user already has an avatar and removePhoto is triggered
- // Avatar having `/images/avatars/avatar` in URL means a profile picture exists for the user
- if (!this.props.myPersonalDetails.avatar.includes('/images/avatars/avatar') && !this.state.avatarImage) {
- PersonalDetails.deleteAvatar(this.props.myPersonalDetails.login);
+ // Reset the changed state
+ this.setState({isAvatarChanged: false});
}
PersonalDetails.setPersonalDetails({
@@ -223,20 +212,13 @@ class ProfilePage extends Component {
value: `${CONST.PRONOUNS.PREFIX}${key}`,
}));
- // Determines if the pronouns/selected pronouns have changed
- const arePronounsUnchanged = this.props.myPersonalDetails.pronouns === this.state.pronouns.trim();
-
- // Determine if the avatar is changed or finished uploading
- const disableSaveWhenAvatarIsProcessed = this.props.myPersonalDetails.avatar === this.state.avatarPreviewURL
- || this.state.isAvatarUpdated
- || this.props.myPersonalDetails.avatarUploading;
-
// Disables button if none of the form values have changed
const isButtonDisabled = (this.props.myPersonalDetails.firstName === this.state.firstName.trim())
&& (this.props.myPersonalDetails.lastName === this.state.lastName.trim())
&& (this.props.myPersonalDetails.timezone.selected === this.state.selectedTimezone)
&& (this.props.myPersonalDetails.timezone.automatic === this.state.isAutomaticTimezone)
- && arePronounsUnchanged && disableSaveWhenAvatarIsProcessed;
+ && (this.props.myPersonalDetails.pronouns === this.state.pronouns.trim())
+ && (!this.state.isAvatarChanged || this.props.myPersonalDetails.avatarUploading);
const pronounsPickerValue = this.state.hasSelfSelectedPronouns ? CONST.PRONOUNS.SELF_SELECT : this.state.pronouns;
@@ -253,9 +235,9 @@ class ProfilePage extends Component {
diff --git a/src/pages/settings/Security/CloseAccountPage.js b/src/pages/settings/Security/CloseAccountPage.js
index 37df0e095396..0dc86b86cd15 100644
--- a/src/pages/settings/Security/CloseAccountPage.js
+++ b/src/pages/settings/Security/CloseAccountPage.js
@@ -86,11 +86,18 @@ class CloseAccountPage extends Component {
{' '}
{this.props.translate('closeAccountPage.closeAccountPermanentlyDeleteData')}
+
+
+ {this.props.translate('closeAccountPage.defaultContact')}
+
+ {' '}
+ {userEmailOrPhone}
+
this.setState({phoneOrEmail: phoneOrEmail.toLowerCase()})}
- label={this.props.translate('closeAccountPage.typeToConfirm', {emailOrPhone: userEmailOrPhone})}
+ label={this.props.translate('closeAccountPage.enterDefaultContact')}
containerStyles={[styles.mt5]}
/>
diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js
index 8fcd062f7654..72e9df3cebfb 100644
--- a/src/pages/settings/Security/SecuritySettingsPage.js
+++ b/src/pages/settings/Security/SecuritySettingsPage.js
@@ -18,7 +18,7 @@ const SecuritySettingsPage = (props) => {
const menuItems = [
{
translationKey: 'passwordPage.changePassword',
- icon: Expensicons.Lock,
+ icon: Expensicons.Key,
action: () => {
Navigation.navigate(ROUTES.SETTINGS_PASSWORD);
},
diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.js
index e20bf4158020..f9bca16bdc72 100644
--- a/src/pages/signin/SignInPageLayout/index.js
+++ b/src/pages/signin/SignInPageLayout/index.js
@@ -1,11 +1,13 @@
+import _ from 'underscore';
import React from 'react';
-import {View} from 'react-native';
+import {View, Pressable} from 'react-native';
import PropTypes from 'prop-types';
import SignInPageContent from './SignInPageContent';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import SVGImage from '../../../components/SVGImage';
import styles from '../../../styles/styles';
import * as StyleUtils from '../../../styles/StyleUtils';
+import * as Link from '../../../libs/actions/Link';
const propTypes = {
/** The children to show inside the layout */
@@ -37,15 +39,22 @@ const SignInPageLayout = (props) => {
return content;
}
+ const hasRedirect = !_.isEmpty(backgroundStyle.redirectUri);
+
return (
{content}
- {
+ Link.openExternalLink(backgroundStyle.redirectUri);
+ }}
+ disabled={!hasRedirect}
>
{
src={backgroundStyle.backgroundImageUri}
resizeMode={props.isMediumScreenWidth ? 'contain' : 'cover'}
/>
-
+
);
diff --git a/src/pages/wallet/WalletStatementPage.js b/src/pages/wallet/WalletStatementPage.js
new file mode 100644
index 000000000000..5fba642960cc
--- /dev/null
+++ b/src/pages/wallet/WalletStatementPage.js
@@ -0,0 +1,62 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import lodashGet from 'lodash/get';
+import {withOnyx} from 'react-native-onyx';
+import moment from 'moment';
+import Str from 'expensify-common/lib/str';
+import Navigation from '../../libs/Navigation/Navigation';
+import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
+import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
+import ScreenWrapper from '../../components/ScreenWrapper';
+import ONYXKEYS from '../../ONYXKEYS';
+import compose from '../../libs/compose';
+import CONFIG from '../../CONFIG';
+import WalletStatementModal from '../../components/WalletStatementModal';
+
+const propTypes = {
+ /** The route object passed to this page from the navigator */
+ route: PropTypes.shape({
+
+ /** Each parameter passed via the URL */
+ params: PropTypes.shape({
+
+ /** The statement year and month as one string, i.e. 202110 */
+ yearMonth: PropTypes.string.isRequired,
+ }).isRequired,
+ }).isRequired,
+
+ ...withLocalizePropTypes,
+};
+
+const WalletStatementPage = (props) => {
+ moment.locale(lodashGet(props, 'preferredLocale', 'en'));
+ const yearMonth = lodashGet(props.route.params, 'yearMonth', null);
+ const year = yearMonth.substring(0, 4) || moment().year();
+ const month = yearMonth.substring(4) || moment().month();
+ const monthName = moment(month, 'M').format('MMMM');
+ const title = `${monthName} ${year} statement`;
+
+ const url = `${CONFIG.EXPENSIFY.URL_EXPENSIFY_COM}statement.php?period=${yearMonth}`;
+ return (
+
+ Navigation.dismissModal(true)}
+ />
+
+
+ );
+};
+
+WalletStatementPage.propTypes = propTypes;
+WalletStatementPage.displayName = 'WalletStatementPage';
+export default compose(
+ withLocalize,
+ withOnyx({
+ preferredLocale: {
+ key: ONYXKEYS.NVP_PREFERRED_LOCALE,
+ },
+ }),
+)(WalletStatementPage);
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js
index 829af0e6c2fe..57a52a16427a 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.js
+++ b/src/pages/workspace/WorkspaceNewRoomPage.js
@@ -21,12 +21,18 @@ import FixedFooter from '../../components/FixedFooter';
import Permissions from '../../libs/Permissions';
import Log from '../../libs/Log';
import KeyboardAvoidingView from '../../components/KeyboardAvoidingView';
+import * as ValidationUtils from '../../libs/ValidationUtils';
const propTypes = {
/** All reports shared with the user */
reports: PropTypes.shape({
+ /** The report name */
reportName: PropTypes.string,
+
+ /** The report type */
type: PropTypes.string,
+
+ /** ID of the policy */
policyID: PropTypes.string,
}).isRequired,
@@ -49,13 +55,13 @@ class WorkspaceNewRoomPage extends React.Component {
this.state = {
roomName: '',
- error: '',
policyID: '',
visibility: CONST.REPORT.VISIBILITY.RESTRICTED,
+ errors: {},
workspaceOptions: [],
};
- this.onWorkspaceSelect = this.onWorkspaceSelect.bind(this);
- this.onSubmit = this.onSubmit.bind(this);
+
+ this.validateAndCreatePolicyRoom = this.validateAndCreatePolicyRoom.bind(this);
}
componentDidMount() {
@@ -76,19 +82,58 @@ class WorkspaceNewRoomPage extends React.Component {
this.setState({workspaceOptions: _.map(workspaces, policy => ({label: policy.name, key: policy.id, value: policy.id}))});
}
+ validateAndCreatePolicyRoom() {
+ if (!this.validate()) {
+ return;
+ }
+ Report.createPolicyRoom(
+ this.state.policyID,
+ this.state.roomName,
+ this.state.visibility,
+ );
+ }
+
/**
- * Called when a workspace is selected.
- * @param {String} policyID
+ * @returns {Boolean}
*/
- onWorkspaceSelect(policyID) {
- this.setState({policyID});
+ validate() {
+ const errors = {};
+
+ // We error if the user doesn't enter a room name or left blank
+ if (!this.state.roomName || this.state.roomName === CONST.POLICY.ROOM_PREFIX) {
+ errors.roomName = this.props.translate('newRoomPage.pleaseEnterRoomName');
+ }
+
+ // We error if the room name already exists.
+ if (ValidationUtils.isExistingRoomName(this.state.roomName, this.props.reports, this.state.policyID)) {
+ errors.roomName = this.props.translate('newRoomPage.roomAlreadyExistsError');
+ }
+
+ // Certain names are reserved for default rooms and should not be used for policy rooms.
+ if (ValidationUtils.isReservedRoomName(this.state.roomName)) {
+ errors.roomName = this.props.translate('newRoomPage.roomNameReservedError');
+ }
+
+ if (!this.state.policyID) {
+ errors.policyID = this.props.translate('newRoomPage.pleaseSelectWorkspace');
+ }
+
+ this.setState({errors});
+ return _.isEmpty(errors);
}
/**
- * Called when the "Create Room" button is pressed.
+ * @param {String} inputKey
+ * @param {String} value
*/
- onSubmit() {
- Report.createPolicyRoom(this.state.policyID, this.state.roomName, this.state.visibility);
+ clearErrorAndSetValue(inputKey, value) {
+ this.setState(prevState => ({
+ [inputKey]: value,
+ errors: {
+ ...prevState.errors,
+ [inputKey]: '',
+ },
+ }));
}
render() {
@@ -97,7 +142,6 @@ class WorkspaceNewRoomPage extends React.Component {
Navigation.dismissModal();
return null;
}
- const shouldDisableSubmit = Boolean(!this.state.roomName || !this.state.policyID || this.state.error);
const visibilityOptions = _.map(_.values(CONST.REPORT.VISIBILITY), visibilityOption => ({
label: this.props.translate(`newRoomPage.visibilityOptions.${visibilityOption}`),
@@ -115,10 +159,10 @@ class WorkspaceNewRoomPage extends React.Component {
{this.props.translate('newRoomPage.roomName')}
this.setState({roomName})}
- onChangeError={error => this.setState({error})}
initialValue={this.state.roomName}
policyID={this.state.policyID}
+ errorText={this.state.errors.roomName}
+ onChangeText={roomName => this.clearErrorAndSetValue('roomName', roomName)}
/>
@@ -127,7 +171,9 @@ class WorkspaceNewRoomPage extends React.Component {
label={this.props.translate('workspace.common.workspace')}
placeholder={{value: '', label: this.props.translate('newRoomPage.selectAWorkspace')}}
items={this.state.workspaceOptions}
- onChange={this.onWorkspaceSelect}
+ errorText={this.state.errors.policyID}
+ hasError={Boolean(this.state.errors.policyID)}
+ onChange={policyID => this.clearErrorAndSetValue('policyID', policyID)}
/>
diff --git a/src/setup/index.js b/src/setup/index.js
index f2b9ed0e4503..81c49c3adbe1 100644
--- a/src/setup/index.js
+++ b/src/setup/index.js
@@ -1,3 +1,4 @@
+import {I18nManager} from 'react-native';
import Onyx from 'react-native-onyx';
import ONYXKEYS from '../ONYXKEYS';
import CONST from '../CONST';
@@ -35,6 +36,11 @@ export default function () {
},
});
+ // Force app layout to work left to right because our design does not currently support devices using this mode
+ I18nManager.allowRTL(false);
+ I18nManager.forceRTL(false);
+ I18nManager.swapLeftAndRightInRTL(false);
+
// Perform any other platform-specific setup
platformSetup();
}
diff --git a/src/stories/AddressSearch.stories.js b/src/stories/AddressSearch.stories.js
new file mode 100644
index 000000000000..6f03680c47f7
--- /dev/null
+++ b/src/stories/AddressSearch.stories.js
@@ -0,0 +1,42 @@
+import React, {useState} from 'react';
+import AddressSearch from '../components/AddressSearch';
+
+/**
+ * We use the Component Story Format for writing stories. Follow the docs here:
+ *
+ * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
+ */
+export default {
+ title: 'Components/AddressSearch',
+ component: AddressSearch,
+ args: {
+ label: 'Enter street',
+ errorText: '',
+ },
+};
+
+const Template = (args) => {
+ const [value, setValue] = useState('');
+ return (
+ setValue(street)}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...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({});
+
+const ErrorStory = Template.bind({});
+ErrorStory.args = {
+ errorText: 'The street you are looking for does not exist',
+};
+
+export {
+ Default,
+ ErrorStory,
+};
diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.js
index f8b0a9b8cbe3..a9f2ffbd5e56 100644
--- a/src/stories/Form.stories.js
+++ b/src/stories/Form.stories.js
@@ -1,6 +1,7 @@
import React from 'react';
import {View} from 'react-native';
import TextInput from '../components/TextInput';
+import AddressSearch from '../components/AddressSearch';
import Form from '../components/Form';
import * as FormActions from '../libs/actions/FormActions';
import styles from '../styles/styles';
@@ -13,7 +14,7 @@ import styles from '../styles/styles';
const story = {
title: 'Components/Form',
component: Form,
- subcomponents: {TextInput},
+ subcomponents: {TextInput, AddressSearch},
};
const Template = (args) => {
@@ -39,6 +40,12 @@ const Template = (args) => {
containerStyles={[styles.mt4]}
isFormInput
/>
+
);
};
diff --git a/src/styles/StyleUtils.js b/src/styles/StyleUtils.js
index 8a20e5ac3b6b..038ed6b5f5da 100644
--- a/src/styles/StyleUtils.js
+++ b/src/styles/StyleUtils.js
@@ -309,8 +309,14 @@ function getFontFamilyMonospace({fontStyle, fontWeight}) {
* @returns {String}
*/
function getEmojiPickerStyle(isSmallScreenWidth) {
+ if (isSmallScreenWidth) {
+ return {
+ width: '100%',
+ };
+ }
return {
- width: isSmallScreenWidth ? '100%' : CONST.EMOJI_PICKER_SIZE,
+ width: CONST.EMOJI_PICKER_SIZE.WIDTH,
+ height: CONST.EMOJI_PICKER_SIZE.HEIGHT,
};
}
@@ -337,8 +343,13 @@ function getLoginPagePromoStyle() {
backgroundColor: colors.blue,
backgroundImageUri: `${CONST.CLOUDFRONT_URL}/images/homepage/brand-stories/freeplan_blue.svg`,
},
+ {
+ backgroundColor: colors.floralwhite,
+ backgroundImageUri: `${CONST.CLOUDFRONT_URL}/images/homepage/brand-stories/cpa-card.svg`,
+ redirectUri: `${CONST.USE_EXPENSIFY_URL}/accountants`,
+ },
];
- return promos[_.random(0, 3)];
+ return promos[_.random(0, 4)];
}
/**
diff --git a/src/styles/colors.js b/src/styles/colors.js
index 90b6c6cbecd9..f195cd62ebb4 100644
--- a/src/styles/colors.js
+++ b/src/styles/colors.js
@@ -8,6 +8,7 @@ export default {
black: '#000000',
blue: '#0185ff',
blueHover: '#0063bf',
+ floralwhite: '#fffaf0',
green: '#03d47c',
greenHover: '#03c775',
orange: '#ff7101',
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 683af72f3c28..2edbe8d6ea88 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -31,6 +31,7 @@ const picker = {
borderStyle: 'solid',
borderColor: themeColors.border,
borderRadius: variables.componentBorderRadiusNormal,
+ textAlign: 'left',
};
const link = {
@@ -120,6 +121,11 @@ const webViewStyles = {
borderRadius: variables.componentBorderRadiusNormal,
borderWidth: 1,
},
+
+ p: {
+ marginTop: 0,
+ marginBottom: 0,
+ },
},
baseFontStyle: {
@@ -651,6 +657,7 @@ const styles = {
color: themeColors.textSupporting,
fontFamily: fontFamily.GTA,
width: '100%',
+ textAlign: 'left',
},
textInputLabelBackground: {
diff --git a/tests/unit/awaitStagingDeploysTest.js b/tests/unit/awaitStagingDeploysTest.js
index 3ed502ac51e2..8b4536ee7b05 100644
--- a/tests/unit/awaitStagingDeploysTest.js
+++ b/tests/unit/awaitStagingDeploysTest.js
@@ -1,6 +1,7 @@
/**
* @jest-environment node
*/
+const core = require('@actions/core');
const _ = require('underscore');
const run = require('../../.github/actions/awaitStagingDeploys/awaitStagingDeploys');
const GitHubUtils = require('../../.github/libs/GithubUtils');
@@ -10,6 +11,9 @@ const TEST_POLL_RATE = 1;
const COMPLETED_WORKFLOW = {status: 'completed'};
const INCOMPLETE_WORKFLOW = {status: 'in_progress'};
+const consoleSpy = jest.spyOn(console, 'log');
+const mockGetInput = jest.fn();
+const mockListPlatformDeploysForTag = jest.fn();
const mockListPlatformDeploys = jest.fn();
const mockListPreDeploys = jest.fn();
const mockListWorkflowRuns = jest.fn().mockImplementation((args) => {
@@ -19,6 +23,10 @@ const mockListWorkflowRuns = jest.fn().mockImplementation((args) => {
return defaultReturn;
}
+ if (!_.isUndefined(args.branch)) {
+ return mockListPlatformDeploysForTag();
+ }
+
if (args.workflow_id === 'platformDeploy.yml') {
return mockListPlatformDeploys();
}
@@ -31,6 +39,9 @@ const mockListWorkflowRuns = jest.fn().mockImplementation((args) => {
});
beforeAll(() => {
+ // Mock core module
+ core.getInput = mockGetInput;
+
// Mock octokit module
const mocktokit = {
actions: {
@@ -41,8 +52,15 @@ beforeAll(() => {
GitHubUtils.POLL_RATE = TEST_POLL_RATE;
});
+beforeEach(() => {
+ consoleSpy.mockClear();
+});
+
describe('awaitStagingDeploys', () => {
test('Should wait for all running staging deploys to finish', () => {
+ mockGetInput.mockImplementation(() => undefined);
+
+ // First ping
mockListPlatformDeploys.mockResolvedValueOnce({
data: {
workflow_runs: [
@@ -57,6 +75,8 @@ describe('awaitStagingDeploys', () => {
workflow_runs: [],
},
});
+
+ // Second ping
mockListPlatformDeploys.mockResolvedValueOnce({
data: {
workflow_runs: [
@@ -71,6 +91,8 @@ describe('awaitStagingDeploys', () => {
workflow_runs: [],
},
});
+
+ // Third ping
mockListPlatformDeploys.mockResolvedValueOnce({
data: {
workflow_runs: [
@@ -87,6 +109,8 @@ describe('awaitStagingDeploys', () => {
],
},
});
+
+ // Fourth ping
mockListPlatformDeploys.mockResolvedValueOnce({
data: {
workflow_runs: [
@@ -104,7 +128,6 @@ describe('awaitStagingDeploys', () => {
},
});
- const consoleSpy = jest.spyOn(console, 'log');
return run()
.then(() => {
expect(consoleSpy).toHaveBeenCalledTimes(4);
@@ -114,4 +137,93 @@ describe('awaitStagingDeploys', () => {
expect(consoleSpy).toHaveBeenLastCalledWith('No current staging deploys found');
});
});
+
+ test('Should only wait for a specific staging deploy to finish', () => {
+ mockGetInput.mockImplementation(() => 'my-tag');
+
+ // First ping
+ mockListPlatformDeploysForTag.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+ mockListPlatformDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+ mockListPreDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+
+ // Second ping
+ mockListPlatformDeploysForTag.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+ mockListPlatformDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ COMPLETED_WORKFLOW,
+ ],
+ },
+ });
+ mockListPreDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ COMPLETED_WORKFLOW,
+ COMPLETED_WORKFLOW,
+ ],
+ },
+ });
+
+ // Third ping
+ mockListPlatformDeploysForTag.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ COMPLETED_WORKFLOW,
+ ],
+ },
+ });
+ mockListPlatformDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ INCOMPLETE_WORKFLOW,
+ COMPLETED_WORKFLOW,
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+ mockListPreDeploys.mockResolvedValueOnce({
+ data: {
+ workflow_runs: [
+ COMPLETED_WORKFLOW,
+ COMPLETED_WORKFLOW,
+ INCOMPLETE_WORKFLOW,
+ ],
+ },
+ });
+
+ return run()
+ .then(() => {
+ expect(consoleSpy).toHaveBeenCalledTimes(3);
+ expect(consoleSpy).toHaveBeenNthCalledWith(1, 'Found 1 staging deploy still running...');
+ expect(consoleSpy).toHaveBeenNthCalledWith(2, 'Found 1 staging deploy still running...');
+ expect(consoleSpy).toHaveBeenLastCalledWith('No current staging deploys found');
+ });
+ });
});
diff --git a/tests/unit/getPullRequestsMergedBetweenTest.sh b/tests/unit/getPullRequestsMergedBetweenTest.sh
index 6a30429c58a0..47e0e9d9c351 100755
--- a/tests/unit/getPullRequestsMergedBetweenTest.sh
+++ b/tests/unit/getPullRequestsMergedBetweenTest.sh
@@ -4,10 +4,11 @@
set -e
TEST_DIR=$(dirname "$(dirname "$(cd "$(dirname "$0")" || exit 1;pwd)/$(basename "$0")")")
+SCRIPTS_DIR="$TEST_DIR/../scripts"
DUMMY_DIR="$HOME/DumDumRepo"
getPullRequestsMergedBetween="$TEST_DIR/utils/getPullRequestsMergedBetween.js"
-source "$TEST_DIR/utils/shellUtils.sh"
+source "$SCRIPTS_DIR/shellUtils.sh"
function print_version {
< package.json jq -r .version