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 ( + + + +