diff --git a/.storybook/public/index.css b/.storybook/public/index.css index 12c2ab80e32b..8ace4b240684 100644 --- a/.storybook/public/index.css +++ b/.storybook/public/index.css @@ -1,24 +1,28 @@ .search-field *, .sidebar-item, .search-result-item--label { - color: #fff !important; + color: #E7ECE9 !important; } .sidebar-subheading *, .search-result-item { - color: #fff; + color: #E7ECE9; } a.sidebar-item > svg { color: #03d47c; } +a.sidebar-item[data-selected="true"], a.sidebar-item[data-selected="true"]:focus, a.sidebar-item[data-selected="true"]:hover { + background: #1A3D32; +} + .search-result-item--label span { - color: #ffffffaa !important; + color: #E7ECE9aa !important; } #panel-tab-content :is(input:checked ~ span:last-of-type, input:not(:checked) ~ span:first-of-type) { - background: #ff7101; - color: #fff; + background: #03D47C; + color: #E7ECE9; } .sidebar-container { - background: #0b1b34; + background: #07271f; } diff --git a/.storybook/public/logo.png b/.storybook/public/logo.png index f956ed3d2264..23c909e83f0b 100644 Binary files a/.storybook/public/logo.png and b/.storybook/public/logo.png differ diff --git a/.storybook/public/logomark.svg b/.storybook/public/logomark.svg index 095f92d9ec26..d1ca5ca84d06 100644 --- a/.storybook/public/logomark.svg +++ b/.storybook/public/logomark.svg @@ -1,15 +1,25 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/.storybook/theme.js b/.storybook/theme.js index e33243058453..0867f6a830b5 100644 --- a/.storybook/theme.js +++ b/.storybook/theme.js @@ -2,14 +2,22 @@ import {create} from '@storybook/theming'; import colors from '../src/styles/colors'; export default create({ - appBg: colors.dark, - barSelectedColor: colors.blue, - base: 'light', - brandTitle: 'Expensify UI Docs', + brandTitle: 'New Expensify UI Docs', brandImage: 'logomark.svg', - colorPrimary: colors.dark, - colorSecondary: colors.orange, fontBase: 'ExpensifyNeue-Regular', fontCode: 'monospace', - textInverseColor: colors.black, + base: 'dark', + appBg: colors.greenHighlightBackground, + colorPrimary: colors.greenDefaultButton, + colorSecondary: colors.green, + appContentBg: colors.greenAppBackground, + textColor: colors.white, + barTextColor: colors.white, + barSelectedColor: colors.green, + barBg: colors.greenAppBackground, + appBorderColor: colors.greenBorders, + inputBg: colors.greenHighlightBackground, + inputBorder: colors.greenBorders, + appBorderRadius: 8, + inputBorderRadius: 8, }); diff --git a/android/app/build.gradle b/android/app/build.gradle index d2329a6dc9c8..921da3ce1bf9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -106,8 +106,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001030506 - versionName "1.3.5-6" + versionCode 1001030600 + versionName "1.3.6-0" } splits { diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 530739306aec..fb256bf7e955 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -59,7 +59,7 @@ Please be aware that compensation for any support in solving an issue is provide - Merged PR within 9 business days - 50% **penalty** - No PR within 12 business days - **Contract terminated** -If the PR causes a regression within 7 days of being deployed to production, contributors are not eligible for the 50% bonus. +If the PR causes a regression at any point within the regression period (starting when the code is merged and ending 7 days after being deployed to production), contributors are not eligible for the 50% bonus. ## Finding Jobs A job could be fixing a bug or working on a new feature. There are two ways you can find a job that you can contribute to: diff --git a/docs/articles/playbooks/Expensify-Playbook-for-Small-to-Medium-Sized-Businesses.md b/docs/articles/playbooks/Expensify-Playbook-for-Small-to-Medium-Sized-Businesses.md index 7dc360a2b82b..c818ce5dcfaf 100644 --- a/docs/articles/playbooks/Expensify-Playbook-for-Small-to-Medium-Sized-Businesses.md +++ b/docs/articles/playbooks/Expensify-Playbook-for-Small-to-Medium-Sized-Businesses.md @@ -10,8 +10,6 @@ This guide provides practical tips and recommendations for small businesses with ## Who you are As a small to medium-sized business owner, your main aim is to achieve success and grow your business. To achieve your goals, it is crucial that you make worthwhile investments in both your workforce and your business processes. This means providing your employees with the resources they need to generate revenue effectively, while also adopting measures to guarantee that expenses are compliant. -![Expense Basics](http://127.0.0.1:4000/assets/images/playbook-expense-basics.png) - ## Step-by-step instructions for setting up Expensify This playbook is built on best practices we’ve developed after processing expenses for tens of thousands of companies around the world. As such, use this playbook as your starting point, knowing that you can customize Expensify to suit your business needs. Every company is different, and your dedicated Setup Specialist is always one chat away with any questions you may have. @@ -102,6 +100,8 @@ This is essentially like setting a daily or individual expense limitation on any *Receipt Required Amount: $75* Receipts are important, and in most cases you prefer an itemized receipt. However, Expensify will generate an IRS-compliant electronic receipt (not itemized) for every expense not tied to hotels expense. For this reason, it’s important to enforce a rule where anytime an employee is on the road, and making business-related purchases at hotel (which happens a lot!), they are required to attach a physical receipt. +![Expense Basics](https://help.expensify.com/assets/images/playbook-expense-basics.png) + At this point, you’ve set enough compliance controls around categorical spend and general expenses for all employees, such that you can put trust in our solution to audit all expenses up front so you don’t have to. Next, let’s dive into how we can comfortably take on more automation, while relying on compliance controls to capture bad behavior (or better yet, instill best practices in our employees). ### Step 7: Set up scheduled submit @@ -115,10 +115,14 @@ For an efficient company, we recommend setting up [Scheduled Submit](https://com Between Expensify's SmartScan technology, automatic categorization, and [DoubleCheck](https://community.expensify.com/discussion/5738/deep-dive-how-does-concierge-receipt-audit-work) features, your employees shouldn't need to do anything more than swipe their Expensify Card or take a photo of their receipt. -Expenses with violations will stay behind for the employee to fix, while expenses that are “in-policy” will move into an approver’s queue to mitigate any potential for delays. Scheduled Submit will ensure all expenses are submitted automatically for approval. +Expenses with violations will stay behind for the employee to fix, while expenses that are “in-policy” will move into an approver’s queue to mitigate any potential for delays. Scheduled Submit will ensure all expenses are submitted automatically for approval. -*“We spent twice as much time and effort on expenses without getting nearly as accurate of results as with Expensify.”* -- Kevin Valuska. AP/AR at Road Trippers +![Scheduled submit](https://help.expensify.com/assets/images/playbook-scheduled-submit.png) + +> _We spent twice as much time and effort on expenses without getting nearly as accurate of results as with Expensify._ +> +> Kevin Valuska +> AP/AR at Road Trippers ### Step 8: Connect your business bank account (US only) If you’re located in the US, you can utilize Expensify’s payment processing and reimbursement features. @@ -147,6 +151,8 @@ We recommend you select *Advanced Approval* as your Approval Mode to set up a mi *Import your employees in bulk via CSV* Given the amount of employees you have, it’s best you import employees in bulk via CSV. You can learn more about using a CSV file to bulk upload employees with *Advanced Approval [here](https://community.expensify.com/discussion/5735/deep-dive-the-ins-and-outs-of-advanced-approval)* +![Bulk import your employees](https://help.expensify.com/assets/images/playbook-impoort-employees.png) + *Manually Approve All Reports* In most cases, at this stage, approvers prefer to review all expenses for a few reasons. 1) We want to make sure expense coding is accurate, and 2) We want to make sure there are no bad actors before we export transactions to our accounting system. @@ -176,6 +182,8 @@ Expensify supports direct card feeds from most financial institutions. Setting u 3. Next, assign the corporate cards to your employees by selecting the employee’s email address and the corresponding card number from the two drop-down menus under the *Assign a Card* section 4. Set a transaction start date (this is really important to avoid pulling in multiple outdated historical expenses that you don’t want employees to submit) +![If you have existing corporate cards](https://help.expensify.com/assets/images/playbook-existing-corporate-card.png) + As mentioned above, we’ll be able to pull in transactions as they post (daily) and handle receipt matching for you and your employees. One benefit of the Expensify Card for your company is being able to see transactions at the point of purchase which provides you with real-time compliance. We even send users push notifications to SmartScan their receipt when it’s required and generate IRS-compliant e-receipts as a backup wherever applicable. #### If you don't have a corporate card, use the Expensify Card (US only) @@ -227,6 +235,8 @@ Similarly, you can send bills directly from Expensify as well. 3. At this point, you can also upload an attachment to further validate the bill if necessary 4. Click *Submit*, we’ll forward the newly created bill directly to your Supplier. +![Send bills directly from Expensify](https://help.expensify.com/assets/images/playbook-new-bill.png) + Reports, invoices, and bills are largely the same, in theory, just with different rules. As such, creating a customer invoice is just like creating an expense report and even a bill. 1. From the *Reports* tab, select the down arrow next to *New Report* and select *Invoice*. @@ -248,7 +258,7 @@ We recommend reporting: - *Quarterly* - for budget comparison reporting. Pull up your BI tool and compare your active budgets with your spend reporting here in Expensify - *Annually* - Run annual spend trend reports with month-over-month spend analysis, and prepare yourself for the upcoming fiscal year. - +![Expenses!](https://help.expensify.com/assets/images/playbook-expenses.png) ### Step 14: Set your Subscription Size and Add a Payment card Our pricing model is unique in the sense that you are in full control of your billing. Meaning, you have the ability to set a minimum number of employees you know will be active each month and you can choose which level of commitment fits best. We recommend setting your subscription to *Annual* to get an additional 50% off on your monthly Expensify bill. In the end, you've spent enough time getting your company fully set up with Expensify, and you've seen how well it supports you and your employees. Committing annually just makes sense. diff --git a/docs/articles/playbooks/Expensify-Playbook-for-US-Based-Bootstrapped-Startups.md b/docs/articles/playbooks/Expensify-Playbook-for-US-Based-Bootstrapped-Startups.md index 92df97d30f87..6c8d5314f718 100644 --- a/docs/articles/playbooks/Expensify-Playbook-for-US-Based-Bootstrapped-Startups.md +++ b/docs/articles/playbooks/Expensify-Playbook-for-US-Based-Bootstrapped-Startups.md @@ -5,7 +5,7 @@ description: Best practices for how to deploy Expensify for your business This playbook details best practices on how Bootstrapped Startups with less than 5 employees can use Expensify to prioritize product development while capturing business-related receipts for future reimbursement. - See our *[Playbook for VC-Backed Startups](https://help.expensify.com/articles/playbooks/Expensify-Playbook-for-US-based-VC-Backed-Startups)* if you have taken venture capital investment and are more concerned with prioritizing top-line revenue growth than achieving near-term profitability -- See our *Playbook for Small Businesses* if you are more concerned with maintaining profitability than growing top-line revenue. [Coming soon…] +- See our *[Playbook for Small to Medium-Sized Businesses]([url](https://help.expensify.com/articles/playbooks/Expensify-Playbook-for-Small-to-Medium-Sized-Businesses))* if you are more concerned with maintaining profitability than growing top-line revenue. # Who you are As a bootstrapped startup, you have surrounded yourself with a small handful of people you trust, and are focused on developing your concept to officially launch and possibly take to outside investors. You are likely running your business with your own money, and possibly a small amount of funding from friends and family. You are either paying yourself a little, or not at all, but at this stage, the company isn’t profitable. And for now, you are capturing receipts so that you can reimburse yourself for startup costs when you either take on investment or start to turn a profit. diff --git a/docs/assets/images/playbook-existing-corporate-card.png b/docs/assets/images/playbook-existing-corporate-card.png new file mode 100644 index 000000000000..5baad14abf7c Binary files /dev/null and b/docs/assets/images/playbook-existing-corporate-card.png differ diff --git a/docs/assets/images/playbook-expense-basics.png b/docs/assets/images/playbook-expense-basics.png index 0c03d4b95386..b0bbd2095415 100644 Binary files a/docs/assets/images/playbook-expense-basics.png and b/docs/assets/images/playbook-expense-basics.png differ diff --git a/docs/assets/images/playbook-expenses.png b/docs/assets/images/playbook-expenses.png new file mode 100644 index 000000000000..f025ebbb51fd Binary files /dev/null and b/docs/assets/images/playbook-expenses.png differ diff --git a/docs/assets/images/playbook-impoort-employees.png b/docs/assets/images/playbook-impoort-employees.png new file mode 100644 index 000000000000..4f4f6f4e50ae Binary files /dev/null and b/docs/assets/images/playbook-impoort-employees.png differ diff --git a/docs/assets/images/playbook-new-bill.png b/docs/assets/images/playbook-new-bill.png new file mode 100644 index 000000000000..8e8a01fe156b Binary files /dev/null and b/docs/assets/images/playbook-new-bill.png differ diff --git a/docs/assets/images/playbook-scheduled-submit.png b/docs/assets/images/playbook-scheduled-submit.png new file mode 100644 index 000000000000..c8c6eb91774c Binary files /dev/null and b/docs/assets/images/playbook-scheduled-submit.png differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7777ad8ea62b..19b3c539b1f1 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.5 + 1.3.6 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.3.5.6 + 1.3.6.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 09f034682ce4..27cf75feac0b 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.5 + 1.3.6 CFBundleSignature ???? CFBundleVersion - 1.3.5.6 + 1.3.6.0 diff --git a/package-lock.json b/package-lock.json index 0fa4933091c3..ea45963699e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.5-6", + "version": "1.3.6-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.5-6", + "version": "1.3.6-0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -68,7 +68,7 @@ "react-native-document-picker": "^8.0.0", "react-native-fast-image": "git+https://github.com/Expensify/react-native-fast-image.git#afedf204bfc253d18f08fdcc5356a2bb82f6a87c", "react-native-gesture-handler": "2.9.0", - "react-native-google-places-autocomplete": "git+https://github.com/Expensify/react-native-google-places-autocomplete.git#e12768f1542e7982d90f6449798f0d6b7f18f192", + "react-native-google-places-autocomplete": "git+https://github.com/Expensify/react-native-google-places-autocomplete.git#6f436a06a3018cb49750bb110b89df75f6a865d5", "react-native-haptic-feedback": "^1.13.0", "react-native-image-pan-zoom": "^2.1.12", "react-native-image-picker": "^5.1.0", @@ -34639,9 +34639,9 @@ } }, "node_modules/react-native-google-places-autocomplete": { - "version": "2.4.1", - "resolved": "git+ssh://git@github.com/Expensify/react-native-google-places-autocomplete.git#e12768f1542e7982d90f6449798f0d6b7f18f192", - "integrity": "sha512-Lk5/8qqzPoqx9ygfHSYtqXFvXiWIY+1tnyoWHVbpyTlPrJOcfkFrELCXaDlokJJqg1luvTLoMtEA1pkDfHYVUg==", + "version": "2.5.1", + "resolved": "git+ssh://git@github.com/Expensify/react-native-google-places-autocomplete.git#6f436a06a3018cb49750bb110b89df75f6a865d5", + "integrity": "sha512-7NiBK83VggJ2HQaHGfJoaPyxtiLu1chwP1VqH9te+PZtf0L9p50IuBQciW+4s173cBamt4U2+mvnCt7zfMFeDg==", "license": "MIT", "dependencies": { "lodash.debounce": "^4.0.8", @@ -64361,9 +64361,9 @@ } }, "react-native-google-places-autocomplete": { - "version": "git+ssh://git@github.com/Expensify/react-native-google-places-autocomplete.git#e12768f1542e7982d90f6449798f0d6b7f18f192", - "integrity": "sha512-Lk5/8qqzPoqx9ygfHSYtqXFvXiWIY+1tnyoWHVbpyTlPrJOcfkFrELCXaDlokJJqg1luvTLoMtEA1pkDfHYVUg==", - "from": "react-native-google-places-autocomplete@git+https://github.com/Expensify/react-native-google-places-autocomplete.git#e12768f1542e7982d90f6449798f0d6b7f18f192", + "version": "git+ssh://git@github.com/Expensify/react-native-google-places-autocomplete.git#6f436a06a3018cb49750bb110b89df75f6a865d5", + "integrity": "sha512-7NiBK83VggJ2HQaHGfJoaPyxtiLu1chwP1VqH9te+PZtf0L9p50IuBQciW+4s173cBamt4U2+mvnCt7zfMFeDg==", + "from": "react-native-google-places-autocomplete@git+https://github.com/Expensify/react-native-google-places-autocomplete.git#6f436a06a3018cb49750bb110b89df75f6a865d5", "requires": { "lodash.debounce": "^4.0.8", "prop-types": "^15.7.2", diff --git a/package.json b/package.json index fb9bb52e81ad..3e5b745af106 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.5-6", + "version": "1.3.6-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -100,7 +100,7 @@ "react-native-document-picker": "^8.0.0", "react-native-fast-image": "git+https://github.com/Expensify/react-native-fast-image.git#afedf204bfc253d18f08fdcc5356a2bb82f6a87c", "react-native-gesture-handler": "2.9.0", - "react-native-google-places-autocomplete": "git+https://github.com/Expensify/react-native-google-places-autocomplete.git#e12768f1542e7982d90f6449798f0d6b7f18f192", + "react-native-google-places-autocomplete": "git+https://github.com/Expensify/react-native-google-places-autocomplete.git#6f436a06a3018cb49750bb110b89df75f6a865d5", "react-native-haptic-feedback": "^1.13.0", "react-native-image-pan-zoom": "^2.1.12", "react-native-image-picker": "^5.1.0", diff --git a/src/components/AddressSearch.js b/src/components/AddressSearch/index.js similarity index 92% rename from src/components/AddressSearch.js rename to src/components/AddressSearch/index.js index b72f430c599b..79022f0c9430 100644 --- a/src/components/AddressSearch.js +++ b/src/components/AddressSearch/index.js @@ -1,18 +1,19 @@ import _ from 'underscore'; -import React, {useState} from 'react'; +import React, {useRef, useState} from 'react'; import PropTypes from 'prop-types'; import {LogBox, ScrollView, View} from 'react-native'; import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete'; import lodashGet from 'lodash/get'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -import styles from '../styles/styles'; -import themeColors from '../styles/themes/default'; -import TextInput from './TextInput'; -import * as ApiUtils from '../libs/ApiUtils'; -import * as GooglePlacesUtils from '../libs/GooglePlacesUtils'; -import CONST from '../CONST'; -import * as StyleUtils from '../styles/StyleUtils'; -import variables from '../styles/variables'; +import withLocalize, {withLocalizePropTypes} from '../withLocalize'; +import styles from '../../styles/styles'; +import themeColors from '../../styles/themes/default'; +import TextInput from '../TextInput'; +import * as ApiUtils from '../../libs/ApiUtils'; +import * as GooglePlacesUtils from '../../libs/GooglePlacesUtils'; +import CONST from '../../CONST'; +import * as StyleUtils from '../../styles/StyleUtils'; +import resetDisplayListViewBorderOnBlur from './resetDisplayListViewBorderOnBlur'; +import variables from '../../styles/variables'; // The error that's being thrown below will be ignored until we fork the // react-native-google-places-autocomplete repo and replace the @@ -92,6 +93,7 @@ const defaultProps = { // Reference: https://github.com/FaridSafi/react-native-google-places-autocomplete/issues/609#issuecomment-886133839 const AddressSearch = (props) => { const [displayListViewBorder, setDisplayListViewBorder] = useState(false); + const containerRef = useRef(); const query = {language: props.preferredLocale, types: 'address'}; if (props.isLimitedToUSA) { query.components = 'country:us'; @@ -202,7 +204,7 @@ const AddressSearch = (props) => { // here: https://github.com/FaridSafi/react-native-google-places-autocomplete#use-inside-a-scrollview-or-flatlist keyboardShouldPersistTaps="always" > - + { defaultValue: props.defaultValue, inputID: props.inputID, shouldSaveDraft: props.shouldSaveDraft, - onBlur: props.onBlur, + onBlur: (event) => { + resetDisplayListViewBorderOnBlur(setDisplayListViewBorder, event, containerRef); + props.onBlur(); + }, autoComplete: 'off', onInputChange: (text) => { if (props.inputID) { @@ -274,6 +279,8 @@ const AddressSearch = (props) => { description: [styles.googleSearchText], separator: [styles.googleSearchSeparator], }} + numberOfLines={2} + isRowScrollable={false} listHoverColor={themeColors.border} listUnderlayColor={themeColors.buttonPressedBG} onLayout={(event) => { diff --git a/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.js b/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.js new file mode 100644 index 000000000000..a4ebdcf5d8c1 --- /dev/null +++ b/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.js @@ -0,0 +1,12 @@ +function resetDisplayListViewBorderOnBlur(setDisplayListViewBorder, event, containerRef) { + // The related target check is required here + // because without it when we select an option, the onBlur will still trigger setting displayListViewBorder to false + // it will make the auto complete component re-render before onPress is called making selecting an option not working. + if (containerRef.current && event.target && containerRef.current.contains(event.relatedTarget)) { + return; + } + setDisplayListViewBorder(false); +} + +export default resetDisplayListViewBorderOnBlur; + diff --git a/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.native.js b/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.native.js new file mode 100644 index 000000000000..e95f1f9c2550 --- /dev/null +++ b/src/components/AddressSearch/resetDisplayListViewBorderOnBlur.native.js @@ -0,0 +1,8 @@ +function resetDisplayListViewBorderOnBlur(setDisplayListViewBorder) { + // The related target check is not required here because in native there is no race condition rendering like on the web + // onPress still called when cliking the option + setDisplayListViewBorder(false); +} + +export default resetDisplayListViewBorderOnBlur; + diff --git a/src/components/CopyTextToClipboard.js b/src/components/CopyTextToClipboard.js index 620fb2f6f231..2dc2b0939538 100644 --- a/src/components/CopyTextToClipboard.js +++ b/src/components/CopyTextToClipboard.js @@ -1,12 +1,14 @@ import React from 'react'; +import {Pressable} from 'react-native'; import PropTypes from 'prop-types'; import Text from './Text'; import * as Expensicons from './Icon/Expensicons'; import Clipboard from '../libs/Clipboard'; +import getButtonState from '../libs/getButtonState'; import Icon from './Icon'; import Tooltip from './Tooltip'; import styles from '../styles/styles'; -import themeColors from '../styles/themes/default'; +import * as StyleUtils from '../styles/StyleUtils'; import variables from '../styles/variables'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; @@ -58,15 +60,19 @@ class CopyTextToClipboard extends React.Component { style={[styles.flexRow, styles.cursorPointer]} suppressHighlighting > - {this.props.text} + {`${this.props.text} `} - + + {({hovered, pressed}) => ( + + )} + ); diff --git a/src/components/OnyxProvider.js b/src/components/OnyxProvider.js index 17868890d672..9ddea6ad0738 100644 --- a/src/components/OnyxProvider.js +++ b/src/components/OnyxProvider.js @@ -6,7 +6,7 @@ import ComposeProviders from './ComposeProviders'; // Set up any providers for individual keys. This should only be used in cases where many components will subscribe to // the same key (e.g. FlatList renderItem components) -const [withNetwork, NetworkProvider] = createOnyxContext(ONYXKEYS.NETWORK); +const [withNetwork, NetworkProvider] = createOnyxContext(ONYXKEYS.NETWORK, {}); const [withPersonalDetails, PersonalDetailsProvider] = createOnyxContext(ONYXKEYS.PERSONAL_DETAILS); const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE); const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS); diff --git a/src/components/Reactions/ReportActionItemReactions.js b/src/components/Reactions/ReportActionItemReactions.js index 4ab90bee33ad..66198e3b7cf2 100644 --- a/src/components/Reactions/ReportActionItemReactions.js +++ b/src/components/Reactions/ReportActionItemReactions.js @@ -58,7 +58,7 @@ const ReportActionItemReactions = (props) => { const reactionsWithCount = _.filter(props.reactions, reaction => reaction.users.length > 0); return ( - + {_.map(reactionsWithCount, (reaction) => { const reactionCount = reaction.users.length; const reactionUsers = _.map(reaction.users, sender => sender.accountID.toString()); diff --git a/src/components/TextInput/baseTextInputPropTypes.js b/src/components/TextInput/baseTextInputPropTypes.js index 30169d995528..4e82f0984670 100644 --- a/src/components/TextInput/baseTextInputPropTypes.js +++ b/src/components/TextInput/baseTextInputPropTypes.js @@ -49,8 +49,7 @@ const propTypes = { /** Forward the inner ref */ innerRef: PropTypes.oneOfType([ PropTypes.func, - // eslint-disable-next-line react/forbid-prop-types - PropTypes.shape({current: PropTypes.any}), + PropTypes.object, ]), /** Maximum characters allowed */ diff --git a/src/components/ValidateCode/ExpiredValidateCodeModal.js b/src/components/ValidateCode/ExpiredValidateCodeModal.js index 1cdd3c8bd1c8..29e1eeae63eb 100644 --- a/src/components/ValidateCode/ExpiredValidateCodeModal.js +++ b/src/components/ValidateCode/ExpiredValidateCodeModal.js @@ -105,7 +105,7 @@ class ExpiredValidateCodeModal extends PureComponent {

- {codeRequestedMessage} + {this.props.translate(codeRequestedMessage)} )} diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js index 64119c475de7..3d56924125e9 100755 --- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js +++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js @@ -131,15 +131,17 @@ class BaseVideoChatButtonAndMenu extends Component { top: this.state.videoChatIconPosition.y + 40, }} > - {_.map(this.menuItemData, ({icon, text, onPress}) => ( - - ))} + + {_.map(this.menuItemData, ({icon, text, onPress}) => ( + + ))} + ); diff --git a/src/components/createOnyxContext.js b/src/components/createOnyxContext.js index a64d17e9c55b..c8452eb9ee83 100644 --- a/src/components/createOnyxContext.js +++ b/src/components/createOnyxContext.js @@ -9,7 +9,7 @@ const propTypes = { children: PropTypes.node.isRequired, }; -export default (onyxKeyName) => { +export default (onyxKeyName, defaultValue) => { const Context = createContext(); const Provider = props => ( @@ -35,6 +35,10 @@ export default (onyxKeyName) => { ...props, [propName]: transformValue ? transformValue(value, props) : value, }; + + if (propsToPass[propName] === undefined && defaultValue) { + propsToPass[propName] = defaultValue; + } return ( // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/languages/en.js b/src/languages/en.js index a3dd82bd7e0a..e323fb2f897e 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -148,7 +148,7 @@ export default { attachmentTooSmall: 'Attachment too small', sizeNotMet: 'Attachment size must be greater than 240 bytes.', wrongFileType: 'Attachment is the wrong type', - notAllowedExtension: 'Attachments must be one of the following types: ', + notAllowedExtension: 'Attachments must be one of the following types:', }, avatarCropModal: { title: 'Edit photo', diff --git a/src/languages/es.js b/src/languages/es.js index 6407ffabea8d..0e8c21b50332 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -147,7 +147,7 @@ export default { attachmentTooSmall: 'Archivo adjunto demasiado pequeño', sizeNotMet: 'El archivo adjunto debe ser mas grande que 240 bytes.', wrongFileType: 'El tipo del archivo adjunto es incorrecto', - notAllowedExtension: 'Los archivos adjuntos deben ser de uno de los siguientes tipos: ', + notAllowedExtension: 'Los archivos adjuntos deben ser de uno de los siguientes tipos:', }, avatarCropModal: { title: 'Editar foto', diff --git a/src/libs/Visibility/index.js b/src/libs/Visibility/index.js index c86d4a1e6965..2d6c7d2f0906 100644 --- a/src/libs/Visibility/index.js +++ b/src/libs/Visibility/index.js @@ -6,7 +6,7 @@ import {AppState} from 'react-native'; * @returns {Boolean} */ function isVisible() { - return document.visibilityState === 'visible' && document.hasFocus(); + return document.visibilityState === 'visible'; } /** diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 43516ae02370..916733dd3a8f 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -395,7 +395,6 @@ function validateSecondaryLogin(contactMethod, validateCode) { partnerUserID: contactMethod, validateCode, }, {optimisticData, successData, failureData}); - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); } /** diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js index e562a417d423..1da859e123c0 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js @@ -45,7 +45,7 @@ const defaultProps = { class BaseReportActionContextMenu extends React.Component { constructor(props) { super(props); - this.wrapperStyle = getReportActionContextMenuStyles(this.props.isMini); + this.wrapperStyle = getReportActionContextMenuStyles(this.props.isMini, this.props.isSmallScreenWidth); this.state = { shouldKeepOpen: false, diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index d934a448941a..277b86f1d611 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -97,6 +97,17 @@ class ContactMethodDetailsPage extends Component { }; } + componentDidUpdate(prevProps) { + const errorFields = lodashGet(this.props.loginList, [this.getContactMethod(), 'errorFields'], {}); + const prevPendingFields = lodashGet(prevProps.loginList, [this.getContactMethod(), 'pendingFields'], {}); + + // Navigate to methods page on successful magic code verification + // validateLogin property of errorFields & prev pendingFields is responsible to decide the status of the magic code verification + if (!errorFields.validateLogin && prevPendingFields.validateLogin === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE) { + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); + } + } + /** * Gets the current contact method from the route params * diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js index 4cfd8a721a15..0c59cb17ba4e 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js @@ -1,4 +1,6 @@ -import React, {Component} from 'react'; +import React, { + useCallback, useMemo, useRef, useState, +} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import {ScrollView} from 'react-native-gesture-handler'; @@ -52,102 +54,87 @@ const defaultProps = { loginList: {}, }; -class NewContactMethodPage extends Component { - constructor(props) { - super(props); - - this.state = { - login: '', - password: '', - }; - this.onLoginChange = this.onLoginChange.bind(this); - this.validateForm = this.validateForm.bind(this); - this.submitForm = this.submitForm.bind(this); - } - - onLoginChange(login) { - this.setState({login}); - } - - /** - * Determine whether the form is valid - * - * @returns {Boolean} - */ - validateForm() { - const login = this.state.login.trim(); +function NewContactMethodPage(props) { + const [login, setLogin] = useState(''); + const [password, setPassword] = useState(''); + const loginInputRef = useRef(null); + + const handleLoginChange = useCallback((value) => { + setLogin(value.trim()); + }, []); + + const handlePasswordChange = useCallback((value) => { + setPassword(value.trim()); + }, []); + + const isFormValid = useMemo(() => { const phoneLogin = LoginUtils.getPhoneNumberWithoutSpecialChars(login); - return (Permissions.canUsePasswordlessLogins(this.props.betas) || this.state.password) + return (Permissions.canUsePasswordlessLogins(props.betas) || password) && (Str.isValidEmail(login) || Str.isValidPhone(phoneLogin)); - } - - submitForm() { - // Trim leading and trailing space from login - const login = this.state.login.trim(); + }, [login, password, props.betas]); + const submitForm = useCallback(() => { // If this login already exists, just go back. - if (lodashGet(this.props.loginList, login)) { + if (lodashGet(props.loginList, login)) { Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); return; } - User.addNewContactMethodAndNavigate(login, this.state.password); - } - - render() { - return ( - { - if (!this.loginInputRef) { - return; - } - this.loginInputRef.focus(); - }} - > - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS)} - onCloseButtonPress={() => Navigation.dismissModal(true)} - /> - - - {this.props.translate('common.pleaseEnterEmailOrPhoneNumber')} - - - this.loginInputRef = el} - value={this.state.login} - onChangeText={this.onLoginChange} - autoCapitalize="none" - returnKeyType={Permissions.canUsePasswordlessLogins(this.props.betas) ? 'done' : 'next'} - /> - - {!Permissions.canUsePasswordlessLogins(this.props.betas) - && ( - - this.setState({password})} - returnKeyType="done" - /> - - )} - - -