diff --git a/.github/actions/composite/setupGitForOSBotifyApp/action.yml b/.github/actions/composite/setupGitForOSBotifyApp/action.yml index 7a90cc45257d..a6c487705c56 100644 --- a/.github/actions/composite/setupGitForOSBotifyApp/action.yml +++ b/.github/actions/composite/setupGitForOSBotifyApp/action.yml @@ -60,6 +60,11 @@ runs: if: runner.debug == '1' run: echo "GIT_TRACE=true" >> "$GITHUB_ENV" + - name: Sync clock + shell: bash + run: sudo sntp -sS time.windows.com + if: runner.os == 'macOS' + - name: Generate a token id: generateToken uses: actions/create-github-app-token@9d97a4282b2c51a2f4f0465b9326399f53c890d4 diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index a58745b742ad..c49530c46faa 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -17,6 +17,11 @@ jobs: - name: Setup NodeJS uses: ./.github/actions/composite/setupNode + - name: Set dummy git credentials + run: | + git config --global user.email "test@test.com" + git config --global user.name "Test" + - name: Run performance testing script shell: bash run: | @@ -27,6 +32,7 @@ jobs: npm install --force npx reassure --baseline git switch --force --detach - + git merge --no-commit --allow-unrelated-histories "$BASELINE_BRANCH" -X ours npm install --force npx reassure --branch diff --git a/android/app/build.gradle b/android/app/build.gradle index f58e92f66ae8..b8205a21d925 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -91,8 +91,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001041001 - versionName "1.4.10-1" + versionCode 1001041122 + versionName "1.4.11-22" } flavorDimensions "default" diff --git a/assets/images/thumbs-up.svg b/assets/images/thumbs-up.svg new file mode 100644 index 000000000000..ef81c88fc854 --- /dev/null +++ b/assets/images/thumbs-up.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 0ff280c4b9c6..bfeb58ceec05 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -10,7 +10,7 @@ "electron-context-menu": "^2.3.0", "electron-log": "^4.4.7", "electron-serve": "^1.0.0", - "electron-updater": "^6.1.4", + "electron-updater": "^6.1.6", "node-machine-id": "^1.1.12" } }, @@ -50,9 +50,9 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz", - "integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.2.tgz", + "integrity": "sha512-Or2/ycVYRGQ876hKMfiz2Ghgzh3WllgPW75jqt1Ta2a5wprpnziFrHpQ9eUq6/ScsVXMnG4PmQqlMsE9NFg8DQ==", "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -150,11 +150,11 @@ "integrity": "sha512-tQJBCbXKoKCfkBC143QCqnEtT1s8dNE2V+b/82NF6lxnGO/2Q3a3GSLHtKl3iEDQgdzTf9pH7p418xq2rXbz1Q==" }, "node_modules/electron-updater": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.4.tgz", - "integrity": "sha512-yYAJc6RQjjV4WtInZVn+ZcLyXRhbVXoomKEfUUwDqIk5s2wxzLhWaor7lrNgxODyODhipjg4SVPMhJHi5EnsCA==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.6.tgz", + "integrity": "sha512-G2bO72i7kv+bVdBAjq6lQn8zkZ3wMRRjxBD4TGBjB77UiuMDUBeP45YAs4y08udPzttGW2qzpnbOUJY5RYWZfw==", "dependencies": { - "builder-util-runtime": "9.2.1", + "builder-util-runtime": "9.2.2", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", @@ -461,9 +461,9 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==" }, "builder-util-runtime": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz", - "integrity": "sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.2.tgz", + "integrity": "sha512-Or2/ycVYRGQ876hKMfiz2Ghgzh3WllgPW75jqt1Ta2a5wprpnziFrHpQ9eUq6/ScsVXMnG4PmQqlMsE9NFg8DQ==", "requires": { "debug": "^4.3.4", "sax": "^1.2.4" @@ -535,11 +535,11 @@ "integrity": "sha512-tQJBCbXKoKCfkBC143QCqnEtT1s8dNE2V+b/82NF6lxnGO/2Q3a3GSLHtKl3iEDQgdzTf9pH7p418xq2rXbz1Q==" }, "electron-updater": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.4.tgz", - "integrity": "sha512-yYAJc6RQjjV4WtInZVn+ZcLyXRhbVXoomKEfUUwDqIk5s2wxzLhWaor7lrNgxODyODhipjg4SVPMhJHi5EnsCA==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.6.tgz", + "integrity": "sha512-G2bO72i7kv+bVdBAjq6lQn8zkZ3wMRRjxBD4TGBjB77UiuMDUBeP45YAs4y08udPzttGW2qzpnbOUJY5RYWZfw==", "requires": { - "builder-util-runtime": "9.2.1", + "builder-util-runtime": "9.2.2", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", diff --git a/desktop/package.json b/desktop/package.json index bf49d93f1a7b..a6b92bde81c4 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -7,7 +7,7 @@ "electron-context-menu": "^2.3.0", "electron-log": "^4.4.7", "electron-serve": "^1.0.0", - "electron-updater": "^6.1.4", + "electron-updater": "^6.1.6", "node-machine-id": "^1.1.12" }, "author": "Expensify, Inc.", diff --git a/docs/_includes/footer.html b/docs/_includes/footer.html index d18ca2199e33..798fb2cf7e96 100644 --- a/docs/_includes/footer.html +++ b/docs/_includes/footer.html @@ -111,6 +111,8 @@

Get Started

