diff --git a/.eslintrc.js b/.eslintrc.js index 4df9493b2e8c..c1b6a9676052 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -122,6 +122,7 @@ module.exports = { }, }, ], + 'rulesdir/avoid-anonymous-functions': 'off', }, }, // This helps disable the `prefer-alias` rule to be enabled for specific directories @@ -276,5 +277,11 @@ module.exports = { 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], }, }, + { + files: ['en.ts', 'es.ts'], + rules: { + 'rulesdir/use-periods-for-error-messages': 'error', + }, + }, ], }; diff --git a/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts b/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts index 7de78e257dc4..26947193cd80 100644 --- a/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts +++ b/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts @@ -8,6 +8,7 @@ import {promiseDoWhile} from '@github/libs/promiseWhile'; type CurrentStagingDeploys = Awaited>['data']['workflow_runs']; function run() { + console.info('[awaitStagingDeploys] POLL RATE', CONST.POLL_RATE); console.info('[awaitStagingDeploys] run()'); console.info('[awaitStagingDeploys] getStringInput', getStringInput); console.info('[awaitStagingDeploys] GitHubUtils', GitHubUtils); diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index d84c6df1a0d3..c91313520673 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12131,6 +12131,7 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); const GithubUtils_1 = __importDefault(__nccwpck_require__(9296)); const promiseWhile_1 = __nccwpck_require__(9438); function run() { + console.info('[awaitStagingDeploys] POLL RATE', CONST_1.default.POLL_RATE); console.info('[awaitStagingDeploys] run()'); console.info('[awaitStagingDeploys] getStringInput', ActionUtils_1.getStringInput); console.info('[awaitStagingDeploys] GitHubUtils', GithubUtils_1.default); @@ -12742,7 +12743,12 @@ function promiseWhile(condition, action) { resolve(); return; } - Promise.resolve(actionResult).then(loop).catch(reject); + Promise.resolve(actionResult) + .then(() => { + // Set a timeout to delay the next loop iteration + setTimeout(loop, 1000); // 1000 ms delay + }) + .catch(reject); } }; loop(); diff --git a/.github/libs/promiseWhile.ts b/.github/libs/promiseWhile.ts index 01c061096d64..401b6ee2e18a 100644 --- a/.github/libs/promiseWhile.ts +++ b/.github/libs/promiseWhile.ts @@ -19,7 +19,12 @@ function promiseWhile(condition: () => boolean, action: (() => Promise) | return; } - Promise.resolve(actionResult).then(loop).catch(reject); + Promise.resolve(actionResult) + .then(() => { + // Set a timeout to delay the next loop iteration + setTimeout(loop, 1000); // 1000 ms delay + }) + .catch(reject); } }; loop(); diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 9eb5bc6eb409..39dfbe8e84a7 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -27,19 +27,49 @@ jobs: }); return jobsData.data; + - name: Fetch Previous Workflow Run + id: previous-workflow-run + uses: actions/github-script@v7 + with: + script: | + const runId = ${{ github.event.workflow_run.id }}; + const allRuns = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'preDeploy.yml', + }); + const filteredRuns = allRuns.data.workflow_runs.filter(run => run.actor.login !== 'OSBotify' && run.status !== 'cancelled'); + const currentIndex = filteredRuns.findIndex(run => run.id === runId); + const previousRun = filteredRuns[currentIndex + 1]; + return previousRun; + + - name: Fetch Previous Workflow Run Jobs + id: previous-workflow-jobs + uses: actions/github-script@v7 + with: + script: | + const previousRun = ${{ steps.previous-workflow-run.outputs.result }}; + const runId = previousRun.id; + const jobsData = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }); + return jobsData.data; + - name: Process Each Failed Job uses: actions/github-script@v7 with: script: | const jobs = ${{ steps.fetch-workflow-jobs.outputs.result }}; - + const previousRun = ${{ steps.previous-workflow-run.outputs.result }}; + const previousRunJobs = ${{ steps.previous-workflow-jobs.outputs.result }}; const headCommit = "${{ github.event.workflow_run.head_commit.id }}"; const prData = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner: context.repo.owner, repo: context.repo.repo, commit_sha: headCommit, }); - const pr = prData.data[0]; const prLink = pr.html_url; const prAuthor = pr.user.login; @@ -50,14 +80,8 @@ jobs: if (jobs.jobs[i].conclusion == 'failure') { const jobName = jobs.jobs[i].name; const jobLink = jobs.jobs[i].html_url; - const issues = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - labels: failureLabel, - state: 'open' - }); - const existingIssue = issues.data.find(issue => issue.title.includes(jobName)); - if (!existingIssue) { + const previousJob = previousRunJobs.jobs.find(job => job.name === jobName); + if (previousJob?.conclusion === 'success') { const annotations = await github.rest.checks.listAnnotations({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/android/app/build.gradle b/android/app/build.gradle index dca58eeb96ca..4a644b8aab74 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046602 - versionName "1.4.66-2" + versionCode 1001046803 + versionName "1.4.68-3" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/arrow-right.svg b/assets/images/arrow-right.svg index df13c75ca414..8d2ded92e791 100644 --- a/assets/images/arrow-right.svg +++ b/assets/images/arrow-right.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + diff --git a/assets/images/back-left.svg b/assets/images/back-left.svg index 51164100ff59..2ddd554e9720 100644 --- a/assets/images/back-left.svg +++ b/assets/images/back-left.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + diff --git a/assets/images/tax.svg b/assets/images/coins.svg similarity index 100% rename from assets/images/tax.svg rename to assets/images/coins.svg diff --git a/assets/images/invoice-generic.svg b/assets/images/invoice-generic.svg new file mode 100644 index 000000000000..d0e2662c4084 --- /dev/null +++ b/assets/images/invoice-generic.svg @@ -0,0 +1,15 @@ + + + + + + + diff --git a/assets/images/play.svg b/assets/images/play.svg index cb781459e44e..5f7e14969529 100644 --- a/assets/images/play.svg +++ b/assets/images/play.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + + diff --git a/assets/images/receipt-scan.svg b/assets/images/receipt-scan.svg new file mode 100644 index 000000000000..ecdf3cf2e115 --- /dev/null +++ b/assets/images/receipt-scan.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts index 8f32a2d95c99..7a196da6b691 100644 --- a/config/webpack/webpack.dev.ts +++ b/config/webpack/webpack.dev.ts @@ -21,16 +21,12 @@ const getConfiguration = (environment: Environment): Promise => process.env.USE_WEB_PROXY === 'false' ? {} : { - proxy: { - // eslint-disable-next-line @typescript-eslint/naming-convention - '/api': 'http://[::1]:9000', - // eslint-disable-next-line @typescript-eslint/naming-convention - '/staging': 'http://[::1]:9000', - // eslint-disable-next-line @typescript-eslint/naming-convention - '/chat-attachments': 'http://[::1]:9000', - // eslint-disable-next-line @typescript-eslint/naming-convention - '/receipts': 'http://[::1]:9000', - }, + proxy: [ + { + context: ['/api', '/staging', '/chat-attachments', '/receipts'], + target: 'http://[::1]:9000', + }, + ], }; const baseConfig = getCommonConfiguration(environment); diff --git a/docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md b/docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md deleted file mode 100644 index 2ff74760b376..000000000000 --- a/docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: International Reimbursements -description: International Reimbursements ---- -# Overview - -If your company’s business bank account is in the US, Canada, the UK, Europe, or Australia, you now have the option to send direct reimbursements to nearly any country across the globe! -The process to enable global reimbursements is dependent on the currency of your reimbursement bank account, so be sure to review the corresponding instructions below. - -# How to request international reimbursements - -## The reimbursement account is in USD - -If your reimbursement bank account is in USD, the first step is connecting the bank account to Expensify. -The individual who plans on sending reimbursements internationally should head to **Settings > Account > Payments > Add Verified Bank Account**. From there, you will provide company details, input personal information, and upload a copy of your ID. - -Once the USD bank account is verified (or if you already had a USD business bank account connected), click the support icon in your Expensify account to inform your Setup Specialist, Account Manager, or Concierge that you’d like to enable international reimbursements. From there, Expensify will ask you to confirm the currencies of the reimbursement and employee bank accounts. - -Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled. - -## The reimbursement account is in AUD, CAD, GBP, EUR - -To request international reimbursements, contact Expensify Support to make that request. - -You can do this by clicking on the support icon and informing your Setup Specialist, Account Manager, or Concierge that you’d like to set up global reimbursements on your account. -From there, Expensify will ask you to confirm both the currencies of the reimbursement and employee bank accounts. - -Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled. - -# How to verify the bank account for sending international payments - -Once international payments are enabled on your Expensify account, the next step is verifying the bank account to send the reimbursements. - -## The reimbursement account is in USD - -First, confirm the workspace settings are set up correctly by doing the following: -1. Head to **Settings > Workspaces > Group > _[Workspace Name]_ > Reports** and check that the workspace currency is USD -2. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the reimbursement method to direct -3. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the USD bank account to the default account - -Once that’s all set, head to **Settings > Account > Payments**, and click **Enable Global Reimbursement** on the bank account (this button may not show for up to 60 minutes after the Expensify team confirms international reimbursements are available on your account). - -From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required. - -## The reimbursement account is in AUD, CAD, GBP, EUR - -First, confirm the workspace currency corresponds with the currency of the reimbursement bank account. You can do this under **Settings > Workspaces > Group > _[Workspace Name]_ > Reports**. It should be AUD, CAD, GBP, or EUR. - -Next, add the bank account to Expensify: -1. Head to **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements** and set the reimbursement method to direct (this button may not show for up to 60 minutes after the Expensify team confirms international reimbursements are available on your account) -2. Click **Add Business Bank Account** -3. If the incorrect country shows as the default, click **Switch Country** to select the correct country -4. Enter the bank account details -5. Click **Save & Continue** - -From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required. - -# How to start reimbursing internationally - -After the bank account is verified for international payments, set the correct bank account as the reimbursement account. - -You can do this under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements** by selecting the reimbursement account as the default account. - -Finally, have your employees add their deposit-only bank accounts. They can do this by logging into their Expensify accounts, heading to **Settings > Account > Payments**, and clicking **Add Deposit-Only Bank Account**. - -# Deep Dive - -## Documents requested - -Our Compliance Team may ask for additional information depending on who initiates the verification or what information you provide on the DocuSign form. - -Examples of additional requested information: -- The reimburser’s proof of address and ID -- Company directors’ proofs of address and IDs -- An authorization letter -- An independently certified documentation such as shareholder agreement from a lawyer, notary, or public accountant if an individual owns more than 25% of the company - -{% include faq-begin.md %} - -## How many people can send reimbursements internationally? - -Once your company is authorized to send global payments, the individual who verified the bank account can share it with additional admins on the workspace. That way, multiple workspace members can send international reimbursements. - -## How long does it take to verify an account for international payments? - -It varies! The verification process can take a few business days to several weeks. It depends on whether or not the information in the DocuSign form is correct if our Compliance Team requires any additional information, and how responsive the employee verifying the company’s details is to our requests. - -## If I already have a USD bank account connected to Expensify, do I need to go through the verification process again to enable international payments? - -If you’ve already connected a US business bank account, you can request to enable global reimbursements by contacting Expensify Support immediately. However, additional steps are required to verify the bank account for international payments. - -## My employee says they don’t have the option to add their non-USD bank account as a deposit account – what should they do? - -Have the employee double-check that their default workspace is set as the workspace that's connected to the bank you're using to send international payments. - -An employee can confirm their default workspace is under **Settings > Workspaces > Group**. The default workspace has a green checkmark next to it. They can change their default workspace by clicking **Default Workspace** on the correct workspace. - -## Who is the “Authorized User” on the International Reimbursement DocuSign form? - -This is the person who will process international reimbursements. The authorized user should be the same person who manages the bank account connection in Expensify. - -## Who should I enter as the “User” on the International Reimbursement form? - -You can leave this form section blank since the “User” is Expensify. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md b/docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md deleted file mode 100644 index 4c64ab1cefe4..000000000000 --- a/docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Managing employees and reports > Approval workflows -description: Set up the workflow that your employees reports should flow through. ---- - - -# About -## Overview - - -This document explains how to manage employee expense reports and approval workflows in Expensify. - - -### Approval workflow modes - - -#### Submit and close -- This is a workflow where no approval occurs in Expensify. -- *What happens after submission?* The report state becomes Closed and is available to view by the member set in Submit reports to and any Workspace Admins. -- *Who should use this workflow?* This mode should be used where you don't require approvals in Expensify. - - -#### Submit and approve -- *Submit and approve* is a workflow where all reports are submitted to a single member for approval. New policies have Submit and Approve enabled by default. -- *What happens after submission?* The report state becomes Processing and it will be sent to the member indicated in Submit reports to for approval. When the member approves the report, the state will become Approved. -- *Who should use this workflow?* This mode should be used where the same person is responsible for approving all reports for your organization. If submitters have different approvers or multiple levels of approval are required, then you will need to use Advance Approval. - - -#### Advanced Approval -- This approval mode is used to handle more complex workflows, including: - - *Multiple levels of approval.* This is for companies that require more than one person to approve a report before it can be reimbursed. The most common scenario is when an employee needs to submit to their manager, and their manager needs to approve and forward that report to their finance department for final approval. - - *Varying approval workflows.* For example, if a company has Team A submitting reports to Manager A, and Team B to Manager B, use Advanced Approval. Group Workspace Admins can also set amount thresholds in the case that a report needs to go to a different approver based on the amount. -- *What happens after submission?* After the report is submitted, it will follow the set approval chain. The report state will be Processing until it is Final Approved. We have provided examples of how to set this up below. -- *Who should use this workflow?* Organizations with complex workflows or 2+ levels of approval. This could be based on manager approvals or where reports over a certain size require additional approvals. -- *For further automation:* use Concierge auto-approval for reports. You can set specific rules and guidelines in your Group Workspace for your team's expenses; if all expenses are below the Manual Approval Threshold and adhere to all the rules, then we will automatically approve these reports on behalf of the approver right after they are submitted. - - -### How to set an approval workflow - -- Step-by-step instructions on how to set this up at the Workspace level [here](link-to-instructions). - -# Deep Dive - -### Setting multiple levels of approval -- 'Submits to' is different than 'Approves to'. - - *Submits to* - is the person you are sending your reports to for 1st level approval - - *Approves to* - is the person you are sending the reports you've approved for higher-level approval -- In the example below, a report needs to be approved by multiple managers: *Submitter > Manager > Director > Finance/Accountant* - - *Submitter (aka. Employee):* This is the person listed under the member column of the People page. - - *First Approver (Manager):* This is the person listed under the Submits to column of the People Page. - - *Second Approver (Director):* This is the person listed as 'Approves to' in the Settings of the First Approver. - - *Final Approver (Finance/Accountant):* This is the person listed as the 'Approves to' in the Settings of the Second Approver. -- This is what this setup looks like in the Workspace Members table. - - Bryan submits his reports to Jim for 1st level approval. -![Screenshot showing the People section of the workspace]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_1.png){:width="100%"} - - - All of the reports Jim approves are submitted to Kevin. Kevin is the 'approves to' in Jim's Settings. -![Screenshot of Policy Member Editor]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_2.png){:width="100%"} - - - All of the reports Kevin approves are submitted to Lucy. Lucy is the 'approves to' in Kevin's Settings. -![Screenshot of Policy Member Editor Approves to]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_3.png){:width="100%"} - - - - Lucy is the final approver, so she doesn't submit her reports to anyone for review. -![Screenshot of Policy Member Editor Final Approver]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_4.png){:width="100%"} - - -- The final outcome: The member in the Submits To line is different than the person noted as the Approves To. -### Adding additional approver levels -- You can also set a specific approver for Reports Totals in Settings. -![Screenshot of Policy Member Editor Approves to]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_5.png){:width="100%"} - -- An example: The submitter's manager can approve any report up to a certain limit, let's say $500, and forward it to accounting. However, if a report is over that $500 limit, it has to be also approved by the department head before being forwarded to accounting. -- To configure, click on Edit Settings next to the approving manager's email address and set the "If Report Total is Over" and "Then Approves to" fields. -![Screenshot of Workspace Member Settings]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_6.png){:width="100%"} -![Screenshot of Policy Member Editor Configure]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_7.png){:width="100%"} - - -### Setting category approvals -- If your expense reports should be reviewed by an additional approver based on specific categories or tags selected on the expenses within the report, set up category approvers and tag approvers. -- Category approvers can be set in the Category settings for each Workspace -- Tag approvers can be set in the Tag settings for each Workspace - - -#### Category approver -- A category approver is a member who is added to the approval workflow for any reports in your Expensify Workspace that contain expenses with a specific category. -- For example: Your HR director Jim may need to approve any relocation expenses submitted by employees. Set Jim up as the category approver for your Relocation category, then any reports containing Relocation expenses will first be routed to Jim before continuing through the approval workflow. -- Adding category approvers - - To add a category approver in your Workspace: - - Navigate to *Settings > Policies > Group > [Workspace Name] > Categories* - - Click *"Edit Settings"* next to the category that requires the additional approver - - Select an approver and click *"Save"* - - -#### Tag approver -- A tag approver is a member who is added to the approval workflow for any reports in your Expensify Workspace that contain expenses with a specific tag. -- For example: If employees must tag project-based expenses with the corresponding project tag. Pam, the project manager is set as the project approver for that project, then any reports containing expenses with that project tag will first be routed to Pam for approval before continuing through the approval workflow. -- Please note: Tag approvers are only supported for a single level of tags, not for multi-level tags. The order in which the report is sent to tag approvers relies on the date of the expense. -- Adding tag approvers - - To add a tag approver in your Workspace: - - Navigate to *Settings > Policies > Group > [Workspace Name] > Tags* - - Click in the "Approver" column next to the tag that requires an additional approver - - -Category and Tag approvers are inserted at the beginning of the approval workflow already set on the People page. This means the workflow will look something like: * *Submitter > Category Approver(s) > Tag Approver(s) > Submits To > Previous approver's Approves To.* - - -### Workflow enforcement -- If you want to ensure your employees cannot override the workflow you set - enable workflow enforcement. As a Workspace Admin, you can choose to enforce your approval workflow by going to Settings > Workspaces > Group > [Workspace Name] > People > Approval Mode. When enabled (which is the default setting for a new workspace), submitters and approvers must adhere to the set approval workflow (recommended). This setting does not apply to Workspace Admins, who are free to submit outside of this workflow diff --git a/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md index 3fe5ec41f5f6..ed44caad546b 100644 --- a/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md +++ b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md @@ -15,9 +15,9 @@ Each month, you’ll be billed for the amount of members you originally set in y For example, let’s say you set your annual subscription size at 10 members and you’re on the Control plan. You’ll be billed $18/member for 10 members each month. However, let’s say in one particular month you had 12 active members, you’d be billed at $18/member for the 10 members in your subscription size + $36/member (pay-per-use rate) for the additional 2 active members. -You can always increase your annual subscription size, which will extend your annual subscription length. You cannot reduce your annual subscription size until your current subscription has ended. If you have any questions about this, reach out to Concierge or your account manager. +You can always increase your annual subscription size, which will extend your annual subscription length. However, you cannot reduce your annual subscription size until your current subscription has ended. If you have any questions about this, contact Concierge or your account manager. ## Pay-per-use -The pay-per-use rate is the full rate per active member without any discounts. The pay-per-use rate for each member on the Collect plan is $20 and on the Control plan is $36. +The pay-per-use rate is the full rate per active member without any discounts. The pay-per-use rate for each member on the Collect plan is $20, and on the Control plan, it is $36. ## How the Expensify Card can reduce your bill Bundling the Expensify Card with an annual subscription ensures you pay the lowest possible monthly price for Expensify. And the more you spend on Expensify Cards, the lower your bill will be. @@ -26,15 +26,19 @@ If at least 50% of your approved USD spend in a given month is on your company Additionally, every month, you receive 1% cash back on all Expensify Card purchases, and 2% if the spend across your Expensify Cards is $250k or more (_applies to USD purchases only_). Any cash back from the Expensify Card is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. ## Savings calculator To see how much money you can save (and even earn!) by using the Expensify Card, check out our [savings calculator](https://use.expensify.com/price-savings-calculator). Just enter a few details and see how much you’ll save! + {% include faq-begin.md %} + ## What if we put less than 50% of our total spend on the Expensify Card? -If you put less than 50% of your total USD spend on your Expensify Card, your bill gets discounted on a sliding scale based on the percentage of use. So if you don't use the Expensify Card at all, you'll be charged the full rate for each member based on your plan and subscription. -Example: +If less than 50% of your total USD spend is on the Expensify Card, the bill is discounted on a sliding scale. + +**Example:** - Annual subscription discount: 50% - % of Expensify Card spend (USD) across all workspaces: 20% - Expensify Card discount: 20% -You save 70% on the price per member on your bill for that month. -Note: USD spend refers to approved USD transactions on the Expensify Card in any given month, compared to all approved USD spend on workspaces in that same month. +In that case, you'd save 70% on the price per member for that month's bill. + +Note: USD spend refers to approved USD transactions on the Expensify Card in any given month. {% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md new file mode 100644 index 000000000000..c5578249289a --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md @@ -0,0 +1,86 @@ +--- +title: Unlimited Virtual Cards +description: Learn more about virtual cards and how they can help your business gain efficiency and insight into company spending. +--- + +# Overview + +For admins to issue virtual cards, your company **must upgrade to Expensify’s new Expensify Visa® Commercial Card.** + +Once upgraded to the new Expensify Card, admins can issue an unlimited number of virtual cards with a fixed or monthly limit for specific company purchases or recurring subscription payments _(e.g., Marketing purchases, Advertising, Travel, Amazon Web Services, etc.)._ + +This feature supports businesses that require tighter controls on company spending, allowing customers to set fixed or monthly spending limits for each virtual card. + +Use virtual cards if your company needs or wants: + +- To use one card per vendor or subscription, +- To issue cards for one-time purchases with a fixed amount, +- To issue cards for events or trips, +- To issue cards with a low limit that renews monthly, + +Admins can also name each virtual card, making it easy to categorize and assign them to specific accounts upon creation. Naming the card ensures a clear and organized overview of expenses within the Expensify platform. + +# How to set up virtual cards + +After adopting the new Expensify Card, domain admins can issue virtual cards to any employee using an email matching your domain. Once created and assigned, the card will be visible under the name given to the card. + +**To assign a virtual card:** + +1. Head to **Settings** > **Domains** > [**Company Cards**](https://www.expensify.com/domain_companycards). +2. Click the **Issue Virtual Cards** button. +3. Enter a card name (i.e., "Google Ads"). +4. Select a domain member to assign the card to. +5. Enter a card limit. +6. Select a **Limit Type** of _Fixed_ or _Monthly_. +7. Click **Issue Card**. + +![The Issue Virtual Cards modal is open in the middle of the screen. There are four options to set; Card Name, Assignee, Card Limit, and Limit type. A cancel (left) and save (right) button are at the bottom right of the modal.]({{site.url}}/assets/images/AdminissuedVirtualCards.png){:width="100%"} + +# How to edit virtual cards + +Domain admin can update the details of a virtual card on the [Company Cards](https://www.expensify.com/domain_companycards) page. + +**To edit a virtual card:** + +1. Click the **Edit** button to the right of the card. +2. Change the editable details. +3. Click **Edit Card** to save the changes. + +# How to terminate a virtual card + +Domain admin can also terminate a virtual card on the [Company Cards](https://www.expensify.com/domain_companycards) page by setting the limit to $0. + +**To terminate a virtual card:** + +1. Click the **Edit** button to the right of the card. +2. Set the limit to $0. +3. Click **Save**. +4. Refresh your web page, and the card will be removed from the list. + +{% include faq-begin.md %} + +**What is the difference between a fixed limit and a monthly limit?** + +There are two different limit types that are best suited for their intended purpose. + +- _Fixed limit_ spend cards are ideal for one-time expenses or providing employees access to a card for a designated purchase. +- _Monthly_ limit spend cards are perfect for managing recurring expenses such as subscriptions and memberships. + +**Where can employees see their virtual cards?** + +Employees can see their assigned virtual cards by navigating to **Settings** > **Account** > [**Credit Cards Import**](https://www.expensify.com/settings?param=%7B%22section%22:%22creditcards%22%7D) in their account. + +On this page, employees can see the remaining card limit, the type of card it is (i.e., fixed or monthly), and view the name given to the card. + +When the employee needs to use the card, they’ll click the **Show Details** button to expose the card details for making purchases. + +_Note: If the employee doesn’t have Two-Factor Authentication (2FA) enabled when they display the card details, they’ll be prompted to enable it. Enabling 2FA for their account provides the best protection from fraud and is **required** to dispute virtual card expenses._ + +**What do I do when there is fraud on one of our virtual cards?** + +If you or an employee loses their virtual card, experiences fraud, or suspects the card details are no longer secure, please [request a new card](https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction) immediately. A domain admin can also set the limit for the card to $0 to terminate the specific card immediately if the employee cannot take action. + +When the employee requests a new card, the compromised card will be terminated immediately. This is best practice for any Expensify Card and if fraud is suspected, action should be taken as soon as possible to reduce financial impact on the company. + +{% include faq-end.md %} + diff --git a/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md b/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md new file mode 100644 index 000000000000..9467c07d95ba --- /dev/null +++ b/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md @@ -0,0 +1,35 @@ +--- +title: Assign tag and category approvers +description: Require an approver for expenses coded with a specific tag or category +--- +
+ +Once your workplace has created tags and categories, approvers can be assigned to them. Tag and category approvers are automatically added to the report approval workflow when a submitted expense contains a specific tag or category. + +For example, if all employees are required to tag project-based expenses with a tag for the project, you can assign the project manager as the approver for that tag. This way, when a report is submitted containing expenses with that project tag, it will first be routed to the project manager for approval before continuing through the rest of the approval workflow. + +If a report contains multiple categories or tags that each require a different reviewer, then each reviewer must review the report before it can be submitted. The report will first go to the category approvers, the tag approvers, and then the approvers assigned in the approval workflow. + +# Assign category approvers + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Categories** tab on the left. +5. Locate the category in the list of categories and click **Edit**. +6. Click the Approver field to select an approver. +7. Click **Save**. + +# Assign tag approvers + +{% include info.html %} +Tag approvers are only supported for a single level of tags, not for multi-level tags. +{% include end-info.html %} + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Tags** tab on the left. +5. Locate the tag in the list of tags and click the Approver field to assign an approver. + +
diff --git a/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md b/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md new file mode 100644 index 000000000000..49b5bd522464 --- /dev/null +++ b/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md @@ -0,0 +1,41 @@ +--- +title: Automatically submit employee reports +description: Use Expensify's Scheduled Submit feature to have your employees' expenses submitted automatically for them +--- +
+ +Scheduled Submit automatically adds expenses to a report and sends them for approval so that your employees do not have to remember to manually submit their reports each week. This allows you to automatically collect employee expenses on a schedule of your choosing. + +With Scheduled Submit, an employee's expenses are automatically gathered onto a report as soon as they create them. If there is not an existing report, a new one is created. The report is then automatically submitted at the cadence you choose—daily, weekly, monthly, twice per month, or by trip. + +{% include info.html %} +If an expense has a violation, Scheduled Submit will not automatically submit it until the violations are corrected. In the meantime, the expense will be removed from the report and added to an open report. +{% include end-info.html %} + +# Enable Scheduled Submit + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left (or click the Individual tab to enable Scheduled Submit for your individual workspace). +3. Click the desired workspace name. +4. Click the **Reports** tab on the left. +5. Click the Scheduled Submit toggle to enable it. +6. Click the “How often expenses submit” dropdown and select the submission schedule: + - **Daily**: Expenses are submitted every evening. Expenses with violations are submitted the day after the violations are corrected. + - **Weekly**: Expenses are submitted once a week. Expenses with violations are submitted the following Sunday after the violations are corrected. + - **Twice a month**: Expenses are submitted on the 15th and the last day of each month. Expenses with violations are submitted at the next cycle (either on the 15th or the last day of the month, whichever is closest). + - **Monthly**: Expenses are submitted once per month. If you select Monthly, you will also select which day of the month the reports will be submitted. Expenses with violations are submitted on the next monthly submission date. + - **By trip**: All expenses that occur in a similar time frame are grouped together. The trip report is created after no new expenses have been submitted for two calendar days. Then the report is submitted the second day, and any new expenses are added to a new trip report. + - **Manually**: Expenses are automatically added to an open report, but the report will require manual submission—it will not be submitted automatically. This is a great option for automatically gathering an employee’s expenses on a report while still requiring the employee to review and submit their report. + +{% include info.html %} +- All submission times are in the evening PDT. +- If you enable Scheduled Submit for your individual workspace and one of your group workspaces also has Scheduled Submit enabled, the group’s submission settings will override your individual workspace settings. +{% include end-info.html %} + +# FAQs + +**I disabled Scheduled Submit. Why do I still get reports submitted by Concierge?** + +Although an Admin can disable scheduled submit for a workspace, employees have the ability to activate schedule submit for their account. If you disable Scheduled Submit but still receive reports from Concierge, the employee has Schedule Submit activated for their individual workspace. + +
diff --git a/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md b/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md index 857217189e50..aa5aea545a23 100644 --- a/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md +++ b/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md @@ -60,6 +60,8 @@ You can retract a submitted report to edit the reported expenses and re-submit t 4. Tap **Retract** at the top of the report. {% include end-option.html %} +**Note:** Workspaces with Instant Submit set as the Scheduled Submit frequency won’t have the option to Retract entire reports, only individual expenses. + {% include end-selector.html %} diff --git a/docs/articles/expensify-classic/settings/Close-or-reopen-account.md b/docs/articles/expensify-classic/settings/account-settings/Close-or-reopen-account.md similarity index 100% rename from docs/articles/expensify-classic/settings/Close-or-reopen-account.md rename to docs/articles/expensify-classic/settings/account-settings/Close-or-reopen-account.md diff --git a/docs/articles/expensify-classic/settings/Notification-Troubleshooting.md b/docs/articles/expensify-classic/settings/account-settings/Set-Notifications.md similarity index 99% rename from docs/articles/expensify-classic/settings/Notification-Troubleshooting.md rename to docs/articles/expensify-classic/settings/account-settings/Set-Notifications.md index 22df0dc7f6ca..0e18d6f22cf5 100644 --- a/docs/articles/expensify-classic/settings/Notification-Troubleshooting.md +++ b/docs/articles/expensify-classic/settings/account-settings/Set-Notifications.md @@ -1,5 +1,5 @@ --- -title: Notification Troubleshooting +title: Set notifications description: This article is about how to troubleshoot notifications from Expensify. --- diff --git a/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md b/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md deleted file mode 100644 index 2d561ea598d9..000000000000 --- a/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Set notifications -description: Select your Expensify notification preferences ---- -
- -{% include info.html %} -This process is currently not available from the mobile app and must be completed from the Expensify website. -{% include end-info.html %} - -1. Hover over Settings and click **Account**. -2. Click the **Preferences** tab on the left. -3. Scroll down to the Contact Preferences section. -4. Select the checkbox for the types of notifications you wish to receive. -
diff --git a/docs/articles/expensify-classic/spending-insights/Insights.md b/docs/articles/expensify-classic/spending-insights/Insights.md index ce07f4b56450..c5ee218352fd 100644 --- a/docs/articles/expensify-classic/spending-insights/Insights.md +++ b/docs/articles/expensify-classic/spending-insights/Insights.md @@ -4,16 +4,16 @@ description: How to get the most out of the Custom Reporing and Insights redirect_from: articles/other/Insights/ --- -{% raw %} -# What is Custom Reporting and Insights? -The Insights dashboard allows you to monitor all aspects of company spend across categories, employees, projects, departments, and more. You can see trends in real time, forecast company budgets, and build unlimited custom reports with help from our trained specialist team. + +# Overview +The Insights dashboard allows you to monitor all aspects of company spending across categories, employees, projects, departments, and more. You can see trends in real-time, forecast company budgets, and build unlimited custom reports with help from our trained specialist team. ![Insights Pie Chart](https://help.expensify.com/assets/images/insights-chart.png){:width="100%"} ## Review your Insights data -1. Navigate to your [Insights page](https://www.expensify.com/expenses?param={"fromInsightsTab":true,"viewMode":"charts"}), located in the left hand menu +1. Navigate to your [Insights page](https://www.expensify.com/expenses?param={"fromInsightsTab":true,"viewMode":"charts"}), located in the left-hand menu 2. Select a specific date range (the default view has the current month pre-selected) -3. Use the filter options to select the categories, tags, employees etc that you want insights on +3. Use the filter options to select the categories, tags, employees, or any other parameter 4. Make sure that View in the top right corner is set to the pie chart icon 5. You can view any dataset in more detail by clicking in the “View Raw Data” column @@ -21,50 +21,50 @@ The Insights dashboard allows you to monitor all aspects of company spend across 1. Switch the View in the top right corner of the [Insights page](https://www.expensify.com/expenses?param={"fromInsightsTab":true,"viewMode":"charts"}) to the lists icon 2. Select the expenses you want to export, either by selecting individual expenses, or checking the select all box (next to Date at the top) -3. Select **Export To** in the top right hand corner to download the report as a .csv file +3. Select **Export To** in the top right-hand corner to download the report as a .csv file ## Create a Custom Export Report for your Expenses 1. Navigate to **Settings > Account > Preferences > scroll down to CSV Export Formats** -2. Build up a report using these [formulas](https://community.expensify.com/discussion/5795/deep-dive-expense-level-formula/p1?new=1) +2. Build up a report using these [formulas]((https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates)) 3. Click the **Custom Export** button on the Insights page and your Account Manager will help get you started on building up your report -## Create a Custom Export Report for your Policy +## Create a Custom Export Report for your Workspace -1. Navigate to **Settings > Policies > Group > [Policy Name] > Export Formats** -2. Build up a report using these [formulas](https://community.expensify.com/discussion/5795/deep-dive-expense-level-formula/p1?new=1) +1. Navigate to **Settings > Workspaces > Group > [Workspace Name] > Export Formats** +2. Build up a report using these [formulas](https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates) 3. If you need any help, click the **Support** button on the top left to contact your Account Manager {% include faq-begin.md %} #### Can I share my custom export report? -If you would like to create a custom export report that can be shared with other policy admins, you can create these by navigating to the **[Settings > Policies > Group > [Policy Name] > Export Formats](https://www.expensify.com/admin_policies?param={"section":"group"})** page. Custom export reports created under **Settings > Account > Preferences** page are only available to the member who created them. +If you would like to create a custom export report that can be shared with other workspace admins, you can do so by navigating to the **[Settings > Workspaces > Group > [Workspace Name] > Export Formats** page. Custom export reports created under the **Settings > Account > Preferences** page are only available to the member who created them. -#### Can I put expenses from different policies on the same report? +#### Can I put expenses from different workspaces on the same report? -Custom export reports created under Settings > Account > Preferences page are able to export expenses from multiple policies, and custom export formats created under Settings > Policies > Group > [Policy Name] > Export Formats are for expenses reported under that policy only. +Custom export reports created under the Settings > Account > Preferences page can export expenses from multiple workspaces, and custom export formats created under Settings > Workspaces> Group > [Workspace Name] > Export Formats are for expenses reported under that workspace only. #### Are there any default export reports available? -Yes! We have [seven default reports](https://community.expensify.com/discussion/5602/deep-dive-default-export-templates) available to export directly from the Reports page: +Yes! We have [seven default reports](https://help.expensify.com/articles/expensify-classic/spending-insights/Default-Export-Templates) available to export directly from the Reports page: - **All Data** - Expense Level Export** - the name says it all! This is for the people who want ALL the details from their expense reports. We're talking Tax, Merchant Category Codes, Approvers - you name it, this report's got it! -- **All Data** - Report Level Export - this is the report for those who don't need to see each individual expense but want to see a line by line breakdown at a report level - submitter, total amount, report ID - that kind of stuff +- **All Data** - Report Level Export - this is the report for those who don't need to see each individual expense but want to see a line-by-line breakdown at a report level - submitter, total amount, report ID - that kind of stuff - **Basic Export** - this is the best way to get a simple breakdown of all your expenses - just the basics - **Canadian Multiple Tax Export** - tax, GST, PST...if you need to know tax then this is the export you want! - **Category Export** - want to see a breakdown of your expenses by Category? This is the export you - **Per Diem Export** - the name says it all - **Tag Export** - much like the Category Export, but for Tags -*To note: these reports will be emailed directly to your email address rather than downloaded on your computer.* +*These reports will be emailed directly to your email address rather than automatically downloaded.* #### How many expenses can I export in one report? -The custom export reports are best for small-to-medium chunks of data. If you want to export large amounts of data, we recommend you use a [default export report](https://community.expensify.com/discussion/5602/deep-dive-default-export-templates) that you can run from the Reports page. +The custom export reports are best for small-to-medium chunks of data. If you want to export large amounts of data, we recommend you use a [default export report](https://help.expensify.com/articles/expensify-classic/spending-insights/Default-Export-Templates) that you can run from the Reports page. #### What other kinds of export reports can my Account Manager help me create? -We’ve built a huge variety of custom reports for customers, so make sure to reach out to your Account Manager for more details. Some examples of custom reports we’ve build for customers before are: +We’ve built a huge variety of custom reports for customers, so make sure to reach out to your Account Manager for more details. Some examples of custom reports we’ve built for customers before are: - Accrual Report - Aged Approval Reports @@ -97,7 +97,5 @@ We’ve built a huge variety of custom reports for customers, so make sure to re - Unposted Procurement Aging Report - Unposted Travel Aging Report - Vendor Spend -- … or anything you can imagine! -{% endraw %} {% include faq-end.md %} diff --git a/docs/articles/expensify-classic/workspaces/tax-tracking.md b/docs/articles/expensify-classic/workspaces/Tax-Tracking.md similarity index 100% rename from docs/articles/expensify-classic/workspaces/tax-tracking.md rename to docs/articles/expensify-classic/workspaces/Tax-Tracking.md diff --git a/docs/articles/new-expensify/chat/Create-a-new-chat.md b/docs/articles/new-expensify/chat/Create-a-new-chat.md new file mode 100644 index 000000000000..db76946b3d83 --- /dev/null +++ b/docs/articles/new-expensify/chat/Create-a-new-chat.md @@ -0,0 +1,112 @@ +--- +title: Create a new chat +description: Start a new private, group, or room chat +redirect_from: articles/other/Everything-About-Chat/ +--- +
+ +Expensify Chat is an instant messaging system that helps you converse with people both inside and outside of your workspace about payments, company updates, and more. Expensify Chats are held in private chats, groups, and rooms. +- **Private chats**: Private conversations for 1-on-1 chats +- **Groups**: Private conversations for 2+ participants +- **Rooms**: Public conversations that are available for all members of your workspace + +# Start a private 1-on-1 chat + +{% include info.html %} +You cannot add more people to a private chat. If later you wish to add more people to the conversation, you’ll need to create a group chat. +{% include end-info.html %} + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + button in the bottom left menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and click their name to start a new chat with them. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + button in the bottom menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and tap their name to start a new chat with them. +{% include end-option.html %} + +{% include end-selector.html %} + +# Start a group chat + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + button in the bottom left menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and click **Add to group**. Repeat this step until all desired participants are added. *Note: Group participants are listed with a green checkmark.* +3. Click **Next**. +4. Update the group image or name. + - **Name**: Click **Group Name** and enter a new name. Then click **Save**. + - **Image**: Click the profile image and select **Upload Image**. Then choose a new image from your computer files and select the desired image zoom. +5. Click **Start group**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + button in the bottom menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and tap **Add to group**. Repeat this step until all desired participants are added. *Note: Group participants are listed with a green checkmark.* +3. Tap **Next** (or **Create chat** if you add only one person to the group). +4. Update the group image or name. + - **Name**: Tap **Group Name** and enter a new name. Then tap **Save**. + - **Image**: Tap the profile image and select **Upload Image**. Then choose a new image from your photos and select the desired image zoom. +5. Tap **Start group**. + +{% include end-option.html %} + +{% include end-selector.html %} + +# Start a chat room + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + button in the bottom left menu and select **Start Chat**. +2. Click the #Room tab at the top. +3. Enter a name for the room. *Note: It cannot be the same as another room in the Workspace.* +4. (Optional) Add a description of the room. +5. Click **Workspace** to select the workspace for the room. +6. Click **Who can post** to determine if all members can post or only Admins. +7. Click **Visibility** to determine who can find the room. + - **Public**: Anyone can find the room (perfect for conferences). + - **Private**: Only people explicitly invited can find the room. + - **Workspace**: Only workspace members can find the room. + +{% include info.html %} +Anyone, including non-Workspace Members, can be invited to a private or restricted room. +{% include end-info.html %} + +8. Click **Create room**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + button in the bottom menu and select **Start Chat**. +2. Tap the #Room tab at the top. +3. Enter a name for the room. *Note: It cannot be the same as another room in the Workspace.* +4. (Optional) Add a description of the room. +5. Tap **Workspace** to select the workspace for the room. +6. Tap **Who can post** to determine if all members can post or only Admins. +7. Tap **Visibility** to determine who can find the room. + - **Public**: Anyone can find the room (perfect for conferences). + - **Private**: Only people explicitly invited can find the room. + - **Workspace**: Only workspace members can find the room. + +{% include info.html %} +Anyone, including non-Workspace Members, can be invited to a private or restricted room. +{% include end-info.html %} + +8. Tap **Create room**. +{% include end-option.html %} + +{% include end-selector.html %} + +# FAQs + +**What's the difference between a private 1-on-1 chat and a group chat with only 2 people?** +With a group chat, you can add additional people to the chat at any time. But you cannot add additional people to a private 1-on-1 chat. +
+ + + + diff --git a/docs/articles/new-expensify/chat/Edit-or-delete-messages.md b/docs/articles/new-expensify/chat/Edit-or-delete-messages.md new file mode 100644 index 000000000000..a19fee42e740 --- /dev/null +++ b/docs/articles/new-expensify/chat/Edit-or-delete-messages.md @@ -0,0 +1,31 @@ +--- +title: Edit or delete messages +description: Edit or delete chat messages you've sent +--- +
+ +{% include info.html %} +You can edit or delete your *own* messages only. Deleting a message cannot be undone. +{% include end-info.html %} + +You have the option to edit or delete any of your messages: +- **Edit message**: Reopens a message so you can make changes. Once a message has been updated, an “edited” label will appear next to it. +- **Delete message**: Removes a message or image for all viewers. + +To edit or delete a message, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open a chat in your inbox. +2. Right-click a message and select **Edit comment** or **Delete comment**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open a chat in your inbox. +2. Press and hold a message and select **Edit comment** or **Delete comment**. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md deleted file mode 100644 index 30eeb4158902..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Expensify Chat for Conference Attendees -description: Best Practices for Conference Attendees -redirect_from: articles/other/Expensify-Chat-For-Conference-Attendees/ ---- - -# Overview -Expensify Chat is the best way to meet and network with other event attendees. No more hunting down your contacts by walking the floor or trying to find someone in crowds at a party. Instead, you can use Expensify Chat to network and collaborate with others throughout the conference. - -To help get you set up for a great event, we’ve created a guide to help you get the most out of using Expensify Chat at the event you’re attending. - -# Getting Started -We’ve rounded up some resources to get you set up on Expensify Chat and ready to start connecting with your fellow attendees: - -- [How to get set up and start using Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-use-chat-in-expensify) -- [How to format text in Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-format-text) -- [How to flag content and/or users for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) - -# Chat Best Practices -To get the most out of your experience at your conference and engage people in a meaningful conversation that will fulfill your goals instead of turning people off, here are some tips on what to do and not to do as an event attendee using Expensify Chat: - -**Do:** -- Chat about non-business topics like where the best coffee is around the event, what great lunch options are available, or where the parties are happening that night! -- Share pictures of your travel before the event to hype everyone up, during the event if you met that person you’ve been meaning to see for years, or a fun pic from a party. -- Try to create fun groups with your fellow attendees around common interests like touring a local sight, going for a morning run, or trying a famous restaurant. - -**Don't:** -- Pitch your services in public rooms like #social or speaking session rooms. -- Start a first message with a stranger with a sales pitch. -- Discuss controversial topics such as politics, religion, or anything you wouldn’t say on a first date. -- In general just remember that you are still here for business, your profile is public, and you’re representing yourself & company, so do not say anything you wouldn’t feel comfortable sharing in a business setting. - -**Pro-Tips:** -Get active in Chat early and often by having real conversations around thought leadership or non-business discussions to stand out from the crowd! Also if you’re in a session and are afraid to ask a question, just ask in the chat room to make sure you can discuss it with the speaker after the session ends. - -By following these tips you’ll ensure that your messages will not be [flagged for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) and you will not mess it up for the rest of us. diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md deleted file mode 100644 index 652fc2ee4d2b..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Expensify Chat for Conference Speakers -description: Best Practices for Conference Speakers -redirect_from: articles/other/Expensify-Chat-For-Conference-Speakers/ ---- - -# Overview -Are you a speaker at an event? Great! We're delighted to provide you with an extraordinary opportunity to connect with your session attendees using Expensify Chat — before, during, and after the event. Expensify Chat offers a powerful platform for introducing yourself and your topic, fostering engaging discussions about your presentation, and maintaining the conversation with attendees even after your session is over. - -# Getting Started -We’ve rounded up some resources to get you set up on Expensify Chat and ready to start connecting with your session attendees: - -- [How to get set up and start using Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-use-chat-in-expensify) -- [How to format text in Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-format-text) -- [How to flag content and/or users for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) - -# Setting Up a Chatroom for Your Session: Checklist -To make the most of Expensify Chat for your session, here's a handy checklist: -- Confirm that your session has an Expensify Chat room, and have the URL link ready to share with attendees in advance. - - You can find the link by clicking on the avatar for your chatroom > “Share Code” > “Copy URL to dashboard” -- Join the chat room as soon as it's ready to begin engaging with your audience right from the start. -- Consider having a session moderator with you on the day to assist with questions and discussions while you're presenting. -- Include the QR code for your session's chat room in your presentation slides. Displaying it prominently on every slide ensures that attendees can easily join the chat throughout your presentation. - -# Tips to Enhance Engagement Around Your Session -By following these steps and utilizing Expensify Chat, you can elevate your session to promote valuable interactions with your audience, and leave a lasting impact beyond the conference. We can't wait to see your sessions thrive with the power of Expensify Chat! - -**Before the event:** -- Share your session's QR code or URL on your social media platforms, your website or other platforms to encourage attendees to join the conversation early on. -- Encourage attendees to ask questions in the chat room before the event, enabling you to tailor your session and address their specific interests. - -**During the event:** -- Keep your QR code readily available during the conference by saving it as a photo on your phone or setting it as your locked screen image. This way, you can easily share it with others you meet. -- Guide your audience back to the QR code and encourage them to ask questions, fostering interactive discussions. - -**After the event:** -- Continue engaging with attendees by responding to their questions and comments, helping you expand your audience and sustain interest. -- Share your presentation slides after the event as well as any photos from your session, allowing attendees to review and share your content with their networks if they want to. - -If you have any questions on how Expensify Chat works, head to our guide [here](https://help.expensify.com/articles/other/Everything-About-Chat). diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md b/docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md deleted file mode 100644 index caeccd1920b1..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Expensify Chat Playbook for Conferences -description: Best practices for how to deploy Expensify Chat for your conference -redirect_from: articles/playbooks/Expensify-Chat-Playbook-for-Conferences/ ---- -# Overview -To help make setting up Expensify Chat for your event and your attendees super simple, we’ve created a guide for all of the technical setup details. - -# Who you are -As a conference organizer, you’re expected to amaze and inspire attendees. You want attendees to get to the right place on time, engage with the speakers, and create relationships with each other that last long after the conference is done. Enter Expensify Chat, a free feature that allows attendees to interact with organizers and other attendees in realtime. With Expensify Chat, you can: - -- Communicate logistics and key information -- Foster conference wide attendee networking -- Organize conversations by topic and audience -- Continue conversations long after the event itself -- Digitize attendee social interaction -- Create an inclusive environment for virtual attendees - -Sounds good? Great! In order to ensure your team, your speakers, and your attendees have the best experience possible, we’ve created a guide on how to use Expensify Chat at your event. - -*Let’s get started!* - - -# Support -Connect with your dedicated account manager in any new.expensify.com #admins room. Your account manager is excited to brainstorm the best ways to make the most out of your event and work through any questions you have about the setup steps below. - -We also have a number of [moderation tools](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) available to admins to help make sure your event is seamless, safe, and fun! - -# Step by step instructions for setting up your conference on Expensify Chat -Based on our experience running conferences atop Expensify Chat, we recommend the following simple steps: - -## Step 1: Create your event workspace in Expensify -To create your event workspace in Expensify: -1. In [new.expensify.com](https://new.expensify.com): “+” > “New workspace” -1. Name the workspace (e.g. “ExpensiCon”) - -## Step 2: Set up all the Expensify Chat rooms you want to feature at your event -**Protip**: Your Expensify account manager can complete this step with you. Chat them in #admins on new.expensify.com to coordinate! - -To create a new chat room: -1. Go to [new.expensify.com](https://new.expensify.com) -1. Go to “+” > New room -1. Name the room (e.g. “#social”) -1. Select the workspace created in step 1 -1. Select “Public” visibility -1. Repeat for each room - -For an easy-to-follow event, we recommend creating these chat rooms: - -- **#social** - This room will include all attendees, speakers, and members of your organizing team. You can use this room to discuss social events, happy hours, dinners, or encourage attendees to mingle, share photos and connect. -- **#announcements** - This room will be used as your main announcement channel, and should only be used by organizers to announce schedule updates or anything important that your attendees need to know. Everyone in your policy will be invited to this channel, but chatting in here isn’t encouraged so to keep the noise to a minimum. -- **Create an individual room for each session** - Attendees will be able to engage with the speaker/session leader and can ask questions about their content either before/during/after the session. -- **Create a room with your Expensify account manager/s** - We can use this room to coordinate using Expensify Chat before, during, and after the event. - -**Protip** Check out our [moderation tools](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) to help flag comments deemed to be spam, inconsiderate, intimidating, bullying, harassment, assault. On any comment just click the flag icon to moderate conversation. - -## Step 3: Add chat room QR codes to the applicable session slide deck -Gather QR codes: -1. Go to [new.expensify.com](https://new.expensify.com) -1. Click into a room and click the room name or avatar in the top header -1. Go into Share Code -1. Screenshot the QR code to add to your deck - -Add the QR code to every slide so that if folks forget to scan the QR code at the beginning of the presentation, they can still join the discussion. - -## Step 4: Plan out your messaging and cadence before the event begins -Expensify Chat is a great place to provide updates leading up to your event -- share news, get folks excited about speakers, and let attendees know of crucial event information like recommended attire, travel info, and more. For example, you might consider: - -**Prep your announcements:** -- Create a document containing drafts of the key messages you intend to send throughout the day. -- If your event's agenda is broken up into hourly blocks, create a separate section for each hour of the event, to make it easy to find the correct section at the right time. -- Start each day with a review of the daily agenda, such as a bullet list summarizing what's happening hour by hour. - -**Post your updates:** -- Designate a team member to post each update in #announce at the designated time. -- Each hour, send a message listing exactly what is happening next – if there are multiple sessions happening simultaneously, list out each, along with a description of the session, a reminder of where it's located, and (most importantly) a link to the chat room for that session -- Write the messages in [markdown format](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-format-text), such that they can be copy/pasted directly into Expensify Chat for sending. - - If there is some formatting issue upon posting, no problem: just edit the comment after sending, and it'll be fixed for everyone. -- We’d also recommend posting your updates on new lines so that if someone has a question about a certain item they can ask in a thread pertaining to that topic, rather than in one consolidated block. - -**Protip**: Your account manager can help you create this document, and would be happy to send each message at the appointed time for you. - -## Step 5: Share Expensify Chat How-To Resources with Speakers, Attendees, Admins -We’ve created a few helpful best practice docs for your speakers, admins, and attendees to help navigate using Expensify Chat at your event. Feel free to share the links below with them! - -- [Expensify Chat for Conference Attendees](https://help.expensify.com/articles/other/Expensify-Chat-For-Conference-Attendees) -- [Expensify Chat for Conference Speakers](https://help.expensify.com/articles/other/Expensify-Chat-For-Conference-Speakers) -- [Expensify Chat for Admins](https://help.expensify.com/articles/other/Expensify-Chat-For-Admins) - -## Step 6: Follow up with attendees after the event -Continue the connections by using Expensify Chat to keep your conference community connected. Encourage attendees to share photos, their favorite memories, funny stories, and more. - -# Conclusion -Once you have completed the above steps you are ready to host your conference on Expensify Chat! Let your account manager know any questions you have over in your [new.expensify.com](https://new.expensify.com) #admins room and start driving activity in your Expensify Chat rooms. Once you’ve reviewed this doc you should have the foundations in place, so a great next step is to start training your speakers on how to use Expensify Chat for their sessions. Coordinate with your account manager to make sure everything goes smoothly! diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-rooms-for-admins.md b/docs/articles/new-expensify/chat/Expensify-Chat-rooms-for-admins.md new file mode 100644 index 000000000000..ff341d4de68f --- /dev/null +++ b/docs/articles/new-expensify/chat/Expensify-Chat-rooms-for-admins.md @@ -0,0 +1,50 @@ +--- +title: Expensify Chat rooms for admins +description: Use the announce and admins chat rooms +--- +
+ +When a workspace is created, an #announce and #admins chat room is automatically created. + +# #announce + +All Workspace Members can use this room to share or discover important company announcements and have conversations with other members. + +By default, all Workspace Members are allowed to send messages in #announce rooms. However, Workspace Admins can update the permissions to allow only admins to post messages in the #announce room. + +## Update messaging permissions + +To allow only admins to post in an #announce room, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the #announce room chat in your inbox. +2. Click the room header. +3. Click **Settings**. +4. Click **Who can post** and select **Admins only**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the #announce room chat in your inbox. +2. Tap the room header. +3. Tap **Settings**. +4. Tap **Who can post** and select **Admins only**. +{% include end-option.html %} + +{% include end-selector.html %} + +# #admins + +Only Workspace Admins can access this room to collaborate with the other admins in the workspace. You can also use this space to: +- Chat with your dedicated Expensify Setup Specialist. +- Chat with your Account Manager (if you have a subscription with 10 or more members). +- Review changes made to your Workspace settings (includes changes made by someone on your team, your dedicated Expensify Setup Specialist, or your dedicated Account Manager). + +# FAQs + +**Someone I don’t recognize is in my #admins room for my Workspace.** + +Your #admins room also includes your dedicated Expensify Setup Specialist who will help you onboard and answer your questions. You can chat with them directly from your #admins room. If you have a subscription of 10 or more members, you can chat with your dedicated Account Manager, who is also added to your #admins room for ongoing product support. + +
diff --git a/docs/articles/new-expensify/chat/Flag-chat-messages.md b/docs/articles/new-expensify/chat/Flag-chat-messages.md new file mode 100644 index 000000000000..4955298bbd6b --- /dev/null +++ b/docs/articles/new-expensify/chat/Flag-chat-messages.md @@ -0,0 +1,33 @@ +--- +title: Flag chat messages +description: Report a message as offensive, spam, etc. +--- +
+ +Flagging a message as offensive (including unwanted behavior or offensive messages or attachments) escalates it to Expensify’s internal moderation team for review. The person who sent the message will be notified of the flag anonymously, and the moderation team will decide what further action is needed. + +Depending on the severity of the offense, messages can be hidden (with an option to reveal) or fully removed. In extreme cases, the sender of the message may be temporarily or permanently blocked from posting. + +{% include info.html %} +Messages sent in public chat rooms are automatically reviewed for offensive content by an automated system. If offensive content is found, the message is sent to Expensify’s internal moderation team for further review. +{% include end-info.html %} + +To flag a message, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat in your inbox. +2. Hover over the message and click the three dot menu icon that appears in the menu at the top right of the message. Then select **Flag as offensive**. +3. Select a category: spam, inconsiderate, intimidation, bullying, harassment, or assault. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat in your inbox. +2. Press and hold the message and select **Flag as offensive**. +3. Select a category: spam, inconsiderate, intimidation, bullying, harassment, or assault. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md deleted file mode 100644 index 096a3d1527be..000000000000 --- a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: Introducing Expensify Chat -description: Everything you need to know about Expensify Chat! -redirect_from: articles/other/Everything-About-Chat/ ---- - - - -# Overview - -For a quick snapshot of how Expensify Chat works, and New Expensify in general, check out our website! - -# What’s Expensify Chat? - -Expensify Chat is an instant messaging and payment platform. You can manage all your payments, whether for business or personal, and discuss the transactions themselves. - -With Expensify Chat, you can start a conversation about that missing receipt your employee forgot to submit or chat about splitting that electric bill with your roommates. Expensify makes sending and receiving money as easy as sending and receiving messages. Chat with anyone directly, in groups, or in rooms. - -# Getting started - -Download New Expensify from the [App Store](https://apps.apple.com/us/app/expensify-cash/id1530278510) or [Google Play](https://play.google.com/store/apps/details?id=com.expensify.chat) to use the chat function. You can also access your account at new.expensify.com from your favorite web browser. - -After downloading the app, log into your new.expensify.com account (you’ll use the same login information as your Expensify Classic account). From there, you can customize your profile and start chatting. - -## How to send messages - -1. Click **+** then **Send message** in New Expensify -2. Choose **Chat** -3. Search for any name, email or phone number -4. Select the individual to begin chatting - -## How to create a group - -1. Click **+**, then **Send message** in New Expensify -2. Search for any name, email or phone number -3. Click **Add to group** -4. Group participants are listed with a green check -5. Repeat steps 1-3 to add more participants to the group -6. Click **Create chat** to start chatting - -## How to create a room - -1. Click **+**, then **Send message** in New Expensify -2. Click **Room** -3. Enter a room name that doesn’t already exist on the intended Workspace -4. Choose the Workspace you want to associate the room with. -5. Choose the room’s visibility setting: -6. Private: Only people explicitly invited can find the room* -7. Restricted: Workspace members can find the room* -8. Public: Anyone can find the room - -*Anyone, including non-Workspace Members, can be invited to a Private or Restricted room. - -## How to invite and remove members - -You can invite people to a Group or Room by @mentioning them or from the Members pane. - -## Mentions: - -1. Type **@** and start typing the person’s name or email address -2. Choose one or more contacts -3. Input message, if desired, then send - - -## Members pane invites: - -1. Click the **Room** or **Group** header -2. Select **Members** -3. Click **Invite** -4. Find and select any contact/s you’d like to invite -5. Click **Next** -6. Write a custom invitation if you like -7. Click **Invite** - -## Members pane removals: - -1. Click the **Room** or **Group** header -2. Select **Members** -3. Find and select any contact/s you’d like to remove -4. Click **Remove** -5. Click **Remove members** - -## How to format text - -- To italicize your message, place an underscore on both sides of the text: _text_ -- To bold your message, place an asterisk on both sides of the text: *text* -- To strikethrough your message, place a tilde on both sides of the text: ~text~ -- To turn your message into code, place a backtick on both sides of the text: `text` -- To turn your text into a blockquote, add an angled bracket (>) in front of the text: - >your text -- To turn your message into a heading, place a number sign (#) in front of the text: -# Heading -- To turn your entire message into code block, place three backticks on both sides of the text: -``` -here's some text -and even more text -``` - -## Message actions - -If you mouse-over a message (or long-press on mobile), you will see the action menu. This allows you to add a reaction, start a thread, copy the link, mark it as unread, edit your own message, delete your own message, or flag it as offensive. - -**Add a reaction**: React with an emoji to the message -**Start a thread**: Start a thread by responding to the message instead of replying in the parent room -**Copy the link**: Share the message link with people who have access (i.e. Group or Room members). Note: Anyone can access messages in Public rooms -**Mark is as unread**: This will highlight the message in your left hand menu -**Edit message**: You can edit your own messages anytime. When you edit a message it will show as *edited* -**Delete message**: Deleting a message will remove it entirely for all viewers -**Flag as offensive**: Flagging a message as offensive escalates it to Expensify’s internal moderation team for review. The person who sent the message will be notified anonymously of the flag and the moderation team will decide what further action is needed - -## Workspace chat rooms - -In addition to 1:1 and group chat, members of a Workspace will have access to two additional rooms; the #announce and #admins rooms. -All Workspace members are added to the #announce room by default. The #announce room lets you share important company announcements and have conversations between Workspace members. - -All Workspace admins can access the #admins room. Use the #admins room to collaborate with the other admins on your Workspace, and chat with your dedicated Expensify Setup Specialist. If you have a subscription of 10 or more members, you're automatically assigned an Account Manager. You can ask for help and collaborate with your Account Manager in this same #admins room. Anytime someone on your team, your dedicated Setup Specialist, or your dedicated account manager makes any changes to your Workspace settings, that update is logged in the #admins room. - -# Deep Dive - -## Flagging content as offensive - -In order to maintain a safe community for our members, Expensify provides tools to report offensive content and unwanted behavior in Expensify Chat. If you see a message (or attachment/image) from another member that you’d like our moderators to review, you can flag it by clicking the flag icon in the message context menu (on desktop) or holding down on the message and selecting “Flag as offensive” (on mobile). - -![Moderation Context Menu](https://help.expensify.com/assets/images/moderation-context-menu.png){:width="100%"} - -Once the flag is selected, you will be asked to categorize the message (such as spam, bullying, and harassment). Select what you feel best represents the issue is with the content, and you’re done - the message will be sent off to our internal team for review. - -![Moderation Flagging Options](https://help.expensify.com/assets/images/moderation-flag-page.png){:width="100%"} - -Depending on the severity of the offense, messages can be hidden (with an option to reveal) or fully removed, and in extreme cases, the sender of the message can be temporarily or permanently blocked from posting. - -You will receive a whisper from Concierge any time your content has been flagged, as well as when you have successfully flagged a piece of content. - -![Moderation Reportee Whisper](https://help.expensify.com/assets/images/moderation-reportee-whisper.png){:width="100%"} -![Moderation Reporter Whisper](https://help.expensify.com/assets/images/moderation-reporter-whisper.png){:width="100%"} - -*Note: Any message sent in public chat rooms are automatically reviewed by an automated system looking for offensive content and sent to our moderators for final decisions if it is found.* - -{% include faq-begin.md %} - -## What are the #announce and #admins rooms? - -In addition to 1:1, Groups, and Workspace rooms, members of a Workspace will have access to two additional rooms; the #announce and #admins rooms. - -All domain members are added to the #announce room by default. The #announce room lets you share important company announcements and have conversations between Workspace members. - -All Workspace admins can access the #admins room. Use the #admins room to collaborate with the other admins on your Workspace, and chat with your dedicated Expensify Setup Specialist. If you have an existing subscription, you're automatically assigned an Account Manager. You can ask for help and collaborate with your Account Manager in this room. - -## Someone I don’t recognize is in my #admins room for my Workspace; who is it? - -After creating your Workspace, you’ll have a dedicated Expensify Setup Sspecialist who will help you onboard and answer your questions. You can chat with them directly in the #admins room or request a call to talk to them over the phone. Later, once you've finished onboarding, if you have a subscription of 10 or more members, a dedicated Account Manager is added to your #admins room for ongoing product support. - -## Can I force a chat to stay at the top of the chats list? - -You sure can! Click on the chat you want to keep at the top of the list, and then click the small **pin** icon. If you want to unpin a chat, just click the **pin** icon again. - -## Can I change the way my chats are displayed? - -The way your chats display in the left-hand menu is customizable. We offer two different options; Most Recent mode and _#focus_ mode. - -- Most Recent mode will display all chats by default, sort them by the most recent, and keep your pinned chats at the top of the list. -- #focus mode will display only unread and pinned chats, and will sort them alphabetically. This setting is perfect for when you need to cut distractions and focus on a crucial project. - -You can find your display mode by clicking on your Profile > Preferences > Priority Mode. -{% include faq-end.md %} diff --git a/docs/articles/new-expensify/chat/Invite-members-to-a-chat-group-or-room.md b/docs/articles/new-expensify/chat/Invite-members-to-a-chat-group-or-room.md new file mode 100644 index 000000000000..d6877f71be07 --- /dev/null +++ b/docs/articles/new-expensify/chat/Invite-members-to-a-chat-group-or-room.md @@ -0,0 +1,97 @@ +--- +title: Invite members to a chat group or room +description: Add new people to a chat group or room +--- +
+ +You can invite people to a group or room by: +- @mentioning them +- Using the Members pane of the chat +- Sharing a link or QR code + +{% include info.html %} +These options are available only for rooms and groups. You cannot add additional people to a private 1-on-1 chat between two people. +{% include end-info.html %} + +# Invite with mention + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat group or room you want to invite someone to. +2. In the message field, type @ and the person’s name or email address. Repeat this step until all desired participants are listed. +3. Enter a message, if desired. +4. Press Enter on your keyboard or click the Send icon. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat group or room you want to invite someone to. +2. In the message field, type @ and the person’s name or email address. Repeat this step until all desired participants are listed. +3. Enter a message, if desired. +4. Tap the Send icon. +{% include end-option.html %} + +{% include end-selector.html %} + +# Invite from the Members pane + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat group or room you want to invite someone to. +2. Click the room or group header. +3. Click **Members**. +4. Click **Invite member**. +5. Find and select any contact(s) you’d like to invite. +6. Click **Invite**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat group or room you want to invite someone to. +2. Tap the room or group header. +3. Tap **Members**. +4. Tap **Invite member**. +5. Find and select any contact(s) you’d like to invite. +6. Tap **Invite**. +{% include end-option.html %} + +{% include end-selector.html %} + +# Share chat link or QR code + +{% include info.html %} +If your group/room is Private, you can only share the chat link with other members of the group/room. If it is a public group/room, anyone can access the chat via the link. +{% include end-info.html %} + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat group or room you want to invite someone to. +2. Click the room or group header. +3. Click **Share code**. +4. Copy the link or share the QR code. + - **Copy link**: Click **Copy URL** and paste the link into another chat, email, Slack, etc. + - **Share QR Code**: Present your device to another user so they can scan the QR code with their device. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat group or room you want to invite someone to. +2. Tap the room or group header. +3. Tap **Share code**. +4. Copy the link or share the QR code. + - **Copy link**: Tap **Copy URL** and paste the link into another chat, email, Slack, etc. + - **Share QR Code**: Present your device to another user so they can scan the QR code with their device. +{% include end-option.html %} + +{% include end-selector.html %} + +# FAQs + +**How do I remove someone from a chat group or room?** + +Currently, members have to remove themselves from a chat. + +
+ + + diff --git a/docs/articles/new-expensify/chat/Leave-a-chat-room.md b/docs/articles/new-expensify/chat/Leave-a-chat-room.md new file mode 100644 index 000000000000..252e7e94f1ac --- /dev/null +++ b/docs/articles/new-expensify/chat/Leave-a-chat-room.md @@ -0,0 +1,23 @@ +--- +title: Leave a chat room +description: Remove a chat room from your inbox +--- +
+ +If you wish to no longer be part of a chat room, you can leave the room. This means that the chat room will no longer be visible in your inbox, and you will no longer see updates posted to the room or be notified of new messages. + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat room. +2. Click the 3 dot menu icon in the top right and select **Leave**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat room. +2. Tap the 3 dot menu icon in the top right and select **Leave**. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/chat/Reorder-chat-inbox.md b/docs/articles/new-expensify/chat/Reorder-chat-inbox.md new file mode 100644 index 000000000000..cd62f95e63e8 --- /dev/null +++ b/docs/articles/new-expensify/chat/Reorder-chat-inbox.md @@ -0,0 +1,49 @@ +--- +title: Reorder chat inbox +description: Change how your chats are displayed in your inbox +--- +
+ +You can customize the order of the chat messages in your inbox by pinning them to the top and/or changing your message priority to Most Recent or #focus: +- **Pin**: Bumps a specific chat up to the top of your inbox list. +- **Message priority**: Determines the order that messages are sorted and displayed: + - **Most Recent**: Displays all chats by default sorted by the most recent, and keep your pinned chats at the top of the list. + - **#focus**: Displays only unread and pinned chats sorted alphabetically. + +# Pin a message + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +Right-click a chat in your inbox and select **Pin**. The chat will now be pinned to the top of your inbox above all of the others. + +To unpin a chat, repeat this process to click the pin icon again to remove it. +{% include end-option.html %} + +{% include option.html value="mobile" %} +Press and hold a chat in your inbox and select **Pin**. The chat will now be pinned to the top of your inbox above all of the others. + +To unpin a chat, repeat this process to tap the pin icon again to remove it. +{% include end-option.html %} + +{% include end-selector.html %} + +# Change message priority + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click the **Preferences** tab on the left. +3. Click **Priority Mode** to select either #focus or Most recent. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap the **Preferences** tab. +3. Tap **Priority Mode** to select either #focus or Most recent. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/chat/Send-and-format-chat-messages.md b/docs/articles/new-expensify/chat/Send-and-format-chat-messages.md new file mode 100644 index 000000000000..edef142a80bf --- /dev/null +++ b/docs/articles/new-expensify/chat/Send-and-format-chat-messages.md @@ -0,0 +1,49 @@ +--- +title: Send and format chat messages +description: Send chat messages and stylize them with markdown +--- +
+ +Once you are added to a chat or create a new chat, you can send messages to other members in the chat and even format the text to include bold, italics, and more. + +{% include info.html %} +Some chat rooms may have permissions that restrict who can send messages. In this case, you won’t be able to send messages in the room if you do not have the required permission level. +{% include end-info.html %} + +To send a message, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click any chat in your inbox to open it. +2. Use the message bar at the bottom of the message to enter a message, add attachments, and add emojis. + - **To add a message**: Click the field labeled “Write something” and type a message. + - **To add an attachment**: Click the plus icon and select **Add attachment**. Then choose the attachment from your files. + - **To add an emoji**: Click the emoji icon to the right of the message field. +3. Press Enter on your keyboard or click the Send icon to send the message. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap any chat in your inbox to open it. +2. Use the message bar at the bottom of the message to enter a message, add attachments, and add emojis. + - **To add a message**: Tap the field labeled “Write something” and type a message. + - **To add an attachment**: Tap the plus icon and select **Add attachment**. Then choose the attachment from your files. + - **To add an emoji**: Tap the emoji icon to the right of the message field. +3. Tap the Send icon to send the message. +{% include end-option.html %} + +{% include end-selector.html %} + +# Format text in a chat message + +You can format the text in a chat message using markdown. + +- _Italicize_: Add an underscore _ on both sides of the text. +- **Bold**: Add two asterisks ** on both sides of the text. +- ~~Strikethrough~~: Add two tildes ~~ on both sides of the text. +- Heading: Add a number sign # in front of the text. +- > Blockquote: Add an angled bracket > in front of the text. +- `Code block for a small amount of text`: Add a backtick ` on both sides of the text. +- Code block for the entire message: Add three backticks ``` at the beginning and the end of the message. + +
diff --git a/docs/articles/new-expensify/chat/Start-a-conversation-thread.md b/docs/articles/new-expensify/chat/Start-a-conversation-thread.md new file mode 100644 index 000000000000..cb3a3aa69296 --- /dev/null +++ b/docs/articles/new-expensify/chat/Start-a-conversation-thread.md @@ -0,0 +1,33 @@ +--- +title: Start a conversation thread +description: Start a private conversation related to a different message +--- +
+ +You can respond directly to a message sent in a chat group or room to start a private 1-on-1 chat with another member about the message (instead of replying to the entire group or room). + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat in your inbox. +2. Right-click the message and select **Reply in thread**. +3. Enter and submit your reply in the new chat. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat in your inbox. +2. Press and hold the message and select **Reply in thread**. +3. Enter and submit your reply in the new chat. +{% include end-option.html %} + +{% include end-selector.html %} + +To return to the conversation where the thread originated from, you can click the link at the top of the thread. + +
+ + + + + + diff --git a/docs/articles/new-expensify/expenses/Send-an-invoice.md b/docs/articles/new-expensify/expenses/Send-an-invoice.md new file mode 100644 index 000000000000..588f0da20154 --- /dev/null +++ b/docs/articles/new-expensify/expenses/Send-an-invoice.md @@ -0,0 +1,52 @@ +--- +title: Send an invoice +description: Notify a customer that a payment is due +--- +
+ +You can send invoices directly from Expensify to notify customers that a payment is due. + +To create and send an invoice, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + icon in the bottom left menu and select **Send Invoice**. +2. Enter the amount due and click **Next**. +3. Enter the email or phone number of the person who should receive the invoice. +4. (Optional) Add additional invoice details, including a description, date, category, tag, and/or tax. +5. Click **Send**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + icon in the bottom left menu and select **Send Invoice**. +2. Enter the amount due and tap **Next**. +3. Enter the email or phone number of the person who should receive the invoice. +4. (Optional) Add additional invoice details, including a description, date, category, tag, and/or tax. +5. Tap **Send**. +{% include end-option.html %} + +{% include end-selector.html %} + +# How the customer pays an invoice + +Once an invoice is sent, the customer receives an automated email or text message to notify them of the invoice. They can use this notification to pay the invoice whenever they are ready. They will: + +1. Click the link in the email or text notification they receive from Expensify. +2. Click **Pay**. +3. Choose **Paying as an individual** or **Paying as a business**. +4. Click **Pay Elsewhere**, which will mark the invoice as Paid. + +Currently, invoices must be paid outside of Expensify. However, the ability to make payments through Expensify is coming soon. + +# FAQs + +**How do I communicate with the sender/recipient about the invoice?** + +You can communicate with the recipient in New Expensify. After sending an invoice, Expensify automatically creates an invoice room between the invoice sender and the payer to discuss anything related to the invoice. You can invite users to join the conversation, remove them from the room, and leave the room at any time. + +**Can you import and export invoices between an accounting integration?** + +Yes, you can export and import invoices between Expensify and your QuickBooks Online or Xero integration. + +
diff --git a/docs/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan.md b/docs/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan.md new file mode 100644 index 000000000000..80ee4d46b444 --- /dev/null +++ b/docs/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan.md @@ -0,0 +1,53 @@ +--- +title: Upgrade from a Free plan to Collect +description: You've been automatically upgraded to a Collect plan +--- +
+ +All customers on a Free plan have been automatically upgraded to a Collect plan! + +The Collect plan is an enhanced version of the Free Plan and unlocks several new features: +- A dedicated Setup Specialist +- Expense approvals +- Invoicing and bill pay +- Custom expense categories and tags +- Multiple active expense reports at a time +- Direct connection to Quickbooks Online +- Direct connection to Xero (coming soon!) + +**The upgrade is free until June 1st!** + +To give you time to try the upgraded plan, you won't be charged for another 30 days. Additionally, if you add a payment card by June 1st, you’ll qualify for a discount over the next year. The discount will be applied to your Expensify bill on a sliding scale for the first 12 months on the Collect plan. + +If you have questions, contact your dedicated Setup Specialist using the #admins room, or email Expensify Support at concierge@expensify.com. + +{% include faq-begin.md %} + +**Is the upgrade optional?** + +No, the upgrade is not optional. This upgrade ensures that every customer gets access to Expensify's best offerings and any new features going forward. + +**Does this mean Expensify will no longer be free for me?** + +Yes, but only if you want to continue using a workspace in Expensify. As always, you can still use Expensify for free without a workspace to track your expenses, chat with your friends, etc. + +**How does the sliding-scale discount work?** + +You’ll receive a discount on your Expensify bill that gradually decreases each month until you reach the full payment amount on June 1, 2025. For the first month, you’ll pay 1/12 of the Collect plan’s cost, and the price will gradually increase over the course of a year. + +**Let's break that down:** +- July 1: You'll receive a ~90% discount on your monthly bill +- December 1: You'll receive a 50% discount +- March 1, 2025: You'll receive a 25% discount +- May 1, 2025: You'll receive a ~10% discount +- June 1, 2025: You pay the full bill amount for the Collect plan (starting at $5 per active member) from this date forward + +The discount will be reflected on your monthly Expensify bill as a “Workspace upgrade discount” and can be combined with any other Expensify discounts—- the annual subscription discount, the Expensify Card discount, and Expensify Card cash back. + +**How do I get in touch with my Setup Specialist?** + +You can reach your Setup Specialist by opening your workspace’s #admins room in your chat inbox and sending a message. + +{% include faq-end.md %} + +
diff --git a/docs/assets/images/AdminissuedVirtualCards.png b/docs/assets/images/AdminissuedVirtualCards.png new file mode 100644 index 000000000000..88df9b2f3fec Binary files /dev/null and b/docs/assets/images/AdminissuedVirtualCards.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv index 674a39e00b61..124b0377584c 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -156,6 +156,8 @@ https://help.expensify.com/articles/expensify-classic/send-payments/Reimbursing- https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO,https://help.expensify.com/articles/expensify-classic/settings/Enable-two-factor-authentication https://help.expensify.com/articles/expensify-classic/workspaces/Budgets,https://help.expensify.com/articles/expensify-classic/workspaces/Set-budgets https://help.expensify.com/articles/expensify-classic/workspaces/Categories,https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories +https://help.expensify.com/articles/expensify-classic/expenses/Per-Diem-Expenses.html,https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses +https://help.expensify.com/articles/expensify-classic/workspaces/Budgets.html,https://help.expensify.com/articles/expensify-classic/workspaces/Set-budgets https://help.expensify.com/articles/expensify-classic/workspaces/Tags,https://help.expensify.com/articles/expensify-classic/workspaces/Create-tags https://help.expensify.com/expensify-classic/hubs/manage-employees-and-report-approvals,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows https://help.expensify.com/articles/expensify-classic/expenses/expenses/Add-an-expense,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index cda215581cff..e448251d7c58 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.66 + 1.4.68 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.66.2 + 1.4.68.3 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 43728a764228..bcbac7d3c0b0 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.66 + 1.4.68 CFBundleSignature ???? CFBundleVersion - 1.4.66.2 + 1.4.68.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 5d52107c99bf..33af14df90a5 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.66 + 1.4.68 CFBundleVersion - 1.4.66.2 + 1.4.68.3 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6ef622bba722..d17d73e5eef0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1380,10 +1380,25 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-quick-sqlite (8.0.0-beta.2): - - React - - React-callinvoker + - react-native-quick-sqlite (8.0.6): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen - React-Core + - React-debug + - React-Fabric + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-release-profiler (0.1.6): - glog - hermes-engine @@ -1816,7 +1831,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.64): + - RNLiveMarkdown (0.1.69): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1834,9 +1849,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.64) + - RNLiveMarkdown/common (= 0.1.69) - Yoga - - RNLiveMarkdown/common (0.1.64): + - RNLiveMarkdown/common (0.1.69): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2062,7 +2077,6 @@ DEPENDENCIES: - ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)" - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libevent (~> 2.1.12) @@ -2454,14 +2468,14 @@ SPEC CHECKSUMS: libvmaf: 27f523f1e63c694d14d534cd0fddd2fab0ae8711 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 lottie-ios: 3d98679b41fa6fd6aff2352b3953dbd3df8a397e - lottie-react-native: 80bda323805fa62005afff0583d2927a89108f20 + lottie-react-native: d0e530160e1a0116ab567343d843033c496d0d97 MapboxCommon: 20466d839cc43381c44df09d19f7f794b55b9a93 MapboxCoreMaps: c21f433decbb295874f0c2464e492166db813b56 MapboxMaps: c3b36646b9038706bbceb5de203bcdd0f411e9d0 MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 Onfido: 342cbecd7a4383e98dfe7f9c35e98aaece599062 - onfido-react-native-sdk: 81e930e77236a0fc3da90e6a6eb834734d8ec2f5 + onfido-react-native-sdk: 3e3b0dd70afa97410fb318d54c6a415137968ef2 Plaid: 7829e84db6d766a751c91a402702946d2977ddcb PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 @@ -2486,26 +2500,26 @@ SPEC CHECKSUMS: React-jsitracing: e8a2dafb9878dbcad02b6b2b88e66267fb427b74 React-logger: 0a57b68dd2aec7ff738195f081f0520724b35dab React-Mapbuffer: 63913773ed7f96b814a2521e13e6d010282096ad - react-native-airship: 6ab7a7974d53f92b0c46548fc198f797fdbf371f - react-native-blob-util: a3ee23cfdde79c769c138d505670055de233b07a - react-native-cameraroll: 95ce0d1a7d2d1fe55bf627ab806b64de6c3e69e9 + react-native-airship: 38e2596999242b68c933959d6145512e77937ac0 + react-native-blob-util: 1ddace5234c62e3e6e4e154d305ad07ef686599b + react-native-cameraroll: f373bebbe9f6b7c3fd2a6f97c5171cda574cf957 react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809 - react-native-geolocation: c1c21a8cda4abae6724a322458f64ac6889b8c2b + react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 - react-native-key-command: 74d18ad516037536c2f671ef0914bcce7739b2f5 + react-native-key-command: 28ccfa09520e7d7e30739480dea4df003493bfe8 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d - react-native-netinfo: 6479e7e2198f936e5abc14a3ec4d469ccbaf81e2 - react-native-pager-view: 9ac6bc0fb3fa31c6d403b253ee361e62ff7ccf7f - react-native-pdf: cd256a00b9d65cb1008dcca2792d7bfb8874838d - react-native-performance: 1aa5960d005159f4ab20be15b44714b53b44e075 - react-native-plaid-link-sdk: 93870f8cd1de8e0acca5cb5020188bdc94e15db6 - react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 - react-native-release-profiler: 42fc8e09b4f6f9b7d14cc5a15c72165e871c0918 + react-native-netinfo: 02d31de0e08ab043d48f2a1a8baade109d7b6ca5 + react-native-pager-view: ccd4bbf9fc7effaf8f91f8dae43389844d9ef9fa + react-native-pdf: 762369633665ec02ac227aefe2f4558b92475c23 + react-native-performance: fb21ff0c9bd7a10789c69d948f25b0067d29f7a9 + react-native-plaid-link-sdk: 2a91ef7e257ae16d180a1ca14ba3041ae0836fbf + react-native-quick-sqlite: e3ab3e0a29d8c705f47a60aaa6ceaa42eb6a9ec1 + react-native-release-profiler: 14ccdc0eeb03bedf625cf68d53d80275a81b19dd react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c - react-native-safe-area-context: e8bdd57d9f8d34cc336f0ee6acb30712a8454446 + react-native-safe-area-context: 9d79895b60b8be151fdf6faef9d2d0591eeecc63 react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 - react-native-webview: a5f5f316527235f869992aaaf05050776198806d + react-native-webview: f8ab7a37905b2366a3e849ce5992b9724f6a528d React-nativeconfig: d7af5bae6da70fa15ce44f045621cf99ed24087c React-NativeModulesApple: 0123905d5699853ac68519607555a9a4f5c7b3ac React-perflogger: 8a1e1af5733004bdd91258dcefbde21e0d1faccd @@ -2530,35 +2544,35 @@ SPEC CHECKSUMS: React-utils: 6e5ad394416482ae21831050928ae27348f83487 ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522 RNAppleAuthentication: 0571c08da8c327ae2afc0261b48b4a515b0286a6 - RNCClipboard: c73bbc2e9012120161f1012578418827983bfd0c - RNCPicker: c77efa39690952647b83d8085520bf50ebf94ecb - RNDeviceInfo: cbf78fdb515ae73e641ee7c6b474f77a0299e7e6 + RNCClipboard: 081418ae3b391b1012c3f41d045e5e39f1beed71 + RNCPicker: a37026a67de0cf1a33ffe8722783527e3b18ea9f + RNDeviceInfo: 449272e9faf2afe94a3fe2896d169e92277fffa8 RNDevMenu: 72807568fe4188bd4c40ce32675d82434b43c45d RNFBAnalytics: f76bfa164ac235b00505deb9fc1776634056898c RNFBApp: 729c0666395b1953198dc4a1ec6deb8fbe1c302e RNFBCrashlytics: 2061ca863e8e2fa1aae9b12477d7dfa8e88ca0f9 RNFBPerf: 389914cda4000fe0d996a752532a591132cbf3f9 - RNFlashList: 5b0e8311e4cf1ad91e410fd7c8526a89fb5826d1 + RNFlashList: 76c2fab003330924ab1a140d13aadf3834dc32e0 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: 1190c218cdaaf029ee1437076a3fbbc3297d89fb + RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: ddc8b2d827febd397c88137ffc7a6e102d511b8b + RNLiveMarkdown: bfabd5938e5af5afc1e60e4e34286b17f8308184 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 - rnmapbox-maps: 3e273e0e867a079ec33df9ee33bb0482434b897d - RNPermissions: 8990fc2c10da3640938e6db1647cb6416095b729 + rnmapbox-maps: 51aee278cc2af8af9298f91a2aad7210739785b4 + RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37 - RNReanimated: 605409e0d0ced6f2e194ae585fedc2f8a1935bf2 - RNScreens: 65a936f4e227b91e4a8e2a7d4c4607355bfefda0 + RNReanimated: 51db0fff543694d931bd3b7cab1a3b36bd86c738 + RNScreens: 9ec969a95987a6caae170ef09313138abf3331e1 RNShare: 2a4cdfc0626ad56b0ef583d424f2038f772afe58 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 - RNSVG: db32cfcad0a221fd175e0882eff7bcba7690380a + RNSVG: 18f1381e046be2f1c30b4724db8d0c966238089f SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 SDWebImageAVIFCoder: 8348fef6d0ec69e129c66c9fe4d74fbfbf366112 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: af09429398d99d524cae2fe00f6f0f6e491ed102 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 - VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf + VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d diff --git a/package-lock.json b/package-lock.json index 9c645b6cbe94..b943b0b81952 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "1.4.66-2", + "version": "1.4.68-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.66-2", + "version": "1.4.68-3", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.64", + "@expensify/react-native-live-markdown": "0.1.69", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -56,7 +56,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -78,7 +78,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.6", + "react-fast-pdf": "^1.0.12", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -108,7 +108,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#da50d2c5c54e268499047f9cc98b8df4196c1ddf", "react-native-plaid-link-sdk": "11.5.0", "react-native-qrcode-svg": "^6.2.0", - "react-native-quick-sqlite": "^8.0.0-beta.2", + "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", "react-native-reanimated": "^3.7.2", "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", @@ -241,8 +241,8 @@ "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", - "webpack-cli": "^4.10.0", - "webpack-dev-server": "^4.9.3", + "webpack-cli": "^5.0.4", + "webpack-dev-server": "^5.0.4", "webpack-merge": "^5.8.0", "yaml": "^2.2.1" }, @@ -3568,9 +3568,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.64", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.64.tgz", - "integrity": "sha512-X6NXYH420wC+BFNOuzJflpegwSKTiuzLvbDeehCpxrtS059Eyb2FbwkzrAVH7TGwDeghFgaQfY9rVkSCGUAbsw==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.69.tgz", + "integrity": "sha512-ZJG6f06lHrNb0s/92JyyvsSDGGZLdU/a/YXir2A5UFCiERVWkgJxcugsYbEMemh2HsWD6GXvhq1Sngj2H620nw==", "engines": { "node": ">= 18.0.0" }, @@ -7092,9 +7092,10 @@ "license": "MIT" }, "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "dev": true, - "license": "MIT" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true }, "node_modules/@lwc/eslint-plugin-lwc": { "version": "1.7.2", @@ -12174,9 +12175,10 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.10", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -12213,9 +12215,10 @@ } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, - "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" @@ -12283,22 +12286,25 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.13", - "license": "MIT", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.30", - "license": "MIT", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", + "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", "dependencies": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" } }, "node_modules/@types/fs-extra": { @@ -12358,6 +12364,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, "node_modules/@types/http-proxy": { "version": "1.17.9", "dev": true, @@ -12469,8 +12480,9 @@ "integrity": "sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==" }, "node_modules/@types/mime": { - "version": "3.0.1", - "license": "MIT" + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/minimatch": { "version": "3.0.5", @@ -12489,6 +12501,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -12615,9 +12636,10 @@ } }, "node_modules/@types/retry": { - "version": "0.12.0", - "dev": true, - "license": "MIT" + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true }, "node_modules/@types/scheduler": { "version": "0.16.2", @@ -12627,20 +12649,32 @@ "version": "7.5.4", "license": "MIT" }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/serve-index": { - "version": "1.9.1", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, - "license": "MIT", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.0", - "license": "MIT", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dependencies": { - "@types/mime": "*", - "@types/node": "*" + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" } }, "node_modules/@types/setimmediate": { @@ -12649,9 +12683,10 @@ "license": "MIT" }, "node_modules/@types/sockjs": { - "version": "0.3.33", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -12705,9 +12740,10 @@ } }, "node_modules/@types/ws": { - "version": "8.5.3", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -13549,31 +13585,42 @@ "license": "MIT" }, "node_modules/@webpack-cli/configtest": { - "version": "1.2.0", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", "dev": true, - "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, "node_modules/@webpack-cli/info": { - "version": "1.5.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", "dev": true, - "license": "MIT", - "dependencies": { - "envinfo": "^7.7.3" + "engines": { + "node": ">=14.15.0" }, "peerDependencies": { - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, "node_modules/@webpack-cli/serve": { - "version": "1.7.0", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", "dev": true, - "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, "peerDependencies": { - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" }, "peerDependenciesMeta": { "webpack-dev-server": { @@ -15531,21 +15578,15 @@ "license": "MIT" }, "node_modules/bonjour-service": { - "version": "1.0.13", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, - "license": "MIT", "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, - "node_modules/bonjour-service/node_modules/array-flatten": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, "node_modules/boolbase": { "version": "1.0.0", "license": "ISC" @@ -15961,6 +16002,21 @@ "version": "1.0.3", "license": "MIT" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.0.0", "license": "MIT", @@ -17874,6 +17930,22 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-browser-id": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", @@ -17889,6 +17961,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/default-browser/node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "dev": true, @@ -18305,15 +18389,11 @@ "license": "MIT", "optional": true }, - "node_modules/dns-equal": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/dns-packet": { - "version": "5.4.0", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, - "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -20202,8 +20282,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", - "integrity": "sha512-zz0/y0apISP1orxXEQOgn+Uod45O4wVypwwtaqcDPV4dH1tC3i4L98NoLSZvLn7Y17EcceSkfN6QCEsscgFTDQ==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", + "integrity": "sha512-9BHjM3kZs7/dil0oykEQFkEhXjVD5liTttmO7ZYtPZkl4j6g97mubY2p9lYpWwpkWckUfvU7nGuZQjahw9xSFA==", "license": "MIT", "dependencies": { "classnames": "2.5.0", @@ -23050,11 +23130,12 @@ } }, "node_modules/interpret": { - "version": "2.2.0", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/invariant": { @@ -23384,6 +23465,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -23453,6 +23567,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "license": "MIT", @@ -26748,6 +26874,16 @@ "language-subtag-registry": "~0.3.2" } }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, "node_modules/lazy-cache": { "version": "1.0.4", "license": "MIT", @@ -28401,8 +28537,9 @@ }, "node_modules/multicast-dns": { "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, - "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" @@ -29477,15 +29614,20 @@ } }, "node_modules/p-retry": { - "version": "4.6.2", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", "dev": true, - "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", "retry": "^0.13.1" }, "engines": { - "node": ">=8" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { @@ -30898,9 +31040,9 @@ } }, "node_modules/react-fast-pdf": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.6.tgz", - "integrity": "sha512-CdAnBSZaLCGLSEuiqWLzzXhV9Wvdf1VRixaXCrb3NFrXyeltahF7PY+u7eU6ynrWZGmNI6g0cMLPv0DQhJEeew==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.12.tgz", + "integrity": "sha512-RSIYTwQVKWFqZKtmtzd4JU/FnsqdGPBtHu/N6xl7TsauAFnEouUJNjmC7Rg/pd010OX1UvyraQKdBIZ5Pf2q0A==", "dependencies": { "react-pdf": "^7.7.0", "react-window": "^1.8.10" @@ -31432,7 +31574,9 @@ } }, "node_modules/react-native-quick-sqlite": { - "version": "8.0.0-beta.2", + "version": "8.0.6", + "resolved": "git+ssh://git@github.com/margelo/react-native-quick-sqlite.git#abc91857d4b3efb2020ec43abd2a508563b64316", + "integrity": "sha512-/tBM6Oh8ye3d+hIhURRA9hlBausKqQmscgyt4ZcKluPjBti0bgLb0cyL8Gyd0cbCakaVgym25VyGjaeicV/01A==", "license": "MIT", "peerDependencies": { "react": "*", @@ -32653,14 +32797,15 @@ } }, "node_modules/rechoir": { - "version": "0.7.1", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, - "license": "MIT", "dependencies": { - "resolve": "^1.9.0" + "resolve": "^1.20.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/redent": { @@ -33050,8 +33195,9 @@ }, "node_modules/retry": { "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } @@ -33116,6 +33262,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-node": { "version": "1.0.0", "dev": true, @@ -33298,10 +33456,12 @@ "license": "MIT" }, "node_modules/selfsigned": { - "version": "2.0.1", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, - "license": "MIT", "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -35310,8 +35470,9 @@ }, "node_modules/thunky": { "version": "1.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true }, "node_modules/time-analytics-webpack-plugin": { "version": "0.1.17", @@ -36933,43 +37094,42 @@ } }, "node_modules/webpack-cli": { - "version": "4.10.0", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.2.0", - "@webpack-cli/info": "^1.5.0", - "@webpack-cli/serve": "^1.7.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", "colorette": "^2.0.14", - "commander": "^7.0.0", + "commander": "^10.0.1", "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", "webpack-merge": "^5.7.3" }, "bin": { "webpack-cli": "bin/cli.js" }, "engines": { - "node": ">=10.13.0" + "node": ">=14.15.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x" + "webpack": "5.x.x" }, "peerDependenciesMeta": { "@webpack-cli/generators": { "optional": true }, - "@webpack-cli/migrate": { - "optional": true - }, "webpack-bundle-analyzer": { "optional": true }, @@ -36984,11 +37144,12 @@ "license": "MIT" }, "node_modules/webpack-cli/node_modules/commander": { - "version": "7.2.0", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=14" } }, "node_modules/webpack-dev-middleware": { @@ -37069,54 +37230,59 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.10.0", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", "dev": true, - "license": "MIT", "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", "default-gateway": "^6.0.3", "express": "^4.17.3", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", + "html-entities": "^2.4.0", "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.0.1", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.4.2" + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { + "webpack": { + "optional": true + }, "webpack-cli": { "optional": true } @@ -37124,8 +37290,9 @@ }, "node_modules/webpack-dev-server/node_modules/ajv-keywords": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -37133,10 +37300,54 @@ "ajv": "^8.8.2" } }, + "node_modules/webpack-dev-server/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/webpack-dev-server/node_modules/colorette": { "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.1.0", @@ -37146,21 +37357,86 @@ "node": ">= 10" } }, - "node_modules/webpack-dev-server/node_modules/memfs": { - "version": "3.5.3", + "node_modules/webpack-dev-server/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, - "license": "Unlicense", "dependencies": { - "fs-monkey": "^1.0.4" + "is-inside-container": "^1.0.0" }, "engines": { - "node": ">= 4.0.0" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/webpack-dev-server/node_modules/schema-utils": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -37176,25 +37452,32 @@ } }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.3", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.2.1.tgz", + "integrity": "sha512-hRLz+jPQXo999Nx9fXVdKlg/aehsw1ajA9skAneGmT03xwmyuhvF93p6HUKKbWhXdcERtGTzUCtIQr+2IQegrA==", "dev": true, - "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.3", + "memfs": "^4.6.0", "mime-types": "^2.1.31", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } } }, "node_modules/webpack-hot-middleware": { diff --git a/package.json b/package.json index 2860b3c1d3a9..6c32f7de5ab4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.66-2", + "version": "1.4.68-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.", @@ -65,7 +65,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.64", + "@expensify/react-native-live-markdown": "0.1.69", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -108,7 +108,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -130,7 +130,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.6", + "react-fast-pdf": "^1.0.12", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -160,7 +160,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#da50d2c5c54e268499047f9cc98b8df4196c1ddf", "react-native-plaid-link-sdk": "11.5.0", "react-native-qrcode-svg": "^6.2.0", - "react-native-quick-sqlite": "^8.0.0-beta.2", + "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", "react-native-reanimated": "^3.7.2", "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", @@ -293,8 +293,8 @@ "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", - "webpack-cli": "^4.10.0", - "webpack-dev-server": "^4.9.3", + "webpack-cli": "^5.0.4", + "webpack-dev-server": "^5.0.4", "webpack-merge": "^5.8.0", "yaml": "^2.2.1" }, diff --git a/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch b/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch new file mode 100644 index 000000000000..dc45a6758d5c --- /dev/null +++ b/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch @@ -0,0 +1,33 @@ +diff --git a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +index e338d90..70a59bf 100644 +--- a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js ++++ b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +@@ -1219,7 +1219,7 @@ class VirtualizedList extends StateSafePureComponent { + zoomScale: 1, + }; + _scrollRef: ?React.ElementRef = null; +- _sentStartForContentLength = 0; ++ _sentStartForFirstVisibleItemKey: ?string = null; + _sentEndForContentLength = 0; + _updateCellsToRenderBatcher: Batchinator; + _viewabilityTuples: Array = []; +@@ -1550,16 +1550,16 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && +- this._listMetrics.getContentLength() !== this._sentStartForContentLength ++ this.state.firstVisibleItemKey !== this._sentStartForFirstVisibleItemKey + ) { +- this._sentStartForContentLength = this._listMetrics.getContentLength(); ++ this._sentStartForFirstVisibleItemKey = this.state.firstVisibleItemKey; + onStartReached({distanceFromStart}); + } + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + if (!isWithinStartThreshold) { +- this._sentStartForContentLength = 0; ++ this._sentStartForFirstVisibleItemKey = null; + } + if (!isWithinEndThreshold) { + this._sentEndForContentLength = 0; diff --git a/patches/@rnmapbox+maps+10.1.11.patch b/patches/@rnmapbox+maps+10.1.11.patch index 9f2df5f4ee6e..5c5b8f0b69bb 100644 --- a/patches/@rnmapbox+maps+10.1.11.patch +++ b/patches/@rnmapbox+maps+10.1.11.patch @@ -11,3 +11,117 @@ index dbd6d0b..1d043f2 100644 val map = mapView.getMapboxMap() it.setDuration(0) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m +index 1808393..ec00542 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + + @interface RCT_EXTERN_MODULE(RNMBXOfflineModule, RCTEventEmitter) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m +index 550f67b..76da02d 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + + @interface RCT_EXTERN_MODULE(RNMBXOfflineModuleLegacy, RCTEventEmitter) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m +index a98e102..e43be8f 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + + @interface RCT_EXTERN_MODULE(RNMBXTileStoreModule, NSObject) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m +index 62205d5..1db2ac4 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + #import + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m +index e23b10c..6a023fa 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + #import + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m +index 8b89774..9f85c35 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + + @class RNMBXLocation; +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m +index d7c05de..f680b86 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + + @interface RCT_EXTERN_MODULE(RNMBXLogging, NSObject) + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m +index 72f9928..f4f5fe2 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + #import + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m +index c0ab14d..6177811 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + #import + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m +index 3b0af79..e00b508 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + + @interface RCT_EXTERN_MODULE(RNMBXModule, NSObject) + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m +index 6fa19e5..54d0ff9 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import + #import + #import + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.mm +similarity index 100% +rename from node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.m +rename to node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.mm diff --git a/patches/react-native+0.73.4+015+fixIOSWebViewCrash.patch b/patches/react-native+0.73.4+015+fixIOSWebViewCrash.patch new file mode 100644 index 000000000000..7c4244f3a811 --- /dev/null +++ b/patches/react-native+0.73.4+015+fixIOSWebViewCrash.patch @@ -0,0 +1,24 @@ +diff --git a/node_modules/react-native/scripts/cocoapods/new_architecture.rb b/node_modules/react-native/scripts/cocoapods/new_architecture.rb +index ba75b019a9b9b2..c9999beb82b7ea 100644 +--- a/node_modules/react-native/scripts/cocoapods/new_architecture.rb ++++ b/node_modules/react-native/scripts/cocoapods/new_architecture.rb +@@ -105,6 +105,10 @@ def self.install_modules_dependencies(spec, new_arch_enabled, folly_version) + current_headers = current_config["HEADER_SEARCH_PATHS"] != nil ? current_config["HEADER_SEARCH_PATHS"] : "" + current_cpp_flags = current_config["OTHER_CPLUSPLUSFLAGS"] != nil ? current_config["OTHER_CPLUSPLUSFLAGS"] : "" + ++ flags_to_add = new_arch_enabled ? ++ "#{@@folly_compiler_flags} -DRCT_NEW_ARCH_ENABLED=1" : ++ "#{@@folly_compiler_flags}" ++ + header_search_paths = ["\"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/Headers/Private/Yoga\""] + if ENV['USE_FRAMEWORKS'] + header_search_paths << "\"$(PODS_ROOT)/DoubleConversion\"" +@@ -124,7 +128,7 @@ def self.install_modules_dependencies(spec, new_arch_enabled, folly_version) + } + end + header_search_paths_string = header_search_paths.join(" ") +- spec.compiler_flags = compiler_flags.empty? ? @@folly_compiler_flags : "#{compiler_flags} #{@@folly_compiler_flags}" ++ spec.compiler_flags = compiler_flags.empty? ? "$(inherited) #{flags_to_add}" : "$(inherited) #{compiler_flags} #{flags_to_add}" + current_config["HEADER_SEARCH_PATHS"] = current_headers.empty? ? + header_search_paths_string : + "#{current_headers} #{header_search_paths_string}" diff --git a/patches/react-native-quick-sqlite+8.0.0-beta.2.patch b/patches/react-native-quick-sqlite+8.0.0-beta.2.patch deleted file mode 100644 index b5810c903873..000000000000 --- a/patches/react-native-quick-sqlite+8.0.0-beta.2.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/react-native-quick-sqlite/android/build.gradle b/node_modules/react-native-quick-sqlite/android/build.gradle -index 323d34e..c2d0c44 100644 ---- a/node_modules/react-native-quick-sqlite/android/build.gradle -+++ b/node_modules/react-native-quick-sqlite/android/build.gradle -@@ -90,7 +90,6 @@ android { - externalNativeBuild { - cmake { - cppFlags "-O2", "-fexceptions", "-frtti", "-std=c++1y", "-DONANDROID" -- abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' - arguments '-DANDROID_STL=c++_shared', - "-DREACT_NATIVE_DIR=${REACT_NATIVE_DIR}", - "-DSQLITE_FLAGS='${SQLITE_FLAGS ? SQLITE_FLAGS : ''}'" diff --git a/patches/react-native-web+0.19.9+007+osr-improvement.patch b/patches/react-native-web+0.19.9+007+osr-improvement.patch new file mode 100644 index 000000000000..074cac3d0e6f --- /dev/null +++ b/patches/react-native-web+0.19.9+007+osr-improvement.patch @@ -0,0 +1,70 @@ +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index b05da08..80aea85 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -332,7 +332,7 @@ class VirtualizedList extends StateSafePureComponent { + zoomScale: 1 + }; + this._scrollRef = null; +- this._sentStartForContentLength = 0; ++ this._sentStartForFirstVisibleItemKey = null; + this._sentEndForContentLength = 0; + this._totalCellLength = 0; + this._totalCellsMeasured = 0; +@@ -1397,8 +1397,8 @@ class VirtualizedList extends StateSafePureComponent { + // Next check if the user just scrolled within the start threshold + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed +- else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this.state.firstVisibleItemKey !== this._sentStartForFirstVisibleItemKey) { ++ this._sentStartForFirstVisibleItemKey = this.state.firstVisibleItemKey; + onStartReached({ + distanceFromStart + }); +@@ -1407,7 +1407,7 @@ class VirtualizedList extends StateSafePureComponent { + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + else { +- this._sentStartForContentLength = isWithinStartThreshold ? this._sentStartForContentLength : 0; ++ this._sentStartForFirstVisibleItemKey = isWithinStartThreshold ? this._sentStartForFirstVisibleItemKey : null; + this._sentEndForContentLength = isWithinEndThreshold ? this._sentEndForContentLength : 0; + } + } +diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +index 459f017..799a6ee 100644 +--- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +@@ -1325,7 +1325,7 @@ class VirtualizedList extends StateSafePureComponent { + zoomScale: 1, + }; + _scrollRef: ?React.ElementRef = null; +- _sentStartForContentLength = 0; ++ _sentStartForFirstVisibleItemKey: ?string = null; + _sentEndForContentLength = 0; + _totalCellLength = 0; + _totalCellsMeasured = 0; +@@ -1675,18 +1675,18 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && +- this._scrollMetrics.contentLength !== this._sentStartForContentLength ++ this.state.firstVisibleItemKey !== this._sentStartForFirstVisibleItemKey + ) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ this._sentStartForFirstVisibleItemKey = this.state.firstVisibleItemKey; + onStartReached({distanceFromStart}); + } + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + else { +- this._sentStartForContentLength = isWithinStartThreshold +- ? this._sentStartForContentLength +- : 0; ++ this._sentStartForFirstVisibleItemKey = isWithinStartThreshold ++ ? this._sentStartForFirstVisibleItemKey ++ : null; + this._sentEndForContentLength = isWithinEndThreshold + ? this._sentEndForContentLength + : 0; diff --git a/src/CONST.ts b/src/CONST.ts index 8ad9eda06501..e33ba5a1b7b8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4,8 +4,10 @@ import dateSubtract from 'date-fns/sub'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; import type {ValueOf} from 'type-fest'; +import BankAccount from './libs/models/BankAccount'; import * as Url from './libs/Url'; import SCREENS from './SCREENS'; +import type PlaidBankAccount from './types/onyx/PlaidBankAccount'; import type {Unit} from './types/onyx/Policy'; type RateAndUnit = { @@ -53,13 +55,14 @@ const chatTypes = { POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', SELF_DM: 'selfDM', + INVOICE: 'invoice', + SYSTEM: 'system', } as const; // Explicit type annotation is required const cardActiveStates: number[] = [2, 3, 4, 7]; const onboardingChoices = { - TRACK: 'newDotTrack', EMPLOYER: 'newDotEmployer', MANAGE_TEAM: 'newDotManageTeam', PERSONAL_SPEND: 'newDotPersonalSpend', @@ -631,7 +634,6 @@ const CONST = { MEMBER: 'member', }, MAX_COUNT_BEFORE_FOCUS_UPDATE: 30, - MAXIMUM_PARTICIPANTS: 8, SPLIT_REPORTID: '-2', ACTIONS: { LIMIT: 50, @@ -798,6 +800,7 @@ const CONST = { EXPENSE: 'expense', IOU: 'iou', TASK: 'task', + INVOICE: 'invoice', }, CHAT_TYPE: chatTypes, WORKSPACE_CHAT_ROOMS: { @@ -841,6 +844,16 @@ const CONST = { OWNER_EMAIL_FAKE: '__FAKE__', OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', + PERMISSIONS: { + READ: 'read', + WRITE: 'write', + SHARE: 'share', + OWN: 'own', + }, + INVOICE_RECEIVER_TYPE: { + INDIVIDUAL: 'individual', + BUSINESS: 'policy', + }, }, NEXT_STEP: { FINISHED: 'Finished!', @@ -849,9 +862,8 @@ const CONST = { MAX_LINES: 16, MAX_LINES_SMALL_SCREEN: 6, MAX_LINES_FULL: -1, - - // The minimum number of typed lines needed to enable the full screen composer - FULL_COMPOSER_MIN_LINES: 3, + // The minimum height needed to enable the full screen composer + FULL_COMPOSER_MIN_HEIGHT: 60, }, MODAL: { MODAL_TYPE: { @@ -1007,6 +1019,11 @@ const CONST = { PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, MAX_REQUEST_RETRIES: 10, + NETWORK_STATUS: { + ONLINE: 'online', + OFFLINE: 'offline', + UNKNOWN: 'unknown', + }, }, WEEK_STARTS_ON: 1, // Monday DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, @@ -1346,8 +1363,9 @@ const CONST = { PERSONAL_INFO: { LEGAL_NAME: 0, DATE_OF_BIRTH: 1, - SSN: 2, - ADDRESS: 3, + ADDRESS: 2, + PHONE_NUMBER: 3, + SSN: 4, }, }, TIER_NAME: { @@ -1372,6 +1390,13 @@ const CONST = { ERROR: 'ERROR', EXIT: 'EXIT', }, + DEFAULT_DATA: { + bankName: '', + plaidAccessToken: '', + bankAccounts: [] as PlaidBankAccount[], + isLoading: false, + errors: {}, + }, }, ONFIDO: { @@ -1450,6 +1475,7 @@ const CONST = { PAY: 'pay', SPLIT: 'split', REQUEST: 'request', + INVOICE: 'invoice', SUBMIT: 'submit', TRACK: 'track', }, @@ -1668,6 +1694,7 @@ const CONST = { NAME: { // Here we will add other connections names when we add support for them QBO: 'quickbooksOnline', + XERO: 'xero', }, SYNC_STAGE_NAME: { STARTING_IMPORT: 'startingImport', @@ -1681,9 +1708,28 @@ const CONST = { QBO_SYNC_PAYMENTS: 'quickbooksOnlineSyncBillPayments', QBO_IMPORT_TAX_CODES: 'quickbooksOnlineSyncTaxCodes', QBO_CHECK_CONNECTION: 'quickbooksOnlineCheckConnection', + QBO_SYNC_TITLE: 'quickbooksOnlineSyncTitle', + QBO_SYNC_LOAD_DATA: 'quickbooksOnlineSyncLoadData', + QBO_SYNC_APPLY_CATEGORIES: 'quickbooksOnlineSyncApplyCategories', + QBO_SYNC_APPLY_CUSTOMERS: 'quickbooksOnlineSyncApplyCustomers', + QBO_SYNC_APPLY_PEOPLE: 'quickbooksOnlineSyncApplyEmployees', + QBO_SYNC_APPLY_CLASSES_LOCATIONS: 'quickbooksOnlineSyncApplyClassesLocations', JOB_DONE: 'jobDone', + XERO_SYNC_STEP: 'xeroSyncStep', + XERO_SYNC_XERO_REIMBURSED_REPORTS: 'xeroSyncXeroReimbursedReports', + XERO_SYNC_EXPENSIFY_REIMBURSED_REPORTS: 'xeroSyncExpensifyReimbursedReports', + XERO_SYNC_IMPORT_CHART_OF_ACCOUNTS: 'xeroSyncImportChartOfAccounts', + XERO_SYNC_IMPORT_CATEGORIES: 'xeroSyncImportCategories', + XERO_SYNC_IMPORT_TRACKING_CATEGORIES: 'xeroSyncImportTrackingCategories', + XERO_SYNC_IMPORT_CUSTOMERS: 'xeroSyncImportCustomers', + XERO_SYNC_IMPORT_BANK_ACCOUNTS: 'xeroSyncImportBankAccounts', + XERO_SYNC_IMPORT_TAX_RATES: 'xeroSyncImportTaxRates', }, }, + ACCESS_VARIANTS: { + PAID: 'paid', + ADMIN: 'admin', + }, }, CUSTOM_UNITS: { @@ -3352,10 +3398,11 @@ const CONST = { }, TAB_SEARCH: { ALL: 'all', - SHARED: 'shared', - DRAFTS: 'drafts', - WAITING_ON_YOU: 'waitingOnYou', - FINISHED: 'finished', + // @TODO: Uncomment when the queries below are implemented + // SHARED: 'shared', + // DRAFTS: 'drafts', + // WAITING_ON_YOU: 'waitingOnYou', + // FINISHED: 'finished', }, STATUS_TEXT_MAX_LENGTH: 100, @@ -3589,7 +3636,6 @@ const CONST = { }, INTRO_CHOICES: { - TRACK: 'newDotTrack', SUBMIT: 'newDotSubmit', MANAGE_TEAM: 'newDotManageTeam', CHAT_SPLIT: 'newDotSplitChat', @@ -3616,19 +3662,6 @@ const CONST = { ONBOARDING_CHOICES: {...onboardingChoices}, ONBOARDING_CONCIERGE: { - [onboardingChoices.TRACK]: - "# Let's start tracking your expenses!\n" + - '\n' + - "To track your expenses, create a workspace to keep everything in one place. Here's how:\n" + - '1. From the home screen, click the green + button > *New Workspace*\n' + - '2. Give your workspace a name (e.g. "My business expenses").\n' + - '\n' + - 'Then, add expenses to your workspace:\n' + - '1. Find your workspace using the search field.\n' + - '2. Click the gray + button next to the message field.\n' + - '3. Click Request money, then add your expense type.\n' + - '\n' + - "We'll store all expenses in your new workspace for easy access. Let me know if you have any questions!", [onboardingChoices.EMPLOYER]: '# Expensify is the fastest way to get paid back!\n' + '\n' + @@ -3669,48 +3702,6 @@ const CONST = { }, ONBOARDING_MESSAGES: { - [onboardingChoices.TRACK]: { - message: 'Here are some essential tasks to keep your business spend in shape for tax season.', - video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-track-business.mp4`, - thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-track-business.jpg`, - duration: 55, - width: 1280, - height: 960, - }, - tasks: [ - { - type: 'createWorkspace', - autoCompleted: true, - title: 'Create a workspace', - description: - 'Create a workspace to track expenses, scan receipts, chat, and more.\n' + - '\n' + - 'Here’s how to create a workspace:\n' + - '\n' + - '1. Click your profile picture.\n' + - '2. Click Workspaces > New workspace.\n' + - '\n' + - 'Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.', - }, - { - type: 'trackExpense', - autoCompleted: false, - title: 'Track an expense', - description: - 'Track an expense in any currency, in just a few clicks.\n' + - '\n' + - 'Here’s how to track an expense:\n' + - '\n' + - '1. Click the green + button.\n' + - '2. Choose Track expense.\n' + - '3. Enter an amount or scan a receipt.\n' + - '4. Click Track.\n' + - '\n' + - 'And you’re done! Yep, it’s that easy.', - }, - ], - }, [onboardingChoices.EMPLOYER]: { message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', video: { @@ -3954,31 +3945,43 @@ const CONST = { DEBUG: 'DEBUG', }, }, - REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX: { - BANK_ACCOUNT: { - ACCOUNT_NUMBERS: 0, - }, - PERSONAL_INFO: { - LEGAL_NAME: 0, - DATE_OF_BIRTH: 1, - SSN: 2, - ADDRESS: 3, - }, - BUSINESS_INFO: { - BUSINESS_NAME: 0, - TAX_ID_NUMBER: 1, - COMPANY_WEBSITE: 2, - PHONE_NUMBER: 3, - COMPANY_ADDRESS: 4, - COMPANY_TYPE: 5, - INCORPORATION_DATE: 6, - INCORPORATION_STATE: 7, + REIMBURSEMENT_ACCOUNT: { + DEFAULT_DATA: { + achData: { + state: BankAccount.STATE.SETUP, + }, + isLoading: false, + errorFields: {}, + errors: {}, + maxAttemptsReached: false, + shouldShowResetModal: false, }, - UBO: { - LEGAL_NAME: 0, - DATE_OF_BIRTH: 1, - SSN: 2, - ADDRESS: 3, + SUBSTEP_INDEX: { + BANK_ACCOUNT: { + ACCOUNT_NUMBERS: 0, + }, + PERSONAL_INFO: { + LEGAL_NAME: 0, + DATE_OF_BIRTH: 1, + SSN: 2, + ADDRESS: 3, + }, + BUSINESS_INFO: { + BUSINESS_NAME: 0, + TAX_ID_NUMBER: 1, + COMPANY_WEBSITE: 2, + PHONE_NUMBER: 3, + COMPANY_ADDRESS: 4, + COMPANY_TYPE: 5, + INCORPORATION_DATE: 6, + INCORPORATION_STATE: 7, + }, + UBO: { + LEGAL_NAME: 0, + DATE_OF_BIRTH: 1, + SSN: 2, + ADDRESS: 3, + }, }, }, CURRENCY_TO_DEFAULT_MILEAGE_RATE: JSON.parse(`{ @@ -4649,9 +4652,9 @@ const CONST = { }, QUICKBOOKS_EXPORT_DATE: { - LAST_EXPENSE: 'lastExpense', - EXPORTED_DATE: 'exportedDate', - SUBMITTED_DATA: 'submittedData', + LAST_EXPENSE: 'LAST_EXPENSE', + REPORT_EXPORTED: 'REPORT_EXPORTED', + REPORT_SUBMITTED: 'REPORT_SUBMITTED', }, QUICKBOOKS_EXPORT_COMPANY_CARD: { @@ -4685,6 +4688,12 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, + + SEARCH_TRANSACTION_TYPE: { + CASH: 'cash', + CARD: 'card', + DISTANCE: 'distance', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 367e3230ae41..1a27d691e2ef 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -340,7 +340,6 @@ const ONYXKEYS = { REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_', REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_', REPORT_DRAFT_COMMENT: 'reportDraftComment_', - REPORT_DRAFT_COMMENT_NUMBER_OF_LINES: 'reportDraftCommentNumberOfLines_', REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_', REPORT_USER_IS_TYPING: 'reportUserIsTyping_', REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_', @@ -359,6 +358,9 @@ const ONYXKEYS = { /** This is deprecated, but needed for a migration, so we still need to include it here so that it will be initialized in Onyx.init */ DEPRECATED_POLICY_MEMBER_LIST: 'policyMemberList_', + + // Search Page related + SNAPSHOT: 'snapshot_', }, /** List of Form ids */ @@ -545,7 +547,6 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; - [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES]: number; [ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean; [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: OnyxTypes.ReportUserIsTyping; [ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean; @@ -563,6 +564,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; + [ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults; }; type OnyxValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 88a7adcbcb84..6e69d2d4e53f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -97,12 +97,12 @@ const ROUTES = { SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', SETTINGS_WALLET_DOMAINCARD: { - route: 'settings/wallet/card/:domain', - getRoute: (domain: string) => `settings/wallet/card/${domain}` as const, + route: 'settings/wallet/card/:cardID?', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}` as const, }, SETTINGS_REPORT_FRAUD: { - route: 'settings/wallet/card/:domain/report-virtual-fraud', - getRoute: (domain: string) => `settings/wallet/card/${domain}/report-virtual-fraud` as const, + route: 'settings/wallet/card/:cardID/report-virtual-fraud', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/report-virtual-fraud` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: { route: 'settings/wallet/card/:domain/get-physical/name', @@ -124,6 +124,7 @@ const ROUTES = { SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', SETTINGS_ADD_BANK_ACCOUNT_REFACTOR: 'settings/wallet/add-bank-account-refactor', SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', + SETTINGS_ENABLE_PAYMENTS_REFACTOR: 'settings/wallet/enable-payments-refactor', SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS: { route: 'settings/wallet/card/:domain/digital-details/update-address', getRoute: (domain: string) => `settings/wallet/card/${domain}/digital-details/update-address` as const, @@ -131,12 +132,12 @@ const ROUTES = { SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: { - route: 'settings/wallet/card/:domain/report-card-lost-or-damaged', - getRoute: (domain: string) => `settings/wallet/card/${domain}/report-card-lost-or-damaged` as const, + route: 'settings/wallet/card/:cardID/report-card-lost-or-damaged', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/report-card-lost-or-damaged` as const, }, SETTINGS_WALLET_CARD_ACTIVATE: { - route: 'settings/wallet/card/:domain/activate', - getRoute: (domain: string) => `settings/wallet/card/${domain}/activate` as const, + route: 'settings/wallet/card/:cardID/activate', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/activate` as const, }, SETTINGS_LEGAL_NAME: 'settings/profile/legal-name', SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', @@ -314,6 +315,11 @@ const ROUTES = { route: ':action/:iouType/start/:transactionID/:reportID', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => `${action as string}/${iouType as string}/start/${transactionID}/${reportID}` as const, }, + MONEY_REQUEST_STEP_SEND_FROM: { + route: 'create/:iouType/from/:transactionID/:reportID', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType as string}/from/${transactionID}/${reportID}`, backTo), + }, MONEY_REQUEST_STEP_CONFIRMATION: { route: ':action/:iouType/confirmation/:transactionID/:reportID', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string) => @@ -374,6 +380,11 @@ const ROUTES = { getRoute: (iouType: IOUType, transactionID: string, reportID: string, backTo = '', action: IOUAction = 'create') => getUrlWithBackToParam(`${action as string}/${iouType as string}/participants/${transactionID}/${reportID}`, backTo), }, + MONEY_REQUEST_STEP_SPLIT_PAYER: { + route: ':action/:iouType/confirmation/:transactionID/:reportID/payer', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/confirmation/${transactionID}/${reportID}/payer`, backTo), + }, MONEY_REQUEST_STEP_SCAN: { route: ':action/:iouType/scan/:transactionID/:reportID', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => diff --git a/src/SCREENS.ts b/src/SCREENS.ts index daf2a9791930..725cab85f12b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -85,6 +85,7 @@ const SCREENS = { TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance', CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account', ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments', + ENABLE_PAYMENTS_REFACTOR: 'Settings_Wallet_EnablePayments_Refactor', CARD_ACTIVATE: 'Settings_Wallet_Card_Activate', REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud', CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', @@ -161,6 +162,8 @@ const SCREENS = { STEP_WAYPOINT: 'Money_Request_Step_Waypoint', STEP_TAX_AMOUNT: 'Money_Request_Step_Tax_Amount', STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate', + STEP_SPLIT_PAYER: 'Money_Request_Step_Split_Payer', + STEP_SEND_FROM: 'Money_Request_Step_Send_From', CURRENCY: 'Money_Request_Currency', WAYPOINT: 'Money_Request_Waypoint', EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 99183a1e6ba7..4acf197ba178 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -51,8 +51,8 @@ type AvatarProps = { /** Owner of the avatar. If user, displayName. If workspace, policy name */ name?: string; - /** Optional account id if it's user avatar */ - accountID?: number; + /** Optional account id if it's user avatar or policy id if it's workspace avatar */ + accountID?: number | string; }; function Avatar({ @@ -80,13 +80,13 @@ function Avatar({ }, [originalSource]); const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; - const iconSize = StyleUtils.getAvatarSize(size); + const imageStyle: StyleProp = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; // We pass the color styles down to the SVG for the workspace and fallback avatar. - const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, accountID); + const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, Number(accountID)); const useFallBackAvatar = imageError || !source || source === Expensicons.FallbackAvatar; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; @@ -94,7 +94,7 @@ function Avatar({ let iconColors; if (isWorkspace) { - iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(name); + iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(accountID?.toString() ?? ''); } else if (useFallBackAvatar) { iconColors = StyleUtils.getBackgroundColorAndFill(theme.buttonHoveredBG, theme.icon); } else { diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index c7a4ece0de97..8942bf97a7dd 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -60,7 +60,8 @@ function AvatarWithDisplayName({ const title = ReportUtils.getReportName(report); const subtitle = ReportUtils.getChatRoomSubtitle(report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); - const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report); + const isMoneyRequestOrReport = + ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report); const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false); diff --git a/src/components/AvatarWithIndicator.tsx b/src/components/AvatarWithIndicator.tsx index 1bf18afb70ff..42b91b3d2d71 100644 --- a/src/components/AvatarWithIndicator.tsx +++ b/src/components/AvatarWithIndicator.tsx @@ -11,10 +11,7 @@ import Tooltip from './Tooltip'; type AvatarWithIndicatorProps = { /** URL for the avatar */ - source?: UserUtils.AvatarSource; - - /** account id if it's user avatar */ - accountID?: number; + source: UserUtils.AvatarSource; /** To show a tooltip on hover */ tooltipText?: string; @@ -26,7 +23,7 @@ type AvatarWithIndicatorProps = { isLoading?: boolean; }; -function AvatarWithIndicator({source, accountID, tooltipText = '', fallbackIcon = Expensicons.FallbackAvatar, isLoading = true}: AvatarWithIndicatorProps) { +function AvatarWithIndicator({source, tooltipText = '', fallbackIcon = Expensicons.FallbackAvatar, isLoading = true}: AvatarWithIndicatorProps) { const styles = useThemeStyles(); return ( @@ -38,7 +35,7 @@ function AvatarWithIndicator({source, accountID, tooltipText = '', fallbackIcon <> diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index a46b37c986ba..72dc53cceb39 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -7,6 +7,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import getButtonState from '@libs/getButtonState'; import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; import Hoverable from './Hoverable'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; @@ -17,7 +18,13 @@ import Tooltip from './Tooltip'; type BannerProps = { /** Text to display in the banner. */ - text: string; + text?: string; + + /** Content to display in the banner. */ + content?: React.ReactNode; + + /** The icon asset to display to the left of the text */ + icon?: IconAsset | null; /** Should this component render the left-aligned exclamation icon? */ shouldShowIcon?: boolean; @@ -41,7 +48,18 @@ type BannerProps = { textStyles?: StyleProp; }; -function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) { +function Banner({ + text, + content, + icon = Expensicons.Exclamation, + onClose, + onPress, + containerStyles, + textStyles, + shouldRenderHTML = false, + shouldShowIcon = false, + shouldShowCloseButton = false, +}: BannerProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -65,15 +83,17 @@ function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRend ]} > - {shouldShowIcon && ( + {shouldShowIcon && icon && ( )} - {shouldRenderHTML ? ( + {content && content} + + {shouldRenderHTML && text ? ( ) : ( & { /** Boolean whether to display the right icon */ shouldShowRightIcon?: boolean; + + /** Whether the button should use split style or not */ + isSplitButton?: boolean; }; type KeyboardShortcutComponentProps = Pick; @@ -198,6 +201,7 @@ function Button( id = '', accessibilityLabel = '', + isSplitButton = false, ...rest }: ButtonProps, ref: ForwardedRef, @@ -253,13 +257,22 @@ function Button( {shouldShowRightIcon && ( - + {!isSplitButton ? ( + + ) : ( + + )} )} diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index a4e6e2c87fec..a28b7ebf0864 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -14,6 +14,7 @@ import type {ButtonWithDropdownMenuProps} from './types'; function ButtonWithDropdownMenu({ success = false, + isSplitButton = true, isLoading = false, isDisabled = false, pressOnEnter = false, @@ -40,7 +41,7 @@ function ButtonWithDropdownMenu({ const [isMenuVisible, setIsMenuVisible] = useState(false); const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); - const caretButton = useRef(null); + const caretButton = useRef(null); const selectedItem = options[selectedItemIndex] || options[0]; const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; @@ -64,7 +65,6 @@ function ButtonWithDropdownMenu({ }); } }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); - return ( {shouldAlwaysShowDropdownMenu || options.length > 1 ? ( @@ -72,8 +72,10 @@ function ButtonWithDropdownMenu({ + + )} ) : ( - + ); } diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx deleted file mode 100644 index 64391909b197..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import {Animated, View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type FloatingMessageCounterContainerProps from './types'; - -function FloatingMessageCounterContainer({containerStyles, children}: FloatingMessageCounterContainerProps) { - const styles = useThemeStyles(); - - return ( - - {children} - - ); -} - -FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; - -export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx deleted file mode 100644 index 8757d66160c4..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type FloatingMessageCounterContainerProps from './types'; - -function FloatingMessageCounterContainer({accessibilityHint, containerStyles, children}: FloatingMessageCounterContainerProps) { - const styles = useThemeStyles(); - - return ( - - {children} - - ); -} - -FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; - -export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts deleted file mode 100644 index cfe791eed79c..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type {StyleProp, ViewStyle} from 'react-native'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; - -type FloatingMessageCounterContainerProps = ChildrenProps & { - /** Styles to be assigned to Container */ - containerStyles?: StyleProp; - - /** Specifies the accessibility hint for the component */ - accessibilityHint?: string; -}; - -export default FloatingMessageCounterContainerProps; diff --git a/src/pages/home/report/ReactionList/BaseReactionList.tsx b/src/pages/home/report/ReactionList/BaseReactionList.tsx index 23417c1395df..6f56f8f09632 100755 --- a/src/pages/home/report/ReactionList/BaseReactionList.tsx +++ b/src/pages/home/report/ReactionList/BaseReactionList.tsx @@ -2,11 +2,11 @@ import Str from 'expensify-common/lib/str'; import React from 'react'; import {FlatList} from 'react-native'; import type {FlatListProps} from 'react-native'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import OptionRow from '@components/OptionRow'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; +import * as UserUtils from '@libs/UserUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -71,7 +71,7 @@ function BaseReactionList({hasUserReacted = false, users, isVisible = false, emo icons: [ { id: item.accountID, - source: item.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(item.avatar, item.accountID), name: item.login ?? '', type: CONST.ICON_TYPE_AVATAR, }, diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 1adb161a92e9..07de62b1eabd 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -19,6 +19,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; +import getIconForAction from '@libs/getIconForAction'; import * as ReportUtils from '@libs/ReportUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; @@ -128,25 +129,30 @@ function AttachmentPickerWithMenuItems({ const moneyRequestOptions = useMemo(() => { const options: MoneyRequestOptions = { [CONST.IOU.TYPE.SPLIT]: { - icon: Expensicons.Receipt, + icon: Expensicons.Transfer, text: translate('iou.splitExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? ''), }, [CONST.IOU.TYPE.SUBMIT]: { - icon: Expensicons.MoneyCircle, + icon: getIconForAction(CONST.IOU.TYPE.REQUEST), text: translate('iou.submitExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SUBMIT, report?.reportID ?? ''), }, [CONST.IOU.TYPE.PAY]: { - icon: Expensicons.Send, + icon: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.PAY, report?.reportID ?? ''), }, [CONST.IOU.TYPE.TRACK]: { - icon: Expensicons.DocumentPlus, + icon: getIconForAction(CONST.IOU.TYPE.TRACK), text: translate('iou.trackExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, report?.reportID ?? ''), }, + [CONST.IOU.TYPE.INVOICE]: { + icon: Expensicons.Invoice, + text: translate('workspace.invoices.sendInvoice'), + onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.INVOICE, report?.reportID ?? ''), + }, }; return ReportUtils.temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs ?? [], canUseTrackExpense).map((option) => ({ diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 8f42da5a1575..469a7300a84f 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -44,6 +44,7 @@ import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutsi import type {ComposerRef, SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; +import variables from '@styles/variables'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; import * as InputFocus from '@userActions/InputFocus'; import * as Report from '@userActions/Report'; @@ -63,9 +64,6 @@ type AnimatedRef = ReturnType; type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; type ComposerWithSuggestionsOnyxProps = { - /** The number of lines the comment should take up */ - numberOfLines: OnyxEntry; - /** The parent report actions for the report */ parentReportActions: OnyxEntry; @@ -215,7 +213,6 @@ function ComposerWithSuggestions( modal, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, parentReportActions, - numberOfLines, // Props: Report reportID, @@ -459,22 +456,9 @@ function ComposerWithSuggestions( ], ); - /** - * Update the number of lines for a comment in Onyx - */ - const updateNumberOfLines = useCallback( - (newNumberOfLines: number) => { - if (newNumberOfLines === numberOfLines) { - return; - } - Report.saveReportCommentNumberOfLines(reportID, newNumberOfLines); - }, - [reportID, numberOfLines], - ); - const prepareCommentAndResetComposer = useCallback((): string => { const trimmedComment = commentRef.current.trim(); - const commentLength = ReportUtils.getCommentLength(trimmedComment); + const commentLength = ReportUtils.getCommentLength(trimmedComment, {reportID}); // Don't submit empty comments or comments that exceed the character limit if (!commentLength || commentLength > CONST.MAX_COMMENT_LENGTH) { @@ -730,6 +714,11 @@ function ComposerWithSuggestions( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const isOnlyEmojiLineHeight = useMemo(() => { + const isOnlyEmoji = EmojiUtils.containsOnlyEmojis(value); + return isOnlyEmoji ? {lineHeight: variables.fontSizeOnlyEmojisHeight} : {}; + }, [value]); + return ( <> @@ -743,7 +732,7 @@ function ComposerWithSuggestions( onChangeText={onChangeText} onKeyPress={triggerHotkeyActions} textAlignVertical="top" - style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} + style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose, isOnlyEmojiLineHeight]} maxLines={maxComposerLines} onFocus={onFocus} onBlur={onBlur} @@ -760,8 +749,6 @@ function ComposerWithSuggestions( isComposerFullSize={isComposerFullSize} value={value} testID="composer" - numberOfLines={numberOfLines ?? undefined} - onNumberOfLinesChange={updateNumberOfLines} shouldCalculateCaretPosition onLayout={onLayout} onScroll={hideSuggestionMenu} @@ -808,12 +795,6 @@ ComposerWithSuggestions.displayName = 'ComposerWithSuggestions'; const ComposerWithSuggestionsWithRef = forwardRef(ComposerWithSuggestions); export default withOnyx, ComposerWithSuggestionsOnyxProps>({ - numberOfLines: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}`, - // We might not have number of lines in onyx yet, for which the composer would be rendered as null - // during the first render, which we want to avoid: - initWithStoredValues: false, - }, modal: { key: ONYXKEYS.MODAL, }, diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 75d0c703b5b1..5bfa2475ee23 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -461,7 +461,7 @@ function ReportActionCompose({ if (value.length === 0 && isComposerFullSize) { Report.setIsComposerFullSize(reportID, false); } - validateCommentMaxLength(value); + validateCommentMaxLength(value, {reportID}); }} /> ); } - if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { + if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report) || ReportUtils.isInvoiceReport(report)) { return ( {transactionThreadReport && !isEmptyObject(transactionThreadReport) ? ( @@ -914,12 +915,12 @@ function ReportActionItem({ originalReportID={originalReportID ?? ''} isArchivedRoom={ReportUtils.isArchivedRoom(report)} displayAsGroup={displayAsGroup} + disabledActions={!ReportUtils.canWriteInReport(report) ? RestrictedReadOnlyContextMenuActions : []} isVisible={hovered && draftMessage === undefined && !hasErrors} draftMessage={draftMessage} isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} setIsEmojiPickerActive={setIsEmojiPickerActive} - transactionThreadReportID={transactionThreadReport?.reportID ?? '0'} /> { // Do nothing if draft exceed the character limit - if (ReportUtils.getCommentLength(draft) > CONST.MAX_COMMENT_LENGTH) { + if (ReportUtils.getCommentLength(draft, {reportID}) > CONST.MAX_COMMENT_LENGTH) { return; } @@ -380,8 +380,8 @@ function ReportActionItemMessageEdit( const focus = focusComposerWithDelay(textInputRef.current); useEffect(() => { - validateCommentMaxLength(draft); - }, [draft, validateCommentMaxLength]); + validateCommentMaxLength(draft, {reportID}); + }, [draft, reportID, validateCommentMaxLength]); return ( <> diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 54b6775cfe13..234147a30bd5 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -3,7 +3,6 @@ import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import Avatar from '@components/Avatar'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxProvider'; @@ -20,6 +19,7 @@ import ControlSelection from '@libs/ControlSelection'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; +import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report, ReportAction} from '@src/types/onyx'; @@ -85,15 +85,14 @@ function ReportActionItemSingle({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && iouReport, [action?.actionName, iouReport]); - const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors); - let avatarSource = avatar; - let avatarAccountId = actorAccountID; + const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport ?? {}); + const isWorkspaceActor = isInvoiceReport || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors)); + let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); if (isWorkspaceActor) { displayName = ReportUtils.getPolicyName(report); actorHint = displayName; avatarSource = ReportUtils.getWorkspaceAvatar(report); - avatarAccountId = undefined; } else if (action?.delegateAccountID && personalDetails[action?.delegateAccountID]) { // We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their // details. This will be improved upon when the Copilot feature is implemented. @@ -101,8 +100,7 @@ function ReportActionItemSingle({ const delegateDisplayName = delegateDetails?.displayName; actorHint = `${delegateDisplayName} (${translate('reportAction.asCopilot')} ${displayName})`; displayName = actorHint; - avatarSource = delegateDetails?.avatar; - avatarAccountId = action.delegateAccountID; + avatarSource = UserUtils.getAvatar(delegateDetails?.avatar ?? '', Number(action.delegateAccountID)); } // If this is a report preview, display names and avatars of both people involved @@ -110,12 +108,16 @@ function ReportActionItemSingle({ const primaryDisplayName = displayName; if (displayAllActors) { // The ownerAccountID and actorAccountID can be the same if the a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice - const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID ? iouReport?.managerID : iouReport?.ownerAccountID; + const secondaryAccountId = iouReport?.ownerAccountID === actorAccountID || isInvoiceReport ? iouReport?.managerID : iouReport?.ownerAccountID; const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? ''; const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); - displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; + + if (!isInvoiceReport) { + displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; + } + secondaryAvatar = { - source: secondaryUserAvatar, + source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId), type: CONST.ICON_TYPE_AVATAR, name: secondaryDisplayName ?? '', id: secondaryAccountId, @@ -129,12 +131,11 @@ function ReportActionItemSingle({ } else { secondaryAvatar = {name: '', source: '', type: 'avatar'}; } - const icon = { - source: avatarSource ?? FallbackAvatar, + source: avatarSource, type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR, name: primaryDisplayName ?? '', - id: avatarAccountId, + id: isWorkspaceActor ? '' : actorAccountID, }; // Since the display name for a report action message is delivered with the report history as an array of fragments @@ -204,8 +205,8 @@ function ReportActionItemSingle({ source={icon.source} type={icon.type} name={icon.name} - fallbackIcon={fallbackIcon} accountID={icon.id} + fallbackIcon={fallbackIcon} /> diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 6f59c1b162c9..c280a093cb13 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -4,7 +4,7 @@ import type {RouteProp} from '@react-navigation/native'; import type {DebouncedFunc} from 'lodash'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; -import type {EmitterSubscription, LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import InvertedFlatList from '@components/InvertedFlatList'; @@ -200,8 +200,7 @@ function ReportActionsList({ ); const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID; const reportActionSize = useRef(sortedVisibleReportActions.length); - const hasNewestReportAction = - sortedReportActions?.[0].created === report.lastVisibleActionCreated || sortedReportActions?.[0].created === transactionThreadReport?.lastVisibleActionCreated; + const hasNewestReportAction = sortedReportActions?.[0].created === report.lastVisibleActionCreated; const hasNewestReportActionRef = useRef(hasNewestReportAction); hasNewestReportActionRef.current = hasNewestReportAction; const previousLastIndex = useRef(lastActionIndex); @@ -307,28 +306,12 @@ function ReportActionsList({ setMessageManuallyMarkedUnread(new Date().getTime()); }); - let unreadActionSubscriptionForTransactionThread: EmitterSubscription | undefined; - let readNewestActionSubscriptionForTransactionThread: EmitterSubscription | undefined; - if (transactionThreadReport?.reportID) { - unreadActionSubscriptionForTransactionThread = DeviceEventEmitter.addListener(`unreadAction_${transactionThreadReport?.reportID}`, (newLastReadTime) => { - resetUnreadMarker(newLastReadTime); - setMessageManuallyMarkedUnread(new Date().getTime()); - }); - - readNewestActionSubscriptionForTransactionThread = DeviceEventEmitter.addListener(`readNewestAction_${transactionThreadReport?.reportID}`, (newLastReadTime) => { - resetUnreadMarker(newLastReadTime); - setMessageManuallyMarkedUnread(0); - }); - } - return () => { unreadActionSubscription.remove(); readNewestActionSubscription.remove(); deletedReportActionSubscription.remove(); - unreadActionSubscriptionForTransactionThread?.remove(); - readNewestActionSubscriptionForTransactionThread?.remove(); }; - }, [report.reportID, transactionThreadReport?.reportID]); + }, [report.reportID]); useEffect(() => { if (linkedReportActionID) { @@ -342,16 +325,14 @@ function ReportActionsList({ const scrollToBottomForCurrentUserAction = useCallback( (isFromCurrentUser: boolean) => { - // If a new comment is added and it's from the current user scroll to the bottom - // otherwise leave the user positioned where they are now in the list. - // Additionally, since the first report action could be a whisper message (new WS) -> - // hasNewestReportAction will be false, check isWhisperAction is false before returning early. - if (!isFromCurrentUser || (!hasNewestReportActionRef.current && !ReportActionsUtils.isWhisperAction(sortedReportActions?.[0]))) { + // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where + // they are now in the list. + if (!isFromCurrentUser || !hasNewestReportActionRef.current) { return; } InteractionManager.runAfterInteractions(() => reportScrollManager.scrollToBottom()); }, - [sortedReportActions, reportScrollManager], + [reportScrollManager], ); useEffect(() => { // Why are we doing this, when in the cleanup of the useEffect we are already calling the unsubscribe function? @@ -416,9 +397,6 @@ function ReportActionsList({ reportScrollManager.scrollToBottom(); readActionSkipped.current = false; Report.readNewestAction(report.reportID); - if (transactionThreadReport?.reportID) { - Report.readNewestAction(transactionThreadReport?.reportID); - } }; /** diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 26f796b8bdc4..cb904327e625 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -348,10 +348,18 @@ function ReportActionsView({ newestReportAction: newestReportAction.pendingAction, firstReportActionID: newestReportAction?.reportActionID, isLoadingOlderReportsFirstNeeded, + reportActionID, })}`, ); - if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + if ( + !reportActionID || + !isFocused || + isLoadingInitialReportActions || + isLoadingOlderReportActions || + network.isOffline || + newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + ) { return; } @@ -368,6 +376,7 @@ function ReportActionsView({ network.isOffline, reportActions.length, newestReportAction, + isFocused, ]); /** diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 04fbd0308390..9a8ca2955127 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -21,6 +21,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import ReportActionCompose from './ReportActionCompose/ReportActionCompose'; +import SystemChatReportFooterMessage from './SystemChatReportFooterMessage'; type ReportFooterOnyxProps = { /** Whether to show the compose input */ @@ -81,6 +82,8 @@ function ReportFooter({ const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; const hideComposer = !ReportUtils.canUserPerformWriteAction(report); + const canWriteInReport = ReportUtils.canWriteInReport(report); + const isSystemChat = ReportUtils.isSystemChat(report); const allPersonalDetails = usePersonalDetails(); @@ -131,7 +134,7 @@ function ReportFooter({ return ( <> {hideComposer && ( - + {isAnonymousUser && !isArchivedRoom && ( )} {isArchivedRoom && } + {!isAnonymousUser && !canWriteInReport && isSystemChat && } {!isSmallScreenWidth && {hideComposer && }} )} diff --git a/src/pages/home/report/SystemChatReportFooterMessage.tsx b/src/pages/home/report/SystemChatReportFooterMessage.tsx new file mode 100644 index 000000000000..c9ccac8f5c18 --- /dev/null +++ b/src/pages/home/report/SystemChatReportFooterMessage.tsx @@ -0,0 +1,91 @@ +import React, {useMemo} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import Banner from '@components/Banner'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import Navigation from '@navigation/Navigation'; +import * as ReportInstance from '@userActions/Report'; +import type {OnboardingPurposeType} from '@src/CONST'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {Policy as PolicyType} from '@src/types/onyx'; + +type SystemChatReportFooterMessageOnyxProps = { + /** Saved onboarding purpose selected by the user */ + choice: OnyxEntry; + + /** The list of this user's policies */ + policies: OnyxCollection; + + /** policyID for main workspace */ + activePolicyID: OnyxEntry>; +}; + +type SystemChatReportFooterMessageProps = SystemChatReportFooterMessageOnyxProps; + +function SystemChatReportFooterMessage({choice, policies, activePolicyID}: SystemChatReportFooterMessageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const adminChatReport = useMemo(() => { + const adminPolicy = activePolicyID + ? PolicyUtils.getPolicy(activePolicyID ?? '') + : Object.values(policies ?? {}).find((policy) => PolicyUtils.shouldShowPolicy(policy, false) && policy?.role === CONST.POLICY.ROLE.ADMIN && policy?.chatReportIDAdmins); + + return ReportUtils.getReport(String(adminPolicy?.chatReportIDAdmins)); + }, [activePolicyID, policies]); + + const content = useMemo(() => { + switch (choice) { + case CONST.ONBOARDING_CHOICES.MANAGE_TEAM: + return ( + <> + {translate('systemChatFooterMessage.newDotManageTeam.phrase1')} + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminChatReport?.reportID ?? ''))}> + {adminChatReport?.reportName ?? CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS} + + {translate('systemChatFooterMessage.newDotManageTeam.phrase2')} + + ); + default: + return ( + <> + {translate('systemChatFooterMessage.default.phrase1')} + ReportInstance.navigateToConciergeChat()}>{CONST?.CONCIERGE_CHAT_NAME} + {translate('systemChatFooterMessage.default.phrase2')} + + ); + } + }, [adminChatReport?.reportName, adminChatReport?.reportID, choice, translate]); + + return ( + {content}} + /> + ); +} + +SystemChatReportFooterMessage.displayName = 'SystemChatReportFooterMessage'; + +export default withOnyx({ + choice: { + key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + activePolicyID: { + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + initialValue: null, + }, +})(SystemChatReportFooterMessage); diff --git a/src/pages/home/report/reportActionSourcePropType.js b/src/pages/home/report/reportActionSourcePropType.js deleted file mode 100644 index 0ad9662eb693..000000000000 --- a/src/pages/home/report/reportActionSourcePropType.js +++ /dev/null @@ -1,3 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.oneOf(['Chronos', 'email', 'ios', 'android', 'web', 'email', '']); diff --git a/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx b/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx index b0287efb8990..e7726fb89537 100644 --- a/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx +++ b/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx @@ -5,6 +5,7 @@ import AvatarWithIndicator from '@components/AvatarWithIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as UserUtils from '@libs/UserUtils'; import ONYXKEYS from '@src/ONYXKEYS'; type ProfileAvatarWithIndicatorProps = { @@ -22,8 +23,7 @@ function ProfileAvatarWithIndicator({isSelected = false}: ProfileAvatarWithIndic diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index adc5f3517c11..9429591b851f 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -15,9 +15,11 @@ import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as App from '@userActions/App'; import * as IOU from '@userActions/IOU'; @@ -42,7 +44,7 @@ const useIsFocused = () => { return isFocused || (topmostCentralPane?.name === SCREENS.SEARCH.CENTRAL_PANE && isSmallScreenWidth); }; -type PolicySelector = Pick; +type PolicySelector = Pick; type FloatingActionButtonAndPopoverOnyxProps = { /** The list of policies the user has access to. */ @@ -54,6 +56,12 @@ type FloatingActionButtonAndPopoverOnyxProps = { /** Information on the last taken action to display as Quick Action */ quickAction: OnyxEntry; + /** The report data of the quick action */ + quickActionReport: OnyxEntry; + + /** The policy data of the quick action */ + quickActionPolicy: OnyxEntry; + /** The current session */ session: OnyxEntry; @@ -80,6 +88,7 @@ const policySelector = (policy: OnyxEntry): PolicySelector => (policy && { type: policy.type, role: policy.role, + id: policy.id, isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, pendingAction: policy.pendingAction, avatar: policy.avatar, @@ -91,7 +100,7 @@ const getQuickActionIcon = (action: QuickActionName): React.FC => { case CONST.QUICK_ACTIONS.REQUEST_MANUAL: return Expensicons.MoneyCircle; case CONST.QUICK_ACTIONS.REQUEST_SCAN: - return Expensicons.Receipt; + return Expensicons.ReceiptScan; case CONST.QUICK_ACTIONS.REQUEST_DISTANCE: return Expensicons.Car; case CONST.QUICK_ACTIONS.SPLIT_MANUAL: @@ -99,9 +108,13 @@ const getQuickActionIcon = (action: QuickActionName): React.FC => { case CONST.QUICK_ACTIONS.SPLIT_DISTANCE: return Expensicons.Transfer; case CONST.QUICK_ACTIONS.SEND_MONEY: - return Expensicons.Send; + return getIconForAction(CONST.IOU.TYPE.SEND); case CONST.QUICK_ACTIONS.ASSIGN_TASK: return Expensicons.Task; + case CONST.QUICK_ACTIONS.TRACK_DISTANCE: + case CONST.QUICK_ACTIONS.TRACK_MANUAL: + case CONST.QUICK_ACTIONS.TRACK_SCAN: + return getIconForAction(CONST.IOU.TYPE.TRACK); default: return Expensicons.MoneyCircle; } @@ -141,7 +154,18 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => { * FAB that can open or close the menu. */ function FloatingActionButtonAndPopover( - {onHideCreateMenu, onShowCreateMenu, isLoading = false, allPolicies, quickAction, session, personalDetails, hasSeenTrackTraining}: FloatingActionButtonAndPopoverProps, + { + onHideCreateMenu, + onShowCreateMenu, + isLoading = false, + allPolicies, + quickAction, + quickActionReport, + quickActionPolicy, + session, + personalDetails, + hasSeenTrackTraining, + }: FloatingActionButtonAndPopoverProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -154,9 +178,7 @@ function FloatingActionButtonAndPopover( const prevIsFocused = usePrevious(isFocused); const {isOffline} = useNetwork(); - const quickActionReport: OnyxEntry = useMemo(() => (quickAction ? ReportUtils.getReport(quickAction.chatReportID) : null), [quickAction]); - - const quickActionPolicy = allPolicies ? allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`] : undefined; + const canSendInvoice = useMemo(() => PolicyUtils.canSendInvoice(allPolicies as OnyxCollection), [allPolicies]); const quickActionAvatars = useMemo(() => { if (quickActionReport) { @@ -297,7 +319,7 @@ function FloatingActionButtonAndPopover( ...(canUseTrackExpense && selfDMReportID ? [ { - icon: Expensicons.DocumentPlus, + icon: getIconForAction(CONST.IOU.TYPE.TRACK), text: translate('iou.trackExpense'), onSelected: () => { interceptAnonymousUser(() => @@ -310,14 +332,16 @@ function FloatingActionButtonAndPopover( ), ); if (!hasSeenTrackTraining && !isOffline) { - Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL); + setTimeout(() => { + Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL); + }, CONST.ANIMATED_TRANSITION); } }, }, ] : []), { - icon: Expensicons.MoneyCircle, + icon: getIconForAction(CONST.IOU.TYPE.REQUEST), text: translate('iou.submitExpense'), onSelected: () => interceptAnonymousUser(() => @@ -343,7 +367,7 @@ function FloatingActionButtonAndPopover( ), }, { - icon: Expensicons.Send, + icon: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {}), onSelected: () => interceptAnonymousUser(() => @@ -355,6 +379,23 @@ function FloatingActionButtonAndPopover( ), ), }, + ...(canSendInvoice + ? [ + { + icon: Expensicons.InvoiceGeneric, + text: translate('workspace.invoices.sendInvoice'), + onSelected: () => + interceptAnonymousUser(() => + IOU.startMoneyRequest( + CONST.IOU.TYPE.INVOICE, + // When starting to create an invoice from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // for all of the routes in the creation flow. + ReportUtils.generateReportID(), + ), + ), + }, + ] + : []), { icon: Expensicons.Task, text: translate('newTaskPage.assignTask'), @@ -418,6 +459,12 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, + }, + quickActionPolicy: { + key: ({quickActionReport}) => `${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`, + }, personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index 8f016ec0d8d9..18b290a81ea4 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -46,6 +46,10 @@ function HoldReasonPage({route}: HoldReasonPageProps) { const {transactionID, reportID, backTo} = route.params; const report = ReportUtils.getReport(reportID); + + // We first check if the report is part of a policy - if not, then it's a personal request (1:1 request) + // For personal requests, we need to allow both users to put the request on hold + const isWorkspaceRequest = ReportUtils.isReportInGroupPolicy(report); const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); const navigateBack = () => { @@ -53,7 +57,10 @@ function HoldReasonPage({route}: HoldReasonPageProps) { }; const onSubmit = (values: FormOnyxValues) => { - if (!ReportUtils.canEditMoneyRequest(parentReportAction)) { + // We have extra isWorkspaceRequest condition since, for 1:1 requests, canEditMoneyRequest will rightly return false + // as we do not allow requestee to edit fields like description and amount. + // But, we still want the requestee to be able to put the request on hold + if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { return; } @@ -68,7 +75,10 @@ function HoldReasonPage({route}: HoldReasonPageProps) { if (!values.comment) { errors.comment = 'common.error.fieldRequired'; } - if (!ReportUtils.canEditMoneyRequest(parentReportAction)) { + // We have extra isWorkspaceRequest condition since, for 1:1 requests, canEditMoneyRequest will rightly return false + // as we do not allow requestee to edit fields like description and amount. + // But, we still want the requestee to be able to put the request on hold + if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { const formErrors = {}; ErrorUtils.addErrorMessage(formErrors, 'reportModified', 'common.error.requestModified'); FormActions.setErrors(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM, formErrors); @@ -76,7 +86,7 @@ function HoldReasonPage({route}: HoldReasonPageProps) { return errors; }, - [parentReportAction], + [parentReportAction, isWorkspaceRequest], ); useEffect(() => { diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx index f6055a271388..46bd34006550 100644 --- a/src/pages/iou/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -97,7 +97,6 @@ function MoneyRequestAmountForm( const textInput = useRef(null); const moneyRequestAmountInput = useRef(null); - const isTaxAmountForm = Navigation.getActiveRoute().includes('taxAmount'); const [formError, setFormError] = useState(''); const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true); @@ -205,6 +204,8 @@ function MoneyRequestAmountForm( */ const submitAndNavigateToNextPage = useCallback( (iouPaymentType?: PaymentMethodType | undefined) => { + const isTaxAmountForm = Navigation.getActiveRoute().includes('taxAmount'); + // Skip the check for tax amount form as 0 is a valid input const currentAmount = moneyRequestAmountInput.current?.getAmount() ?? ''; if (!currentAmount.length || (!isTaxAmountForm && isAmountInvalid(currentAmount))) { @@ -224,7 +225,7 @@ function MoneyRequestAmountForm( onSubmitButtonPress({amount: currentAmount, currency, paymentMethod: iouPaymentType}); }, - [taxAmount, isTaxAmountForm, onSubmitButtonPress, currency, formattedTaxAmount, initializeAmount], + [taxAmount, onSubmitButtonPress, currency, formattedTaxAmount, initializeAmount], ); const buttonText: string = useMemo(() => { diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 95c7b09ce1c1..1bbf0d02a941 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -1,7 +1,7 @@ import {useFocusEffect} from '@react-navigation/native'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -17,6 +17,7 @@ import * as IOUUtils from '@libs/IOUUtils'; import * as KeyDownPressListener from '@libs/KeyboardShortcut/KeyDownPressListener'; import Navigation from '@libs/Navigation/Navigation'; import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; @@ -43,6 +44,9 @@ type IOURequestStartPageOnyxProps = { /** The transaction being modified */ transaction: OnyxEntry; + + /** The list of all policies */ + allPolicies: OnyxCollection; }; type IOURequestStartPageProps = IOURequestStartPageOnyxProps & WithWritableReportOrNotFoundProps; @@ -56,6 +60,7 @@ function IOURequestStartPage({ }, selectedTab, transaction, + allPolicies, }: IOURequestStartPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -67,6 +72,7 @@ function IOURequestStartPage({ [CONST.IOU.TYPE.PAY]: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitExpense'), [CONST.IOU.TYPE.TRACK]: translate('iou.trackExpense'), + [CONST.IOU.TYPE.INVOICE]: translate('workspace.invoices.sendInvoice'), }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const {canUseP2PDistanceRequests} = usePermissions(iouType); @@ -100,7 +106,7 @@ function IOURequestStartPage({ const shouldDisplayDistanceRequest = (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate) && iouType !== CONST.IOU.TYPE.SPLIT; // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the exoense - const isAllowedToCreateRequest = isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); + const isAllowedToCreateRequest = isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType) || PolicyUtils.canSendInvoice(allPolicies); const navigateBack = () => { Navigation.closeRHPFlow(); @@ -138,7 +144,7 @@ function IOURequestStartPage({ title={tabTitles[iouType]} onBackButtonPress={navigateBack} /> - {iouType !== CONST.IOU.TYPE.SEND && iouType !== CONST.IOU.TYPE.PAY ? ( + {iouType !== CONST.IOU.TYPE.SEND && iouType !== CONST.IOU.TYPE.PAY && iouType !== CONST.IOU.TYPE.INVOICE ? ( ( transaction: { key: ({route}) => `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${route?.params.transactionID ?? 0}`, }, + allPolicies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, })(IOURequestStartPage); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 3a29f35fac8d..b7f0e0cbb880 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -24,6 +24,7 @@ import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionSt import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as Policy from '@userActions/Policy'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -72,17 +73,18 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF const {canUseP2PDistanceRequests} = usePermissions(); const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); const [betas] = useOnyx(ONYXKEYS.BETAS); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: didScreenTransitionEnd, }); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; - const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; - const isIOUSplit = iouType === CONST.IOU.TYPE.SPLIT; const isCategorizeOrShareAction = [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].includes(action); + const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE; + useEffect(() => { Report.searchInServer(debouncedSearchTerm.trim()); }, [debouncedSearchTerm]); @@ -128,22 +130,10 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF isCategorizeOrShareAction ? 0 : undefined, ); - const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( - debouncedSearchTerm, - participants, - chatOptions.recentReports, - chatOptions.personalDetails, - maxParticipantsReached, - personalDetails, - true, - ); + const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(debouncedSearchTerm, participants, chatOptions.recentReports, chatOptions.personalDetails, personalDetails, true); newSections.push(formatResults.section); - if (maxParticipantsReached) { - return [newSections, {}]; - } - newSections.push({ title: translate('common.recents'), data: chatOptions.recentReports, @@ -179,7 +169,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF action, canUseP2PDistanceRequests, iouRequestType, - maxParticipantsReached, personalDetails, translate, didScreenTransitionEnd, @@ -193,13 +182,26 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF */ const addSingleParticipant = useCallback( (option) => { - onParticipantsAdded([ + const newParticipants = [ { ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID'), selected: true, iouType, }, - ]); + ]; + + if (iouType === CONST.IOU.TYPE.INVOICE) { + const primaryPolicy = Policy.getPrimaryPolicy(activePolicyID); + + newParticipants.push({ + policyID: primaryPolicy.id, + isSender: true, + selected: false, + iouType, + }); + } + + onParticipantsAdded(newParticipants); onFinish(); }, // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to trigger this callback when iouType changes @@ -255,10 +257,9 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF lodashGet(newChatOptions, 'personalDetails', []).length + lodashGet(newChatOptions, 'recentReports', []).length !== 0, Boolean(newChatOptions.userToInvite), debouncedSearchTerm.trim(), - maxParticipantsReached, lodashSome(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), ), - [maxParticipantsReached, newChatOptions, participants, debouncedSearchTerm], + [newChatOptions, participants, debouncedSearchTerm], ); // Right now you can't split an expense with a workspace and other additional participants @@ -270,8 +271,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF // canUseP2PDistanceRequests is true if the iouType is track expense, but we don't want to allow splitting distance with track expense yet const isAllowedToSplit = (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && - iouType !== CONST.IOU.TYPE.PAY && - iouType !== CONST.IOU.TYPE.TRACK && + ![CONST.IOU.TYPE.PAY, CONST.IOU.TYPE.TRACK, CONST.IOU.TYPE.INVOICE].includes(iouType) && ![CONST.IOU.ACTION.SHARE, CONST.IOU.ACTION.SUBMIT, CONST.IOU.ACTION.CATEGORIZE].includes(action); const handleConfirmSelection = useCallback( @@ -298,7 +298,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF return ( <> - {!isDismissed && ( + {shouldShowReferralBanner && ( ); - }, [handleConfirmSelection, participants.length, isDismissed, referralContentType, shouldShowSplitBillErrorMessage, styles, translate]); + }, [handleConfirmSelection, participants.length, isDismissed, referralContentType, shouldShowSplitBillErrorMessage, styles, translate, shouldShowReferralBanner]); return ( { + if (personalDetails?.[transaction?.splitPayerAccountIDs?.[0] ?? -1]) { + return personalDetails?.[transaction?.splitPayerAccountIDs?.[0] ?? -1]; + } + + const participant = transaction?.participants?.find((val) => val.accountID === (transaction?.splitPayerAccountIDs?.[0] ?? -1)); + + return { + login: participant?.login ?? '', + accountID: participant?.accountID ?? -1, + avatar: Expensicons.FallbackAvatar, + displayName: participant?.login ?? '', + isOptimisticPersonalDetail: true, + }; + }, [personalDetails, transaction?.participants, transaction?.splitPayerAccountIDs]); const requestType = TransactionUtils.getRequestType(transaction); @@ -100,6 +115,9 @@ function IOURequestStepConfirmation({ if (iouType === CONST.IOU.TYPE.PAY) { return translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}); } + if (iouType === CONST.IOU.TYPE.INVOICE) { + return translate('workspace.invoices.sendInvoice'); + } return translate('iou.submitExpense'); }, [iouType, report, translate, isSharingTrackExpense, isCategorizingTrackExpense, isSubmittingFromTrackExpense]); @@ -107,9 +125,14 @@ function IOURequestStepConfirmation({ () => transaction?.participants?.map((participant) => { const participantAccountID = participant.accountID ?? 0; + + if (participant.isSender && iouType === CONST.IOU.TYPE.INVOICE) { + return participant; + } + return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); }) ?? [], - [transaction?.participants, personalDetails], + [transaction?.participants, personalDetails, iouType], ); const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); const formHasBeenSubmitted = useRef(false); @@ -327,6 +350,7 @@ function IOURequestStepConfirmation({ existingSplitChatReportID: report?.reportID, billable: transaction.billable, iouRequestType: transaction.iouRequestType, + splitPayerAccountIDs: transaction.splitPayerAccountIDs ?? [], }); } return; @@ -348,11 +372,17 @@ function IOURequestStepConfirmation({ tag: transaction.tag, billable: !!transaction.billable, iouRequestType: transaction.iouRequestType, + splitPayerAccountIDs: transaction.splitPayerAccountIDs, }); } return; } + if (iouType === CONST.IOU.TYPE.INVOICE) { + IOU.sendInvoice(currentUserPersonalDetails.accountID, transaction, report, receiptFile, policy, policyTags, policyCategories); + return; + } + if (iouType === CONST.IOU.TYPE.TRACK || isCategorizingTrackExpense || isSharingTrackExpense) { if (receiptFile && transaction) { // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. @@ -429,18 +459,21 @@ function IOURequestStepConfirmation({ }, [ transaction, + report, iouType, receiptFile, requestType, requestMoney, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, - report?.reportID, trackExpense, createDistanceRequest, isSharingTrackExpense, isCategorizingTrackExpense, action, + policy, + policyTags, + policyCategories, ], ); @@ -537,6 +570,7 @@ function IOURequestStepConfirmation({ isDistanceRequest={requestType === CONST.IOU.REQUEST_TYPE.DISTANCE} shouldShowSmartScanFields={IOUUtils.isMovingTransactionFromTrackExpense(action) ? transaction?.amount !== 0 : requestType !== CONST.IOU.REQUEST_TYPE.SCAN} action={action} + payeePersonalDetails={payeePersonalDetails} /> )} @@ -548,13 +582,13 @@ IOURequestStepConfirmation.displayName = 'IOURequestStepConfirmation'; const IOURequestStepConfirmationWithOnyx = withOnyx({ policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY}${IOU.getIOURequestPolicyID(transaction, report)}`, }, policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, report)}`, }, policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${IOU.getIOURequestPolicyID(transaction, report)}`, }, })(IOURequestStepConfirmation); /* eslint-disable rulesdir/no-negated-variables */ diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.tsx b/src/pages/iou/request/step/IOURequestStepCurrency.tsx index d03136063b61..8669563f3b9f 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.tsx +++ b/src/pages/iou/request/step/IOURequestStepCurrency.tsx @@ -1,11 +1,9 @@ -import Str from 'expensify-common/lib/str'; -import React, {useMemo, useState} from 'react'; +import React from 'react'; import {Keyboard} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; -import type {ListItem} from '@components/SelectionList/types'; +import CurrencySelectionList from '@components/CurrencySelectionList'; +import type {CurrencyListItem} from '@components/CurrencySelectionList/types'; import useLocalize from '@hooks/useLocalize'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -16,35 +14,25 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {getUrlWithBackToParam} from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {CurrencyList, Transaction} from '@src/types/onyx'; +import type {Transaction} from '@src/types/onyx'; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; type IOURequestStepCurrencyOnyxProps = { - /** Constant, list of available currencies */ - currencyList: OnyxEntry; - /** The draft transaction object being modified in Onyx */ draftTransaction: OnyxEntry; }; type IOURequestStepCurrencyProps = IOURequestStepCurrencyOnyxProps & WithFullTransactionOrNotFoundProps; -type CurrencyListItem = ListItem & { - currencyName: string; - currencyCode: string; -}; - function IOURequestStepCurrency({ - currencyList, route: { params: {backTo, iouType, pageIndex, reportID, transactionID, action, currency: selectedCurrency = ''}, }, draftTransaction, }: IOURequestStepCurrencyProps) { const {translate} = useLocalize(); - const [searchValue, setSearchValue] = useState(''); const {currency: originalCurrency = ''} = ReportUtils.getTransactionDetails(draftTransaction) ?? {}; const currency = CurrencyUtils.isValidCurrencyCode(selectedCurrency) ? selectedCurrency : originalCurrency; @@ -76,35 +64,6 @@ function IOURequestStepCurrency({ navigateBack(option.currencyCode); }; - const {sections, headerMessage, initiallyFocusedOptionKey} = useMemo(() => { - const currencyOptions: CurrencyListItem[] = Object.entries(currencyList ?? {}).map(([currencyCode, currencyInfo]) => { - const isSelectedCurrency = currencyCode === currency.toUpperCase(); - return { - currencyName: currencyInfo?.name ?? '', - text: `${currencyCode} - ${CurrencyUtils.getLocalizedCurrencySymbol(currencyCode)}`, - currencyCode, - keyForList: currencyCode, - isSelected: isSelectedCurrency, - }; - }); - - const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim()), 'i'); - const filteredCurrencies = currencyOptions.filter((currencyOption) => searchRegex.test(currencyOption.text ?? '') || searchRegex.test(currencyOption.currencyName)); - const isEmpty = searchValue.trim() && !filteredCurrencies.length; - - return { - initiallyFocusedOptionKey: filteredCurrencies.find((filteredCurrency) => filteredCurrency.currencyCode === currency.toUpperCase())?.keyForList, - sections: isEmpty - ? [] - : [ - { - data: filteredCurrencies, - }, - ], - headerMessage: isEmpty ? translate('common.noResultsFound') : '', - }; - }, [currencyList, searchValue, currency, translate]); - return ( {({didScreenTransitionEnd}) => ( - { + { if (!didScreenTransitionEnd) { return; } confirmCurrencySelection(option); }} - headerMessage={headerMessage} - initiallyFocusedOptionKey={initiallyFocusedOptionKey} - showScrollIndicator + initiallySelectedCurrencyCode={currency.toUpperCase()} /> )} @@ -138,7 +91,6 @@ function IOURequestStepCurrency({ IOURequestStepCurrency.displayName = 'IOURequestStepCurrency'; const IOURequestStepCurrencyWithOnyx = withOnyx({ - currencyList: {key: ONYXKEYS.CURRENCY_LIST}, draftTransaction: { key: ({route}) => { const transactionID = route?.params?.transactionID ?? 0; diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index b50495ac47bd..bc6f71b23228 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -63,7 +63,7 @@ function IOURequestStepMerchant({ const merchant = ReportUtils.getTransactionDetails(isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction)?.merchant; const isEmptyMerchant = merchant === '' || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const isMerchantRequired = ReportUtils.isGroupPolicy(report) || transaction?.participants?.some((participant) => Boolean(participant.isPolicyExpenseChat)); + const isMerchantRequired = ReportUtils.isReportInGroupPolicy(report) || transaction?.participants?.some((participant) => Boolean(participant.isPolicyExpenseChat)); const navigateBack = () => { Navigation.goBack(backTo); }; diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 374d4e9777cf..be95cb03e95b 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -55,6 +55,9 @@ function IOURequestStepParticipants({ if (iouType === CONST.IOU.TYPE.PAY) { return translate('iou.paySomeone', {}); } + if (iouType === CONST.IOU.TYPE.INVOICE) { + return translate('workspace.invoices.sendInvoice'); + } return translate('iou.submitExpense'); }, [iouType, translate, isSplitRequest, action]); diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx new file mode 100644 index 000000000000..6de3780aa6e8 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -0,0 +1,105 @@ +import React, {useMemo} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import * as Expensicons from '@components/Icon/Expensicons'; +import SelectionList from '@components/SelectionList'; +import type {ListItem} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Policy} from '@src/types/onyx'; +import StepScreenWrapper from './StepScreenWrapper'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; + +type WorkspaceListItem = ListItem & { + value: string; +}; + +type IOURequestStepSendFromOnyxProps = { + /** The list of all policies */ + allPolicies: OnyxCollection; +}; + +type IOURequestStepSendFromProps = IOURequestStepSendFromOnyxProps & + WithWritableReportOrNotFoundProps & + WithFullTransactionOrNotFoundProps; + +function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestStepSendFromProps) { + const {translate} = useLocalize(); + const {transactionID, backTo} = route.params; + + const selectedWorkspace = useMemo(() => transaction?.participants?.find((participant) => participant.isSender), [transaction]); + + const workspaceOptions: WorkspaceListItem[] = useMemo(() => { + const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); + return activeAdminWorkspaces.map((policy) => ({ + text: policy.name, + value: policy.id, + keyForList: policy.id, + icons: [ + { + source: policy?.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy.name), + fallbackIcon: Expensicons.FallbackWorkspaceAvatar, + name: policy.name, + type: CONST.ICON_TYPE_WORKSPACE, + }, + ], + isSelected: selectedWorkspace?.policyID === policy.id, + })); + }, [allPolicies, selectedWorkspace]); + + const navigateBack = () => { + Navigation.goBack(backTo); + }; + + const selectWorkspace = (item: WorkspaceListItem) => { + const newParticipants = (transaction?.participants ?? []).filter((participant) => participant.accountID); + + newParticipants.push({ + policyID: item.value, + isSender: true, + selected: false, + }); + + IOU.setMoneyRequestParticipants(transactionID, newParticipants); + navigateBack(); + }; + + return ( + + + + ); +} + +IOURequestStepSendFrom.displayName = 'IOURequestStepSendFrom'; + +export default withWritableReportOrNotFound( + withFullTransactionOrNotFound( + withOnyx({ + allPolicies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + })(IOURequestStepSendFrom), + ), +); diff --git a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx new file mode 100644 index 000000000000..c2ee404b1205 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx @@ -0,0 +1,103 @@ +import React, {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {usePersonalDetails} from '@components/OnyxProvider'; +import SelectionList from '@components/SelectionList'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; +import * as IOUUtils from '@libs/IOUUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import type {OptionData} from '@libs/ReportUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; +import StepScreenWrapper from './StepScreenWrapper'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; + +type IOURequestStepSplitPayerProps = WithWritableReportOrNotFoundProps & { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + transaction: OnyxEntry; +}; + +function IOURequestStepSplitPayer({ + route: { + params: {iouType, transactionID, action, backTo}, + }, + transaction, + report, +}: IOURequestStepSplitPayerProps) { + const {translate} = useLocalize(); + const personalDetails = usePersonalDetails(); + const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + + const currentUserOption = useMemo( + () => ({ + accountID: currentUserPersonalDetails.accountID, + searchText: currentUserPersonalDetails.login, + selected: true, + }), + [currentUserPersonalDetails], + ); + + const sections = useMemo(() => { + const participants = transaction?.participants ?? []; + const participantOptions = + [currentUserOption, ...participants] + ?.filter((participant) => Boolean(participant.accountID)) + ?.map((participant) => OptionsListUtils.getParticipantsOption(participant, personalDetails)) ?? []; + return [ + { + title: '', + data: participantOptions.map((participantOption) => ({ + ...participantOption, + isSelected: !!transaction?.splitPayerAccountIDs && transaction?.splitPayerAccountIDs?.includes(participantOption.accountID ?? 0), + })), + }, + ]; + }, [transaction?.participants, personalDetails, transaction?.splitPayerAccountIDs, currentUserOption]); + + const navigateBack = () => { + Navigation.goBack(backTo); + }; + + const setSplitPayer = (item: Participant | OptionData) => { + IOU.setSplitPayer(transactionID, item.accountID ?? 0); + navigateBack(); + }; + + return ( + + + + ); +} + +IOURequestStepSplitPayer.displayName = 'IOURequestStepSplitPayer'; + +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepSplitPayerWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepSplitPayer); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepSplitPayerWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepSplitPayerWithWritableReportOrNotFound); + +export default IOURequestStepSplitPayerWithFullTransactionOrNotFound; diff --git a/src/pages/iou/request/step/IOURequestStepTag.tsx b/src/pages/iou/request/step/IOURequestStepTag.tsx index a62720cbd13a..ff1a1c01600d 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.tsx +++ b/src/pages/iou/request/step/IOURequestStepTag.tsx @@ -77,7 +77,7 @@ function IOURequestStepTag({ const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - const shouldShowTag = ReportUtils.isGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); + const shouldShowTag = ReportUtils.isReportInGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = !shouldShowTag || (isEditing && (isSplitBill ? !canEditSplitBill : reportAction && !canEditMoneyRequest(reportAction))); diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx index 5abb0cfdaabb..5576c3dedb8a 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -96,7 +96,7 @@ function IOURequestStepWaypoint({ // If the user is online, and they are trying to save a value without using the autocomplete, show an error message instructing them to use a selected address instead. // That enables us to save the address with coordinates when it is selected if (!isOffline && waypointValue !== '' && waypointAddress !== waypointValue) { - ErrorUtils.addErrorMessage(errors, `waypoint${pageIndex}`, 'distance.errors.selectSuggestedAddress'); + ErrorUtils.addErrorMessage(errors, `waypoint${pageIndex}`, 'distance.error.selectSuggestedAddress'); } return errors; @@ -204,7 +204,7 @@ function IOURequestStepWaypoint({ ref={(e: HTMLElement | null) => { textInput.current = e as unknown as TextInput; }} - hint={!isOffline ? 'distance.errors.selectSuggestedAddress' : ''} + hint={!isOffline ? 'distance.error.selectSuggestedAddress' : ''} containerStyles={[styles.mt4]} label={translate('distance.address')} defaultValue={waypointAddress} diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index e29ee52f32a7..68712b730115 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -33,7 +33,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE | typeof SCREENS.MONEY_REQUEST.STEP_SCAN - | typeof SCREENS.MONEY_REQUEST.STEP_CURRENCY; + | typeof SCREENS.MONEY_REQUEST.STEP_CURRENCY + | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM; type Route = RouteProp; diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 81a6ded4e7a3..4a020ee8d411 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -32,7 +32,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS | typeof SCREENS.MONEY_REQUEST.STEP_MERCHANT | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT - | typeof SCREENS.MONEY_REQUEST.STEP_SCAN; + | typeof SCREENS.MONEY_REQUEST.STEP_SCAN + | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM; type Route = RouteProp; diff --git a/src/pages/iouReportPropTypes.js b/src/pages/iouReportPropTypes.js deleted file mode 100644 index 284f0915dbbc..000000000000 --- a/src/pages/iouReportPropTypes.js +++ /dev/null @@ -1,18 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** The report ID of the IOU */ - reportID: PropTypes.string, - - /** The report ID of the chat associated with the IOU */ - chatReportID: PropTypes.string, - - /** The total amount in cents */ - total: PropTypes.number, - - /** The owner of the IOUReport */ - ownerAccountID: PropTypes.number, - - /** The currency of the IOUReport */ - currency: PropTypes.string, -}); diff --git a/src/pages/nextStepPropTypes.js b/src/pages/nextStepPropTypes.js deleted file mode 100644 index 4bf4d265ddef..000000000000 --- a/src/pages/nextStepPropTypes.js +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types'; - -const messagePropType = PropTypes.shape({ - text: PropTypes.string, - type: PropTypes.string, - action: PropTypes.string, -}); - -export default PropTypes.shape({ - /** The message parts of the next step */ - message: PropTypes.arrayOf(messagePropType), - - /** The title for the next step */ - title: PropTypes.string, - - /** Whether the user should take some sort of action in order to unblock the report */ - requiresUserAction: PropTypes.bool, - - /** The type of next step */ - type: PropTypes.oneOf(['neutral', 'alert', null]), - - /** If the "Undo submit" button should be visible */ - showUndoSubmit: PropTypes.bool, - - /** Deprecated - If the next step should be displayed on mobile, related to OldApp */ - showForMobile: PropTypes.bool, - - /** If the next step should be displayed at the expense level */ - showForExpense: PropTypes.bool, - - /** An optional alternate message to display on expenses instead of what is provided in the "message" field */ - expenseMessage: PropTypes.arrayOf(messagePropType), - - /** The next person in the approval chain of the report */ - nextReceiver: PropTypes.string, - - /** An array of buttons to be displayed next to the next step */ - buttons: PropTypes.objectOf( - PropTypes.shape({ - text: PropTypes.string, - tooltip: PropTypes.string, - disabled: PropTypes.bool, - hidden: PropTypes.bool, - // eslint-disable-next-line react/forbid-prop-types - data: PropTypes.object, - }), - ), -}); diff --git a/src/pages/personalDetailsPropType.js b/src/pages/personalDetailsPropType.js deleted file mode 100644 index 37cab8e56abd..000000000000 --- a/src/pages/personalDetailsPropType.js +++ /dev/null @@ -1,53 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - // First name of the current user from their personal details - firstName: PropTypes.string, - - // Last name of the current user from their personal details - lastName: PropTypes.string, - - // Display name of the current user from their personal details - displayName: PropTypes.string, - - // Avatar URL of the current user from their personal details - avatar: PropTypes.string, - - // Flag to set when Avatar uploading - avatarUploading: PropTypes.bool, - - // accountID of the current user from their personal details - accountID: PropTypes.number, - - // login of the current user from their personal details - login: PropTypes.string, - - // pronouns of the current user from their personal details - pronouns: PropTypes.string, - - // local currency for the user - localCurrencyCode: PropTypes.string, - - // timezone of the current user from their personal details - timezone: PropTypes.shape({ - // Value of selected timezone - selected: PropTypes.string, - - // Whether timezone is automatically set - // TODO: remove string type after backend fix - // Some personal details return 'true' (string) for this value instead of true (boolean) - automatic: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - }), - - // custom status - status: PropTypes.shape({ - // The emoji code of the draft status - emojiCode: PropTypes.string, - - // The text of the draft status - text: PropTypes.string, - - // The timestamp of when the status should be cleared - clearAfter: PropTypes.string, // ISO 8601 format - }), -}); diff --git a/src/pages/policyMemberPropType.js b/src/pages/policyMemberPropType.js deleted file mode 100644 index 22a4d355fbfb..000000000000 --- a/src/pages/policyMemberPropType.js +++ /dev/null @@ -1,15 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** Role of the user in the policy */ - role: PropTypes.string, - - /** - * Errors from api calls on the specific user - * {: 'error message', : 'error message 2'} - */ - errors: PropTypes.objectOf(PropTypes.string), - - /** Is this action pending? */ - pendingAction: PropTypes.string, -}); diff --git a/src/pages/reportMetadataPropTypes.js b/src/pages/reportMetadataPropTypes.js deleted file mode 100644 index 65ed01952977..000000000000 --- a/src/pages/reportMetadataPropTypes.js +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** Are we loading newer report actions? */ - isLoadingNewerReportActions: PropTypes.bool, - - /** Are we loading older report actions? */ - isLoadingOlderReportActions: PropTypes.bool, - - /** Flag to check if the report actions data are loading */ - isLoadingInitialReportActions: PropTypes.bool, -}); diff --git a/src/pages/safeAreaInsetPropTypes.js b/src/pages/safeAreaInsetPropTypes.js deleted file mode 100644 index 9b301463fcae..000000000000 --- a/src/pages/safeAreaInsetPropTypes.js +++ /dev/null @@ -1,10 +0,0 @@ -import PropTypes from 'prop-types'; - -const safeAreaInsetPropTypes = PropTypes.shape({ - top: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, -}); - -export default safeAreaInsetPropTypes; diff --git a/src/pages/settings/Preferences/PriorityModePage.tsx b/src/pages/settings/Preferences/PriorityModePage.tsx index 677d3813acd7..2a4cd196f177 100644 --- a/src/pages/settings/Preferences/PriorityModePage.tsx +++ b/src/pages/settings/Preferences/PriorityModePage.tsx @@ -1,5 +1,5 @@ import React, {useCallback} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -12,7 +12,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PriorityMode} from '@src/types/onyx'; type PriorityModeItem = { value: ValueOf; @@ -22,15 +21,9 @@ type PriorityModeItem = { isSelected: boolean; }; -type PriorityModePageOnyxProps = { - /** The chat priority mode */ - priorityMode: PriorityMode; -}; - -type PriorityModePageProps = PriorityModePageOnyxProps; - -function PriorityModePage({priorityMode}: PriorityModePageProps) { +function PriorityModePage() { const {translate} = useLocalize(); + const [priorityMode] = useOnyx(ONYXKEYS.NVP_PRIORITY_MODE, {selector: (mode) => mode ?? CONST.PRIORITY_MODE.DEFAULT}); const styles = useThemeStyles(); const priorityModes = Object.values(CONST.PRIORITY_MODE).map((mode) => ({ value: mode, @@ -73,8 +66,4 @@ function PriorityModePage({priorityMode}: PriorityModePageProps) { PriorityModePage.displayName = 'PriorityModePage'; -export default withOnyx({ - priorityMode: { - key: ONYXKEYS.NVP_PRIORITY_MODE, - }, -})(PriorityModePage); +export default PriorityModePage; diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx index a031cf6363f4..1d1d6583ffa8 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx @@ -15,11 +15,10 @@ import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as CardUtils from '@libs/CardUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import type {PublicScreensParamList} from '@libs/Navigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as CardSettings from '@userActions/Card'; import CONST from '@src/CONST'; @@ -34,7 +33,7 @@ type ActivatePhysicalCardPageOnyxProps = { cardList: OnyxEntry>; }; -type ActivatePhysicalCardPageProps = ActivatePhysicalCardPageOnyxProps & StackScreenProps; +type ActivatePhysicalCardPageProps = ActivatePhysicalCardPageOnyxProps & StackScreenProps; const LAST_FOUR_DIGITS_LENGTH = 4; const MAGIC_INPUT_MIN_HEIGHT = 86; @@ -42,7 +41,7 @@ const MAGIC_INPUT_MIN_HEIGHT = 86; function ActivatePhysicalCardPage({ cardList, route: { - params: {domain = ''}, + params: {cardID = ''}, }, }: ActivatePhysicalCardPageProps) { const theme = useTheme(); @@ -55,10 +54,8 @@ function ActivatePhysicalCardPage({ const [lastFourDigits, setLastFourDigits] = useState(''); const [lastPressedDigit, setLastPressedDigit] = useState(''); - const domainCards = CardUtils.getDomainCards(cardList)[domain] ?? []; - const physicalCard = domainCards.find((card) => !card.nameValuePairs?.isVirtual); - const cardID = physicalCard?.cardID ?? 0; - const cardError = ErrorUtils.getLatestErrorMessage(physicalCard ?? {}); + const inactiveCard = cardList?.[cardID]; + const cardError = ErrorUtils.getLatestErrorMessage(inactiveCard ?? {}); const activateCardCodeInputRef = useRef(null); @@ -66,19 +63,21 @@ function ActivatePhysicalCardPage({ * If state of the card is CONST.EXPENSIFY_CARD.STATE.OPEN, navigate to card details screen. */ useEffect(() => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (physicalCard?.isLoading || cardList?.[cardID]?.state !== CONST.EXPENSIFY_CARD.STATE.OPEN) { + if (inactiveCard?.state !== CONST.EXPENSIFY_CARD.STATE.OPEN || inactiveCard?.isLoading) { return; } - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain)); - }, [cardID, cardList, domain, physicalCard?.isLoading]); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID)); + }, [cardID, cardList, inactiveCard?.isLoading, inactiveCard?.state]); useEffect( () => () => { - CardSettings.clearCardListErrors(cardID); + if (!inactiveCard?.cardID) { + return; + } + CardSettings.clearCardListErrors(inactiveCard?.cardID); }, - [cardID], + [inactiveCard?.cardID], ); /** @@ -95,8 +94,8 @@ function ActivatePhysicalCardPage({ const onCodeInput = (text: string) => { setFormError(''); - if (cardError) { - CardSettings.clearCardListErrors(cardID); + if (cardError && inactiveCard?.cardID) { + CardSettings.clearCardListErrors(inactiveCard?.cardID); } setLastFourDigits(text); @@ -109,18 +108,21 @@ function ActivatePhysicalCardPage({ setFormError('activateCardPage.error.thatDidntMatch'); return; } + if (inactiveCard?.cardID === undefined) { + return; + } - CardSettings.activatePhysicalExpensifyCard(lastFourDigits, cardID); - }, [lastFourDigits, cardID]); + CardSettings.activatePhysicalExpensifyCard(lastFourDigits, inactiveCard?.cardID); + }, [lastFourDigits, inactiveCard?.cardID]); - if (isEmptyObject(physicalCard)) { + if (isEmptyObject(inactiveCard)) { return ; } return ( Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID))} backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.PREFERENCES.ROOT].backgroundColor} illustration={LottieAnimations.Magician} scrollViewContainerStyles={[styles.mnh100]} @@ -148,7 +150,7 @@ function ActivatePhysicalCardPage({