diff --git a/.github/workflows/deployBlocker.yml b/.github/workflows/deployBlocker.yml
index 517cd606b3d9..08eaf2df83a6 100644
--- a/.github/workflows/deployBlocker.yml
+++ b/.github/workflows/deployBlocker.yml
@@ -24,14 +24,14 @@ jobs:
run: |
echo "DEPLOY_BLOCKER_URL=${{ github.event.issue.html_url }}" >> $GITHUB_ENV
echo "DEPLOY_BLOCKER_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
- echo "DEPLOY_BLOCKER_TITLE=$(sed -e "s/'/'\\\\''/g; s/\`/\\\\\`/g; 1s/^/'/; \$s/\$/'/" <<< ${{ github.event.issue.title }})" >> $GITHUB_ENV
+ echo "DEPLOY_BLOCKER_TITLE=$(sed -e "s/'/'\\\\''/g; s/\`/\\\\\`/g; 1s/^/'/; \$s/\$/'/" <<< "'${{ github.event.issue.title }}'")" >> $GITHUB_ENV
- name: Get URL, title, & number of new deploy blocker (pull request)
if: ${{ github.event_name == 'pull_request' }}
run: |
echo "DEPLOY_BLOCKER_URL=${{ github.event.pull_request.html_url }}" >> $GITHUB_ENV
echo "DEPLOY_BLOCKER_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
- echo "DEPLOY_BLOCKER_TITLE=$(sed -e "s/'/'\\\\''/g; s/\`/\\\\\`/g; 1s/^/'/; \$s/\$/'/" <<< ${{ github.event.pull_request.title }})" >> $GITHUB_ENV
+ echo "DEPLOY_BLOCKER_TITLE=$(sed -e "s/'/'\\\\''/g; s/\`/\\\\\`/g; 1s/^/'/; \$s/\$/'/" <<< "'${{ github.event.pull_request.title }}'")" >> $GITHUB_ENV
- name: Update StagingDeployCash with new deploy blocker
uses: Expensify/App/.github/actions/createOrUpdateStagingDeploy@main
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 76c3917b8c83..7e78114c801a 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -208,10 +208,8 @@ jobs:
APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }}
APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }}
- # TODO: uncomment when we want to release iOS to production
- name: Run Fastlane for App Store release
- # if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'true' }}
- if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'true' && 'false' == 'true' }}
+ if: ${{ env.SHOULD_DEPLOY_PRODUCTION == 'true' }}
run: bundle exec fastlane ios production
env:
VERSION: ${{ env.NEW_IOS_VERSION }}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 31d74f37188a..8f2666cc7949 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,7 +27,7 @@ If you are hired for an Upwork job and have any job-specific questions, please a
If you've found a vulnerability, please email security@expensify.com with the subject `Vulnerability Report` instead of creating an issue.
## Payment for Contributions
-We hire and pay external contributors via Upwork.com. If you'd like to be paid for contributing, please create an Upwork account and apply for a job in the [Upwork issue list](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2). Payment for your contributions will be made no less than 7 days after the pull request is merged to allow for regression testing. We hire one contributor for each Upwork job. New contributors are limited to working on one job at a time, however experienced contributors may work on numerous jobs simultaneously. If you have not received payment after 8 days of the PR being deployed to production, please email contributors@expensify.com referencing the GH issue and your GH handle.
+We hire and pay external contributors via Upwork.com. If you'd like to be paid for contributing, please create an Upwork account and apply for a job in the [Upwork issue list](https://www.upwork.com/ab/jobs/search/?q=Expensify%20React%20Native&sort=recency&user_location_match=2). If you think your compensation should be increased for a specific job, you can request a reevaluation by commenting in the Github issue where the Upwork job was posted. Payment for your contributions will be made no less than 7 days after the pull request is merged to allow for regression testing. We hire one contributor for each Upwork job. New contributors are limited to working on one job at a time, however experienced contributors may work on numerous jobs simultaneously. If you have not received payment after 8 days of the PR being deployed to production, please email contributors@expensify.com referencing the GH issue and your GH handle.
## Finding Jobs
There are two ways you can find a job that you can contribute to:
@@ -87,7 +87,7 @@ In this scenario, it’s possible that you found a bug or enhancement that we ha
10. An Expensify engineer will be assigned to your pull request automatically to review.
11. Provide daily updates until reaching completion of your PR.
-#### Submit your pull request for final request
+#### Submit your pull request for final review
12. When you are ready to submit your pull request for final review, make sure the following checks pass:
1. CLA - You must sign our [Contributor License Agreement](https://github.com/Expensify/App/blob/main/CLA.md) by following the CLA bot instructions that will be posted on your PR
2. Tests - All tests must pass before a merge of a pull request
diff --git a/android/app/build.gradle b/android/app/build.gradle
index d19255aebb4f..59904cfa80a8 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -150,8 +150,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001008500
- versionName "1.0.85-0"
+ versionCode 1001008505
+ versionName "1.0.85-5"
}
splits {
abi {
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 1da3decd94d2..f65610ba14aa 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -30,7 +30,7 @@
CFBundleVersion
- 1.0.85.0
+ 1.0.85.5
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index f9feb824287a..1f1945216456 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.0.85.0
+ 1.0.85.5
diff --git a/package-lock.json b/package-lock.json
index e5efdf8502a2..fcd4f7cef9f6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.0.85-0",
+ "version": "1.0.85-5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -19849,9 +19849,9 @@
"dev": true
},
"damerau-levenshtein": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
- "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz",
+ "integrity": "sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==",
"dev": true
},
"dashdash": {
@@ -21619,9 +21619,9 @@
}
},
"eslint-config-expensify": {
- "version": "2.0.15",
- "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.15.tgz",
- "integrity": "sha512-wFlWvG0V9mu+PLQeUF3liSnddwC+GNHisFACgK0ATwbJP7pAtAKH4AnL3K/aABDEDw83xAnWQvnlT5EnaJ8rAA==",
+ "version": "2.0.16",
+ "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.16.tgz",
+ "integrity": "sha512-tN7cns6nVREB4EhJnt+KoJYWafdSKfCPq/9qdoZfgUjxsxrZLnjLHjjW1iiXfA/9WRXDUu3g2qFIGDPnP/IRIw==",
"dev": true,
"requires": {
"@lwc/eslint-plugin-lwc": "^0.11.0",
@@ -21641,18 +21641,18 @@
"dev": true
},
"ansi-escapes": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
- "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
"requires": {
- "type-fest": "^0.11.0"
+ "type-fest": "^0.21.3"
},
"dependencies": {
"type-fest": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
- "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true
}
}
@@ -21897,9 +21897,9 @@
}
},
"chalk": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
- "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@@ -22031,9 +22031,9 @@
}
},
"rxjs": {
- "version": "6.6.6",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz",
- "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==",
+ "version": "6.6.7",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
+ "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
@@ -22104,29 +22104,33 @@
}
},
"eslint-import-resolver-node": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz",
- "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==",
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.5.tgz",
+ "integrity": "sha512-XMoPKjSpXbkeJ7ZZ9icLnJMTY5Mc1kZbCakHquaFsXPpyWOwK0TK6CODO+0ca54UoM9LKOxyUNnoVZRl8TeaAg==",
"dev": true,
"requires": {
- "debug": "^2.6.9",
- "resolve": "^1.13.1"
+ "debug": "^3.2.7",
+ "resolve": "^1.20.0"
},
"dependencies": {
"debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"requires": {
- "ms": "2.0.0"
+ "ms": "^2.1.1"
}
},
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
+ "resolve": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
+ "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ }
}
}
},
@@ -22218,22 +22222,22 @@
}
},
"eslint-module-utils": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz",
- "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==",
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz",
+ "integrity": "sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q==",
"dev": true,
"requires": {
- "debug": "^2.6.9",
+ "debug": "^3.2.7",
"pkg-dir": "^2.0.0"
},
"dependencies": {
"debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"requires": {
- "ms": "2.0.0"
+ "ms": "^2.1.1"
}
},
"find-up": {
@@ -22255,12 +22259,6 @@
"path-exists": "^3.0.0"
}
},
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- },
"p-limit": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
@@ -36094,8 +36092,8 @@
}
},
"react-native-onyx": {
- "version": "git+https://github.com/Expensify/react-native-onyx.git#073760394ed8d83304beabf227f2c2fb75b3f04f",
- "from": "git+https://github.com/Expensify/react-native-onyx.git#073760394ed8d83304beabf227f2c2fb75b3f04f",
+ "version": "git+https://github.com/Expensify/react-native-onyx.git#2908a47dee13d99ce756bebb75740ed2f27c2d2e",
+ "from": "git+https://github.com/Expensify/react-native-onyx.git#2908a47dee13d99ce756bebb75740ed2f27c2d2e",
"requires": {
"ascii-table": "0.0.9",
"expensify-common": "git+https://github.com/Expensify/expensify-common.git#2e5cff552cf132da90a3fb9756e6b4fb6ae7b40c",
diff --git a/package.json b/package.json
index fe177619c996..7194145cc15f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.0.85-0",
+ "version": "1.0.85-5",
"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.",
@@ -84,7 +84,7 @@
"react-native-image-picker": "^4.0.3",
"react-native-keyboard-spacer": "^0.4.1",
"react-native-modal": "^11.10.0",
- "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#073760394ed8d83304beabf227f2c2fb75b3f04f",
+ "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#2908a47dee13d99ce756bebb75740ed2f27c2d2e",
"react-native-pdf": "^6.2.2",
"react-native-permissions": "^3.0.1",
"react-native-picker-select": "8.0.4",
@@ -144,7 +144,7 @@
"electron-notarize": "^1.0.0",
"electron-reloader": "^1.2.0",
"eslint": "^7.6.0",
- "eslint-config-expensify": "^2.0.15",
+ "eslint-config-expensify": "^2.0.16",
"eslint-loader": "^4.0.2",
"eslint-plugin-detox": "^1.0.0",
"eslint-plugin-jest": "^24.1.0",
diff --git a/src/App.js b/src/App.js
index 269af1f82456..c24c06fc082e 100644
--- a/src/App.js
+++ b/src/App.js
@@ -7,6 +7,7 @@ import ErrorBoundary from './components/ErrorBoundary';
import Expensify from './Expensify';
import {LocaleContextProvider} from './components/withLocalize';
import OnyxProvider from './components/OnyxProvider';
+import HTMLEngineProvider from './components/HTMLEngineProvider';
import ComposeProviders from './components/ComposeProviders';
LogBox.ignoreLogs([
@@ -25,6 +26,7 @@ const App = () => (
OnyxProvider,
SafeAreaProvider,
LocaleContextProvider,
+ HTMLEngineProvider,
]}
>
diff --git a/src/components/Avatar.js b/src/components/Avatar.js
index c3e64dbac2b2..aa56b6262d28 100644
--- a/src/components/Avatar.js
+++ b/src/components/Avatar.js
@@ -12,7 +12,7 @@ const propTypes = {
imageStyles: PropTypes.arrayOf(PropTypes.object),
/** Extra styles to pass to View wrapper */
- containerStyles: PropTypes.arrayOf(PropTypes.object),
+ containerStyles: PropTypes.oneOfType([PropTypes.object, PropTypes.arrayOf(PropTypes.object)]),
/** Set the size of Avatar */
size: PropTypes.oneOf(['default', 'small']),
diff --git a/src/components/CurrentWalletBalance.js b/src/components/CurrentWalletBalance.js
index b58fe6108dd1..f24b4cff8942 100644
--- a/src/components/CurrentWalletBalance.js
+++ b/src/components/CurrentWalletBalance.js
@@ -34,13 +34,15 @@ const CurrentWalletBalance = (props) => {
);
}
- const formattedBalance = Number(props.userWallet.availableBalance).toFixed(2);
-
+ const formattedBalance = props.numberFormat(
+ props.userWallet.availableBalance,
+ {style: 'currency', currency: 'USD'},
+ );
return (
- {`$${formattedBalance}`}
+ {`${formattedBalance}`}
);
};
diff --git a/src/components/ExpensiTextInput/BaseExpensiTextInput.js b/src/components/ExpensiTextInput/BaseExpensiTextInput.js
index 7194a35fe27c..6c911d4fde85 100644
--- a/src/components/ExpensiTextInput/BaseExpensiTextInput.js
+++ b/src/components/ExpensiTextInput/BaseExpensiTextInput.js
@@ -2,6 +2,7 @@ import React, {Component} from 'react';
import {
Animated, TextInput, View, TouchableWithoutFeedback,
} from 'react-native';
+import Str from 'expensify-common/lib/str';
import ExpensiTextInputLabel from './ExpensiTextInputLabel';
import {propTypes, defaultProps} from './propTypes';
import themeColors from '../../styles/themes/default';
@@ -19,7 +20,7 @@ class BaseExpensiTextInput extends Component {
constructor(props) {
super(props);
- const hasValue = props.value.length > 0;
+ const hasValue = props.value && props.value.length > 0;
this.state = {
isFocused: false,
@@ -30,27 +31,59 @@ class BaseExpensiTextInput extends Component {
};
this.input = null;
+ this.value = hasValue ? props.value : '';
+ this.isLabelActive = false;
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
+ this.setValue = this.setValue.bind(this);
+ }
+
+ componentDidMount() {
+ // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514
+ if (this.props.autoFocus && this.input) {
+ this.input.focus();
+ }
}
onFocus() {
if (this.props.onFocus) { this.props.onFocus(); }
this.setState({isFocused: true});
- if (this.props.value.length === 0) {
+ this.activateLabel();
+ }
+
+ onBlur() {
+ if (this.props.onBlur) { this.props.onBlur(); }
+ this.setState({isFocused: false});
+ this.deactivateLabel();
+ }
+
+ /**
+ * Set Value & activateLabel
+ *
+ * @param {String} value
+ * @memberof BaseExpensiTextInput
+ */
+ setValue(value) {
+ this.value = value;
+ Str.result(this.props.onChangeText, value);
+ this.activateLabel();
+ }
+
+ activateLabel() {
+ if (this.value.length >= 0 && !this.isLabelActive) {
this.animateLabel(
ACTIVE_LABEL_TRANSLATE_Y,
ACTIVE_LABEL_TRANSLATE_X(this.props.translateX),
ACTIVE_LABEL_SCALE,
);
+ this.isLabelActive = true;
}
}
- onBlur() {
- if (this.props.onBlur) { this.props.onBlur(); }
- this.setState({isFocused: false});
- if (this.props.value.length === 0) {
+ deactivateLabel() {
+ if (this.value.length === 0) {
this.animateLabel(INACTIVE_LABEL_TRANSLATE_Y, INACTIVE_LABEL_TRANSLATE_X, INACTIVE_LABEL_SCALE);
+ this.isLabelActive = false;
}
}
@@ -84,13 +117,14 @@ class BaseExpensiTextInput extends Component {
inputStyle,
ignoreLabelTranslateX,
innerRef,
+ autoFocus,
...inputProps
} = this.props;
const hasLabel = Boolean(label.length);
return (
- this.input.focus()}>
+ this.input.focus()} focusable={false}>
diff --git a/src/components/RenderHTML/BaseRenderHTML.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
similarity index 83%
rename from src/components/RenderHTML/BaseRenderHTML.js
rename to src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
index 42058198ced7..4259d3e4de02 100755
--- a/src/components/RenderHTML/BaseRenderHTML.js
+++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
@@ -1,8 +1,10 @@
/* eslint-disable react/prop-types */
import _ from 'underscore';
-import React from 'react';
-import {useWindowDimensions, TouchableOpacity} from 'react-native';
-import HTML, {
+import React, {useMemo} from 'react';
+import {TouchableOpacity} from 'react-native';
+import {
+ TRenderEngineProvider,
+ RenderHTMLConfigProvider,
defaultHTMLElementModels,
TNodeChildrenRenderer,
splitBoxModelStyle,
@@ -18,20 +20,16 @@ import ThumbnailImage from '../ThumbnailImage';
import variables from '../../styles/variables';
import themeColors from '../../styles/themes/default';
import Text from '../Text';
-import {
- propTypes as renderHTMLPropTypes,
- defaultProps as renderHTMLDefaultProps,
-} from './renderHTMLPropTypes';
const propTypes = {
/** Whether text elements should be selectable */
textSelectable: PropTypes.bool,
- ...renderHTMLPropTypes,
+ children: PropTypes.node,
};
const defaultProps = {
textSelectable: false,
- ...renderHTMLDefaultProps,
+ children: null,
};
const MAX_IMG_DIMENSIONS = 512;
@@ -221,31 +219,40 @@ const renderersProps = {
},
};
-const BaseRenderHTML = ({html, debug, textSelectable}) => {
- const {width} = useWindowDimensions();
- const containerWidth = width * 0.8;
+const defaultViewProps = {style: {alignItems: 'flex-start'}};
+
+// We are using the explicit composite architecture for performance gains.
+// Configuration for RenderHTML is handled in a top-level component providing
+// context to RenderHTMLSource components. See https://git.io/JRcZb
+// Beware that each prop should be referentialy stable between renders to avoid
+// costly invalidations and commits.
+const BaseHTMLEngineProvider = ({children, textSelectable}) => {
+ // We need to memoize this prop to make it referentially stable.
+ const defaultTextProps = useMemo(() => ({selectable: textSelectable}), [textSelectable]);
return (
-
+ >
+
+ {children}
+
+
);
};
-BaseRenderHTML.displayName = 'BaseRenderHTML';
-BaseRenderHTML.propTypes = propTypes;
-BaseRenderHTML.defaultProps = defaultProps;
+BaseHTMLEngineProvider.displayName = 'BaseHTMLEngineProvider';
+BaseHTMLEngineProvider.propTypes = propTypes;
+BaseHTMLEngineProvider.defaultProps = defaultProps;
-export default BaseRenderHTML;
+export default BaseHTMLEngineProvider;
diff --git a/src/components/RenderHTML/renderHTMLPropTypes.js b/src/components/HTMLEngineProvider/htmlEnginePropTypes.js
similarity index 57%
rename from src/components/RenderHTML/renderHTMLPropTypes.js
rename to src/components/HTMLEngineProvider/htmlEnginePropTypes.js
index 58032059e4b9..6c8537c8d228 100644
--- a/src/components/RenderHTML/renderHTMLPropTypes.js
+++ b/src/components/HTMLEngineProvider/htmlEnginePropTypes.js
@@ -1,14 +1,14 @@
import PropTypes from 'prop-types';
const propTypes = {
- /** HTML string to render */
- html: PropTypes.string.isRequired,
+ children: PropTypes.node,
- /** Optional debug flag */
+ /** Optional debug flag. Prints the TRT in the console when true. */
debug: PropTypes.bool,
};
const defaultProps = {
+ children: null,
debug: false,
};
diff --git a/src/components/HTMLEngineProvider/index.js b/src/components/HTMLEngineProvider/index.js
new file mode 100755
index 000000000000..a96bccad3c2e
--- /dev/null
+++ b/src/components/HTMLEngineProvider/index.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import BaseHTMLEngineProvider from './BaseHTMLEngineProvider';
+import {defaultProps, propTypes} from './htmlEnginePropTypes';
+import withWindowDimensions from '../withWindowDimensions';
+import canUseTouchScreen from '../../libs/canUseTouchscreen';
+
+const HTMLEngineProvider = ({isSmallScreenWidth, debug, children}) => (
+
+ {children}
+
+);
+
+HTMLEngineProvider.displayName = 'HTMLEngineProvider';
+HTMLEngineProvider.propTypes = propTypes;
+HTMLEngineProvider.defaultProps = defaultProps;
+
+export default withWindowDimensions(HTMLEngineProvider);
diff --git a/src/components/HTMLEngineProvider/index.native.js b/src/components/HTMLEngineProvider/index.native.js
new file mode 100755
index 000000000000..b524495e7084
--- /dev/null
+++ b/src/components/HTMLEngineProvider/index.native.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import BaseHTMLEngineProvider from './BaseHTMLEngineProvider';
+import {propTypes, defaultProps} from './htmlEnginePropTypes';
+
+const HTMLEngineProvider = ({debug, children}) => (
+
+ {children}
+
+);
+
+HTMLEngineProvider.displayName = 'HTMLEngineProvider';
+HTMLEngineProvider.propTypes = propTypes;
+HTMLEngineProvider.defaultProps = defaultProps;
+
+export default HTMLEngineProvider;
diff --git a/src/components/RenderHTML.js b/src/components/RenderHTML.js
new file mode 100644
index 000000000000..74f8a888b2e0
--- /dev/null
+++ b/src/components/RenderHTML.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import {useWindowDimensions} from 'react-native';
+import PropTypes from 'prop-types';
+import {RenderHTMLSource} from 'react-native-render-html';
+
+// We are using the explicit composite architecture for performance gains.
+// Configuration for RenderHTML is handled in a top-level component providing
+// context to RenderHTMLSource components. See https://git.io/JRcZb
+// The provider is available at src/components/HTMLEngineProvider/
+const RenderHTML = ({html}) => {
+ const {width} = useWindowDimensions();
+ return (
+
+ );
+};
+
+RenderHTML.displayName = 'RenderHTML';
+RenderHTML.propTypes = {
+ /** HTML string to render */
+ html: PropTypes.string.isRequired,
+};
+RenderHTML.defaultProps = {};
+
+export default RenderHTML;
diff --git a/src/components/RenderHTML/index.js b/src/components/RenderHTML/index.js
deleted file mode 100755
index 405d2621154b..000000000000
--- a/src/components/RenderHTML/index.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import BaseRenderHTML from './BaseRenderHTML';
-import withWindowDimensions from '../withWindowDimensions';
-import {
- propTypes,
- defaultProps,
-} from './renderHTMLPropTypes';
-import canUseTouchScreen from '../../libs/canUseTouchscreen';
-
-const RenderHTML = ({html, debug, isSmallScreenWidth}) => (
-
-);
-
-RenderHTML.displayName = 'RenderHTML';
-RenderHTML.propTypes = propTypes;
-RenderHTML.defaultProps = defaultProps;
-
-export default withWindowDimensions(RenderHTML);
diff --git a/src/components/RenderHTML/index.native.js b/src/components/RenderHTML/index.native.js
deleted file mode 100755
index 99048ea86c37..000000000000
--- a/src/components/RenderHTML/index.native.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-import BaseRenderHTML from './BaseRenderHTML';
-import {
- propTypes,
- defaultProps,
-} from './renderHTMLPropTypes';
-
-const RenderHTML = ({html, debug}) => (
-
-);
-
-RenderHTML.displayName = 'RenderHTML';
-RenderHTML.propTypes = propTypes;
-RenderHTML.defaultProps = defaultProps;
-
-export default RenderHTML;
diff --git a/src/components/TextInputWithLabel.js b/src/components/TextInputWithLabel.js
index 40ac02a9fcbc..310a4a91ca31 100644
--- a/src/components/TextInputWithLabel.js
+++ b/src/components/TextInputWithLabel.js
@@ -54,7 +54,12 @@ const TextInputWithLabel = props => (
)}
diff --git a/src/languages/en.js b/src/languages/en.js
index 864bc7aac79b..22a746f24070 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -135,6 +135,9 @@ export default {
reportActionsView: {
beFirstPersonToComment: 'Be the first person to comment',
},
+ reportActionsViewMarkerBadge: {
+ newMsg: ({count}) => `${count} new message${count > 1 ? 's' : ''}`,
+ },
reportTypingIndicator: {
isTyping: 'is typing...',
areTyping: 'are typing...',
@@ -337,6 +340,7 @@ export default {
notFound: {
chatYouLookingForCannotBeFound: 'The chat you are looking for cannot be found.',
getMeOutOfHere: 'Get me out of here',
+ iouReportNotFound: 'The payment details you are looking for cannot be found.',
},
setPasswordPage: {
enterPassword: 'Enter a password',
diff --git a/src/languages/es.js b/src/languages/es.js
index 8d3dee117fbf..0dfd38895d84 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -135,6 +135,9 @@ export default {
reportActionsView: {
beFirstPersonToComment: 'Sé el primero en comentar',
},
+ reportActionsViewMarkerBadge: {
+ newMsg: ({count}) => `${count} mensaje${count > 1 ? 's' : ''} nuevo${count > 1 ? 's' : ''}`,
+ },
reportTypingIndicator: {
isTyping: 'está escribiendo...',
areTyping: 'están escribiendo...',
@@ -335,8 +338,9 @@ export default {
createGroup: 'Crear Grupo',
},
notFound: {
- chatYouLookingForCannotBeFound: 'No se pudo encontrar el chat que estabas buscando.',
+ chatYouLookingForCannotBeFound: 'El chat que estás buscando no se ha podido encontrar.',
getMeOutOfHere: 'Sácame de aquí',
+ iouReportNotFound: 'Los detalles del pago que estás buscando no se han podido encontrar.',
},
setPasswordPage: {
enterPassword: 'Escribe una contraseña',
diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js
index 28b3075e45ab..8c6522a040c2 100644
--- a/src/libs/ValidationUtils.js
+++ b/src/libs/ValidationUtils.js
@@ -1,7 +1,7 @@
import moment from 'moment';
import CONST from '../CONST';
-import Growl from './Growl';
import {translateLocal} from './translate';
+import {showBankAccountFormValidationError} from './actions/BankAccounts';
/**
* Validating that this is a valid address (PO boxes are not allowed)
@@ -74,27 +74,27 @@ function isValidSSNLastFour(ssnLast4) {
*/
function isValidIdentity(identity) {
if (!isValidAddress(identity.street)) {
- Growl.error(translateLocal('bankAccount.error.address'));
+ showBankAccountFormValidationError(translateLocal('bankAccount.error.address'));
return false;
}
if (identity.state === '') {
- Growl.error(translateLocal('bankAccount.error.addressState'));
+ showBankAccountFormValidationError(translateLocal('bankAccount.error.addressState'));
return false;
}
if (!isValidZipCode(identity.zipCode)) {
- Growl.error(translateLocal('bankAccount.error.zipCode'));
+ showBankAccountFormValidationError(translateLocal('bankAccount.error.zipCode'));
return false;
}
if (!isValidDate(identity.dob)) {
- Growl.error(translateLocal('bankAccount.error.dob'));
+ showBankAccountFormValidationError(translateLocal('bankAccount.error.dob'));
return false;
}
if (!isValidSSNLastFour(identity.ssnLast4)) {
- Growl.error(translateLocal('bankAccount.error.ssnLast4'));
+ showBankAccountFormValidationError(translateLocal('bankAccount.error.ssnLast4'));
return false;
}
diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js
index 1732f7d812fc..78d8e0a1f9f4 100644
--- a/src/libs/actions/BankAccounts.js
+++ b/src/libs/actions/BankAccounts.js
@@ -602,6 +602,10 @@ function validateBankAccount(bankAccountID, validateCode) {
});
}
+function showBankAccountFormValidationError(error) {
+ Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error}).then(() => Growl.error(error));
+}
+
/**
* Create or update the bank account in db with the updated data.
*
@@ -773,7 +777,7 @@ function setupWithdrawalAccount(data) {
goToWithdrawalAccountSetupStep(nextStep, achData);
if (error) {
- Growl.error(error, 5000);
+ showBankAccountFormValidationError(error);
}
})
.catch((response) => {
@@ -783,7 +787,7 @@ function setupWithdrawalAccount(data) {
});
}
-function hideExistingOwnersError() {
+function hideBankAccountErrors() {
Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error: '', existingOwnersList: ''});
}
@@ -799,5 +803,6 @@ export {
goToWithdrawalAccountSetupStep,
setupWithdrawalAccount,
validateBankAccount,
- hideExistingOwnersError,
+ hideBankAccountErrors,
+ showBankAccountFormValidationError,
};
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index 4f9a766e8f36..76b778472720 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -461,11 +461,17 @@ function removeOptimisticActions(reportID) {
*
* @param {Number} iouReportID - ID of the report we are fetching
* @param {Number} chatReportID - associated chatReportID, set as an iouReport field
+ * @param {Boolean} [shouldRedirectIfEmpty=false] - Whether to redirect to Active Report Screen if IOUReport is empty
* @returns {Promise}
*/
-function fetchIOUReportByID(iouReportID, chatReportID) {
+function fetchIOUReportByID(iouReportID, chatReportID, shouldRedirectIfEmpty = false) {
return fetchIOUReport(iouReportID, chatReportID)
.then((iouReportObject) => {
+ if (!iouReportObject && shouldRedirectIfEmpty) {
+ Growl.error(translateLocal('notFound.iouReportNotFound'));
+ Navigation.navigate(ROUTES.REPORT);
+ return;
+ }
setLocalIOUReportData(iouReportObject);
return iouReportObject;
});
diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js
index fec0dafc581c..481fff07e3d7 100644
--- a/src/libs/actions/SignInRedirect.js
+++ b/src/libs/actions/SignInRedirect.js
@@ -45,21 +45,18 @@ function redirectToSignIn(errorMessage) {
const activeClients = currentActiveClients;
const preferredLocale = currentPreferredLocale;
- // We must set the authToken to null so we can navigate to "signin" it's not possible to navigate to the route as
- // it only exists when the authToken is null.
- Onyx.set(ONYXKEYS.SESSION, {authToken: null})
+ // Clearing storage discards the authToken. This causes a redirect to the SignIn screen
+ Onyx.clear()
.then(() => {
- Onyx.clear().then(() => {
- if (preferredLocale) {
- Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, preferredLocale);
- }
- if (errorMessage) {
- Onyx.set(ONYXKEYS.SESSION, {error: errorMessage});
- }
- if (activeClients && activeClients.length > 0) {
- Onyx.set(ONYXKEYS.ACTIVE_CLIENTS, activeClients);
- }
- });
+ if (preferredLocale) {
+ Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, preferredLocale);
+ }
+ if (errorMessage) {
+ Onyx.set(ONYXKEYS.SESSION, {error: errorMessage});
+ }
+ if (activeClients && activeClients.length > 0) {
+ Onyx.set(ONYXKEYS.ACTIVE_CLIENTS, activeClients);
+ }
});
}
diff --git a/src/libs/numberFormat/index.native.js b/src/libs/numberFormat/index.native.js
index de5366f4faba..f18ac62148f7 100644
--- a/src/libs/numberFormat/index.native.js
+++ b/src/libs/numberFormat/index.native.js
@@ -4,8 +4,9 @@ import '@formatjs/intl-locale/polyfill';
import '@formatjs/intl-pluralrules/polyfill';
import '@formatjs/intl-numberformat/polyfill';
-// Load en Locale data
+// Load en & es Locale data
import '@formatjs/intl-numberformat/locale-data/en';
+import '@formatjs/intl-numberformat/locale-data/es';
function numberFormat(locale, number, options) {
return new Intl.NumberFormat(locale, options).format(number);
diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js
index bcedfdb74cd2..3fe264dc5e4a 100644
--- a/src/pages/ReimbursementAccount/BankAccountStep.js
+++ b/src/pages/ReimbursementAccount/BankAccountStep.js
@@ -23,7 +23,7 @@ import Text from '../../components/Text';
import ExpensiTextInput from '../../components/ExpensiTextInput';
import {
goToWithdrawalAccountSetupStep,
- hideExistingOwnersError,
+ hideBankAccountErrors,
setupWithdrawalAccount,
} from '../../libs/actions/BankAccounts';
import ConfirmModal from '../../components/ConfirmModal';
@@ -130,8 +130,8 @@ class BankAccountStep extends React.Component {
const isFromPlaid = this.props.achData.setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID;
const shouldDisableInputs = Boolean(this.props.achData.bankAccountID) || isFromPlaid;
const existingOwners = this.props.reimbursementAccount.existingOwners;
- const isExistingOwnersErrorVisible = Boolean(this.props.reimbursementAccount.error
- && existingOwners);
+ const error = this.props.reimbursementAccount.error;
+ const isExistingOwnersErrorVisible = Boolean(error && existingOwners);
return (
this.setState({routingNumber})}
+ onChangeText={(routingNumber) => {
+ if (error === this.props.translate('bankAccount.error.routingNumber')) {
+ hideBankAccountErrors();
+ }
+ this.setState({routingNumber});
+ }}
disabled={shouldDisableInputs}
+ errorText={error === this.props.translate('bankAccount.error.routingNumber')
+ ? error : ''}
/>
diff --git a/src/pages/ReimbursementAccount/BeneficialOwnersStep.js b/src/pages/ReimbursementAccount/BeneficialOwnersStep.js
index 8562fbef130a..e86f22f44bf4 100644
--- a/src/pages/ReimbursementAccount/BeneficialOwnersStep.js
+++ b/src/pages/ReimbursementAccount/BeneficialOwnersStep.js
@@ -2,6 +2,7 @@ import _ from 'underscore';
import React from 'react';
import PropTypes from 'prop-types';
import {ScrollView, View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
import Text from '../../components/Text';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import styles from '../../styles/styles';
@@ -11,17 +12,28 @@ import Button from '../../components/Button';
import IdentityForm from './IdentityForm';
import FixedFooter from '../../components/FixedFooter';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
-import {goToWithdrawalAccountSetupStep, setupWithdrawalAccount} from '../../libs/actions/BankAccounts';
+import {
+ goToWithdrawalAccountSetupStep,
+ setupWithdrawalAccount,
+} from '../../libs/actions/BankAccounts';
import Navigation from '../../libs/Navigation/Navigation';
import CONST from '../../CONST';
import {isValidIdentity} from '../../libs/ValidationUtils';
import Growl from '../../libs/Growl';
+import ONYXKEYS from '../../ONYXKEYS';
+import compose from '../../libs/compose';
const propTypes = {
/** Name of the company */
companyName: PropTypes.string.isRequired,
...withLocalizePropTypes,
+
+ /** Bank account currently in setup */
+ reimbursementAccount: PropTypes.shape({
+ /** Error set when handling the API response */
+ error: PropTypes.string,
+ }).isRequired,
};
class BeneficialOwnersStep extends React.Component {
@@ -172,6 +184,7 @@ class BeneficialOwnersStep extends React.Component {
dob: owner.dob || '',
ssnLast4: owner.ssnLast4 || '',
}}
+ error={this.props.reimbursementAccount.error}
/>
{this.state.beneficialOwners.length > 1 && (
this.removeBeneficialOwner(owner)}>
@@ -230,5 +243,11 @@ class BeneficialOwnersStep extends React.Component {
}
BeneficialOwnersStep.propTypes = propTypes;
-
-export default withLocalize(BeneficialOwnersStep);
+export default compose(
+ withLocalize,
+ withOnyx({
+ reimbursementAccount: {
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ },
+ }),
+)(BeneficialOwnersStep);
diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js
index 5260a4ff1306..2bc48d644b16 100644
--- a/src/pages/ReimbursementAccount/CompanyStep.js
+++ b/src/pages/ReimbursementAccount/CompanyStep.js
@@ -4,9 +4,15 @@ import React from 'react';
import {View, ScrollView} from 'react-native';
import Str from 'expensify-common/lib/str';
import moment from 'moment';
+import PropTypes from 'prop-types';
+import {withOnyx} from 'react-native-onyx';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import CONST from '../../CONST';
-import {goToWithdrawalAccountSetupStep, setupWithdrawalAccount} from '../../libs/actions/BankAccounts';
+import {
+ goToWithdrawalAccountSetupStep, hideBankAccountErrors,
+ setupWithdrawalAccount,
+ showBankAccountFormValidationError,
+} from '../../libs/actions/BankAccounts';
import Navigation from '../../libs/Navigation/Navigation';
import Text from '../../components/Text';
import ExpensiTextInput from '../../components/ExpensiTextInput';
@@ -20,9 +26,21 @@ import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize
import {
isValidAddress, isValidDate, isValidIndustryCode, isValidZipCode,
} from '../../libs/ValidationUtils';
+import compose from '../../libs/compose';
+import ONYXKEYS from '../../ONYXKEYS';
import ConfirmModal from '../../components/ConfirmModal';
import ExpensiPicker from '../../components/ExpensiPicker';
+const propTypes = {
+ /** Bank account currently in setup */
+ reimbursementAccount: PropTypes.shape({
+ /** Error set when handling the API response */
+ error: PropTypes.string,
+ }).isRequired,
+
+ ...withLocalizePropTypes,
+};
+
class CompanyStep extends React.Component {
constructor(props) {
super(props);
@@ -68,38 +86,47 @@ class CompanyStep extends React.Component {
*/
validate() {
if (!this.state.password.trim()) {
+ showBankAccountFormValidationError(this.props.translate('common.passwordCannotBeBlank'));
return false;
}
if (!isValidAddress(this.state.addressStreet)) {
+ showBankAccountFormValidationError(this.props.translate('bankAccount.error.addressStreet'));
return false;
}
if (this.state.addressState === '') {
+ showBankAccountFormValidationError(this.props.translate('bankAccount.error.addressState'));
return false;
}
if (!isValidZipCode(this.state.addressZipCode)) {
+ showBankAccountFormValidationError(this.props.translate('bankAccount.error.zipCode'));
return false;
}
if (!Str.isValidURL(this.state.website)) {
+ showBankAccountFormValidationError(this.props.translate('bankAccount.error.website'));
return false;
}
if (!/[0-9]{9}/.test(this.state.companyTaxID)) {
+ showBankAccountFormValidationError(this.props.translate('bankAccount.error.taxID'));
return false;
}
if (!isValidDate(this.state.incorporationDate)) {
+ showBankAccountFormValidationError(this.props.translate('bankAccount.error.incorporationDate'));
return false;
}
if (!isValidIndustryCode(this.state.industryCode)) {
+ showBankAccountFormValidationError(this.props.translate('bankAccount.error.industryCode'));
return false;
}
if (!this.state.hasNoConnectionToCannabis) {
+ showBankAccountFormValidationError(this.props.translate('bankAccount.error.restrictedBusiness'));
return false;
}
@@ -121,6 +148,7 @@ class CompanyStep extends React.Component {
const shouldDisableCompanyTaxID = Boolean(this.props.achData.bankAccountID && this.props.achData.companyTaxID);
const missingRequiredFields = this.requiredFields.reduce((acc, curr) => acc || !this.state[curr].trim(), false);
const shouldDisableSubmitButton = !this.state.hasNoConnectionToCannabis || missingRequiredFields;
+ const error = this.props.reimbursementAccount.error;
return (
<>
@@ -143,8 +171,16 @@ class CompanyStep extends React.Component {
this.setState({addressStreet})}
+ onChangeText={(addressStreet) => {
+ if (error === this.props.translate('bankAccount.error.addressStreet')) {
+ hideBankAccountErrors();
+ }
+ this.setState({addressStreet});
+ }}
value={this.state.addressStreet}
+ errorText={error === this.props.translate('bankAccount.error.addressStreet')
+ ? this.props.translate('bankAccount.error.addressStreet')
+ : ''}
/>
@@ -165,8 +201,16 @@ class CompanyStep extends React.Component {
this.setState({addressZipCode})}
+ onChangeText={(addressZipCode) => {
+ if (error === this.props.translate('bankAccount.error.zipCode')) {
+ hideBankAccountErrors();
+ }
+ this.setState({addressZipCode});
+ }}
value={this.state.addressZipCode}
+ errorText={error === this.props.translate('bankAccount.error.zipCode')
+ ? this.props.translate('bankAccount.error.zipCode')
+ : ''}
/>
this.setState({website})}
+ onChangeText={(website) => {
+ if (error === this.props.translate('bankAccount.error.website')) {
+ hideBankAccountErrors();
+ }
+ this.setState({website});
+ }}
value={this.state.website}
+ errorText={error === this.props.translate('bankAccount.error.website')
+ ? this.props.translate('bankAccount.error.website')
+ : ''}
/>
this.setState({companyTaxID})}
+ onChangeText={(companyTaxID) => {
+ if (error === this.props.translate('bankAccount.error.taxID')) {
+ hideBankAccountErrors();
+ }
+ this.setState({companyTaxID});
+ }}
value={this.state.companyTaxID}
disabled={shouldDisableCompanyTaxID}
+ errorText={error === this.props.translate('bankAccount.error.taxID')
+ ? this.props.translate('bankAccount.error.taxID')
+ : ''}
/>
this.setState({incorporationDate})}
+ onChangeText={(incorporationDate) => {
+ if (error === this.props.translate('bankAccount.error.incorporationDate')) {
+ hideBankAccountErrors();
+ }
+ this.setState({incorporationDate});
+ }}
value={this.state.incorporationDate}
placeholder={this.props.translate('companyStep.incorporationDatePlaceholder')}
+ errorText={error === this.props.translate('bankAccount.error.incorporationDate')
+ ? this.props.translate('bankAccount.error.incorporationDate')
+ : ''}
/>
@@ -223,8 +291,16 @@ class CompanyStep extends React.Component {
helpLinkText={this.props.translate('common.whatThis')}
helpLinkURL="https://www.naics.com/search/"
containerStyles={[styles.mt4]}
- onChangeText={industryCode => this.setState({industryCode})}
+ onChangeText={(industryCode) => {
+ if (error === this.props.translate('bankAccount.error.industryCode')) {
+ hideBankAccountErrors();
+ }
+ this.setState({industryCode});
+ }}
value={this.state.industryCode}
+ errorText={error === this.props.translate('bankAccount.error.industryCode')
+ ? this.props.translate('bankAccount.error.industryCode')
+ : ''}
/>
this.setState({password})}
+ onChangeText={(password) => {
+ if (error === this.props.translate('common.passwordCannotBeBlank')) {
+ hideBankAccountErrors();
+ }
+ this.setState({password});
+ }}
value={this.state.password}
onSubmitEditing={this.submit}
+ errorText={error === this.props.translate('common.passwordCannotBeBlank')
+ ? this.props.translate('common.passwordCannotBeBlank')
+ : ''}
/>
this.setState({isConfirmModalOpen: false})}
prompt="Please double check any highlighted fields and try again."
isVisible={this.state.isConfirmModalOpen}
@@ -279,6 +363,12 @@ class CompanyStep extends React.Component {
}
}
-CompanyStep.propTypes = withLocalizePropTypes;
-
-export default withLocalize(CompanyStep);
+CompanyStep.propTypes = propTypes;
+export default compose(
+ withLocalize,
+ withOnyx({
+ reimbursementAccount: {
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ },
+ }),
+)(CompanyStep);
diff --git a/src/pages/ReimbursementAccount/IdentityForm.js b/src/pages/ReimbursementAccount/IdentityForm.js
index 96d3d585c638..cbcf3c4e6a32 100644
--- a/src/pages/ReimbursementAccount/IdentityForm.js
+++ b/src/pages/ReimbursementAccount/IdentityForm.js
@@ -6,6 +6,8 @@ import StatePicker from '../../components/StatePicker';
import ExpensiTextInput from '../../components/ExpensiTextInput';
import styles from '../../styles/styles';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
+import {translateLocal} from '../../libs/translate';
+import {hideBankAccountErrors} from '../../libs/actions/BankAccounts';
const propTypes = {
/** Style for wrapping View */
@@ -41,6 +43,9 @@ const propTypes = {
ssnLast4: PropTypes.string,
}),
+ /** Any errors that can arise from form validation */
+ error: PropTypes.string,
+
...withLocalizePropTypes,
};
@@ -56,10 +61,11 @@ const defaultProps = {
dob: '',
ssnLast4: '',
},
+ error: '',
};
const IdentityForm = ({
- translate, values, onFieldChange, style,
+ translate, values, onFieldChange, style, error,
}) => {
const {
firstName, lastName, street, city, state, zipCode, dob, ssnLast4,
@@ -87,19 +93,37 @@ const IdentityForm = ({
containerStyles={[styles.mt4]}
placeholder={translate('common.dateFormat')}
value={dob}
- onChangeText={val => onFieldChange('dob', val)}
+ onChangeText={(val) => {
+ if (error === translateLocal('bankAccount.error.dob')) {
+ hideBankAccountErrors();
+ }
+ onFieldChange('dob', val);
+ }}
+ errorText={error === translateLocal('bankAccount.error.dob') ? error : ''}
/>
onFieldChange('ssnLast4', val)}
+ onChangeText={(val) => {
+ if (error === translateLocal('bankAccount.error.ssnLast4')) {
+ hideBankAccountErrors();
+ }
+ onFieldChange('ssnLast4', val);
+ }}
+ errorText={error === translateLocal('bankAccount.error.ssnLast4') ? error : ''}
/>
onFieldChange('street', val)}
+ onChangeText={(val) => {
+ if (error === translateLocal('bankAccount.error.address')) {
+ hideBankAccountErrors();
+ }
+ onFieldChange('street', val);
+ }}
+ errorText={error === translateLocal('bankAccount.error.address') ? error : ''}
/>
@@ -121,7 +145,13 @@ const IdentityForm = ({
label={translate('common.zip')}
containerStyles={[styles.mt4]}
value={zipCode}
- onChangeText={val => onFieldChange('zipCode', val)}
+ onChangeText={(val) => {
+ if (error === translateLocal('bankAccount.error.zipCode')) {
+ hideBankAccountErrors();
+ }
+ onFieldChange('zipCode', val);
+ }}
+ errorText={error === translateLocal('bankAccount.error.zipCode') ? error : ''}
/>
);
diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js
index 2287724daa9b..122b5825acbf 100644
--- a/src/pages/ReimbursementAccount/RequestorStep.js
+++ b/src/pages/ReimbursementAccount/RequestorStep.js
@@ -1,6 +1,8 @@
import React from 'react';
import lodashGet from 'lodash/get';
import {View, ScrollView} from 'react-native';
+import PropTypes from 'prop-types';
+import {withOnyx} from 'react-native-onyx';
import styles from '../../styles/styles';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
@@ -9,13 +11,28 @@ import TextLink from '../../components/TextLink';
import Navigation from '../../libs/Navigation/Navigation';
import CheckboxWithLabel from '../../components/CheckboxWithLabel';
import Text from '../../components/Text';
-import {goToWithdrawalAccountSetupStep, setupWithdrawalAccount} from '../../libs/actions/BankAccounts';
+import {
+ goToWithdrawalAccountSetupStep,
+ setupWithdrawalAccount,
+} from '../../libs/actions/BankAccounts';
import Button from '../../components/Button';
import FixedFooter from '../../components/FixedFooter';
import IdentityForm from './IdentityForm';
import {isValidIdentity} from '../../libs/ValidationUtils';
import Growl from '../../libs/Growl';
import Onfido from '../../components/Onfido';
+import compose from '../../libs/compose';
+import ONYXKEYS from '../../ONYXKEYS';
+
+const propTypes = {
+ /** Bank account currently in setup */
+ reimbursementAccount: PropTypes.shape({
+ /** Error set when handling the API response */
+ error: PropTypes.string,
+ }).isRequired,
+
+ ...withLocalizePropTypes,
+};
class RequestorStep extends React.Component {
constructor(props) {
@@ -116,6 +133,7 @@ class RequestorStep extends React.Component {
dob: this.state.dob,
ssnLast4: this.state.ssnLast4,
}}
+ error={this.props.reimbursementAccount.error}
/>
this.setState({isLoading: false}), 150);
+ this.loadingTimerId = setTimeout(() => this.setState({isLoading: false}), 0);
}
/**
@@ -102,16 +124,30 @@ class ReportScreen extends React.Component {
return null;
}
+ const reportID = this.getReportID();
return (
Navigation.navigate(ROUTES.HOME)}
/>
-
-
- {!this.shouldShowLoader() && }
+
+
+ {!this.shouldShowLoader() && }
+ {this.props.session.shouldShowComposeInput && (
+ Keyboard.dismiss()}>
+
+
+ )}
+
+
);
}
@@ -124,4 +160,7 @@ export default withOnyx({
isSidebarLoaded: {
key: ONYXKEYS.IS_SIDEBAR_LOADED,
},
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
})(ReportScreen);
diff --git a/src/pages/home/report/MarkerBadge.js b/src/pages/home/report/MarkerBadge.js
new file mode 100644
index 000000000000..f060f305dd05
--- /dev/null
+++ b/src/pages/home/report/MarkerBadge.js
@@ -0,0 +1,125 @@
+import React, {PureComponent} from 'react';
+import {Animated, Text, View} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../../../styles/styles';
+import Button from '../../../components/Button';
+import Icon from '../../../components/Icon';
+import {Close, DownArrow} from '../../../components/Icon/Expensicons';
+import themeColors from '../../../styles/themes/default';
+import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
+
+const MARKER_NOT_ACTIVE_TRANSLATE_Y = -30;
+const MARKER_ACTIVE_TRANSLATE_Y = 10;
+const propTypes = {
+ /** Count of new messages to show in the badge */
+ count: PropTypes.number,
+
+ /** Whether the marker is active */
+ active: PropTypes.bool,
+
+ /** Callback to be called when user closes the badge */
+ onClose: PropTypes.func,
+
+ /** Callback to be called when user clicks the marker */
+ onClick: PropTypes.func,
+
+ ...withLocalizePropTypes,
+};
+const defaultProps = {
+ count: 0,
+ active: false,
+ onClose: () => {},
+ onClick: () => {},
+};
+class MarkerBadge extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.translateY = new Animated.Value(MARKER_NOT_ACTIVE_TRANSLATE_Y);
+ this.show = this.show.bind(this);
+ this.hide = this.hide.bind(this);
+ }
+
+ componentDidUpdate() {
+ if (this.props.active && this.props.count > 0) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ }
+
+ show() {
+ Animated.spring(this.translateY, {
+ toValue: MARKER_ACTIVE_TRANSLATE_Y,
+ duration: 80,
+ useNativeDriver: true,
+ }).start();
+ }
+
+ hide() {
+ Animated.spring(this.translateY, {
+ toValue: MARKER_NOT_ACTIVE_TRANSLATE_Y,
+ duration: 80,
+ useNativeDriver: true,
+ }).start();
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+ );
+ }
+}
+
+MarkerBadge.propTypes = propTypes;
+MarkerBadge.defaultProps = defaultProps;
+MarkerBadge.displayName = 'MarkerBadge';
+
+export default withLocalize(MarkerBadge);
diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js
index 15241841116b..5a69a14c263c 100644
--- a/src/pages/home/report/ReportActionItemFragment.js
+++ b/src/pages/home/report/ReportActionItemFragment.js
@@ -61,8 +61,7 @@ class ReportActionItemFragment extends React.PureComponent {
return fragment.html !== fragment.text
? (
' : '')}
- debug={false}
+ html={fragment.html + (fragment.isEdited ? '' : '')}
/>
) : (
-200 && this.state.isMarkerActive) {
+ this.hideMarker();
+ }
+ }
+
+ /**
+ * Show the new MarkerBadge
+ */
+ showMarker() {
+ this.setState({isMarkerActive: true});
+ }
+
+ /**
+ * Hide the new MarkerBadge
+ */
+ hideMarker() {
+ this.setState({isMarkerActive: false});
+ }
+
+ /**
+ * keeps track of the Scroll offset of the main messages list
+ *
+ * @param {Object} {nativeEvent}
+ */
+ trackScroll({nativeEvent}) {
+ this.currentScrollOffset = -nativeEvent.contentOffset.y;
+ this.toggleMarker();
+ }
+
/**
* Runs when the FlatList finishes laying out
*/
@@ -427,6 +478,12 @@ class ReportActionsView extends React.Component {
return (
<>
+
>
diff --git a/src/pages/home/report/ReportView.js b/src/pages/home/report/ReportView.js
deleted file mode 100644
index 1eb4ea57589a..000000000000
--- a/src/pages/home/report/ReportView.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react';
-import {Keyboard, View} from 'react-native';
-import PropTypes from 'prop-types';
-import {withOnyx} from 'react-native-onyx';
-import ReportActionsView from './ReportActionsView';
-import ReportActionCompose from './ReportActionCompose';
-import {addAction} from '../../../libs/actions/Report';
-import KeyboardSpacer from '../../../components/KeyboardSpacer';
-import styles from '../../../styles/styles';
-import SwipeableView from '../../../components/SwipeableView';
-import ONYXKEYS from '../../../ONYXKEYS';
-import CONST from '../../../CONST';
-
-const propTypes = {
- /** The ID of the report the selected report */
- reportID: PropTypes.number.isRequired,
-
- /* Onyx Keys */
-
- /** Whether or not to show the Compose Input */
- session: PropTypes.shape({
- shouldShowComposeInput: PropTypes.bool,
- }),
-};
-
-const defaultProps = {
- session: {
- shouldShowComposeInput: true,
- },
-};
-
-const ReportView = ({reportID, session}) => (
-
-
-
- {session.shouldShowComposeInput && (
- Keyboard.dismiss()}>
- addAction(reportID, text)}
- reportID={reportID}
- />
-
- )}
-
-
-);
-
-ReportView.propTypes = propTypes;
-ReportView.defaultProps = defaultProps;
-ReportView.displayName = 'ReportView';
-
-export default withOnyx({
- session: {
- key: ONYXKEYS.SESSION,
- },
-})(ReportView);
diff --git a/src/pages/iou/IOUDetailsModal.js b/src/pages/iou/IOUDetailsModal.js
index 17be9bc16467..822149b4a5d3 100644
--- a/src/pages/iou/IOUDetailsModal.js
+++ b/src/pages/iou/IOUDetailsModal.js
@@ -107,7 +107,7 @@ class IOUDetailsModal extends Component {
componentDidMount() {
this.isComponentMounted = true;
- fetchIOUReportByID(this.props.route.params.iouReportID, this.props.route.params.chatReportID);
+ fetchIOUReportByID(this.props.route.params.iouReportID, this.props.route.params.chatReportID, true);
this.addVenmoPaymentOptionIfAvailable();
this.addExpensifyPaymentOptionIfAvailable();
}
diff --git a/src/pages/settings/AddSecondaryLoginPage.js b/src/pages/settings/AddSecondaryLoginPage.js
index 5dae82dec11c..8438f3caded9 100755
--- a/src/pages/settings/AddSecondaryLoginPage.js
+++ b/src/pages/settings/AddSecondaryLoginPage.js
@@ -72,6 +72,7 @@ class AddSecondaryLoginPage extends Component {
};
this.formType = props.route.params.type;
this.submitForm = this.submitForm.bind(this);
+ this.onSecondaryLoginChange = this.onSecondaryLoginChange.bind(this);
this.validateForm = this.validateForm.bind(this);
this.phoneNumberInputRef = null;
@@ -81,6 +82,15 @@ class AddSecondaryLoginPage extends Component {
Onyx.merge(ONYXKEYS.USER, {error: ''});
}
+ onSecondaryLoginChange(login) {
+ if (this.formType === CONST.LOGIN_TYPE.EMAIL) {
+ this.setState({login});
+ } else if (this.formType === CONST.LOGIN_TYPE.PHONE
+ && (CONST.REGEX.DIGITS_AND_PLUS.test(login) || login === '')) {
+ this.setState({login});
+ }
+ }
+
/**
* Add a secondary login to a user's account
*/
@@ -133,7 +143,7 @@ class AddSecondaryLoginPage extends Component {
: 'profilePage.emailAddress')}
ref={el => this.phoneNumberInputRef = el}
value={this.state.login}
- onChangeText={login => this.setState({login})}
+ onChangeText={this.onSecondaryLoginChange}
keyboardType={this.formType === CONST.LOGIN_TYPE.PHONE
? CONST.KEYBOARD_TYPE.PHONE_PAD : undefined}
returnKeyType="done"
diff --git a/src/pages/settings/Payments/PaymentsPage.js b/src/pages/settings/Payments/PaymentsPage.js
index 70296695c111..0958973677e8 100644
--- a/src/pages/settings/Payments/PaymentsPage.js
+++ b/src/pages/settings/Payments/PaymentsPage.js
@@ -105,7 +105,7 @@ class PaymentsPage extends React.Component {
{this.props.translate('paymentsPage.paymentMethodsTitle')}
diff --git a/src/pages/settings/Profile/LoginField.js b/src/pages/settings/Profile/LoginField.js
index bd2d336cf529..12539a1ae416 100755
--- a/src/pages/settings/Profile/LoginField.js
+++ b/src/pages/settings/Profile/LoginField.js
@@ -1,5 +1,5 @@
import React, {Component} from 'react';
-import {View, Pressable} from 'react-native';
+import {View} from 'react-native';
import PropTypes from 'prop-types';
import Text from '../../../components/Text';
import styles from '../../../styles/styles';
@@ -12,6 +12,7 @@ import Navigation from '../../../libs/Navigation/Navigation';
import {resendValidateCode} from '../../../libs/actions/User';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import Button from '../../../components/Button';
+import MenuItem from '../../../components/MenuItem';
const propTypes = {
/** Label to display on login form */
@@ -85,21 +86,14 @@ class LoginField extends Component {
{this.props.label}
{!this.props.login.partnerUserID ? (
- Navigation.navigate(ROUTES.getSettingsAddLoginRoute(this.props.type))}
- >
-
-
-
-
-
-
- {`${this.props.translate('common.add')} ${this.props.label}`}
-
-
-
-
+
+
) : (
diff --git a/src/styles/styles.js b/src/styles/styles.js
index aa5acafba932..d71e788b16a4 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -15,9 +15,11 @@ import textInputAlignSelf from './utilities/textInputAlignSelf';
import CONST from '../CONST';
import positioning from './utilities/positioning';
import codeStyles from './codeStyles';
+import visibility from './utilities/visibility';
const expensiPicker = {
backgroundColor: 'transparent',
+ color: themeColors.text,
fontFamily: fontFamily.GTA,
fontSize: variables.fontSizeNormal,
paddingHorizontal: 12,
@@ -249,7 +251,8 @@ const styles = {
},
buttonDropdown: {
- marginLeft: 1,
+ borderLeftWidth: 1,
+ borderColor: themeColors.textReversed,
},
noRightBorderRadius: {
@@ -604,6 +607,10 @@ const styles = {
noOutline: addOutlineWidth({}, 0),
+ errorOutline: {
+ borderColor: colors.red,
+ },
+
textLabelSupporting: {
fontFamily: fontFamily.GTA,
fontSize: variables.fontSizeLabel,
@@ -1980,6 +1987,25 @@ const styles = {
communicationsLinkHeight: {
height: 20,
},
+
+ reportMarkerBadgeWrapper: {
+ position: 'absolute',
+ left: '50%',
+ top: 0,
+ zIndex: 100,
+ ...visibility('hidden'),
+ },
+
+ reportMarkerBadge: {
+ left: '-50%',
+ ...visibility('visible'),
+ },
+
+ reportMarkerBadgeTransformation: translateY => ({
+ transform: [
+ {translateY},
+ ],
+ }),
};
const baseCodeTagStyles = {
diff --git a/src/styles/utilities/spacing.js b/src/styles/utilities/spacing.js
index 32a0abb4e8ad..a376ad817217 100644
--- a/src/styles/utilities/spacing.js
+++ b/src/styles/utilities/spacing.js
@@ -73,6 +73,10 @@ export default {
marginRight: 20,
},
+ mrn5: {
+ marginRight: -20,
+ },
+
ml1: {
marginLeft: 4,
},
@@ -93,6 +97,10 @@ export default {
marginLeft: 20,
},
+ mln5: {
+ marginLeft: -20,
+ },
+
mt1: {
marginTop: 4,
},
diff --git a/src/styles/utilities/visibility/index.js b/src/styles/utilities/visibility/index.js
new file mode 100644
index 000000000000..4cd93699d460
--- /dev/null
+++ b/src/styles/utilities/visibility/index.js
@@ -0,0 +1 @@
+export default visibility => ({visibility});
diff --git a/src/styles/utilities/visibility/index.native.js b/src/styles/utilities/visibility/index.native.js
new file mode 100644
index 000000000000..56bf55ff188d
--- /dev/null
+++ b/src/styles/utilities/visibility/index.native.js
@@ -0,0 +1 @@
+export default () => ({});