diff --git a/.github/actions/composite/buildAndroidAPK/action.yml b/.github/actions/composite/buildAndroidAPK/action.yml index 819234df0bc3..fc280ab2a223 100644 --- a/.github/actions/composite/buildAndroidAPK/action.yml +++ b/.github/actions/composite/buildAndroidAPK/action.yml @@ -13,7 +13,7 @@ runs: - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 with: - ruby-version: '2.7' + ruby-version: "2.7" bundler-cache: true - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef @@ -26,4 +26,4 @@ runs: uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05 with: name: ${{ inputs.ARTIFACT_NAME }} - path: android/app/build/outputs/apk/e2eRelease/app-e2eRelease.apk + path: android/app/build/outputs/apk/e2e/release/app-e2e-release.apk diff --git a/.github/actions/javascript/getPullRequestDetails/action.yml b/.github/actions/javascript/getPullRequestDetails/action.yml index a59cf55bdf9f..ed2c60f018a1 100644 --- a/.github/actions/javascript/getPullRequestDetails/action.yml +++ b/.github/actions/javascript/getPullRequestDetails/action.yml @@ -13,8 +13,14 @@ inputs: outputs: MERGE_COMMIT_SHA: description: 'The merge_commit_sha of the given pull request' + HEAD_COMMIT_SHA: + description: 'The head_commit_sha of the given pull request' MERGE_ACTOR: description: 'The actor who merged the pull request' + IS_MERGED: + description: 'True if the pull request is merged' + FORKED_REPO_URL: + description: 'Output forked repo URL if PR includes changes from a fork' runs: using: 'node16' main: './index.js' diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index d8f9cad138d9..f7f1e5fc7ac7 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -84,12 +84,7 @@ jobs: - name: Unmerged PR - Fetch head ref of unmerged PR if: ${{ !fromJSON(steps.getPullRequestDetails.outputs.IS_MERGED) }} run: | - if [[ ${{ steps.getPullRequestDetails.outputs.FORKED_REPO_URL }} != '' ]]; then - git remote add pr_remote ${{ steps.getPullRequestDetails.outputs.FORKED_REPO_URL }} - git fetch pr_remote ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} --no-tags --depth=1 - else - git fetch origin ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} --no-tags --depth=1 - fi + git fetch origin ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} --no-tags --depth=1 - name: Unmerged PR - Set dummy git credentials before merging if: ${{ !fromJSON(steps.getPullRequestDetails.outputs.IS_MERGED) }} @@ -101,7 +96,7 @@ jobs: if: ${{ !fromJSON(steps.getPullRequestDetails.outputs.IS_MERGED) }} id: getMergeCommitShaIfUnmergedPR run: | - git merge --no-commit ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} + git merge --allow-unrelated-histories --no-commit ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} git checkout ${{ steps.getPullRequestDetails.outputs.HEAD_COMMIT_SHA }} env: GITHUB_TOKEN: ${{ github.token }} @@ -140,18 +135,19 @@ jobs: name: baseline-apk-${{ needs.buildBaseline.outputs.VERSION }} path: zip - # The downloaded artifact will be a file named "app-e2eRelease.apk" so we have to rename it + # The downloaded artifact will be a file named "app-e2e-release.apk" so we have to rename it - name: Rename baseline APK - run: mv "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease.apk" "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease-baseline.apk" + run: mv "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2e-release.apk" "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease-baseline.apk" - name: Download delta APK uses: actions/download-artifact@e9ef242655d12993efdcda9058dee2db83a2cb9b + id: downloadDeltaAPK with: name: delta-apk-${{ needs.buildDelta.outputs.DELTA_REF }} path: zip - name: Rename delta APK - run: mv "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease.apk" "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease-compare.apk" + run: mv "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2e-release.apk" "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2eRelease-compare.apk" - name: Copy e2e code into zip folder run: cp -r tests/e2e zip diff --git a/.storybook/preview.js b/.storybook/preview.js index b198c0d2d626..a989960794f2 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -6,7 +6,7 @@ import './fonts.css'; import ComposeProviders from '../src/components/ComposeProviders'; import HTMLEngineProvider from '../src/components/HTMLEngineProvider'; import OnyxProvider from '../src/components/OnyxProvider'; -import {LocaleContextProvider} from '../src/components/withLocalize'; +import {LocaleContextProvider} from '../src/components/LocaleContextProvider'; import {KeyboardStateProvider} from '../src/components/withKeyboardState'; import {EnvironmentProvider} from '../src/components/withEnvironment'; import {WindowDimensionsProvider} from '../src/components/withWindowDimensions'; diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association index a9e2b0383691..d6da0232f2fc 100644 --- a/.well-known/apple-app-site-association +++ b/.well-known/apple-app-site-association @@ -75,6 +75,10 @@ { "/": "/teachersunite/*", "comment": "Teachers Unite!" + }, + { + "/": "/search/*", + "comment": "Search" } ] } diff --git a/android/app/build.gradle b/android/app/build.gradle index bcac489f6828..1e2e9b46fad6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -58,7 +58,7 @@ project.ext.envConfigFiles = [ adhocRelease: ".env.adhoc", developmentRelease: ".env", developmentDebug: ".env", - e2eRelease: ".env.production" + e2eRelease: "tests/e2e/.env.e2e" ] /** @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001037402 - versionName "1.3.74-2" + versionCode 1001037503 + versionName "1.3.75-3" } flavorDimensions "default" @@ -136,10 +136,20 @@ android { signingConfig signingConfigs.debug } release { - signingConfig signingConfigs.release productFlavors.production.signingConfig signingConfigs.release minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + + signingConfig null + // buildTypes take precedence over productFlavors when it comes to the signing configuration, + // thus we need to manually set the signing config, so that the e2e uses the debug config again. + // In other words, the signingConfig setting above will be ignored when we build the flavor in release mode. + productFlavors.all { flavor -> + // All release builds should be signed with the release config ... + flavor.signingConfig signingConfigs.release + } + // ... except for the e2e flavor, which we maybe want to build locally: + productFlavors.e2e.signingConfig signingConfigs.debug } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8d69c62bfd1f..dc135fa9834e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -67,6 +67,7 @@ + @@ -83,6 +84,7 @@ + diff --git a/assets/images/chatbubbles.svg b/assets/images/chatbubbles.svg new file mode 100644 index 000000000000..6194c43e631e --- /dev/null +++ b/assets/images/chatbubbles.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/assets/images/google-meet.svg b/assets/images/google-meet.svg index 138a11859321..980cd102f67a 100644 --- a/assets/images/google-meet.svg +++ b/assets/images/google-meet.svg @@ -1,8 +1,7 @@ - \ No newline at end of file + + + + + diff --git a/assets/images/zoom-icon.svg b/assets/images/zoom-icon.svg index 6c6ed03cb2f3..24d019654795 100644 --- a/assets/images/zoom-icon.svg +++ b/assets/images/zoom-icon.svg @@ -1 +1,7 @@ - \ No newline at end of file + + + + + diff --git a/docs/articles/expensify-classic/account-settings/Account-Access.md b/docs/articles/expensify-classic/account-settings/Account-Access.md deleted file mode 100644 index b3126201715f..000000000000 --- a/docs/articles/expensify-classic/account-settings/Account-Access.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Account Access -description: Account Access ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/account-settings/Account-Details.md b/docs/articles/expensify-classic/account-settings/Account-Details.md new file mode 100644 index 000000000000..46a6c6ba0c25 --- /dev/null +++ b/docs/articles/expensify-classic/account-settings/Account-Details.md @@ -0,0 +1,5 @@ +--- +title: Account Details +description: Account Details +--- +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md b/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md index 36e0a2194d24..b5f5ec8be048 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Apply-Tax.md @@ -1,5 +1,39 @@ --- title: Apply Tax -description: Apply Tax +description: This is article shows you how to apply taxes to your expenses! --- -## Resource Coming Soon! + + + +# About + +There are two types of tax in Expensify: Simple Tax (i.e. one tax rate) and Complex Tax (i.e. more than one tax rate). This article shows you how to apply both to your expenses! + + +# How-to Apply Tax + +When Tax Tracking is enabled on a Workspace, the default tax rate is selected under **Settings > Workspace > _Workspace Name_ > Tax**, with the default tax rate applied to all expenses automatically. + +There may be multiple tax rates set up within your Workspace, so if the tax on your receipt is different to the default tax that has been applied, you can select the appropriate rate from the tax drop-down on the web expense editor or the mobile app. + +If the tax amount on your receipt is different to the calculated amount or the tax rate doesn’t show up, you can always manually type in the correct tax amount. + + +# FAQ + +## How do I set up multiple taxes (GST/PST/QST) on indirect connections? +Expenses sometimes have more than one tax applied to them - for example in Canada, expenses can have both a Federal GST and a provincial PST or QST. + +To handle these, you can create a single tax that combines both taxes into a single effective tax rate. For example, if you have a GST of 5% and PST of 7%, adding the two tax rates together gives you an effective tax rate of 12%. + +From the Reports page, you can select Reports and then click **Export To > Tax Report** to generate a CSV containing all the expense information, including the split-out taxes. + + +# Deep Dive + +If you have a receipt that has more than one tax rate (i.e. Complex Tax) on it, then there are two options for handling this in Expensify! + +Many tax authorities do not require the reporting of tax amounts by rate and the easiest approach is to apply the highest rate on the receipt and then modify the tax amount to reflect the amount shown on the receipt if this is less. Please check with your local tax advisor if this approach will be allowed. + +Alternatively, you can apply each specific tax rate by splitting the expense into the components that each rate will be applied to. To do this, click on **Split Expense** and apply the correct tax rate to each part. + diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md b/docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md index e565e59dc754..7fa714189542 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Create-Expenses.md @@ -48,11 +48,11 @@ You can also create a number of future 'placeholder' expenses for your recurring # How to Edit Bulk Expenses Editing expenses in bulk will allow you to apply the same coding across multiple expenses and is a web-only feature. To bulk edit expenses: Go to the Expenses page. -To narrow down your selection, use the filters (e.g. "Merchant" and "Open") to find the specific expenses you want to edit. +To narrow down your selection, use the filters (e.g. "Merchant" and "Draft") to find the specific expenses you want to edit. Select all the expenses you want to edit. Click on the **Edit Multiple** button at the top of the page. # How to Edit Expenses on a Report -If you’d like to edit expenses within an Open report: +If you’d like to edit expenses within a Draft report: 1. Click on the Report containing all the expenses. 2. Click on **Details**. @@ -61,8 +61,8 @@ If you’d like to edit expenses within an Open report: If you've already submitted your report, you'll need to Retract it or have it Unapproved first before you can edit the expenses. - # FAQ + ## Does Expensify account for duplicates? Yes, Expensify will account for duplicates. Expensify works behind the scenes to identify duplicate expenses before they are submitted, warning employees when they exist. If a duplicate expense is submitted, the same warning will be shown to the approver responsible for reviewing the report. @@ -71,6 +71,7 @@ If two expenses are SmartScanned on the same day for the same amount, they will The expenses were split from a single expense, The expenses were imported from a credit card, or Matching email receipts sent to receipts@expensify.com were received with different timestamps. + ## How do I resolve a duplicate expense? If Concierge has let you know it's flagged a receipt as a duplicate, scanning the receipt again will trigger the same duplicate flagging.Users have the ability to resolve duplicates by either deleting the duplicated transactions, merging them, or ignoring them (if they are legitimately separate expenses of the same date and amount). @@ -88,12 +89,13 @@ Click the **Undelete** button and you're all set. You’ll find the expense on y ## What are the different Expense statuses? There are a number of different expense statuses in Expensify: -1. **Unreported**: Unreported expenses are not yet part of a report (and therefore unsubmitted) and are not viewable by anyone but the expense creator/owner. -2. **Open**: Open expenses are on a report that's still in progress, and are unsubmitted. Your Policy Admin will be able to view them, making it a collaborative step toward reimbursement. +1. **Personal**: Personal expenses are not yet part of a report (and therefore unsubmitted) and are not viewable by anyone but the expense creator/owner. +2. **Draft**: Draft expenses are seen as still in progress, and are unsubmitted. Your Policy Admin will be able to view them, making this a collaborative step toward reimbursement. 3. **Processing**: Processing expenses are submitted, but waiting for approval. 4. **Approved**: If it's a non-reimbursable expense, the workflow is complete at this point. If it's a reimbursable expense, you're one step closer to getting paid. 5. **Reimbursed**: Reimbursed expenses are fully settled. You can check the Report Comments to see when you'll get paid. 6. **Closed**: Sometimes an expense accidentally ends up on your Individual Policy, falling into the Closed status. You’ll need to reopen the report and change the Policy by clicking on the **Details** tab in order to resubmit your report. + ## What are Violations? Violations represent errors or discrepancies that Expensify has picked up and need to be corrected before a report can be successfully submitted. The one exception is when an expense comment is added, it will override the violation - as the user is providing a valid reason for submission. @@ -101,8 +103,9 @@ Violations represent errors or discrepancies that Expensify has picked up and ne To enable or configure violations according to your policy, go to **Settings > Policies > _Policy Name_ > Expenses > Expense Violations**. Keep in mind that Expensify includes certain system mandatory violations that can't be disabled, even if your policy has violations turned off. You can spot violations by the exclamation marks (!) attached to expenses. Hovering over the symbol will provide a brief description and you can find more detailed information below the list of expenses. The two types of violations are: -**Red**: These indicate violations directly tied to your report's Policy settings. They are clear rule violations that must be addressed before submission. -**Yellow**: Concierge will highlight items that require attention but may not necessarily need corrective action. For example, if a receipt was SmartScanned and then the amount was modified, we’ll bring it to your attention so that it can be manually reviewed. +1. **Red**: These indicate violations directly tied to your report's Policy settings. They are clear rule violations that must be addressed before submission. +2. **Yellow**: Concierge will highlight items that require attention but may not necessarily need corrective action. For example, if a receipt was SmartScanned and then the amount was modified, we’ll bring it to your attention so that it can be manually reviewed. + ## How to Track Attendees Attendee tracking makes it easy to track shared expenses and maintain transparency in your group spending. @@ -116,9 +119,10 @@ External attendees are considered users outside your group policy or domain. To 1. Click or tap the **Attendee** field within your expense. 2. Type in the individual's name or email address. 3. Tap **Add** to include the attendee. -You can continue adding more attendees or save the Expense. +4. You can continue adding more attendees or save the Expense. + To remove an attendee from an expense: -Open the expense. -Click or tap the **Attendees** field to display the list of attendees. -From the list, de-select the attendees you'd like to remove from the expense. +1. Open the expense. +2. Click or tap the **Attendees** field to display the list of attendees. +3. From the list, de-select the attendees you'd like to remove from the expense. diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md b/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md index e7705a32f215..a8444b98c951 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Merge-Expenses.md @@ -1,5 +1,65 @@ --- title: Merge Expenses -description: Merge Expenses +description: This article shows you all the ways that you can merge your expenses in Expensify! --- -## Resource Coming Soon! + + +# About +The merge expense function helps combine two separate expenses into one. This is useful when the same expense has been accidentally entered more than once, or if you have a connected credit card and an imported expense didn’t automatically merge with a manual entry. + +# How-to merge expenses +It’s important to note that merging expenses doesn't add the two values together. Instead, merging them combines both expenses to create a single, consolidated expense. + +Keep in mind: +1. Merging expenses cannot be undone. +2. You can only merge two expenses at a time. +3. You can merge a cash expense with a credit card expense, or two cash expenses - but not two credit card expenses. +4. In order to merge, both expenses will need to be in a Personal or Draft status. + +# How to merge expenses on the web app +To merge two expenses from the Expenses page: +1. Sign into your Expensify account. +2. Navigate to the Expenses page on the left-hand navigation. +3. Click the checkboxes next to the two expenses you wish to merge. +4. Click **Merge**. +5. You'll be able to choose which aspect of each of the two expenses you would like to be used on the resulting expense, such as the receipt image, card, merchant, category, and more. + +To merge two expenses from the Reports page: +1. Sign into your Expensify account. +2. Navigate to the Reports page on the left-hand navigation. +3. Click the Report that contains the expenses that you wish to merge. +4. Click on the **Details** tab, then the Pencil icon. +5. Select the two expenses that you wish to merge. +6. You'll be able to choose which aspect of each of the two expenses you would like to be used on the resulting expense, such as the receipt image, card, merchant, category, and more. + +# How to merge expenses on the Expensify mobile app +On the mobile app, merging is prompted when you see the message _"Potential duplicate expense detected"_. Simply tap **Resolve Now** to take a closer look, then hit **Merge Expense**, and you're done! + +If the expenses exist on two different reports, you will be asked which report you'd like the newly created single expense to be reported onto. + +# FAQ + +## Can you merge expenses across different reports? + +You cannot merge expenses across different reports. Expenses will only merge if they are on the same report. If you have expenses across different reports that you wish to merge, you’ll need to move both expenses onto the same report (and ensure they are in the Draft status) in order to merge them. + +## Can you merge expenses across different accounts? + +You cannot merge expenses across two separate accounts. You will need to choose one submitter and transfer the expense information to that user's account in order to merge the expense. + +## Can you merge expenses with different currencies? + +Yes, you can merge expenses with different currencies. The conversion amount will be based on the daily exchange rate for the date of the transaction, as long as the converted rates are within +/- 5%. If the currencies are the same, then the amounts must be an exact match to merge. + +## Can Expensify automatically merge a cash expense with a credit card expense? + +Yes, Expensify can merge a cash expense with a credit card expense. A receipt will need to be SmartScanned via the app or forwarded to [receipts@expensify.com](mailto:receipts@expensify.com) in order to merge with a card expense. Note that the SmartScan must be fully completed and not stopped or edited, otherwise the two won’t merge. + +## It doesn’t look like my cash and card expenses merged properly. What are some troubleshooting tips? +First, check the expense types - you can only merge a SmartScanned receipt (which will initially show with a cash icon) with a card transaction imported from a bank or via CSV. + +If the card expense in your Expensify account is older than the receipt you're trying to merge it with, they won't merge, and if the receipt is dated more than 7 days prior to the card expense, then they also will not merge. + +If you have any expenses that are more than 90 days old from the date they were incurred (not the date they were imported to Expensify), Expensify will not automatically merge them. This safeguard helps prevent the merging of very old expenses that might not align with recent transactions or receipts. + +Lastly, transactions imported with the Expensify API (via the Expense Importer) will not automatically merge with SmartScanned transactions. diff --git a/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md b/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md index fb4f756b2820..ea808695e7cd 100644 --- a/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md +++ b/docs/articles/expensify-classic/get-paid-back/reports/Create-A-Report.md @@ -1,5 +1,166 @@ --- title: Create a Report -description: Create a Report +description: Learn how to create and edit reports in Expensify --- -## Resource Coming Soon! + + +# Overview + +This article covers all the basics of creating, editing, deleting and managing your reports. + +# How to create a report + +_Using the web app:_ + +To create a report on the Expensify website, click the New Report button on the **Reports** page. + +_Using the mobile app:_ + +Tap the ☰ icon. +Tap **Reports**. +Tap the **+** icon. +Choose your desired report type. + +# How to edit a report + +## Adding expenses to a report + +You can add expenses to the report by clicking **Add Expenses** at the top of the report. + +## Removing expenses from a report on the Expensify web app + +To remove expenses from the report on the web app, click the red ❌ next to the expense. + +## Removing expenses from a report on the Expensify mobile app + +To remove an expense on an Android device, hold the expense and tap **Delete**. + +To remove an expense on an iOS device, swipe the expense to the left and tap **Delete**. + +## Editing the report title + +To edit the report title, click the pencil icon next to the name. To save your changes, tap the enter key on your keyboard. + +**Note:** You may be unable to edit your reports' titles based on the settings. + +## Bulk-editing expenses on a report + +Click Details in the top-right of the report on the web app, then click the pencil icon to bring up the editing modal. You can click the pencil icon to the left of an expense to edit it, or you can edit multiple expenses at once by ticking the checkbox of the expenses you’d like to bulk-edit and then clicking **Edit Multiple** at the top of the modal. + +## Commenting on the report + +You can comment on the report by adding your comment to the **Report Comments** section at the bottom. Expensify will also log report actions here. + +## Attachments + +If you’d like to attach a photo or document to the report, follow the instructions below to add the attachment to your report comment section. + +_Using the web app:_ + +1. Click the **Paperclip** icon in the comment box of the **Report Comments** section. +2. Select the file to attach. +3. Check the preview of the attachment and click Upload. + +_Using the mobile app:_ + +1. Tap into the report. +2. Scroll to the bottom of the report and tap the paper clip icon to attach a file. + +**Note:** Report comments support jpeg, jpg, png, gif, csv, and pdf files. + +## Changing the report's workspace + +To change the report's workspace, click **Details** in the top-right of the report on the web app, then select the correct workspace from the **Workspace** drop-down. + +## Changing the report type (Expense Report/Invoice) + +To change the report type, click **Details** in the top-right of the report on the web app, then select the correct report type from the **Type** drop-down. + +## Changing the layout of the report + +There are three ways you can change the report layout under the Details section of the report. To do this, select the desired layout from the relevant drop-down menu: + + - **View** - Choose between a Basic or Detailed report view. + - **Group By** - Choose to group expenses on the report based on their Category or Tag. + - **Split By** - Split out the expenses based on their Reimbursable or Billable status. + +# How to submit a report + +1. Click **Submit** in the top-left of the report (or **Submit Report** at the top in the mobile app). +2. Verify the approver and click **Submit** again. + +# How to retract your report (Undo Submit) + +As long as the report is still in a Processing state, you can retract this submission to put the report back to Draft status to make corrections and re-submit. + +To retract a **Processing** report on the web app, click the Undo Submit button at the upper left-hand corner of the report. + +To complete this from the mobile app, simply open the report from within your app and click the **Retract** button at the top of the report. + +# How to share a report + +Click Details in the top-right of the report on the web app to bring up the sharing settings. The following options are available: + + - Click the **Printer** icon to print the report. + - Click the **Download** icon to download a PDF of the report + - Click the **Share** icon to share the report via email or SMS. + +# How to close a report + +You can close your report if you don't need it approved by your employer. + +_To close a report on the Expensify website:_ + +1. Navigate to the report in question. +2. Click **Mark as Closed** at the top of the report. +3. You can re-open a report once it’s closed by clicking **Undo Close** at the top of the report. + +# How to delete a report + +_Deleting a report on the web app:_ + +Click Details in the top-right of the report on the web app, then click the Trash icon to delete the report. Any expenses on the report will move to an Unreported state. + +_Deleting a report on the mobile app:_ + +To delete a Draft report on an Android, press and hold the report name and tap **Delete**. + +To delete a Draft report on an iOS device, go to the **Reports** screen, swipe the report to the left, and tap **Delete**. + +_Deleting a report in the Processing, Approved, Reimbursed or Closed state:_ + +If you want to delete a Processing or Closed report, please follow the How to undo your report submission instructions in this article to move the report back into an Draft status, then follow the steps above. + +If you want to delete an Approved or Reimbursed report, please speak to your Company Admin as this may not be possible. + +# How to move expenses between reports + +Navigate to your Expenses page. +Tick the checkbox next to each expense you'd like to move. +Click the Add To Report button in the top right corner. +Select your desired report from the drop-down. + +# How to use Guided Review to clean up your report + +Open your report on the web app and click Review at the top. The system will walk you through each violation on the report. +As you go through each violation, click View to look at the expense in more detail or resolve any violations. +Click Next to move on to the next item. +Click Finish to complete the review process when you’re done. + +# FAQ + +## Is there a difference between Expense Reports, Bills, and Invoices? + +**Expense Reports** are submitted by an employee to their employer. They contain either personally incurred expenses that the employee should be reimbursed for, or non-reimbursable expenses (such as company card expenses) incurred by the employee that require tracking for accounting purposes. + +**Invoices** are reports that a business or contractor will send to another business to charge them for goods or services the business received. Each invoice will have a matching **Bill** owned by the recipient so they may use it to pay the invoice sender. + +## Which report type should I use? + +If you bought something on a company card or need to be reimbursed by your employer, you’ll need an **Expense Report**. + +If someone external to the business sends you an invoice for their services, you’ll want a **Bill** (or even better - use our Bill Pay process) + +## When should I submit my report? + +Your Company Admin can answer this one, and they may have configured the workspace’s [Scheduled Submit] setting to enforce a regular cadence for you. If not, you can still set this up under your [Individual workspace]. diff --git a/docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md b/docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md index c2cc25b32373..a31c0a582fd7 100644 --- a/docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md +++ b/docs/articles/expensify-classic/get-paid-back/reports/Reimbursements.md @@ -1,5 +1,42 @@ ---- -title: Reimbursements -description: Reimbursements ---- -## Resource Coming Soon! +# Overview + +If you want to know more about how and when you’ll be reimbursed through Expensify, we’ve answered your questions below. + +# How to Get Reimbursed + +To get paid back after submitting a report for reimbursement, you’ll want to be sure to connect your bank account. You can do that under **Settings** > **Account** > **Payments** > **Add a Deposit Account**. Once your employer has approved your report, the reimbursement will be paid into the account you added. + +# Deep Dive + +## Reimbursement Timing + +### US Bank Accounts + +If your company uses Expensify's ACH reimbursement we'll first check to see if the report is eligible for Rapid Reimbursement (next business day). For a report to be eligible for Rapid Reimbursement it must fall under two limits: + + - $100 per deposit bank account per day or less for the individuals being reimbursed or businesses receiving payments for bills. + - Less than $10,000 being disbursed in a 24-hour time period from the verified bank account being used to pay the reimbursement. + +If the request passes both checks, then you can expect to see funds deposited into your bank account on the next business day. + +If either limit has been reached, then you can expect to see funds deposited within your bank account within the typical ACH timeframe of 3-5 business days. + +### International Bank Accounts + +If receiving reimbursement to an international deposit account via Global Reimbursement, you should expect to see funds deposited in your bank account within 4 business days. + +## Bank Processing Timeframes + +Banks only process transactions and ACH activity on weekdays that are not bank holidays. These are considered business days. Additionally, the business day on which a transaction will be processed depends upon whether or not a request is created before or after the cutoff time, which is typically 3 pm PST. +For example, if your reimbursement is initiated at 4 pm on Wednesday, this is past the bank's cutoff time, and it will not begin processing until the next business day. +If that same reimbursement starts processing on Thursday, and it's estimated to take 3-5 business days, this will cover a weekend, and both days are not considered business days. So, assuming there are no bank holidays added into this mix, here is how that reimbursement timeline would play out: + +**Wednesday**: Reimbursement initiated after 3 pm PST; will be processed the next business day by your company’s bank. +**Thursday**: Your company's bank will begin processing the withdrawal request +**Friday**: Business day 1 +**Saturday**: Weekend +**Sunday**: Weekend +**Monday**: Business day 2 +**Tuesday**: Business day 3 +**Wednesday**: Business day 4 +**Thursday**: Business day 5 diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Greenhouse.md b/docs/articles/expensify-classic/integrations/HR-integrations/Greenhouse.md index 3ee1c8656b4b..b44e5a090d17 100644 --- a/docs/articles/expensify-classic/integrations/HR-integrations/Greenhouse.md +++ b/docs/articles/expensify-classic/integrations/HR-integrations/Greenhouse.md @@ -1,5 +1,43 @@ --- -title: Coming Soon -description: Coming Soon +title: Greenhouse Integration +description: Automatically send candidates from Greenhouse to Expensify for easy reimbursement --- -## Resource Coming Soon! + +# Overview +Expensify's direct integration with Greenhouse allows you to automatically send candidates from Greenhouse to Expensify for easy reimbursement. The integration can set the candidate's recruiter or recruiting coordinator as approver in Expensify. + +## Prerequisites of the integration +- You must be a Workspace Admin in Expensify and an Admin in Greenhouse with Developer Permissions to complete this connection. This can be the same person or two different people. +- Each Greenhouse candidate record must have an email address in order to send to Expensify since we use this as the unique identifier in Expensify. +- We highly recommend that you create a specific Expensify workspace for candidates so that you can set up a separate workflow and a different set of Categories and Tags from what your employees would see. + +# How to connect Greenhouse to Expensify +## Establish the connection from Expensify + +1. Log into Expensify as a Workspace admin and navigate to **Settings > Workspaces > _[Workspace Name]_ > Connections** +2. Under Greenhouse, click **Connect to Greenhouse** then click **Sync with Greenhouse**, which will open the "Greenhouse Integration" instructions page in a new browser window + +## Create the Web hook + +1. Click the link under Step 1 on the Greenhouse Integration instructions page, or log into your Greenhouse account and navigate to **Configure > Dev Center > Web Hooks > Web Hooks**. +2. After landing on the "Create a New Web Hook" page, follow the steps on the Greenhouse Integration instructions page to create the web hook. + +## Create the custom candidate field + +1. Click the link under Step 2 on the Greenhouse Integration instructions page, or log into your Greenhouse account and navigate to **Configure > Custom Options > Custom Company Fields > Candidates** +2. Follow the steps on the Greenhouse Integration instructions page to create the custom Candidate field. +3. Click **Finish** (Step 3 on the Greenhouse Integration instructions page) to finish connecting Greenhouse with Expensify. + +# How to send candidates from Greenhouse to Expensify +## In Greenhouse: + +1. Log into Greenhouse and go to any candidate’s Details tab +2. Confirm that the Email field is filled in +3. Optionally select the Recruiter field to set the recruiter as the candidate's expense approver in Expensify (Note: if you'd prefer to have the Recruiting Coordinator used as the default approver, please reach out to concierge@expensify.com or your account manager to request that we change the default approver on your behalf) +4. Send this candidate to Expensify by toggling the **Invite to Expensify** field to **Yes** and clicking **Save** + +## In Expensify: + +1. Navigate to **Settings > Policies > Group > _[Workspace Name]_ > Members** +2. The candidate you just sent to Expensify should be listed in the workspace members list +3. If the Recruiter (or Recruiting Coordinator) field was filled in in Greenhouse, the candidate will already be configured to submit reports to that recruiter for approval. If no Recruiter was selected, then the candidate will submit based on the Expensify workspace approval settings. diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/Rippling.md b/docs/articles/expensify-classic/integrations/HR-integrations/Rippling.md index 3ee1c8656b4b..fa4aaec3376f 100644 --- a/docs/articles/expensify-classic/integrations/HR-integrations/Rippling.md +++ b/docs/articles/expensify-classic/integrations/HR-integrations/Rippling.md @@ -1,5 +1,13 @@ --- -title: Coming Soon -description: Coming Soon +title: Rippling Integration +description: Sync employee and expense data between Expensify and Rippling --- -## Resource Coming Soon! +# Overview +The Rippling integration allows mutual customers to sync employee and expense data between Expensify and Rippling. The Rippling integration allows you to: +1. **Automate Employee Management:** Automatically create and remove employee accounts in Expensify directly from Rippling. +2. **Simplify Employee Access to Expensify:** Employees can sign into Expensify from their Rippling SSO bar using SAML single sign-on. +3. **Import Reimbursable Expense Reports:** Admins can export reimbursable expense reports from Expensify directly into Rippling Payroll. + +# How to use the Rippling integration +The Rippling team manages this integration. To connect your Expensify workspace with Rippling, please visit the Rippling App Shop and sign into your Rippling account. +For instructions on how to connect, and for troubleshooting the integration, please contact the Rippling support team by emailing support@rippling.com. diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md index a65dc378a793..65238457f1a9 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md @@ -9,34 +9,34 @@ This guide is for those who are part of a **Group Workspace**. Each member has a role that defines what they can see and do in the workspace. Most members will have the role of "Employee." -# How to Manage User Roles +# How to manage user roles -To find and edit the roles of group workspace members, go to **Settings > Workspaces > Group > [Your Specific Workspace Name] > Members > Workspace Members** +To find and edit the roles of group workspace members, go to **Settings > Workspaces > Group > _[Workspace Name]_ > Members > Workspace Members** Here you'll see the list of members in your group workspace. To change their roles, click **Settings** next to the member’s name and choose the role that the member needs. Next, let’s go over the various user roles that are available on a group workspace. -## The Employee Role +### The Employee Role - **What can they do:** Employees can only see their own expense reports or reports that have been submitted to or shared with them. They can't change settings or invite new users. - **Who is it for:** Regular employees who only need to manage their own expenses, or managers who are reviewing expense reports for a few users but don’t need global visibility. - **Approvers:** Members who approve expenses can either be Employees, Admins, or Workspace Auditors, depending on how much control they need. - **Billable:** Employees are billable actors if they take actions on a report on your Group Workspace (including **SmartScanning** a receipt). -## Workspace Admin Role +### Workspace Admin Role - **What can they do:** Admins have full control. They can change settings, invite members, and view all reports. They can also process reimbursements if they have access to the company’s account. - **Billing Owners:** Billing owners are Admins by default. **Workspace Admins** are assigned by the owner or another admin. - **Billable:** Yes, if they perform actions like changing settings or inviting users. Just viewing reports is not billable. -## Workspace Auditor Role +### Workspace Auditor Role - **What can they do:** Workspace Auditors can see all reports, make comments, and export them. They can also mark reports as reimbursed if they're the final approver. - **Who is it for:** Accountants, bookkeepers, and internal or external audit agents who need to view but not edit workspace settings. - **Billable:** Yes, if they perform any actions like commenting or exporting a report. Viewing alone doesn't incur a charge. -## Technical Contact +### Technical Contact - **What can they do:** In case of connection issues, alerts go to the billing owner by default. You can set a technical contact if you want alerts to go to an IT administrator instead. - **How to set one:** Go to **Settings > Workspaces > Group > [Workspace Name] > Connections > Technical Contact**. diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md index e107734216f5..7c21b12a83e1 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate.md @@ -1,8 +1,54 @@ --- -title: Coming Soon -description: Coming Soon +title: Vacation Delegate +description: In Expensify, a vacation delegate is someone you choose to act on your behalf when you're on vacation or taking personal time off. --- -## Resource Coming Soon! + +# Overview + +A delegate is someone who can handle approving expense reports for you, which is especially useful when you're out of the office! + +In Expensify, a **Vacation Delegate** is someone you choose to act on your behalf when you're on vacation or taking personal time off. They will approve expense reports just like you would, and everything moves forward as usual afterward. + +The system keeps a detailed audit trail, showing exactly when your delegate stepped in to approve a report for you. And if your delegate also goes on vacation, they can have their own delegate, so reports keep getting approved. + +By using this feature, you ensure that all reports get the approvals they need, even when you're not around. + +# How to use Vacation Delegate + +If you're planning to take some time off, you can use the **Vacation Delegate** feature to assign someone to approve expense reports for you. The reports will continue on their usual path as if you had approved them yourself. + +## Set a Vacation Delegate for yourself + +1. Go to the Expensify website (note: you can't do this from the mobile app). +2. Navigate to **Settings > Your Account > Account Details** and scroll down to find **Vacation Delegate**. +3. Enter the email address of the person you're designating as your delegate and click **Set Delegate**. + +Voila! You've set a vacation delegate. Any reports that usually come to you will now go to your delegate instead. When you return, you can easily remove the delegate by clicking a link at the top of the Expensify homepage. + +## Setting a Vacation Delegate as a Domain Admin + +1. Head to **Settings > Domains > [Your Domain Name] > Domain Members > Edit Settings** +2. Enter the delegate's email address and click **Save.** + +Your delegate's actions will be noted in the history and comments of each report they approve, so you can keep track of what happened while you were away. + +# Deep Dive + +## An audit trail of delegate actions + +The system records every action your vacation delegate takes on your behalf in the **Report History and Comments**. So, you can see when they approved an expense report for you. + +# FAQs + +## Why can't my Vacation Delegate reimburse reports that they approve? + +If your **Vacation Delegate** also needs to reimburse reports on your behalf whilst you're away, they'll also need access to the reimbursement account. + +If they do not have access to the reimbursement account used on your workspace, they won’t have the option to reimburse reports, even as your **Vacation Delegate**. + +## What if my Vacation Delegate is also on vacation? + +Don't worry, your delegate can also pick their own **Vacation Delegate**. This way, expense reports continue to get approved even if multiple people are away. + -Kayak.md Lyft.md TrainLine.md TravelPerk.md Trip Actions.md TripCatcher.md Uber.md \ No newline at end of file diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Admins.md b/docs/articles/expensify-classic/policy-and-domain-settings/Admins.md deleted file mode 100644 index cea96cfe2057..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Admins.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Admins -description: Admins ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Admins.md b/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Admins.md deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Admins.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Members.md b/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Members.md deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Domain-Members.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Overview.md b/docs/articles/expensify-classic/policy-and-domain-settings/Overview.md deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Overview.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Reports.md b/docs/articles/expensify-classic/policy-and-domain-settings/Reports.md deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Reports.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Tax.md b/docs/articles/expensify-classic/policy-and-domain-settings/Tax.md deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Tax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/Trips.md b/docs/articles/expensify-classic/policy-and-domain-settings/Trips.md deleted file mode 100644 index 3ee1c8656b4b..000000000000 --- a/docs/articles/expensify-classic/policy-and-domain-settings/Trips.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/reports/Currency.md b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Currency.md new file mode 100644 index 000000000000..e5c9096fa610 --- /dev/null +++ b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Currency.md @@ -0,0 +1,5 @@ +--- +title: Currency +description: Currency +--- +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/reports/Report-Fields-And-Titles.md b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Report-Fields-And-Titles.md new file mode 100644 index 000000000000..47b96f495a1c --- /dev/null +++ b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Report-Fields-And-Titles.md @@ -0,0 +1,5 @@ +--- +title: Report Fields & Titles +description: Report Fields & Titles +--- +## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/reports/Scheduled-Submit.md b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Scheduled-Submit.md new file mode 100644 index 000000000000..c05df92bbbff --- /dev/null +++ b/docs/articles/expensify-classic/policy-and-domain-settings/reports/Scheduled-Submit.md @@ -0,0 +1,38 @@ +--- +title: Scheduled Submit +description: How to use the Scheduled Submit feature +--- +# Overview + +Scheduled Submit reduces the delay between the time an employee creates an expense to when it is submitted to the admin. This gives admins significantly faster visibility into employee spend. Without Scheduled Submit enabled, expenses can be left Unreported giving workspace admins no visibility into employee spend. + +The biggest delay in expense management is the time it takes for an employee to actually submit the expense after it is incurred. Scheduled Submit allows you to automatically collect employee expenses on a schedule of your choosing without delaying the process while you wait for employees to submit them. + +It works like this: Employee expenses are automatically gathered onto a report. If there is not an existing report, a new one will be created. This report is submitted automatically at the cadence you choose (daily, weekly, monthly, twice month, by trip). + +# How to enable Scheduled Submit + +**For workspace admins**: To enable Scheduled Submit on your group workspace, follow **Settings > Workspaces > Group > *[Workspace Name]* > Reports > Scheduled Submit**. From there, toggle Scheduled Submit to Enabled. Then, choose your desired frequency from the dropdown menu. +For individuals or employees: To enable Scheduled Submit on your individual workspace, follow **Settings > Workspaces > Individual > *[Workspace Name]* > Reports > Scheduled Submit**. From there, toggle Scheduled Submit to Enabled. Then, choose your desired frequency from the dropdown menu. + +## Scheduled Submit frequency options + +**Daily**: Each night, expenses without violations will be submitted. Expenses with violations will remain on an open report until the violations are corrected, after which they will be submitted in the evening (PDT). + +**Weekly**: Expenses that are free of violations will be submitted on a weekly basis. However, expenses with violations will be held in a new open report and combined with any new expenses. They will then be submitted at the end of the following weekly cycle, specifically on Sunday evening (PDT). + +**Twice a month**: Expenses that are violation-free will be submitted on both the 15th and the last day of each month, in the evening (PDT). Expenses with violations will not be submitted, but moved on to a new open report so the employee can resolve the violations and then will be submitted at the conclusion of the next cycle. + +**Monthly**: Expenses that are free from violations will be submitted on a monthly basis. Expenses with violations will be held back and moved to a new Open report so the violations can be resolved, and they will be submitted on the evening (PDT) of the specified date. + +**By trip**: Expenses are grouped by trip. This is calculated by grouping all expenses together that occur in a similar time frame. If two full days pass without any new expenses being created, the trip report will be submitted on the evening of the second day. Any expenses generated after this submission will initiate a new trip report. Please note that the "2-day" period refers to a date-based interval, not a 48-hour time frame. + +**Manually**: An open report will be created, and expenses will be added to it automatically. However, it's important to note that the report will not be submitted automatically; manual submission of reports will be required.This is a great option for automatically gathering all an employee’s expenses on a report for the employee’s convenience, but they will still need to review and submit the report. + +# Deep Dive + +## Schedule Submit Override +If Scheduled Submit is disabled at the group workspace level or configured the frequency as "Manually," the individual workspace settings of a user will take precedence and be applied. This means an employee can still set up Scheduled Submit for themselves even if the admin has not enabled it. We highly recommend Scheduled Submit as it helps put your expenses on auto-pilot! + +## Personal Card Transactions +Personal card transactions are handled differently compared to other expenses. If a user has added a card through Settings > Account > Credit Card Import, they need to make sure it is set as non-reimbursable and transactions must be automatically merged with a SmartScanned receipt. If transactions are set to come in as reimbursable or they aren’t merged with a SmartScanned receipt, Scheduled Submit settings will not apply. diff --git a/docs/articles/expensify-classic/policy-and-domain-settings/tax-tracking.md b/docs/articles/expensify-classic/policy-and-domain-settings/tax-tracking.md new file mode 100644 index 000000000000..7b859c5101b1 --- /dev/null +++ b/docs/articles/expensify-classic/policy-and-domain-settings/tax-tracking.md @@ -0,0 +1,19 @@ +--- +title: Tax +description: How to track expense taxes +--- +# Overview +Expensify’s tax tracking feature allows you to: +- Add tax names, rates, and codes whether you’re connected to an accounting system or not. +- Enable/disable taxes you’d like to make available to users. +- Set a default tax for Workspace currency expenses and, optionally, another default tax (including exempt) for foreign currency expenses which - will automatically apply to all new expenses. + +# How to Enable Tax Tracking +Tax tracking can be enabled in the Tax section of the Workspace settings of any Workspace, whether group or individual. +## If Connected to an Accounting Integration +If your group Workspace is connected to Xero, QuickBooks Online, Sage Intacct, or NetSuite, make sure to first enable tax via the connection configuration page (Settings > Policies > Group > [Workspace Name] > Connections > Configure) and then sync the connection. Your tax rates will be imported from the accounting system and indicated by its logo. +## Not Connected to an Accounting Integration +If your Workspace is not connected to an accounting system, go to Settings > Policies > Group > [Workspace Name] > Tax to enable tax. + +# Tracking Tax by Expense Category +To set a different tax rate for a specific expense type in the Workspace currency, go to Settings > Workspaces > Group > [Workspace Name] > Categories page. Click "Edit Rules" next to the desired category and set the "Category default tax". This will be applied to new expenses, overriding the default Workspace currency tax rate. diff --git a/docs/expensify-classic/hubs/policy-and-domain-settings/reports.html b/docs/expensify-classic/hubs/policy-and-domain-settings/reports.html new file mode 100644 index 000000000000..86641ee60b7d --- /dev/null +++ b/docs/expensify-classic/hubs/policy-and-domain-settings/reports.html @@ -0,0 +1,5 @@ +--- +layout: default +--- + +{% include section.html %} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index c7d0f2f4f0f5..dac53193fdc6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -17,7 +17,7 @@ platform :android do desc "Generate a new local APK for e2e testing" lane :build_e2e do ENV["ENVFILE"]="tests/e2e/.env.e2e" - ENV["ENTRY_FILE"]="#{Dir.pwd}/../src/libs/E2E/reactNativeLaunchingTest.js" + ENV["ENTRY_FILE"]="src/libs/E2E/reactNativeLaunchingTest.js" ENV["E2E_TESTING"]="true" gradle( diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f41740a8bcb2..c16c6dce16ca 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.74 + 1.3.75 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.74.2 + 1.3.75.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 95714ea2cc9f..21803f251753 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.74 + 1.3.75 CFBundleSignature ???? CFBundleVersion - 1.3.74.2 + 1.3.75.3 diff --git a/package-lock.json b/package-lock.json index 64ee3cf6308f..8ab673766f6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.74-2", + "version": "1.3.75-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.74-2", + "version": "1.3.75-3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -90,7 +90,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.87", + "react-native-onyx": "1.0.97", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", @@ -41204,9 +41204,9 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.87", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.87.tgz", - "integrity": "sha512-6mIhobSwpClDDGnJm9XEdjnpEdWfFesJ18J8Ifsb4tL6AVi+uxos5bnlZcOoMbtlUk3UozrgSyTjMfFrkD/aZA==", + "version": "1.0.97", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.97.tgz", + "integrity": "sha512-6w4pp9Ktm4lQ6jIS+ZASQ5tYwRU1lt751yxfddvmN646XZefj4iDvC7uQaUnAgg1xL52dEV5RZWaI3sQ3e9AGQ==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -77269,9 +77269,9 @@ } }, "react-native-onyx": { - "version": "1.0.87", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.87.tgz", - "integrity": "sha512-6mIhobSwpClDDGnJm9XEdjnpEdWfFesJ18J8Ifsb4tL6AVi+uxos5bnlZcOoMbtlUk3UozrgSyTjMfFrkD/aZA==", + "version": "1.0.97", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.97.tgz", + "integrity": "sha512-6w4pp9Ktm4lQ6jIS+ZASQ5tYwRU1lt751yxfddvmN646XZefj4iDvC7uQaUnAgg1xL52dEV5RZWaI3sQ3e9AGQ==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index cd93f718679e..b7acd8bd7b55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.74-2", + "version": "1.3.75-3", "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.", @@ -133,7 +133,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.87", + "react-native-onyx": "1.0.97", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", diff --git a/scripts/android-repackage-app-bundle-and-sign.sh b/scripts/android-repackage-app-bundle-and-sign.sh index fe4ee1e4b8fc..1636edc21388 100755 --- a/scripts/android-repackage-app-bundle-and-sign.sh +++ b/scripts/android-repackage-app-bundle-and-sign.sh @@ -1,4 +1,5 @@ #!/bin/bash +source ./scripts/shellUtils.sh ### # Takes an android app that has been built with the debug keystore, @@ -41,7 +42,7 @@ if [ ! -f "$NEW_BUNDLE_FILE" ]; then echo "Bundle file not found: $NEW_BUNDLE_FILE" exit 1 fi -OUTPUT_APK=$(realpath "$OUTPUT_APK") +OUTPUT_APK=$(get_abs_path "$OUTPUT_APK") # check if "apktool" command is available if ! command -v apktool &> /dev/null then diff --git a/scripts/shellUtils.sh b/scripts/shellUtils.sh index 876933af9766..4c9e2febc34d 100644 --- a/scripts/shellUtils.sh +++ b/scripts/shellUtils.sh @@ -41,3 +41,46 @@ function join_by_string { shift printf "%s" "$first" "${@/#/$separator}" } + +# Usage: get_abs_path +# Will make a path absolute, resolving any relative paths +# example: get_abs_path "./foo/bar" +get_abs_path() { + local the_path=$1 + local -a path_elements + IFS='/' read -ra path_elements <<< "$the_path" + + # If the path is already absolute, start with an empty string. + # We'll prepend the / later when reconstructing the path. + if [[ "$the_path" = /* ]]; then + abs_path="" + else + abs_path="$(pwd)" + fi + + # Handle each path element + for element in "${path_elements[@]}"; do + if [ "$element" = "." ] || [ -z "$element" ]; then + continue + elif [ "$element" = ".." ]; then + # Remove the last element from abs_path + abs_path=$(dirname "$abs_path") + else + # Append element to the absolute path + abs_path="${abs_path}/${element}" + fi + done + + # Remove any trailing '/' + while [[ $abs_path == */ ]]; do + abs_path=${abs_path%/} + done + + # Special case for root + [ -z "$abs_path" ] && abs_path="/" + + # Special case to remove any starting '//' when the input path was absolute + abs_path=${abs_path/#\/\//\/} + + echo "$abs_path" +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 284c6115d7b8..1d2e07345c24 100644 --- a/src/App.js +++ b/src/App.js @@ -9,7 +9,7 @@ import {PickerStateProvider} from 'react-native-picker-select'; import CustomStatusBar from './components/CustomStatusBar'; import ErrorBoundary from './components/ErrorBoundary'; import Expensify from './Expensify'; -import {LocaleContextProvider} from './components/withLocalize'; +import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; import HTMLEngineProvider from './components/HTMLEngineProvider'; import PopoverContextProvider from './components/PopoverProvider'; diff --git a/src/CONST.ts b/src/CONST.ts index e487514f150e..0a262d868de9 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -992,6 +992,11 @@ const CONST = { STATEMENT: 'STATEMENT_NAVIGATE', CONCIERGE: 'CONCIERGE_NAVIGATE', }, + MTL_WALLET_PROGRAM_ID: '760', + PROGRAM_ISSUERS: { + EXPENSIFY_PAYMENTS: 'Expensify Payments LLC', + BANCORP_BANK: 'The Bancorp Bank', + }, }, PLAID: { @@ -1232,6 +1237,7 @@ const CONST = { EMOJI_NAME: /:[\w+-]+:/g, EMOJI_SUGGESTIONS: /:[a-zA-Z0-9_+-]{1,40}$/, AFTER_FIRST_LINE_BREAK: /\n.*/g, + LINE_BREAK: /\n/g, CODE_2FA: /^\d{6}$/, ATTACHMENT_ID: /chat-attachments\/(\d+)/, HAS_COLON_ONLY_AT_THE_BEGINNING: /^:[^:]+$/, @@ -1262,6 +1268,8 @@ const CONST = { DATE_TIME_FORMAT: /^\d{2}-\d{2} \d{2}:\d{2} [AP]M$/, ATTACHMENT_ROUTE: /\/r\/(\d*)\/attachment/, ILLEGAL_FILENAME_CHARACTERS: /\/|<|>|\*|"|:|\?|\\|\|/g, + + ENCODE_PERCENT_CHARACTER: /%(25)+/g, }, PRONOUNS: { @@ -1369,6 +1377,7 @@ const CONST = { MERCHANT: 'merchant', CATEGORY: 'category', RECEIPT: 'receipt', + DISTANCE: 'distance', TAG: 'tag', }, FOOTER: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6649a33fe15e..a1afc4fef2c1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1,5 +1,4 @@ import {ValueOf} from 'type-fest'; -import {OnyxUpdate} from 'react-native-onyx'; import DeepValueOf from './types/utils/DeepValueOf'; import * as OnyxTypes from './types/onyx'; import CONST from './CONST'; @@ -30,9 +29,6 @@ const ONYXKEYS = { /** Note: These are Persisted Requests - not all requests in the main queue as the key name might lead one to believe */ PERSISTED_REQUESTS: 'networkRequestQueue', - /** Onyx updates from a response, or success or failure data from a request. */ - QUEUED_ONYX_UPDATES: 'queuedOnyxUpdates', - /** Stores current date */ CURRENT_DATE: 'currentDate', @@ -242,6 +238,7 @@ const ONYXKEYS = { POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', REPORT: 'report_', + REPORT_METADATA: 'reportMetadata_', REPORT_ACTIONS: 'reportActions_', REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_', REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_', @@ -306,7 +303,6 @@ type OnyxValues = { [ONYXKEYS.DEVICE_ID]: string; [ONYXKEYS.IS_SIDEBAR_LOADED]: boolean; [ONYXKEYS.PERSISTED_REQUESTS]: OnyxTypes.Request[]; - [ONYXKEYS.QUEUED_ONYX_UPDATES]: OnyxUpdate[]; [ONYXKEYS.CURRENT_DATE]: string; [ONYXKEYS.CREDENTIALS]: OnyxTypes.Credentials; [ONYXKEYS.IOU]: OnyxTypes.IOU; @@ -380,6 +376,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; + [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportAction; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: string; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 78d5f4d54888..b2dafa643b22 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -5,19 +5,18 @@ import CONST from './CONST'; * This is a file containing constants for all of the routes we want to be able to go to */ -// prettier-ignore export default { HOME: '', /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ CONCIERGE: 'concierge', FLAG_COMMENT: { route: 'flag/:reportID/:reportActionID', - getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}` + getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}`, }, SEARCH: 'search', DETAILS: { route: 'details', - getRoute: (login: string) => `details?login=${encodeURIComponent(login)}` + getRoute: (login: string) => `details?login=${encodeURIComponent(login)}`, }, PROFILE: { route: 'a/:accountID', @@ -31,7 +30,7 @@ export default { VALIDATE_LOGIN: 'v/:accountID/:validateCode', GET_ASSISTANCE: { route: 'get-assistance/:taskID', - getRoute: (taskID: string) => `get-assistance/${taskID}` + getRoute: (taskID: string) => `get-assistance/${taskID}`, }, UNLINK_LOGIN: 'u/:accountID/:validateCode', APPLE_SIGN_IN: 'sign-in-with-apple', @@ -85,6 +84,16 @@ export default { SETTINGS_PERSONAL_DETAILS_LEGAL_NAME: 'settings/profile/personal-details/legal-name', SETTINGS_PERSONAL_DETAILS_DATE_OF_BIRTH: 'settings/profile/personal-details/date-of-birth', SETTINGS_PERSONAL_DETAILS_ADDRESS: 'settings/profile/personal-details/address', + SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY: { + route: 'settings/profile/personal-details/address/country', + getRoute: (country: string, backTo?: string) => { + let route = `settings/profile/personal-details/address/country?country=${country}`; + if (backTo) { + route += `&backTo=${encodeURIComponent(backTo)}`; + } + return route; + }, + }, SETTINGS_CONTACT_METHODS: 'settings/profile/contact-methods', SETTINGS_CONTACT_METHOD_DETAILS: { route: 'settings/profile/contact-methods/:contactMethod/details', @@ -102,11 +111,11 @@ export default { REPORT: 'r', REPORT_WITH_ID: { route: 'r/:reportID?/:reportActionID?', - getRoute: (reportID: string) => `r/${reportID}` + getRoute: (reportID: string) => `r/${reportID}`, }, EDIT_REQUEST: { route: 'r/:threadReportID/edit/:field', - getRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}` + getRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}`, }, EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', @@ -114,89 +123,89 @@ export default { }, REPORT_WITH_ID_DETAILS_SHARE_CODE: { route: 'r/:reportID/details/shareCode', - getRoute: (reportID: string) => `r/${reportID}/details/shareCode` + getRoute: (reportID: string) => `r/${reportID}/details/shareCode`, }, REPORT_ATTACHMENTS: { route: 'r/:reportID/attachment', - getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}` + getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}`, }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', - getRoute: (reportID: string) => `r/${reportID}/participants` + getRoute: (reportID: string) => `r/${reportID}/participants`, }, REPORT_WITH_ID_DETAILS: { route: 'r/:reportID/details', - getRoute: (reportID: string) => `r/${reportID}/details` + getRoute: (reportID: string) => `r/${reportID}/details`, }, REPORT_SETTINGS: { route: 'r/:reportID/settings', - getRoute: (reportID: string) => `r/${reportID}/settings` + getRoute: (reportID: string) => `r/${reportID}/settings`, }, REPORT_SETTINGS_ROOM_NAME: { route: 'r/:reportID/settings/room-name', - getRoute: (reportID: string) => `r/${reportID}/settings/room-name` + getRoute: (reportID: string) => `r/${reportID}/settings/room-name`, }, REPORT_SETTINGS_NOTIFICATION_PREFERENCES: { route: 'r/:reportID/settings/notification-preferences', - getRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences` + getRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences`, }, REPORT_SETTINGS_WRITE_CAPABILITY: { route: 'r/:reportID/settings/who-can-post', - getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post` + getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post`, }, REPORT_WELCOME_MESSAGE: { route: 'r/:reportID/welcomeMessage', - getRoute: (reportID: string) => `r/${reportID}/welcomeMessage` + getRoute: (reportID: string) => `r/${reportID}/welcomeMessage`, }, SPLIT_BILL_DETAILS: { route: 'r/:reportID/split/:reportActionID', - getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}` + getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}`, }, TASK_TITLE: { route: 'r/:reportID/title', - getRoute: (reportID: string) => `r/${reportID}/title` + getRoute: (reportID: string) => `r/${reportID}/title`, }, TASK_DESCRIPTION: { route: 'r/:reportID/description', - getRoute: (reportID: string) => `r/${reportID}/description` + getRoute: (reportID: string) => `r/${reportID}/description`, }, TASK_ASSIGNEE: { route: 'r/:reportID/assignee', - getRoute: (reportID: string) => `r/${reportID}/assignee` + getRoute: (reportID: string) => `r/${reportID}/assignee`, }, PRIVATE_NOTES_VIEW: { route: 'r/:reportID/notes/:accountID', - getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}` + getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}`, }, PRIVATE_NOTES_LIST: { route: 'r/:reportID/notes', - getRoute: (reportID: string) => `r/${reportID}/notes` + getRoute: (reportID: string) => `r/${reportID}/notes`, }, PRIVATE_NOTES_EDIT: { route: 'r/:reportID/notes/:accountID/edit', - getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit` + getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit`, }, // To see the available iouType, please refer to CONST.IOU.MONEY_REQUEST_TYPE MONEY_REQUEST: { route: ':iouType/new/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}`, }, MONEY_REQUEST_AMOUNT: { route: ':iouType/new/amount/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/amount/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/amount/${reportID}`, }, MONEY_REQUEST_PARTICIPANTS: { route: ':iouType/new/participants/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}`, }, MONEY_REQUEST_CONFIRMATION: { route: ':iouType/new/confirmation/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}`, }, MONEY_REQUEST_DATE: { route: ':iouType/new/date/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}`, }, MONEY_REQUEST_CURRENCY: { route: ':iouType/new/currency/:reportID?', @@ -204,35 +213,39 @@ export default { }, MONEY_REQUEST_DESCRIPTION: { route: ':iouType/new/description/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/description/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/description/${reportID}`, }, MONEY_REQUEST_CATEGORY: { route: ':iouType/new/category/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}`, }, MONEY_REQUEST_TAG: { route: ':iouType/new/tag/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}`, }, MONEY_REQUEST_MERCHANT: { route: ':iouType/new/merchant/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}`, }, MONEY_REQUEST_WAYPOINT: { route: ':iouType/new/waypoint/:waypointIndex', - getRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}` + getRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}`, }, MONEY_REQUEST_RECEIPT: { route: ':iouType/new/receipt/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}`, }, - MONEY_REQUEST_ADDRESS: { + MONEY_REQUEST_DISTANCE: { route: ':iouType/new/address/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}`, + }, + MONEY_REQUEST_EDIT_WAYPOINT: { + route: 'r/:threadReportID/edit/distance/:transactionID/waypoint/:waypointIndex', + getRoute: (threadReportID: number, transactionID: string, waypointIndex: number) => `r/${threadReportID}/edit/distance/${transactionID}/waypoint/${waypointIndex}`, }, MONEY_REQUEST_DISTANCE_TAB: { route: ':iouType/new/:reportID?/distance', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance` + getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance`, }, MONEY_REQUEST_MANUAL_TAB: ':iouType/new/:reportID?/manual', MONEY_REQUEST_SCAN_TAB: ':iouType/new/:reportID?/scan', @@ -259,47 +272,47 @@ export default { WORKSPACE_NEW_ROOM: 'workspace/new-room', WORKSPACE_INITIAL: { route: 'workspace/:policyID', - getRoute: (policyID: string) => `workspace/${policyID}` + getRoute: (policyID: string) => `workspace/${policyID}`, }, WORKSPACE_INVITE: { route: 'workspace/:policyID/invite', - getRoute: (policyID: string) => `workspace/${policyID}/invite` + getRoute: (policyID: string) => `workspace/${policyID}/invite`, }, WORKSPACE_INVITE_MESSAGE: { route: 'workspace/:policyID/invite-message', - getRoute: (policyID: string) => `workspace/${policyID}/invite-message` + getRoute: (policyID: string) => `workspace/${policyID}/invite-message`, }, WORKSPACE_SETTINGS: { route: 'workspace/:policyID/settings', - getRoute: (policyID: string) => `workspace/${policyID}/settings` + getRoute: (policyID: string) => `workspace/${policyID}/settings`, }, WORKSPACE_CARD: { route: 'workspace/:policyID/card', - getRoute: (policyID: string) => `workspace/${policyID}/card` + getRoute: (policyID: string) => `workspace/${policyID}/card`, }, WORKSPACE_REIMBURSE: { route: 'workspace/:policyID/reimburse', - getRoute: (policyID: string) => `workspace/${policyID}/reimburse` + getRoute: (policyID: string) => `workspace/${policyID}/reimburse`, }, WORKSPACE_RATE_AND_UNIT: { route: 'workspace/:policyID/rateandunit', - getRoute: (policyID: string) => `workspace/${policyID}/rateandunit` + getRoute: (policyID: string) => `workspace/${policyID}/rateandunit`, }, WORKSPACE_BILLS: { route: 'workspace/:policyID/bills', - getRoute: (policyID: string) => `workspace/${policyID}/bills` + getRoute: (policyID: string) => `workspace/${policyID}/bills`, }, WORKSPACE_INVOICES: { route: 'workspace/:policyID/invoices', - getRoute: (policyID: string) => `workspace/${policyID}/invoices` + getRoute: (policyID: string) => `workspace/${policyID}/invoices`, }, WORKSPACE_TRAVEL: { route: 'workspace/:policyID/travel', - getRoute: (policyID: string) => `workspace/${policyID}/travel` + getRoute: (policyID: string) => `workspace/${policyID}/travel`, }, WORKSPACE_MEMBERS: { route: 'workspace/:policyID/members', - getRoute: (policyID: string) => `workspace/${policyID}/members` + getRoute: (policyID: string) => `workspace/${policyID}/members`, }, // These are some on-off routes that will be removed once they're no longer needed (see GH issues for details) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 44075a4ec1eb..e7b18bbd8d69 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -358,7 +358,7 @@ function Composer({ const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); setTextInputWidth(computedStyle.width); - const computedNumberOfLines = ComposerUtils.getNumberOfLines(maxLines, lineHeight, paddingTopAndBottom, textInput.current.scrollHeight); + const computedNumberOfLines = ComposerUtils.getNumberOfLines(lineHeight, paddingTopAndBottom, textInput.current.scrollHeight, maxLines); const generalNumberOfLines = computedNumberOfLines === 0 ? numberOfLinesProp : computedNumberOfLines; onNumberOfLinesChange(generalNumberOfLines); diff --git a/src/components/CountryPicker/CountrySelectorModal.js b/src/components/CountryPicker/CountrySelectorModal.js deleted file mode 100644 index 6c6cd19af0c7..000000000000 --- a/src/components/CountryPicker/CountrySelectorModal.js +++ /dev/null @@ -1,105 +0,0 @@ -import _ from 'underscore'; -import React, {useMemo, useEffect} from 'react'; -import PropTypes from 'prop-types'; -import CONST from '../../CONST'; -import useLocalize from '../../hooks/useLocalize'; -import HeaderWithBackButton from '../HeaderWithBackButton'; -import SelectionList from '../SelectionList'; -import Modal from '../Modal'; -import ScreenWrapper from '../ScreenWrapper'; -import styles from '../../styles/styles'; -import searchCountryOptions from '../../libs/searchCountryOptions'; -import StringUtils from '../../libs/StringUtils'; - -const propTypes = { - /** Whether the modal is visible */ - isVisible: PropTypes.bool.isRequired, - - /** Country value selected */ - currentCountry: PropTypes.string, - - /** Function to call when the user selects a Country */ - onCountrySelected: PropTypes.func, - - /** Function to call when the user closes the Country modal */ - onClose: PropTypes.func, - - /** The search value from the selection list */ - searchValue: PropTypes.string.isRequired, - - /** Function to call when the user types in the search input */ - setSearchValue: PropTypes.func.isRequired, -}; - -const defaultProps = { - currentCountry: '', - onClose: () => {}, - onCountrySelected: () => {}, -}; - -function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySelected, setSearchValue, searchValue}) { - const {translate} = useLocalize(); - - useEffect(() => { - if (isVisible) { - return; - } - setSearchValue(''); - }, [isVisible, setSearchValue]); - - const countries = useMemo( - () => - _.map(_.keys(CONST.ALL_COUNTRIES), (countryISO) => { - const countryName = translate(`allCountries.${countryISO}`); - return { - value: countryISO, - keyForList: countryISO, - text: countryName, - isSelected: currentCountry === countryISO, - searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`), - }; - }), - [translate, currentCountry], - ); - - const searchResults = searchCountryOptions(searchValue, countries); - const headerMessage = searchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : ''; - - return ( - - - - - - - ); -} - -CountrySelectorModal.propTypes = propTypes; -CountrySelectorModal.defaultProps = defaultProps; -CountrySelectorModal.displayName = 'CountrySelectorModal'; - -export default CountrySelectorModal; diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js deleted file mode 100644 index 8f5c89b1bce8..000000000000 --- a/src/components/CountryPicker/index.js +++ /dev/null @@ -1,90 +0,0 @@ -import React, {useState} from 'react'; -import {View} from 'react-native'; -import PropTypes from 'prop-types'; -import styles from '../../styles/styles'; -import MenuItemWithTopDescription from '../MenuItemWithTopDescription'; -import useLocalize from '../../hooks/useLocalize'; -import CountrySelectorModal from './CountrySelectorModal'; -import FormHelpMessage from '../FormHelpMessage'; -import refPropTypes from '../refPropTypes'; - -const propTypes = { - /** Form Error description */ - errorText: PropTypes.string, - - /** Country to display */ - value: PropTypes.string, - - /** Callback to call when the input changes */ - onInputChange: PropTypes.func, - - /** A ref to forward to MenuItemWithTopDescription */ - forwardedRef: refPropTypes, -}; - -const defaultProps = { - value: undefined, - forwardedRef: undefined, - errorText: '', - onInputChange: () => {}, -}; - -function CountryPicker({value, errorText, onInputChange, forwardedRef}) { - const {translate} = useLocalize(); - const [isPickerVisible, setIsPickerVisible] = useState(false); - const [searchValue, setSearchValue] = useState(''); - - const showPickerModal = () => { - setIsPickerVisible(true); - }; - - const hidePickerModal = () => { - setIsPickerVisible(false); - }; - - const updateCountryInput = (country) => { - if (country.value !== value) { - onInputChange(country.value); - } - hidePickerModal(); - }; - - const title = value ? translate(`allCountries.${value}`) : ''; - const descStyle = title.length === 0 ? styles.textNormal : null; - - return ( - - - - - - - - ); -} - -CountryPicker.propTypes = propTypes; -CountryPicker.defaultProps = defaultProps; -CountryPicker.displayName = 'CountryPicker'; - -export default React.forwardRef((props, ref) => ( - -)); diff --git a/src/components/CountrySelector.js b/src/components/CountrySelector.js new file mode 100644 index 000000000000..2788f3cea8e3 --- /dev/null +++ b/src/components/CountrySelector.js @@ -0,0 +1,77 @@ +import React, {useEffect} from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import styles from '../styles/styles'; +import Navigation from '../libs/Navigation/Navigation'; +import ROUTES from '../ROUTES'; +import useLocalize from '../hooks/useLocalize'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import FormHelpMessage from './FormHelpMessage'; + +const propTypes = { + /** Form error text. e.g when no country is selected */ + errorText: PropTypes.string, + + /** Callback called when the country changes. */ + onInputChange: PropTypes.func.isRequired, + + /** Current selected country */ + value: PropTypes.string, + + /** inputID used by the Form component */ + // eslint-disable-next-line react/no-unused-prop-types + inputID: PropTypes.string.isRequired, + + /** React ref being forwarded to the MenuItemWithTopDescription */ + forwardedRef: PropTypes.func, +}; + +const defaultProps = { + errorText: '', + value: undefined, + forwardedRef: () => {}, +}; + +function CountrySelector({errorText, value: countryCode, onInputChange, forwardedRef}) { + const {translate} = useLocalize(); + + const title = countryCode ? translate(`allCountries.${countryCode}`) : ''; + const countryTitleDescStyle = title.length === 0 ? styles.textNormal : null; + + useEffect(() => { + // This will cause the form to revalidate and remove any error related to country name + onInputChange(countryCode); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [countryCode]); + + return ( + + { + const activeRoute = Navigation.getActiveRoute().replace(/\?.*/, ''); + Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY.getRoute(countryCode, activeRoute)); + }} + /> + + + + + ); +} + +CountrySelector.propTypes = propTypes; +CountrySelector.defaultProps = defaultProps; +CountrySelector.displayName = 'CountrySelector'; + +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/components/DistanceRequest.js b/src/components/DistanceRequest.js index 5e9b73f2eb3a..aa7878337f21 100644 --- a/src/components/DistanceRequest.js +++ b/src/components/DistanceRequest.js @@ -5,39 +5,31 @@ import lodashGet from 'lodash/get'; import lodashIsNil from 'lodash/isNil'; import PropTypes from 'prop-types'; import _ from 'underscore'; - import CONST from '../CONST'; import ROUTES from '../ROUTES'; import ONYXKEYS from '../ONYXKEYS'; - import styles from '../styles/styles'; import variables from '../styles/variables'; -import theme from '../styles/themes/default'; - -import transactionPropTypes from './transactionPropTypes'; - +import LinearGradient from './LinearGradient'; +import * as MapboxToken from '../libs/actions/MapboxToken'; import useNetwork from '../hooks/useNetwork'; -import usePrevious from '../hooks/usePrevious'; import useLocalize from '../hooks/useLocalize'; - -import * as ErrorUtils from '../libs/ErrorUtils'; import Navigation from '../libs/Navigation/Navigation'; -import * as MapboxToken from '../libs/actions/MapboxToken'; +import reportPropTypes from '../pages/reportPropTypes'; +import DotIndicatorMessage from './DotIndicatorMessage'; +import * as ErrorUtils from '../libs/ErrorUtils'; +import usePrevious from '../hooks/usePrevious'; +import theme from '../styles/themes/default'; import * as Transaction from '../libs/actions/Transaction'; import * as TransactionUtils from '../libs/TransactionUtils'; import * as IOUUtils from '../libs/IOUUtils'; - import Button from './Button'; import DistanceMapView from './DistanceMapView'; -import LinearGradient from './LinearGradient'; import * as Expensicons from './Icon/Expensicons'; import PendingMapView from './MapView/PendingMapView'; -import DotIndicatorMessage from './DotIndicatorMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; -import {iouPropTypes} from '../pages/iou/propTypes'; -import reportPropTypes from '../pages/reportPropTypes'; -import * as IOU from '../libs/actions/IOU'; import * as StyleUtils from '../styles/StyleUtils'; +import transactionPropTypes from './transactionPropTypes'; import ScreenWrapper from './ScreenWrapper'; import FullPageNotFoundView from './BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from './HeaderWithBackButton'; @@ -46,18 +38,12 @@ const MAX_WAYPOINTS = 25; const MAX_WAYPOINTS_TO_DISPLAY = 4; const propTypes = { - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** Type of money request (i.e. IOU) */ - iouType: PropTypes.oneOf(_.values(CONST.IOU.MONEY_REQUEST_TYPE)), + /** The transactionID of this request */ + transactionID: PropTypes.string, /** The report to which the distance request is associated */ report: reportPropTypes, - /** The optimistic transaction for this request */ - transaction: transactionPropTypes, - /** Data about Mapbox token for calling Mapbox API */ mapboxAccessToken: PropTypes.shape({ /** Temporary token for Mapbox API */ @@ -67,6 +53,15 @@ const propTypes = { expiration: PropTypes.string, }), + /** Are we editing an existing distance request, or creating a new one? */ + isEditingRequest: PropTypes.bool, + + /** Called on submit of this page */ + onSubmit: PropTypes.func.isRequired, + + /* Onyx Props */ + transaction: transactionPropTypes, + /** React Navigation route */ route: PropTypes.shape({ /** Params from the route */ @@ -81,16 +76,16 @@ const propTypes = { }; const defaultProps = { - iou: {}, - iouType: '', + transactionID: '', report: {}, - transaction: {}, + isEditingRequest: false, mapboxAccessToken: { token: '', }, + transaction: {}, }; -function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, route}) { +function DistanceRequest({transactionID, report, transaction, mapboxAccessToken, route, isEditingRequest, onSubmit}) { const [shouldShowGradient, setShouldShowGradient] = useState(false); const [scrollContainerHeight, setScrollContainerHeight] = useState(0); const [scrollContentHeight, setScrollContentHeight] = useState(0); @@ -99,6 +94,7 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, const isEditing = lodashGet(route, 'path', '').includes('address'); const reportID = lodashGet(report, 'reportID', ''); + const iouType = lodashGet(route, 'params.iouType', ''); const waypoints = useMemo(() => lodashGet(transaction, 'comment.waypoints', {}), [transaction]); const previousWaypoints = usePrevious(waypoints); const numberOfWaypoints = _.size(waypoints); @@ -107,6 +103,7 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, const lastWaypointIndex = numberOfWaypoints - 1; const isLoadingRoute = lodashGet(transaction, 'comment.isLoading', false); + const isLoading = lodashGet(transaction, 'isLoading', false); const hasRouteError = !!lodashGet(transaction, 'errorFields.route'); const hasRoute = TransactionUtils.hasRoute(transaction); const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints); @@ -159,12 +156,12 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, }, []); useEffect(() => { - if (!iou.transactionID || !_.isEmpty(waypoints)) { + if (!transactionID || !_.isEmpty(waypoints)) { return; } // Create the initial start and stop waypoints - Transaction.createInitialWaypoints(iou.transactionID); - }, [iou.transactionID, waypoints]); + Transaction.createInitialWaypoints(transactionID); + }, [transactionID, waypoints]); const updateGradientVisibility = (event = {}) => { // If a waypoint extends past the bottom of the visible area show the gradient, else hide it. @@ -176,8 +173,8 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, return; } - Transaction.getRoute(iou.transactionID, validatedWaypoints); - }, [shouldFetchRoute, iou.transactionID, validatedWaypoints, isOffline]); + Transaction.getRoute(transactionID, validatedWaypoints); + }, [shouldFetchRoute, transactionID, validatedWaypoints, isOffline]); useEffect(() => { if (numberOfWaypoints <= numberOfPreviousWaypoints) { @@ -192,13 +189,12 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, Navigation.goBack(isEditing ? ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID) : ROUTES.HOME); }; - const navigateToNextPage = () => { - if (isEditing) { - Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); - return; - } - - IOU.navigateToNextPage(iou, iouType, reportID, report); + /** + * Takes the user to the page for editing a specific waypoint + * @param {Number} index of the waypoint to edit + */ + const navigateToWaypointEditPage = (index) => { + Navigation.navigate(isEditingRequest ? ROUTES.MONEY_REQUEST_EDIT_WAYPOINT.getRoute(report.reportID, transactionID, index) : ROUTES.MONEY_REQUEST_WAYPOINT.getRoute('request', index)); }; const content = ( @@ -237,7 +233,7 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken, secondaryIcon={waypointIcon} secondaryIconFill={theme.icon} shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_WAYPOINT.getRoute('request', index))} + onPress={() => navigateToWaypointEditPage(index)} key={key} /> ); @@ -261,10 +257,7 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken,