diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b8a13b47437a..2e1a01ec1301 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,7 +2,7 @@
Welcome! Thanks for checking out Expensify.cash and for taking the time to contribute!
## Getting Started
-This guide is specifically for external contributors. For a general overview of the repo, check out our README located [here](https://github.com/Expensify/Expensify.cash/blob/master/README.md). The parts of the README to pay particular attention to are how to [run the web app](https://github.com/Expensify/Expensify.cash#running-the-web-app-via-production-api-proxy-contributors-) and how to [run the desktop/mobile apps](https://github.com/Expensify/Expensify.cash#running-the-desktop-and-mobile-apps-via-production-api-contributors-) locally using our production API.
+This guide is specifically for external contributors. For a general overview of the repo, check out our README located [here](https://github.com/Expensify/Expensify.cash/blob/master/README.md). The part of the README to pay particular attention to is how to [run the app](https://github.com/Expensify/Expensify.cash#running-the-apps-via-production-api-proxy-contributors-) locally using our production API.
#### Test Accounts
You can create as many accounts as needed in order to test your changes. You can create your test accounts directly from [expensify.cash](https://expensify.cash/). Right now, accounts can't chat with each other by default. If you want your test accounts to be able to chat with each other, post in the #expensify-contributors [Slack channel](https://github.com/Expensify/Expensify.cash/blob/master/CONTRIBUTING.md#asking-questions) to ask someone to add your test accounts to a policy that allows chatting.
@@ -11,7 +11,7 @@ You can create as many accounts as needed in order to test your changes. You can
This project and everyone participating in it is governed by the Expensify [Code of Conduct](https://github.com/Expensify/Expensify.cash/blob/master/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [contributors@expensify.com](mailto:contributors@expensify.com).
## Asking Questions
-The best way to ask questions is to join our #expensify-contributors Slack channel. To request an invite to the channel, just email contributors@expensify.com with the subject "Slack Channel Invite" and we'll send you an invite! Please do not create issues to ask questions.
+The best way to ask questions is to join our #expensify-contributors Slack channel. To request an invite to the channel, just email contributors@expensify.com with the subject "Slack Channel Invite" and we'll send you an invite! Please do not create issues to ask questions.
## Reporting Vulnerabilities
If you've found a vulnerability, please email security@expensify.com with the subject `Vulnerability Report` instead of creating an issue.
@@ -20,12 +20,12 @@ If you've found a vulnerability, please email security@expensify.com with the su
If you'd like to create a new issue, please first make sure the issue does not exist in the [issue list](https://github.com/Expensify/Expensify.cash/issues). When creating a new issue, please include all the required information on the issue template.
## Payment for Contributions
-We are currently managing payment via Upwork. If you'd like to be paid for your contributions, please apply to fix the issue from our [Upwork issue list](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2). Each issue in this repo will also link out to the associated Upwork job.
+We are currently managing payment via Upwork. If you'd like to be paid for your contributions, please apply to fix the issue from our [Upwork issue list](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2). Each issue in this repo will also link out to the associated Upwork job.
## Submitting a Pull Request
#### Proposing a Change
1. Fork this repository and create a new branch
-1. [Open a PR](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) and be sure to fill in all the required information on the PR template.
+1. [Open a PR](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork). Be sure to fill in all the required information on the PR template, and be sure all of your [commits are signed](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/signing-commits).
1. An Expensify engineer will be automatically assigned to review your PR
1. You will need all checks to pass:
1. CLA - You must sign our [Contributor License Agreement](https://github.com/Expensify/Expensify.cash/blob/master/CLA.md) by following the CLA bot instructions that will be posted on your PR
diff --git a/README.md b/README.md
index 136840447fa0..f4127fcdea4d 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,14 @@
-# [Expensify.cash](https://Expensify.cash)
-
## Philosophy
This application is built with the following principles.
1. **Data Flow** - Ideally, this is how data flows through the app:
@@ -92,16 +97,14 @@ Now, all of your API calls will be using the ngrok route.
## Running the MacOS desktop app 🖥
* To run the **Development app**, run: `npm run desktop`, this will start a new Electron process running on your MacOS desktop in the `dist/Mac` folder.
-## Running the web app via production API proxy (Contributors) 🧑💻
+## Running the apps via production API proxy (Contributors) 🧑💻
If you don't have full-access to Expensify's development environment you will need to run the app against the production API.
* Copy the `.env.production` variables into your `.env` file
* Set `EXPENSIFY_URL_COM` environment variable to be empty (**Note:** this means it should be `EXPENSIFY_URL_COM=`, not completely omitted)
-* Run the **Development Server**: `npm run proxy`
-
-## Running the desktop and mobile apps via production API (Contributors) 🧑💻
-If you don't have full-access to Expensify's development environment you will need to run the app against the production API.
-* Copy the `.env.production` variables into your `.env` file (**Note:** Reset `EXPENSIFY_URL_COM` if you previously deleted it)
-* Run the desktop, iOS, or Android builds via `npm run desktop`, `npm run ios`, `npm run android` respectively
+* To run the web app, run the **Development Server**: `npm run proxy`
+* To run the desktop app: `npm run desktop`
+* To run the iOS app: `npm run ios`
+* To run the Android app: `npm run android`
## Running the tests 🎰
### Unit tests
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 6927bfd0a91a..c9e7754b051a 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -148,8 +148,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 281
- versionName "1.0.1-280"
+ versionCode 292
+ versionName "1.0.1-291"
}
splits {
abi {
diff --git a/android/app/src/main/res/drawable-hdpi/ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_notification.png
index e084753fb90e..15320cfb4d44 100644
Binary files a/android/app/src/main/res/drawable-hdpi/ic_notification.png and b/android/app/src/main/res/drawable-hdpi/ic_notification.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_notification.png
index 669579148c61..24b381c77b93 100644
Binary files a/android/app/src/main/res/drawable-mdpi/ic_notification.png and b/android/app/src/main/res/drawable-mdpi/ic_notification.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_notification.png
index 18c06a8d16c3..4f88da13fbc7 100644
Binary files a/android/app/src/main/res/drawable-xhdpi/ic_notification.png and b/android/app/src/main/res/drawable-xhdpi/ic_notification.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png
index da94014c33f0..ad6e656c672b 100644
Binary files a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png and b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png
index 96f1cb18a2be..972b817fa33b 100644
Binary files a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png and b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 62bcc8e5d368..1c978ebca5f7 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index 5add8a771a06..7a5cf1884b2e 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/assets/images/expensify-logo-round.png b/assets/images/expensify-logo-round.png
index 01e85cfc4e8a..0e150943264e 100644
Binary files a/assets/images/expensify-logo-round.png and b/assets/images/expensify-logo-round.png differ
diff --git a/assets/images/welcome-screenshot-wide.png b/assets/images/welcome-screenshot-wide.png
new file mode 100644
index 000000000000..3b236c219f1b
Binary files /dev/null and b/assets/images/welcome-screenshot-wide.png differ
diff --git a/assets/images/welcome-screenshot.png b/assets/images/welcome-screenshot.png
new file mode 100644
index 000000000000..e4708fea4eaa
Binary files /dev/null and b/assets/images/welcome-screenshot.png differ
diff --git a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/Contents.json
index a90a5722ff43..2c82e7765222 100644
--- a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/Contents.json
+++ b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -31,13 +31,13 @@
"size" : "40x40"
},
{
- "filename" : "iOSIcon@2x.png",
+ "filename" : "iOS@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
- "filename" : "iOSIcon@3x.png",
+ "filename" : "iOS@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
@@ -91,7 +91,7 @@
"size" : "83.5x83.5"
},
{
- "filename" : "iOSIcon_AppStore@2x.png",
+ "filename" : "Store.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
diff --git a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/Store.png b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/Store.png
new file mode 100644
index 000000000000..cd3bdb9a4b07
Binary files /dev/null and b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/Store.png differ
diff --git a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOS@2x.png b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOS@2x.png
new file mode 100644
index 000000000000..a0ba63905920
Binary files /dev/null and b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOS@2x.png differ
diff --git a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOS@3x.png b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOS@3x.png
new file mode 100644
index 000000000000..4e5679068784
Binary files /dev/null and b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOS@3x.png differ
diff --git a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOSIcon@2x.png b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOSIcon@2x.png
deleted file mode 100644
index 8f4ddc74a644..000000000000
Binary files a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOSIcon@2x.png and /dev/null differ
diff --git a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOSIcon@3x.png b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOSIcon@3x.png
deleted file mode 100644
index 341d8b607f9e..000000000000
Binary files a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOSIcon@3x.png and /dev/null differ
diff --git a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOSIcon_AppStore@2x.png b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOSIcon_AppStore@2x.png
deleted file mode 100644
index e6c4814e34cc..000000000000
Binary files a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iOSIcon_AppStore@2x.png and /dev/null differ
diff --git a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPad.png b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPad.png
index 2ae019f646eb..5d4cbe5f4c19 100644
Binary files a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPad.png and b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPad.png differ
diff --git a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPad@2x.png b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPad@2x.png
index e250046114dc..3d73e33910a1 100644
Binary files a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPad@2x.png and b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPad@2x.png differ
diff --git a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPadPro.png b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPadPro.png
index 9f8af3a99843..338d9fc1422e 100644
Binary files a/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPadPro.png and b/ios/ExpensifyCash/Images.xcassets/AppIcon.appiconset/iPadPro.png differ
diff --git a/ios/ExpensifyCash/Info.plist b/ios/ExpensifyCash/Info.plist
index 5b9691187a9b..f7186704c5ab 100644
--- a/ios/ExpensifyCash/Info.plist
+++ b/ios/ExpensifyCash/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 281
+ 292
ITSAppUsesNonExemptEncryption
LSRequiresIPhoneOS
diff --git a/ios/ExpensifyCashTests/Info.plist b/ios/ExpensifyCashTests/Info.plist
index a838c15c13a2..761748984535 100644
--- a/ios/ExpensifyCashTests/Info.plist
+++ b/ios/ExpensifyCashTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 281
+ 292
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 2b0958a7bb48..2e7bc7f46517 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -338,7 +338,7 @@ PODS:
- React
- react-native-safe-area-context (3.1.8):
- React-Core
- - react-native-webview (10.10.0):
+ - react-native-webview (11.0.2):
- React-Core
- React-RCTActionSheet (0.63.3):
- React-Core/RCTActionSheetHeaders (= 0.63.3)
@@ -649,7 +649,7 @@ SPEC CHECKSUMS:
react-native-progress-bar-android: ce95a69f11ac580799021633071368d08aaf9ad8
react-native-progress-view: 5816e8a6be812c2b122c6225a2a3db82d9008640
react-native-safe-area-context: 01158a92c300895d79dee447e980672dc3fb85a6
- react-native-webview: 2e330b109bfd610e9818bf7865d1979f898960a7
+ react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
React-RCTActionSheet: 53ea72699698b0b47a6421cb1c8b4ab215a774aa
React-RCTAnimation: 1befece0b5183c22ae01b966f5583f42e69a83c2
React-RCTBlob: 0b284339cbe4b15705a05e2313a51c6d8b51fa40
diff --git a/package-lock.json b/package-lock.json
index 99ba25be8bc9..d87716693791 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "expensify.cash",
- "version": "1.0.1-280",
+ "version": "1.0.1-291",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -10072,8 +10072,8 @@
}
},
"expensify-common": {
- "version": "git+https://github.com/Expensify/expensify-common.git#cd9f195ed1fd340e7e890c41672a97af4f2956ca",
- "from": "git+https://github.com/Expensify/expensify-common.git#cd9f195ed1fd340e7e890c41672a97af4f2956ca",
+ "version": "git+https://github.com/Expensify/expensify-common.git#d1bfacca6fb92f7e89a2a9cdf0863fad6481c0f2",
+ "from": "git+https://github.com/Expensify/expensify-common.git#d1bfacca6fb92f7e89a2a9cdf0863fad6481c0f2",
"requires": {
"classnames": "2.2.5",
"clipboard": "2.0.4",
@@ -19505,6 +19505,59 @@
"lodash.merge": "^4.6.2",
"react": "^16.13.1",
"underscore": "^1.11.0"
+ },
+ "dependencies": {
+ "expensify-common": {
+ "version": "git+https://github.com/Expensify/expensify-common.git#cd9f195ed1fd340e7e890c41672a97af4f2956ca",
+ "from": "git+https://github.com/Expensify/expensify-common.git#cd9f195ed1fd340e7e890c41672a97af4f2956ca",
+ "requires": {
+ "classnames": "2.2.5",
+ "clipboard": "2.0.4",
+ "html-entities": "^1.3.1",
+ "jquery": "3.3.1",
+ "lodash.get": "4.4.2",
+ "lodash.has": "4.5.2",
+ "moment": "2.20.1",
+ "prop-types": "15.7.2",
+ "react": "16.12.0",
+ "react-dom": "16.12.0",
+ "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5",
+ "underscore": "1.9.1"
+ },
+ "dependencies": {
+ "react": {
+ "version": "16.12.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
+ "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2"
+ }
+ },
+ "underscore": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz",
+ "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg=="
+ }
+ }
+ },
+ "moment": {
+ "version": "2.20.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz",
+ "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg=="
+ },
+ "react-dom": {
+ "version": "16.12.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz",
+ "integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.6.2",
+ "scheduler": "^0.18.0"
+ }
+ }
}
},
"react-native-pdf": {
diff --git a/package.json b/package.json
index e2fabe60de36..2caccadc4648 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "expensify.cash",
- "version": "1.0.1-280",
+ "version": "1.0.1-291",
"author": "Expensify, Inc.",
"homepage": "https://expensify.cash",
"description": "Expensify.cash is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
@@ -42,7 +42,7 @@
"electron-log": "^4.2.4",
"electron-serve": "^1.0.0",
"electron-updater": "^4.3.4",
- "expensify-common": "git+https://github.com/Expensify/expensify-common.git#cd9f195ed1fd340e7e890c41672a97af4f2956ca",
+ "expensify-common": "git+https://github.com/Expensify/expensify-common.git#d1bfacca6fb92f7e89a2a9cdf0863fad6481c0f2",
"file-loader": "^6.0.0",
"html-entities": "^1.3.1",
"lodash.get": "^4.4.2",
diff --git a/src/CONST.js b/src/CONST.js
index b237c1c24db5..709e7b5c7005 100644
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -4,6 +4,7 @@ const CONST = {
CLOUDFRONT_URL,
PDF_VIEWER_URL: '/pdf/web/viewer.html',
EXPENSIFY_ICON_URL: `${CLOUDFRONT_URL}/images/favicon-2019.png`,
+ UPWORK_URL: 'https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&user_location_match=2',
REPORT: {
SINGLE_USER_DM: 'singleUserDM',
GROUP_USERS_DM: 'groupUsersDM',
diff --git a/src/Expensify.js b/src/Expensify.js
index ecea4da00124..21324aaa0ae6 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -6,7 +6,7 @@ import {recordCurrentlyViewedReportID, recordCurrentRoute} from './libs/actions/
import HomePage from './pages/home/HomePage';
import NotFoundPage from './pages/NotFound';
import SetPasswordPage from './pages/SetPasswordPage';
-import SignInPage from './pages/SignInPage';
+import SignInPage from './pages/signin/SignInPage';
import listenToStorageEvents from './libs/listenToStorageEvents';
import * as ActiveClientManager from './libs/ActiveClientManager';
import ONYXKEYS from './ONYXKEYS';
@@ -104,12 +104,11 @@ class Expensify extends Component {
);
}
- const redirectTo = !this.state.authToken ? ROUTES.SIGNIN : this.props.redirectTo;
return (
{/* If there is ever a property for redirecting, we do the redirect here */}
{/* Leave this as a ternary or else iOS throws an error about text not being wrapped in */}
- {redirectTo ? : null}
+ {this.props.redirectTo ? : null}
{/* We must record the currentlyViewedReportID when hitting the 404 page so */}
@@ -119,17 +118,27 @@ class Expensify extends Component {
(
this.state.authToken
?
:
)}
/>
+
-
+ (
+
+ // Need to do this for every page that the user needs to be logged in to access
+ this.state.authToken
+ ?
+ :
+ )}
+ />
);
diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index b89355a0ebbb..1e1bdb60aa6d 100644
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -2,6 +2,11 @@
* This is a file containing constants for all the top level keys in our store
*/
export default {
+ // Holds information about the users account that is logging in
+ ACCOUNT: 'account',
+
+ // Holds an array of client IDs which is used for multi-tabs on web in order to know
+ // which tab is the leader, and which ones are the followers
ACTIVE_CLIENTS: 'activeClients2',
// When this key is changed, the active page changes (see Expensify.js and `redirect` in actions/App.js)
diff --git a/src/libs/API.js b/src/libs/API.js
index 66f3485be9f1..aeab7db04d12 100644
--- a/src/libs/API.js
+++ b/src/libs/API.js
@@ -26,7 +26,15 @@ Onyx.connect({
* @return {Boolean}
*/
function isAuthTokenRequired(command) {
- return !_.contains(['Log', 'Graphite_Timer', 'Authenticate'], command);
+ return !_.contains([
+ 'Log',
+ 'Graphite_Timer',
+ 'Authenticate',
+ 'GetAccountStatus',
+ 'SetGithubUsername',
+ 'SetPassword',
+ 'User_SignUp',
+ ], command);
}
/**
@@ -254,6 +262,19 @@ function CreateChatReport(parameters) {
return request(commandName, parameters);
}
+/**
+ * @param {Object} parameters
+ * @param {String} parameters.email
+ * @returns {Promise}
+ */
+function User_SignUp(parameters) {
+ const commandName = 'User_SignUp';
+ requireParameters([
+ 'email',
+ ], parameters, commandName);
+ return request(commandName, parameters);
+}
+
/**
* @param {Object} parameters
* @param {String} parameters.authToken
@@ -297,8 +318,18 @@ function DeleteLogin(parameters) {
*/
function Get(parameters) {
const commandName = 'Get';
- requireParameters(['returnValueList'],
- parameters, commandName);
+ requireParameters(['returnValueList'], parameters, commandName);
+ return request(commandName, parameters);
+}
+
+/**
+ * @param {Object} parameters
+ * @param {String} parameters.email
+ * @returns {Promise}
+ */
+function GetAccountStatus(parameters) {
+ const commandName = 'GetAccountStatus';
+ requireParameters(['email'], parameters, commandName);
return request(commandName, parameters);
}
@@ -407,6 +438,28 @@ function Report_UpdateLastRead(parameters) {
return request(commandName, parameters);
}
+/**
+ * @param {Object} parameters
+ * @param {Number} parameters.email
+ * @returns {Promise}
+ */
+function ResendValidateCode(parameters) {
+ const commandName = 'ResendValidateCode';
+ requireParameters(['email'], parameters, commandName);
+ return request(commandName, parameters);
+}
+
+/**
+ * @param {Object} parameters
+ * @param {String} parameters.githubUsername
+ * @returns {Promise}
+ */
+function SetGithubUsername(parameters) {
+ const commandName = 'SetGithubUsername';
+ requireParameters(['email', 'githubUsername'], parameters, commandName);
+ return request(commandName, parameters);
+}
+
/**
* @param {Object} parameters
* @param {String} parameters.password
@@ -415,7 +468,7 @@ function Report_UpdateLastRead(parameters) {
*/
function SetPassword(parameters) {
const commandName = 'SetPassword';
- requireParameters(['password', 'validateCode'], parameters, commandName);
+ requireParameters(['email', 'password', 'validateCode'], parameters, commandName);
return request(commandName, parameters);
}
@@ -426,6 +479,7 @@ export {
CreateLogin,
DeleteLogin,
Get,
+ GetAccountStatus,
Graphite_Timer,
Log,
PersonalDetails_GetForEmails,
@@ -434,5 +488,8 @@ export {
Report_GetHistory,
Report_TogglePinned,
Report_UpdateLastRead,
+ ResendValidateCode,
+ SetGithubUsername,
SetPassword,
+ User_SignUp,
};
diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.js b/src/libs/Notification/LocalNotification/BrowserNotifications.js
index 7d562a51ba3e..01c8a7930ceb 100644
--- a/src/libs/Notification/LocalNotification/BrowserNotifications.js
+++ b/src/libs/Notification/LocalNotification/BrowserNotifications.js
@@ -1,9 +1,8 @@
// Web and desktop implementation only. Do not import for direct use. Use LocalNotification.
import Str from 'expensify-common/lib/str';
-import CONST from '../../../CONST';
import focusApp from './focusApp';
+import EXPENSIFY_ICON_URL from '../../../../assets/images/expensify-logo-round.png';
-const EXPENSIFY_ICON_URL = `${CONST.CLOUDFRONT_URL}/images/favicon-2019.png`;
const DEFAULT_DELAY = 4000;
/**
diff --git a/src/libs/actions/Session.js b/src/libs/actions/Session.js
index e6336329478a..f853f8720500 100644
--- a/src/libs/actions/Session.js
+++ b/src/libs/actions/Session.js
@@ -7,10 +7,9 @@ import * as API from '../API';
import CONFIG from '../../CONFIG';
import PushNotification from '../Notification/PushNotification';
import ROUTES from '../../ROUTES';
-import {redirect} from './App';
import Timing from './Timing';
-let credentials;
+let credentials = {};
Onyx.connect({
key: ONYXKEYS.CREDENTIALS,
callback: val => credentials = val,
@@ -33,37 +32,103 @@ function setSuccessfulSignInData(data, exitTo) {
}
/**
- * Sign in with the API
+ * Create an account for the user logging in.
+ * This will send them a notification with a link to click on to validate the account and set a password
*
- * @param {String} partnerUserID
- * @param {String} partnerUserSecret
+ * @param {String} login
+ */
+function createAccount(login) {
+ Onyx.merge(ONYXKEYS.SESSION, {error: ''});
+
+ API.User_SignUp({
+ email: login,
+ isViaExpensifyCash: true
+ }).then((response) => {
+ if (response.jsonCode !== 200) {
+ let errorMessage = response.message || `Unknown API Error: ${response.jsonCode}`;
+ if (!response.message && response.jsonCode === 405) {
+ errorMessage = 'Cannot create an account that is under a controlled domain';
+ }
+ Onyx.merge(ONYXKEYS.SESSION, {error: errorMessage});
+ Onyx.merge(ONYXKEYS.CREDENTIALS, {login: null});
+ }
+ });
+}
+
+/**
+ * Clears the Onyx store and redirects user to the sign in page
+ */
+function signOut() {
+ Timing.clearData();
+ redirectToSignIn();
+ if (!credentials || !credentials.login) {
+ return;
+ }
+
+ API.DeleteLogin({
+ partnerUserID: credentials.login,
+ partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
+ partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
+ doNotRetry: true,
+ })
+ .catch(error => Onyx.merge(ONYXKEYS.SESSION, {error: error.message}));
+}
+
+/**
+ * Checks the API to see if an account exists for the given login
+ *
+ * @param {String} login
+ */
+function fetchAccountDetails(login) {
+ Onyx.merge(ONYXKEYS.SESSION, {error: ''});
+
+ API.GetAccountStatus({email: login, isViaExpensifyCash: true})
+ .then((response) => {
+ if (response.jsonCode === 200) {
+ Onyx.merge(ONYXKEYS.CREDENTIALS, {login});
+ Onyx.merge(ONYXKEYS.ACCOUNT, {
+ accountExists: response.accountExists,
+ canAccessExpensifyCash: response.canAccessExpensifyCash,
+ requiresTwoFactorAuth: response.requiresTwoFactorAuth,
+ });
+
+ if (!response.accountExists) {
+ createAccount(login);
+ }
+ }
+ });
+}
+
+/**
+ * Sign the user into the application. This will first authenticate their account
+ * then it will create a temporary login for them which is used when re-authenticating
+ * after an authToken expires.
+ *
+ * @param {String} password
+ * @param {String} exitTo
* @param {String} [twoFactorAuthCode]
- * @param {String} [exitTo]
*/
-function signIn(partnerUserID, partnerUserSecret, twoFactorAuthCode = '', exitTo) {
- Onyx.merge(ONYXKEYS.SESSION, {loading: true, error: ''});
+function signIn(password, exitTo, twoFactorAuthCode) {
+ Onyx.merge(ONYXKEYS.SESSION, {error: ''});
API.Authenticate({
useExpensifyLogin: true,
partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
- partnerUserID,
- partnerUserSecret,
+ partnerUserID: credentials.login,
+ partnerUserSecret: password,
twoFactorAuthCode,
})
-
- // After the user authenticates, create a new login for the user so that we can reauthenticate when the
- // authtoken expires
.then((authenticateResponse) => {
const login = Str.guid('expensify.cash-');
- const password = Str.guid();
+ const temporaryPassword = Str.guid();
API.CreateLogin({
authToken: authenticateResponse.authToken,
partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
partnerUserID: login,
- partnerUserSecret: password,
+ partnerUserSecret: temporaryPassword,
doNotRetry: true,
})
.then((createLoginResponse) => {
@@ -73,44 +138,62 @@ function signIn(partnerUserID, partnerUserSecret, twoFactorAuthCode = '', exitTo
setSuccessfulSignInData(createLoginResponse, exitTo);
- if (credentials && credentials.login) {
- // If we have an old login for some reason, we should delete it before storing the new details
+ // If we have an old login for some reason, we should delete it before storing the new details
+ if (credentials.login) {
API.DeleteLogin({
partnerUserID: credentials.login,
partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
doNotRetry: true,
})
- .catch(error => Onyx.merge(ONYXKEYS.SESSION, {error: error.message}));
+ .catch(console.debug);
}
- Onyx.merge(ONYXKEYS.CREDENTIALS, {login, password});
+ Onyx.merge(ONYXKEYS.CREDENTIALS, {password});
+ })
+ .catch((error) => {
+ Onyx.merge(ONYXKEYS.SESSION, {error: error.message});
});
})
.catch((error) => {
- console.debug('[SIGNIN] Request error', error);
Onyx.merge(ONYXKEYS.SESSION, {error: error.message});
- })
- .finally(() => Onyx.merge(ONYXKEYS.SESSION, {loading: false}));
+ });
}
/**
- * Clears the Onyx store and redirects user to the sign in page
+ * Puts the github username into Onyx so that it can be used when creating accounts or logins
+ *
+ * @param {String} username
*/
-function signOut() {
- Timing.clearData();
- redirectToSignIn();
- if (!credentials || !credentials.login) {
- return;
- }
+function setGitHubUsername(username) {
+ Onyx.merge(ONYXKEYS.SESSION, {error: ''});
- API.DeleteLogin({
- partnerUserID: credentials.login,
- partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
- partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
- doNotRetry: true,
- })
- .catch(error => Onyx.merge(ONYXKEYS.SESSION, {error: error.message}));
+ API.SetGithubUsername({email: credentials.login, githubUsername: username})
+ .then((response) => {
+ if (response.jsonCode === 200) {
+ Onyx.merge(ONYXKEYS.CREDENTIALS, {githubUsername: username});
+ Onyx.merge(ONYXKEYS.ACCOUNT, {canAccessExpensifyCash: true});
+ return;
+ }
+
+ // This request can fail if an invalid GitHub username was entered
+ Onyx.merge(ONYXKEYS.SESSION, {error: 'Please enter a valid GitHub username'});
+ });
+}
+
+/**
+ * Resend the validation link to the user that is validating their account
+ * this happens in the createAccount() flow
+ */
+function resendValidationLink() {
+ API.ResendValidateCode({email: credentials.login});
+}
+
+/**
+ * Restart the sign in process by clearing everything from Onyx
+ */
+function restartSignin() {
+ Onyx.clear();
}
/**
@@ -121,19 +204,28 @@ function signOut() {
*/
function setPassword(password, validateCode) {
API.SetPassword({
+ email: credentials.login,
password,
validateCode,
})
- .then(() => {
- // @TODO check for 200 response and log the user in properly (like the sign in flow).
- // For now we can just redirect to root
- Onyx.merge(ONYXKEYS.CREDENTIALS, {password});
- redirect('/');
+ .then((response) => {
+ if (response.jsonCode === 200) {
+ Onyx.merge(ONYXKEYS.CREDENTIALS, {password});
+ setSuccessfulSignInData(response, '/home/');
+ return;
+ }
+
+ // This request can fail if the password is not complex enough
+ Onyx.merge(ONYXKEYS.SESSION, {error: response.message});
});
}
export {
+ fetchAccountDetails,
+ setGitHubUsername,
+ setPassword,
signIn,
signOut,
- setPassword,
+ resendValidationLink,
+ restartSignin,
};
diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js
index bfe58939efdd..c4fe0be14a2a 100644
--- a/src/pages/SetPasswordPage.js
+++ b/src/pages/SetPasswordPage.js
@@ -85,7 +85,7 @@ class SetPasswordPage extends Component {
<>
-
+
;
- }
-
- const isLoading = session.loading;
- return (
- <>
-
-
-
-
-
-
-
- Login
- this.setState({login: text})}
- onSubmitEditing={this.submitForm}
- autoCapitalize="none"
- />
-
-
- Password
- this.setState({password: text})}
- onSubmitEditing={this.submitForm}
- />
-
-
- Two Factor Code
- this.setState({twoFactorAuthCode: text})}
- onSubmitEditing={this.submitForm}
- />
-
-
-
- {isLoading ? (
-
- ) : (
- Log In
- )}
-
- {this.props.session && !_.isEmpty(this.props.session.error) && (
-
- {this.props.session.error}
-
- )}
-
-
-
- >
- );
- }
-}
-
-App.propTypes = propTypes;
-App.defaultProps = defaultProps;
-
-export default compose(
- withRouter,
- withOnyx({
- session: {key: ONYXKEYS.SESSION},
- })
-)(App);
diff --git a/src/pages/signin/GithubUsernameForm.js b/src/pages/signin/GithubUsernameForm.js
new file mode 100644
index 000000000000..9ee86ba89b43
--- /dev/null
+++ b/src/pages/signin/GithubUsernameForm.js
@@ -0,0 +1,85 @@
+import React from 'react';
+import {Text, TextInput, View} from 'react-native';
+import styles from '../../styles/styles';
+import SubmitButton from './SubmitButton';
+import {setGitHubUsername} from '../../libs/actions/Session';
+
+class GithubUsernameForm extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.validateAndSubmitForm = this.validateAndSubmitForm.bind(this);
+
+ this.state = {
+ formError: false,
+ githubUsername: '',
+ isLoading: false,
+ };
+ }
+
+ /**
+ * Check that all the form fields are valid, then trigger the submit callback
+ */
+ validateAndSubmitForm() {
+ if (!this.state.githubUsername.trim()) {
+ this.setState({formError: 'Please enter your GitHub username'});
+ return;
+ }
+
+ this.setState({
+ formError: null,
+ isLoading: true,
+ });
+
+ // Save the github username to their account
+ setGitHubUsername(this.state.githubUsername);
+ }
+
+ render() {
+ return (
+ <>
+
+
+ GitHub Username
+ this.setState({githubUsername: text})}
+ onSubmitEditing={this.validateAndSubmitForm}
+ autoCapitalize="none"
+ autoFocus
+ />
+
+
+
+
+ {this.state.formError && (
+
+ {this.state.formError}
+
+ )}
+
+
+
+
+ You're on the waitlist!
+
+
+ Thanks for adding yourself to the waitlist. If you're a developer, just enter your
+ {' '}
+ GitHub username, and we'll grant you instant access to the dev-only beta. Otherwise,
+ {' '}
+ you're all set -- we'll let you know when to check back.
+
+
+ >
+ );
+ }
+}
+
+export default GithubUsernameForm;
diff --git a/src/pages/signin/LoginForm/LoginFormNarrow.js b/src/pages/signin/LoginForm/LoginFormNarrow.js
new file mode 100644
index 000000000000..4d1897170257
--- /dev/null
+++ b/src/pages/signin/LoginForm/LoginFormNarrow.js
@@ -0,0 +1,116 @@
+import React from 'react';
+import {
+ Image, Text, TextInput, View
+} from 'react-native';
+import styles from '../../../styles/styles';
+import themeColors from '../../../styles/themes/default';
+import SubmitButton from '../SubmitButton';
+import openURLInNewTab from '../../../libs/openURLInNewTab';
+import {fetchAccountDetails} from '../../../libs/actions/Session';
+import welcomeScreenshot from '../../../../assets/images/welcome-screenshot.png';
+import CONST from '../../../CONST';
+
+class LoginFormNarrow extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.validateAndSubmitForm = this.validateAndSubmitForm.bind(this);
+
+ this.state = {
+ formError: false,
+ login: '',
+ isLoading: false,
+ };
+ }
+
+ /**
+ * Check that all the form fields are valid, then trigger the submit callback
+ */
+ validateAndSubmitForm() {
+ if (!this.state.login.trim()) {
+ this.setState({formError: 'Please enter an email or phone number'});
+ return;
+ }
+
+ this.setState({
+ formError: null,
+ isLoading: true,
+ });
+
+ // Check if this login has an account associated with it or not
+ fetchAccountDetails(this.state.login);
+ }
+
+ render() {
+ return (
+
+
+ Sign up for the waitlist
+ this.setState({login: text})}
+ onSubmitEditing={this.validateAndSubmitForm}
+ autoCapitalize="none"
+ placeholder="Email or phone"
+ placeholderTextColor={themeColors.textSupporting}
+ />
+
+
+
+
+
+ {this.state.formError && (
+
+ {this.state.formError}
+
+ )}
+
+
+
+
+
+
+
+ With Expensify.cash, chat and payments are the same thing. Launching Summer 2021,
+ {' '}
+ join the waitlist to be first in line!
+
+
+
+
+
+ Attention Open Source Developers:
+
+
+ Enter your Github handle on the next page to skip the wait and join our dev-only beta;
+ {' '}
+ help build tomorrow and
+ {' '}
+ openURLInNewTab(CONST.UPWORK_URL)}
+ >
+ earn cash
+
+ {' '}
+ today!
+
+
+
+ );
+ }
+}
+
+export default LoginFormNarrow;
diff --git a/src/pages/signin/LoginForm/LoginFormWide.js b/src/pages/signin/LoginForm/LoginFormWide.js
new file mode 100644
index 000000000000..81fe78a4e219
--- /dev/null
+++ b/src/pages/signin/LoginForm/LoginFormWide.js
@@ -0,0 +1,108 @@
+import React from 'react';
+import {Text, TextInput, View} from 'react-native';
+import {fetchAccountDetails} from '../../../libs/actions/Session';
+import styles from '../../../styles/styles';
+import SubmitButton from '../SubmitButton';
+import openURLInNewTab from '../../../libs/openURLInNewTab';
+import CONST from '../../../CONST';
+
+class LoginFormWide extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.validateAndSubmitForm = this.validateAndSubmitForm.bind(this);
+
+ this.state = {
+ formError: false,
+ login: '',
+ isLoading: false,
+ };
+ }
+
+ /**
+ * Check that all the form fields are valid, then trigger the submit callback
+ */
+ validateAndSubmitForm() {
+ if (!this.state.login.trim()) {
+ this.setState({formError: 'Please enter an email or phone number'});
+ return;
+ }
+
+ this.setState({
+ formError: null,
+ isLoading: true,
+ });
+
+ // Check if this login has an account associated with it or not
+ fetchAccountDetails(this.state.login);
+ }
+
+ render() {
+ return (
+ <>
+
+
+ Sign up for the waitlist
+ this.setState({login: text})}
+ onSubmitEditing={this.validateAndSubmitForm}
+ autoCapitalize="none"
+ placeholder="Email or phone"
+ autoFocus
+ />
+
+
+
+
+
+ {this.state.formError && (
+
+ {this.state.formError}
+
+ )}
+
+
+
+
+
+ With Expensify.cash, chat and payments are the same thing. Launching Summer 2021,
+ {' '}
+ join the waitlist to be first in line!
+
+
+
+
+
+ Attention Open Source Developers:
+
+
+ Enter your Github handle on the next page to skip the wait and join our dev-only beta;
+ {' '}
+ help build tomorrow and
+ {' '}
+ openURLInNewTab(CONST.UPWORK_URL)}
+ >
+ earn cash
+
+ {' '}
+ today!
+
+
+
+ >
+ );
+ }
+}
+
+export default LoginFormWide;
diff --git a/src/pages/signin/LoginForm/index.js b/src/pages/signin/LoginForm/index.js
new file mode 100644
index 000000000000..0d2869c53aaf
--- /dev/null
+++ b/src/pages/signin/LoginForm/index.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import {Dimensions} from 'react-native';
+import _ from 'underscore';
+import variables from '../../../styles/variables';
+import LoginFormNarrow from './LoginFormNarrow';
+import LoginFormWide from './LoginFormWide';
+
+class LoginForm extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.toggleScreenWidth = _.debounce(this.toggleScreenWidth.bind(this), 1000, true);
+
+ this.state = {
+ isWideScreen: null,
+ };
+ }
+
+ componentDidMount() {
+ Dimensions.addEventListener('change', this.toggleScreenWidth);
+ this.toggleScreenWidth({window: Dimensions.get('window')});
+ }
+
+ componentWillUnmount() {
+ Dimensions.removeEventListener('change', this.toggleScreenWidth);
+ }
+
+ /**
+ * Fired when the windows dimensions changes
+ * @param {Object} changedWindow
+ */
+ toggleScreenWidth({window: changedWindow}) {
+ this.setState({
+ isWideScreen: changedWindow.width > variables.mobileResponsiveWidthBreakpoint,
+ });
+ }
+
+ render() {
+ if (this.state.isWideScreen === null) {
+ return null;
+ }
+
+ return this.state.isWideScreen
+ ?
+ : ;
+ }
+}
+
+export default LoginForm;
diff --git a/src/pages/signin/LoginForm/index.native.js b/src/pages/signin/LoginForm/index.native.js
new file mode 100644
index 000000000000..e0f7e358f4fd
--- /dev/null
+++ b/src/pages/signin/LoginForm/index.native.js
@@ -0,0 +1,3 @@
+import LoginFormNarrow from './LoginFormNarrow';
+
+export default LoginFormNarrow;
diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js
new file mode 100644
index 000000000000..545e088dc8d1
--- /dev/null
+++ b/src/pages/signin/PasswordForm.js
@@ -0,0 +1,124 @@
+import React from 'react';
+import {Text, TextInput, View} from 'react-native';
+import PropTypes from 'prop-types';
+import {withOnyx} from 'react-native-onyx';
+import {withRouter} from '../../libs/Router';
+import styles from '../../styles/styles';
+import SubmitButton from './SubmitButton';
+import themeColors from '../../styles/themes/default';
+import {signIn} from '../../libs/actions/Session';
+import compose from '../../libs/compose';
+import ONYXKEYS from '../../ONYXKEYS';
+
+const propTypes = {
+ // These are from withRouter
+ // eslint-disable-next-line react/forbid-prop-types
+ match: PropTypes.object.isRequired,
+
+ /* Onyx Props */
+
+ // The details about the account that the user is signing in with
+ account: PropTypes.shape({
+ // Whether or not the account already exists
+ accountExists: PropTypes.bool,
+
+ // Whether or not there have been chat reports shared with this user
+ canAccessExpensifyCash: PropTypes.bool,
+
+ // Whether or not two factor authentication is required
+ requiresTwoFactorAuth: PropTypes.bool,
+ }),
+};
+
+const defaultProps = {
+ account: {},
+};
+
+class PasswordForm extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.validateAndSubmitForm = this.validateAndSubmitForm.bind(this);
+
+ this.state = {
+ formError: false,
+ password: '',
+ twoFactorAuthCode: '',
+ isLoading: false,
+ };
+ }
+
+ /**
+ * Check that all the form fields are valid, then trigger the submit callback
+ */
+ validateAndSubmitForm() {
+ if (!this.state.password.trim()
+ || (this.props.account.requiresTwoFactorAuth && !this.state.twoFactorAuthCode.trim())
+ ) {
+ this.setState({formError: 'Please fill out all fields'});
+ return;
+ }
+
+ this.setState({
+ formError: null,
+ isLoading: true,
+ });
+
+ signIn(this.state.password, this.props.match.params.exitTo, this.state.twoFactorAuthCode);
+ }
+
+ render() {
+ return (
+
+
+ Password
+ this.setState({password: text})}
+ onSubmitEditing={this.validateAndSubmitForm}
+ autoFocus
+ />
+
+ {this.props.account.requiresTwoFactorAuth && (
+
+ Two Factor Code
+ this.setState({twoFactorAuthCode: text})}
+ onSubmitEditing={this.validateAndSubmitForm}
+ />
+
+ )}
+
+
+
+ {this.state.formError && (
+
+ {this.state.formError}
+
+ )}
+
+ );
+ }
+}
+
+PasswordForm.propTypes = propTypes;
+PasswordForm.defaultProps = defaultProps;
+
+export default compose(
+ withRouter,
+ withOnyx({
+ account: {key: ONYXKEYS.ACCOUNT},
+ }),
+)(PasswordForm);
diff --git a/src/pages/signin/ResendValidationForm.js b/src/pages/signin/ResendValidationForm.js
new file mode 100644
index 000000000000..765e6eeaaa11
--- /dev/null
+++ b/src/pages/signin/ResendValidationForm.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import {Text, View} from 'react-native';
+import styles from '../../styles/styles';
+import SubmitButton from './SubmitButton';
+import {resendValidationLink} from '../../libs/actions/Session';
+
+class ResendValidationForm extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.validateAndSubmitForm = this.validateAndSubmitForm.bind(this);
+
+ this.state = {
+ formSuccess: '',
+ isLoading: false,
+ };
+ }
+
+ /**
+ * Check that all the form fields are valid, then trigger the submit callback
+ */
+ validateAndSubmitForm() {
+ this.setState({
+ formSuccess: 'Link has been re-sent',
+ });
+
+ resendValidationLink();
+
+ setTimeout(() => {
+ this.setState({formSuccess: ''});
+ }, 5000);
+ }
+
+ render() {
+ return (
+
+
+
+ Please validate your account by clicking on the link we just sent you.
+
+
+
+
+
+
+ {this.state.formSuccess && (
+
+ {this.state.formSuccess}
+
+ )}
+
+ );
+ }
+}
+
+export default ResendValidationForm;
diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js
new file mode 100644
index 000000000000..41eb0975712c
--- /dev/null
+++ b/src/pages/signin/SignInPage.js
@@ -0,0 +1,147 @@
+import React, {Component} from 'react';
+import {
+ SafeAreaView, Text, View
+} from 'react-native';
+import PropTypes from 'prop-types';
+import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
+import compose from '../../libs/compose';
+import {Redirect} from '../../libs/Router';
+import ROUTES from '../../ROUTES';
+import ONYXKEYS from '../../ONYXKEYS';
+import styles from '../../styles/styles';
+import CustomStatusBar from '../../components/CustomStatusBar';
+import updateUnread from '../../libs/UnreadIndicatorUpdater/updateUnread/index';
+import SignInPageLayout from './SignInPageLayout';
+import LoginForm from './LoginForm';
+import GithubUsernameForm from './GithubUsernameForm';
+import PasswordForm from './PasswordForm';
+import ResendValidationForm from './ResendValidationForm';
+
+const propTypes = {
+ /* Onyx Props */
+
+ // The details about the account that the user is signing in with
+ account: PropTypes.shape({
+ // Whether or not the account already exists
+ accountExists: PropTypes.bool,
+
+ // Whether or not there have been chat reports shared with this user
+ canAccessExpensifyCash: PropTypes.bool,
+ }),
+
+ // The credentials of the person signing in
+ credentials: PropTypes.shape({
+ login: PropTypes.string,
+ githubUsername: PropTypes.string,
+ password: PropTypes.string,
+ twoFactorAuthCode: PropTypes.string,
+ }),
+
+ // The session of the logged in person
+ session: PropTypes.shape({
+ // Error to display when there is a session error returned
+ authToken: PropTypes.string,
+
+ // Error to display when there is a session error returned
+ error: PropTypes.string,
+ }),
+};
+
+const defaultProps = {
+ account: {},
+ session: {},
+ credentials: {},
+};
+
+class SignInPage extends Component {
+ componentDidMount() {
+ // Always reset the unread counter to zero on this page
+ updateUnread(0);
+ }
+
+ render() {
+ // If we end up on the sign in page and have an authToken then
+ // we are signed in and should be brought back to the site root
+ if (this.props.session.authToken) {
+ return ;
+ }
+
+ // Show the login form if
+ // - A login has not been entered yet
+ const showLoginForm = !this.props.credentials.login;
+
+ // Show the GitHub username form if
+ // - A login has been entered
+ // - AND they do not have access to this app yet
+ // - AND the user hasn't entered a GitHub username yet
+ // - AND a password hasn't been entered yet
+ const showGithubUsernameForm = this.props.credentials.login
+ && !this.props.account.canAccessExpensifyCash
+ && !this.props.credentials.githubUsername
+ && !this.props.credentials.password;
+
+ // Show the password form if
+ // - A login has been entered
+ // - AND a GitHub username has been entered OR they already have access to expensify cash
+ // - AND an account exists already for this login
+ // - AND a password hasn't been entered yet
+ const showPasswordForm = this.props.credentials.login
+ && (
+ this.props.credentials.githubUsername
+ || this.props.account.canAccessExpensifyCash
+ )
+ && this.props.account.accountExists
+ && !this.props.credentials.password;
+
+ // Show the resend validation link form if
+ // - A login has been entered
+ // - AND a GitHub username has been entered OR they already have access to this app
+ // - AND an account did not exist for that login
+ const showResendValidationLinkForm = this.props.credentials.login
+ && (
+ this.props.credentials.githubUsername
+ || this.props.account.canAccessExpensifyCash
+ )
+ && !this.props.account.accountExists;
+
+ return (
+ <>
+
+
+
+ {showLoginForm && }
+
+ {showGithubUsernameForm && }
+
+ {showPasswordForm && }
+
+ {showResendValidationLinkForm && }
+
+ {/* Because of the custom layout of the login form, session errors are displayed differently */}
+ {!showLoginForm && (
+
+ {this.props.session && !_.isEmpty(this.props.session.error) && (
+
+ {this.props.session.error}
+
+ )}
+
+ )}
+
+
+ >
+ );
+ }
+}
+
+SignInPage.propTypes = propTypes;
+SignInPage.defaultProps = defaultProps;
+
+export default compose(
+ withOnyx({
+ account: {key: ONYXKEYS.ACCOUNT},
+ credentials: {key: ONYXKEYS.CREDENTIALS},
+ session: {key: ONYXKEYS.SESSION},
+ })
+)(SignInPage);
diff --git a/src/pages/signin/SignInPageLayout/SignInPageLayoutNarrow.js b/src/pages/signin/SignInPageLayout/SignInPageLayoutNarrow.js
new file mode 100644
index 000000000000..897350120580
--- /dev/null
+++ b/src/pages/signin/SignInPageLayout/SignInPageLayoutNarrow.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import {
+ Image, ScrollView, Text, View
+} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../../../styles/styles';
+import logo from '../../../../assets/images/expensify-logo-round.png';
+
+const propTypes = {
+ // The children to show inside the layout
+ children: PropTypes.node.isRequired,
+};
+
+const SignInPageLayoutNarrow = ({children}) => (
+
+
+
+
+
+
+
+
+
+ Expensify.cash
+
+
+
+ {children}
+
+
+
+);
+
+SignInPageLayoutNarrow.propTypes = propTypes;
+SignInPageLayoutNarrow.displayName = 'SignInPageLayoutNarrow';
+
+
+export default SignInPageLayoutNarrow;
diff --git a/src/pages/signin/SignInPageLayout/SignInPageLayoutWide.js b/src/pages/signin/SignInPageLayout/SignInPageLayoutWide.js
new file mode 100644
index 000000000000..d6c1ceaa8096
--- /dev/null
+++ b/src/pages/signin/SignInPageLayout/SignInPageLayoutWide.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import {
+ Image, Text, View
+} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../../../styles/styles';
+import logo from '../../../../assets/images/expensify-logo-round.png';
+import welcomeScreenshot from '../../../../assets/images/welcome-screenshot-wide.png';
+
+const propTypes = {
+ // The children to show inside the layout
+ children: PropTypes.node.isRequired,
+};
+
+const SignInPageLayoutWide = ({children}) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Expensify.cash
+
+
+
+ {children}
+
+
+
+);
+
+SignInPageLayoutWide.propTypes = propTypes;
+SignInPageLayoutWide.displayName = 'SignInPageLayoutWide';
+
+
+export default SignInPageLayoutWide;
diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.js
new file mode 100644
index 000000000000..1e19b2d35921
--- /dev/null
+++ b/src/pages/signin/SignInPageLayout/index.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import {Dimensions} from 'react-native';
+import _ from 'underscore';
+import variables from '../../../styles/variables';
+import SignInPageLayoutNarrow from './SignInPageLayoutNarrow';
+import SignInPageLayoutWide from './SignInPageLayoutWide';
+
+class LoginForm extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.toggleScreenWidth = _.debounce(this.toggleScreenWidth.bind(this), 1000, true);
+
+ this.state = {
+ isWideScreen: null,
+ };
+ }
+
+ componentDidMount() {
+ Dimensions.addEventListener('change', this.toggleScreenWidth);
+ this.toggleScreenWidth({window: Dimensions.get('window')});
+ }
+
+ componentWillUnmount() {
+ Dimensions.removeEventListener('change', this.toggleScreenWidth);
+ }
+
+ /**
+ * Fired when the windows dimensions changes
+ * @param {Object} changedWindow
+ */
+ toggleScreenWidth({window: changedWindow}) {
+ this.setState({
+ isWideScreen: changedWindow.width > variables.mobileResponsiveWidthBreakpoint,
+ });
+ }
+
+ render() {
+ if (this.state.isWideScreen === null) {
+ return null;
+ }
+
+ return this.state.isWideScreen
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ ?
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ : ;
+ }
+}
+
+export default LoginForm;
diff --git a/src/pages/signin/SignInPageLayout/index.native.js b/src/pages/signin/SignInPageLayout/index.native.js
new file mode 100644
index 000000000000..775b3cd9fc53
--- /dev/null
+++ b/src/pages/signin/SignInPageLayout/index.native.js
@@ -0,0 +1,3 @@
+import SignInPageLayoutNarrow from './SignInPageLayoutNarrow';
+
+export default SignInPageLayoutNarrow;
diff --git a/src/pages/signin/SubmitButton.js b/src/pages/signin/SubmitButton.js
new file mode 100644
index 000000000000..12215cf873c8
--- /dev/null
+++ b/src/pages/signin/SubmitButton.js
@@ -0,0 +1,82 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ ActivityIndicator, Text, TouchableOpacity, View
+} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import styles from '../../styles/styles';
+import themeColors from '../../styles/themes/default';
+import ONYXKEYS from '../../ONYXKEYS';
+import {restartSignin} from '../../libs/actions/Session';
+
+const propTypes = {
+ // The text for the button label
+ text: PropTypes.string.isRequired,
+
+ // Indicates whether the button should be disabled and in the loading state
+ isLoading: PropTypes.bool.isRequired,
+
+ // A function that is called when the button is clicked on
+ onClick: PropTypes.func.isRequired,
+
+ // Whether or not to show the restart sign in button
+ showRestartButton: PropTypes.bool,
+
+ /* Onyx Props */
+
+ // The session of the logged in person
+ session: PropTypes.shape({
+ // Error to display when there is a session error returned
+ error: PropTypes.string,
+ }),
+};
+const defaultProps = {
+ showRestartButton: true,
+ session: {},
+};
+
+const SubmitButton = (props) => {
+ // When there is an error in the session (a sign on error) then the button should be
+ // enabled so the form can be submitted again
+ const isLoading = props.isLoading && !props.session.error;
+ return (
+ <>
+
+ {isLoading ? (
+
+ ) : (
+
+ {props.text}
+
+ )}
+
+
+ {props.showRestartButton && (
+
+
+
+ Change Expensify login
+
+
+
+ )}
+ >
+ );
+};
+
+SubmitButton.propTypes = propTypes;
+SubmitButton.defaultProps = defaultProps;
+SubmitButton.displayName = 'SubmitButton';
+
+export default withOnyx({
+ session: {key: ONYXKEYS.SESSION},
+})(SubmitButton);
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 9bdce32db1fd..e1335dd3f627 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -37,17 +37,24 @@ const styles = {
marginLeft: 8,
},
- mt2: {
- marginTop: 20,
- },
-
mt1: {
marginTop: 4,
},
-
+ mt2: {
+ marginTop: 8,
+ },
mt3: {
marginTop: 12,
},
+ mt4: {
+ marginTop: 16,
+ },
+ mt5: {
+ marginTop: 20,
+ },
+ mt6: {
+ marginTop: 24,
+ },
mb1: {
marginBottom: 4,
@@ -61,7 +68,12 @@ const styles = {
mb4: {
marginBottom: 16,
},
-
+ mb5: {
+ marginBottom: 20,
+ },
+ mb6: {
+ marginBottom: 24,
+ },
mbn5: {
marginBottom: -5,
},
@@ -83,6 +95,9 @@ const styles = {
width: '100%',
height: '100%',
},
+ width50p: {
+ width: '50%',
+ },
flex0: {
flex: 0,
@@ -160,6 +175,24 @@ const styles = {
height: '100%',
},
+ link: {
+ color: themeColors.link,
+ textDecorationColor: themeColors.link,
+ },
+
+ h1: {
+ color: themeColors.heading,
+ fontFamily: fontFamily.GTA_BOLD,
+ fontSize: variables.fontSizeh1,
+ fontWeight: fontWeightBold,
+ },
+
+ h3: {
+ fontFamily: fontFamily.GTA_BOLD,
+ fontSize: variables.fontSizeNormal,
+ fontWeight: fontWeightBold,
+ },
+
h4: {
fontFamily: fontFamily.GTA_BOLD,
fontSize: variables.fontSizeLabel,
@@ -168,6 +201,7 @@ const styles = {
textP: {
color: themeColors.text,
+ fontFamily: fontFamily.GTA,
fontSize: variables.fontSizeNormal,
lineHeight: 20,
},
@@ -342,15 +376,21 @@ const styles = {
textInputNoOutline: addOutlineWidth({}, 0),
formLabel: {
- color: themeColors.heading,
+ color: themeColors.text,
fontSize: variables.fontSizeLabel,
- fontWeight: '600',
lineHeight: 18,
- marginBottom: 4,
+ marginBottom: 8,
},
formError: {
- color: themeColors.errorText,
+ color: themeColors.textError,
+ fontSize: variables.fontSizeLabel,
+ lineHeight: 18,
+ marginBottom: 4,
+ },
+
+ formSuccess: {
+ color: themeColors.textSuccess,
fontSize: variables.fontSizeLabel,
lineHeight: 18,
marginBottom: 4,
@@ -358,21 +398,37 @@ const styles = {
signInPage: {
backgroundColor: themeColors.sidebar,
- height: '100%',
padding: 20,
+ minHeight: '100%',
},
signInPageLogo: {
+ height: variables.componentSizeLarge,
+ marginBottom: 24,
+ },
+
+ signInPageLogoNative: {
alignItems: 'center',
- height: variables.componentSizeNormal,
+ height: variables.componentSizeLarge,
justifyContent: 'center',
width: '100%',
- marginBottom: 24,
+ marginBottom: 20,
+ marginTop: 20,
},
signinLogo: {
- height: variables.componentSizeNormal,
- width: variables.componentSizeNormal,
+ height: variables.componentSizeLarge,
+ width: variables.componentSizeLarge,
+ },
+
+ signinWelcomeScreenshot: {
+ height: 354,
+ width: 295,
+ },
+
+ signinWelcomeScreenshotWide: {
+ height: 592,
+ width: 295,
},
genericView: {
@@ -381,9 +437,22 @@ const styles = {
},
signInPageInner: {
+ paddingTop: 40,
marginLeft: 'auto',
marginRight: 'auto',
- maxWidth: 325,
+ maxWidth: 800,
+ width: '100%',
+ },
+
+ signInPageInnerNative: {
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ maxWidth: 295,
+ width: '100%',
+ },
+
+ loginFormContainer: {
+ maxWidth: 295,
width: '100%',
},
@@ -1006,10 +1075,7 @@ const webViewStyles = {
fontWeight: fontWeightBold,
},
- a: {
- color: themeColors.link,
- textDecorationColor: themeColors.link,
- },
+ a: styles.link,
blockquote: {
borderLeftColor: themeColors.border,
diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js
index 242e2970edbc..912114ea9044 100644
--- a/src/styles/themes/default.js
+++ b/src/styles/themes/default.js
@@ -5,20 +5,21 @@ export default {
link: colors.blue,
componentBG: colors.white,
appBG: colors.white,
+ heading: colors.charcoal,
sidebar: colors.gray1,
border: colors.gray2,
borderFocus: colors.blue,
icon: colors.gray3,
textSupporting: colors.gray4,
text: colors.gray5,
- heading: colors.charcoal,
+ textError: colors.red,
+ textSuccess: colors.green,
textBackground: colors.gray1,
textReversed: colors.white,
textMutedReversed: colors.gray3,
buttonSuccessBG: colors.green,
online: colors.green,
offline: colors.gray3,
- errorText: colors.red,
sidebarButtonBG: 'rgba(198, 201, 202, 0.25)',
modalBackdrop: colors.gray3,
pillBG: colors.gray2,
diff --git a/src/styles/variables.js b/src/styles/variables.js
index 4332c688549f..f29bdf4a0523 100644
--- a/src/styles/variables.js
+++ b/src/styles/variables.js
@@ -1,11 +1,14 @@
export default {
contentHeaderHeight: 65,
- componentSizeNormal: 40,
componentSizeSmall: 28,
+ componentSizeNormal: 40,
+ componentSizeLarge: 50,
componentBorderRadius: 8,
fontSizeSmall: 11,
fontSizeLabel: 13,
fontSizeNormal: 15,
+ fontSizeLarge: 17,
+ fontSizeh1: 19,
mobileResponsiveWidthBreakpoint: 1000,
safeInsertPercentage: 0.7,
};
diff --git a/tests/e2e/loginTest.e2e.js b/tests/e2e/loginTest.e2e.js
index 154f32fe9367..f69ab4b5399b 100644
--- a/tests/e2e/loginTest.e2e.js
+++ b/tests/e2e/loginTest.e2e.js
@@ -5,5 +5,5 @@ jest.setTimeout(120000 * 10);
describe('Test login page', () => {
beforeEach(() => device.reloadReactNative());
- it('should have a Log In button visible', () => expect(element(by.text('Log In'))).toBeVisible());
+ it('should have a Log In button visible', () => expect(element(by.text('Continue'))).toBeVisible());
});
diff --git a/web/favicon-unread.png b/web/favicon-unread.png
index c5de4c8308ec..b1057d32c600 100644
Binary files a/web/favicon-unread.png and b/web/favicon-unread.png differ
diff --git a/web/favicon.png b/web/favicon.png
index fc79905bf7fb..0e150943264e 100644
Binary files a/web/favicon.png and b/web/favicon.png differ