diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association index c871764117ed..a9e2b0383691 100644 --- a/.well-known/apple-app-site-association +++ b/.well-known/apple-app-site-association @@ -68,6 +68,10 @@ "/": "/workspace/*", "comment": "Workspace Details" }, + { + "/": "/get-assistance/*", + "comment": "Get Assistance Pages" + }, { "/": "/teachersunite/*", "comment": "Teachers Unite!" diff --git a/android/app/build.gradle b/android/app/build.gradle index bcac489f6828..afe24fc37700 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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 1001037403 + versionName "1.3.74-3" } flavorDimensions "default" diff --git a/docs/404.html b/docs/404.html index 1773388c6923..4338293218cc 100644 --- a/docs/404.html +++ b/docs/404.html @@ -1,8 +1,8 @@ --- permalink: /404.html --- -
- +
+ Hmm it's not here...
That page is nowhere to be found.
diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index 6e4095569a6d..3ad2276713da 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -641,30 +641,13 @@ button { } .centered-content { - height: 240px; + width: 100%; + height: calc(100vh - 56px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; text-align: center; - font-size: larger; - position: absolute; - top: calc((100vh - 240px) / 2); - - width: 380px; - right: calc((100vw - 380px) / 2); - @include breakpoint($breakpoint-tablet) { - width: 500px; - right: calc((100vw - 500px) / 2); - } - - &.with-lhn { - right: calc((100vw - 380px) / 2); - - @include breakpoint($breakpoint-tablet) { - right: calc((100vw - 320px - 500px ) / 2); - } - - @include breakpoint($breakpoint-desktop) { - right: calc((100vw - 420px - 500px) / 2); - } - } div { margin-top: 8px; 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 8323be7b8e3f..e565e59dc754 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 @@ -1,5 +1,124 @@ --- -title: Create Expenses -description: Create Expenses +title: Create-Expenses.md +description: This is an article that shows you all the ways that you can create Expenses in Expensify! --- -## Resource Coming Soon! + + +# About +Whether you're using SmartScan for automatic expense creation, or manually creating, splitting, or duplicating expenses, you can rest assured your expenses will be correctly tracked in Expensify. + +# How-to Create Expenses +## Using SmartScan +Use the big green camera button within the Expensify mobile app to snap a photo of your physical receipt to have it SmartScanned. +For digital or emailed receipts, simply forward them to receipts@expensify.com and it will be SmartScanned and added to your Expensify account. + +There’s no need to keep the app open and most SmartScans are finished within the hour. If more details are needed, Concierge will reach out to you with a friendly message. +## Using the Mobile App +Simply tap the **+** icon in the top-right corner +Choose **Expense** and then select **Manually Create**. +If you don't have a receipt handy or want to add it later, fill in your expense details and click the **Save** button. +## Using the Expensify Website +Log into the Expensify website +Click on the **Expenses** page and find the **New Expense** dropdown. +Select your expense type, hit the **Save** button and you're all set. +You can then add details like the Merchant and Category, attach a receipt image, and even add a description. +# How to Split an Expense +Splitting an expense in Expensify allows you to break down a single expense into multiple expenses. Each split expense is treated as an individual expense which can be categorized and tagged separately. The same receipt image will be attached to all of the split expenses, allowing you to divide a single expense into smaller, more manageable expenses. +To split an expense on the mobile app: + +1. Open an expense. +2. At the bottom of the screen, tap **More Options**. +3. Then, use the **Split** button to divide the expense. + +To split an expense on the Expensify website: + +1. Click on the expense you want to split. +2. Click on the **Split** button. + - On the Expenses page, this button is at the top. + - Within an individual expense, you'll find it at the bottom. +3. This will automatically be split in two, but you can decide how many expenses you want to split it into by clicking on the **Add Split** button. + - Remember, the total of all pieces must add up to the original expense amount, and no piece can have a $0.00 amount (or you won't be able to save the changes). + +# How to Create Bulk Expenses + +If you have multiple saved receipt images or PDFs to upload, you can drag and drop them onto your Expenses page in batches of ten - this will start the SmartScan process for all of them. + +You can also create a number of future 'placeholder' expenses for your recurring expenses (such as recurring bills or subscriptions) which you don't have receipts for by clicking *New Expense > Create Multiple* to quickly add multiple expenses in batches of up to ten. + +# 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. +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: + +1. Click on the Report containing all the expenses. +2. Click on **Details**. +3. Click on the Pencil icon. +3. Select the **Edit Multiple** button. + +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. + +If two expenses are SmartScanned on the same day for the same amount, they will be flagged as duplicates unless: +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). + +## How do I recover a duplicate or undelete an expense? + +To recover a duplicate or undelete an expense: +Log into your Expensify account on the website and navigate to the Expenses page +Use the filters to search for deleted expenses by selecting the "Deleted" filter +Select the checkbox next to the expenses you want to restore +Click the **Undelete** button and you're all set. You’ll find the expense on your Expenses page again. + +# Deep Dive + +## 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. +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. + +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. +## How to Track Attendees + +Attendee tracking makes it easy to track shared expenses and maintain transparency in your group spending. + +Internal attendees are considered users within your policies or domain. To add internal attendees on mobile or web: +1. Click or tap the **Attendee** field within your expense. +2. Select the internal attendees you'd like to add from the list of searchable users. +3. You can continue adding more attendees or save the Expense. + +External attendees are considered users outside your group policy or domain. To add external attendees: +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. +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. + diff --git a/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md b/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md index b71fd1a3c8bf..29380dab5a5b 100644 --- a/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md +++ b/docs/articles/expensify-classic/get-paid-back/expenses/Upload-Receipts.md @@ -1,5 +1,36 @@ --- -title: Upload Receipts -description: Upload Receipts +title: Upload-Receipts.md +description: This article shows you all the ways that you can upload your receipts to Expensify! --- -## Resource Coming Soon! + + +# About +Need to get paid? Check out this guide to see all the ways that you can upload your receipts to Expensify - whether it’s by SmartScanning them by forwarding via email or manually by taking a picture of a receipt, we’ll cover it here! + +# How-to Upload Receipts +## SmartScan +The easiest way to upload your receipts to Expensify is to SmartScan them with Expensify’s mobile app or forward a receipt from your email inbox! + +When you SmartScan a receipt, we’ll read the Merchant, Date and Amount of the transaction, create an expense, and add it to your Expensify account automatically. The best practice is to take a picture of the receipt at the time of purchase or forward it to your Expensify account from the point of sale system. If you have a credit card connected and you upload a receipt that matches a card expense, the SmartScanned receipt will automatically merge with the imported card expense instead. + +## Email Receipts +To SmartScan a receipt on your mobile app, tap the green camera button, point and shoot! You can also forward your digital receipts (or photos of receipts) to receipts@expensify.com from the email address associated with your Expensify account, and they’ll be SmartScanned. This may take a few minutes because Expensify aims to have the most accurate OCR. + +## Manually Upload +To upload receipts on the web, simply navigate to the Expenses page and click on **New Expense**. Select **Scan Receipt** and choose the file you would like to upload, or drag-and-drop your image directly into the Expenses page, and that will start the SmartScanning process! + +# FAQ +## How do you SmartScan multiple receipts? +You can utilize the Rapid Fire Mode to quickly SmartScan multiple receipts at once! + +To activate it, tap on the green camera button in the mobile app and then tap on the camera icon on the bottom right. When you see the little fire icon on the camera, Rapid Fire Mode has been activated - tap the camera icon again to disable Rapid Fire Mode. + +## How do you create an expense from an email address that is different from your Expensify login? +You can email a receipt from a different email address by adding it as a Secondary Login to your Expensify account - this ensures that any receipts sent from this email to receipts@expensify.com will be associated with your current Expensify account. + +Once that email address has been added as a Secondary Login, simply forward your receipt image or emails to receipts@expensify.com. + +## How do you crop or rotate a receipt image? +You can crop and rotate a receipt image on the web app, and you can only edit one expense at a time. + +Navigate to your Expenses page and locate the expense whose receipt image you'd like to edit, then click the expense to open the Edit screen. If there is an image file associated with the receipt, you will see the Rotate and Crop buttons. Alternatively, you can also navigate to your Reports page, click on a report, and locate the individual expense. 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/manage-employees-and-report-approvals/User-Roles.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/User-Roles.md index 3ee1c8656b4b..a65dc378a793 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 @@ -1,5 +1,63 @@ --- -title: Coming Soon -description: Coming Soon +title: User Roles +description: Each member has a role that defines what they can see and do in the workspace. --- -## Resource Coming Soon! + +# Overview + +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 + +To find and edit the roles of group workspace members, go to **Settings > Workspaces > Group > [Your Specific 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 + +- **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 + +- **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 + +- **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 + +- **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**. +- **Billable:** The technical contact doesn’t need to be a group workspace member and so is not counted towards your billable activity. + +Note: running expense analytics from **Insights** follows the same rules. All the reports and data graphs you generate will be created based on the expense data you have access to. + +# Deep Dive + +## Expense Data Visibility + +The amount of expense data you can see depends on your role within any group workspaces you're part of: + +- **Employees:** Whether you're on a free or paid plan, if you're not approving expenses, you'll only see your own expenses. +- **Approvers:** If you approve expenses for your team and also submit your own, you can view both individual and team-wide expenses and analytics. +- **Admins:** Users with an admin role can see analytics and data for every expense report made by anyone on the workspace. + +If you need to see more data, here are some options: + +- **Become an Admin:** Check within your organization if you can be upgraded to an admin role in your group workspaces. +- **Become a Copilot:** Ask to be added as a **Copilot** to an existing admin account, which will allow you some additional viewing privileges. +- **Become an Approver:** You could also be added as an **Approver** in an existing workflow to view more data. + + diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f41740a8bcb2..73e22053eda1 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.74.2 + 1.3.74.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 95714ea2cc9f..5e7f02699579 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.3.74.2 + 1.3.74.3 diff --git a/package-lock.json b/package-lock.json index 64ee3cf6308f..d8cba15c32af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.74-2", + "version": "1.3.74-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.74-2", + "version": "1.3.74-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index cd93f718679e..24fdeaaed66d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.74-2", + "version": "1.3.74-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.", diff --git a/src/CONST.ts b/src/CONST.ts index 4d216285bc50..4f34e7cb2136 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -440,6 +440,12 @@ const CONST = { INTERNAL_DEV_EXPENSIFY_URL: 'https://www.expensify.com.dev', STAGING_EXPENSIFY_URL: 'https://staging.expensify.com', EXPENSIFY_URL: 'https://www.expensify.com', + BANK_ACCOUNT_PERSONAL_DOCUMENTATION_INFO_URL: + 'https://community.expensify.com/discussion/6983/faq-why-do-i-need-to-provide-personal-documentation-when-setting-up-updating-my-bank-account', + PERSONAL_DATA_PROTECTION_INFO_URL: 'https://community.expensify.com/discussion/5677/deep-dive-security-how-expensify-protects-your-information', + ONFIDO_FACIAL_SCAN_POLICY_URL: 'https://onfido.com/facial-scan-policy-and-release/', + ONFIDO_PRIVACY_POLICY_URL: 'https://onfido.com/privacy/', + ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/', // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'http://localhost:', @@ -982,6 +988,10 @@ const CONST = { GOLD: 'GOLD', SILVER: 'SILVER', }, + WEB_MESSAGE_TYPE: { + STATEMENT: 'STATEMENT_NAVIGATE', + CONCIERGE: 'CONCIERGE_NAVIGATE', + }, }, PLAID: { @@ -1359,6 +1369,7 @@ const CONST = { MERCHANT: 'merchant', CATEGORY: 'category', RECEIPT: 'receipt', + DISTANCE: 'distance', TAG: 'tag', }, FOOTER: { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 78d5f4d54888..00f3a4012664 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', @@ -102,11 +101,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 +113,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 +203,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 +262,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/AttachmentModal.js b/src/components/AttachmentModal.js index c0fe0e2d26f8..23545de26cfd 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -30,6 +30,7 @@ import useWindowDimensions from '../hooks/useWindowDimensions'; import Navigation from '../libs/Navigation/Navigation'; import ROUTES from '../ROUTES'; import useNativeDriver from '../libs/useNativeDriver'; +import useNetwork from '../hooks/useNetwork'; /** * Modal render prop component that exposes modal launching triggers that can be used @@ -121,6 +122,7 @@ function AttachmentModal(props) { : undefined, ); const {translate} = useLocalize(); + const {isOffline} = useNetwork(); const onCarouselAttachmentChange = props.onCarouselAttachmentChange; @@ -350,7 +352,7 @@ function AttachmentModal(props) { downloadAttachment(source)} shouldShowCloseButton={!props.isSmallScreenWidth} shouldShowBackButton={props.isSmallScreenWidth} diff --git a/src/components/DistanceRequest.js b/src/components/DistanceRequest.js index 5e9b73f2eb3a..30e5adfc62b0 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,