+ + diff --git a/docs/_sass/_colors.scss b/docs/_sass/_colors.scss index 5556c43d87f6..f0c89d31c580 100644 --- a/docs/_sass/_colors.scss +++ b/docs/_sass/_colors.scss @@ -1,14 +1,36 @@ -$color-green400: #03D47C; -$color-green-icons: #8B9C8F; -$color-green-borders: #1A3D32; -$color-button-background: #1A3D32; -$color-button-hovered: #2A604F; -$color-green-highlightBG: #072419; -$color-green-highlightBG-hover: #06231c; -$color-green-appBG: #061B09; -$color-green-hover: #00a862; -$color-light-gray-green: #AFBBB0; -$color-blue300: #5AB0FF; +// Product Color Spectrum +$color-product-dark-100: #061B09; +$color-product-dark-200: #072419; +$color-product-dark-300: #0A2E25; +$color-product-dark-400: #1A3D32; +$color-product-dark-500: #224F41; +$color-product-dark-600: #2A604F; +$color-product-dark-700: #8B9C8F; +$color-product-dark-800: #AFBBB0; +$color-product-dark-900: #E7ECE9; + +// Colors for Links and Success $color-blue200: #B0D9FF; -$color-white: #E7ECE9; -$color-gray-label: #afbbb0; +$color-blue300: #5AB0FF; +$color-green400: #03D47C; +$color-green500: #00a862; + +// Overlay BG color +$color-overlay-background: rgba(26, 61, 50, 0.72); + +// UI Colors +$color-text: $color-product-dark-900; +$color-text-supporting: $color-product-dark-800; +$color-icons: $color-product-dark-700; +$color-borders: $color-product-dark-400; +$color-highlightBG: $color-product-dark-200; +$color-row-hover: $color-product-dark-300; +$color-appBG: $color-product-dark-100; +$color-success: $color-green400; +$color-accent : $color-green400; +$color-link: $color-blue300; +$color-link-hovered: $color-blue200; +$color-button-background: $color-product-dark-400; +$color-button-background-hover: $color-product-dark-500; +$color-button-success-background: $color-green400; +$color-button-success-background-hover: $color-green500; diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index 7a0804b0f962..eaaa1c63badb 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -3,19 +3,6 @@ @import 'fonts'; @import 'search-bar'; -$color-appBG: $color-green-appBG; -$color-highlightBG: $color-green-highlightBG; -$color-accent : $color-green400; -$color-borders: $color-green-borders; -$color-icons: $color-green-icons; -$color-text: $color-white; -$color-link: $color-blue300; -$color-link-hovered: $color-blue200; -$color-success: $color-green400; -$color-text-supporting: $color-light-gray-green; -$color-green-hover: $color-green-hover; -$color-gray-label: $color-gray-label; - * { margin: 0; padding: 0; @@ -78,6 +65,7 @@ body { height: 100%; min-height: 100%; background: $color-appBG; + color: $color-text-supporting; } hr { @@ -148,7 +136,7 @@ textarea { font-weight: 400; font-family: "ExpensifyNeue", "Helvetica Neue", "Helvetica", Arial, sans-serif; font-size: 16px; - color: $color-text; + color: $color-text-supporting; } button { @@ -159,7 +147,7 @@ button { font-weight: bold; &.success { - background-color: $color-success; + background-color: $color-button-success-background; color: $color-text; width: 100%; border-radius: 100px; @@ -167,7 +155,7 @@ button { padding-right: 20px; &:hover { - background-color: desaturate($color-success, 15%); + background-color: $color-button-success-background-hover; cursor: pointer; } @@ -262,6 +250,7 @@ button { .lhn-header { padding: 24px; + @include breakpoint($breakpoint-tablet) { padding: 44px; } @@ -269,6 +258,7 @@ button { #header-button { display: block; padding-right: 24px; + @include breakpoint($breakpoint-tablet) { display: none; } @@ -282,7 +272,7 @@ button { margin-right: auto; @include breakpoint($breakpoint-desktop) { - width: 210px; + width: 180px; align-content: normal; display: flex; margin-left: 0; @@ -372,7 +362,7 @@ button { // Box shadow is used here because border-radius and border-collapse don't work together. It leads to double borders. // https://stackoverflow.com/questions/628301/the-border-radius-property-and-border-collapsecollapse-dont-mix-how-can-i-use border-style: hidden; - box-shadow: 0 0 0 1px $color-green-borders; + box-shadow: 0 0 0 1px $color-borders; } th:first-child { @@ -394,12 +384,12 @@ button { th, td { padding: 6px 13px; - border: 1px solid $color-green-borders; + border: 1px solid $color-borders; } thead tr th { font-weight: bold; - background-color: $color-green-highlightBG; + background-color: $color-highlightBG; } .img-wrap { @@ -457,11 +447,11 @@ button { .link { display: inline; - color: $color-link; + color: $color-text-supporting; cursor: pointer; &:hover { - color: $color-link-hovered; + color: $color-link; } } @@ -530,7 +520,7 @@ button { background-color: $color-highlightBG; &:hover { - background-color: darken($color-highlightBG, 1%); + background-color: $color-row-hover; } .row { @@ -629,13 +619,14 @@ button { } p.description { + color: $color-text-supporting; padding: 20px 0 20px 0; } p.url { padding: 0; font-size: 0.8em; - color: $color-gray-label; + color: $color-text-supporting; } } @@ -739,7 +730,7 @@ button { .get-help { flex-wrap: wrap; - margin-top: auto; + margin-top: 40px; } .floating-concierge-button { @@ -773,9 +764,12 @@ button { h3 { color: $color-success; + font-family: "ExpensifyNewKansas", "Helvetica Neue", "Helvetica", Arial, sans-serif; font-size: 17px; - font-weight: 700; + font-weight: 500; + padding: 0; margin-bottom: 16px; + margin-top: 0; } ul { @@ -787,13 +781,13 @@ button { margin: 0 0 8px; a { - color: $color-text; + color: $color-text-supporting; display: block; padding: 4px 0; word-break: break-word; &:hover { - color: $color-success; + color: $color-link; } } } @@ -803,11 +797,11 @@ button { padding-bottom: 20px; a { - color: $color-text; + color: $color-text-supporting; display: inline-block; &:hover { - color: $color-success; + color: $color-link; } } } diff --git a/docs/_sass/_search-bar.scss b/docs/_sass/_search-bar.scss index c2185ef8f36a..f414d25fc266 100644 --- a/docs/_sass/_search-bar.scss +++ b/docs/_sass/_search-bar.scss @@ -2,20 +2,6 @@ @import 'colors'; @import 'fonts'; -$color-appBG: $color-green-appBG; -$color-highlightBG: $color-green-highlightBG; -$color-highlightBG-hover: $color-green-highlightBG-hover; -$color-accent : $color-green400; -$color-borders: $color-green-borders; -$color-icons: $color-green-icons; -$color-text: $color-white; -$color-link: $color-blue300; -$color-link-hovered: $color-blue200; -$color-success: $color-green400; -$color-text-supporting: $color-light-gray-green; -$color-green-hover: $color-green-hover; -$color-gray-label: $color-gray-label; - .search-icon { margin: auto 0px; } @@ -81,7 +67,7 @@ $color-gray-label: $color-gray-label; left: 0; right: 0; bottom: 0; - background-color: rgba(0, 0, 0, 0.4); + background-color: $color-overlay-background; z-index: 1; } @@ -158,7 +144,7 @@ label.search-label { transform: translateY(-50%); left: 20px; pointer-events: none; - color: $color-gray-label; + color: $color-text-supporting; transform-origin: left top; user-select: none; transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1), color 150ms cubic-bezier(0.4, 0, 0.2, 1), top 500ms; @@ -194,14 +180,14 @@ label.search-label { margin-left: 15px; margin-right: 20px; border-radius: 25px; - background-color: $color-green400; + background-color: $color-button-success-background; cursor: pointer; width: 40px; height: 40px; } .gsc-search-button.gsc-search-button-v2:hover { - background-color: $color-green-hover; + background-color: $color-button-success-background-hover; } .gsc-search-button.gsc-search-button-v2 svg { @@ -227,7 +213,7 @@ label.search-label { /* Change Font result Paragraph color */ .gsc-results .gs-webResult:not(.gs-no-results-result):not(.gs-error-result) .gs-snippet, .gs-fileFormatType { - color: $color-text; + color: $color-text-supporting; } @@ -278,7 +264,7 @@ label.search-label { color: $color-text; &:hover { - background-color: $color-button-hovered; + background-color: $color-button-background-hover; text-decoration: none; } } diff --git a/docs/articles/expensify-classic/account-settings/Profile-Settings.md b/docs/articles/expensify-classic/account-settings/Profile-Settings.md deleted file mode 100644 index 3b2a0b830926..000000000000 --- a/docs/articles/expensify-classic/account-settings/Profile-Settings.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Profile Settings -description: Profile Settings ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Cards.md b/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Cards.md deleted file mode 100644 index 71edcdeba00d..000000000000 --- a/docs/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Cards.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Personal Cards -description: Connect your credit card directly to Expensify to easily track your personal finances. ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/getting-started/Referral-Program.md b/docs/articles/expensify-classic/get-paid-back/Referral-Program.md similarity index 100% rename from docs/articles/expensify-classic/getting-started/Referral-Program.md rename to docs/articles/expensify-classic/get-paid-back/Referral-Program.md diff --git a/docs/articles/expensify-classic/getting-started/Mobile-App.md b/docs/articles/expensify-classic/getting-started/Mobile-App.md deleted file mode 100644 index 7fa57abbdf61..000000000000 --- a/docs/articles/expensify-classic/getting-started/Mobile-App.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Using the App -description: Using the App ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md index 3ee1c8656b4b..cf2f0f59a4a0 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Domains-Overview.md @@ -1,5 +1,140 @@ --- -title: Coming Soon -description: Coming Soon +title: Domains +description: Want to gain greater control over your company settings in Expensify? Read on to find out more about our Domains feature and how it can help you save time and effort when managing your company expenses. --- -## Resource Coming Soon! + +# Overview +Domains is a feature in Expensify that allows admins to have more nuanced control over a specific Expensify activity, as well as providing a bird’s eye view of company card expenditure. Think of it as your command center for things like managing user account access, enforcing stricter Workspace rules for certain groups, or issuing cards and reconciling statements. +There are several settings within Domains that you can configure so that you have more control and visibility into your organization’s settings. Those features are: +- Company Cards +- Domain Admins +- Domain Members + - Two-Factor Authentication +- Domain Groups + - Domain Group Settings +- Reporting Tools +- SAML + +There are two ways to use Domains – as an unverified domain or a verified domain. An unverified domain allows you to import Company Cards and manage them, whereas a verified domain allows you to do that in addition to: +1. Receive vendor bills in Expensify +2. Fine-tune user restrictions using domain Groups +3. Configure SAML SSO for easier login to Expensify +4. Set vacation delegates for your domain members +5. Use consolidated domain billing + +# How to claim a domain +To use the domains feature with an unverified domain, you’ll need to claim the domain first. +To claim a domain, you need to be a Workspace Admin with a company email address. This allows you to manage company bills, company cards, and reconciliation. Claiming requires an email matching your company's domain. +1. Create an Expensify account +2. Set up an expense Workspace +3. Go to **Settings > _Domains_**. +Whichever member runs through those steps will automatically be made a Domain Admin. + + +# How to verify a domain +To use the domains feature with a verified domain, you’ll want to go through the steps of verifying it. + +To verify domain ownership, follow these steps: +1. Log in to your DNS service provider, which could be your Domain Name Registrar like NameCheap or GoDaddy, a dedicated DNS service provider like DNSMadeEasy or Amazon Route53, or managed internally by your company's IT department. +2. Find the page for editing DNS records for expensify.com. This might be labeled as DNS Management or Zone File Editor. +3. Add a new TXT record and set the value as: **532F6180D8** +4. Save your changes +5. Click the Verify button to confirm domain ownership + +After successful verification, you can remove the TXT DNS record. Please note that an email will be sent to all Expensify users on the domain to inform them that their accounts will be under Domain Control after verification. + +**Tips:** +Not sure how to do this? Check the below guides from some of the most popular hosts on the web: +[123-reg.co.uk](https://www.123-reg.co.uk/) +[One.com](https://www.one.com/en/) +[Wix.com](https://www.wix.com/) +Google/GSuite +[Godaddy](https://www.godaddy.com/) +When creating the TXT record, input only the code and no other values or information. +You can always confirm if you added the TXT code correctly here: https://viewdns.info/dnsrecord/?domain=[enterdomainhere] + +# Domain settings + +## Domain Admins +Domain Admins have full authority over domain settings. They can modify member group names and rules, link or modify Company Cards, and add or remove domain members and other admins. + +### Adding a Domain Admin +1. Head to **Settings > Domains > [Domain Name] > Domain Admins** +2. In the "Email or Phone" field, type in the email address of the person you want to make a Domain Admin (this can be any email not specifically tied to the domain) +3. Click "Add Admin" + +### Removing a Domain Admin: +1. If you're already a Domain Admin, go to **Settings > Domains > [Domain Name] > Domain Admins** +2. Locate the list of Domain Admins and find the one you want to remove +3. Next to the Domain Admin's name, click the red trash can icon. This will remove that person from the Domain Admin role + +## Domain Members +A domain member is a user associated with a specific domain (usually a company or another group) in Expensify and typically managed by a Domain Admin. This is also where you can enable Two-Factor authentication for your domain. + +### Adding users to the domain +When a Domain Admin adds a user to the domain, that will create a new Expensify account for that user, and they'll receive invitations to set up their account. Users can also join a verified domain by creating their own account, as long as they have an email address associated with that domain (e.g. yourname@yourcompany.com). Once they have verified the account, all Domain Admins will be notified, and the employee will be added to the Default Group. +**Important Note:** If someone who isn't a Domain Admin invites a user to a Workspace before they're invited to the domain, their account will be created, but in a closed state. A closed state means that the account cannot be used until it has been validated. Once the Domain Admin has invited the user, the user will receive a magic link to verify their account, sign in, and open the account completely. + +### How to add users +1. In your web account, go to **Settings > Domains > [Domain Name] > Domain Admins** +2. In the email field, enter the user you want to invite. This will create their Expensify account and send them an invitation + +### Removing users from the Domain +Removing a user means taking them out of your domain and closing their Expensify account completely if they don't have another login. Be cautious because closing an account is permanent and deletes any unsubmitted or processing reports. + +### How to remove users +In your web account, go to **Settings > Domains > [Domain Name] > Domain Admins** +Check the box next to the employee's name you want to remove, then click “Close Accounts”. + +### Important notes about closing accounts through Domain settings: +If a user has a Secondary Login linked to their Expensify account, they can still access their account after it's closed in the domain. This is helpful for accessing financial data, like tax-related receipts. +Closing an account through the domain permanently removes any unsubmitted receipts/reports. Make sure to approve or reimburse all employee reports before closing an account. +If an employee doesn't have a Secondary Login, they'll be automatically removed from the group Workspace. If they have a Secondary Login, it will continue to be associated with the group Workspace. + +## Domain Groups +Domain Groups can be accessed if you have verified your domain. Groups are used to set rules or permissions for groups of users so you can enforce multiple different expense workspaces and rules. If you are a Domain Admin, you can create and edit Domain Groups under **Settings > Domains > _Domain Name_ > Groups**. + +### Creating Domain Groups +1. In your Expensify account on the web, navigate to **Settings > Domains > _Domain Name_ > Groups** +2. Select “Create Group” to create the group. This will allow you to name the Group, as well as configure permissions that will apply to members of the Group. + +### Adding members to a Domain Group +1. In your Expensify account on the web, navigate to **Settings > Domains > [Domain Name] > Domain Members** +2. Select the checkbox next to the domain members you wish to add to the Domain Group +3. Select “Add to Group” to select the Group you wish to add them to + +### Editing Domain Groups +1. In your Expensify account on the web, navigate to **Settings > Domains > _Domain Name_ > Groups** +2. Next to the Group you wish to edit, select “Edit” +3. This will open the Edit Permission Group pane, where you can edit the rules and permissions for that group +4. Make your edits and click “Save” + +## Domain Group settings +These are the settings that can be customized for each group you have created. Typically, companies use two groups (Employees and Managers) and enforce stricter rules for Employees. The settings are: +- Strict Workspace Enforcement: When enabled, all Workspace rules must be followed for a report to be submitted. If a rule is violated, the report can't be submitted until the issue is fixed. Employees can't bypass this by dismissing notifications. +- Login Restrictions: Enabling this prevents users from using non-company email addresses as their primary login. Secondary logins are still allowed. +- Workspace Creation and Removal Restrictions: This feature stops users from creating new group workspaces or unsubscribing from existing workspaces. Admins who need these abilities should be in a separate group with this restriction turned off. +- Preferred Workspace: When enabled, group members can only create reports under one designated Workspace. They can move a report to a different Workspace or their personal one later if needed. This helps keep personal and company expenses separate. If a company card uses a specific Workspace, this setting overrides it for more control over company card expenses. +- Setting a Preferred Workspace: If Preferred Workspace is on, you can choose a default group Workspace for all Group Members. + +## SAML +To enable SAML SSO in Expensify you will first need to claim and verify your domain. Once you have a verified domain, you can access SAML SSO by navigating to **Settings > Domains > _Domain Name_ > SAML** + +## Enable Two-Factor Authentication (2FA) +1. As a Domain Admin, head to: **Settings > Domains > _Your Domain Name_ > Domain Members** +2. Turn on Two Factor Authentication by toggling it to ENABLED +3. Any Domain members that do not have two-factor authentication enabled will be asked to set it up on their Home page when they next log in, and won't be able to use Expensify until they do. +4. To turn it off, simply toggle it off and refresh the page. + +**Tips:** +- When using SAML, two-factor authentication cannot be required. +- For disputing digital Expensify Card purchases, two-factor authentication must be enabled. +- It might take up to 2 hours for domain-level enforcement to take effect, and users will be prompted to configure their individual 2FA settings on their next login to Expensify. + +# FAQ + +## How many domains can I have? +You can manage multiple domains by adding them through **Settings > Domains > New Domain**. However, to verify additional domains, you must be a Workspace Admin on a Control Workspace. Keep in mind that the Collect plan allows verification for just one domain. + +## What’s the difference between claiming a domain and verifying a domain? +Claiming a domain is limited to users with matching email domains, and allows Workspace Admins with a company email to manage bills, company cards, and reconciliation. Verifying a domain offers extra features and security. diff --git a/docs/articles/new-expensify/billing-and-plan-types/Referral-Program.md b/docs/articles/new-expensify/get-paid-back/Referral-Program.md similarity index 100% rename from docs/articles/new-expensify/billing-and-plan-types/Referral-Program.md rename to docs/articles/new-expensify/get-paid-back/Referral-Program.md diff --git a/docs/articles/new-expensify/get-paid-back/Request-Money.md b/docs/articles/new-expensify/get-paid-back/Request-Money.md index a2b765915af0..43a72a075de7 100644 --- a/docs/articles/new-expensify/get-paid-back/Request-Money.md +++ b/docs/articles/new-expensify/get-paid-back/Request-Money.md @@ -1,6 +1,36 @@ --- -title: Request Money -description: Request Money +title: Request Money and Split Bills with Friends +description: Everything you need to know about Requesting Money and Splitting Bills with Friends! redirect_from: articles/request-money/Request-and-Split-Bills/ --- -## Resource Coming Soon! + + + +# How do these Payment Features work? +Our suite of money movement features enables you to request money owed by an individual or split a bill with a group. + +**Request Money** lets your friends pay you back directly in Expensify. When you send a payment request to a friend, Expensify will display the amount owed and the option to pay the corresponding request in a chat between you. + +**Split Bill** allows you to split payments between friends and ensures the person who settled the tab gets paid back. + +These two features ensure you can live in the moment and settle up afterward. + +# How to Request Money +- Select the Green **+** button and choose **Request Money** +- Enter the amount **$** they owe and click **Next** +- Search for the user or enter their email! +- Enter a reason for the request (optional) +- Click **Request!** +- If you change your mind, all you have to do is click **Cancel** +- The user will be able to **Settle up outside of Expensify** or pay you via **Venmo** or **PayPal.me** + +# How to Split a Bill +- Select the Green **+** button and choose **Split Bill** +- Enter the total amount for the bill and click **Next** +- Search for users or enter their emails and **Select** +- Enter a reason for the split +- The split is then shared equally between the attendees + +# FAQs +## Can I request money from more than one person at a time? +If you need to request money for more than one person at a time, you’ll want to use the Split Bill feature. The Request Money option is for one-to-one payments between two people. diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index a97ae1101937..29d2bcc0e386 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.10 + 1.4.11 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.10.1 + 1.4.11.22 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 7eabdca438bc..7728e7e588b2 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.10 + 1.4.11 CFBundleSignature ???? CFBundleVersion - 1.4.10.1 + 1.4.11.22 diff --git a/package-lock.json b/package-lock.json index 6d125e2f6b27..3354917d363f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.10-1", + "version": "1.4.11-22", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.10-1", + "version": "1.4.11-22", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -50,7 +50,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#ee14b3255da33d2b6924c357f43393251b6dc6d2", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#927c8409e4454e15a1b95ed0a312ff8fee38f0f0", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -29894,8 +29894,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#ee14b3255da33d2b6924c357f43393251b6dc6d2", - "integrity": "sha512-u5Is3a/jD9KJ/LtSofeevauDlxZX5++w+VENP2cNhT1lm1GSwRM5FlTT7bIVSyrHr0cppAgl7cLiW2aPDr2hKA==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#927c8409e4454e15a1b95ed0a312ff8fee38f0f0", + "integrity": "sha512-s9l/Zy3UjDBrq0WTkgEue1DXLRkkYtuqnANQlVmODHJ9HkJADjrVSv2D0U3ltqd9X7vLCLCmmwl5AUE6466gGg==", "license": "MIT", "dependencies": { "classnames": "2.3.1", @@ -74403,9 +74403,9 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#ee14b3255da33d2b6924c357f43393251b6dc6d2", - "integrity": "sha512-u5Is3a/jD9KJ/LtSofeevauDlxZX5++w+VENP2cNhT1lm1GSwRM5FlTT7bIVSyrHr0cppAgl7cLiW2aPDr2hKA==", - "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#ee14b3255da33d2b6924c357f43393251b6dc6d2", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#927c8409e4454e15a1b95ed0a312ff8fee38f0f0", + "integrity": "sha512-s9l/Zy3UjDBrq0WTkgEue1DXLRkkYtuqnANQlVmODHJ9HkJADjrVSv2D0U3ltqd9X7vLCLCmmwl5AUE6466gGg==", + "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#927c8409e4454e15a1b95ed0a312ff8fee38f0f0", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", diff --git a/package.json b/package.json index dc1964eac407..daac5b27740c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.10-1", + "version": "1.4.11-22", "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.", @@ -98,7 +98,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#ee14b3255da33d2b6924c357f43393251b6dc6d2", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#927c8409e4454e15a1b95ed0a312ff8fee38f0f0", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", diff --git a/src/CONST.ts b/src/CONST.ts index 6f1fe37f661d..072f780b54ae 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -55,6 +55,9 @@ const CONST = { ALLOWED_RECEIPT_EXTENSIONS: ['jpg', 'jpeg', 'gif', 'png', 'pdf', 'htm', 'html', 'text', 'rtf', 'doc', 'tif', 'tiff', 'msword', 'zip', 'xml', 'message'], }, + // This is limit set on servers, do not update without wider internal discussion + API_TRANSACTION_CATEGORY_MAX_LENGTH: 255, + AUTO_AUTH_STATE: { NOT_STARTED: 'not-started', SIGNING_IN: 'signing-in', @@ -508,6 +511,7 @@ const CONST = { TASKREOPENED: 'TASKREOPENED', POLICYCHANGELOG: { ADD_APPROVER_RULE: 'POLICYCHANGELOG_ADD_APPROVER_RULE', + ADD_BUDGET: 'POLICYCHANGELOG_ADD_BUDGET', ADD_CATEGORY: 'POLICYCHANGELOG_ADD_CATEGORY', ADD_CUSTOM_UNIT: 'POLICYCHANGELOG_ADD_CUSTOM_UNIT', ADD_CUSTOM_UNIT_RATE: 'POLICYCHANGELOG_ADD_CUSTOM_UNIT_RATE', @@ -517,6 +521,7 @@ const CONST = { ADD_TAG: 'POLICYCHANGELOG_ADD_TAG', DELETE_ALL_TAGS: 'POLICYCHANGELOG_DELETE_ALL_TAGS', DELETE_APPROVER_RULE: 'POLICYCHANGELOG_DELETE_APPROVER_RULE', + DELETE_BUDGET: 'POLICYCHANGELOG_DELETE_BUDGET', DELETE_CATEGORY: 'POLICYCHANGELOG_DELETE_CATEGORY', DELETE_CUSTOM_UNIT: 'POLICYCHANGELOG_DELETE_CUSTOM_UNIT', DELETE_CUSTOM_UNIT_RATE: 'POLICYCHANGELOG_DELETE_CUSTOM_UNIT_RATE', @@ -538,6 +543,7 @@ const CONST = { UPDATE_AUTOHARVESTING: 'POLICYCHANGELOG_UPDATE_AUTOHARVESTING', UPDATE_AUTOREIMBURSEMENT: 'POLICYCHANGELOG_UPDATE_AUTOREIMBURSEMENT', UPDATE_AUTOREPORTING_FREQUENCY: 'POLICYCHANGELOG_UPDATE_AUTOREPORTING_FREQUENCY', + UPDATE_BUDGET: 'POLICYCHANGELOG_UPDATE_BUDGET', UPDATE_CATEGORY: 'POLICYCHANGELOG_UPDATE_CATEGORY', UPDATE_CURRENCY: 'POLICYCHANGELOG_UPDATE_CURRENCY', UPDATE_CUSTOM_UNIT: 'POLICYCHANGELOG_UPDATE_CUSTOM_UNIT', @@ -645,6 +651,9 @@ const CONST = { OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', }, + NEXT_STEP: { + FINISHED: 'Finished!', + }, COMPOSER: { MAX_LINES: 16, MAX_LINES_SMALL_SCREEN: 6, @@ -1090,11 +1099,6 @@ const CONST = { USER_CANCELLED: 'User canceled flow.', USER_TAPPED_BACK: 'User exited by clicking the back button.', USER_EXITED: 'User exited by manual action.', - USER_CAMERA_DENINED: 'Onfido.OnfidoFlowError', - USER_CAMERA_PERMISSION: 'Encountered an error: cameraPermission', - // eslint-disable-next-line max-len - USER_CAMERA_CONSENT_DENIED: - 'Unexpected result Intent. It might be a result of incorrect integration, make sure you only pass Onfido intent to handleActivityResult. It might be due to unpredictable crash or error. Please report the problem to android-sdk@onfido.com. Intent: null \n resultCode: 0', }, }, @@ -1161,6 +1165,7 @@ const CONST = { DECLINE: 'decline', CANCEL: 'cancel', DELETE: 'delete', + APPROVE: 'approve', }, AMOUNT_MAX_LENGTH: 10, RECEIPT_STATE: { @@ -2721,17 +2726,125 @@ const CONST = { EXPENSIFY_LOGO_SIZE_RATIO: 0.22, EXPENSIFY_LOGO_MARGIN_RATIO: 0.03, }, + /** + * Acceptable values for the `accessibilityRole` prop on react native components. + * + * **IMPORTANT:** Do not use with the `role` prop as it can cause errors. + * + * @deprecated ACCESSIBILITY_ROLE is deprecated. Please use CONST.ROLE instead. + */ ACCESSIBILITY_ROLE: { + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ BUTTON: 'button', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ LINK: 'link', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ MENUITEM: 'menuitem', - TEXT: 'presentation', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + TEXT: 'text', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ RADIO: 'radio', - IMAGEBUTTON: 'img button', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + IMAGEBUTTON: 'imagebutton', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ CHECKBOX: 'checkbox', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + SWITCH: 'switch', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + ADJUSTABLE: 'adjustable', + + /** + * @deprecated Please stop using the accessibilityRole prop and use the role prop instead. + */ + IMAGE: 'image', + }, + /** + * Acceptable values for the `role` attribute on react native components. + * + * **IMPORTANT:** Not for use with the `accessibilityRole` prop, as it accepts different values, and new components + * should use the `role` prop instead. + */ + ROLE: { + /** Use for elements with important, time-sensitive information. */ + ALERT: 'alert', + /** Use for elements that act as buttons. */ + BUTTON: 'button', + /** Use for elements representing checkboxes. */ + CHECKBOX: 'checkbox', + /** Use for elements that allow a choice from multiple options. */ + COMBOBOX: 'combobox', + /** Use with scrollable lists to represent a grid layout. */ + GRID: 'grid', + /** Use for section headers or titles. */ + HEADING: 'heading', + /** Use for image elements. */ + IMG: 'img', + /** Use for elements that navigate to other pages or content. */ + LINK: 'link', + /** Use to identify a list of items. */ + LIST: 'list', + /** Use for a list of choices or options. */ + MENU: 'menu', + /** Use for a container of multiple menus. */ + MENUBAR: 'menubar', + /** Use for items within a menu. */ + MENUITEM: 'menuitem', + /** Use when no specific role is needed. */ + NONE: 'none', + /** Use for elements that don't require a specific role. */ + PRESENTATION: 'presentation', + /** Use for elements showing progress of a task. */ + PROGRESSBAR: 'progressbar', + /** Use for radio buttons. */ + RADIO: 'radio', + /** Use for groups of radio buttons. */ + RADIOGROUP: 'radiogroup', + /** Use for scrollbar elements. */ + SCROLLBAR: 'scrollbar', + /** Use for text fields that are used for searching. */ + SEARCHBOX: 'searchbox', + /** Use for adjustable elements like sliders. */ + SLIDER: 'slider', + /** Use for a button that opens a list of choices. */ + SPINBUTTON: 'spinbutton', + /** Use for elements providing a summary of app conditions. */ + SUMMARY: 'summary', + /** Use for on/off switch elements. */ SWITCH: 'switch', - ADJUSTABLE: 'slider', - IMAGE: 'img', + /** Use for tab elements in a tab list. */ + TAB: 'tab', + /** Use for a list of tabs. */ + TABLIST: 'tablist', + /** Use for timer elements. */ + TIMER: 'timer', + /** Use for toolbars containing action buttons or components. */ + TOOLBAR: 'toolbar', }, TRANSLATION_KEYS: { ATTACHMENT: 'common.attachment', diff --git a/src/Expensify.js b/src/Expensify.js index aece93c0ff4d..756df5b79b88 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -112,6 +112,7 @@ function Expensify(props) { }, [props.isCheckingPublicRoom]); const isAuthenticated = useMemo(() => Boolean(lodashGet(props.session, 'authToken', null)), [props.session]); + const autoAuthState = useMemo(() => lodashGet(props.session, 'autoAuthState', ''), [props.session]); const contextValue = useMemo( () => ({ @@ -207,7 +208,10 @@ function Expensify(props) { } return ( - + {shouldInit && ( <> diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 9cd55b41455b..933ae678da23 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -383,7 +383,7 @@ type OnyxValues = { [ONYXKEYS.COUNTRY]: string; [ONYXKEYS.USER]: OnyxTypes.User; [ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation; - [ONYXKEYS.LOGIN_LIST]: Record; + [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; [ONYXKEYS.SESSION]: OnyxTypes.Session; [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; [ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf; @@ -403,8 +403,8 @@ type OnyxValues = { [ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido; [ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails; [ONYXKEYS.WALLET_TERMS]: OnyxTypes.WalletTerms; - [ONYXKEYS.BANK_ACCOUNT_LIST]: Record; - [ONYXKEYS.FUND_LIST]: Record; + [ONYXKEYS.BANK_ACCOUNT_LIST]: OnyxTypes.BankAccountList; + [ONYXKEYS.FUND_LIST]: OnyxTypes.FundList; [ONYXKEYS.CARD_LIST]: Record; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; @@ -440,7 +440,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.POLICY_DRAFTS]: OnyxTypes.Policy; [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory; [ONYXKEYS.COLLECTION.POLICY_TAGS]: OnyxTypes.PolicyTags; - [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember; + [ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers; [ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories; [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers; @@ -453,12 +453,13 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES]: number; [ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean; - [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: boolean; + [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: OnyxTypes.ReportUserIsTyping; [ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean; [ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup; [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; + [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; // Forms [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.AddDebitCardForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 53763d6d7cd1..425ff73af56b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -34,7 +34,7 @@ const ROUTES = { VALIDATE_LOGIN: 'v/:accountID/:validateCode', GET_ASSISTANCE: { route: 'get-assistance/:taskID', - getRoute: (taskID: string) => `get-assistance/${taskID}` as const, + getRoute: (taskID: string, backTo: string) => getUrlWithBackToParam(`get-assistance/${taskID}`, backTo), }, UNLINK_LOGIN: 'u/:accountID/:validateCode', APPLE_SIGN_IN: 'sign-in-with-apple', diff --git a/src/components/AddressForm.js b/src/components/AddressForm.js index 98650f94232b..1621328d388f 100644 --- a/src/components/AddressForm.js +++ b/src/components/AddressForm.js @@ -11,7 +11,8 @@ import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import AddressSearch from './AddressSearch'; import CountrySelector from './CountrySelector'; -import Form from './Form'; +import FormProvider from './Form/FormProvider'; +import InputWrapper from './Form/InputWrapper'; import StatePicker from './StatePicker'; import TextInput from './TextInput'; @@ -115,7 +116,7 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS }, []); return ( -
- { @@ -146,7 +148,8 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS /> - - {isUSAForm ? ( - ) : ( - )} - - - + ); } diff --git a/src/components/AmountTextInput.js b/src/components/AmountTextInput.js index bd88712432a8..5efcc003d853 100644 --- a/src/components/AmountTextInput.js +++ b/src/components/AmountTextInput.js @@ -55,7 +55,7 @@ function AmountTextInput(props) { blurOnSubmit={false} selection={props.selection} onSelectionChange={props.onSelectionChange} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} onKeyPress={props.onKeyPress} /> ); diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js index 1e2d18bc4691..6161ba140726 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js @@ -58,7 +58,7 @@ function BaseAnchorForAttachmentsOnly(props) { onPressOut={props.onPressOut} onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} accessibilityLabel={fileName} - role={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ROLE.BUTTON} > (linkRef = el)} style={StyleSheet.flatten([style, defaultTextStyle])} - role={CONST.ACCESSIBILITY_ROLE.LINK} + role={CONST.ROLE.LINK} hrefAttrs={{ rel, target: isEmail || !linkProps.href ? '_self' : target, diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 3187bf3604e8..712ef6be769e 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -8,7 +8,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Report, ReportAction} from '@src/types/onyx'; +import type {PersonalDetailsList, Report, ReportAction} from '@src/types/onyx'; import Banner from './Banner'; type ArchivedReportFooterOnyxProps = { @@ -16,7 +16,7 @@ type ArchivedReportFooterOnyxProps = { reportClosedAction: OnyxEntry; /** Personal details of all users */ - personalDetails: OnyxEntry>; + personalDetails: OnyxEntry; }; type ArchivedReportFooterProps = ArchivedReportFooterOnyxProps & { diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 4dd0a96c31b9..79be536945ac 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -2,7 +2,7 @@ import Str from 'expensify-common/lib/str'; import lodashExtend from 'lodash/extend'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Animated, Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -90,6 +90,9 @@ const propTypes = { /** Denotes whether it is a workspace avatar or not */ isWorkspaceAvatar: PropTypes.bool, + + /** Whether it is a receipt attachment or not */ + isReceiptAttachment: PropTypes.bool, }; const defaultProps = { @@ -107,19 +110,18 @@ const defaultProps = { onModalHide: () => {}, onCarouselAttachmentChange: () => {}, isWorkspaceAvatar: false, + isReceiptAttachment: false, }; function AttachmentModal(props) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const onModalHideCallbackRef = useRef(null); const [isModalOpen, setIsModalOpen] = useState(props.defaultOpen); const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false); const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false); const [isAuthTokenRequired, setIsAuthTokenRequired] = useState(props.isAuthTokenRequired); - const [isAttachmentReceipt, setIsAttachmentReceipt] = useState(null); const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(''); const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); const [source, setSource] = useState(props.source); @@ -155,7 +157,6 @@ function AttachmentModal(props) { (attachment) => { setSource(attachment.source); setFile(attachment.file); - setIsAttachmentReceipt(attachment.isReceipt); setIsAuthTokenRequired(attachment.isAuthTokenRequired); onCarouselAttachmentChange(attachment); }, @@ -358,7 +359,7 @@ function AttachmentModal(props) { const sourceForAttachmentView = props.source || source; const threeDotsMenuItems = useMemo(() => { - if (!isAttachmentReceipt || !props.parentReport || !props.parentReportActions) { + if (!props.isReceiptAttachment || !props.parentReport || !props.parentReportActions) { return []; } const menuItems = []; @@ -372,8 +373,8 @@ function AttachmentModal(props) { icon: Expensicons.Camera, text: props.translate('common.replace'), onSelected: () => { - onModalHideCallbackRef.current = () => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT)); closeModal(); + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT)); }, }); } @@ -393,17 +394,17 @@ function AttachmentModal(props) { } return menuItems; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isAttachmentReceipt, props.parentReport, props.parentReportActions, props.policy, props.transaction, file]); + }, [props.isReceiptAttachment, props.parentReport, props.parentReportActions, props.policy, props.transaction, file]); // There are a few things that shouldn't be set until we absolutely know if the file is a receipt or an attachment. - // isAttachmentReceipt will be null until its certain what the file is, in which case it will then be true|false. + // props.isReceiptAttachment will be null until its certain what the file is, in which case it will then be true|false. let headerTitle = props.headerTitle; let shouldShowDownloadButton = false; let shouldShowThreeDotsButton = false; - if (!_.isNull(isAttachmentReceipt)) { - headerTitle = translate(isAttachmentReceipt ? 'common.receipt' : 'common.attachment'); - shouldShowDownloadButton = props.allowDownload && isDownloadButtonReadyToBeShown && !isAttachmentReceipt && !isOffline; - shouldShowThreeDotsButton = isAttachmentReceipt && isModalOpen; + if (!_.isEmpty(props.report)) { + headerTitle = translate(props.isReceiptAttachment ? 'common.receipt' : 'common.attachment'); + shouldShowDownloadButton = props.allowDownload && isDownloadButtonReadyToBeShown && !props.isReceiptAttachment && !isOffline; + shouldShowThreeDotsButton = props.isReceiptAttachment && isModalOpen; } return ( @@ -420,10 +421,6 @@ function AttachmentModal(props) { }} onModalHide={(e) => { props.onModalHide(e); - if (onModalHideCallbackRef.current) { - onModalHideCallbackRef.current(); - } - setShouldLoadAttachment(false); }} propagateSwipe @@ -444,7 +441,7 @@ function AttachmentModal(props) { shouldOverlay /> - {!_.isEmpty(props.report) ? ( + {!_.isEmpty(props.report) && !props.isReceiptAttachment ? ( ) )} @@ -487,7 +485,7 @@ function AttachmentModal(props) { )} )} - {isAttachmentReceipt && ( + {props.isReceiptAttachment && ( )} - {!isAttachmentReceipt && ( + {!props.isReceiptAttachment && ( {children} diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js index 50d53ee842c2..6994a7a210bf 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js +++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js @@ -1,10 +1,7 @@ import {Parser as HtmlParser} from 'htmlparser2'; -import lodashGet from 'lodash/get'; import _ from 'underscore'; import * as FileUtils from '@libs/fileDownload/FileUtils'; -import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; @@ -15,7 +12,7 @@ import CONST from '@src/CONST'; * @param {Object} transaction * @returns {Array} */ -function extractAttachmentsFromReport(parentReportAction, reportActions, transaction) { +function extractAttachmentsFromReport(parentReportAction, reportActions) { const actions = [parentReportAction, ...ReportActionsUtils.getSortedReportActions(_.values(reportActions))]; const attachments = []; @@ -43,32 +40,10 @@ function extractAttachmentsFromReport(parentReportAction, reportActions, transac }); _.forEach(actions, (action, key) => { - if (!ReportActionsUtils.shouldReportActionBeVisible(action, key)) { + if (!ReportActionsUtils.shouldReportActionBeVisible(action, key) || ReportActionsUtils.isMoneyRequestAction(action)) { return; } - // We're handling receipts differently here because receipt images are not - // part of the report action message, the images are constructed client-side - if (ReportActionsUtils.isMoneyRequestAction(action)) { - const transactionID = lodashGet(action, ['originalMessage', 'IOUTransactionID']); - if (!transactionID) { - return; - } - - if (TransactionUtils.hasReceipt(transaction)) { - const {image} = ReceiptUtils.getThumbnailAndImageURIs(transaction); - const isLocalFile = typeof image === 'string' && _.some(CONST.ATTACHMENT_LOCAL_URL_PREFIX, (prefix) => image.startsWith(prefix)); - attachments.unshift({ - source: tryResolveUrlFromApiRoot(image), - isAuthTokenRequired: !isLocalFile, - file: {name: transaction.filename}, - isReceipt: true, - transactionID, - }); - return; - } - } - const decision = _.get(action, ['message', 0, 'moderationDecision', 'decision'], ''); const hasBeenFlagged = decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN; const html = _.get(action, ['message', 0, 'html'], '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`); diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js index 141e619e489e..fb31e32de91c 100644 --- a/src/components/Attachments/AttachmentCarousel/index.js +++ b/src/components/Attachments/AttachmentCarousel/index.js @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {FlatList, Keyboard, PixelRatio, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -28,7 +27,7 @@ const viewabilityConfig = { itemVisiblePercentThreshold: 95, }; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, transaction}) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate}) { const styles = useThemeStyles(); const scrollRef = useRef(null); @@ -39,21 +38,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const [attachments, setAttachments] = useState([]); const [activeSource, setActiveSource] = useState(source); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); - const [isReceipt, setIsReceipt] = useState(false); - const compareImage = useCallback( - (attachment) => { - if (attachment.isReceipt && isReceipt) { - return attachment.transactionID === transaction.transactionID; - } - return attachment.source === source; - }, - [source, isReceipt, transaction], - ); + const compareImage = useCallback((attachment) => attachment.source === source, [source]); useEffect(() => { const parentReportAction = parentReportActions[report.parentReportActionID]; - const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions, transaction); + const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions); const initialPage = _.findIndex(attachmentsFromReport, compareImage); @@ -88,12 +78,10 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, // to get the index of the current page const entry = _.first(viewableItems); if (!entry) { - setIsReceipt(false); setActiveSource(null); return; } - setIsReceipt(entry.item.isReceipt); setPage(entry.index); setActiveSource(entry.item.source); @@ -227,7 +215,6 @@ AttachmentCarousel.defaultProps = defaultProps; AttachmentCarousel.displayName = 'AttachmentCarousel'; export default compose( - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ reportActions: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, @@ -241,15 +228,6 @@ export default compose( canEvict: false, }, }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({report, parentReportActions}) => { - const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]); - return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - }), withLocalize, withWindowDimensions, )(AttachmentCarousel); diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js index 6bf4e63c01e7..ea45509d6ce3 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.js +++ b/src/components/Attachments/AttachmentCarousel/index.native.js @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {Keyboard, PixelRatio, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -18,7 +17,7 @@ import extractAttachmentsFromReport from './extractAttachmentsFromReport'; import AttachmentCarouselPager from './Pager'; import useCarouselArrows from './useCarouselArrows'; -function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, transaction, onClose}) { +function AttachmentCarousel({report, reportActions, parentReportActions, source, onNavigate, setDownloadButtonVisibility, translate, onClose}) { const styles = useThemeStyles(); const pagerRef = useRef(null); @@ -28,21 +27,12 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const [activeSource, setActiveSource] = useState(source); const [isPinchGestureRunning, setIsPinchGestureRunning] = useState(true); const [shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows] = useCarouselArrows(); - const [isReceipt, setIsReceipt] = useState(false); - const compareImage = useCallback( - (attachment) => { - if (attachment.isReceipt && isReceipt) { - return attachment.transactionID === transaction.transactionID; - } - return attachment.source === source; - }, - [source, isReceipt, transaction], - ); + const compareImage = useCallback((attachment) => attachment.source === source, [source]); useEffect(() => { const parentReportAction = parentReportActions[report.parentReportActionID]; - const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions, transaction); + const attachmentsFromReport = extractAttachmentsFromReport(parentReportAction, reportActions); const initialPage = _.findIndex(attachmentsFromReport, compareImage); @@ -77,7 +67,6 @@ function AttachmentCarousel({report, reportActions, parentReportActions, source, const item = attachments[newPageIndex]; setPage(newPageIndex); - setIsReceipt(item.isReceipt); setActiveSource(item.source); onNavigate(item); @@ -172,7 +161,6 @@ AttachmentCarousel.defaultProps = defaultProps; AttachmentCarousel.displayName = 'AttachmentCarousel'; export default compose( - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ reportActions: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, @@ -186,14 +174,5 @@ export default compose( canEvict: false, }, }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({report, parentReportActions}) => { - const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]); - return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`; - }, - }, - }), withLocalize, )(AttachmentCarousel); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js index 22bcf259ed77..3b080e47e4d1 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js @@ -28,7 +28,7 @@ function AttachmentViewImage({source, file, isAuthTokenRequired, loadComplete, o onPress={onPress} disabled={loadComplete} style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]} - role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={file.name || translate('attachmentView.unknownFilename')} > {children} diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js index fc443e5ea17b..a61adcf04043 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js @@ -36,7 +36,7 @@ function AttachmentViewImage({source, file, isAuthTokenRequired, isFocused, isUs onPress={onPress} disabled={loadComplete} style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]} - role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={file.name || translate('attachmentView.unknownFilename')} > {children} diff --git a/src/components/AvatarCropModal/AvatarCropModal.js b/src/components/AvatarCropModal/AvatarCropModal.js index dcb0470c5ee5..419891d9bdef 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.js +++ b/src/components/AvatarCropModal/AvatarCropModal.js @@ -413,7 +413,7 @@ function AvatarCropModal(props) { onLayout={initializeSliderContainer} onPressIn={(e) => runOnUI(sliderOnPress)(e.nativeEvent.locationX)} accessibilityLabel="slider" - role={CONST.ACCESSIBILITY_ROLE.ADJUSTABLE} + role={CONST.ROLE.SLIDER} > {shouldShowSubscriptAvatar ? ( ReportUtils.navigateToDetailsPage(report)} style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]} accessibilityLabel={title} - role={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ROLE.BUTTON} > {headerView} diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index eabcd3aa85c5..9b061ba5c670 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -297,7 +297,7 @@ function AvatarWithImagePicker({ setIsMenuVisible((prev) => !prev)} - role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={translate('avatarWithImagePicker.editImage')} disabled={isAvatarCropModalOpen} ref={anchorRef} diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 82212c66db04..b670921dff4c 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -47,7 +47,7 @@ function Badge({success = false, error = false, pressable = false, text, environ diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 9cbd19e03dc7..eb99d4b09396 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -293,7 +293,7 @@ function Button( ]} id={id} accessibilityLabel={accessibilityLabel} - role={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ROLE.BUTTON} hoverDimmingValue={1} > {renderContent()} diff --git a/src/components/ButtonWithDropdownMenu.js b/src/components/ButtonWithDropdownMenu.js index 321cd79f1318..a5f311740f19 100644 --- a/src/components/ButtonWithDropdownMenu.js +++ b/src/components/ButtonWithDropdownMenu.js @@ -80,7 +80,7 @@ function ButtonWithDropdownMenu(props) { const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); const caretButton = useRef(null); - const selectedItem = props.options[selectedItemIndex]; + const selectedItem = props.options[selectedItemIndex] || _.first(props.options); const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(props.buttonSize); const isButtonSizeLarge = props.buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; diff --git a/src/components/CardPreview.js b/src/components/CardPreview.tsx similarity index 62% rename from src/components/CardPreview.js rename to src/components/CardPreview.tsx index df944d930a92..6dc8abfb80ef 100644 --- a/src/components/CardPreview.js +++ b/src/components/CardPreview.tsx @@ -1,41 +1,28 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import ExpensifyCardImage from '@assets/images/expensify-card.svg'; import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; +import {PrivatePersonalDetails, Session} from '@src/types/onyx'; import Text from './Text'; -const propTypes = { +type CardPreviewOnyxProps = { /** User's private personal details */ - privatePersonalDetails: PropTypes.shape({ - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - }), + privatePersonalDetails: OnyxEntry; /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged-in user email */ - email: PropTypes.string, - }), + session: OnyxEntry; }; -const defaultProps = { - privatePersonalDetails: { - legalFirstName: '', - legalLastName: '', - }, - session: { - email: '', - }, -}; +type CardPreviewProps = CardPreviewOnyxProps; -function CardPreview({privatePersonalDetails: {legalFirstName, legalLastName}, session: {email}}) { +function CardPreview({privatePersonalDetails, session}: CardPreviewProps) { const styles = useThemeStyles(); usePrivatePersonalDetails(); - const cardHolder = legalFirstName && legalLastName ? `${legalFirstName} ${legalLastName}` : email; + const {legalFirstName, legalLastName} = privatePersonalDetails ?? {}; + const cardHolder = legalFirstName && legalLastName ? `${legalFirstName} ${legalLastName}` : session?.email ?? ''; return ( @@ -55,11 +42,9 @@ function CardPreview({privatePersonalDetails: {legalFirstName, legalLastName}, s ); } -CardPreview.propTypes = propTypes; -CardPreview.defaultProps = defaultProps; CardPreview.displayName = 'CardPreview'; -export default withOnyx({ +export default withOnyx({ privatePersonalDetails: { key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, }, diff --git a/src/components/CollapsibleSection/index.tsx b/src/components/CollapsibleSection/index.tsx index 570d00559af8..04574c5fd057 100644 --- a/src/components/CollapsibleSection/index.tsx +++ b/src/components/CollapsibleSection/index.tsx @@ -32,7 +32,7 @@ function CollapsibleSection({title, children}: CollapsibleSectionProps) { ); } diff --git a/src/components/CountrySelector.js b/src/components/CountrySelector.js index 13fc215f1d8c..b138bc949937 100644 --- a/src/components/CountrySelector.js +++ b/src/components/CountrySelector.js @@ -7,6 +7,7 @@ import useThemeStyles from '@styles/useThemeStyles'; import ROUTES from '@src/ROUTES'; import FormHelpMessage from './FormHelpMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import refPropTypes from './refPropTypes'; const propTypes = { /** Form error text. e.g when no country is selected */ @@ -23,7 +24,7 @@ const propTypes = { inputID: PropTypes.string.isRequired, /** React ref being forwarded to the MenuItemWithTopDescription */ - forwardedRef: PropTypes.func, + forwardedRef: refPropTypes, }; const defaultProps = { diff --git a/src/components/CurrencySymbolButton.js b/src/components/CurrencySymbolButton.js index 4d43ec3d93e0..47c25a43ad11 100644 --- a/src/components/CurrencySymbolButton.js +++ b/src/components/CurrencySymbolButton.js @@ -23,7 +23,7 @@ function CurrencySymbolButton({onCurrencyButtonPress, currencySymbol}) { {currencySymbol} diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js index 10a53dc25bbb..ac6454d25975 100644 --- a/src/components/DatePicker/index.js +++ b/src/components/DatePicker/index.js @@ -76,7 +76,7 @@ function DatePicker({containerStyles, defaultValue, disabled, errorText, inputID icon={Expensicons.Calendar} label={label} accessibilityLabel={label} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} value={value || selectedDate || ''} placeholder={placeholder || translate('common.dateFormat')} errorText={errorText} diff --git a/src/components/DeeplinkWrapper/index.website.js b/src/components/DeeplinkWrapper/index.website.js index 166818e4ae27..d81c99657dd8 100644 --- a/src/components/DeeplinkWrapper/index.website.js +++ b/src/components/DeeplinkWrapper/index.website.js @@ -16,6 +16,8 @@ const propTypes = { children: PropTypes.node.isRequired, /** User authentication status */ isAuthenticated: PropTypes.bool.isRequired, + /** The auto authentication status */ + autoAuthState: PropTypes.string, }; function isMacOSWeb() { @@ -36,7 +38,7 @@ function promptToOpenInDesktopApp() { App.beginDeepLinkRedirect(!isMagicLink); } } -function DeeplinkWrapper({children, isAuthenticated}) { +function DeeplinkWrapper({children, isAuthenticated, autoAuthState}) { const [currentScreen, setCurrentScreen] = useState(); const [hasShownPrompt, setHasShownPrompt] = useState(false); const removeListener = useRef(); @@ -69,7 +71,7 @@ function DeeplinkWrapper({children, isAuthenticated}) { return routeRegex.test(window.location.pathname); }); // Making a few checks to exit early before checking authentication status - if (!isMacOSWeb() || isUnsupportedDeeplinkRoute || CONFIG.ENVIRONMENT === CONST.ENVIRONMENT.DEV || hasShownPrompt) { + if (!isMacOSWeb() || isUnsupportedDeeplinkRoute || hasShownPrompt || CONFIG.ENVIRONMENT === CONST.ENVIRONMENT.DEV || autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED) { return; } // We want to show the prompt immediately if the user is already authenticated. @@ -92,7 +94,7 @@ function DeeplinkWrapper({children, isAuthenticated}) { promptToOpenInDesktopApp(); setHasShownPrompt(true); } - }, [currentScreen, hasShownPrompt, isAuthenticated]); + }, [currentScreen, hasShownPrompt, isAuthenticated, autoAuthState]); return children; } diff --git a/src/components/EmojiPicker/CategoryShortcutButton.js b/src/components/EmojiPicker/CategoryShortcutButton.js index 8738f210276e..1fcbe7e863e4 100644 --- a/src/components/EmojiPicker/CategoryShortcutButton.js +++ b/src/components/EmojiPicker/CategoryShortcutButton.js @@ -41,7 +41,7 @@ function CategoryShortcutButton(props) { onHoverOut={() => setIsHighlighted(false)} style={({pressed}) => [StyleUtils.getButtonBackgroundColorStyle(getButtonState(false, pressed)), styles.categoryShortcutButton, isHighlighted && styles.emojiItemHighlighted]} accessibilityLabel={`emojiPicker.headers.${props.code}`} - role={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ROLE.BUTTON} > {({hovered, pressed}) => ( diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 95db6eb41167..263f5929d567 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -485,7 +485,7 @@ function EmojiPickerMenu(props) { 0} /> diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js index ae2cdf46dfc0..52d4a0db8812 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.js @@ -103,7 +103,7 @@ class EmojiPickerMenuItem extends PureComponent { this.props.themeStyles.emojiItem, ]} accessibilityLabel={this.props.emoji} - role={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ROLE.BUTTON} > {this.props.emoji} diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js index 7934cc0f03d4..1726ff5b6543 100644 --- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js +++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.js @@ -80,7 +80,7 @@ class EmojiPickerMenuItem extends PureComponent { this.props.themeStyles.emojiItem, ]} accessibilityLabel={this.props.emoji} - role={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ROLE.BUTTON} > {this.props.emoji} diff --git a/src/components/EmojiPicker/EmojiSkinToneList.js b/src/components/EmojiPicker/EmojiSkinToneList.js index 25fc9ad0836a..69690fa882c9 100644 --- a/src/components/EmojiPicker/EmojiSkinToneList.js +++ b/src/components/EmojiPicker/EmojiSkinToneList.js @@ -49,7 +49,7 @@ function EmojiSkinToneList(props) { onPress={toggleIsSkinToneListVisible} style={[styles.flexRow, styles.alignSelfCenter, styles.justifyContentStart, styles.alignItemsCenter]} accessibilityLabel={props.translate('emojiPicker.skinTonePickerLabel')} - role={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ROLE.BUTTON} > {currentSkinTone.code} diff --git a/src/components/FlatList/index.android.js b/src/components/FlatList/index.android.js deleted file mode 100644 index f7c3da39ed84..000000000000 --- a/src/components/FlatList/index.android.js +++ /dev/null @@ -1,79 +0,0 @@ -import {useFocusEffect} from '@react-navigation/native'; -import PropTypes from 'prop-types'; -import React, {forwardRef, useCallback, useContext} from 'react'; -import {FlatList} from 'react-native'; -import {ActionListContext} from '@pages/home/ReportScreenContext'; - -const propTypes = { - /** Same as for FlatList */ - onScroll: PropTypes.func, - - /** Same as for FlatList */ - onLayout: PropTypes.func, - - /** Same as for FlatList */ - // eslint-disable-next-line react/forbid-prop-types - maintainVisibleContentPosition: PropTypes.object, - - /** Passed via forwardRef so we can access the FlatList ref */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(FlatList)})]).isRequired, -}; - -const defaultProps = { - /** Same as for FlatList */ - onScroll: undefined, - - /** Same as for FlatList */ - onLayout: undefined, - - /** Same as for FlatList */ - maintainVisibleContentPosition: undefined, -}; - -// FlatList wrapped with the freeze component will lose its scroll state when frozen (only for Android). -// CustomFlatList saves the offset and use it for scrollToOffset() when unfrozen. -function CustomFlatList(props) { - const {scrollPosition, setScrollPosition} = useContext(ActionListContext); - - const onScreenFocus = useCallback(() => { - if (!props.innerRef.current || !scrollPosition.offset) { - return; - } - if (props.innerRef.current && scrollPosition.offset) { - props.innerRef.current.scrollToOffset({offset: scrollPosition.offset, animated: false}); - } - }, [scrollPosition.offset, props.innerRef]); - - useFocusEffect( - useCallback(() => { - onScreenFocus(); - }, [onScreenFocus]), - ); - - return ( - props.onScroll(event)} - onMomentumScrollEnd={(event) => { - setScrollPosition({offset: event.nativeEvent.contentOffset.y}); - }} - ref={props.innerRef} - /> - ); -} - -CustomFlatList.propTypes = propTypes; -CustomFlatList.defaultProps = defaultProps; - -const CustomFlatListWithRef = forwardRef((props, ref) => ( - -)); - -CustomFlatListWithRef.displayName = 'CustomFlatListWithRef'; - -export default CustomFlatListWithRef; diff --git a/src/components/FlatList/index.android.tsx b/src/components/FlatList/index.android.tsx new file mode 100644 index 000000000000..84345f6e0ed4 --- /dev/null +++ b/src/components/FlatList/index.android.tsx @@ -0,0 +1,43 @@ +import {useFocusEffect} from '@react-navigation/native'; +import React, {ForwardedRef, forwardRef, useCallback, useContext} from 'react'; +import {FlatList, FlatListProps} from 'react-native'; +import {ActionListContext} from '@pages/home/ReportScreenContext'; + +// FlatList wrapped with the freeze component will lose its scroll state when frozen (only for Android). +// CustomFlatList saves the offset and use it for scrollToOffset() when unfrozen. +function CustomFlatList(props: FlatListProps, ref: ForwardedRef) { + const {scrollPosition, setScrollPosition} = useContext(ActionListContext); + + const onScreenFocus = useCallback(() => { + if (typeof ref === 'function') { + return; + } + if (!ref?.current || !scrollPosition?.offset) { + return; + } + if (ref.current && scrollPosition.offset) { + ref.current.scrollToOffset({offset: scrollPosition.offset, animated: false}); + } + }, [scrollPosition?.offset, ref]); + + useFocusEffect( + useCallback(() => { + onScreenFocus(); + }, [onScreenFocus]), + ); + + return ( + + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + onScroll={(event) => props.onScroll?.(event)} + onMomentumScrollEnd={(event) => { + setScrollPosition({offset: event.nativeEvent.contentOffset.y}); + }} + ref={ref} + /> + ); +} + +CustomFlatList.displayName = 'CustomFlatListWithRef'; +export default forwardRef(CustomFlatList); diff --git a/src/components/FlatList/index.js b/src/components/FlatList/index.ts similarity index 100% rename from src/components/FlatList/index.js rename to src/components/FlatList/index.ts diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index af2511fc9f74..63953d8303db 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -112,7 +112,7 @@ function getInitialValueByType(valueType) { function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnChange, children, formState, network, enabledWhenOffline, draftValues, onSubmit, ...rest}) { const inputRefs = useRef({}); const touchedInputs = useRef({}); - const [inputValues, setInputValues] = useState({}); + const [inputValues, setInputValues] = useState(() => ({...draftValues})); const [errors, setErrors] = useState({}); const hasServerError = useMemo(() => Boolean(formState) && !_.isEmpty(formState.errors), [formState]); diff --git a/src/components/FormHelpMessage.js b/src/components/FormHelpMessage.js deleted file mode 100644 index bec02c3d51f0..000000000000 --- a/src/components/FormHelpMessage.js +++ /dev/null @@ -1,62 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import * as Localize from '@libs/Localize'; -import stylePropTypes from '@styles/stylePropTypes'; -import useTheme from '@styles/themes/useTheme'; -import useThemeStyles from '@styles/useThemeStyles'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; -import Text from './Text'; - -const propTypes = { - /** Error or hint text. Ignored when children is not empty */ - message: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - - /** Children to render next to dot indicator */ - children: PropTypes.node, - - /** Indicates whether to show error or hint */ - isError: PropTypes.bool, - - /** Container style props */ - style: stylePropTypes, -}; - -const defaultProps = { - message: '', - children: null, - isError: true, - style: [], -}; - -function FormHelpMessage(props) { - const theme = useTheme(); - const styles = useThemeStyles(); - if (_.isEmpty(props.message) && _.isEmpty(props.children)) { - return null; - } - - const translatedMessage = Localize.translateIfPhraseKey(props.message); - return ( - - {props.isError && ( - - )} - - - {props.children || {translatedMessage}} - - - ); -} - -FormHelpMessage.propTypes = propTypes; -FormHelpMessage.defaultProps = defaultProps; -FormHelpMessage.displayName = 'FormHelpMessage'; - -export default FormHelpMessage; diff --git a/src/components/FormHelpMessage.tsx b/src/components/FormHelpMessage.tsx new file mode 100644 index 000000000000..27a1f5827d75 --- /dev/null +++ b/src/components/FormHelpMessage.tsx @@ -0,0 +1,49 @@ +import isEmpty from 'lodash/isEmpty'; +import React from 'react'; +import {StyleProp, View, ViewStyle} from 'react-native'; +import * as Localize from '@libs/Localize'; +import useTheme from '@styles/themes/useTheme'; +import useThemeStyles from '@styles/useThemeStyles'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import Text from './Text'; + +type FormHelpMessageProps = { + /** Error or hint text. Ignored when children is not empty */ + message?: Localize.MaybePhraseKey; + + /** Children to render next to dot indicator */ + children?: React.ReactNode; + + /** Indicates whether to show error or hint */ + isError?: boolean; + + /** Container style props */ + style?: StyleProp; +}; + +function FormHelpMessage({message = '', children, isError = true, style}: FormHelpMessageProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + if (isEmpty(message) && isEmpty(children)) { + return null; + } + + const translatedMessage = Localize.translateIfPhraseKey(message); + + return ( + + {isError && ( + + )} + {children ?? {translatedMessage}} + + ); +} + +FormHelpMessage.displayName = 'FormHelpMessage'; + +export default FormHelpMessage; diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js index 8cddd3c017de..9a2d7c673c5b 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js @@ -60,8 +60,12 @@ function BaseHTMLEngineProvider(props) { }), 'mention-user': defaultHTMLElementModels.span.extend({tagName: 'mention-user'}), 'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}), + 'next-steps': defaultHTMLElementModels.span.extend({ + tagName: 'next-steps', + mixedUAStyles: {...styles.textLabelSupporting}, + }), }), - [styles.colorMuted, styles.formError, styles.mb0], + [styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting], ); // We need to memoize this prop to make it referentially stable. diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js index 7cbdf8d69831..f5ecc106d629 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js @@ -77,7 +77,7 @@ function ImageRenderer(props) { ReportUtils.isArchivedRoom(report), ) } - role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} > { onPressIn={props.onPressIn} onPressOut={props.onPressOut} onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} - role={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ROLE.PRESENTATION} accessibilityLabel={props.translate('accessibilityHints.prestyledText')} > diff --git a/src/components/HeaderWithBackButton/index.js b/src/components/HeaderWithBackButton/index.js index aa73ab2d3327..9de8bd6a22c7 100755 --- a/src/components/HeaderWithBackButton/index.js +++ b/src/components/HeaderWithBackButton/index.js @@ -141,7 +141,7 @@ function HeaderWithBackButton({ Navigation.navigate(ROUTES.GET_ASSISTANCE.getRoute(guidesCallTaskID))))} + onPress={singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.GET_ASSISTANCE.getRoute(guidesCallTaskID, Navigation.getActiveRoute()))))} style={[styles.touchableButtonImage]} role="button" accessibilityLabel={translate('getAssistancePage.questionMarkButtonTooltip')} @@ -168,7 +168,7 @@ function HeaderWithBackButton({ policy); - const cleanAllPolicyMembers = _.pick(props.allPolicyMembers, (policyMembers) => policyMembers); - - const paymentCardList = props.fundList || {}; - - // All of the error & info-checking methods are put into an array. This is so that using _.some() will return - // early as soon as the first error / info condition is returned. This makes the checks very efficient since - // we only care if a single error / info condition exists anywhere. - const errorCheckingMethods = [ - () => !_.isEmpty(props.userWallet.errors), - () => PaymentMethods.hasPaymentMethodError(props.bankAccountList, paymentCardList), - () => _.some(cleanPolicies, PolicyUtils.hasPolicyError), - () => _.some(cleanPolicies, PolicyUtils.hasCustomUnitsError), - () => _.some(cleanAllPolicyMembers, PolicyUtils.hasPolicyMemberError), - () => !_.isEmpty(props.reimbursementAccount.errors), - () => UserUtils.hasLoginListError(props.loginList), - - // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) - () => !_.isEmpty(props.walletTerms.errors) && !props.walletTerms.chatReportID, - ]; - const infoCheckingMethods = [() => UserUtils.hasLoginListInfo(props.loginList)]; - const shouldShowErrorIndicator = _.some(errorCheckingMethods, (errorCheckingMethod) => errorCheckingMethod()); - const shouldShowInfoIndicator = !shouldShowErrorIndicator && _.some(infoCheckingMethods, (infoCheckingMethod) => infoCheckingMethod()); - - const indicatorColor = shouldShowErrorIndicator ? theme.danger : theme.success; - const indicatorStyles = [styles.alignItemsCenter, styles.justifyContentCenter, styles.statusIndicator(indicatorColor)]; - - return (shouldShowErrorIndicator || shouldShowInfoIndicator) && ; -} - -Indicator.defaultProps = defaultProps; -Indicator.propTypes = propTypes; -Indicator.displayName = 'Indicator'; - -export default withOnyx({ - allPolicyMembers: { - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - bankAccountList: { - key: ONYXKEYS.BANK_ACCOUNT_LIST, - }, - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - }, - fundList: { - key: ONYXKEYS.FUND_LIST, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, - walletTerms: { - key: ONYXKEYS.WALLET_TERMS, - }, - loginList: { - key: ONYXKEYS.LOGIN_LIST, - }, -})(Indicator); diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx new file mode 100644 index 000000000000..5332c4bd984f --- /dev/null +++ b/src/components/Indicator.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import {StyleSheet, View} from 'react-native'; +import {OnyxCollection, withOnyx} from 'react-native-onyx'; +import {OnyxEntry} from 'react-native-onyx/lib/types'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as UserUtils from '@libs/UserUtils'; +import useTheme from '@styles/themes/useTheme'; +import useThemeStyles from '@styles/useThemeStyles'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {BankAccountList, FundList, LoginList, Policy, PolicyMembers, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; + +type CheckingMethod = () => boolean; + +type IndicatorOnyxProps = { + /** The employee list of all policies (coming from Onyx) */ + allPolicyMembers: OnyxCollection; + + /** All the user's policies (from Onyx via withFullPolicy) */ + policies: OnyxCollection; + + /** List of bank accounts */ + bankAccountList: OnyxEntry; + + /** List of user cards */ + fundList: OnyxEntry; + + /** The user's wallet (coming from Onyx) */ + userWallet: OnyxEntry; + + /** Bank account attached to free plan */ + reimbursementAccount: OnyxEntry; + + /** Information about the user accepting the terms for payments */ + walletTerms: OnyxEntry; + + /** Login list for the user that is signed in */ + loginList: OnyxEntry; +}; + +type IndicatorProps = IndicatorOnyxProps; + +function Indicator({reimbursementAccount, allPolicyMembers, policies, bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + + // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and + // those should be cleaned out before doing any error checking + const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => !!policy)); + const cleanAllPolicyMembers = Object.fromEntries(Object.entries(allPolicyMembers ?? {}).filter(([, policyMembers]) => !!policyMembers)); + + // All of the error & info-checking methods are put into an array. This is so that using _.some() will return + // early as soon as the first error / info condition is returned. This makes the checks very efficient since + // we only care if a single error / info condition exists anywhere. + const errorCheckingMethods: CheckingMethod[] = [ + () => Object.keys(userWallet?.errors ?? {}).length > 0, + () => PaymentMethods.hasPaymentMethodError(bankAccountList, fundList), + () => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError), + () => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError), + () => Object.values(cleanAllPolicyMembers).some(PolicyUtils.hasPolicyMemberError), + () => Object.keys(reimbursementAccount?.errors ?? {}).length > 0, + () => !!loginList && UserUtils.hasLoginListError(loginList), + + // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) + () => Object.keys(walletTerms?.errors ?? {}).length > 0 && !walletTerms?.chatReportID, + ]; + const infoCheckingMethods: CheckingMethod[] = [() => !!loginList && UserUtils.hasLoginListInfo(loginList)]; + const shouldShowErrorIndicator = errorCheckingMethods.some((errorCheckingMethod) => errorCheckingMethod()); + const shouldShowInfoIndicator = !shouldShowErrorIndicator && infoCheckingMethods.some((infoCheckingMethod) => infoCheckingMethod()); + + const indicatorColor = shouldShowErrorIndicator ? theme.danger : theme.success; + const indicatorStyles = [styles.alignItemsCenter, styles.justifyContentCenter, styles.statusIndicator(indicatorColor)]; + + return (shouldShowErrorIndicator || shouldShowInfoIndicator) && ; +} + +Indicator.displayName = 'Indicator'; + +export default withOnyx({ + allPolicyMembers: { + key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + bankAccountList: { + key: ONYXKEYS.BANK_ACCOUNT_LIST, + }, + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, + fundList: { + key: ONYXKEYS.FUND_LIST, + }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, + loginList: { + key: ONYXKEYS.LOGIN_LIST, + }, +})(Indicator); diff --git a/src/components/InlineCodeBlock/WrappedText.js b/src/components/InlineCodeBlock/WrappedText.js deleted file mode 100644 index f00ec891116b..000000000000 --- a/src/components/InlineCodeBlock/WrappedText.js +++ /dev/null @@ -1,77 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {Fragment} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; -import Text from '@components/Text'; -import useThemeStyles from '@styles/useThemeStyles'; -import CONST from '@src/CONST'; - -/** - * Breaks the text into matrix - * for eg: My Name is Rajat - * [ - * [My,' ',Name,' ',' ',is,' ',Rajat], - * ] - * - * @param {String} text - * @returns {Array} - */ -function getTextMatrix(text) { - return _.map(text.split('\n'), (row) => _.without(row.split(CONST.REGEX.SPACE_OR_EMOJI), '')); -} - -const propTypes = { - /** Required text */ - children: PropTypes.string.isRequired, - - /** Style to be applied to Text */ - // eslint-disable-next-line react/forbid-prop-types - textStyles: PropTypes.arrayOf(PropTypes.object), - - /** Style for each word(Token) in the text, remember that token also includes whitespaces among words */ - // eslint-disable-next-line react/forbid-prop-types - wordStyles: PropTypes.arrayOf(PropTypes.object), -}; - -const defaultProps = { - textStyles: [], - wordStyles: [], -}; - -function WrappedText(props) { - const styles = useThemeStyles(); - if (!_.isString(props.children)) { - return null; - } - - const textMatrix = getTextMatrix(props.children); - return ( - <> - {_.map(textMatrix, (rowText, rowIndex) => ( - - {_.map(rowText, (colText, colIndex) => ( - // Outer View is important to vertically center the Text - - - {colText} - - - ))} - - ))} - - ); -} - -WrappedText.propTypes = propTypes; -WrappedText.defaultProps = defaultProps; -WrappedText.displayName = 'WrappedText'; - -export default WrappedText; diff --git a/src/components/InlineCodeBlock/WrappedText.tsx b/src/components/InlineCodeBlock/WrappedText.tsx new file mode 100644 index 000000000000..6dbd17f18e2a --- /dev/null +++ b/src/components/InlineCodeBlock/WrappedText.tsx @@ -0,0 +1,66 @@ +import React, {Fragment} from 'react'; +import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import Text from '@components/Text'; +import useThemeStyles from '@styles/useThemeStyles'; +import CONST from '@src/CONST'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type WrappedTextProps = ChildrenProps & { + /** Style to be applied to Text */ + textStyles?: StyleProp; + + /** + * Style for each individual word (token) in the text. Note that a token can also include whitespace characters between words. + */ + wordStyles?: StyleProp; +}; + +/** + * Breaks the text into matrix + * + * @example + * const text = "My Name is Rajat"; + * const resultMatrix = getTextMatrix(text); + * console.log(resultMatrix); + * // Output: + * // [ + * // ['My', ' ', 'Name', ' ', 'is', ' ', 'Rajat'], + * // ] + */ +function getTextMatrix(text: string): string[][] { + return text.split('\n').map((row) => row.split(CONST.REGEX.SPACE_OR_EMOJI).filter((value) => value !== '')); +} + +function WrappedText({children, wordStyles, textStyles}: WrappedTextProps) { + const styles = useThemeStyles(); + + if (typeof children !== 'string') { + return null; + } + + const textMatrix = getTextMatrix(children); + + return textMatrix.map((rowText, rowIndex) => ( + + {rowText.map((colText, colIndex) => ( + // Outer View is important to vertically center the Text + + + {colText} + + + ))} + + )); +} + +WrappedText.displayName = 'WrappedText'; + +export default WrappedText; diff --git a/src/components/InlineCodeBlock/index.js b/src/components/InlineCodeBlock/index.js deleted file mode 100644 index 84666931d9b2..000000000000 --- a/src/components/InlineCodeBlock/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import Text from '@components/Text'; -import inlineCodeBlockPropTypes from './inlineCodeBlockPropTypes'; - -function InlineCodeBlock(props) { - const TDefaultRenderer = props.TDefaultRenderer; - const textStyles = _.omit(props.textStyle, 'textDecorationLine'); - - return ( - - {props.defaultRendererProps.tnode.data} - - ); -} - -InlineCodeBlock.propTypes = inlineCodeBlockPropTypes; -InlineCodeBlock.displayName = 'InlineCodeBlock'; -export default InlineCodeBlock; diff --git a/src/components/InlineCodeBlock/index.native.js b/src/components/InlineCodeBlock/index.native.tsx similarity index 50% rename from src/components/InlineCodeBlock/index.native.js rename to src/components/InlineCodeBlock/index.native.tsx index 983463222532..308b88e76e88 100644 --- a/src/components/InlineCodeBlock/index.native.js +++ b/src/components/InlineCodeBlock/index.native.tsx @@ -1,26 +1,27 @@ import React from 'react'; +import type {TText} from 'react-native-render-html'; import useThemeStyles from '@styles/useThemeStyles'; -import inlineCodeBlockPropTypes from './inlineCodeBlockPropTypes'; +import type InlineCodeBlockProps from './types'; import WrappedText from './WrappedText'; -function InlineCodeBlock(props) { +function InlineCodeBlock({TDefaultRenderer, defaultRendererProps, textStyle, boxModelStyle}: InlineCodeBlockProps) { const styles = useThemeStyles(); - const TDefaultRenderer = props.TDefaultRenderer; + return ( - {props.defaultRendererProps.tnode.data} + {defaultRendererProps.tnode.data} ); } -InlineCodeBlock.propTypes = inlineCodeBlockPropTypes; InlineCodeBlock.displayName = 'InlineCodeBlock'; + export default InlineCodeBlock; diff --git a/src/components/InlineCodeBlock/index.tsx b/src/components/InlineCodeBlock/index.tsx new file mode 100644 index 000000000000..0802d4752661 --- /dev/null +++ b/src/components/InlineCodeBlock/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import {StyleSheet} from 'react-native'; +import type {TText} from 'react-native-render-html'; +import Text from '@components/Text'; +import type InlineCodeBlockProps from './types'; + +function InlineCodeBlock({TDefaultRenderer, textStyle, defaultRendererProps, boxModelStyle}: InlineCodeBlockProps) { + const flattenTextStyle = StyleSheet.flatten(textStyle); + const {textDecorationLine, ...textStyles} = flattenTextStyle; + + return ( + + {defaultRendererProps.tnode.data} + + ); +} + +InlineCodeBlock.displayName = 'InlineCodeBlock'; + +export default InlineCodeBlock; diff --git a/src/components/InlineCodeBlock/inlineCodeBlockPropTypes.js b/src/components/InlineCodeBlock/inlineCodeBlockPropTypes.js deleted file mode 100644 index e8430d17d849..000000000000 --- a/src/components/InlineCodeBlock/inlineCodeBlockPropTypes.js +++ /dev/null @@ -1,10 +0,0 @@ -import PropTypes from 'prop-types'; - -const inlineCodeBlockPropTypes = { - TDefaultRenderer: PropTypes.func.isRequired, - defaultRendererProps: PropTypes.object.isRequired, - boxModelStyle: PropTypes.any.isRequired, - textStyle: PropTypes.any.isRequired, -}; - -export default inlineCodeBlockPropTypes; diff --git a/src/components/InlineCodeBlock/types.ts b/src/components/InlineCodeBlock/types.ts new file mode 100644 index 000000000000..a100177e41a7 --- /dev/null +++ b/src/components/InlineCodeBlock/types.ts @@ -0,0 +1,11 @@ +import {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {TDefaultRenderer, TDefaultRendererProps, TText} from 'react-native-render-html'; + +type InlineCodeBlockProps = { + TDefaultRenderer: TDefaultRenderer; + textStyle: StyleProp; + defaultRendererProps: TDefaultRendererProps; + boxModelStyle: StyleProp; +}; + +export default InlineCodeBlockProps; diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 053f201ac109..9b7141249180 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -208,7 +208,7 @@ function OptionRowLHN(props) { props.isFocused ? styles.sidebarLinkActive : null, (hovered || isContextMenuActive) && !props.isFocused ? props.hoverStyle || styles.sidebarLinkHover : null, ]} - role={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.navigatesToChat')} needsOffscreenAlphaCompositing={props.optionItem.icons.length >= 2} > diff --git a/src/components/LocationErrorMessage/BaseLocationErrorMessage.js b/src/components/LocationErrorMessage/BaseLocationErrorMessage.js index a86cb8bd7bd9..5c0552e6bce7 100644 --- a/src/components/LocationErrorMessage/BaseLocationErrorMessage.js +++ b/src/components/LocationErrorMessage/BaseLocationErrorMessage.js @@ -64,7 +64,7 @@ function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationEr onPress={onClose} onMouseDown={(e) => e.preventDefault()} style={[styles.touchableButtonImage]} - role={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.close')} > diff --git a/src/components/MapView/index.js b/src/components/MapView/index.js deleted file mode 100644 index 551f57e34ed2..000000000000 --- a/src/components/MapView/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import MapView from './MapView'; - -export default MapView; diff --git a/src/components/MapView/index.tsx b/src/components/MapView/index.tsx new file mode 100644 index 000000000000..f273845fe4c0 --- /dev/null +++ b/src/components/MapView/index.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import MapView from './MapView'; +import {ComponentProps} from './types'; + +function MapViewComponent(props: ComponentProps) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +export default MapViewComponent; diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 0e07fcd22b4c..a56729f630d5 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -189,7 +189,7 @@ const MenuItem = React.forwardRef((props, ref) => { ]} disabled={props.disabled} ref={ref} - role={CONST.ACCESSIBILITY_ROLE.MENUITEM} + role={CONST.ROLE.MENUITEM} accessibilityLabel={props.title ? props.title.toString() : ''} > {({pressed}) => ( diff --git a/src/components/MessagesRow.tsx b/src/components/MessagesRow.tsx index 02b78942dfcf..c0b0a7807deb 100644 --- a/src/components/MessagesRow.tsx +++ b/src/components/MessagesRow.tsx @@ -48,7 +48,7 @@ function MessagesRow({messages = {}, type, onClose = () => {}, containerStyles, diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 880e46b2592a..9a1f59d64efa 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -75,19 +75,24 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const policyType = lodashGet(policy, 'type'); const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; + const isGroupPolicy = _.contains([CONST.POLICY.TYPE.CORPORATE, CONST.POLICY.TYPE.TEAM], policyType); const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === moneyRequestReport.managerID; - const isPayer = policyType === CONST.POLICY.TYPE.CORPORATE ? isPolicyAdmin && isApproved : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); + const isPayer = isGroupPolicy + ? // In a group policy, the admin approver can pay the report directly by skipping the approval step + isPolicyAdmin && (isApproved || isManager) + : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport); - const shouldShowSettlementButton = useMemo( + const shouldShowPayButton = useMemo( () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport), [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport], ); const shouldShowApproveButton = useMemo(() => { - if (policyType !== CONST.POLICY.TYPE.CORPORATE) { + if (!isGroupPolicy) { return false; } return isManager && !isDraft && !isApproved && !isSettled; - }, [policyType, isManager, isDraft, isApproved, isSettled]); + }, [isGroupPolicy, isManager, isDraft, isApproved, isSettled]); + const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextSteps = isFromPaidPolicy && nextStep && !_.isEmpty(nextStep.message); @@ -120,22 +125,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt onPress={(paymentType) => IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)} enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS} addBankAccountRoute={bankAccountRoute} + shouldHidePaymentOptions={!shouldShowPayButton} + shouldShowApproveButton={shouldShowApproveButton} style={[styles.pv2]} formattedAmount={formattedAmount} /> )} - {shouldShowApproveButton && !isSmallScreenWidth && ( - -