diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 029d32f8589c..e0fd082f0b8b 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -84,7 +84,7 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c
- [ ] I verified any copy / text shown in the product is localized by adding it to `src/languages/*` files and using the [translation method](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60)
- [ ] If any non-english text was added/modified, I verified the translation was requested/reviewed in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
- [ ] I verified all numbers, amounts, dates and phone numbers shown in the product are using the [localization methods](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60-L68)
- - [ ] I verified any copy / text that was added to the app is correct English and approved by marketing by adding the `Waiting for Copy` label for a copy review on the original GH to get the correct copy.
+ - [ ] I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is approved by marketing by adding the `Waiting for Copy` label for a copy review on the original GH to get the correct copy.
- [ ] I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
- [ ] I verified the JSDocs style guidelines (in [`STYLE.md`](https://github.com/Expensify/App/blob/main/contributingGuides/STYLE.md#jsdocs)) were followed
- [ ] If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
diff --git a/__mocks__/react-native-key-command.js b/__mocks__/react-native-key-command.js
new file mode 100644
index 000000000000..092ab120a142
--- /dev/null
+++ b/__mocks__/react-native-key-command.js
@@ -0,0 +1,13 @@
+const registerKeyCommands = () => {};
+const unregisterKeyCommands = () => {};
+const constants = {};
+const eventEmitter = () => {};
+const addListener = () => {};
+
+export {
+ registerKeyCommands,
+ unregisterKeyCommands,
+ constants,
+ eventEmitter,
+ addListener,
+};
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 1f6cef81a2e7..5bd5feb9db6a 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -106,8 +106,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001030000
- versionName "1.3.0-0"
+ versionCode 1001030001
+ versionName "1.3.0-1"
}
splits {
diff --git a/android/app/src/main/java/com/expensify/chat/MainActivity.java b/android/app/src/main/java/com/expensify/chat/MainActivity.java
index bd90ee9abd02..3e6381000409 100644
--- a/android/app/src/main/java/com/expensify/chat/MainActivity.java
+++ b/android/app/src/main/java/com/expensify/chat/MainActivity.java
@@ -2,7 +2,9 @@
import android.os.Bundle;
import android.content.pm.ActivityInfo;
+import android.view.KeyEvent;
import com.expensify.chat.bootsplash.BootSplash;
+import com.expensify.reactnativekeycommand.KeyCommandModule;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
@@ -44,4 +46,34 @@ protected void onCreate(Bundle savedInstanceState) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
-}
\ No newline at end of file
+
+ /**
+ * This method is called when a key down event has occurred.
+ * Forwards the event to the KeyCommandModule
+ */
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // Disabling hardware ESCAPE support which is handled by Android
+ if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) {
+ return false;
+ }
+ KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event);
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+ // Disabling hardware ESCAPE support which is handled by Android
+ if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) { return false; }
+ KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event);
+ return super.onKeyLongPress(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ // Disabling hardware ESCAPE support which is handled by Android
+ if (event.getKeyCode() == KeyEvent.KEYCODE_ESCAPE) { return false; }
+ KeyCommandModule.getInstance().onKeyDownEvent(keyCode, event);
+ return super.onKeyUp(keyCode, event);
+ }
+}
diff --git a/assets/images/expensify-logo--adhoc.svg b/assets/images/expensify-logo--adhoc.svg
new file mode 100644
index 000000000000..63b6f896e3a5
--- /dev/null
+++ b/assets/images/expensify-logo--adhoc.svg
@@ -0,0 +1,47 @@
+
+
+
diff --git a/assets/images/expensify-logo--dev.svg b/assets/images/expensify-logo--dev.svg
new file mode 100644
index 000000000000..42a0f1d8e952
--- /dev/null
+++ b/assets/images/expensify-logo--dev.svg
@@ -0,0 +1,43 @@
+
+
+
diff --git a/assets/images/expensify-logo--staging.svg b/assets/images/expensify-logo--staging.svg
new file mode 100644
index 000000000000..335c41a294e3
--- /dev/null
+++ b/assets/images/expensify-logo--staging.svg
@@ -0,0 +1,46 @@
+
+
+
diff --git a/assets/images/home-background--desktop.svg b/assets/images/home-background--desktop.svg
new file mode 100644
index 000000000000..7241c46036c0
--- /dev/null
+++ b/assets/images/home-background--desktop.svg
@@ -0,0 +1,8839 @@
+
+
+
diff --git a/assets/images/home-background--mobile.svg b/assets/images/home-background--mobile.svg
new file mode 100644
index 000000000000..0af3aac59146
--- /dev/null
+++ b/assets/images/home-background--mobile.svg
@@ -0,0 +1,6556 @@
+
+
+
diff --git a/assets/images/home-fade-gradient--mobile.svg b/assets/images/home-fade-gradient--mobile.svg
new file mode 100644
index 000000000000..ca03eb3323af
--- /dev/null
+++ b/assets/images/home-fade-gradient--mobile.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/assets/images/home-fade-gradient.svg b/assets/images/home-fade-gradient.svg
new file mode 100644
index 000000000000..6aada6633a8b
--- /dev/null
+++ b/assets/images/home-fade-gradient.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/assets/images/product-illustrations/home-illustration-hands.svg b/assets/images/product-illustrations/home-illustration-hands.svg
new file mode 100644
index 000000000000..9a70d8cc6363
--- /dev/null
+++ b/assets/images/product-illustrations/home-illustration-hands.svg
@@ -0,0 +1,545 @@
+
+
+
diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md
index 5beafc1dd542..135be96f9d79 100644
--- a/contributingGuides/REVIEWER_CHECKLIST.md
+++ b/contributingGuides/REVIEWER_CHECKLIST.md
@@ -23,7 +23,7 @@
- [ ] I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
- [ ] I verified any copy / text shown in the product is localized by adding it to `src/languages/*` files and using the [translation method](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60)
- [ ] I verified all numbers, amounts, dates and phone numbers shown in the product are using the [localization methods](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60-L68)
- - [ ] I verified any copy / text that was added to the app is correct English and approved by marketing by adding the `Waiting for Copy` label for a copy review on the original GH to get the correct copy.
+ - [ ] I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is approved by marketing by adding the `Waiting for Copy` label for a copy review on the original GH to get the correct copy.
- [ ] I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
- [ ] I verified the JSDocs style guidelines (in [`STYLE.md`](https://github.com/Expensify/App/blob/main/contributingGuides/STYLE.md#jsdocs)) were followed
- [ ] If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
diff --git a/docs/_data/routes.yml b/docs/_data/routes.yml
index 1393c94c51a8..7f10a60381a9 100644
--- a/docs/_data/routes.yml
+++ b/docs/_data/routes.yml
@@ -38,8 +38,8 @@ hubs:
title: Expensify Playbook for US-Based VC-Backed Startups
- href: Expensify-Playbook-for-US-Based-Bootstrapped-Startups
title: Expensify Playbook for US-Based Bootstrapped Startups
- - href: Expensify-Playbook-for-US-Based-Small-Businesses
- title: Expensify Playbook for US-Based Small Businesses
+ - href: Expensify-Playbook-for-Small-to-Medium-Sized-Businesses
+ title: Expensify Playbook for Small to Medium-Sized Businesses
- href: other
title: Other
diff --git a/docs/articles/playbooks/Expensify-Playbook-for-US-Based-Small-Businesses.md b/docs/articles/playbooks/Expensify-Playbook-for-Small-to-Medium-Sized-Businesses.md
similarity index 99%
rename from docs/articles/playbooks/Expensify-Playbook-for-US-Based-Small-Businesses.md
rename to docs/articles/playbooks/Expensify-Playbook-for-Small-to-Medium-Sized-Businesses.md
index cb656d7a5d08..c07a987e8bf7 100644
--- a/docs/articles/playbooks/Expensify-Playbook-for-US-Based-Small-Businesses.md
+++ b/docs/articles/playbooks/Expensify-Playbook-for-Small-to-Medium-Sized-Businesses.md
@@ -1,5 +1,5 @@
---
-title: Expensify Playbook for US Based Small to Medium-Sized Businesses
+title: Expensify Playbook for Small to Medium-Sized Businesses
description: Best practices for how to deploy Expensify for your business
---
This guide provides practical tips and recommendations for small businesses with 100 to 250 employees to effectively use Expensify to improve spend visibility, facilitate employee reimbursements, and reduce the risk of fraudulent expenses.
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 8d218b39f973..84aa8cd47d56 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -30,7 +30,7 @@
CFBundleVersion
- 1.3.0.0
+ 1.3.0.1
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index c7d7076eb1b3..3e41767a8d70 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.3.0.0
+ 1.3.0.1
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 28de3a7fa527..86707162d74c 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -514,6 +514,8 @@ PODS:
- React
- react-native-image-picker (5.1.0):
- React-Core
+ - react-native-key-command (0.9.1):
+ - React-Core
- react-native-netinfo (8.3.1):
- React-Core
- react-native-pdf (6.6.2):
@@ -765,6 +767,7 @@ DEPENDENCIES:
- react-native-flipper (from `../node_modules/react-native-flipper`)
- "react-native-image-manipulator (from `../node_modules/@oguzhnatly/react-native-image-manipulator`)"
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
+ - react-native-key-command (from `../node_modules/react-native-key-command`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-pdf (from `../node_modules/react-native-pdf`)
- react-native-performance (from `../node_modules/react-native-performance`)
@@ -916,6 +919,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@oguzhnatly/react-native-image-manipulator"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
+ react-native-key-command:
+ :path: "../node_modules/react-native-key-command"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-pdf:
@@ -1002,7 +1007,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Airship: 19ead2c0bdc791c1b9d6ebb7940aaac99614414e
AirshipFrameworkProxy: 037a0ad6491757c45de2c70a6cc47bae5fcfa32b
- boost: 57d2868c099736d80fcd648bf211b4431e51a558
+ boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: ff54429f0110d3c722630a98096ba689c39f6d5f
@@ -1045,7 +1050,7 @@ SPEC CHECKSUMS:
Permission-LocationWhenInUse: 3ba99e45c852763f730eabecec2870c2382b7bd4
Plaid: 7d340abeadb46c7aa1a91f896c5b22395a31fcf2
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
- RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
+ RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: e9e7b8b45aa9bedb2fdad71740adf07a7265b9be
RCTTypeSafety: 9ae0e9206625e995f0df4d5b9ddc94411929fb30
React: a71c8e1380f07e01de721ccd52bcf9c03e81867d
@@ -1067,6 +1072,7 @@ SPEC CHECKSUMS:
react-native-flipper: dc5290261fbeeb2faec1bdc57ae6dd8d562e1de4
react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56
react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b
+ react-native-key-command: e49d6e44d44705779696d8d3a5ac6b9e3a198941
react-native-netinfo: 1a6035d3b9780221d407c277ebfb5722ace00658
react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
react-native-performance: 224bd53e6a835fda4353302cf891d088a0af7406
diff --git a/package-lock.json b/package-lock.json
index 1bd773abf334..d1c30ca218ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.0-0",
+ "version": "1.3.0-1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.0-0",
+ "version": "1.3.0-1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -70,13 +70,14 @@
"react-native-image-pan-zoom": "^2.1.12",
"react-native-image-picker": "^5.1.0",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972",
+ "react-native-key-command": "^0.9.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
"react-native-onyx": "1.0.38",
"react-native-pdf": "^6.6.2",
"react-native-performance": "^4.0.0",
"react-native-permissions": "^3.0.1",
- "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#84ee97dec11c2e65609511eb5a757d61bbeeab79",
+ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#c92aab10ffe24acde04f1c205e19decb9fa4e188",
"react-native-plaid-link-sdk": "^10.0.0",
"react-native-quick-sqlite": "^8.0.0-beta.2",
"react-native-reanimated": "3.0.0-rc.10",
@@ -34670,6 +34671,21 @@
"integrity": "sha512-jNNpW5byieb7pb/l0HRvmCav4BtzpTzgC+ybT+Wbi2yyroOukveVvnjwWnmoOeuGynsYB4Yt5eGrWZnPnJSwqQ==",
"license": "MIT"
},
+ "node_modules/react-native-key-command": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/react-native-key-command/-/react-native-key-command-0.9.1.tgz",
+ "integrity": "sha512-di7G5q66eI0xL14B4kcVfm7azGET07henwu21N8hb71sZpDZGsAJ1WFuR32SwbnkLVNhEk7FJAIH/5Sh+dDQoA==",
+ "dependencies": {
+ "events": "^3.3.0",
+ "underscore": "^1.13.4"
+ },
+ "peerDependencies": {
+ "react": "^18.1.0",
+ "react-dom": "18.1.0",
+ "react-native": "^0.70.4",
+ "react-native-web": "^0.18.1"
+ }
+ },
"node_modules/react-native-localize": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-2.2.6.tgz",
@@ -34778,8 +34794,8 @@
},
"node_modules/react-native-picker-select": {
"version": "8.0.4",
- "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#84ee97dec11c2e65609511eb5a757d61bbeeab79",
- "integrity": "sha512-AvrO+pSH5mjJCosqPuFKB9kBmyTFXS+OWXEYBXVFBDzAYpq18U2DDYlpZoA54xhPbamDCz3xT3UDx0iFoG2GTg==",
+ "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#c92aab10ffe24acde04f1c205e19decb9fa4e188",
+ "integrity": "sha512-AEJ8URKcSR3UrJWln8ISgAuluBYYJgQY/dTOVVMWC62Mhf9w/vVA8TU9YKDHaJKspKUizY0b1HrplggmoZxPuQ==",
"license": "MIT",
"dependencies": {
"lodash.isequal": "^4.5.0"
@@ -34912,22 +34928,22 @@
}
},
"node_modules/react-native-web": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.12.tgz",
- "integrity": "sha512-fboP7yqobJ8InSr4fP+bQ3scOtSQtUoPcR+HWasH8b/fk/RO+mWcJs/8n+lewy9WTZc2D68ha7VwRDviUshEWA==",
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.1.tgz",
+ "integrity": "sha512-kWZKJU/OIvVriC7R7WN2Wpai6QllD9f7RDUuaBj2G9FdXaybSQFgmuhey4n6naqbLnj720im2PtcnH54Cn/UXw==",
"peer": true,
"dependencies": {
- "@babel/runtime": "^7.18.6",
"create-react-class": "^15.7.0",
- "fbjs": "^3.0.4",
- "inline-style-prefixer": "^6.0.1",
+ "fbjs": "^3.0.0",
+ "inline-style-prefixer": "^6.0.0",
"normalize-css-color": "^1.0.2",
"postcss-value-parser": "^4.2.0",
+ "prop-types": "^15.6.0",
"styleq": "^0.1.2"
},
"peerDependencies": {
- "react": "^17.0.2 || ^18.0.0",
- "react-dom": "^17.0.2 || ^18.0.0"
+ "react": ">=17.0.2",
+ "react-dom": ">=17.0.2"
}
},
"node_modules/react-native-web-lottie": {
@@ -64334,6 +64350,15 @@
"integrity": "sha512-jNNpW5byieb7pb/l0HRvmCav4BtzpTzgC+ybT+Wbi2yyroOukveVvnjwWnmoOeuGynsYB4Yt5eGrWZnPnJSwqQ==",
"from": "react-native-image-size@git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972"
},
+ "react-native-key-command": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/react-native-key-command/-/react-native-key-command-0.9.1.tgz",
+ "integrity": "sha512-di7G5q66eI0xL14B4kcVfm7azGET07henwu21N8hb71sZpDZGsAJ1WFuR32SwbnkLVNhEk7FJAIH/5Sh+dDQoA==",
+ "requires": {
+ "events": "^3.3.0",
+ "underscore": "^1.13.4"
+ }
+ },
"react-native-localize": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/react-native-localize/-/react-native-localize-2.2.6.tgz",
@@ -64379,9 +64404,9 @@
"requires": {}
},
"react-native-picker-select": {
- "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#84ee97dec11c2e65609511eb5a757d61bbeeab79",
- "integrity": "sha512-AvrO+pSH5mjJCosqPuFKB9kBmyTFXS+OWXEYBXVFBDzAYpq18U2DDYlpZoA54xhPbamDCz3xT3UDx0iFoG2GTg==",
- "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#84ee97dec11c2e65609511eb5a757d61bbeeab79",
+ "version": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#c92aab10ffe24acde04f1c205e19decb9fa4e188",
+ "integrity": "sha512-AEJ8URKcSR3UrJWln8ISgAuluBYYJgQY/dTOVVMWC62Mhf9w/vVA8TU9YKDHaJKspKUizY0b1HrplggmoZxPuQ==",
+ "from": "react-native-picker-select@git+https://github.com/Expensify/react-native-picker-select.git#c92aab10ffe24acde04f1c205e19decb9fa4e188",
"requires": {
"lodash.isequal": "^4.5.0"
}
@@ -64470,17 +64495,17 @@
}
},
"react-native-web": {
- "version": "0.18.12",
- "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.12.tgz",
- "integrity": "sha512-fboP7yqobJ8InSr4fP+bQ3scOtSQtUoPcR+HWasH8b/fk/RO+mWcJs/8n+lewy9WTZc2D68ha7VwRDviUshEWA==",
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.18.1.tgz",
+ "integrity": "sha512-kWZKJU/OIvVriC7R7WN2Wpai6QllD9f7RDUuaBj2G9FdXaybSQFgmuhey4n6naqbLnj720im2PtcnH54Cn/UXw==",
"peer": true,
"requires": {
- "@babel/runtime": "^7.18.6",
"create-react-class": "^15.7.0",
- "fbjs": "^3.0.4",
- "inline-style-prefixer": "^6.0.1",
+ "fbjs": "^3.0.0",
+ "inline-style-prefixer": "^6.0.0",
"normalize-css-color": "^1.0.2",
"postcss-value-parser": "^4.2.0",
+ "prop-types": "^15.6.0",
"styleq": "^0.1.2"
}
},
diff --git a/package.json b/package.json
index 435e5e657206..45dcf37f3ab8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.0-0",
+ "version": "1.3.0-1",
"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.",
@@ -101,13 +101,14 @@
"react-native-image-pan-zoom": "^2.1.12",
"react-native-image-picker": "^5.1.0",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972",
+ "react-native-key-command": "^0.9.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
"react-native-onyx": "1.0.38",
"react-native-pdf": "^6.6.2",
"react-native-performance": "^4.0.0",
"react-native-permissions": "^3.0.1",
- "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#84ee97dec11c2e65609511eb5a757d61bbeeab79",
+ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#c92aab10ffe24acde04f1c205e19decb9fa4e188",
"react-native-plaid-link-sdk": "^10.0.0",
"react-native-quick-sqlite": "^8.0.0-beta.2",
"react-native-reanimated": "3.0.0-rc.10",
diff --git a/src/CONFIG.js b/src/CONFIG.js
index d430f9d9883a..3ba8e267d503 100644
--- a/src/CONFIG.js
+++ b/src/CONFIG.js
@@ -64,6 +64,7 @@ export default {
PARTNER_PASSWORD: lodashGet(Config, 'EXPENSIFY_PARTNER_PASSWORD', 'e21965746fd75f82bb66'),
EXPENSIFY_CASH_REFERER: 'ecash',
CONCIERGE_URL_PATHNAME: 'concierge/',
+ DEVPORTAL_URL_PATHNAME: '_devportal/',
CONCIERGE_URL: `${expensifyURL}concierge/`,
},
IS_IN_PRODUCTION: Platform.OS === 'web' ? process.env.NODE_ENV === 'production' : !__DEV__,
diff --git a/src/CONST.js b/src/CONST.js
index 56adbbe7fe32..20e28c18e928 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -1,5 +1,6 @@
import lodashGet from 'lodash/get';
import Config from 'react-native-config';
+import * as KeyCommand from 'react-native-key-command';
import * as Url from './libs/Url';
const CLOUDFRONT_DOMAIN = 'cloudfront.net';
@@ -7,10 +8,20 @@ const CLOUDFRONT_URL = `https://d2k5nsl2zxldvw.${CLOUDFRONT_DOMAIN}`;
const ACTIVE_EXPENSIFY_URL = Url.addTrailingForwardSlash(lodashGet(Config, 'NEW_EXPENSIFY_URL', 'https://new.expensify.com'));
const USE_EXPENSIFY_URL = 'https://use.expensify.com';
const PLATFORM_OS_MACOS = 'Mac OS';
+const PLATFORM_IOS = 'iOS';
const ANDROID_PACKAGE_NAME = 'com.expensify.chat';
const USA_COUNTRY_NAME = 'United States';
const CURRENT_YEAR = new Date().getFullYear();
+const keyModifierControl = lodashGet(KeyCommand, 'constants.keyModifierControl', 'keyModifierControl');
+const keyModifierCommand = lodashGet(KeyCommand, 'constants.keyModifierCommand', 'keyModifierCommand');
+const keyModifierShiftControl = lodashGet(KeyCommand, 'constants.keyModifierShiftControl', 'keyModifierShiftControl');
+const keyModifierShiftCommand = lodashGet(KeyCommand, 'constants.keyModifierShiftCommand', 'keyModifierShiftCommand');
+const keyInputEscape = lodashGet(KeyCommand, 'constants.keyInputEscape', 'keyInputEscape');
+const keyInputEnter = lodashGet(KeyCommand, 'constants.keyInputEnter', 'keyInputEnter');
+const keyInputUpArrow = lodashGet(KeyCommand, 'constants.keyInputUpArrow', 'keyInputUpArrow');
+const keyInputDownArrow = lodashGet(KeyCommand, 'constants.keyInputDownArrow', 'keyInputDownArrow');
+
const CONST = {
ANDROID_PACKAGE_NAME,
ANIMATED_TRANSITION: 300,
@@ -223,6 +234,7 @@ const CONST = {
CTRL: {
DEFAULT: 'control',
[PLATFORM_OS_MACOS]: 'meta',
+ [PLATFORM_IOS]: 'meta',
},
SHIFT: {
DEFAULT: 'shift',
@@ -233,46 +245,91 @@ const CONST = {
descriptionKey: 'search',
shortcutKey: 'K',
modifiers: ['CTRL'],
+ trigger: {
+ DEFAULT: {input: 'k', modifierFlags: keyModifierControl},
+ [PLATFORM_OS_MACOS]: {input: 'k', modifierFlags: keyModifierCommand},
+ [PLATFORM_IOS]: {input: 'k', modifierFlags: keyModifierCommand},
+ },
},
NEW_GROUP: {
descriptionKey: 'newGroup',
shortcutKey: 'K',
modifiers: ['CTRL', 'SHIFT'],
+ trigger: {
+ DEFAULT: {input: 'k', modifierFlags: keyModifierShiftControl},
+ [PLATFORM_OS_MACOS]: {input: 'k', modifierFlags: keyModifierShiftCommand},
+ [PLATFORM_IOS]: {input: 'k', modifierFlags: keyModifierShiftCommand},
+ },
},
SHORTCUT_MODAL: {
descriptionKey: 'openShortcutDialog',
shortcutKey: 'I',
modifiers: ['CTRL'],
+ trigger: {
+ DEFAULT: {input: 'i', modifierFlags: keyModifierControl},
+ [PLATFORM_OS_MACOS]: {input: 'i', modifierFlags: keyModifierCommand},
+ [PLATFORM_IOS]: {input: 'i', modifierFlags: keyModifierCommand},
+ },
},
ESCAPE: {
descriptionKey: 'escape',
shortcutKey: 'Escape',
modifiers: [],
+ trigger: {
+ DEFAULT: {input: keyInputEscape},
+ [PLATFORM_OS_MACOS]: {input: keyInputEscape},
+ [PLATFORM_IOS]: {input: keyInputEscape},
+ },
},
ENTER: {
descriptionKey: null,
shortcutKey: 'Enter',
modifiers: [],
+ trigger: {
+ DEFAULT: {input: keyInputEnter},
+ [PLATFORM_OS_MACOS]: {input: keyInputEnter},
+ [PLATFORM_IOS]: {input: keyInputEnter},
+ },
},
CTRL_ENTER: {
descriptionKey: null,
shortcutKey: 'Enter',
modifiers: ['CTRL'],
+ trigger: {
+ DEFAULT: {input: keyInputEnter, modifierFlags: keyModifierControl},
+ [PLATFORM_OS_MACOS]: {input: keyInputEnter, modifierFlags: keyModifierCommand},
+ [PLATFORM_IOS]: {input: keyInputEnter, modifierFlags: keyModifierCommand},
+ },
},
COPY: {
descriptionKey: 'copy',
shortcutKey: 'C',
modifiers: ['CTRL'],
+ trigger: {
+ DEFAULT: {input: 'c', modifierFlags: keyModifierControl},
+ [PLATFORM_OS_MACOS]: {input: 'c', modifierFlags: keyModifierCommand},
+ [PLATFORM_IOS]: {input: 'c', modifierFlags: keyModifierCommand},
+ },
},
ARROW_UP: {
descriptionKey: null,
shortcutKey: 'ArrowUp',
modifiers: [],
+ trigger: {
+ DEFAULT: {input: keyInputUpArrow},
+ [PLATFORM_OS_MACOS]: {input: keyInputUpArrow},
+ [PLATFORM_IOS]: {input: keyInputUpArrow},
+ },
},
ARROW_DOWN: {
descriptionKey: null,
shortcutKey: 'ArrowDown',
modifiers: [],
+ trigger: {
+ DEFAULT: {input: keyInputDownArrow},
+ [PLATFORM_OS_MACOS]: {input: keyInputDownArrow},
+ [PLATFORM_IOS]: {input: keyInputDownArrow},
+ },
},
TAB: {
descriptionKey: null,
@@ -504,6 +561,7 @@ const CONST = {
SUCCESS: 200,
NOT_AUTHENTICATED: 407,
EXP_ERROR: 666,
+ MANY_WRITES_ERROR: 665,
UNABLE_TO_RETRY: 'unableToRetry',
},
HTTP_STATUS: {
@@ -780,7 +838,7 @@ const CONST = {
WINDOWS: 'Windows',
MAC_OS: PLATFORM_OS_MACOS,
ANDROID: 'Android',
- IOS: 'iOS',
+ IOS: PLATFORM_IOS,
LINUX: 'Linux',
NATIVE: 'Native',
},
@@ -2201,6 +2259,12 @@ const CONST = {
PATHS_TO_TREAT_AS_EXTERNAL: [
'NewExpensify.dmg',
],
+ PAYPAL_SUPPORTED_CURRENCIES: [
+ 'AUD', 'BRL', 'CAD', 'CZK', 'DKK', 'EUR', 'HKD', 'HUF',
+ 'ILS', 'JPY', 'MYR', 'MXN', 'TWD', 'NZD', 'NOK', 'PHP',
+ 'PLN', 'GBP', 'RUB', 'SGD', 'SEK', 'CHF', 'THB', 'USD',
+ ],
+ CONCIERGE_TRAVEL_URL: 'https://community.expensify.com/discussion/7066/introducing-concierge-travel',
};
export default CONST;
diff --git a/src/Expensify.js b/src/Expensify.js
index b73954be6c77..5702bf0cf05d 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -1,7 +1,9 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {PureComponent} from 'react';
+import React, {
+ useCallback, useState, useEffect, useRef, useLayoutEffect, useMemo,
+} from 'react';
import {AppState, Linking} from 'react-native';
import Onyx, {withOnyx} from 'react-native-onyx';
@@ -84,27 +86,52 @@ const defaultProps = {
screenShareRequest: null,
};
-class Expensify extends PureComponent {
- constructor(props) {
- super(props);
+function Expensify(props) {
+ const appStateChangeListener = useRef(null);
+ const [isNavigationReady, setIsNavigationReady] = useState(false);
+ const [isOnyxMigrated, setIsOnyxMigrated] = useState(false);
+ const [isSplashShown, setIsSplashShown] = useState(true);
+ const isAuthenticated = useMemo(() => Boolean(lodashGet(props.session, 'authToken', null)), [props.session]);
+
+ const initializeClient = () => {
+ if (!Visibility.isVisible()) {
+ return;
+ }
+
+ ActiveClientManager.init();
+ };
+
+ const setNavigationReady = useCallback(() => {
+ setIsNavigationReady(true);
+
+ // Navigate to any pending routes now that the NavigationContainer is ready
+ Navigation.setIsNavigationReady();
+ }, []);
+
+ useLayoutEffect(() => {
// Initialize this client as being an active client
ActiveClientManager.init();
- this.setNavigationReady = this.setNavigationReady.bind(this);
- this.initializeClient = this.initializeClient.bind(true);
- this.appStateChangeListener = null;
- this.state = {
- isNavigationReady: false,
- isOnyxMigrated: false,
- isSplashShown: true,
- };
// Used for the offline indicator appearing when someone is offline
NetworkConnection.subscribeToNetInfo();
- }
-
- componentDidMount() {
- setTimeout(() => this.reportBootSplashStatus(), 30 * 1000);
+ }, []);
+
+ useEffect(() => {
+ setTimeout(() => {
+ BootSplash
+ .getVisibilityStatus()
+ .then((status) => {
+ const appState = AppState.currentState;
+ Log.info('[BootSplash] splash screen status', false, {appState, status});
+
+ if (status === 'visible') {
+ const propsToLog = _.omit(props, ['children', 'session']);
+ propsToLog.isAuthenticated = isAuthenticated;
+ Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false);
+ }
+ });
+ }, 30 * 1000);
// This timer is set in the native layer when launching the app and we stop it here so we can measure how long
// it took for the main app itself to load.
@@ -114,118 +141,78 @@ class Expensify extends PureComponent {
migrateOnyx()
.then(() => {
// In case of a crash that led to disconnection, we want to remove all the push notifications.
- if (!this.isAuthenticated()) {
+ if (!isAuthenticated) {
PushNotification.clearNotifications();
}
- this.setState({isOnyxMigrated: true});
+ setIsOnyxMigrated(true);
});
- this.appStateChangeListener = AppState.addEventListener('change', this.initializeClient);
+ appStateChangeListener.current = AppState.addEventListener('change', initializeClient);
// Open chat report from a deep link (only mobile native)
Linking.addEventListener('url', state => Report.openReportFromDeepLink(state.url));
- }
- componentDidUpdate() {
- if (!this.state.isNavigationReady || !this.state.isSplashShown) {
+ return () => {
+ if (!appStateChangeListener.current) { return; }
+ appStateChangeListener.current.remove();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run again
+ }, []);
+
+ useEffect(() => {
+ if (!isNavigationReady || !isSplashShown) {
return;
}
- const shouldHideSplash = !this.isAuthenticated() || this.props.isSidebarLoaded;
+ const shouldHideSplash = !isAuthenticated || props.isSidebarLoaded;
if (shouldHideSplash) {
BootSplash.hide();
- // eslint-disable-next-line react/no-did-update-set-state
- this.setState({isSplashShown: false});
+ setIsSplashShown(false);
// If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report
Linking.getInitialURL().then(url => Report.openReportFromDeepLink(url));
}
- }
+ }, [props.isSidebarLoaded, isNavigationReady, isSplashShown, isAuthenticated]);
- componentWillUnmount() {
- if (!this.appStateChangeListener) { return; }
- this.appStateChangeListener.remove();
+ // Display a blank page until the onyx migration completes
+ if (!isOnyxMigrated) {
+ return null;
}
- setNavigationReady() {
- this.setState({isNavigationReady: true});
-
- // Navigate to any pending routes now that the NavigationContainer is ready
- Navigation.setIsNavigationReady();
- }
-
- /**
- * @returns {boolean}
- */
- isAuthenticated() {
- const authToken = lodashGet(this.props, 'session.authToken', null);
- return Boolean(authToken);
- }
-
- initializeClient() {
- if (!Visibility.isVisible()) {
- return;
- }
-
- ActiveClientManager.init();
- }
-
- reportBootSplashStatus() {
- BootSplash
- .getVisibilityStatus()
- .then((status) => {
- const appState = AppState.currentState;
- Log.info('[BootSplash] splash screen status', false, {appState, status});
-
- if (status === 'visible') {
- const props = _.omit(this.props, ['children', 'session']);
- props.isAuthenticated = this.isAuthenticated();
- Log.alert('[BootSplash] splash screen is still visible', {props}, false);
- }
- });
- }
-
- render() {
- // Display a blank page until the onyx migration completes
- if (!this.state.isOnyxMigrated) {
- return null;
- }
-
- return (
-
- {!this.state.isSplashShown && (
- <>
-
-
-
+ {!isSplashShown && (
+ <>
+
+
+
+ {/* We include the modal for showing a new update at the top level so the option is always present. */}
+ {props.updateAvailable ? : null}
+ {props.screenShareRequest ? (
+ User.joinScreenShare(props.screenShareRequest.accessToken, props.screenShareRequest.roomName)}
+ onCancel={User.clearScreenShareRequest}
+ prompt={props.translate('guides.screenShareRequest')}
+ confirmText={props.translate('common.join')}
+ cancelText={props.translate('common.decline')}
+ isVisible
/>
- {/* We include the modal for showing a new update at the top level so the option is always present. */}
- {this.props.updateAvailable ? : null}
- {this.props.screenShareRequest ? (
- User.joinScreenShare(this.props.screenShareRequest.accessToken, this.props.screenShareRequest.roomName)}
- onCancel={User.clearScreenShareRequest}
- prompt={this.props.translate('guides.screenShareRequest')}
- confirmText={this.props.translate('common.join')}
- cancelText={this.props.translate('common.decline')}
- isVisible
- />
- ) : null}
- >
- )}
-
-
-
- );
- }
+ ) : null}
+ >
+ )}
+
+
+
+ );
}
Expensify.propTypes = propTypes;
diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index 6fe8a0625cd8..49d5435c8ebd 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -102,16 +102,17 @@ export default {
// Collection Keys
COLLECTION: {
+ DOWNLOAD: 'download_',
+ POLICY: 'policy_',
+ POLICY_MEMBER_LIST: 'policyMemberList_',
REPORT: 'report_',
REPORT_ACTIONS: 'reportActions_',
+ REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_',
REPORT_DRAFT_COMMENT: 'reportDraftComment_',
REPORT_DRAFT_COMMENT_NUMBER_OF_LINES: 'reportDraftCommentNumberOfLines_',
- REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_',
- REPORT_USER_IS_TYPING: 'reportUserIsTyping_',
- POLICY: 'policy_',
REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_',
- POLICY_MEMBER_LIST: 'policyMemberList_',
- DOWNLOAD: 'download_',
+ REPORT_USER_IS_TYPING: 'reportUserIsTyping_',
+ TRANSACTION: 'transactions_',
},
// Indicates which locale should be used
@@ -187,6 +188,7 @@ export default {
HOME_ADDRESS_FORM: 'homeAddressForm',
NEW_ROOM_FORM: 'newRoomForm',
ROOM_SETTINGS_FORM: 'roomSettingsForm',
+ MONEY_REQUEST_DESCRIPTION_FORM: 'moneyRequestDescriptionForm',
},
// Whether we should show the compose input or not
diff --git a/src/ROUTES.js b/src/ROUTES.js
index 470768a594ac..423169d6dd3e 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -75,6 +75,7 @@ export default {
getIOUSendRoute: reportID => `${IOU_SEND}/${reportID}`,
IOU_BILL_CURRENCY: `${IOU_BILL_CURRENCY}/:reportID?`,
IOU_REQUEST_CURRENCY: `${IOU_REQUEST_CURRENCY}/:reportID?`,
+ MONEY_REQUEST_DESCRIPTION: `${IOU_REQUEST}/description`,
IOU_SEND_CURRENCY: `${IOU_SEND_CURRENCY}/:reportID?`,
IOU_SEND_ADD_BANK_ACCOUNT: `${IOU_SEND}/add-bank-account`,
IOU_SEND_ADD_DEBIT_CARD: `${IOU_SEND}/add-debit-card`,
diff --git a/src/components/Alert/index.js b/src/components/Alert/index.js
new file mode 100644
index 000000000000..dc3a722c2331
--- /dev/null
+++ b/src/components/Alert/index.js
@@ -0,0 +1,24 @@
+import _ from 'underscore';
+
+/**
+ * Shows an alert modal with ok and cancel options.
+ *
+ * @param {String} title The title of the alert
+ * @param {String} description The description of the alert
+ * @param {Object[]} options An array of objects with `style` and `onPress` properties
+ */
+export default (title, description, options) => {
+ const result = _.filter(window.confirm([title, description], Boolean)).join('\n');
+
+ if (result) {
+ const confirmOption = _.find(options, ({style}) => style !== 'cancel');
+ if (confirmOption && confirmOption.onPress) {
+ confirmOption.onPress();
+ }
+ } else {
+ const cancelOption = _.find(options, ({style}) => style === 'cancel');
+ if (cancelOption && cancelOption.onPress) {
+ cancelOption.onPress();
+ }
+ }
+};
diff --git a/src/components/Alert/index.native.js b/src/components/Alert/index.native.js
new file mode 100644
index 000000000000..31c837a7dd6b
--- /dev/null
+++ b/src/components/Alert/index.native.js
@@ -0,0 +1,3 @@
+import {Alert} from 'react-native';
+
+export default Alert.alert;
diff --git a/src/components/AvatarCropModal/AvatarCropModal.js b/src/components/AvatarCropModal/AvatarCropModal.js
index 97d1ba0368d0..761c453c1964 100644
--- a/src/components/AvatarCropModal/AvatarCropModal.js
+++ b/src/components/AvatarCropModal/AvatarCropModal.js
@@ -78,6 +78,9 @@ const AvatarCropModal = (props) => {
const translateSlider = useSharedValue(0);
const isPressableEnabled = useSharedValue(true);
+ // Check if image cropping, saving or uploading is in progress
+ const isLoading = useSharedValue(false);
+
// The previous offset values are maintained to recalculate the offset value in proportion
// to the container size, especially when the window size is first decreased and then increased
const prevMaxOffsetX = useSharedValue(0);
@@ -114,11 +117,12 @@ const AvatarCropModal = (props) => {
translateSlider.value = 0;
prevMaxOffsetX.value = 0;
prevMaxOffsetY.value = 0;
+ isLoading.value = false;
setImageContainerSize(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
setSliderContainerSize(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
setIsImageContainerInitialized(false);
setIsImageInitialized(false);
- }, [originalImageHeight, originalImageWidth, prevMaxOffsetX, prevMaxOffsetY, rotation, scale, translateSlider, translateX, translateY]);
+ }, [originalImageHeight, originalImageWidth, prevMaxOffsetX, prevMaxOffsetY, rotation, scale, translateSlider, translateX, translateY, isLoading]);
// In order to calculate proper image position/size/animation, we have to know its size.
// And we have to update image size if image url changes.
@@ -305,6 +309,10 @@ const AvatarCropModal = (props) => {
// Crops an image that was provided in the imageUri prop, using the current position/scale
// then calls onSave and onClose callbacks
const cropAndSaveImage = useCallback(() => {
+ if (isLoading.value) {
+ return;
+ }
+ isLoading.value = true;
const smallerSize = Math.min(originalImageHeight.value, originalImageWidth.value);
const size = smallerSize / scale.value;
const imageCenterX = originalImageWidth.value / 2;
@@ -334,8 +342,11 @@ const AvatarCropModal = (props) => {
.then((newImage) => {
props.onClose();
props.onSave(newImage);
+ })
+ .catch(() => {
+ isLoading.value = false;
});
- }, [originalImageHeight.value, originalImageWidth.value, scale.value, translateX.value, imageContainerSize, translateY.value, props, rotation.value]);
+ }, [originalImageHeight.value, originalImageWidth.value, scale.value, translateX.value, imageContainerSize, translateY.value, props, rotation.value, isLoading]);
/**
* @param {Number} locationX
@@ -346,7 +357,7 @@ const AvatarCropModal = (props) => {
'worklet';
- if (!isPressableEnabled.value) {
+ if (!locationX || !isPressableEnabled.value) {
return;
}
const newSliderValue = clamp(locationX, [0, sliderContainerSize]);
diff --git a/src/components/Button.js b/src/components/Button/index.js
similarity index 93%
rename from src/components/Button.js
rename to src/components/Button/index.js
index 329153d1993b..b19e405211d6 100644
--- a/src/components/Button.js
+++ b/src/components/Button/index.js
@@ -1,19 +1,20 @@
import React, {Component} from 'react';
import {Pressable, ActivityIndicator, View} from 'react-native';
import PropTypes from 'prop-types';
-import styles from '../styles/styles';
-import themeColors from '../styles/themes/default';
-import OpacityView from './OpacityView';
-import Text from './Text';
-import KeyboardShortcut from '../libs/KeyboardShortcut';
-import Icon from './Icon';
-import CONST from '../CONST';
-import * as StyleUtils from '../styles/StyleUtils';
-import HapticFeedback from '../libs/HapticFeedback';
-import withNavigationFallback from './withNavigationFallback';
-import compose from '../libs/compose';
-import * as Expensicons from './Icon/Expensicons';
-import withNavigationFocus from './withNavigationFocus';
+import styles from '../../styles/styles';
+import themeColors from '../../styles/themes/default';
+import OpacityView from '../OpacityView';
+import Text from '../Text';
+import KeyboardShortcut from '../../libs/KeyboardShortcut';
+import Icon from '../Icon';
+import CONST from '../../CONST';
+import * as StyleUtils from '../../styles/StyleUtils';
+import HapticFeedback from '../../libs/HapticFeedback';
+import withNavigationFallback from '../withNavigationFallback';
+import compose from '../../libs/compose';
+import * as Expensicons from '../Icon/Expensicons';
+import withNavigationFocus from '../withNavigationFocus';
+import validateSubmitShortcut from './validateSubmitShortcut';
const propTypes = {
/** The text for the button label */
@@ -157,10 +158,10 @@ class Button extends Component {
// Setup and attach keypress handler for pressing the button with Enter key
this.unsubscribe = KeyboardShortcut.subscribe(shortcutConfig.shortcutKey, (e) => {
- if (!this.props.isFocused || this.props.isDisabled || this.props.isLoading || (e && e.target.nodeName === 'TEXTAREA')) {
+ if (!validateSubmitShortcut(this.props.isFocused, this.props.isDisabled, this.props.isLoading, e)) {
return;
}
- e.preventDefault();
+
this.props.onPress();
}, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true, false, this.props.enterKeyEventListenerPriority, false);
}
diff --git a/src/components/Button/validateSubmitShortcut/index.js b/src/components/Button/validateSubmitShortcut/index.js
new file mode 100644
index 000000000000..bfe5c79483fa
--- /dev/null
+++ b/src/components/Button/validateSubmitShortcut/index.js
@@ -0,0 +1,19 @@
+/**
+ * Validate if the submit shortcut should be triggered depending on the button state
+ *
+ * @param {boolean} isFocused Whether Button is on active screen
+ * @param {boolean} isDisabled Indicates whether the button should be disabled
+ * @param {boolean} isLoading Indicates whether the button should be disabled and in the loading state
+ * @param {Object} event Focused input event
+ * @returns {boolean} Returns `true` if the shortcut should be triggered
+ */
+function validateSubmitShortcut(isFocused, isDisabled, isLoading, event) {
+ if (!isFocused || isDisabled || isLoading || (event && event.target.nodeName === 'TEXTAREA')) {
+ return false;
+ }
+
+ event.preventDefault();
+ return true;
+}
+
+export default validateSubmitShortcut;
diff --git a/src/components/Button/validateSubmitShortcut/index.native.js b/src/components/Button/validateSubmitShortcut/index.native.js
new file mode 100644
index 000000000000..2822fa56d590
--- /dev/null
+++ b/src/components/Button/validateSubmitShortcut/index.native.js
@@ -0,0 +1,17 @@
+/**
+ * Validate if the submit shortcut should be triggered depending on the button state
+ *
+ * @param {boolean} isFocused Whether Button is on active screen
+ * @param {boolean} isDisabled Indicates whether the button should be disabled
+ * @param {boolean} isLoading Indicates whether the button should be disabled and in the loading state
+ * @returns {boolean} Returns `true` if the shortcut should be triggered
+ */
+function validateSubmitShortcut(isFocused, isDisabled, isLoading) {
+ if (!isFocused || isDisabled || isLoading) {
+ return false;
+ }
+
+ return true;
+}
+
+export default validateSubmitShortcut;
diff --git a/src/components/ContextMenuItem.js b/src/components/ContextMenuItem.js
index 9dace75bec03..07059a92c5ed 100644
--- a/src/components/ContextMenuItem.js
+++ b/src/components/ContextMenuItem.js
@@ -55,10 +55,9 @@ class ContextMenuItem extends Component {
* Method to call parent onPress and toggleDelayButtonState
*/
triggerPressAndUpdateSuccess() {
- if (this.props.isDelayButtonStateComplete) {
- return;
+ if (!this.props.isDelayButtonStateComplete) {
+ this.props.onPress();
}
- this.props.onPress();
// We only set the success state when we have icon or text to represent the success state
// We may want to replace this check by checking the Result from OnPress Callback in future.
diff --git a/src/components/ExpensifyWordmark.js b/src/components/ExpensifyWordmark.js
new file mode 100644
index 000000000000..11999dad22d2
--- /dev/null
+++ b/src/components/ExpensifyWordmark.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import {View} from 'react-native';
+import ProductionLogo from '../../assets/images/expensify-wordmark.svg';
+import DevLogo from '../../assets/images/expensify-logo--dev.svg';
+import StagingLogo from '../../assets/images/expensify-logo--staging.svg';
+import AdHocLogo from '../../assets/images/expensify-logo--adhoc.svg';
+import CONST from '../CONST';
+import withEnvironment, {environmentPropTypes} from './withEnvironment';
+import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
+import compose from '../libs/compose';
+import themeColors from '../styles/themes/default';
+import styles from '../styles/styles';
+import * as StyleUtils from '../styles/StyleUtils';
+import variables from '../styles/variables';
+
+const propTypes = {
+ ...environmentPropTypes,
+ ...windowDimensionsPropTypes,
+};
+
+const logoComponents = {
+ [CONST.ENVIRONMENT.DEV]: DevLogo,
+ [CONST.ENVIRONMENT.STAGING]: StagingLogo,
+ [CONST.ENVIRONMENT.PRODUCTION]: ProductionLogo,
+};
+
+const ExpensifyWordmark = (props) => {
+ // PascalCase is required for React components, so capitalize the const here
+ const LogoComponent = logoComponents[props.environment] || AdHocLogo;
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+ExpensifyWordmark.displayName = 'ExpensifyWordmark';
+ExpensifyWordmark.propTypes = propTypes;
+export default compose(
+ withEnvironment,
+ withWindowDimensions,
+)(ExpensifyWordmark);
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
index 92c327bc6053..2d400d31fe12 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
@@ -32,6 +32,7 @@ const AnchorRenderer = (props) => {
&& !CONST.PATHS_TO_TREAT_AS_EXTERNAL.includes(attrPath) ? attrPath : '';
const internalExpensifyPath = hasExpensifyOrigin
&& !attrPath.startsWith(CONFIG.EXPENSIFY.CONCIERGE_URL_PATHNAME)
+ && !attrPath.startsWith(CONFIG.EXPENSIFY.DEVPORTAL_URL_PATHNAME)
&& attrPath;
const navigateToLink = () => {
// There can be messages from Concierge with links to specific NewDot reports. Those URLs look like this:
diff --git a/src/components/Icon/Illustrations.js b/src/components/Icon/Illustrations.js
index 167d8e90f29f..0523fdbbc1e7 100644
--- a/src/components/Icon/Illustrations.js
+++ b/src/components/Icon/Illustrations.js
@@ -39,6 +39,7 @@ import ConciergeNew from '../../../assets/images/simple-illustrations/simple-ill
import MoneyBadge from '../../../assets/images/simple-illustrations/simple-illustration__moneybadge.svg';
import TreasureChest from '../../../assets/images/simple-illustrations/simple-illustration__treasurechest.svg';
import ThumbsUpStars from '../../../assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg';
+import Hands from '../../../assets/images/product-illustrations/home-illustration-hands.svg';
export {
Abracadabra,
@@ -82,4 +83,5 @@ export {
MoneyBadge,
TreasureChest,
ThumbsUpStars,
+ Hands,
};
diff --git a/src/components/KeyboardShortcutsModal.js b/src/components/KeyboardShortcutsModal.js
index 81ad2f642831..a5454e280f0d 100644
--- a/src/components/KeyboardShortcutsModal.js
+++ b/src/components/KeyboardShortcutsModal.js
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {View} from 'react-native';
+import {View, ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import HeaderWithCloseButton from './HeaderWithCloseButton';
@@ -34,18 +34,26 @@ const defaultProps = {
class KeyboardShortcutsModal extends React.Component {
componentDidMount() {
- const shortcutConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUT_MODAL;
- this.unsubscribeShortcutModal = KeyboardShortcut.subscribe(shortcutConfig.shortcutKey, () => {
+ const openShortcutModalConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUT_MODAL;
+ this.unsubscribeShortcutModal = KeyboardShortcut.subscribe(openShortcutModalConfig.shortcutKey, () => {
ModalActions.close();
KeyboardShortcutsActions.showKeyboardShortcutModal();
- }, shortcutConfig.descriptionKey, shortcutConfig.modifiers, true);
+ }, openShortcutModalConfig.descriptionKey, openShortcutModalConfig.modifiers, true);
+
+ const closeShortcutModalConfig = CONST.KEYBOARD_SHORTCUTS.ESCAPE;
+ this.unsubscribeEscapeModal = KeyboardShortcut.subscribe(closeShortcutModalConfig.shortcutKey, () => {
+ ModalActions.close();
+ KeyboardShortcutsActions.hideKeyboardShortcutModal();
+ }, closeShortcutModalConfig.descriptionKey, closeShortcutModalConfig.modifiers, true, true);
}
componentWillUnmount() {
- if (!this.unsubscribeShortcutModal) {
- return;
+ if (this.unsubscribeShortcutModal) {
+ this.unsubscribeShortcutModal();
+ }
+ if (this.unsubscribeEscapeModal) {
+ this.unsubscribeEscapeModal();
}
- this.unsubscribeShortcutModal();
}
/**
@@ -85,7 +93,7 @@ class KeyboardShortcutsModal extends React.Component {
onClose={KeyboardShortcutsActions.hideKeyboardShortcutModal}
>
-
+
{this.props.translate('keyboardShortcutModal.subtitle')}
@@ -95,7 +103,7 @@ class KeyboardShortcutsModal extends React.Component {
})}
-
+
);
}
diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js
index 49aa459b728d..e61cef1379df 100644
--- a/src/components/LocalePicker.js
+++ b/src/components/LocalePicker.js
@@ -48,7 +48,7 @@ const LocalePicker = (props) => {
size={props.size}
value={props.preferredLocale}
containerStyles={props.size === 'small' ? [styles.pickerContainerSmall] : []}
- backgroundColor={themeColors.midtone}
+ backgroundColor={themeColors.signInPage}
/>
);
};
diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js
index 527b2620e557..c3e823715a33 100644
--- a/src/components/MenuItem.js
+++ b/src/components/MenuItem.js
@@ -1,8 +1,6 @@
import _ from 'underscore';
import React from 'react';
-import {
- View, Pressable,
-} from 'react-native';
+import {View} from 'react-native';
import Text from './Text';
import styles from '../styles/styles';
import * as StyleUtils from '../styles/StyleUtils';
@@ -18,9 +16,14 @@ import colors from '../styles/colors';
import variables from '../styles/variables';
import MultipleAvatars from './MultipleAvatars';
import * as defaultWorkspaceAvatars from './Icon/WorkspaceDefaultAvatars';
+import PressableWithSecondaryInteraction from './PressableWithSecondaryInteraction';
+import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
+import * as DeviceCapabilities from '../libs/DeviceCapabilities';
+import ControlSelection from '../libs/ControlSelection';
const propTypes = {
...menuItemPropTypes,
+ ...windowDimensionsPropTypes,
};
const defaultProps = {
@@ -30,7 +33,8 @@ const defaultProps = {
shouldShowBasicTitle: false,
shouldShowDescriptionOnTop: false,
wrapperStyle: [],
- style: {},
+ style: styles.popoverMenuItem,
+ titleStyle: {},
success: false,
icon: undefined,
iconWidth: undefined,
@@ -45,11 +49,13 @@ const defaultProps = {
subtitle: undefined,
iconType: CONST.ICON_TYPE_ICON,
onPress: () => {},
+ onSecondaryInteraction: undefined,
interactive: true,
fallbackIcon: Expensicons.FallbackAvatar,
brickRoadIndicator: '',
floatRightAvatars: [],
shouldStackHorizontally: false,
+ shouldBlockSelection: false,
};
const MenuItem = (props) => {
@@ -59,7 +65,7 @@ const MenuItem = (props) => {
(props.shouldShowBasicTitle ? undefined : styles.textStrong),
(props.interactive && props.disabled ? {...styles.disabledText, ...styles.userSelectNone} : undefined),
styles.pre,
- ], props.style);
+ ], props.titleStyle);
const descriptionVerticalMargin = props.shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1;
const descriptionTextStyle = StyleUtils.combineStyles([
styles.textLabelSupporting,
@@ -67,10 +73,10 @@ const MenuItem = (props) => {
styles.breakWord,
styles.lineHeightNormal,
props.title ? descriptionVerticalMargin : undefined,
- ], props.style);
+ ]);
return (
- {
if (props.disabled) {
return;
@@ -82,12 +88,16 @@ const MenuItem = (props) => {
props.onPress(e);
}}
+ onPressIn={() => props.shouldBlockSelection && props.isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
+ onPressOut={ControlSelection.unblock}
+ onSecondaryInteraction={props.onSecondaryInteraction}
style={({hovered, pressed}) => ([
- styles.popoverMenuItem,
+ props.style,
StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || hovered, pressed, props.success, props.disabled, props.interactive), true),
..._.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle],
])}
disabled={props.disabled}
+ ref={props.forwardedRef}
>
{({hovered, pressed}) => (
<>
@@ -210,12 +220,14 @@ const MenuItem = (props) => {
>
)}
-
+
);
};
MenuItem.propTypes = propTypes;
MenuItem.defaultProps = defaultProps;
MenuItem.displayName = 'MenuItem';
-
-export default MenuItem;
+export default withWindowDimensions(React.forwardRef((props, ref) => (
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+)));
diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js
index e442d3766893..fe7d40b582f8 100644
--- a/src/components/MenuItemList.js
+++ b/src/components/MenuItemList.js
@@ -3,6 +3,8 @@ import _ from 'underscore';
import PropTypes from 'prop-types';
import MenuItem from './MenuItem';
import menuItemPropTypes from './menuItemPropTypes';
+import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu';
+import {CONTEXT_MENU_TYPES} from '../pages/home/report/ContextMenu/ContextMenuActions';
const propTypes = {
/** An array of props that are pass to individual MenuItem components */
@@ -12,17 +14,38 @@ const defaultProps = {
menuItems: [],
};
-const MenuItemList = props => (
- <>
- {_.map(props.menuItems, menuItemProps => (
-
- ))}
- >
-);
+const MenuItemList = (props) => {
+ let popoverAnchor;
+
+ /**
+ * Handle the secondary interaction for a menu item.
+ *
+ * @param {*} link the menu item link or function to get the link
+ * @param {Event} e the interaction event
+ */
+ const secondaryInteraction = (link, e) => {
+ if (typeof link === 'function') {
+ link().then(url => ReportActionContextMenu.showContextMenu(CONTEXT_MENU_TYPES.LINK, e, url, popoverAnchor));
+ } else if (!_.isEmpty(link)) {
+ ReportActionContextMenu.showContextMenu(CONTEXT_MENU_TYPES.LINK, e, link, popoverAnchor);
+ }
+ };
+
+ return (
+ <>
+ {_.map(props.menuItems, menuItemProps => (
+