diff --git a/.github/actions/composite/setupGitForOSBotify/action.yml b/.github/actions/composite/setupGitForOSBotify/action.yml
index adf90789976c..0c216e400248 100644
--- a/.github/actions/composite/setupGitForOSBotify/action.yml
+++ b/.github/actions/composite/setupGitForOSBotify/action.yml
@@ -2,6 +2,9 @@ name: 'Setup Git for OSBotify'
description: 'Setup Git for OSBotify'
inputs:
+ OP_VAULT:
+ description: 1Password vault where OSBotify GPG key can be found
+ required: true
OP_SERVICE_ACCOUNT_TOKEN:
description: "1Password service account token"
required: true
@@ -16,7 +19,7 @@ runs:
shell: bash
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.OP_SERVICE_ACCOUNT_TOKEN }}
- run: op read "op://${{ vars.OP_VAULT }}/OSBotify-private-key.asc/OSBotify-private-key.asc" --force --out-file ./OSBotify-private-key.asc
+ run: op read "op://${{ inputs.OP_VAULT }}/OSBotify-private-key.asc/OSBotify-private-key.asc" --force --out-file ./OSBotify-private-key.asc
- name: Import OSBotify GPG Key
shell: bash
diff --git a/.github/actions/composite/setupGitForOSBotifyApp/action.yml b/.github/actions/composite/setupGitForOSBotifyApp/action.yml
index 559bff9a648b..246f9d29e6ac 100644
--- a/.github/actions/composite/setupGitForOSBotifyApp/action.yml
+++ b/.github/actions/composite/setupGitForOSBotifyApp/action.yml
@@ -5,6 +5,9 @@ name: "Setup Git for OSBotify"
description: "Setup Git for OSBotify"
inputs:
+ OP_VAULT:
+ description: 1Password vault where OSBotify GPG key can be found
+ required: true
OP_SERVICE_ACCOUNT_TOKEN:
description: "1Password service account token"
required: true
@@ -31,7 +34,7 @@ runs:
shell: bash
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.OP_SERVICE_ACCOUNT_TOKEN }}
- run: op read "op://${{ vars.OP_VAULT }}/OSBotify-private-key.asc/OSBotify-private-key.asc" --force --out-file ./OSBotify-private-key.asc
+ run: op read "op://${{ inputs.OP_VAULT }}/OSBotify-private-key.asc/OSBotify-private-key.asc" --force --out-file ./OSBotify-private-key.asc
- name: Import OSBotify GPG Key
shell: bash
diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml
index 5816de115e86..19882cac0148 100644
--- a/.github/workflows/cherryPick.yml
+++ b/.github/workflows/cherryPick.yml
@@ -27,6 +27,7 @@ jobs:
id: setupGitForOSBotify
uses: ./.github/actions/composite/setupGitForOSBotifyApp
with:
+ OP_VAULT: ${{ vars.OP_VAULT }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }}
OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }}
diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml
index 34fea70f9dac..ca1f377196df 100644
--- a/.github/workflows/createNewVersion.yml
+++ b/.github/workflows/createNewVersion.yml
@@ -64,6 +64,7 @@ jobs:
uses: ./.github/actions/composite/setupGitForOSBotify
id: setupGitForOSBotify
with:
+ OP_VAULT: ${{ vars.OP_VAULT }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
- name: Generate new E/App version
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index e0d8d0db7119..1973c58ac4b6 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -32,6 +32,7 @@ jobs:
uses: ./.github/actions/composite/setupGitForOSBotifyApp
id: setupGitForOSBotify
with:
+ OP_VAULT: ${{ vars.OP_VAULT }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }}
OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }}
@@ -377,6 +378,7 @@ jobs:
run: |
op read "op://${{ vars.OP_VAULT }}/NewApp_AppStore/NewApp_AppStore.mobileprovision" --force --out-file ./NewApp_AppStore.mobileprovision
op read "op://${{ vars.OP_VAULT }}/NewApp_AppStore_Notification_Service/NewApp_AppStore_Notification_Service.mobileprovision" --force --out-file ./NewApp_AppStore_Notification_Service.mobileprovision
+ op read "op://${{ vars.OP_VAULT }}/NewApp_AppStore_Share_Extension/NewApp_AppStore_Share_Extension.mobileprovision" --force --out-file ./NewApp_AppStore_Share_Extension.mobileprovision
op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12
op read "op://${{ vars.OP_VAULT }}/ios-fastlane-json-key.json/ios-fastlane-json-key.json" --force --out-file ./ios-fastlane-json-key.json
diff --git a/.github/workflows/finishReleaseCycle.yml b/.github/workflows/finishReleaseCycle.yml
index ca030e95de1d..ef24ba98fd61 100644
--- a/.github/workflows/finishReleaseCycle.yml
+++ b/.github/workflows/finishReleaseCycle.yml
@@ -22,6 +22,7 @@ jobs:
uses: ./.github/actions/composite/setupGitForOSBotifyApp
id: setupGitForOSBotify
with:
+ OP_VAULT: ${{ vars.OP_VAULT }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }}
OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }}
@@ -87,6 +88,7 @@ jobs:
id: setupGitForOSBotify
uses: ./.github/actions/composite/setupGitForOSBotifyApp
with:
+ OP_VAULT: ${{ vars.OP_VAULT }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }}
OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }}
@@ -128,6 +130,7 @@ jobs:
- name: Setup git for OSBotify
uses: ./.github/actions/composite/setupGitForOSBotifyApp
with:
+ OP_VAULT: ${{ vars.OP_VAULT }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }}
OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }}
diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml
index 10ca10882464..8f9c00f314ca 100644
--- a/.github/workflows/preDeploy.yml
+++ b/.github/workflows/preDeploy.yml
@@ -104,6 +104,7 @@ jobs:
- name: Setup Git for OSBotify
uses: ./.github/actions/composite/setupGitForOSBotifyApp
with:
+ OP_VAULT: ${{ vars.OP_VAULT }}
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
OS_BOTIFY_APP_ID: ${{ secrets.OS_BOTIFY_APP_ID }}
OS_BOTIFY_PRIVATE_KEY: ${{ secrets.OS_BOTIFY_PRIVATE_KEY }}
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index e6af1475d604..fc337b868392 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -186,7 +186,7 @@ jobs:
run: |
op read "op://${{ vars.OP_VAULT }}/NewApp_AdHoc/NewApp_AdHoc.mobileprovision" --force --out-file ./NewApp_AdHoc.mobileprovision
op read "op://${{ vars.OP_VAULT }}/NewApp_AdHoc_Notification_Service/NewApp_AdHoc_Notification_Service.mobileprovision" --force --out-file ./NewApp_AdHoc_Notification_Service.mobileprovision
- op read "op://${{ vars.OP_VAULT }}/NewApp_AdHoc_Share_Extension.mobileprovision/NewApp_AdHoc_Share_Extension.mobileprovision" --force --out-file ./NewApp_AdHoc_Share_Extension.mobileprovision
+ op read "op://${{ vars.OP_VAULT }}/NewApp_AdHoc_Share_Extension/NewApp_AdHoc_Share_Extension.mobileprovision" --force --out-file ./NewApp_AdHoc_Share_Extension.mobileprovision
op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12
- name: Configure AWS Credentials
diff --git a/Mobile-Expensify b/Mobile-Expensify
index 5aa866093c9e..c92e59b1cc08 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 5aa866093c9eed80a527ede55f8926c4c22a947d
+Subproject commit c92e59b1cc08497d1b00adce8fe471cbfe23caa0
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 34dc321c4b64..f2e22693e65b 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009009600
- versionName "9.0.96-0"
+ versionCode 1009009800
+ versionName "9.0.98-0"
// 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/simple-illustrations/simple-illustration__creditcards--green.svg b/assets/images/simple-illustrations/simple-illustration__creditcards--green.svg
new file mode 100644
index 000000000000..bd74430af00a
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__creditcards--green.svg
@@ -0,0 +1,55 @@
+
+
\ No newline at end of file
diff --git a/contributingGuides/APPLE_GOOGLE_SIGNIN.md b/contributingGuides/APPLE_GOOGLE_SIGNIN.md
index cc3e256be399..dbbd7a564d7b 100644
--- a/contributingGuides/APPLE_GOOGLE_SIGNIN.md
+++ b/contributingGuides/APPLE_GOOGLE_SIGNIN.md
@@ -157,7 +157,7 @@ index 4286a26033..850f8944ca 100644
index ca2da6f56b..2c191598f0 100644
--- a/src/libs/Navigation/linkingConfig/prefixes.ts
+++ b/src/libs/Navigation/linkingConfig/prefixes.ts
- @@ -8,6 +8,7 @@ const prefixes: LinkingOptions['prefixes'] = [
+ @@ -8,6 +8,7 @@ const prefixes: LinkingOptions['prefixes'] = [
'https://www.expensify.cash',
'https://staging.expensify.cash',
'https://dev.new.expensify.com',
diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md
index 6c0a5b460654..f0db6723b4e5 100644
--- a/contributingGuides/NAVIGATION.md
+++ b/contributingGuides/NAVIGATION.md
@@ -207,4 +207,4 @@ The action for the first step created with `getMinimalAction` looks like this:
```
### Deeplinking
-There is no minimal action for deeplinking directly to the `Profile` screen. But because the `Settings_root` is not on the stack, pressing UP will reset the params for navigators to the correct ones.
+There is no minimal action for deeplinking directly to the `Profile` screen. But because the `Settings_root` is not on the stack, pressing UP will reset the params for navigators to the correct ones.
\ No newline at end of file
diff --git a/contributingGuides/NAVIGATION_TESTS.md b/contributingGuides/NAVIGATION_TESTS.md
new file mode 100644
index 000000000000..adb98e79712e
--- /dev/null
+++ b/contributingGuides/NAVIGATION_TESTS.md
@@ -0,0 +1,270 @@
+# Navigation tests
+
+#### There should be a proper report under attachment screen after reload
+
+1. Open any report with image attachment on narrow layout.
+2. Open attachment.
+3. Reload the page.
+4. Verify that after pressing back arrow in the header you are on the report where you sent the attachment.
+
+
+#### There is a proper split navigator under RHP with a sidebar screen only for screens that can be opened from the sidebar
+
+1. Open the browser on narrow layout with url `/settings/profile/status`.
+2. Reload the page.
+3. Verify that after pressing back arrow in the header you are on the settings root page.
+
+
+#### There is a proper split navigator under the overlay after refreshing page with RHP/LHP on wide screen
+
+1. Open the browser on wide screen with url `/settings/profile/display-name`.
+2. Verify that you can see settings profile page under the overlay of RHP.
+
+
+#### There is a proper split navigator under the overlay after deeplinking to page with RHP/LHP on wide screen
+
+1. Open the browser on wide screen.
+2. Open any report.
+3. Send message with url `/settings/profile/display-name`.
+4. Press the sent link
+5. Verify that the settings profile screen is now visible under the overlay
+
+#### The Workspace list page is displayed (SCREENS.SETTINGS.WORKSPACES) after clicking the Settings tab from the Workspace settings screen
+
+1. Open any workspace settings (Settings → Workspaces → Select any workspace)
+2. Click the Settings button on the bottom tab.
+3. Verify that the Workspace list is displayed (`/settings/workspaces`)
+4. Select any workspace again.
+5. Reload the page.
+6. Click the Settings button on the bottom tab.
+7. Verify that the Workspace list is displayed (`/settings/workspaces`)
+
+
+#### The last visited screen in the settings tab is saved when switching between tabs
+
+1. Open the app.
+2. Go to the settings tab.
+3. Open the workspace list.
+4. Select any workspace.
+5. Switch between tabs and open the settings tabs again.
+6. Verify that the last visited page in this tab is displayed.
+
+
+#### The Workspace selected in the application is reset when you select a chat that does not belong to the current policy
+
+1. Open the home page.
+2. Click on the Expensify icon in the upper left corner.
+3. Select any workspace.
+4. Click on the magnifying glass above the list of available chats.
+5. Select a chat that does not belong to the workspace selected in the third step.
+6. Verify if the chat is opened and the global workspace is selected.
+
+
+#### The selected workspace is saved between Search and Inbox tabs
+
+1. Open the Inbox tab.
+2. Change the workspace using the workspace switcher.
+3. Switch to the Search tab and verify if the workspace selected in the second step is also selected in the Search.
+4. Change the workspace once again.
+5. Go back to the Inbox.
+6. Verify if the workspace selected in the fourth step is also selected in the Inbox tab.
+
+#### Going up to the workspace list page after refreshing on the workspace settings and pressing the up button
+
+1. Open the workspace settings from the deep link (use a link in format: `/settings/workspaces/:policyID:/profile`)
+2. Click the app’s back button.
+3. Verify if the workspace list is displayed.
+
+#### Going up to the RHP screen provided in the backTo parameter in the url
+
+1. Open the settings tab.
+2. Go to the Profile page.
+3. Click the Address button.
+4. Click the Country button.
+5. Reload the page.
+6. Click the app’s back button.
+7. Verify if the Profile address page is displayed (`/settings/profile/address`)
+
+#### There is proper split navigator under the overlay after refreshing page in RHP that includes valid reportID in params
+
+wide layout :
+
+1. Open any report.
+2. Open report details (press the chat header).
+3. Reload the app.
+4. Verify that the report under the overlay is the same as the one opened in report details.
+
+narrow layout :
+
+1. Open any report
+2. Open report details (press the chat header).
+3. Reload the app.
+4. Verify that after pressing back arrow in the header you are on the report previously seen in the details page.
+
+#### Navigating back to the Workspace Switcher from the created workspace
+
+1. Open the app and go to the Inbox tab.
+2. Open the workspace switcher (Click on the button in the upper left corner).
+3. Create a new workspace by clicking on the + button.
+4. Navigate back using the back button in the app.
+5. Verify if the workspace switcher is displayed with the report screen below it
+
+#### Going up to the sidebar screen
+
+Linked issue: https://github.com/Expensify/App/pull/44138
+
+1. Go to Subscription page in the settings tab.
+2. Click on Request refund button
+3. Verify that modal shown
+4. Next click Downgrade...
+5. Verify that modal got closed, your account is downgraded and the Home page is opened.
+
+#### Navigating back from the Search page with invalid query parameters
+
+1. Open the search page with invalid query parameters (e.g `/search?q=from%3a`)
+2. Press the app's back button on the not found page.
+3. Verify that the Search page with default query parameters is displayed.
+
+#### Navigating to the chat from the link in the thread
+
+1. Open any chat.
+2. If there are no messages in the chat, send a message.
+3. Press reply in thread.
+4. Press the "From" link in the displayed header.
+5. Verify if the link correctly redirects to the chat opened in the first step.
+
+#### Expense - App does not open destination report after submitting expense
+
+Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2432400819
+
+1. Launch the app.
+2. Open FAB > Submit expense > Manual.
+3. Submit a manual expense to any user (as long as the user is not the currrently opened report and the receiver is not workspace chat).
+4. Verify if the destination report is opened after submitting expense.
+
+#### QBO - Preferred exporter/Export date tab do not auto-close after value selected
+
+Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433342220
+
+Precondition: Workspace with QBO integration connected.
+
+1. Go to Workspace > Accounting.
+2. Click on Export > Preferred exporter (or Export date).
+3. Click on value.
+4. Verify if the value chosen in the third step is selected and the app redirects to the Export page.
+
+#### Web - Hold - App flickers after entering reason and saving it when holding expense
+
+Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433389682
+
+1. Launch the app.
+2. Open DM with any user.
+3. Submit two expenses to them.
+4. Click on the expense preview to go to expense report.
+5. Click on any preview to go to transaction thread.
+6. Go back to expense report.
+7. Right click on the expense preview in Step 5 > Hold.
+8. Enter a reason and save it.
+9. Verify if the app does not flicker after entering reason and saving it.
+
+#### Group - App returns to group settings page after saving group name
+
+Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433381800
+
+1. Launch the app.
+2. Create a group chat.
+3. Go to group chat.
+4. Click on the group chat header.
+5. Click Group name field.
+6. Click Save.
+7. Verify if the app returs to group details RHP after saving group name.
+
+#### Going up to a screen with any params
+
+Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2432694948
+
+1. Press the FAB.
+2. Select "Book travel".
+3. Press "Book travel" in the new RHP pane.
+4. Press "Country".
+5. Select any country.
+6. Verify that the country you selected is actually visible in the form.
+
+#### Change params of existing attachments screens instead of pushing new screen on the stack
+
+Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2432360626
+
+1. Open any chat.
+2. Send at least two images.
+3. Open attachment by pressing on image.
+4. Press arrow on the side of attachment modal to navigate to the second image.
+5. Close the modal with X in the corner.
+6. Verify that the modal is now fully closed.
+
+#### Navigate instead of push for reports with same reportID
+
+Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433351709
+
+1. Open app on wide layout web.
+2. Go to report A (any report).
+3. Go to report B (any report with message).
+4. Press reply in thread.
+5. Press on header subtitle.
+6. Press on the report B in the sidebar.
+7. Verify that the message you replied to is no longer highlighted.
+8. Press the browsers back button.
+9. Verify that you are on the A report.
+
+
+#### Don't push the default full screen route if not necessary.
+
+1. Open app on wide layout web.
+2. Open search tab.
+3. Press track expense.
+4. Verify that the split navigator hasn't changed under the overlay.
+
+#### BA - Back button on connect bank account modal opens incorporation state modal
+
+Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433261611
+
+Precondition: Use staging server (it can be set in Settings >> Troubleshoot)
+
+1. Launch the app.
+2. Navigate to Settings >> Workspaces >> Workspace >> Workflows.
+3. Select Connect with Plaid option.
+4. Go through the Plaid flow (Added Wells Fargo details).
+5. Complete the Personal info, Company info & agreements section.
+6. Note user redirected to page with the header Connect bank account and the option to disconnect your now set up bank account.
+7. Tap back button on connect bank account modal.
+8. Verify if the connect bank account modal is closed and the Workflows page is opened with the bank account added.
+
+#### App opens room details page when tapping RHP back button after saving Private notes in DM
+
+Linked issue: https://github.com/Expensify/App/pull/49539#issuecomment-2433321607
+
+1. Launch the app.
+2. Open DM with any user that does not have content in Private notes.
+3. Click on the chat header.
+4. Click Private notes.
+5. Enter anything and click Save.
+6. Click on the RHP back button.
+7. Verify if the Profile RHP Page is opened (URL in the format /a/:accountID).
+
+#### Opening particular onboarding pages from a link and going back
+
+Linked issue: https://github.com/Expensify/App/issues/50177
+
+1. Sign in as a new user.
+2. Select Something else from the onboarding flow.
+3. Reopen/refresh the app.
+4. Verify the Personal detail step is shown.
+5. Go back.
+6. Verify you are navigated back to the Purpose step.
+7. Select Manage my team.
+8. Choose the employee size.
+9. Reopen/refresh the app.
+10. Verify the connection integration step is shown.
+11. Go back.
+12. Verify you are navigated back to the employee size step.
+13. Go back.
+14. Verify you are navigated back to the Purpose step.
\ No newline at end of file
diff --git a/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md
index ae2c790bb3ae..9a9e4b1ceec4 100644
--- a/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md
+++ b/docs/articles/expensify-classic/expensify-billing/Billing-Overview.md
@@ -1,86 +1,52 @@
---
-title: Billing Overview
-description: Learn about Expensify billing, including active member charges, annual subscription savings, and pay-per-use options. Discover how the Expensify Card can reduce costs and maximize value.
+title: Billing and Subscriptions
+description: Expensify Billing Overview
---
-Expensify’s billing is based on monthly member activity. You’ll be charged for the previous month’s usage at the beginning of each month. Your bill depends on:
-- Plan type: Annual subscription or pay-per-use.
-- Expensify Visa® Commercial Card usage: Discounts are available based on your card spending.
+At the beginning of each month, the workspace's Billing Owner is billed for the previous month’s activity.
+Your Expensify bill is determined by the following:
+- The number of active members in your workspace
+- Whether you have a Collect or Control plan
+- Whether you’re on pay-per-use or an annual subscription
+- Whether you’re using the Expensify Visa® Commercial Card
----
-# How Billing Works
-- Billing occurs on the first of each month for the previous month’s usage.
-- Only Group Workspace owners are billed.
-- View billing receipts in:
- Settings > Account > Payments > Billing History.
+An active member is any member who creates, submits, approves, reimburses, or exports a report in Expensify in a given month. This includes Copilots and automatic actions by Concierge.
-**Tip: Designate one billing owner for all Group Workspaces to streamline billing management.**
+Your billing receipts can be viewed under **Settings** > **Account** > **Payments** > **Billing History**. We recommend appointing a single billing owner for each Group Workspace.
----
-## What is an Active Member?
-An active member is anyone who performs any of these actions in Expensify during a month:
-- Chats
-- Creates, submits, approves, reimburses, or exports a report
-- Uses the Copilot feature to take an action in another user's account
+# Save Money on Your Expensify Bill
----
-# Annual Subscription
-
-## Key Benefits:
-- Save 50% per active member compared to pay-per-use billing.
- - Collect plan: $10 per member (vs. $20).
- - Control plan: $18 per member (vs. $36).
-- Set your monthly active member count upfront and pay a fixed rate.
-
-## How It Works:
-- You’ll be billed for the number of members set in your subscription.
-- Extra active members beyond your subscription size are charged at the pay-per-use rate.
-
-**Example:**
-- Plan: Control
-- Subscription size: 10 members
- - Cost: $18/member x 10 members = $180/month
-- Scenario: 12 active members in one month
- - Cost for additional two members: $36/member = $72
- - Total bill: $252
-
-**Adjustments:**
-- You can increase your subscription size by extending your subscription period.
-- Reductions are only allowed after your current subscription ends.
----
-# Pay-Per-Use Billing
-- Charges apply at full rates with no discounts.
- - Collect plan: $20 per active member.
- - Control plan: $36 per active member.
+## Annual Subscription + Expensify Card
----
-# How the Expensify Card Reduces Your Bill
+Save the most money on Expensify by pairing an annual subscription with the Expensify Visa® Commercial Card. Then, if at least 50% of your total settled US spend in a given month is on the Expensify Card, you’ll pay the best possible price for Expensify:
-## Bundling Benefits:
-- Combine an Expensify Card with an annual subscription for the lowest price per member.
-- Spending at least 50% of your total settled US spend on Expensify Cards earns a further 50% discount.
+- **Collect Plan:** $5 per active member per month
+- **Control Plan:** $9 per active member per month
-## Discount Breakdown:
-- Collect plan: $5/member.
-- Control plan: $9/member.
+**You also get cash back!** Earn 1% cash back on all Expensify Card purchases or 2% if card spending reaches $250,000 or more monthly (for US purchases only). Cash back first applies to your Expensify bill, with any remainder deposited directly into your bank account.
-## Additional Savings:
-- Earn 1% cash back on Expensify Card purchases.
- - 2% cash back if total card spend exceeds $250k (US purchases only).
- - Cashback is first applied to your bill, reducing costs further. Any surplus is deposited into your bank account.
+Use Expensify’s [savings calculator](https://use.expensify.com/resource-center/tools/savings-calculator) to see your potential savings with the Expensify Card.
-## Savings Calculator
-Use our [savings calculator](https://use.expensify.com/price-savings-calculator) to estimate potential savings and earnings with the Expensify Card. Enter your details to see the results!
+## Annual Subscription vs Pay-per-use
----
-# FAQ
+**Annual Subscription**
+
+You get a 50% discount with an annual subscription:
+
+- **Collect Plan:** $10 per active member per month
+- **Control Plan:** $18 per active member per month
+
+If your active members exceed your subscription size, additional members are billed at the pay-per-use rate for that month. You can increase your subscription size at any time (extending your annual term) but can only reduce it once your current subscription period ends.
+
+**Pay-per-use**
+
+Rates for pay-per-use plans are applied at full price:
+
+- **Collect plan:** $20 per active member per month
+- **Control plan:** $36 per active member per month
-## What if less than 50% of the spend is on Expensify Cards?
-Discounts are applied on a sliding scale based on your Expensify Card spend percentage.
+# Transfer Ownership of Billing
-**Example:**
-- Annual subscription discount: 50%
-- Expensify Card spend (US purchases): 20% of total spend
-- Expensify Card discount: 20%
-- Total savings: 70% discount on the per-member price for that month.
+If another member needs to own the billing on a given workspace, they must first be a Workspace Admin. Then, from their account, the Admin will head to **Settings > Workspaces > [Workspace Name] > Members > [Current Workspace Owner] > Transfer Ownership**.
+From there, the new billing owner will add a payment card and be charged for the member activity on the workspace on the first of the month.
diff --git a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md
index 23c1bc58e5fc..943879e1ca8a 100644
--- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md
+++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md
@@ -172,6 +172,10 @@ Yes, you can [split an expense](https://help.expensify.com/articles/new-expensif
Yes, you can edit an expense until it is paid. When an expense is submitted, the details can be edited except for the amount and date.
+**Can I add multiple receipts to a newly created expense?**
+
+Yes, you can add multiple receipt images to a new expense. One possible way to do this is to combine all receipts into a single PDF file. From there, simply upload the combined PDF to the expense.
+
**What are expense reports?**
In Expensify, expense reports group expenses in a batch to be paid or reconciled. When a draft report is open, all new expenses are added to it.
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index b9930ca92324..0c1a74e35f67 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -353,6 +353,10 @@ platform :ios do
path: "./NewApp_AppStore_Notification_Service.mobileprovision"
)
+ install_provisioning_profile(
+ path: "./NewApp_AppStore_Share_Extension.mobileprovision"
+ )
+
build_app(
workspace: "./ios/NewExpensify.xcworkspace",
scheme: "New Expensify",
@@ -361,6 +365,7 @@ platform :ios do
provisioningProfiles: {
"com.chat.expensify.chat" => "(NewApp) AppStore",
"com.chat.expensify.chat.NotificationServiceExtension" => "(NewApp) AppStore: Notification Service",
+ "com.chat.expensify.chat.ShareViewController" => "(NewApp) AppStore: Share Extension",
},
manageAppVersionAndBuildNumber: false
}
@@ -485,6 +490,10 @@ platform :ios do
path: "./NewApp_AdHoc_Notification_Service.mobileprovision"
)
+ install_provisioning_profile(
+ path: "./NewApp_AdHoc_Share_Extension.mobileprovision"
+ )
+
build_app(
workspace: "./ios/NewExpensify.xcworkspace",
skip_profile_detection: true,
@@ -495,6 +504,7 @@ platform :ios do
provisioningProfiles: {
"com.expensify.chat.adhoc" => "(NewApp) AdHoc",
"com.expensify.chat.adhoc.NotificationServiceExtension" => "(NewApp) AdHoc: Notification Service",
+ "com.expensify.chat.adhoc.ShareViewController" => "(NewApp) AdHoc: Share Extension",
},
manageAppVersionAndBuildNumber: false
}
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 273195e3d3ef..39ce30e2ad64 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -23,7 +23,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 9.0.96
+ 9.0.98CFBundleSignature????CFBundleURLTypes
@@ -44,7 +44,7 @@
CFBundleVersion
- 9.0.96.0
+ 9.0.98.0FullStoryOrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index efc2b7c9e3d3..8b582bea0735 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 9.0.96
+ 9.0.98CFBundleSignature????CFBundleVersion
- 9.0.96.0
+ 9.0.98.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 86725f46a824..90b593948766 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 9.0.96
+ 9.0.98CFBundleVersion
- 9.0.96.0
+ 9.0.98.0NSExtensionNSExtensionPointIdentifier
diff --git a/jest.config.js b/jest.config.js
index 13645e720c8e..ea9a23f01362 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -5,6 +5,7 @@ module.exports = {
`/tests/ui/**/*.${testFileExtension}`,
`/tests/unit/**/*.${testFileExtension}`,
`/tests/actions/**/*.${testFileExtension}`,
+ `/tests/navigation/**/*.${testFileExtension}`,
`/?(*.)+(spec|test).${testFileExtension}`,
],
transform: {
diff --git a/package-lock.json b/package-lock.json
index ef6566356d88..66f776456fe3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.96-0",
+ "version": "9.0.98-0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.96-0",
+ "version": "9.0.98-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 864de1116a38..ff13d99660ae 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.96-0",
+ "version": "9.0.98-0",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/patches/@react-navigation+stack+6.3.29+002+dontDetachScreen.patch b/patches/@react-navigation+stack+6.3.29+002+dontDetachScreen.patch
index c65ebbb98007..44e6d591b748 100644
--- a/patches/@react-navigation+stack+6.3.29+002+dontDetachScreen.patch
+++ b/patches/@react-navigation+stack+6.3.29+002+dontDetachScreen.patch
@@ -43,7 +43,7 @@ index 7558eb3..b7bb75e 100644
}) : STATE_TRANSITIONING_OR_BELOW_TOP;
}
+
-+ const isHomeScreenAndNotOnTop = (route.name === 'BottomTabNavigator' || route.name === 'Workspace_Initial') && isScreenActive !== STATE_ON_TOP;
++ const shouldNotDetachScreen = route?.dontDetachScreen && isScreenActive !== STATE_ON_TOP;
+
const {
headerShown = true,
@@ -53,7 +53,7 @@ index 7558eb3..b7bb75e 100644
style: StyleSheet.absoluteFill,
enabled: detachInactiveScreens,
- active: isScreenActive,
-+ active: isHomeScreenAndNotOnTop ? STATE_TRANSITIONING_OR_BELOW_TOP : isScreenActive,
++ active: shouldNotDetachScreen ? STATE_TRANSITIONING_OR_BELOW_TOP : isScreenActive,
freezeOnBlur: freezeOnBlur,
pointerEvents: "box-none"
}, /*#__PURE__*/React.createElement(CardContainer, {
@@ -62,7 +62,7 @@ index 7558eb3..b7bb75e 100644
onTransitionEnd: onTransitionEnd,
isNextScreenTransparent: isNextScreenTransparent,
- detachCurrentScreen: detachCurrentScreen
-+ detachCurrentScreen: isHomeScreenAndNotOnTop ? false : detachCurrentScreen,
++ detachCurrentScreen: shouldNotDetachScreen ? false : detachCurrentScreen,
}));
})), isFloatHeaderAbsolute ? floatingHeader : null);
}
diff --git a/src/App.tsx b/src/App.tsx
index 3513cb23953b..f3d37fe87c0b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,12 +6,12 @@ import {PickerStateProvider} from 'react-native-picker-select';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import '../wdyr';
import ActiveElementRoleProvider from './components/ActiveElementRoleProvider';
-import ActiveWorkspaceContextProvider from './components/ActiveWorkspaceProvider';
import ColorSchemeWrapper from './components/ColorSchemeWrapper';
import ComposeProviders from './components/ComposeProviders';
import CustomStatusBarAndBackground from './components/CustomStatusBarAndBackground';
import CustomStatusBarAndBackgroundContextProvider from './components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContextProvider';
import ErrorBoundary from './components/ErrorBoundary';
+import FullScreenBlockingViewContextProvider from './components/FullScreenBlockingViewContextProvider';
import HTMLEngineProvider from './components/HTMLEngineProvider';
import InitialURLContextProvider from './components/InitialURLContextProvider';
import {InputBlurContextProvider} from './components/InputBlurContext';
@@ -36,7 +36,6 @@ import CONFIG from './CONFIG';
import Expensify from './Expensify';
import {CurrentReportIDContextProvider} from './hooks/useCurrentReportID';
import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop';
-import {ReportIDsContextProvider} from './hooks/useReportIDs';
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext';
import type {Route} from './ROUTES';
@@ -90,8 +89,6 @@ function App({url}: AppProps) {
EnvironmentProvider,
CustomStatusBarAndBackgroundContextProvider,
ActiveElementRoleProvider,
- ActiveWorkspaceContextProvider,
- ReportIDsContextProvider,
PlaybackContextProvider,
FullScreenContextProvider,
VolumeContextProvider,
@@ -101,6 +98,7 @@ function App({url}: AppProps) {
SearchRouterContextProvider,
ProductTrainingContextProvider,
InputBlurContextProvider,
+ FullScreenBlockingViewContextProvider,
]}
>
diff --git a/src/CONST.ts b/src/CONST.ts
index e747ea4afa33..55f0dafd8517 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -7,6 +7,7 @@ import invertBy from 'lodash/invertBy';
import Config from 'react-native-config';
import * as KeyCommand from 'react-native-key-command';
import type {ValueOf} from 'type-fest';
+import type ResponsiveLayoutResult from './hooks/useResponsiveLayout/types';
import type {Video} from './libs/actions/Report';
import type {MileageRate} from './libs/DistanceRequestUtils';
import BankAccount from './libs/models/BankAccount';
@@ -4816,13 +4817,15 @@ const CONST = {
SF_COORDINATES: [-122.4194, 37.7749],
NAVIGATION: {
- TYPE: {
- UP: 'UP',
- },
ACTION_TYPE: {
REPLACE: 'REPLACE',
PUSH: 'PUSH',
NAVIGATE: 'NAVIGATE',
+
+ /** These action types are custom for RootNavigator */
+ SWITCH_POLICY_ID: 'SWITCH_POLICY_ID',
+ DISMISS_MODAL: 'DISMISS_MODAL',
+ OPEN_WORKSPACE_SPLIT: 'OPEN_WORKSPACE_SPLIT',
},
},
TIME_PERIOD: {
@@ -6635,6 +6638,22 @@ const CONST = {
SCAN_TEST_TOOLTIP: 'scanTestTooltip',
},
SMART_BANNER_HEIGHT: 152,
+
+ NAVIGATION_TESTS: {
+ DEFAULT_PARENT_ROUTE: {key: 'parentRouteKey', name: 'ParentNavigator'},
+ DEFAULT_USE_RESPONSIVE_LAYOUT_VALUE: {
+ shouldUseNarrowLayout: true,
+ isSmallScreenWidth: true,
+ isInNarrowPaneModal: false,
+ isExtraSmallScreenHeight: false,
+ isMediumScreenWidth: false,
+ isLargeScreenWidth: false,
+ isExtraSmallScreenWidth: false,
+ isSmallScreen: false,
+ onboardingIsMediumOrLargerScreenWidth: false,
+ } as ResponsiveLayoutResult,
+ },
+
TRAVEL: {
DEFAULT_DOMAIN: 'domain',
PROVISIONING: {
diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts
index b7c7a71c2828..6014ab22e953 100644
--- a/src/NAVIGATORS.ts
+++ b/src/NAVIGATORS.ts
@@ -4,7 +4,6 @@
* */
export default {
CENTRAL_PANE_NAVIGATOR: 'CentralPaneNavigator',
- BOTTOM_TAB_NAVIGATOR: 'BottomTabNavigator',
LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator',
RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator',
ONBOARDING_MODAL_NAVIGATOR: 'OnboardingModalNavigator',
@@ -12,5 +11,7 @@ export default {
WELCOME_VIDEO_MODAL_NAVIGATOR: 'WelcomeVideoModalNavigator',
EXPLANATION_MODAL_NAVIGATOR: 'ExplanationModalNavigator',
MIGRATED_USER_MODAL_NAVIGATOR: 'MigratedUserModalNavigator',
- FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator',
+ REPORTS_SPLIT_NAVIGATOR: 'ReportsSplitNavigator',
+ SETTINGS_SPLIT_NAVIGATOR: 'SettingsSplitNavigator',
+ WORKSPACE_SPLIT_NAVIGATOR: 'WorkspaceSplitNavigator',
} as const;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 61f2054c57e3..b98717b51f5d 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -152,16 +152,12 @@ const ROUTES = {
SETTINGS_ADD_DELEGATE: 'settings/security/delegate',
SETTINGS_DELEGATE_ROLE: {
route: 'settings/security/delegate/:login/role/:role',
- getRoute: (login: string, role?: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}` as const,
+ getRoute: (login: string, role?: string, backTo?: string) => getUrlWithBackToParam(`settings/security/delegate/${encodeURIComponent(login)}/role/${role}`, backTo),
},
SETTINGS_UPDATE_DELEGATE_ROLE: {
route: 'settings/security/delegate/:login/update-role/:currentRole',
getRoute: (login: string, currentRole: string) => `settings/security/delegate/${encodeURIComponent(login)}/update-role/${currentRole}` as const,
},
- SETTINGS_UPDATE_DELEGATE_ROLE_MAGIC_CODE: {
- route: 'settings/security/delegate/:login/update-role/:role/magic-code',
- getRoute: (login: string, role: string) => `settings/security/delegate/${encodeURIComponent(login)}/update-role/${role}/magic-code` as const,
- },
SETTINGS_DELEGATE_CONFIRM: {
route: 'settings/security/delegate/:login/role/:role/confirm',
getRoute: (login: string, role: string, showValidateActionModal?: boolean) => {
@@ -199,15 +195,15 @@ const ROUTES = {
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: {
route: 'settings/wallet/card/:domain/get-physical/name',
- getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/name` as const,
+ getRoute: (domain: string, backTo?: string) => getUrlWithBackToParam(`settings/wallet/card/${domain}/get-physical/name`, backTo),
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE: {
route: 'settings/wallet/card/:domain/get-physical/phone',
- getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/phone` as const,
+ getRoute: (domain: string, backTo?: string) => getUrlWithBackToParam(`settings/wallet/card/${domain}/get-physical/phone`, backTo),
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS: {
route: 'settings/wallet/card/:domain/get-physical/address',
- getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/address` as const,
+ getRoute: (domain: string, backTo?: string) => getUrlWithBackToParam(`settings/wallet/card/${domain}/get-physical/address`, backTo),
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_CONFIRM: {
route: 'settings/wallet/card/:domain/get-physical/confirm',
@@ -692,7 +688,7 @@ const ROUTES = {
},
MONEY_REQUEST_STEP_PARTICIPANTS: {
route: ':action/:iouType/participants/:transactionID/:reportID',
- getRoute: (iouType: IOUType, transactionID: string | undefined, reportID: string, backTo = '', action: IOUAction = 'create') =>
+ getRoute: (iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', action: IOUAction = 'create') =>
getUrlWithBackToParam(`${action as string}/${iouType as string}/participants/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_SPLIT_PAYER: {
@@ -1254,7 +1250,7 @@ const ROUTES = {
},
WORKSPACE_EDIT_TAGS: {
route: 'settings/workspaces/:policyID/tags/:orderWeight/edit',
- getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tags/${orderWeight}/edit` as const,
+ getRoute: (policyID: string, orderWeight: number, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/tags/${orderWeight}/edit` as const, backTo),
},
WORKSPACE_TAG_EDIT: {
route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/edit',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index e1f4e505b42b..19fcf8b4a367 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -36,7 +36,7 @@ const SCREENS = {
PUBLIC_DOMAIN_ERROR: 'Travel_PublicDomainError',
},
SEARCH: {
- CENTRAL_PANE: 'Search_Central_Pane',
+ ROOT: 'Search_Root',
REPORT_RHP: 'Search_Report_RHP',
ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP',
ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP',
@@ -61,7 +61,6 @@ const SCREENS = {
SAVED_SEARCH_RENAME_RHP: 'Search_Saved_Search_Rename_RHP',
ADVANCED_FILTERS_IN_RHP: 'Search_Advanced_Filters_In_RHP',
TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP',
- BOTTOM_TAB: 'Search_Bottom_Tab',
},
SETTINGS: {
ROOT: 'Settings_Root',
@@ -150,7 +149,6 @@ const SCREENS = {
DELEGATE_ROLE: 'Settings_Delegate_Role',
DELEGATE_CONFIRM: 'Settings_Delegate_Confirm',
UPDATE_DELEGATE_ROLE: 'Settings_Delegate_Update_Role',
- UPDATE_DELEGATE_ROLE_MAGIC_CODE: 'Settings_Delegate_Update_Magic_Code',
},
},
SAVE_THE_WORLD: {
diff --git a/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx b/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx
index 466f0f492c8e..140c21ff8dd4 100644
--- a/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx
+++ b/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx
@@ -1,11 +1,14 @@
import {createContext} from 'react';
type ActiveWorkspaceContextType = {
- activeWorkspaceID?: string;
- setActiveWorkspaceID: (activeWorkspaceID?: string) => void;
+ activeWorkspaceID: string | undefined;
+ setActiveWorkspaceID: (workspaceID: string | undefined) => void;
};
-const ActiveWorkspaceContext = createContext({activeWorkspaceID: undefined, setActiveWorkspaceID: () => undefined});
+const ActiveWorkspaceContext = createContext({
+ activeWorkspaceID: undefined,
+ setActiveWorkspaceID: () => {},
+});
export default ActiveWorkspaceContext;
-export {type ActiveWorkspaceContextType};
+export type {ActiveWorkspaceContextType};
diff --git a/src/components/ActiveWorkspaceProvider/index.tsx b/src/components/ActiveWorkspaceProvider/index.tsx
index bc7260cdf10b..98a72e770638 100644
--- a/src/components/ActiveWorkspaceProvider/index.tsx
+++ b/src/components/ActiveWorkspaceProvider/index.tsx
@@ -1,16 +1,27 @@
-import React, {useMemo, useState} from 'react';
+import {useNavigationState} from '@react-navigation/native';
+import React, {useEffect, useMemo, useState} from 'react';
import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext';
+import getPolicyIDFromState from '@libs/Navigation/helpers/getPolicyIDFromState';
+import type {RootNavigatorParamList, State} from '@libs/Navigation/types';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
function ActiveWorkspaceContextProvider({children}: ChildrenProps) {
- const [activeWorkspaceID, setActiveWorkspaceID] = useState(undefined);
+ const policyID = useNavigationState((state) => getPolicyIDFromState(state as State));
+
+ const [activeWorkspaceID, setActiveWorkspaceID] = useState(policyID);
+
+ useEffect(() => {
+ setActiveWorkspaceID(policyID);
+ }, [policyID]);
const value = useMemo(
() => ({
activeWorkspaceID,
+
+ // We are exporting setActiveWorkspace to speed up updating this value after changing activeWorkspaceID to avoid flickering of workspace avatar.
setActiveWorkspaceID,
}),
- [activeWorkspaceID, setActiveWorkspaceID],
+ [activeWorkspaceID],
);
return {children};
diff --git a/src/components/ActiveWorkspaceProvider/index.website.tsx b/src/components/ActiveWorkspaceProvider/index.website.tsx
deleted file mode 100644
index 82e46d70f896..000000000000
--- a/src/components/ActiveWorkspaceProvider/index.website.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import React, {useCallback, useMemo, useState} from 'react';
-import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext';
-import CONST from '@src/CONST';
-import type ChildrenProps from '@src/types/utils/ChildrenProps';
-
-function ActiveWorkspaceContextProvider({children}: ChildrenProps) {
- const [activeWorkspaceID, updateActiveWorkspaceID] = useState(undefined);
-
- const setActiveWorkspaceID = useCallback((workspaceID: string | undefined) => {
- updateActiveWorkspaceID(workspaceID);
- if (workspaceID && sessionStorage) {
- sessionStorage?.setItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID, workspaceID);
- } else {
- sessionStorage?.removeItem(CONST.SESSION_STORAGE_KEYS.ACTIVE_WORKSPACE_ID);
- }
- }, []);
-
- const value = useMemo(
- () => ({
- activeWorkspaceID,
- setActiveWorkspaceID,
- }),
- [activeWorkspaceID, setActiveWorkspaceID],
- );
-
- return {children};
-}
-
-export default ActiveWorkspaceContextProvider;
diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx
index 4286c6c834eb..e8da2b56cab9 100644
--- a/src/components/AttachmentModal.tsx
+++ b/src/components/AttachmentModal.tsx
@@ -294,8 +294,8 @@ function AttachmentModal({
const deleteAndCloseModal = useCallback(() => {
detachReceipt(transaction?.transactionID);
setIsDeleteReceiptConfirmModalVisible(false);
- Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID));
- }, [transaction, report]);
+ Navigation.goBack();
+ }, [transaction]);
const isValidFile = useCallback(
(fileObject: FileObject) =>
@@ -435,9 +435,11 @@ function AttachmentModal({
text: translate('common.replace'),
onSelected: () => {
closeModal(true);
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, report?.reportID, Navigation.getActiveRouteWithoutParams()),
- );
+ Navigation.isNavigationReady().then(() => {
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, report?.reportID, Navigation.getActiveRouteWithoutParams()),
+ );
+ });
},
});
}
diff --git a/src/components/BlockingViews/ForceFullScreenView/index.tsx b/src/components/BlockingViews/ForceFullScreenView/index.tsx
index 8a02028168fa..f83c35aa038c 100644
--- a/src/components/BlockingViews/ForceFullScreenView/index.tsx
+++ b/src/components/BlockingViews/ForceFullScreenView/index.tsx
@@ -1,10 +1,24 @@
-import React from 'react';
+import {useRoute} from '@react-navigation/native';
+import React, {useContext, useEffect} from 'react';
import {View} from 'react-native';
+import {FullScreenBlockingViewContext} from '@components/FullScreenBlockingViewContextProvider';
import useThemeStyles from '@hooks/useThemeStyles';
import type ForceFullScreenViewProps from './types';
function ForceFullScreenView({children, shouldForceFullScreen = false}: ForceFullScreenViewProps) {
+ const route = useRoute();
const styles = useThemeStyles();
+ const {addRouteKey, removeRouteKey} = useContext(FullScreenBlockingViewContext);
+
+ useEffect(() => {
+ if (!shouldForceFullScreen) {
+ return;
+ }
+
+ addRouteKey(route.key);
+
+ return () => removeRouteKey(route.key);
+ }, [addRouteKey, removeRouteKey, route, shouldForceFullScreen]);
if (shouldForceFullScreen) {
return {children};
diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx
index 7c4ce77c8a99..d751d8bc666b 100644
--- a/src/components/BlockingViews/FullPageNotFoundView.tsx
+++ b/src/components/BlockingViews/FullPageNotFoundView.tsx
@@ -63,7 +63,7 @@ function FullPageNotFoundView({
onBackButtonPress = () => Navigation.goBack(),
shouldShowLink = true,
shouldShowBackButton = true,
- onLinkPress = () => Navigation.dismissModal(),
+ onLinkPress = () => Navigation.goBackToHome(),
shouldForceFullScreen = false,
subtitleStyle,
shouldDisplaySearchRouter,
diff --git a/src/components/DeeplinkWrapper/index.website.tsx b/src/components/DeeplinkWrapper/index.website.tsx
index 158bf43b2dfe..da344514040e 100644
--- a/src/components/DeeplinkWrapper/index.website.tsx
+++ b/src/components/DeeplinkWrapper/index.website.tsx
@@ -1,9 +1,9 @@
import {Str} from 'expensify-common';
import {useEffect, useRef, useState} from 'react';
import {isMobile} from '@libs/Browser';
+import shouldPreventDeeplinkPrompt from '@libs/Navigation/helpers/shouldPreventDeeplinkPrompt';
import Navigation from '@libs/Navigation/Navigation';
import navigationRef from '@libs/Navigation/navigationRef';
-import shouldPreventDeeplinkPrompt from '@libs/Navigation/shouldPreventDeeplinkPrompt';
import {beginDeepLinkRedirect, beginDeepLinkRedirectAfterTransition} from '@userActions/App';
import {getInternalNewExpensifyPath} from '@userActions/Link';
import {isAnonymousUser} from '@userActions/Session';
diff --git a/src/components/DelegateNoAccessWrapper.tsx b/src/components/DelegateNoAccessWrapper.tsx
index c49d15890112..f810b60d8ca9 100644
--- a/src/components/DelegateNoAccessWrapper.tsx
+++ b/src/components/DelegateNoAccessWrapper.tsx
@@ -23,6 +23,7 @@ type DelegateNoAccessWrapperProps = {
accessDeniedVariants?: AccessDeniedVariants[];
shouldForceFullScreen?: boolean;
children?: (() => React.ReactNode) | React.ReactNode;
+ onBackButtonPress?: () => void;
};
function isDelegate(account: OnyxEntry) {
@@ -35,7 +36,7 @@ function isSubmitter(account: OnyxEntry) {
return isDelegateOnlySubmitter;
}
-function DelegateNoAccessWrapper({accessDeniedVariants = [], shouldForceFullScreen, ...props}: DelegateNoAccessWrapperProps) {
+function DelegateNoAccessWrapper({accessDeniedVariants = [], shouldForceFullScreen, onBackButtonPress, ...props}: DelegateNoAccessWrapperProps) {
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const isPageAccessDenied = accessDeniedVariants.reduce((acc, variant) => {
const accessDeniedFunction = DENIED_ACCESS_VARIANTS[variant];
@@ -49,6 +50,10 @@ function DelegateNoAccessWrapper({accessDeniedVariants = [], shouldForceFullScre
shouldShow
shouldForceFullScreen={shouldForceFullScreen}
onBackButtonPress={() => {
+ if (onBackButtonPress) {
+ onBackButtonPress();
+ return;
+ }
if (shouldUseNarrowLayout) {
Navigation.dismissModal();
return;
diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx
index 5c9263e911b0..32c86a0af476 100644
--- a/src/components/FeatureTrainingModal.tsx
+++ b/src/components/FeatureTrainingModal.tsx
@@ -1,6 +1,6 @@
import type {VideoReadyForDisplayEvent} from 'expo-av';
import type {ImageContentFit} from 'expo-image';
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useCallback, useEffect, useLayoutEffect, useState} from 'react';
import {InteractionManager, View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
@@ -10,6 +10,7 @@ import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
+import {parseFSAttributes} from '@libs/Fullstory';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import {dismissTrackTrainingModal} from '@userActions/User';
@@ -264,6 +265,14 @@ function FeatureTrainingModal({
onConfirm?.();
}, [onConfirm, closeModal]);
+ /**
+ * Extracts values from the non-scraped attribute WEB_PROP_ATTR at build time
+ * to ensure necessary properties are available for further processing.
+ * Reevaluates "fs-class" to dynamically apply styles or behavior based on
+ * updated attribute values.
+ */
+ useLayoutEffect(parseFSAttributes, []);
+
return (
{({safeAreaPaddingBottomStyle}) => (
@@ -286,7 +295,11 @@ function FeatureTrainingModal({
...modalInnerContainerStyle,
}}
>
-
+
{renderIllustration()}
diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx
index 8ca495ede289..687d952c195e 100644
--- a/src/components/FloatingActionButton.tsx
+++ b/src/components/FloatingActionButton.tsx
@@ -6,12 +6,11 @@ import {Platform} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import Svg, {Path} from 'react-native-svg';
-import useBottomTabIsFocused from '@hooks/useBottomTabIsFocused';
-import useIsCurrentRouteHome from '@hooks/useIsCurrentRouteHome';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import getPlatform from '@libs/getPlatform';
+import useIsHomeRouteActive from '@navigation/helpers/useIsHomeRouteActive';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -58,9 +57,12 @@ type FloatingActionButtonProps = {
/* An accessibility role for the button */
role: Role;
+
+ /* If the tooltip is allowed to be shown */
+ isTooltipAllowed: boolean;
};
-function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) {
+function FloatingActionButton({onPress, isActive, accessibilityLabel, role, isTooltipAllowed}: FloatingActionButtonProps, ref: ForwardedRef) {
const {success, buttonDefaultBG, textLight, textDark} = useTheme();
const styles = useThemeStyles();
const borderRadius = styles.floatingActionButton.borderRadius;
@@ -68,13 +70,12 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo
const {shouldUseNarrowLayout} = useResponsiveLayout();
const platform = getPlatform();
const isNarrowScreenOnWeb = shouldUseNarrowLayout && platform === CONST.PLATFORM.WEB;
- const isFocused = useBottomTabIsFocused();
const [isSidebarLoaded] = useOnyx(ONYXKEYS.IS_SIDEBAR_LOADED, {initialValue: false});
- const isActiveRouteHome = useIsCurrentRouteHome();
+ const isHomeRouteActive = useIsHomeRouteActive(shouldUseNarrowLayout);
const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(
CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP,
// On Home screen, We need to wait for the sidebar to load before showing the tooltip because there is the Concierge tooltip which is higher priority
- isFocused && (!isActiveRouteHome || isSidebarLoaded),
+ isTooltipAllowed && (!isHomeRouteActive || isSidebarLoaded),
);
const sharedValue = useSharedValue(isActive ? 1 : 0);
const buttonRef = ref;
diff --git a/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts b/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts
deleted file mode 100644
index f6a4f5ba6e83..000000000000
--- a/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import NAVIGATORS from '@src/NAVIGATORS';
-import SCREENS from '@src/SCREENS';
-
-const BOTTOM_TAB_SCREENS = [SCREENS.HOME, SCREENS.SETTINGS.ROOT, NAVIGATORS.BOTTOM_TAB_NAVIGATOR, SCREENS.SEARCH.BOTTOM_TAB];
-
-export default BOTTOM_TAB_SCREENS;
diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx
index cf7db5442aa8..33f8aa85d6cf 100644
--- a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx
+++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx
@@ -1,11 +1,11 @@
import {useIsFocused, useRoute} from '@react-navigation/native';
import {FocusTrap} from 'focus-trap-react';
import React, {useMemo} from 'react';
-import BOTTOM_TAB_SCREENS from '@components/FocusTrap/BOTTOM_TAB_SCREENS';
import sharedTrapStack from '@components/FocusTrap/sharedTrapStack';
import TOP_TAB_SCREENS from '@components/FocusTrap/TOP_TAB_SCREENS';
import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import {isSidebarScreenName} from '@libs/Navigation/helpers/isNavigatorName';
import CONST from '@src/CONST';
import type FocusTrapProps from './FocusTrapProps';
@@ -18,8 +18,8 @@ function FocusTrapForScreen({children, focusTrapSettings}: FocusTrapProps) {
if (typeof focusTrapSettings?.active !== 'undefined') {
return focusTrapSettings.active;
}
- // Focus trap can't be active on bottom tab screens because it would block access to the tab bar.
- if (BOTTOM_TAB_SCREENS.find((screen) => screen === route.name)) {
+ // Focus trap can't be active on sidebar screens because it would block access to the tab bar.
+ if (isSidebarScreenName(route.name)) {
return false;
}
diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
index 32e063f03109..7941c2c575a9 100644
--- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
+++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts
@@ -1,4 +1,3 @@
-import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
/**
@@ -6,7 +5,6 @@ import SCREENS from '@src/SCREENS';
* focus trap when rendered on a wide screen to allow navigation between them using the keyboard
*/
const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [
- NAVIGATORS.BOTTOM_TAB_NAVIGATOR,
SCREENS.HOME,
SCREENS.SETTINGS.ROOT,
SCREENS.REPORT,
@@ -31,7 +29,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [
SCREENS.WORKSPACE.EXPENSIFY_CARD,
SCREENS.WORKSPACE.COMPANY_CARDS,
SCREENS.WORKSPACE.DISTANCE_RATES,
- SCREENS.SEARCH.CENTRAL_PANE,
+ SCREENS.SEARCH.ROOT,
SCREENS.SETTINGS.TROUBLESHOOT,
SCREENS.SETTINGS.SAVE_THE_WORLD,
SCREENS.WORKSPACE.RULES,
diff --git a/src/components/FullScreenBlockingViewContextProvider.tsx b/src/components/FullScreenBlockingViewContextProvider.tsx
new file mode 100644
index 000000000000..fed3e33412b2
--- /dev/null
+++ b/src/components/FullScreenBlockingViewContextProvider.tsx
@@ -0,0 +1,61 @@
+import React, {createContext, useCallback, useMemo, useState} from 'react';
+
+type FullScreenBlockingViewContextValue = {
+ addRouteKey: (key: string) => void;
+ removeRouteKey: (key: string) => void;
+ isBlockingViewVisible: boolean;
+};
+
+type FullScreenBlockingViewContextProviderProps = {
+ children: React.ReactNode;
+};
+
+const defaultValue: FullScreenBlockingViewContextValue = {
+ addRouteKey: () => {},
+ removeRouteKey: () => {},
+ isBlockingViewVisible: false,
+};
+
+const FullScreenBlockingViewContext = createContext(defaultValue);
+
+/**
+ * Provides a context for getting information about the visibility of a full-screen blocking view.
+ * This context allows the blocking view to add or remove route keys, which determine
+ * whether the blocking view is displayed on a screen. If there are any route keys present,
+ * the blocking view is considered visible.
+ * This information is necessary because we don't want to show the TopLevelBottomTabBar when the blocking view is visible.
+ */
+function FullScreenBlockingViewContextProvider({children}: FullScreenBlockingViewContextProviderProps) {
+ const [routeKeys, setRouteKeys] = useState>(new Set());
+
+ const addRouteKey = useCallback((key: string) => {
+ setRouteKeys((prevKeys) => new Set(prevKeys).add(key));
+ }, []);
+
+ const removeRouteKey = useCallback((key: string) => {
+ setRouteKeys((prevKeys) => {
+ const newKeys = new Set(prevKeys);
+ newKeys.delete(key);
+ return newKeys;
+ });
+ }, []);
+
+ const isBlockingViewVisible = useMemo(() => routeKeys.size > 0, [routeKeys]);
+
+ const contextValue = useMemo(
+ () => ({
+ addRouteKey,
+ removeRouteKey,
+ isBlockingViewVisible,
+ }),
+ [addRouteKey, removeRouteKey, isBlockingViewVisible],
+ );
+
+ return {children};
+}
+
+export default FullScreenBlockingViewContextProvider;
+
+export {FullScreenBlockingViewContext};
+
+export type {FullScreenBlockingViewContextProviderProps, FullScreenBlockingViewContextValue};
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx
index fcae31dd7d2f..1caeaa3fd152 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx
@@ -10,14 +10,12 @@ import Text from '@components/Text';
import useCurrentReportID from '@hooks/useCurrentReportID';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
-import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
-import type {RootStackParamList, State} from '@libs/Navigation/types';
-import Navigation, {navigationRef} from '@navigation/Navigation';
+import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
+import Navigation from '@navigation/Navigation';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
+import ROUTES from '@src/ROUTES';
import type {Report} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import MentionReportContext from './MentionReportContext';
@@ -74,9 +72,8 @@ function MentionReportRenderer({style, tnode, TDefaultRenderer, ...defaultRender
const {reportID, mentionDisplayText} = mentionDetails;
let navigationRoute: Route | undefined = reportID ? ROUTES.REPORT_WITH_ID.getRoute(reportID) : undefined;
- const topmostCentralPaneRoute = getTopmostCentralPaneRoute(navigationRef.getRootState() as State);
const backTo = Navigation.getActiveRoute();
- if (topmostCentralPaneRoute?.name === SCREENS.SEARCH.CENTRAL_PANE) {
+ if (isSearchTopmostFullScreenRoute()) {
navigationRoute = reportID ? ROUTES.SEARCH_REPORT.getRoute({reportID, backTo}) : undefined;
}
const isCurrentRoomMention = reportID === currentReportIDValue;
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index fd5eb9f6fd58..93c233321916 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -89,6 +89,7 @@ import ConciergeBubble from '@assets/images/simple-illustrations/simple-illustra
import ConciergeNew from '@assets/images/simple-illustrations/simple-illustration__concierge.svg';
import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg';
import CreditCardEyes from '@assets/images/simple-illustrations/simple-illustration__creditcardeyes.svg';
+import CreditCardsNewGreen from '@assets/images/simple-illustrations/simple-illustration__creditcards--green.svg';
import EmailAddress from '@assets/images/simple-illustrations/simple-illustration__email-address.svg';
import EmptyState from '@assets/images/simple-illustrations/simple-illustration__empty-state.svg';
import EnvelopeReceipt from '@assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg';
@@ -198,6 +199,7 @@ export {
MoneyReceipts,
PinkBill,
CreditCardsNew,
+ CreditCardsNewGreen,
InvoiceBlue,
LaptopwithSecondScreenandHourglass,
LockOpen,
diff --git a/src/components/ImportOnyxState/index.native.tsx b/src/components/ImportOnyxState/index.native.tsx
index bdd805241c55..e3eca1993313 100644
--- a/src/components/ImportOnyxState/index.native.tsx
+++ b/src/components/ImportOnyxState/index.native.tsx
@@ -1,18 +1,17 @@
import React, {useState} from 'react';
import ReactNativeBlobUtil from 'react-native-blob-util';
-import Onyx, {useOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import type {FileObject} from '@components/AttachmentModal';
-import {KEYS_TO_PRESERVE, setIsUsingImportedState, setPreservedUserSession} from '@libs/actions/App';
+import {setIsUsingImportedState, setPreservedUserSession} from '@libs/actions/App';
import {setShouldForceOffline} from '@libs/actions/Network';
+import {rollbackOngoingRequest} from '@libs/actions/PersistedRequests';
+import {cleanAndTransformState, importState} from '@libs/ImportOnyxStateUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {OnyxValues} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import BaseImportOnyxState from './BaseImportOnyxState';
import type ImportOnyxStateProps from './types';
-import {cleanAndTransformState} from './utils';
-
-const CHUNK_SIZE = 100;
function readOnyxFile(fileUri: string) {
const filePath = decodeURIComponent(fileUri.replace('file://', ''));
@@ -25,27 +24,6 @@ function readOnyxFile(fileUri: string) {
});
}
-function chunkArray(array: T[], size: number): T[][] {
- const result = [];
- for (let i = 0; i < array.length; i += size) {
- result.push(array.slice(i, i + size));
- }
- return result;
-}
-
-function applyStateInChunks(state: OnyxValues) {
- const entries = Object.entries(state);
- const chunks = chunkArray(entries, CHUNK_SIZE);
-
- let promise = Promise.resolve();
- chunks.forEach((chunk) => {
- const partialOnyxState = Object.fromEntries(chunk) as Partial;
- promise = promise.then(() => Onyx.multiSet(partialOnyxState));
- });
-
- return promise;
-}
-
export default function ImportOnyxState({setIsLoading}: ImportOnyxStateProps) {
const [isErrorModalVisible, setIsErrorModalVisible] = useState(false);
const [session] = useOnyx(ONYXKEYS.SESSION);
@@ -58,19 +36,23 @@ export default function ImportOnyxState({setIsLoading}: ImportOnyxStateProps) {
setIsLoading(true);
readOnyxFile(file.uri)
.then((fileContent: string) => {
+ rollbackOngoingRequest();
const transformedState = cleanAndTransformState(fileContent);
const currentUserSessionCopy = {...session};
setPreservedUserSession(currentUserSessionCopy);
setShouldForceOffline(true);
- Onyx.clear(KEYS_TO_PRESERVE).then(() => {
- applyStateInChunks(transformedState).then(() => {
- setIsUsingImportedState(true);
- Navigation.navigate(ROUTES.HOME);
- });
- });
+ return importState(transformedState);
})
- .catch(() => {
+ .then(() => {
+ setIsUsingImportedState(true);
+ Navigation.navigate(ROUTES.HOME);
+ })
+ .catch((error) => {
+ console.error('Error importing state:', error);
setIsErrorModalVisible(true);
+ })
+ .finally(() => {
+ setIsLoading(false);
});
};
diff --git a/src/components/ImportOnyxState/index.tsx b/src/components/ImportOnyxState/index.tsx
index 2f9a2b70b65b..8f199a176539 100644
--- a/src/components/ImportOnyxState/index.tsx
+++ b/src/components/ImportOnyxState/index.tsx
@@ -1,15 +1,16 @@
import React, {useState} from 'react';
-import Onyx, {useOnyx} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
import type {FileObject} from '@components/AttachmentModal';
-import {KEYS_TO_PRESERVE, setIsUsingImportedState, setPreservedUserSession} from '@libs/actions/App';
+import {setIsUsingImportedState, setPreservedUserSession} from '@libs/actions/App';
import {setShouldForceOffline} from '@libs/actions/Network';
+import {rollbackOngoingRequest} from '@libs/actions/PersistedRequests';
+import {cleanAndTransformState, importState} from '@libs/ImportOnyxStateUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {OnyxValues} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import BaseImportOnyxState from './BaseImportOnyxState';
import type ImportOnyxStateProps from './types';
-import {cleanAndTransformState} from './utils';
export default function ImportOnyxState({setIsLoading}: ImportOnyxStateProps) {
const [isErrorModalVisible, setIsErrorModalVisible] = useState(false);
@@ -21,26 +22,33 @@ export default function ImportOnyxState({setIsLoading}: ImportOnyxStateProps) {
}
setIsLoading(true);
+
const blob = new Blob([file as BlobPart]);
const response = new Response(blob);
response
.text()
.then((text) => {
+ rollbackOngoingRequest();
const fileContent = text;
+
const transformedState = cleanAndTransformState(fileContent);
+
const currentUserSessionCopy = {...session};
setPreservedUserSession(currentUserSessionCopy);
setShouldForceOffline(true);
- Onyx.clear(KEYS_TO_PRESERVE).then(() => {
- Onyx.multiSet(transformedState).then(() => {
- setIsUsingImportedState(true);
- Navigation.navigate(ROUTES.HOME);
- });
- });
+
+ return importState(transformedState);
+ })
+ .then(() => {
+ setIsUsingImportedState(true);
+ Navigation.navigate(ROUTES.HOME);
})
- .catch(() => {
+ .catch((error) => {
+ console.error('Error importing state:', error);
setIsErrorModalVisible(true);
+ })
+ .finally(() => {
setIsLoading(false);
});
};
diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx
index 4e6327ec5661..307acae4d5d3 100644
--- a/src/components/KYCWall/BaseKYCWall.tsx
+++ b/src/components/KYCWall/BaseKYCWall.tsx
@@ -106,7 +106,7 @@ function KYCWall({
if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
openPersonalBankAccountSetupView();
} else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) {
- Navigation.navigate(addDebitCardRoute);
+ Navigation.navigate(addDebitCardRoute ?? ROUTES.HOME);
} else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) {
if (iouReport && isIOUReport(iouReport)) {
const {policyID, workspaceChatReportID, reportPreviewReportActionID, adminsChatReportID} = createWorkspaceFromIOUPayment(iouReport) ?? {};
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index 1e7a9f796641..5ab05797a02b 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -16,9 +16,9 @@ import SubscriptAvatar from '@components/SubscriptAvatar';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
-import useIsCurrentRouteHome from '@hooks/useIsCurrentRouteHome';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useRootNavigationState from '@hooks/useRootNavigationState';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -43,6 +43,7 @@ import FreeTrial from '@pages/settings/Subscription/FreeTrial';
import variables from '@styles/variables';
import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
+import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {OptionRowLHNProps} from './types';
@@ -64,16 +65,17 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+');
const isChatUsedForOnboarding = isChatUsedForOnboardingReportUtils(report, introSelected?.choice);
const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? isAdminRoom(report) && isChatUsedForOnboarding : isConciergeChatReport(report);
- const isActiveRouteHome = useIsCurrentRouteHome();
+
+ const isReportsSplitNavigatorLast = useRootNavigationState((state) => state?.routes?.at(-1)?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR);
const {tooltipToRender, shouldShowTooltip} = useMemo(() => {
const tooltip = shouldShowGetStartedTooltip ? CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCEIRGE_LHN_GBR : CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.LHN_WORKSPACE_CHAT_TOOLTIP;
const shouldShowTooltips = shouldShowWokspaceChatTooltip || shouldShowGetStartedTooltip;
- const shouldTooltipBeVisible = shouldUseNarrowLayout ? isScreenFocused && isActiveRouteHome : isActiveRouteHome && !isFullscreenVisible;
+ const shouldTooltipBeVisible = shouldUseNarrowLayout ? isScreenFocused && isReportsSplitNavigatorLast : isReportsSplitNavigatorLast && !isFullscreenVisible;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
return {tooltipToRender: tooltip, shouldShowTooltip: shouldShowTooltips && shouldTooltipBeVisible};
- }, [shouldShowGetStartedTooltip, shouldShowWokspaceChatTooltip, isScreenFocused, shouldUseNarrowLayout, isActiveRouteHome, isFullscreenVisible]);
+ }, [shouldShowGetStartedTooltip, shouldShowWokspaceChatTooltip, isScreenFocused, shouldUseNarrowLayout, isReportsSplitNavigatorLast, isFullscreenVisible]);
const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip);
diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx
index 9fb61ea99f75..5fc99560a27c 100644
--- a/src/components/Lottie/index.tsx
+++ b/src/components/Lottie/index.tsx
@@ -9,7 +9,7 @@ import useAppState from '@hooks/useAppState';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {getBrowser, isMobile} from '@libs/Browser';
-import isSideModalNavigator from '@libs/Navigation/isSideModalNavigator';
+import isSideModalNavigator from '@libs/Navigation/helpers/isSideModalNavigator';
import CONST from '@src/CONST';
import {useSplashScreenStateContext} from '@src/SplashScreenStateContext';
diff --git a/src/components/MigratedUserWelcomeModal.tsx b/src/components/MigratedUserWelcomeModal.tsx
index 312f6e0f77d0..6a1221797bda 100644
--- a/src/components/MigratedUserWelcomeModal.tsx
+++ b/src/components/MigratedUserWelcomeModal.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {useLayoutEffect} from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -6,6 +6,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import {dismissProductTraining} from '@libs/actions/Welcome';
import convertToLTR from '@libs/convertToLTR';
+import {parseFSAttributes} from '@libs/Fullstory';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {FeatureListItem} from './FeatureList';
@@ -36,6 +37,14 @@ function OnboardingWelcomeVideo() {
const StyleUtils = useStyleUtils();
const {shouldUseNarrowLayout} = useResponsiveLayout();
+ /**
+ * Extracts values from the non-scraped attribute WEB_PROP_ATTR at build time
+ * to ensure necessary properties are available for further processing.
+ * Reevaluates "fs-class" to dynamically apply styles or behavior based on
+ * updated attribute values.
+ */
+ useLayoutEffect(parseFSAttributes, []);
+
return (
-
+
{ExpensifyFeatures.map(({translationKey, icon}) => (
isScanRequestUtil(transaction), [transaction]);
+ const isCreateExpenseFlow = transaction?.isFromGlobalCreate && !isPerDiemRequest;
const transactionID = transaction?.transactionID;
const customUnitRateID = getRateID(transaction);
@@ -339,18 +339,6 @@ function MoneyRequestConfirmationList({
const isCategoryRequired = !!policy?.requiresCategory;
- const shouldDisableParticipant = (participant: Participant): boolean => {
- if (isDraftReport(participant.reportID)) {
- return true;
- }
-
- if (!participant.isInvoiceRoom && !participant.isPolicyExpenseChat && !participant.isSelfDM && isOptimisticPersonalDetail(participant.accountID ?? CONST.DEFAULT_NUMBER_ID)) {
- return true;
- }
-
- return false;
- };
-
useEffect(() => {
if (shouldDisplayFieldError && didConfirmSplit) {
setFormError('iou.error.genericSmartscanFailureMessage');
@@ -596,7 +584,7 @@ function MoneyRequestConfirmationList({
return {
...participantOption,
isSelected: false,
- isInteractive: !shouldDisableParticipant(participantOption),
+ isInteractive: false,
rightElement: (
{amount ? convertToDisplayString(amount, iouCurrencyCode) : ''}
@@ -613,7 +601,7 @@ function MoneyRequestConfirmationList({
...participantOption,
tabIndex: -1,
isSelected: false,
- isInteractive: !shouldDisableParticipant(participantOption),
+ isInteractive: false,
rightElement: (
({
...participant,
isSelected: false,
- isInteractive: transaction?.isFromGlobalCreate,
- shouldShowRightIcon: transaction?.isFromGlobalCreate,
+ isInteractive: isCreateExpenseFlow,
+ shouldShowRightIcon: isCreateExpenseFlow,
}));
options.push({
title: translate('common.to'),
@@ -730,7 +718,7 @@ function MoneyRequestConfirmationList({
}
return options;
- }, [isTypeSplit, translate, payeePersonalDetails, getSplitSectionHeader, splitParticipants, selectedParticipants, transaction?.isFromGlobalCreate]);
+ }, [isTypeSplit, translate, payeePersonalDetails, getSplitSectionHeader, splitParticipants, selectedParticipants, isCreateExpenseFlow]);
useEffect(() => {
if (!isDistanceRequest || (isMovingTransactionFromTrackExpense && !isPolicyExpenseChat) || !transactionID) {
@@ -805,11 +793,11 @@ function MoneyRequestConfirmationList({
* Navigate to the participant step
*/
const navigateToParticipantPage = () => {
- if (!transaction?.isFromGlobalCreate) {
+ if (!isCreateExpenseFlow) {
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(CONST.IOU.TYPE.CREATE, transactionID, transaction.reportID));
};
/**
@@ -821,7 +809,7 @@ function MoneyRequestConfirmationList({
return;
}
if (iouType === CONST.IOU.TYPE.INVOICE && !hasInvoicingDetails(policy)) {
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.getRoute(iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.getRoute(iouType, transactionID, reportID, Navigation.getActiveRoute()));
return;
}
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index 0c934ff12500..4e7f271b2cf2 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -373,7 +373,7 @@ function MoneyRequestConfirmationListFooter({
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
disabled={didConfirm}
interactive={!isReadOnly}
@@ -396,7 +396,7 @@ function MoneyRequestConfirmationListFooter({
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
brickRoadIndicator={shouldDisplayDistanceRateError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
disabled={didConfirm}
@@ -472,10 +472,7 @@ function MoneyRequestConfirmationListFooter({
return;
}
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID),
- CONST.NAVIGATION.ACTION_TYPE.PUSH,
- );
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID));
}}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
@@ -577,7 +574,7 @@ function MoneyRequestConfirmationListFooter({
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
interactive
shouldRenderAsHTML
@@ -623,7 +620,7 @@ function MoneyRequestConfirmationListFooter({
if (!transactionID) {
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SUBRATE_EDIT.getRoute(action, iouType, transactionID, reportID, index, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SUBRATE_EDIT.getRoute(action, iouType, transactionID, reportID, index, Navigation.getActiveRoute()));
}}
disabled={didConfirm}
interactive={!isReadOnly}
@@ -768,7 +765,7 @@ function MoneyRequestConfirmationListFooter({
if (!transaction?.transactionID) {
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID, reportID, Navigation.getActiveRoute()));
}}
style={styles.moneyRequestMenuItem}
labelStyle={styles.mt2}
@@ -793,7 +790,7 @@ function MoneyRequestConfirmationListFooter({
if (!transactionID) {
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESTINATION_EDIT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESTINATION_EDIT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
disabled={didConfirm}
interactive={!isReadOnly}
@@ -809,7 +806,7 @@ function MoneyRequestConfirmationListFooter({
if (!transactionID) {
return;
}
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TIME_EDIT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TIME_EDIT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
disabled={didConfirm}
interactive={!isReadOnly}
@@ -832,9 +829,7 @@ function MoneyRequestConfirmationListFooter({
return;
}
- Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
- );
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
/>
))}
diff --git a/src/components/MultiGestureCanvas/usePinchGesture.ts b/src/components/MultiGestureCanvas/usePinchGesture.ts
index 7f5cecc4e949..6f4cf761a0e4 100644
--- a/src/components/MultiGestureCanvas/usePinchGesture.ts
+++ b/src/components/MultiGestureCanvas/usePinchGesture.ts
@@ -104,6 +104,11 @@ const usePinchGesture = ({
.enabled(pinchEnabled)
// The first argument is not used, but must be defined
.onTouchesDown((_evt, state) => {
+ // react-compiler optimization unintentionally make all the callbacks run on the JS thread.
+ // Adding the worklet directive here will make all the callbacks run on UI thread back.
+
+ 'worklet';
+
// We don't want to activate pinch gesture when we are swiping in the pager
if (!shouldDisableTransformationGestures.get()) {
return;
diff --git a/src/components/Navigation/BottomTabBar/BOTTOM_TABS.ts b/src/components/Navigation/BottomTabBar/BOTTOM_TABS.ts
new file mode 100644
index 000000000000..96eeeb5e66ea
--- /dev/null
+++ b/src/components/Navigation/BottomTabBar/BOTTOM_TABS.ts
@@ -0,0 +1,7 @@
+const BOTTOM_TABS = {
+ HOME: 'HOME',
+ SEARCH: 'SEARCH',
+ SETTINGS: 'SETTINGS',
+} as const;
+
+export default BOTTOM_TABS;
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/components/Navigation/BottomTabBar/index.tsx
similarity index 62%
rename from src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
rename to src/components/Navigation/BottomTabBar/index.tsx
index dc76523044f1..f516eb43d04f 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
+++ b/src/components/Navigation/BottomTabBar/index.tsx
@@ -1,41 +1,46 @@
import React, {memo, useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import DebugTabView from '@components/Navigation/DebugTabView';
import {PressableWithFeedback} from '@components/Pressable';
import {useProductTrainingContext} from '@components/ProductTrainingContext';
import type {SearchQueryString} from '@components/Search/types';
import Text from '@components/Text';
import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
-import useBottomTabIsFocused from '@hooks/useBottomTabIsFocused';
import useLocalize from '@hooks/useLocalize';
import {useReportIDs} from '@hooks/useReportIDs';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import clearSelectedText from '@libs/clearSelectedText/clearSelectedText';
import getPlatform from '@libs/getPlatform';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
-import Navigation from '@libs/Navigation/Navigation';
-import type {AuthScreensParamList, RootStackParamList, State} from '@libs/Navigation/types';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
+import {getPreservedSplitNavigatorState} from '@navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState';
+import {isFullScreenName} from '@navigation/helpers/isNavigatorName';
+import Navigation from '@navigation/Navigation';
import navigationRef from '@navigation/navigationRef';
+import type {AuthScreensParamList, RootNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types';
import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar';
import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton';
import variables from '@styles/variables';
import CONST from '@src/CONST';
+import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
-import DebugTabView from './DebugTabView';
+import BOTTOM_TABS from './BOTTOM_TABS';
type BottomTabBarProps = {
- selectedTab: string | undefined;
+ selectedTab: ValueOf;
+ isTooltipAllowed?: boolean;
};
/**
@@ -66,7 +71,7 @@ function handleQueryWithPolicyID(query: SearchQueryString, activePolicyID?: stri
return SearchQueryUtils.buildSearchQueryString(queryJSON);
}
-function BottomTabBar({selectedTab}: BottomTabBarProps) {
+function BottomTabBar({selectedTab, isTooltipAllowed = false}: BottomTabBarProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -74,13 +79,13 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
const {orderedReportIDs} = useReportIDs();
const [user] = useOnyx(ONYXKEYS.USER);
const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
const [chatTabBrickRoad, setChatTabBrickRoad] = useState(undefined);
- const isFocused = useBottomTabIsFocused();
const platform = getPlatform();
const isWebOrDesktop = platform === CONST.PLATFORM.WEB || platform === CONST.PLATFORM.DESKTOP;
const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(
CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.BOTTOM_NAV_INBOX_TOOLTIP,
- selectedTab !== SCREENS.HOME && isFocused,
+ isTooltipAllowed && selectedTab !== BOTTOM_TABS.HOME,
);
useEffect(() => {
setChatTabBrickRoad(getChatTabBrickRoad(activeWorkspaceID, orderedReportIDs));
@@ -89,25 +94,25 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
}, [activeWorkspaceID, orderedReportIDs, reportActions]);
const navigateToChats = useCallback(() => {
- if (selectedTab === SCREENS.HOME) {
+ if (selectedTab === BOTTOM_TABS.HOME) {
return;
}
+
hideProductTrainingTooltip();
- const route = activeWorkspaceID ? (`/w/${activeWorkspaceID}/${ROUTES.HOME}` as Route) : ROUTES.HOME;
- Navigation.navigate(route);
- }, [activeWorkspaceID, selectedTab, hideProductTrainingTooltip]);
+ Navigation.navigate(ROUTES.HOME);
+ }, [hideProductTrainingTooltip, selectedTab]);
const navigateToSearch = useCallback(() => {
- if (selectedTab === SCREENS.SEARCH.BOTTOM_TAB) {
+ if (selectedTab === BOTTOM_TABS.SEARCH) {
return;
}
clearSelectedText();
interceptAnonymousUser(() => {
- const rootState = navigationRef.getRootState() as State;
- const lastSearchRoute = rootState.routes.filter((route) => route.name === SCREENS.SEARCH.CENTRAL_PANE).at(-1);
+ const rootState = navigationRef.getRootState() as State;
+ const lastSearchRoute = rootState.routes.findLast((route) => route.name === SCREENS.SEARCH.ROOT);
if (lastSearchRoute) {
- const {q, ...rest} = lastSearchRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.CENTRAL_PANE];
+ const {q, ...rest} = lastSearchRoute.params as AuthScreensParamList[typeof SCREENS.SEARCH.ROOT];
const cleanedQuery = handleQueryWithPolicyID(q, activeWorkspaceID);
Navigation.navigate(
@@ -126,6 +131,73 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
});
}, [activeWorkspaceID, selectedTab]);
+ /**
+ * The settings tab is related to SettingsSplitNavigator and WorkspaceSplitNavigator.
+ * If the user opens this tab from another tab, it is necessary to check whether it has not been opened before.
+ * If so, all previously opened screens have be pushed to the navigation stack to maintain the order of screens within the tab.
+ * If the user clicks on the settings tab while on this tab, this button should go back to the previous screen within the tab.
+ */
+ const showSettingsPage = useCallback(() => {
+ const rootState = navigationRef.getRootState();
+ const topmostFullScreenRoute = rootState.routes.findLast((route) => isFullScreenName(route.name));
+
+ if (!topmostFullScreenRoute) {
+ return;
+ }
+
+ const lastRouteOfTopmostFullScreenRoute = 'state' in topmostFullScreenRoute ? topmostFullScreenRoute.state?.routes.at(-1) : undefined;
+
+ if (lastRouteOfTopmostFullScreenRoute && lastRouteOfTopmostFullScreenRoute.name === SCREENS.SETTINGS.WORKSPACES && shouldUseNarrowLayout) {
+ Navigation.goBack(ROUTES.SETTINGS);
+ return;
+ }
+
+ if (topmostFullScreenRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) {
+ Navigation.goBack(ROUTES.SETTINGS);
+ return;
+ }
+
+ interceptAnonymousUser(() => {
+ const lastSettingsOrWorkspaceNavigatorRoute = rootState.routes.findLast(
+ (rootRoute) => rootRoute.name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR || rootRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
+ );
+
+ // If there is no settings or workspace navigator route, then we should open the settings navigator.
+ if (!lastSettingsOrWorkspaceNavigatorRoute) {
+ Navigation.navigate(ROUTES.SETTINGS);
+ return;
+ }
+
+ const state = lastSettingsOrWorkspaceNavigatorRoute.state ?? getPreservedSplitNavigatorState(lastSettingsOrWorkspaceNavigatorRoute.key);
+
+ // If there is a workspace navigator route, then we should open the workspace initial screen as it should be "remembered".
+ if (lastSettingsOrWorkspaceNavigatorRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) {
+ const params = state?.routes.at(0)?.params as WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL];
+
+ // Screens of this navigator should always have policyID
+ if (params.policyID) {
+ // This action will put settings split under the workspace split to make sure that we can swipe back to settings split.
+ navigationRef.dispatch({
+ type: CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT,
+ payload: {
+ policyID: params.policyID,
+ },
+ });
+ }
+ return;
+ }
+
+ // If there is settings workspace screen in the settings navigator, then we should open the settings workspaces as it should be "remembered".
+ if (state?.routes?.at(-1)?.name === SCREENS.SETTINGS.WORKSPACES) {
+ Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
+ return;
+ }
+
+ // Otherwise we should simply open the settings navigator.
+ Navigation.navigate(ROUTES.SETTINGS);
+ });
+ }, [shouldUseNarrowLayout]);
+
return (
<>
{!!user?.isDebugModeEnabled && (
@@ -158,7 +230,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
@@ -171,7 +243,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
styles.textSmall,
styles.textAlignCenter,
styles.mt1Half,
- selectedTab === SCREENS.HOME ? styles.textBold : styles.textSupporting,
+ selectedTab === BOTTOM_TABS.HOME ? styles.textBold : styles.textSupporting,
styles.bottomTabBarLabel,
]}
>
@@ -189,7 +261,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
@@ -199,16 +271,19 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) {
styles.textSmall,
styles.textAlignCenter,
styles.mt1Half,
- selectedTab === SCREENS.SEARCH.BOTTOM_TAB ? styles.textBold : styles.textSupporting,
+ selectedTab === BOTTOM_TABS.SEARCH ? styles.textBold : styles.textSupporting,
styles.bottomTabBarLabel,
]}
>
{translate('common.reports')}
-
+
-
+
>
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx b/src/components/Navigation/DebugTabView.tsx
similarity index 100%
rename from src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/DebugTabView.tsx
rename to src/components/Navigation/DebugTabView.tsx
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/components/Navigation/TopBar.tsx
similarity index 100%
rename from src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx
rename to src/components/Navigation/TopBar.tsx
diff --git a/src/components/Navigation/TopLevelBottomTabBar/SCREENS_WITH_BOTTOM_TAB_BAR.ts b/src/components/Navigation/TopLevelBottomTabBar/SCREENS_WITH_BOTTOM_TAB_BAR.ts
new file mode 100644
index 000000000000..87c6da192ad1
--- /dev/null
+++ b/src/components/Navigation/TopLevelBottomTabBar/SCREENS_WITH_BOTTOM_TAB_BAR.ts
@@ -0,0 +1,6 @@
+import {SIDEBAR_TO_SPLIT} from '@libs/Navigation/linkingConfig/RELATIONS';
+import SCREENS from '@src/SCREENS';
+
+const SCREENS_WITH_BOTTOM_TAB_BAR = [...Object.keys(SIDEBAR_TO_SPLIT), SCREENS.SEARCH.ROOT, SCREENS.SETTINGS.WORKSPACES];
+
+export default SCREENS_WITH_BOTTOM_TAB_BAR;
diff --git a/src/components/Navigation/TopLevelBottomTabBar/getIsBottomTabVisibleDirectly.ts b/src/components/Navigation/TopLevelBottomTabBar/getIsBottomTabVisibleDirectly.ts
new file mode 100644
index 000000000000..418f2cb1c3cc
--- /dev/null
+++ b/src/components/Navigation/TopLevelBottomTabBar/getIsBottomTabVisibleDirectly.ts
@@ -0,0 +1,9 @@
+import type {NavigationState} from '@react-navigation/native';
+import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
+
+// Visible directly means not through the overlay. So the full screen (split navigator or search) has to be the last route on the root stack.
+function getIsBottomTabVisibleDirectly(state: NavigationState) {
+ return isFullScreenName(state?.routes.at(-1)?.name);
+}
+
+export default getIsBottomTabVisibleDirectly;
diff --git a/src/components/Navigation/TopLevelBottomTabBar/getIsScreenWithBottomTabFocused.ts b/src/components/Navigation/TopLevelBottomTabBar/getIsScreenWithBottomTabFocused.ts
new file mode 100644
index 000000000000..ad3dbae4905c
--- /dev/null
+++ b/src/components/Navigation/TopLevelBottomTabBar/getIsScreenWithBottomTabFocused.ts
@@ -0,0 +1,14 @@
+import {findFocusedRoute} from '@react-navigation/native';
+import type {NavigationState} from '@react-navigation/native';
+import {isSplitNavigatorName} from '@libs/Navigation/helpers/isNavigatorName';
+import SCREENS_WITH_BOTTOM_TAB_BAR from './SCREENS_WITH_BOTTOM_TAB_BAR';
+
+function getIsScreenWithBottomTabFocused(state: NavigationState) {
+ const focusedRoute = findFocusedRoute(state);
+
+ // We are checking if the focused route is a split navigator because there may be a brief moment where the navigator doesn't have state yet.
+ // That mens we don't have screen with bottom tab focused. This caused glitching.
+ return SCREENS_WITH_BOTTOM_TAB_BAR.includes(focusedRoute?.name ?? '') || isSplitNavigatorName(focusedRoute?.name);
+}
+
+export default getIsScreenWithBottomTabFocused;
diff --git a/src/components/Navigation/TopLevelBottomTabBar/getSelectedTab.ts b/src/components/Navigation/TopLevelBottomTabBar/getSelectedTab.ts
new file mode 100644
index 000000000000..6ea83f6d1817
--- /dev/null
+++ b/src/components/Navigation/TopLevelBottomTabBar/getSelectedTab.ts
@@ -0,0 +1,12 @@
+import type {NavigationState} from '@react-navigation/native';
+import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
+import {FULLSCREEN_TO_TAB} from '@libs/Navigation/linkingConfig/RELATIONS';
+import type {FullScreenName} from '@libs/Navigation/types';
+import NAVIGATORS from '@src/NAVIGATORS';
+
+function getSelectedTab(state: NavigationState) {
+ const topmostFullScreenRoute = state?.routes.findLast((route) => isFullScreenName(route.name));
+ return FULLSCREEN_TO_TAB[(topmostFullScreenRoute?.name as FullScreenName) ?? NAVIGATORS.REPORTS_SPLIT_NAVIGATOR];
+}
+
+export default getSelectedTab;
diff --git a/src/components/Navigation/TopLevelBottomTabBar/index.tsx b/src/components/Navigation/TopLevelBottomTabBar/index.tsx
new file mode 100644
index 000000000000..9da5f63d306b
--- /dev/null
+++ b/src/components/Navigation/TopLevelBottomTabBar/index.tsx
@@ -0,0 +1,71 @@
+import type {ParamListBase} from '@react-navigation/native';
+import React, {useContext, useEffect, useRef, useState} from 'react';
+import {InteractionManager, View} from 'react-native';
+import {FullScreenBlockingViewContext} from '@components/FullScreenBlockingViewContextProvider';
+import BottomTabBar from '@components/Navigation/BottomTabBar';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
+import getIsBottomTabVisibleDirectly from './getIsBottomTabVisibleDirectly';
+import getIsScreenWithBottomTabFocused from './getIsScreenWithBottomTabFocused';
+import getSelectedTab from './getSelectedTab';
+
+type TopLevelBottomTabBarProps = {
+ state: PlatformStackNavigationState;
+};
+
+/**
+ * TopLevelBottomTabBar is displayed when the user can interact with the bottom tab bar.
+ * We hide it when:
+ * 1. The bottom tab bar is not visible.
+ * 2. There is transition between screens with and without the bottom tab bar.
+ * 3. The bottom tab bar is under the overlay.
+ * For cases 2 and 3, local bottom tab bar mounted on the screen will be displayed.
+ */
+function TopLevelBottomTabBar({state}: TopLevelBottomTabBarProps) {
+ const styles = useThemeStyles();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const {paddingBottom} = useStyledSafeAreaInsets();
+ const [isAfterClosingTransition, setIsAfterClosingTransition] = useState(false);
+ const cancelAfterInteractions = useRef | undefined>();
+ const {isBlockingViewVisible} = useContext(FullScreenBlockingViewContext);
+
+ // That means it's visible and it's not covered by the overlay.
+ const isBottomTabVisibleDirectly = getIsBottomTabVisibleDirectly(state);
+ const selectedTab = getSelectedTab(state);
+ const isScreenWithBottomTabFocused = getIsScreenWithBottomTabFocused(state);
+
+ const shouldDisplayBottomBar = shouldUseNarrowLayout ? isScreenWithBottomTabFocused : isBottomTabVisibleDirectly;
+ const isReadyToDisplayBottomBar = isAfterClosingTransition && shouldDisplayBottomBar && !isBlockingViewVisible;
+
+ useEffect(() => {
+ if (!shouldDisplayBottomBar) {
+ // If the bottom tab is not visible, that means there is a screen covering it.
+ // In that case we need to set the flag to true because there will be a transition for which we need to wait.
+ setIsAfterClosingTransition(false);
+ } else {
+ // If the bottom tab should be visible, we want to wait for transition to finish.
+ cancelAfterInteractions.current = InteractionManager.runAfterInteractions(() => {
+ setIsAfterClosingTransition(true);
+ });
+ return () => cancelAfterInteractions.current?.cancel();
+ }
+ }, [shouldDisplayBottomBar]);
+
+ return (
+
+ {/* We are not rendering BottomTabBar conditionally for two reasons
+ 1. It's faster to hide/show it than mount a new when needed.
+ 2. We need to hide tooltips as well if they were displayed. */}
+
+
+ );
+}
+
+TopLevelBottomTabBar.displayName = 'TopLevelBottomTabBar';
+
+export default TopLevelBottomTabBar;
diff --git a/src/components/Navigation/TopLevelBottomTabBar/useIsBottomTabVisibleDirectly.ts b/src/components/Navigation/TopLevelBottomTabBar/useIsBottomTabVisibleDirectly.ts
new file mode 100644
index 000000000000..52b7a215d4e0
--- /dev/null
+++ b/src/components/Navigation/TopLevelBottomTabBar/useIsBottomTabVisibleDirectly.ts
@@ -0,0 +1,10 @@
+import {useNavigationState} from '@react-navigation/native';
+import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
+
+// Visible directly means not through the overlay. So the full screen (split navigator or search) has to be the last route on the root stack.
+function useIsBottomTabVisibleDirectly() {
+ const isBottomTabVisibleDirectly = useNavigationState((state) => isFullScreenName(state?.routes.at(-1)?.name));
+ return isBottomTabVisibleDirectly;
+}
+
+export default useIsBottomTabVisibleDirectly;
diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx
index f9d84fb423bb..3e554db813ce 100644
--- a/src/components/ParentNavigationSubtitle.tsx
+++ b/src/components/ParentNavigationSubtitle.tsx
@@ -47,7 +47,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct
Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID));
if (isVisibleAction) {
// Pop the chat report screen before navigating to the linked report action.
- Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID, parentReportActionID), true);
+ Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID, parentReportActionID));
}
}}
accessibilityLabel={translate('threads.parentNavigationSummary', {reportName, workspaceName})}
diff --git a/src/components/ProductTrainingContext/index.tsx b/src/components/ProductTrainingContext/index.tsx
index 0d8a115f2a4e..820552901452 100644
--- a/src/components/ProductTrainingContext/index.tsx
+++ b/src/components/ProductTrainingContext/index.tsx
@@ -1,4 +1,4 @@
-import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react';
+import React, {createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
@@ -9,6 +9,7 @@ import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import {parseFSAttributes} from '@libs/Fullstory';
import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -174,10 +175,21 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou
};
}, [tooltipName, registerTooltip, unregisterTooltip, shouldShow]);
+ /**
+ * Extracts values from the non-scraped attribute WEB_PROP_ATTR at build time
+ * to ensure necessary properties are available for further processing.
+ * Reevaluates "fs-class" to dynamically apply styles or behavior based on
+ * updated attribute values.
+ */
+ useLayoutEffect(parseFSAttributes, []);
+
const renderProductTrainingTooltip = useCallback(() => {
const tooltip = TOOLTIPS[tooltipName];
return (
-
+ );
+ const targetedReportID = reportID ?? reportAction?.childReportID ?? '';
- if (topmostCentralPaneRoute?.name !== SCREENS.SEARCH.CENTRAL_PANE && isTextHold) {
+ if (!isSearchTopmostFullScreenRoute() && isTextHold) {
changeMoneyRequestHoldStatus(reportAction, ROUTES.REPORT_WITH_ID.getRoute(targetedReportID));
return;
}
diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx
index 638ef0737ed5..ee543765169d 100644
--- a/src/components/ScreenWrapper.tsx
+++ b/src/components/ScreenWrapper.tsx
@@ -14,7 +14,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {isMobile, isMobileWebKit, isSafari} from '@libs/Browser';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
-import type {AuthScreensParamList, RootStackParamList} from '@libs/Navigation/types';
+import type {ReportsSplitNavigatorParamList, RootNavigatorParamList} from '@libs/Navigation/types';
import addViewportResizeListener from '@libs/VisualViewport';
import toggleTestToolsModal from '@userActions/TestTool';
import CONST from '@src/CONST';
@@ -42,6 +42,9 @@ type ScreenWrapperProps = {
/** Returns a function as a child to pass insets to or a node to render without insets */
children: ReactNode | React.FC;
+ /** Content to display under the offline indicator */
+ bottomContent?: ReactNode;
+
/** A unique ID to find the screen wrapper in tests */
testID: string;
@@ -98,7 +101,7 @@ type ScreenWrapperProps = {
*
* This is required because transitionEnd event doesn't trigger in the testing environment.
*/
- navigation?: PlatformStackNavigationProp | PlatformStackNavigationProp;
+ navigation?: PlatformStackNavigationProp | PlatformStackNavigationProp;
/** Whether to show offline indicator on wide screens */
shouldShowOfflineIndicatorInWideScreen?: boolean;
@@ -137,6 +140,7 @@ function ScreenWrapper(
shouldShowOfflineIndicatorInWideScreen = false,
shouldUseCachedViewportHeight = false,
focusTrapSettings,
+ bottomContent,
}: ScreenWrapperProps,
ref: ForwardedRef,
) {
@@ -147,7 +151,7 @@ function ScreenWrapper(
* so in other places where ScreenWrapper is used, we need to
* fallback to useNavigation.
*/
- const navigationFallback = useNavigation>();
+ const navigationFallback = useNavigation>();
const navigation = navigationProp ?? navigationFallback;
const isFocused = useIsFocused();
const {windowHeight} = useWindowDimensions(shouldUseCachedViewportHeight);
@@ -258,22 +262,23 @@ function ScreenWrapper(
}, []);
const {insets, paddingTop, paddingBottom, safeAreaPaddingBottomStyle, unmodifiedPaddings} = useStyledSafeAreaInsets();
- const paddingStyle: StyleProp = {};
+ const paddingTopStyle: StyleProp = {};
+ const paddingBottomStyle: StyleProp = {};
const isSafeAreaTopPaddingApplied = includePaddingTop;
if (includePaddingTop) {
- paddingStyle.paddingTop = paddingTop;
+ paddingTopStyle.paddingTop = paddingTop;
}
if (includePaddingTop && ignoreInsetsConsumption) {
- paddingStyle.paddingTop = unmodifiedPaddings.top;
+ paddingTopStyle.paddingTop = unmodifiedPaddings.top;
}
// We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked.
if (includeSafeAreaPaddingBottom) {
- paddingStyle.paddingBottom = paddingBottom;
+ paddingBottomStyle.paddingBottom = paddingBottom;
}
if (includeSafeAreaPaddingBottom && ignoreInsetsConsumption) {
- paddingStyle.paddingBottom = unmodifiedPaddings.bottom;
+ paddingBottomStyle.paddingBottom = unmodifiedPaddings.bottom;
}
const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && isMobileWebKit());
@@ -293,7 +298,7 @@ function ScreenWrapper(
>
@@ -347,6 +352,7 @@ function ScreenWrapper(
+ {bottomContent}
);
diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx
index aba45db33e00..f715e137f79c 100644
--- a/src/components/ScrollOffsetContextProvider.tsx
+++ b/src/components/ScrollOffsetContextProvider.tsx
@@ -2,9 +2,9 @@ import type {ParamListBase} from '@react-navigation/native';
import React, {createContext, useCallback, useEffect, useMemo, useRef} from 'react';
import {useOnyx} from 'react-native-onyx';
import usePrevious from '@hooks/usePrevious';
+import {isSidebarScreenName} from '@libs/Navigation/helpers/isNavigatorName';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {NavigationPartialRoute, State} from '@libs/Navigation/types';
-import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
@@ -78,14 +78,11 @@ function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProp
}, []);
const cleanStaleScrollOffsets: ScrollOffsetContextValue['cleanStaleScrollOffsets'] = useCallback((state) => {
- const bottomTabNavigator = state.routes.find((route) => route.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR);
- if (bottomTabNavigator?.state && 'routes' in bottomTabNavigator.state) {
- const bottomTabNavigatorRoutes = bottomTabNavigator.state.routes;
- const scrollOffsetkeysOfExistingScreens = bottomTabNavigatorRoutes.map((route) => getKey(route));
- for (const key of Object.keys(scrollOffsetsRef.current)) {
- if (!scrollOffsetkeysOfExistingScreens.includes(key)) {
- delete scrollOffsetsRef.current[key];
- }
+ const sidebarRoutes = state.routes.filter((route) => isSidebarScreenName(route.name));
+ const scrollOffsetkeysOfExistingScreens = sidebarRoutes.map((route) => getKey(route));
+ for (const key of Object.keys(scrollOffsetsRef.current)) {
+ if (!scrollOffsetkeysOfExistingScreens.includes(key)) {
+ delete scrollOffsetsRef.current[key];
}
}
}, []);
diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index 6c6b4dc689d3..5e201a921e0d 100644
--- a/src/components/Search/SearchRouter/SearchRouter.tsx
+++ b/src/components/Search/SearchRouter/SearchRouter.tsx
@@ -1,4 +1,4 @@
-import {useNavigationState} from '@react-navigation/native';
+import {findFocusedRoute, useNavigationState} from '@react-navigation/native';
import isEqual from 'lodash/isEqual';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
@@ -27,11 +27,13 @@ import type {OptionData} from '@libs/ReportUtils';
import {getAutocompleteQueryWithComma, getQueryWithoutAutocompletedPart} from '@libs/SearchAutocompleteUtils';
import {getQueryWithUpdatedValues, sanitizeSearchValue} from '@libs/SearchQueryUtils';
import Navigation from '@navigation/Navigation';
+import type {ReportsSplitNavigatorParamList} from '@navigation/types';
import variables from '@styles/variables';
import {navigateToAndOpenReport} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
import type Report from '@src/types/onyx/Report';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import {getQueryWithSubstitutions} from './getQueryWithSubstitutions';
@@ -91,7 +93,11 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps)
const textInputRef = useRef(null);
const contextualReportID = useNavigationState, string | undefined>((state) => {
- return state?.routes.at(-1)?.params?.reportID;
+ const focusedRoute = findFocusedRoute(state);
+ if (focusedRoute?.name === SCREENS.REPORT) {
+ // We're guaranteed that the type of params is of SCREENS.REPORT
+ return (focusedRoute.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]).reportID;
+ }
});
const getAdditionalSections: GetAdditionalSectionsCallback = useCallback(
diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx
index 7aa7e7305bc8..947dcc9c48c5 100644
--- a/src/components/Search/SearchRouter/SearchRouterContext.tsx
+++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx
@@ -1,6 +1,6 @@
import React, {useContext, useMemo, useRef, useState} from 'react';
import type {AnimatedTextInputRef} from '@components/RNTextInput';
-import isSearchTopmostCentralPane from '@navigation/isSearchTopmostCentralPane';
+import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import * as Modal from '@userActions/Modal';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
@@ -49,7 +49,7 @@ function SearchRouterContextProvider({children}: ChildrenProps) {
// So we need a function that is based on ref to correctly open/close it
// When user is on `/search` page we focus the Input instead of showing router
const toggleSearch = () => {
- const isUserOnSearchPage = isSearchTopmostCentralPane();
+ const isUserOnSearchPage = isSearchTopmostFullScreenRoute();
if (isUserOnSearchPage && searchPageInputRef.current) {
if (searchPageInputRef.current.isFocused()) {
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index 153e635da7b6..406afcbb5e2d 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -19,7 +19,7 @@ import {createTransactionThread, search} from '@libs/actions/Search';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
import memoize from '@libs/memoize';
-import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane';
+import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import {generateReportID} from '@libs/ReportUtils';
import {buildSearchQueryString} from '@libs/SearchQueryUtils';
@@ -330,7 +330,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
useEffect(
() => () => {
- if (isSearchTopmostCentralPane()) {
+ if (isSearchTopmostFullScreenRoute()) {
return;
}
clearSelectedTransactions();
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index 8f55ecd7a924..2edf248e12d4 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -96,6 +96,7 @@ function BaseSelectionList(
sectionTitleStyles,
textInputAutoFocus = true,
shouldShowTextInputAfterHeader = false,
+ shouldShowHeaderMessageAfterHeader = false,
includeSafeAreaPaddingBottom = true,
shouldTextInputInterceptSwipe = false,
listHeaderContent,
@@ -123,6 +124,7 @@ function BaseSelectionList(
listItemTitleStyles,
initialNumToRender = 12,
listItemTitleContainerStyles,
+ isScreenFocused = false,
}: BaseSelectionListProps,
ref: ForwardedRef,
) {
@@ -380,6 +382,9 @@ function BaseSelectionList(
*/
const selectRow = useCallback(
(item: TItem, indexToFocus?: number) => {
+ if (!isFocused && !isScreenFocused) {
+ return;
+ }
// In single-selection lists we don't care about updating the focused index, because the list is closed after selecting an item
if (canSelectMultiple) {
if (sections.length > 1 && !item.isSelected) {
@@ -412,6 +417,8 @@ function BaseSelectionList(
setFocusedIndex,
onSelectRow,
shouldPreventDefaultFocusOnSelectRow,
+ isFocused,
+ isScreenFocused,
],
);
@@ -805,6 +812,14 @@ function BaseSelectionList(
},
);
+ const headerMessageContent = () =>
+ (!isLoadingNewOptions || headerMessage !== translate('common.noResultsFound') || (flattenedSections.allOptions.length === 0 && !showLoadingPlaceholder)) &&
+ !!headerMessage && (
+
+ {headerMessage}
+
+ );
+
const {safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets();
// TODO: test _every_ component that uses SelectionList
@@ -813,11 +828,7 @@ function BaseSelectionList(
{shouldShowTextInput && !shouldShowTextInputAfterHeader && renderInput()}
{/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */}
{/* This is misleading because we might be in the process of loading fresh options from the server. */}
- {(!isLoadingNewOptions || headerMessage !== translate('common.noResultsFound') || (flattenedSections.allOptions.length === 0 && !showLoadingPlaceholder)) && !!headerMessage && (
-
- {headerMessage}
-
- )}
+ {!shouldShowHeaderMessageAfterHeader && headerMessageContent()}
{!!headerContent && headerContent}
{flattenedSections.allOptions.length === 0 && (showLoadingPlaceholder || shouldShowListEmptyContent) ? (
renderListEmptyContent()
@@ -859,6 +870,7 @@ function BaseSelectionList(
<>
{listHeaderContent}
{renderInput()}
+ {shouldShowHeaderMessageAfterHeader && headerMessageContent()}
>
) : (
listHeaderContent
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index ffdb1c956d8b..fc3dd2758ab7 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -469,6 +469,9 @@ type BaseSelectionListProps = Partial & {
/** Whether the text input should be shown after list header */
shouldShowTextInputAfterHeader?: boolean;
+ /** Whether the header message should be shown after list header */
+ shouldShowHeaderMessageAfterHeader?: boolean;
+
/** Whether to include padding bottom */
includeSafeAreaPaddingBottom?: boolean;
@@ -659,6 +662,9 @@ type BaseSelectionListProps = Partial & {
/** Initial number of items to render */
initialNumToRender?: number;
+
+ /** Whether the screen is focused or not. (useIsFocused state does not work in tab screens, e.g. SearchPageBottomTab) */
+ isScreenFocused?: boolean;
} & TRightHandSideComponent;
type SelectionListHandle = {
diff --git a/src/components/SelectionListWithModal/index.tsx b/src/components/SelectionListWithModal/index.tsx
index a4c8b83b29d8..81afd267abea 100644
--- a/src/components/SelectionListWithModal/index.tsx
+++ b/src/components/SelectionListWithModal/index.tsx
@@ -106,6 +106,7 @@ function SelectionListWithModal(
ref={ref}
sections={sections}
onLongPressRow={handleLongPressRow}
+ isScreenFocused={isScreenFocused}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
/>
diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
index 0408d9bd20e2..ad37f59c8ef5 100644
--- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
+++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx
@@ -1,5 +1,5 @@
import {Portal} from '@gorhom/portal';
-import React, {useMemo, useRef, useState} from 'react';
+import React, {useLayoutEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {View as RNView} from 'react-native';
@@ -8,6 +8,7 @@ import AnimatedPressableWithoutFeedback from '@components/AnimatedPressableWitho
import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/TransparentOverlay/TransparentOverlay';
import Text from '@components/Text';
import useStyleUtils from '@hooks/useStyleUtils';
+import {parseFSAttributes} from '@libs/Fullstory';
import CONST from '@src/CONST';
import type {BaseGenericTooltipProps} from './types';
@@ -101,14 +102,31 @@ function BaseGenericTooltip({
});
});
+ /**
+ * Extracts values from the non-scraped attribute WEB_PROP_ATTR at build time
+ * to ensure necessary properties are available for further processing.
+ * Reevaluates "fs-class" to dynamically apply styles or behavior based on
+ * updated attribute values.
+ */
+ useLayoutEffect(parseFSAttributes, []);
+
let content;
if (renderTooltipContent) {
- content = {renderTooltipContent()};
+ content = (
+
+ {renderTooltipContent()}
+
+ );
} else {
content = (
{text}
diff --git a/src/components/Tooltip/BaseGenericTooltip/index.tsx b/src/components/Tooltip/BaseGenericTooltip/index.tsx
index 9ae49defc0fa..bfa84cd1830d 100644
--- a/src/components/Tooltip/BaseGenericTooltip/index.tsx
+++ b/src/components/Tooltip/BaseGenericTooltip/index.tsx
@@ -8,6 +8,7 @@ import TransparentOverlay from '@components/AutoCompleteSuggestions/AutoComplete
import {PopoverContext} from '@components/PopoverProvider';
import Text from '@components/Text';
import useStyleUtils from '@hooks/useStyleUtils';
+import {parseFSAttributes} from '@libs/Fullstory';
import CONST from '@src/CONST';
import textRef from '@src/types/utils/textRef';
import viewRef from '@src/types/utils/viewRef';
@@ -120,14 +121,32 @@ function BaseGenericTooltip({
return StyleUtils.getTooltipAnimatedStyles({tooltipContentWidth: contentMeasuredWidth, tooltipWrapperHeight: wrapperMeasuredHeight, currentSize: animation});
});
+ /**
+ * Extracts values from the non-scraped attribute WEB_PROP_ATTR at build time
+ * to ensure necessary properties are available for further processing.
+ * Reevaluates "fs-class" to dynamically apply styles or behavior based on
+ * updated attribute values.
+ */
+ useLayoutEffect(parseFSAttributes, []);
+
let content;
if (renderTooltipContent) {
- content = {renderTooltipContent()};
+ content = (
+
+ {renderTooltipContent()}
+
+ );
} else {
content = (
;
-};
-
-export default function withNavigation(
- WrappedComponent: ComponentType>,
-): (props: Omit, ref: ForwardedRef) => React.JSX.Element | null {
- function WithNavigation(props: Omit, ref: ForwardedRef) {
- const navigation = useNavigation();
- return (
-
- );
- }
-
- WithNavigation.displayName = `withNavigation(${getComponentDisplayName(WrappedComponent)})`;
- return React.forwardRef(WithNavigation);
-}
diff --git a/src/components/withNavigationTransitionEnd.tsx b/src/components/withNavigationTransitionEnd.tsx
index 69e04ff22e35..0bb6f1ffa448 100644
--- a/src/components/withNavigationTransitionEnd.tsx
+++ b/src/components/withNavigationTransitionEnd.tsx
@@ -3,14 +3,14 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react';
import React, {useEffect, useState} from 'react';
import getComponentDisplayName from '@libs/getComponentDisplayName';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
-import type {RootStackParamList} from '@libs/Navigation/types';
+import type {RootNavigatorParamList} from '@libs/Navigation/types';
type WithNavigationTransitionEndProps = {didScreenTransitionEnd: boolean};
export default function (WrappedComponent: ComponentType>): React.ComponentType> {
function WithNavigationTransitionEnd(props: TProps, ref: ForwardedRef) {
const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
- const navigation = useNavigation>();
+ const navigation = useNavigation>();
useEffect(() => {
const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => {
diff --git a/src/components/withPrepareCentralPaneScreen/index.native.tsx b/src/components/withPrepareCentralPaneScreen/index.native.tsx
deleted file mode 100644
index 84ba31cd63fd..000000000000
--- a/src/components/withPrepareCentralPaneScreen/index.native.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import type React from 'react';
-import freezeScreenWithLazyLoading from '@libs/freezeScreenWithLazyLoading';
-
-/**
- * This higher-order function is dependent on the platform. On native platforms, screens that aren't already displayed in the navigation stack should be frozen to prevent unnecessary rendering.
- * It's handled this way only on mobile platforms because on the web, more than one screen is displayed in a wide layout, so these screens shouldn't be frozen.
- */
-export default function withPrepareCentralPaneScreen(lazyComponent: () => React.ComponentType) {
- return freezeScreenWithLazyLoading(lazyComponent);
-}
diff --git a/src/components/withPrepareCentralPaneScreen/index.tsx b/src/components/withPrepareCentralPaneScreen/index.tsx
deleted file mode 100644
index f53368188b3d..000000000000
--- a/src/components/withPrepareCentralPaneScreen/index.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import type React from 'react';
-
-/**
- * This higher-order function is dependent on the platform. On native platforms, screens that aren't already displayed in the navigation stack should be frozen to prevent unnecessary rendering.
- * It's handled this way only on mobile platforms because on the web, more than one screen is displayed in a wide layout, so these screens shouldn't be frozen.
- */
-export default function withPrepareCentralPaneScreen(lazyComponent: () => React.ComponentType) {
- return lazyComponent;
-}
diff --git a/src/hooks/useActiveCentralPaneRoute.ts b/src/hooks/useActiveCentralPaneRoute.ts
deleted file mode 100644
index 05354e810c3d..000000000000
--- a/src/hooks/useActiveCentralPaneRoute.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import {useContext} from 'react';
-import ActiveCentralPaneRouteContext from '@libs/Navigation/AppNavigator/Navigators/ActiveCentralPaneRouteContext';
-import type {AuthScreensParamList, NavigationPartialRoute} from '@libs/Navigation/types';
-
-function useActiveCentralPaneRoute(): NavigationPartialRoute | undefined {
- return useContext(ActiveCentralPaneRouteContext);
-}
-
-export default useActiveCentralPaneRoute;
diff --git a/src/hooks/useActiveWorkspace.ts b/src/hooks/useActiveWorkspace.ts
index cce3c2a4b31f..0ba5426895e1 100644
--- a/src/hooks/useActiveWorkspace.ts
+++ b/src/hooks/useActiveWorkspace.ts
@@ -1,6 +1,6 @@
import {useContext} from 'react';
-import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext';
import type {ActiveWorkspaceContextType} from '@components/ActiveWorkspace/ActiveWorkspaceContext';
+import ActiveWorkspaceContext from '@components/ActiveWorkspace/ActiveWorkspaceContext';
function useActiveWorkspace(): ActiveWorkspaceContextType {
return useContext(ActiveWorkspaceContext);
diff --git a/src/hooks/useActiveWorkspaceFromNavigationState.ts b/src/hooks/useActiveWorkspaceFromNavigationState.ts
deleted file mode 100644
index 0308ece138a6..000000000000
--- a/src/hooks/useActiveWorkspaceFromNavigationState.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import {useNavigationState} from '@react-navigation/native';
-import Log from '@libs/Log';
-import type {BottomTabNavigatorParamList} from '@libs/Navigation/types';
-import SCREENS from '@src/SCREENS';
-
-/**
- * Get the currently selected policy ID stored in the navigation state. This hook should only be called only from screens in BottomTab.
- * Differences between this hook and useActiveWorkspace:
- * - useActiveWorkspaceFromNavigationState reads the active workspace id directly from the navigation state, it's a bit slower than useActiveWorkspace and it can be called only from BottomTabScreens.
- * It allows to read a value of policyID immediately after the update.
- * - useActiveWorkspace allows to read the current policyID anywhere, it's faster because it doesn't require searching in the navigation state.
- */
-function useActiveWorkspaceFromNavigationState() {
- // The last policyID value is always stored in the last route in BottomTabNavigator.
- const activeWorkspaceID = useNavigationState((state) => {
- // SCREENS.HOME is a screen located in the BottomTabNavigator, if it's not in state.routeNames it means that this hook was called from a screen in another navigator.
- if (!state.routeNames.includes(SCREENS.HOME)) {
- Log.warn('useActiveWorkspaceFromNavigationState should be called only from BottomTab screens');
- }
-
- const lastHomeParams = state.routes.findLast((route) => route.name === SCREENS.HOME)?.params ?? {};
-
- if ('policyID' in lastHomeParams) {
- return lastHomeParams.policyID;
- }
- });
-
- return activeWorkspaceID;
-}
-
-export default useActiveWorkspaceFromNavigationState;
diff --git a/src/hooks/useBottomTabIsFocused.ts b/src/hooks/useBottomTabIsFocused.ts
deleted file mode 100644
index 90b74097ecd8..000000000000
--- a/src/hooks/useBottomTabIsFocused.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import type {EventArg, NavigationContainerEventMap} from '@react-navigation/native';
-import {useIsFocused, useNavigationState} from '@react-navigation/native';
-import {useEffect, useState} from 'react';
-import CENTRAL_PANE_SCREENS from '@libs/Navigation/AppNavigator/CENTRAL_PANE_SCREENS';
-import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
-import getTopmostFullScreenRoute from '@libs/Navigation/getTopmostFullScreenRoute';
-import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
-import type {CentralPaneName, FullScreenName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types';
-import SCREENS from '@src/SCREENS';
-import useResponsiveLayout from './useResponsiveLayout';
-
-const useBottomTabIsFocused = () => {
- const [isScreenFocused, setIsScreenFocused] = useState(false);
- useEffect(() => {
- const listener = (event: EventArg<'state', false, NavigationContainerEventMap['state']['data']>) => {
- const routName = Navigation.getRouteNameFromStateEvent(event);
- if (routName === SCREENS.SEARCH.CENTRAL_PANE || routName === SCREENS.SETTINGS_CENTRAL_PANE || routName === SCREENS.HOME) {
- setIsScreenFocused(true);
- return;
- }
- setIsScreenFocused(false);
- };
- navigationRef.addListener('state', listener);
- return () => navigationRef.removeListener('state', listener);
- }, []);
-
- const {shouldUseNarrowLayout} = useResponsiveLayout();
- const isFocused = useIsFocused();
- const topmostFullScreenName = useNavigationState | undefined>(getTopmostFullScreenRoute);
- const topmostCentralPane = useNavigationState | undefined>(getTopmostCentralPaneRoute);
-
- // If there is a full screen view such as Workspace Settings or Not Found screen, the bottom tab should not be considered focused
- if (topmostFullScreenName) {
- return false;
- }
- // On the Search screen, isFocused returns false, but it is actually focused
- if (shouldUseNarrowLayout) {
- return isFocused || isScreenFocused;
- }
- // On desktop screen sizes, isFocused always returns false, so we cannot rely on it alone to determine if the bottom tab is focused
- return isFocused || Object.keys(CENTRAL_PANE_SCREENS).includes(topmostCentralPane?.name ?? '');
-};
-
-export default useBottomTabIsFocused;
diff --git a/src/hooks/useIsCurrentRouteHome.ts b/src/hooks/useIsCurrentRouteHome.ts
deleted file mode 100644
index e4950a9accc7..000000000000
--- a/src/hooks/useIsCurrentRouteHome.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import {useNavigationState} from '@react-navigation/native';
-import getTopmostRouteName from '@libs/Navigation/getTopmostRouteName';
-import SCREENS from '@src/SCREENS';
-
-/** Determine if the current route is the home screen */
-function useIsCurrentRouteHome() {
- const activeRoute = useNavigationState(getTopmostRouteName);
- const isActiveRouteHome = activeRoute === SCREENS.HOME;
- return isActiveRouteHome;
-}
-
-export default useIsCurrentRouteHome;
diff --git a/src/hooks/useRootNavigationState.ts b/src/hooks/useRootNavigationState.ts
new file mode 100644
index 000000000000..289fe164be10
--- /dev/null
+++ b/src/hooks/useRootNavigationState.ts
@@ -0,0 +1,33 @@
+import type {NavigationState} from '@react-navigation/routers';
+import {useEffect, useRef, useState} from 'react';
+import navigationRef from '@libs/Navigation/navigationRef';
+
+type Selector = (state: NavigationState) => T;
+
+/**
+ * Hook to get a value from the current root navigation state using a selector.
+ *
+ * @param selector Selector function to get a value from the state.
+ */
+function useRootNavigationState(selector: Selector): T {
+ const [result, setResult] = useState(() => selector(navigationRef.getRootState()));
+
+ // We store the selector in a ref to avoid re-subscribing listeners every render
+ const selectorRef = useRef(selector);
+
+ useEffect(() => {
+ selectorRef.current = selector;
+ });
+
+ useEffect(() => {
+ const unsubscribe = navigationRef.addListener('state', (e) => {
+ setResult(selectorRef.current(e.data.state as NavigationState));
+ });
+
+ return unsubscribe;
+ }, []);
+
+ return result;
+}
+
+export default useRootNavigationState;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index df4e2f1cb397..c5d88693f61f 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -175,6 +175,7 @@ import type {
UnapproveWithIntegrationWarningParams,
UnshareParams,
UntilTimeParams,
+ UpdateAutoReportingFrequencyParams,
UpdatedTheDistanceMerchantParams,
UpdatedTheRequestParams,
UpdateRoleParams,
@@ -2526,15 +2527,12 @@ const translations = {
toLearnMore: ' to learn more.',
termsAndConditions: {
header: 'Before we continue...',
- title: 'Please read the Terms & Conditions for travel',
- subtitle: 'To enable travel on your workspace you must agree to our ',
+ title: 'Terms & conditions',
+ subtitle: 'Please agree to the Expensify Travel ',
termsconditions: 'terms & conditions',
travelTermsAndConditions: 'terms & conditions',
- helpDocIntro: 'Check out this ',
- helpDocOutro: 'for more information or reach out to Concierge or your Account Manager.',
- helpDoc: 'Help Doc',
- agree: 'I agree to the travel ',
- error: 'You must accept the Terms & Conditions for travel to continue',
+ agree: 'I agree to the ',
+ error: 'You must agree to the Expensify Travel terms & conditions to continue',
},
flight: 'Flight',
flightDetails: {
@@ -2709,6 +2707,15 @@ const translations = {
return 'Member';
}
},
+ frequency: {
+ manual: 'Manually',
+ instant: 'Instant',
+ immediate: 'Daily',
+ trip: 'By trip',
+ weekly: 'Weekly',
+ semimonthly: 'Twice a month',
+ monthly: 'Monthly',
+ },
planType: 'Plan type',
submitExpense: 'Submit your expenses below:',
defaultCategory: 'Default category',
@@ -3796,6 +3803,9 @@ const translations = {
noAccountsFound: 'No accounts found',
defaultCard: 'Default card',
noAccountsFoundDescription: ({connection}: ConnectionParams) => `Please add the account in ${connection} and sync the connection again.`,
+ expensifyCardBannerTitle: 'Get the Expensify Card',
+ expensifyCardBannerSubtitle: 'Enjoy cash back on every US purchase, up to 50% off your Expensify bill, unlimited virtual cards, and so much more.',
+ expensifyCardBannerLearnMoreButton: 'Learn more',
},
workflows: {
title: 'Workflows',
@@ -5081,6 +5091,8 @@ const translations = {
leftWorkspace: ({nameOrEmail}: LeftWorkspaceParams) => `${nameOrEmail} left the workspace`,
removeMember: ({email, role}: AddEmployeeParams) => `removed ${role === 'member' || role === 'user' ? 'member' : 'admin'} ${email}`,
removedConnection: ({connectionName}: ConnectionNameParams) => `removed connection to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
+ updateAutoReportingFrequency: ({oldFrequency, newFrequency}: UpdateAutoReportingFrequencyParams) =>
+ `updated the submission frequency to "${newFrequency}" (previously "${oldFrequency}")`,
},
},
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index a4507194aedd..54292ec0f511 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -174,6 +174,7 @@ import type {
UnapproveWithIntegrationWarningParams,
UnshareParams,
UntilTimeParams,
+ UpdateAutoReportingFrequencyParams,
UpdatedTheDistanceMerchantParams,
UpdatedTheRequestParams,
UpdateRoleParams,
@@ -2551,15 +2552,12 @@ const translations = {
toLearnMore: ' para obtener más información.',
termsAndConditions: {
header: 'Antes de continuar...',
- title: 'Por favor, lee los Términos y condiciones para reservar viajes',
- subtitle: 'Para permitir la opción de reservar viajes en tu espacio de trabajo debe aceptar nuestros ',
+ title: 'Términos y condiciones de Expensify Travel',
+ subtitle: 'Por favor, acepta los ',
termsconditions: 'términos y condiciones',
- travelTermsAndConditions: 'términos y condiciones de viaje',
- helpDocIntro: 'Consulta este ',
- helpDocOutro: 'para obtener más información o comunícate con Concierge o tu gestor de cuentas.',
- helpDoc: 'documento de ayuda',
+ travelTermsAndConditions: 'términos y condiciones',
agree: 'Acepto los ',
- error: 'Debes aceptar los Términos y condiciones para que el viaje continúe',
+ error: 'Debes aceptar los términos y condiciones de Expensify Travel para continuar',
},
flight: 'Vuelo',
flightDetails: {
@@ -2734,6 +2732,15 @@ const translations = {
return 'Miembro';
}
},
+ frequency: {
+ manual: 'Manualmente',
+ instant: 'Instantáneo',
+ immediate: 'Diaria',
+ trip: 'Por viaje',
+ weekly: 'Semanal',
+ semimonthly: 'Dos veces al mes',
+ monthly: 'Mensual',
+ },
planType: 'Tipo de plan',
submitExpense: 'Envía tus gastos a continuación:',
defaultCategory: 'Categoría predeterminada',
@@ -3841,6 +3848,10 @@ const translations = {
noAccountsFound: 'No se han encontrado cuentas',
defaultCard: 'Tarjeta predeterminada',
noAccountsFoundDescription: ({connection}: ConnectionParams) => `Añade la cuenta en ${connection} y sincroniza la conexión de nuevo.`,
+ expensifyCardBannerTitle: 'Obtén la Tarjeta Expensify',
+ expensifyCardBannerSubtitle:
+ 'Disfruta de una devolución en cada compra en Estados Unidos, hasta un 50% de descuento en tu factura de Expensify, tarjetas virtuales ilimitadas y mucho más.',
+ expensifyCardBannerLearnMoreButton: 'Más información',
},
workflows: {
title: 'Flujos de trabajo',
@@ -5133,6 +5144,8 @@ const translations = {
leftWorkspace: ({nameOrEmail}: LeftWorkspaceParams) => `${nameOrEmail} salió del espacio de trabajo`,
removeMember: ({email, role}: AddEmployeeParams) => `eliminado ${role === 'miembro' || role === 'user' ? 'miembro' : 'administrador'} ${email}`,
removedConnection: ({connectionName}: ConnectionNameParams) => `eliminó la conexión a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
+ updateAutoReportingFrequency: ({oldFrequency, newFrequency}: UpdateAutoReportingFrequencyParams) =>
+ `actualizó la frecuencia de envíos a "${newFrequency}" (previamente "${oldFrequency}")`,
},
},
},
diff --git a/src/languages/params.ts b/src/languages/params.ts
index e1d3e5b34c4e..2a04be84ae21 100644
--- a/src/languages/params.ts
+++ b/src/languages/params.ts
@@ -349,6 +349,11 @@ type ConnectionNameParams = {
connectionName: AllConnectionName;
};
+type UpdateAutoReportingFrequencyParams = {
+ oldFrequency: string;
+ newFrequency: string;
+};
+
type LastSyncDateParams = {
connectionName: string;
formattedDate: string;
@@ -810,6 +815,7 @@ export type {
UpdateRoleParams,
LeftWorkspaceParams,
RemoveMemberParams,
+ UpdateAutoReportingFrequencyParams,
DateParams,
FiltersAmountBetweenParams,
StatementPageTitleParams,
diff --git a/src/libs/API/parameters/SyncPolicyToNSQSParams.ts b/src/libs/API/parameters/SyncPolicyToNSQSParams.ts
index 319ccb2f1d50..aa867403586d 100644
--- a/src/libs/API/parameters/SyncPolicyToNSQSParams.ts
+++ b/src/libs/API/parameters/SyncPolicyToNSQSParams.ts
@@ -1,5 +1,6 @@
type SyncPolicyToNSQSParams = {
policyID: string;
+ netSuiteAccountID: string;
idempotencyKey: string;
};
diff --git a/src/libs/Environment/getEnvironment/index.native.ts b/src/libs/Environment/getEnvironment/index.native.ts
index 6d298c3fdae9..5dac0ad1d73e 100644
--- a/src/libs/Environment/getEnvironment/index.native.ts
+++ b/src/libs/Environment/getEnvironment/index.native.ts
@@ -1,3 +1,4 @@
+import {NativeModules} from 'react-native';
import Config from 'react-native-config';
import betaChecker from '@libs/Environment/betaChecker';
import CONST from '@src/CONST';
@@ -28,6 +29,12 @@ function getEnvironment(): Promise {
return;
}
+ // If we don't use Development, and we're in the HybridApp, we should use Production
+ if (NativeModules.HybridAppModule) {
+ environment = CONST.ENVIRONMENT.PRODUCTION;
+ return;
+ }
+
// If we haven't set the environment yet and we aren't on dev/adhoc, check to see if this is a beta build
betaChecker.isBetaBuild().then((isBeta) => {
environment = isBeta ? CONST.ENVIRONMENT.STAGING : CONST.ENVIRONMENT.PRODUCTION;
diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts
index a2d83259c52b..a183666fbdcc 100644
--- a/src/libs/IOUUtils.ts
+++ b/src/libs/IOUUtils.ts
@@ -26,13 +26,13 @@ function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: I
// If the participants were automatically added to the transaction, then the user needs taken back to the starting step
switch (requestType) {
case CONST.IOU.REQUEST_TYPE.DISTANCE:
- Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_DISTANCE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID));
+ Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_DISTANCE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID), {compareParams: false});
break;
case CONST.IOU.REQUEST_TYPE.SCAN:
- Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID));
+ Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID), {compareParams: false});
break;
default:
- Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_MANUAL.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID));
+ Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_MANUAL.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID), {compareParams: false});
break;
}
}
diff --git a/src/components/ImportOnyxState/utils.ts b/src/libs/ImportOnyxStateUtils.ts
similarity index 51%
rename from src/components/ImportOnyxState/utils.ts
rename to src/libs/ImportOnyxStateUtils.ts
index 94779868384d..249924291a92 100644
--- a/src/components/ImportOnyxState/utils.ts
+++ b/src/libs/ImportOnyxStateUtils.ts
@@ -1,6 +1,10 @@
import cloneDeep from 'lodash/cloneDeep';
+import type {OnyxEntry, OnyxKey} from 'react-native-onyx';
import type {UnknownRecord} from 'type-fest';
+import type {OnyxCollectionKey, OnyxCollectionValuesMapping, OnyxValues} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
+import type CollectionDataSet from '@src/types/utils/CollectionDataSet';
+import {clearOnyxStateBeforeImport, importOnyxCollectionState, importOnyxRegularState} from './actions/ImportOnyxState';
// List of Onyx keys from the .txt file we want to keep for the local override
const keysToOmit = [ONYXKEYS.ACTIVE_CLIENTS, ONYXKEYS.FREQUENTLY_USED_EMOJIS, ONYXKEYS.NETWORK, ONYXKEYS.CREDENTIALS, ONYXKEYS.PREFERRED_THEME];
@@ -12,7 +16,7 @@ function isRecord(value: unknown): value is Record {
function transformNumericKeysToArray(data: UnknownRecord): UnknownRecord | unknown[] {
const dataCopy = cloneDeep(data);
if (!isRecord(dataCopy)) {
- return Array.isArray(dataCopy) ? (dataCopy as UnknownRecord[]).map(transformNumericKeysToArray) : (dataCopy as UnknownRecord);
+ return Array.isArray(dataCopy) ? (dataCopy as UnknownRecord[]).map(transformNumericKeysToArray) : dataCopy;
}
const keys = Object.keys(dataCopy);
@@ -50,4 +54,35 @@ function cleanAndTransformState(state: string): T {
return transformedState;
}
-export {transformNumericKeysToArray, cleanAndTransformState};
+function importState(transformedState: OnyxValues): Promise {
+ const collectionKeys = [...new Set(Object.values(ONYXKEYS.COLLECTION))];
+ const collectionsMap = new Map>();
+ const regularState: Partial>> = {};
+
+ Object.entries(transformedState).forEach(([entryKey, entryValue]) => {
+ const key = entryKey as OnyxKey;
+ const value = entryValue as NonNullable>;
+
+ const collectionKey = collectionKeys.find((cKey) => key.startsWith(cKey));
+ if (collectionKey) {
+ if (!collectionsMap.has(collectionKey)) {
+ collectionsMap.set(collectionKey, {});
+ }
+
+ const collection = collectionsMap.get(collectionKey);
+ if (!collection) {
+ return;
+ }
+
+ collection[key as OnyxCollectionKey] = value;
+ } else {
+ regularState[key] = value;
+ }
+ });
+
+ return clearOnyxStateBeforeImport()
+ .then(() => importOnyxCollectionState(collectionsMap))
+ .then(() => importOnyxRegularState(regularState));
+}
+
+export {cleanAndTransformState, importState, transformNumericKeysToArray};
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 9968251e226a..ec1c06b2bbaf 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -1,23 +1,22 @@
+import type {RouteProp} from '@react-navigation/native';
import {findFocusedRoute, useNavigation} from '@react-navigation/native';
-import React, {memo, useEffect, useMemo, useRef, useState} from 'react';
-import {NativeModules, View} from 'react-native';
+import React, {memo, useEffect, useMemo, useRef} from 'react';
+import {NativeModules} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Onyx, {withOnyx} from 'react-native-onyx';
-import type {ValueOf} from 'type-fest';
import ActiveGuidesEventListener from '@components/ActiveGuidesEventListener';
+import ActiveWorkspaceContextProvider from '@components/ActiveWorkspaceProvider';
import ComposeProviders from '@components/ComposeProviders';
import OptionsListContextProvider from '@components/OptionListContextProvider';
import {SearchContextProvider} from '@components/Search/SearchContext';
import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext';
import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal';
import TestToolsModal from '@components/TestToolsModal';
-import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useOnboardingFlowRouter from '@hooks/useOnboardingFlow';
-import usePermissions from '@hooks/usePermissions';
+import {ReportIDsContextProvider} from '@hooks/useReportIDs';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
-import useThemeStyles from '@hooks/useThemeStyles';
import setFullscreenVisibility from '@libs/actions/setFullscreenVisibility';
import {READ_COMMANDS} from '@libs/API/types';
import HttpUtils from '@libs/HttpUtils';
@@ -25,18 +24,15 @@ import KeyboardShortcut from '@libs/KeyboardShortcut';
import Log from '@libs/Log';
import NavBarManager from '@libs/NavBarManager';
import getCurrentUrl from '@libs/Navigation/currentUrl';
+import {isOnboardingFlowName} from '@libs/Navigation/helpers/isNavigatorName';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
import Presentation from '@libs/Navigation/PlatformStackNavigation/navigationOptions/presentation';
-import type {PlatformStackNavigationOptions} from '@libs/Navigation/PlatformStackNavigation/types';
-import shouldOpenOnAdminRoom from '@libs/Navigation/shouldOpenOnAdminRoom';
-import type {AuthScreensParamList, CentralPaneName, CentralPaneScreensParamList} from '@libs/Navigation/types';
-import {isOnboardingFlowName} from '@libs/NavigationUtils';
+import type {AuthScreensParamList} from '@libs/Navigation/types';
import NetworkConnection from '@libs/NetworkConnection';
import onyxSubscribe from '@libs/onyxSubscribe';
import * as Pusher from '@libs/Pusher/pusher';
import PusherConnectionManager from '@libs/PusherConnectionManager';
-import * as ReportUtils from '@libs/ReportUtils';
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import * as SessionUtils from '@libs/SessionUtils';
import ConnectionCompletePage from '@pages/ConnectionCompletePage';
@@ -62,21 +58,17 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
-import beforeRemoveReportOpenedFromSearchRHP from './beforeRemoveReportOpenedFromSearchRHP';
-import CENTRAL_PANE_SCREENS from './CENTRAL_PANE_SCREENS';
-import createResponsiveStackNavigator from './createResponsiveStackNavigator';
+import createRootStackNavigator from './createRootStackNavigator';
+import {reportsSplitsWithEnteringAnimation, workspaceSplitsWithoutEnteringAnimation} from './createRootStackNavigator/GetStateForActionHandlers';
import defaultScreenOptions from './defaultScreenOptions';
-import hideKeyboardOnSwipe from './hideKeyboardOnSwipe';
-import BottomTabNavigator from './Navigators/BottomTabNavigator';
import ExplanationModalNavigator from './Navigators/ExplanationModalNavigator';
import FeatureTrainingModalNavigator from './Navigators/FeatureTrainingModalNavigator';
-import FullScreenNavigator from './Navigators/FullScreenNavigator';
import LeftModalNavigator from './Navigators/LeftModalNavigator';
import MigratedUserWelcomeModalNavigator from './Navigators/MigratedUserWelcomeModalNavigator';
import OnboardingModalNavigator from './Navigators/OnboardingModalNavigator';
import RightModalNavigator from './Navigators/RightModalNavigator';
import WelcomeVideoModalNavigator from './Navigators/WelcomeVideoModalNavigator';
-import useRootNavigatorOptions from './useRootNavigatorOptions';
+import useRootNavigatorScreenOptions from './useRootNavigatorScreenOptions';
type AuthScreensProps = {
/** Session of currently logged in user */
@@ -101,29 +93,10 @@ const loadReportAvatar = () => require('../../../pages/Rep
const loadReceiptView = () => require('../../../pages/TransactionReceiptPage').default;
const loadWorkspaceJoinUser = () => require('@pages/workspace/WorkspaceJoinUserPage').default;
-function getCentralPaneScreenInitialParams(screenName: CentralPaneName, initialReportID?: string): Partial> {
- if (screenName === SCREENS.SEARCH.CENTRAL_PANE) {
- // Generate default query string with buildSearchQueryString without argument.
- return {q: SearchQueryUtils.buildSearchQueryString()};
- }
-
- if (screenName === SCREENS.REPORT) {
- return {
- openOnAdminRoom: shouldOpenOnAdminRoom() ? true : undefined,
- reportID: initialReportID,
- };
- }
-
- return undefined;
-}
-
-function getCentralPaneScreenListeners(screenName: CentralPaneName) {
- if (screenName === SCREENS.REPORT) {
- return {beforeRemove: beforeRemoveReportOpenedFromSearchRHP};
- }
-
- return {};
-}
+const loadReportSplitNavigator = () => require('./Navigators/ReportsSplitNavigator').default;
+const loadSettingsSplitNavigator = () => require('./Navigators/SettingsSplitNavigator').default;
+const loadWorkspaceSplitNavigator = () => require('./Navigators/WorkspaceSplitNavigator').default;
+const loadSearchPage = () => require('@pages/Search/SearchPage').default;
function initializePusher() {
return Pusher.init({
@@ -203,7 +176,7 @@ function handleNetworkReconnect() {
}
}
-const RootStack = createResponsiveStackNavigator();
+const RootStack = createRootStackNavigator();
// We want to delay the re-rendering for components(e.g. ReportActionCompose)
// that depends on modal visibility until Modal is completely closed and its focused
// When modal screen is focused, update modal visibility in Onyx
@@ -242,11 +215,8 @@ const modalScreenListenersWithCancelSearch = {
function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDAppliedToClient}: AuthScreensProps) {
const theme = useTheme();
- const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const rootNavigatorOptions = useRootNavigatorOptions();
- const {canUseDefaultRooms} = usePermissions();
- const {activeWorkspaceID} = useActiveWorkspace();
+ const rootNavigatorScreenOptions = useRootNavigatorScreenOptions();
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const {toggleSearch} = useSearchRouterContext();
@@ -266,17 +236,6 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
return NativeModules.HybridAppModule && Navigation.getActiveRoute().includes(ROUTES.ONBOARDING_EMPLOYEES.route) && isOnboardingCompleted === true;
}, [isOnboardingCompleted]);
- const [initialReportID] = useState(() => {
- const currentURL = getCurrentUrl();
- const reportIdFromPath = currentURL && new URL(currentURL).pathname.match(CONST.REGEX.REPORT_ID_FROM_PATH)?.at(1);
- if (reportIdFromPath) {
- return reportIdFromPath;
- }
-
- const initialReport = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom(), activeWorkspaceID);
- return initialReport?.reportID;
- });
-
useEffect(() => {
NavBarManager.setButtonStyle(theme.navigationBarButtonsStyle);
@@ -427,10 +386,49 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
+ // Animation is disabled when navigating to the sidebar screen
+ const getWorkspaceSplitNavigatorOptions = ({route}: {route: RouteProp}) => {
+ // We don't need to do anything special for the wide screen.
+ if (!shouldUseNarrowLayout) {
+ return rootNavigatorScreenOptions.splitNavigator;
+ }
+
+ // On the narrow screen, we want to animate this navigator if it is opened from the settings split.
+ // If it is opened from other tab, we don't want to animate it on the entry.
+ // There is a hook inside the workspace navigator that changes animation to SLIDE_FROM_RIGHT after entering.
+ // This way it can be animated properly when going back to the settings split.
+ const animationEnabled = !workspaceSplitsWithoutEnteringAnimation.has(route.key);
+
+ return {
+ ...rootNavigatorScreenOptions.splitNavigator,
+
+ // Allow swipe to go back from this split navigator to the settings navigator.
+ gestureEnabled: true,
+ animation: animationEnabled ? Animations.SLIDE_FROM_RIGHT : Animations.NONE,
+ };
+ };
+
+ // Animation is enabled when navigating to the report screen
+ const getReportsSplitNavigatorOptions = ({route}: {route: RouteProp}) => {
+ // We don't need to do anything special for the wide screen.
+ if (!shouldUseNarrowLayout) {
+ return rootNavigatorScreenOptions.splitNavigator;
+ }
+
+ // On the narrow screen, we want to animate this navigator if pushed ReportsSplitNavigator includes ReportScreen
+ const animationEnabled = reportsSplitsWithEnteringAnimation.has(route.key);
+
+ return {
+ ...rootNavigatorScreenOptions.splitNavigator,
+ animation: animationEnabled ? Animations.SLIDE_FROM_RIGHT : Animations.NONE,
+ };
+ };
+
const clearStatus = () => {
User.clearCustomStatus();
User.clearDraftCustomStatus();
};
+
useEffect(() => {
if (!currentUserPersonalDetails.status?.clearAfter) {
return;
@@ -453,198 +451,184 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
clearStatus();
}, [currentUserPersonalDetails.status?.clearAfter]);
- const CentralPaneScreenOptions: PlatformStackNavigationOptions = {
- ...hideKeyboardOnSwipe,
- headerShown: false,
- title: 'New Expensify',
- web: {
- // Prevent unnecessary scrolling
- cardStyle: styles.cardStyleNavigator,
- },
- };
-
return (
-
-
-
-
+
+
+ {/* This has to be the first navigator in auth screens. */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(isOnboardingCompleted === false || shouldRenderOnboardingExclusivelyOnHybridApp) && (
{
+ Modal.setDisableDismissOnEscape(true);
+ },
+ beforeRemove: () => Modal.setDisableDismissOnEscape(false),
}}
- listeners={fullScreenListeners}
- getComponent={loadValidateLoginPage}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {(isOnboardingCompleted === false || shouldRenderOnboardingExclusivelyOnHybridApp) && (
- {
- Modal.setDisableDismissOnEscape(true);
- },
- beforeRemove: () => Modal.setDisableDismissOnEscape(false),
- }}
- />
- )}
-
-
-
-
- {Object.entries(CENTRAL_PANE_SCREENS).map(([screenName, componentGetter]) => {
- const centralPaneName = screenName as CentralPaneName;
- return (
-
- );
- })}
-
-
-
-
+ )}
+
+
+
+
+
+
+
);
diff --git a/src/libs/Navigation/AppNavigator/CENTRAL_PANE_SCREENS.tsx b/src/libs/Navigation/AppNavigator/CENTRAL_PANE_SCREENS.tsx
deleted file mode 100644
index 5bbe2046040a..000000000000
--- a/src/libs/Navigation/AppNavigator/CENTRAL_PANE_SCREENS.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import type {CentralPaneName} from '@libs/Navigation/types';
-import withPrepareCentralPaneScreen from '@src/components/withPrepareCentralPaneScreen';
-import SCREENS from '@src/SCREENS';
-import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
-
-type Screens = Partial React.ComponentType>>;
-
-const CENTRAL_PANE_SCREENS = {
- [SCREENS.SETTINGS.WORKSPACES]: withPrepareCentralPaneScreen(() => require('../../../pages/workspace/WorkspacesListPage').default),
- [SCREENS.SETTINGS.PREFERENCES.ROOT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Preferences/PreferencesPage').default),
- [SCREENS.SETTINGS.SECURITY]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Security/SecuritySettingsPage').default),
- [SCREENS.SETTINGS.PROFILE.ROOT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Profile/ProfilePage').default),
- [SCREENS.SETTINGS.WALLET.ROOT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Wallet/WalletPage').default),
- [SCREENS.SETTINGS.ABOUT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/AboutPage/AboutPage').default),
- [SCREENS.SETTINGS.TROUBLESHOOT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Troubleshoot/TroubleshootPage').default),
- [SCREENS.SETTINGS.SAVE_THE_WORLD]: withPrepareCentralPaneScreen(() => require('../../../pages/TeachersUnite/SaveTheWorldPage').default),
- [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: withPrepareCentralPaneScreen(() => require('../../../pages/settings/Subscription/SubscriptionSettingsPage').default),
- [SCREENS.SEARCH.CENTRAL_PANE]: withPrepareCentralPaneScreen(() => require('../../../pages/Search/SearchPage').default),
- [SCREENS.REPORT]: withPrepareCentralPaneScreen(() => require('../../../pages/home/ReportScreen').default),
-} satisfies Screens;
-
-export default CENTRAL_PANE_SCREENS;
diff --git a/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.native.ts b/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.native.ts
new file mode 100644
index 000000000000..a0e386be0e0d
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.native.ts
@@ -0,0 +1,11 @@
+import type {NavigationState} from '@react-navigation/native';
+import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
+
+function getIsScreenBlurred(state: NavigationState, currentRouteKey: string) {
+ // If the screen is one of the last two fullscreen routes in the stack, it is not freezed on native platforms.
+ // One screen below the focused one should not be freezed to allow users to return by swiping left.
+ const lastTwoFullScreenRoutes = state.routes.filter((route) => isFullScreenName(route.name)).slice(-2);
+ return !lastTwoFullScreenRoutes.some((route) => route.key === currentRouteKey);
+}
+
+export default getIsScreenBlurred;
diff --git a/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.ts b/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.ts
new file mode 100644
index 000000000000..0b51e817a0ba
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/FreezeWrapper/getIsScreenBlurred/index.ts
@@ -0,0 +1,9 @@
+import type {NavigationState} from '@react-navigation/native';
+import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
+
+function getIsScreenBlurred(state: NavigationState, currentRouteKey: string) {
+ const lastFullScreenRoute = state.routes.findLast((route) => isFullScreenName(route.name));
+ return lastFullScreenRoute?.key !== currentRouteKey;
+}
+
+export default getIsScreenBlurred;
diff --git a/src/libs/Navigation/AppNavigator/FreezeWrapper/index.native.tsx b/src/libs/Navigation/AppNavigator/FreezeWrapper/index.native.tsx
new file mode 100644
index 000000000000..d61166fde419
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/FreezeWrapper/index.native.tsx
@@ -0,0 +1,23 @@
+import {useNavigation, useRoute} from '@react-navigation/native';
+import React, {useEffect, useState} from 'react';
+import {Freeze} from 'react-freeze';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+import getIsScreenBlurred from './getIsScreenBlurred';
+
+function FreezeWrapper({children}: ChildrenProps) {
+ const navigation = useNavigation();
+ const currentRoute = useRoute();
+
+ const [isScreenBlurred, setIsScreenBlurred] = useState(false);
+
+ useEffect(() => {
+ const unsubscribe = navigation.addListener('state', (e) => setIsScreenBlurred(getIsScreenBlurred(e.data.state, currentRoute.key)));
+ return () => unsubscribe();
+ }, [currentRoute.key, navigation]);
+
+ return {children};
+}
+
+FreezeWrapper.displayName = 'FreezeWrapper';
+
+export default FreezeWrapper;
diff --git a/src/libs/Navigation/AppNavigator/FreezeWrapper/index.tsx b/src/libs/Navigation/AppNavigator/FreezeWrapper/index.tsx
new file mode 100644
index 000000000000..129543ce7ba5
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/FreezeWrapper/index.tsx
@@ -0,0 +1,30 @@
+import {useNavigation, useRoute} from '@react-navigation/native';
+import React, {useEffect, useLayoutEffect, useState} from 'react';
+import {Freeze} from 'react-freeze';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
+import getIsScreenBlurred from './getIsScreenBlurred';
+
+function FreezeWrapper({children}: ChildrenProps) {
+ const navigation = useNavigation();
+ const currentRoute = useRoute();
+
+ const [isScreenBlurred, setIsScreenBlurred] = useState(false);
+ const [freezed, setFreezed] = useState(false);
+
+ useEffect(() => {
+ const unsubscribe = navigation.addListener('state', (e) => setIsScreenBlurred(getIsScreenBlurred(e.data.state, currentRoute.key)));
+ return () => unsubscribe();
+ }, [currentRoute.key, navigation]);
+
+ // Decouple the Suspense render task so it won't be interrupted by React's concurrent mode
+ // and stuck in an infinite loop
+ useLayoutEffect(() => {
+ setFreezed(isScreenBlurred);
+ }, [isScreenBlurred]);
+
+ return {children};
+}
+
+FreezeWrapper.displayName = 'FreezeWrapper';
+
+export default FreezeWrapper;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index ba585268cd6c..404e60609eaf 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -598,8 +598,6 @@ const SettingsModalStackNavigator = createModalStackNavigator
require('../../../../pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateRolePage').default,
[SCREENS.SETTINGS.DELEGATE.DELEGATE_CONFIRM]: () => require('../../../../pages/settings/Security/AddDelegate/ConfirmDelegatePage').default,
- [SCREENS.SETTINGS.DELEGATE.UPDATE_DELEGATE_ROLE_MAGIC_CODE]: () =>
- require('../../../../pages/settings/Security/AddDelegate/UpdateDelegateRole/UpdateDelegateMagicCodePage').default,
[SCREENS.WORKSPACE.RULES_CUSTOM_NAME]: () => require('../../../../pages/workspace/rules/RulesCustomNamePage').default,
[SCREENS.WORKSPACE.RULES_AUTO_APPROVE_REPORTS_UNDER]: () => require('../../../../pages/workspace/rules/RulesAutoApproveReportsUnderPage').default,
[SCREENS.WORKSPACE.RULES_RANDOM_REPORT_AUDIT]: () => require('../../../../pages/workspace/rules/RulesRandomReportAuditPage').default,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/ActiveCentralPaneRouteContext.ts b/src/libs/Navigation/AppNavigator/Navigators/ActiveCentralPaneRouteContext.ts
deleted file mode 100644
index 6f37126584a2..000000000000
--- a/src/libs/Navigation/AppNavigator/Navigators/ActiveCentralPaneRouteContext.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react';
-import type {AuthScreensParamList, NavigationPartialRoute} from '@libs/Navigation/types';
-
-const ActiveCentralPaneRouteContext = React.createContext | undefined>(undefined);
-
-export default ActiveCentralPaneRouteContext;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
deleted file mode 100644
index b4b71549f7ec..000000000000
--- a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import {useNavigationState} from '@react-navigation/native';
-import React from 'react';
-import createCustomBottomTabNavigator from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator';
-import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
-import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
-import type {PlatformStackNavigationOptions} from '@libs/Navigation/PlatformStackNavigation/types';
-import type {BottomTabNavigatorParamList, CentralPaneName, NavigationPartialRoute, RootStackParamList} from '@libs/Navigation/types';
-import SidebarScreen from '@pages/home/sidebar/SidebarScreen';
-import SearchPageBottomTab from '@pages/Search/SearchPageBottomTab';
-import SCREENS from '@src/SCREENS';
-import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
-import ActiveCentralPaneRouteContext from './ActiveCentralPaneRouteContext';
-
-const loadInitialSettingsPage = () => require('../../../../pages/settings/InitialSettingsPage').default;
-const Tab = createCustomBottomTabNavigator();
-
-const screenOptions: PlatformStackNavigationOptions = {
- animation: Animations.FADE,
- headerShown: false,
-};
-
-function BottomTabNavigator() {
- const activeRoute = useNavigationState | undefined>(getTopmostCentralPaneRoute);
- return (
-
-
-
-
-
-
-
- );
-}
-
-BottomTabNavigator.displayName = 'BottomTabNavigator';
-
-export default BottomTabNavigator;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
deleted file mode 100644
index 86c9bce765b7..000000000000
--- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React from 'react';
-import {View} from 'react-native';
-import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useThemeStyles from '@hooks/useThemeStyles';
-import createCustomFullScreenNavigator from '@libs/Navigation/AppNavigator/createCustomFullScreenNavigator';
-import useRootNavigatorOptions from '@libs/Navigation/AppNavigator/useRootNavigatorOptions';
-import type {FullScreenNavigatorParamList} from '@libs/Navigation/types';
-import SCREENS from '@src/SCREENS';
-import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
-
-const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default;
-
-const RootStack = createCustomFullScreenNavigator();
-
-type Screens = Partial React.ComponentType>>;
-
-const CENTRAL_PANE_WORKSPACE_SCREENS = {
- [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceProfilePage').default,
- [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default,
- [SCREENS.WORKSPACE.INVOICES]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicesPage').default,
- [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../pages/workspace/WorkspaceMembersPage').default,
- [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/accounting/PolicyAccountingPage').default,
- [SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesPage').default,
- [SCREENS.WORKSPACE.MORE_FEATURES]: () => require('../../../../pages/workspace/WorkspaceMoreFeaturesPage').default,
- [SCREENS.WORKSPACE.TAGS]: () => require('../../../../pages/workspace/tags/WorkspaceTagsPage').default,
- [SCREENS.WORKSPACE.TAXES]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesPage').default,
- [SCREENS.WORKSPACE.REPORT_FIELDS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default,
- [SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default,
- [SCREENS.WORKSPACE.COMPANY_CARDS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardsPage').default,
- [SCREENS.WORKSPACE.PER_DIEM]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemPage').default,
- [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default,
- [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default,
-} satisfies Screens;
-
-function FullScreenNavigator() {
- const styles = useThemeStyles();
- const {shouldUseNarrowLayout} = useResponsiveLayout();
- const rootNavigatorOptions = useRootNavigatorOptions();
-
- return (
-
-
-
-
- {Object.entries(CENTRAL_PANE_WORKSPACE_SCREENS).map(([screenName, componentGetter]) => (
-
- ))}
-
-
-
- );
-}
-
-FullScreenNavigator.displayName = 'FullScreenNavigator';
-
-export {CENTRAL_PANE_WORKSPACE_SCREENS};
-export default FullScreenNavigator;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx
new file mode 100644
index 000000000000..1e1672565ab3
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/Navigators/ReportsSplitNavigator.tsx
@@ -0,0 +1,68 @@
+import React, {useState} from 'react';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
+import usePermissions from '@hooks/usePermissions';
+import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
+import FreezeWrapper from '@libs/Navigation/AppNavigator/FreezeWrapper';
+import useSplitNavigatorScreenOptions from '@libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions';
+import getCurrentUrl from '@libs/Navigation/currentUrl';
+import shouldOpenOnAdminRoom from '@libs/Navigation/helpers/shouldOpenOnAdminRoom';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {AuthScreensParamList, ReportsSplitNavigatorParamList} from '@libs/Navigation/types';
+import * as ReportUtils from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import type NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
+
+const loadReportScreen = () => require('@pages/home/ReportScreen').default;
+const loadSidebarScreen = () => require('@pages/home/sidebar/BaseSidebarScreen').default;
+
+const Split = createSplitNavigator();
+
+/**
+ * This SplitNavigator includes the HOME screen ( component) with a list of reports as a sidebar screen and the REPORT screen displayed as a central one.
+ * There can be multiple report screens in the stack with different report IDs.
+ */
+function ReportsSplitNavigator({route}: PlatformStackScreenProps) {
+ const {canUseDefaultRooms} = usePermissions();
+ const {activeWorkspaceID} = useActiveWorkspace();
+ const splitNavigatorScreenOptions = useSplitNavigatorScreenOptions();
+
+ const [initialReportID] = useState(() => {
+ const currentURL = getCurrentUrl();
+ const reportIdFromPath = currentURL && new URL(currentURL).pathname.match(CONST.REGEX.REPORT_ID_FROM_PATH)?.at(1);
+ if (reportIdFromPath) {
+ return reportIdFromPath;
+ }
+
+ const initialReport = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom(), activeWorkspaceID);
+ return initialReport?.reportID;
+ });
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+ReportsSplitNavigator.displayName = 'ReportsSplitNavigator';
+
+export default ReportsSplitNavigator;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx
new file mode 100644
index 000000000000..1a90cdb5313d
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx
@@ -0,0 +1,63 @@
+import {useRoute} from '@react-navigation/native';
+import React from 'react';
+import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
+import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
+import useSplitNavigatorScreenOptions from '@libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions';
+import type {SettingsSplitNavigatorParamList} from '@libs/Navigation/types';
+import SCREENS from '@src/SCREENS';
+import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
+
+const loadInitialSettingsPage = () => require('../../../../pages/settings/InitialSettingsPage').default;
+
+type Screens = Partial React.ComponentType>>;
+
+const CENTRAL_PANE_SETTINGS_SCREENS = {
+ [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../../pages/workspace/WorkspacesListPage').default,
+ [SCREENS.SETTINGS.PREFERENCES.ROOT]: () => require('../../../../pages/settings/Preferences/PreferencesPage').default,
+ [SCREENS.SETTINGS.SECURITY]: () => require('../../../../pages/settings/Security/SecuritySettingsPage').default,
+ [SCREENS.SETTINGS.PROFILE.ROOT]: () => require('../../../../pages/settings/Profile/ProfilePage').default,
+ [SCREENS.SETTINGS.WALLET.ROOT]: () => require('../../../../pages/settings/Wallet/WalletPage').default,
+ [SCREENS.SETTINGS.ABOUT]: () => require('../../../../pages/settings/AboutPage/AboutPage').default,
+ [SCREENS.SETTINGS.TROUBLESHOOT]: () => require('../../../../pages/settings/Troubleshoot/TroubleshootPage').default,
+ [SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default,
+ [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: () => require('../../../../pages/settings/Subscription/SubscriptionSettingsPage').default,
+} satisfies Screens;
+
+const Split = createSplitNavigator();
+
+function SettingsSplitNavigator() {
+ const route = useRoute();
+ const splitNavigatorScreenOptions = useSplitNavigatorScreenOptions();
+
+ return (
+
+
+
+ {Object.entries(CENTRAL_PANE_SETTINGS_SCREENS).map(([screenName, componentGetter]) => {
+ return (
+
+ );
+ })}
+
+
+ );
+}
+
+SettingsSplitNavigator.displayName = 'SettingsSplitNavigator';
+
+export {CENTRAL_PANE_SETTINGS_SCREENS};
+export default SettingsSplitNavigator;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx
new file mode 100644
index 000000000000..c1b0fcc481f7
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx
@@ -0,0 +1,86 @@
+import React, {useEffect} from 'react';
+import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
+import {workspaceSplitsWithoutEnteringAnimation} from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers';
+import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
+import useSplitNavigatorScreenOptions from '@libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions';
+import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {AuthScreensParamList, WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
+import type NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
+
+type Screens = Partial React.ComponentType>>;
+
+const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default;
+
+const CENTRAL_PANE_WORKSPACE_SCREENS = {
+ [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceProfilePage').default,
+ [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default,
+ [SCREENS.WORKSPACE.INVOICES]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicesPage').default,
+ [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../pages/workspace/WorkspaceMembersPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: () => require('../../../../pages/workspace/accounting/PolicyAccountingPage').default,
+ [SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesPage').default,
+ [SCREENS.WORKSPACE.MORE_FEATURES]: () => require('../../../../pages/workspace/WorkspaceMoreFeaturesPage').default,
+ [SCREENS.WORKSPACE.TAGS]: () => require('../../../../pages/workspace/tags/WorkspaceTagsPage').default,
+ [SCREENS.WORKSPACE.TAXES]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesPage').default,
+ [SCREENS.WORKSPACE.REPORT_FIELDS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default,
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default,
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardsPage').default,
+ [SCREENS.WORKSPACE.PER_DIEM]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemPage').default,
+ [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default,
+ [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default,
+} satisfies Screens;
+
+const Split = createSplitNavigator();
+
+function WorkspaceSplitNavigator({route, navigation}: PlatformStackScreenProps) {
+ const splitNavigatorScreenOptions = useSplitNavigatorScreenOptions();
+
+ useEffect(() => {
+ const unsubscribe = navigation.addListener('transitionEnd', () => {
+ // We want to call this function only once.
+ unsubscribe();
+
+ // If we open this screen from a different tab, then it won't have animation.
+ if (!workspaceSplitsWithoutEnteringAnimation.has(route.key)) {
+ return;
+ }
+
+ // We want to set animation after mounting so it will animate on going UP to the settings split.
+ navigation.setOptions({animation: Animations.SLIDE_FROM_RIGHT});
+ });
+
+ return unsubscribe;
+ }, [navigation, route.key]);
+
+ return (
+
+
+
+ {Object.entries(CENTRAL_PANE_WORKSPACE_SCREENS).map(([screenName, componentGetter]) => (
+
+ ))}
+
+
+ );
+}
+
+WorkspaceSplitNavigator.displayName = 'WorkspaceSplitNavigator';
+
+export {CENTRAL_PANE_WORKSPACE_SCREENS};
+export default WorkspaceSplitNavigator;
diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.tsx b/src/libs/Navigation/AppNavigator/PublicScreens.tsx
index 00b9a98e78bd..77e379e80293 100644
--- a/src/libs/Navigation/AppNavigator/PublicScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/PublicScreens.tsx
@@ -20,9 +20,10 @@ const RootStack = createPlatformStackNavigator();
function PublicScreens() {
return (
- {/* The structure for the HOME route has to be the same in public and auth screens. That's why the name for SignInPage is BOTTOM_TAB_NAVIGATOR. */}
+ {/* The structure for the HOME route has to be the same in public and auth screens. That's why the name for SignInPage is REPORTS_SPLIT_NAVIGATOR. */}
) {
- if (!navigationRef.current) {
- return;
- }
-
- const state = navigationRef.current?.getRootState() as State;
-
- if (!state) {
- return;
- }
-
- const shouldPopHome =
- state.routes?.length >= 3 &&
- state.routes.at(-1)?.name === SCREENS.REPORT &&
- state.routes.at(-2)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR &&
- state.routes.at(-3)?.name === SCREENS.SEARCH.CENTRAL_PANE &&
- getTopmostBottomTabRoute(state)?.name === SCREENS.HOME;
-
- if (!shouldPopHome) {
- return;
- }
-
- event.preventDefault();
- const bottomTabState = state?.routes?.at(0)?.state;
- navigationRef.dispatch({...StackActions.pop(), target: bottomTabState?.key});
- Navigation.goBack();
-}
-
-export default beforeRemoveReportOpenedFromSearchRHP;
diff --git a/src/libs/Navigation/AppNavigator/beforeRemoveReportOpenedFromSearchRHP/index.ts b/src/libs/Navigation/AppNavigator/beforeRemoveReportOpenedFromSearchRHP/index.ts
deleted file mode 100644
index b5d8f835ab43..000000000000
--- a/src/libs/Navigation/AppNavigator/beforeRemoveReportOpenedFromSearchRHP/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-/** The issue fixed by this handler is related to navigating back on native platforms. For more information, see the index.native.ts file in this folder */
-function beforeRemoveReportOpenedFromSearchRHP() {}
-
-export default beforeRemoveReportOpenedFromSearchRHP;
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabNavigationContentWrapper.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabNavigationContentWrapper.tsx
deleted file mode 100644
index dd93a6df7b1e..000000000000
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabNavigationContentWrapper.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import {View} from 'react-native';
-import ScreenWrapper from '@components/ScreenWrapper';
-import useThemeStyles from '@hooks/useThemeStyles';
-import type {NavigationContentWrapperProps} from '@libs/Navigation/PlatformStackNavigation/types';
-
-function BottomTabNavigationContentWrapper({children, displayName}: NavigationContentWrapperProps) {
- const styles = useThemeStyles();
-
- return (
-
- {children}
-
- );
-}
-
-export default BottomTabNavigationContentWrapper;
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx
deleted file mode 100644
index 2461c542ec7d..000000000000
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import type {ParamListBase} from '@react-navigation/native';
-import {createNavigatorFactory} from '@react-navigation/native';
-import React from 'react';
-import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent';
-import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
-import type {ExtraContentProps, PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
-import BottomTabBar from './BottomTabBar';
-import BottomTabNavigationContentWrapper from './BottomTabNavigationContentWrapper';
-import useCustomState from './useCustomState';
-
-const defaultScreenOptions: PlatformStackNavigationOptions = {
- animation: Animations.NONE,
-};
-
-function ExtraContent({state}: ExtraContentProps) {
- const selectedTab = state.routes.at(-1)?.name;
- return ;
-}
-
-const CustomBottomTabNavigatorComponent = createPlatformStackNavigatorComponent('CustomBottomTabNavigator', {
- useCustomState,
- defaultScreenOptions,
- NavigationContentWrapper: BottomTabNavigationContentWrapper,
- ExtraContent,
-});
-
-function createCustomBottomTabNavigator() {
- return createNavigatorFactory, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, typeof CustomBottomTabNavigatorComponent>(
- CustomBottomTabNavigatorComponent,
- )();
-}
-
-export default createCustomBottomTabNavigator;
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/useCustomState.ts b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/useCustomState.ts
deleted file mode 100644
index cf8ffd81840f..000000000000
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/useCustomState.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {useMemo} from 'react';
-import type {CustomStateHookProps} from '@libs/Navigation/PlatformStackNavigation/types';
-import type {NavigationStateRoute} from '@libs/Navigation/types';
-import SCREENS from '@src/SCREENS';
-
-function useCustomState({state}: CustomStateHookProps) {
- return useMemo(() => {
- const routesToRender = [state.routes.at(-1)] as NavigationStateRoute[];
-
- // We need to render at least one HOME screen to make sure everything load properly. This may be not necessary after changing how IS_SIDEBAR_LOADED is handled.
- // Currently this value will be switched only after the first HOME screen is rendered.
- if (routesToRender.at(0)?.name !== SCREENS.HOME) {
- const routeToRender = state.routes.find((route) => route.name === SCREENS.HOME);
- if (routeToRender) {
- routesToRender.unshift(routeToRender);
- }
- }
-
- return {stateToRender: {...state, routes: routesToRender, index: routesToRender.length - 1}};
- }, [state]);
-}
-
-export default useCustomState;
diff --git a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/CustomFullScreenRouter.tsx b/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/CustomFullScreenRouter.tsx
deleted file mode 100644
index ba8de1f298bd..000000000000
--- a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/CustomFullScreenRouter.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import type {ParamListBase, PartialState, RouterConfigOptions} from '@react-navigation/native';
-import {StackRouter} from '@react-navigation/native';
-import Onyx from 'react-native-onyx';
-import getIsNarrowLayout from '@libs/getIsNarrowLayout';
-import type {PlatformStackNavigationState, PlatformStackRouterOptions} from '@libs/Navigation/PlatformStackNavigation/types';
-import * as PolicyUtils from '@libs/PolicyUtils';
-import ONYXKEYS from '@src/ONYXKEYS';
-import SCREENS from '@src/SCREENS';
-
-type StackState = PlatformStackNavigationState | PartialState>;
-
-const isAtLeastOneInState = (state: StackState, screenName: string): boolean => state.routes.some((route) => route.name === screenName);
-
-let isLoadingReportData = true;
-Onyx.connect({
- key: ONYXKEYS.IS_LOADING_REPORT_DATA,
- initWithStoredValues: false,
- callback: (value) => (isLoadingReportData = value ?? false),
-});
-
-function adaptStateIfNecessary(state: StackState) {
- const isNarrowLayout = getIsNarrowLayout();
- const workspaceCentralPane = state.routes.at(-1);
- const policyID =
- workspaceCentralPane?.params && 'policyID' in workspaceCentralPane.params && typeof workspaceCentralPane.params.policyID === 'string'
- ? workspaceCentralPane.params.policyID
- : undefined;
- const policy = PolicyUtils.getPolicy(policyID ?? '');
- const isPolicyAccessible = PolicyUtils.isPolicyAccessible(policy);
-
- // There should always be WORKSPACE.INITIAL screen in the state to make sure go back works properly if we deeplinkg to a subpage of settings.
- // The only exception is when the workspace is invalid or inaccessible.
- if (!isAtLeastOneInState(state, SCREENS.WORKSPACE.INITIAL)) {
- if (isNarrowLayout && !isLoadingReportData && !isPolicyAccessible) {
- return;
- }
- // @ts-expect-error Updating read only property
- // noinspection JSConstantReassignment
- state.stale = true; // eslint-disable-line
-
- // This is necessary for ts to narrow type down to PartialState.
- if (state.stale === true) {
- // Unshift the root screen to fill left pane.
- state.routes.unshift({
- name: SCREENS.WORKSPACE.INITIAL,
- params: workspaceCentralPane?.params,
- });
- }
- }
-
- // If the screen is wide, there should be at least two screens inside:
- // - WORKSPACE.INITIAL to cover left pane.
- // - WORKSPACE.PROFILE (first workspace settings screen) to cover central pane.
- if (!isNarrowLayout) {
- if (state.routes.length === 1 && state.routes.at(0)?.name === SCREENS.WORKSPACE.INITIAL) {
- // @ts-expect-error Updating read only property
- // noinspection JSConstantReassignment
- state.stale = true; // eslint-disable-line
- // Push the default settings central pane screen.
- if (state.stale === true) {
- state.routes.push({
- name: SCREENS.WORKSPACE.PROFILE,
- params: state.routes.at(0)?.params,
- });
- }
- }
- // eslint-disable-next-line no-param-reassign, @typescript-eslint/non-nullable-type-assertion-style
- (state.index as number) = state.routes.length - 1;
- }
-}
-
-function CustomFullScreenRouter(options: PlatformStackRouterOptions) {
- const stackRouter = StackRouter(options);
-
- return {
- ...stackRouter,
- getInitialState({routeNames, routeParamList, routeGetIdList}: RouterConfigOptions) {
- const initialState = stackRouter.getInitialState({routeNames, routeParamList, routeGetIdList});
- adaptStateIfNecessary(initialState);
-
- // If we needed to modify the state we need to rehydrate it to get keys for new routes.
- if (initialState.stale) {
- return stackRouter.getRehydratedState(initialState, {routeNames, routeParamList, routeGetIdList});
- }
-
- return initialState;
- },
- getRehydratedState(partialState: StackState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): PlatformStackNavigationState {
- adaptStateIfNecessary(partialState);
- const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList});
- return state;
- },
- };
-}
-
-export default CustomFullScreenRouter;
diff --git a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx
deleted file mode 100644
index f3d605e1824f..000000000000
--- a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type {ParamListBase} from '@react-navigation/native';
-import {createNavigatorFactory} from '@react-navigation/native';
-import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange';
-import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent';
-import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions';
-import type {PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
-import CustomFullScreenRouter from './CustomFullScreenRouter';
-
-const CustomFullScreenNavigatorComponent = createPlatformStackNavigatorComponent('CustomFullScreenNavigator', {
- createRouter: CustomFullScreenRouter,
- useCustomEffects: useNavigationResetOnLayoutChange,
- defaultScreenOptions: defaultPlatformStackScreenOptions,
-});
-
-function createCustomFullScreenNavigator() {
- return createNavigatorFactory, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, typeof CustomFullScreenNavigatorComponent>(
- CustomFullScreenNavigatorComponent,
- )();
-}
-
-export default createCustomFullScreenNavigator;
diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts
deleted file mode 100644
index 3cbb5acb81e5..000000000000
--- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/CustomRouter.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native';
-import {findFocusedRoute, getPathFromState, StackRouter} from '@react-navigation/native';
-import type {ParamListBase} from '@react-navigation/routers';
-import getIsNarrowLayout from '@libs/getIsNarrowLayout';
-import * as Localize from '@libs/Localize';
-import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
-import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
-import {linkingConfig} from '@libs/Navigation/linkingConfig';
-import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath';
-import type {PlatformStackRouterOptions} from '@libs/Navigation/PlatformStackNavigation/types';
-import type {NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types';
-import {isCentralPaneName, isOnboardingFlowName} from '@libs/NavigationUtils';
-import * as Welcome from '@userActions/Welcome';
-import CONST from '@src/CONST';
-import NAVIGATORS from '@src/NAVIGATORS';
-import SCREENS from '@src/SCREENS';
-import syncBrowserHistory from './syncBrowserHistory';
-
-function insertRootRoute(state: State, routeToInsert: NavigationPartialRoute) {
- const nonModalRoutes = state.routes.filter(
- (route) => route.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR && route.name !== NAVIGATORS.LEFT_MODAL_NAVIGATOR && route.name !== NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR,
- );
- const modalRoutes = state.routes.filter(
- (route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || route.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR || route.name === NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR,
- );
-
- // It's safe to modify this state before returning in getRehydratedState.
-
- // @ts-expect-error Updating read only property
- // noinspection JSConstantReassignment
- state.routes = [...nonModalRoutes, routeToInsert, ...modalRoutes]; // eslint-disable-line
-
- // @ts-expect-error Updating read only property
- // noinspection JSConstantReassignment
- state.index = state.routes.length - 1; // eslint-disable-line
-
- // @ts-expect-error Updating read only property
- // noinspection JSConstantReassignment
- state.stale = true; // eslint-disable-line
-}
-
-function compareAndAdaptState(state: StackNavigationState) {
- // If the state of the last path is not defined the getPathFromState won't work correctly.
- if (!state?.routes.at(-1)?.state) {
- return;
- }
-
- // We need to be sure that the bottom tab state is defined.
- const topmostBottomTabRoute = getTopmostBottomTabRoute(state);
- const isNarrowLayout = getIsNarrowLayout();
-
- // This solutions is heuristics and will work for our cases. We may need to improve it in the future if we will have more cases to handle.
- if (topmostBottomTabRoute && !isNarrowLayout) {
- const fullScreenRoute = state.routes.find((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR);
-
- // If there is fullScreenRoute we don't need to add anything.
- if (fullScreenRoute) {
- return;
- }
-
- // We will generate a template state and compare the current state with it.
- // If there is a difference in the screens that should be visible under the overlay, we will add the screen from templateState to the current state.
- const pathFromCurrentState = getPathFromState(state, linkingConfig.config);
- const {adaptedState: templateState} = getAdaptedStateFromPath(pathFromCurrentState, linkingConfig.config);
-
- if (!templateState) {
- return;
- }
-
- const templateFullScreenRoute = templateState.routes.find((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR);
-
- // If templateFullScreenRoute is defined, and full screen route is not in the state, we need to add it.
- if (templateFullScreenRoute) {
- insertRootRoute(state, templateFullScreenRoute);
- return;
- }
-
- const topmostCentralPaneRoute = state.routes.filter((route) => isCentralPaneName(route.name)).at(-1);
- const templateCentralPaneRoute = templateState.routes.find((route) => isCentralPaneName(route.name));
-
- const topmostCentralPaneRouteExtracted = getTopmostCentralPaneRoute(state);
- const templateCentralPaneRouteExtracted = getTopmostCentralPaneRoute(templateState as State);
-
- // If there is no templateCentralPaneRoute, we don't have anything to add.
- if (!templateCentralPaneRoute) {
- return;
- }
-
- // If there is no topmostCentralPaneRoute in the state and template state has one, we need to add it.
- if (!topmostCentralPaneRoute) {
- insertRootRoute(state, templateCentralPaneRoute);
- return;
- }
-
- // If there is central pane route in state and template state has one, we need to check if they are the same.
- if (topmostCentralPaneRouteExtracted && templateCentralPaneRouteExtracted && topmostCentralPaneRouteExtracted.name !== templateCentralPaneRouteExtracted.name) {
- // Not every RHP screen has matching central pane defined. In that case we use the REPORT screen as default for initial screen.
- // But we don't want to override the central pane for those screens as they may be opened with different central panes under the overlay.
- // e.g. i-know-a-teacher may be opened with different central panes under the overlay
- if (templateCentralPaneRouteExtracted.name === SCREENS.REPORT) {
- return;
- }
- insertRootRoute(state, templateCentralPaneRoute);
- }
- }
-}
-
-function shouldPreventReset(state: StackNavigationState, action: CommonActions.Action | StackActionType) {
- if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) {
- return false;
- }
- const currentFocusedRoute = findFocusedRoute(state);
- const targetFocusedRoute = findFocusedRoute(action?.payload);
-
- // We want to prevent the user from navigating back to a non-onboarding screen if they are currently on an onboarding screen
- if (isOnboardingFlowName(currentFocusedRoute?.name) && !isOnboardingFlowName(targetFocusedRoute?.name)) {
- Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
- return true;
- }
-
- return false;
-}
-
-function CustomRouter(options: PlatformStackRouterOptions) {
- const stackRouter = StackRouter(options);
-
- return {
- ...stackRouter,
- getRehydratedState(partialState: StackNavigationState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): StackNavigationState {
- compareAndAdaptState(partialState);
- const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList});
- return state;
- },
- getStateForAction(state: StackNavigationState, action: CommonActions.Action | StackActionType, configOptions: RouterConfigOptions) {
- if (shouldPreventReset(state, action)) {
- syncBrowserHistory(state);
- return state;
- }
- return stackRouter.getStateForAction(state, action, configOptions);
- },
- };
-}
-
-export default CustomRouter;
diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/SearchRoute.tsx b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/SearchRoute.tsx
deleted file mode 100644
index 2455587660ab..000000000000
--- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/SearchRoute.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react';
-import {View} from 'react-native';
-import useThemeStyles from '@hooks/useThemeStyles';
-import type {ExtraContentProps} from '@libs/Navigation/PlatformStackNavigation/types';
-
-function SearchRoute({searchRoute, descriptors}: ExtraContentProps) {
- const styles = useThemeStyles();
-
- if (!searchRoute) {
- return null;
- }
-
- const key = searchRoute.key;
- const descriptor = descriptors[key];
-
- if (!descriptor) {
- return null;
- }
-
- return {descriptor.render()};
-}
-
-export default SearchRoute;
diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx
deleted file mode 100644
index 9ac2ffd6c8f9..000000000000
--- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/index.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import type {ParamListBase} from '@react-navigation/native';
-import {createNavigatorFactory} from '@react-navigation/native';
-import useNavigationResetRootOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetRootOnLayoutChange';
-import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent';
-import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions';
-import type {PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
-import CustomRouter from './CustomRouter';
-import RenderSearchRoute from './SearchRoute';
-import useStateWithSearch from './useStateWithSearch';
-
-const ResponsiveStackNavigatorComponent = createPlatformStackNavigatorComponent('ResponsiveStackNavigator', {
- createRouter: CustomRouter,
- defaultScreenOptions: defaultPlatformStackScreenOptions,
- useCustomState: useStateWithSearch,
- useCustomEffects: useNavigationResetRootOnLayoutChange,
- ExtraContent: RenderSearchRoute,
-});
-
-function createResponsiveStackNavigator() {
- return createNavigatorFactory, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, typeof ResponsiveStackNavigatorComponent>(
- ResponsiveStackNavigatorComponent,
- )();
-}
-
-export default createResponsiveStackNavigator;
diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/useStateWithSearch.ts b/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/useStateWithSearch.ts
deleted file mode 100644
index 73984af34d2e..000000000000
--- a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/useStateWithSearch.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import type {ParamListBase} from '@react-navigation/native';
-import {useMemo} from 'react';
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
-import type {CustomStateHookProps, PlatformStackNavigationState, PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
-import type {RootStackParamList, State} from '@libs/Navigation/types';
-import {isCentralPaneName} from '@libs/NavigationUtils';
-import SCREENS from '@src/SCREENS';
-
-type Routes = PlatformStackNavigationState['routes'];
-function reduceCentralPaneRoutes(routes: Routes): Routes {
- const result: Routes = [];
- let count = 0;
- const reverseRoutes = [...routes].reverse();
-
- reverseRoutes.forEach((route) => {
- if (isCentralPaneName(route.name)) {
- // Remove all central pane routes except the last 3. This will improve performance.
- if (count < 3) {
- result.push(route);
- count++;
- }
- } else {
- result.push(route);
- }
- });
-
- return result.reverse();
-}
-
-function useStateWithSearch({state}: CustomStateHookProps) {
- const {shouldUseNarrowLayout} = useResponsiveLayout();
- return useMemo(() => {
- const routes = reduceCentralPaneRoutes(state.routes);
-
- if (shouldUseNarrowLayout) {
- const isSearchCentralPane = (route: PlatformStackRouteProp) =>
- getTopmostCentralPaneRoute({routes: [route]} as State)?.name === SCREENS.SEARCH.CENTRAL_PANE;
-
- const lastRoute = routes.at(-1);
- const lastSearchCentralPane = lastRoute && isSearchCentralPane(lastRoute) ? lastRoute : undefined;
- const filteredRoutes = routes.filter((route) => !isSearchCentralPane(route));
-
- // On narrow layout, if we are on /search route we want to hide all central pane routes and show only the bottom tab navigator.
- if (lastSearchCentralPane) {
- const filteredRoute = filteredRoutes.at(0);
- if (filteredRoute) {
- return {
- stateToRender: {
- ...state,
- index: 0,
- routes: [filteredRoute],
- },
- searchRoute: lastSearchCentralPane,
- };
- }
- }
-
- return {
- stateToRender: {
- ...state,
- index: filteredRoutes.length - 1,
- routes: filteredRoutes,
- },
- searchRoute: undefined,
- };
- }
-
- return {
- stateToRender: {
- ...state,
- index: routes.length - 1,
- routes: [...routes],
- },
- searchRoute: undefined,
- };
- }, [state, shouldUseNarrowLayout]);
-}
-
-export default useStateWithSearch;
diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
new file mode 100644
index 000000000000..a225301101c4
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
@@ -0,0 +1,260 @@
+import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native';
+import {StackActions} from '@react-navigation/native';
+import type {ParamListBase, Router} from '@react-navigation/routers';
+import Log from '@libs/Log';
+import getPolicyIDFromState from '@libs/Navigation/helpers/getPolicyIDFromState';
+import type {RootNavigatorParamList, State} from '@libs/Navigation/types';
+import * as SearchQueryUtils from '@libs/SearchQueryUtils';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import type {OpenWorkspaceSplitActionType, PushActionType, SwitchPolicyIdActionType} from './types';
+
+const MODAL_ROUTES_TO_DISMISS: string[] = [
+ NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
+ NAVIGATORS.LEFT_MODAL_NAVIGATOR,
+ NAVIGATORS.RIGHT_MODAL_NAVIGATOR,
+ NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR,
+ NAVIGATORS.FEATURE_TRANING_MODAL_NAVIGATOR,
+ SCREENS.NOT_FOUND,
+ SCREENS.ATTACHMENTS,
+ SCREENS.TRANSACTION_RECEIPT,
+ SCREENS.PROFILE_AVATAR,
+ SCREENS.WORKSPACE_AVATAR,
+ SCREENS.REPORT_AVATAR,
+ SCREENS.CONCIERGE,
+];
+
+const workspaceSplitsWithoutEnteringAnimation = new Set();
+const reportsSplitsWithEnteringAnimation = new Set();
+
+/**
+ * Handles the OPEN_WORKSPACE_SPLIT action.
+ * If the user is on other tab than settings and the workspace split is "remembered", this action will be called after pressing the settings tab.
+ * It will push the settings split navigator first and then push the workspace split navigator.
+ * This allows the user to swipe back on the iOS to the settings split navigator underneath.
+ */
+function handleOpenWorkspaceSplitAction(
+ state: StackNavigationState,
+ action: OpenWorkspaceSplitActionType,
+ configOptions: RouterConfigOptions,
+ stackRouter: Router, CommonActions.Action | StackActionType>,
+) {
+ const actionToPushSettingsSplitNavigator = StackActions.push(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR, {
+ screen: SCREENS.SETTINGS.WORKSPACES,
+ });
+
+ const actionToPushWorkspaceSplitNavigator = StackActions.push(NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, {
+ screen: SCREENS.WORKSPACE.INITIAL,
+ params: {
+ policyID: action.payload.policyID,
+ },
+ });
+
+ const stateWithSettingsSplitNavigator = stackRouter.getStateForAction(state, actionToPushSettingsSplitNavigator, configOptions);
+
+ if (!stateWithSettingsSplitNavigator) {
+ Log.hmmm('[handleOpenWorkspaceSplitAction] SettingsSplitNavigator has not been found in the navigation state.');
+ return null;
+ }
+
+ const rehydratedStateWithSettingsSplitNavigator = stackRouter.getRehydratedState(stateWithSettingsSplitNavigator, configOptions);
+ const stateWithWorkspaceSplitNavigator = stackRouter.getStateForAction(rehydratedStateWithSettingsSplitNavigator, actionToPushWorkspaceSplitNavigator, configOptions);
+
+ if (!stateWithWorkspaceSplitNavigator) {
+ Log.hmmm('[handleOpenWorkspaceSplitAction] WorkspaceSplitNavigator has not been found in the navigation state.');
+ return null;
+ }
+
+ const lastFullScreenRoute = stateWithWorkspaceSplitNavigator.routes.at(-1);
+
+ if (lastFullScreenRoute?.key) {
+ // If the user opened the workspace split navigator from a different tab, we don't want to animate the entering transition.
+ // To make it feel like bottom tab navigator.
+ workspaceSplitsWithoutEnteringAnimation.add(lastFullScreenRoute.key);
+ }
+
+ return stateWithWorkspaceSplitNavigator;
+}
+
+/**
+ * Handles the SWITCH_POLICY_ID action.
+ * Information about the currently selected policy can be found in the last ReportsSplitNavigator or Search_Root.
+ * As the policy can only be changed from Search or Inbox Tab, after changing the policy a new ReportsSplitNavigator or Search_Root with the changed policy has to be pushed to the navigation state.
+ */
+function handleSwitchPolicyID(
+ state: StackNavigationState,
+ action: SwitchPolicyIdActionType,
+ configOptions: RouterConfigOptions,
+ stackRouter: Router, CommonActions.Action | StackActionType>,
+ setActiveWorkspaceID: (workspaceID: string | undefined) => void,
+) {
+ const lastRoute = state.routes.at(-1);
+ if (lastRoute?.name === SCREENS.SEARCH.ROOT) {
+ const currentParams = lastRoute.params as RootNavigatorParamList[typeof SCREENS.SEARCH.ROOT];
+ const queryJSON = SearchQueryUtils.buildSearchQueryJSON(currentParams.q);
+ if (!queryJSON) {
+ return null;
+ }
+
+ if (action.payload.policyID) {
+ queryJSON.policyID = action.payload.policyID;
+ } else {
+ delete queryJSON.policyID;
+ }
+
+ const newAction = StackActions.push(SCREENS.SEARCH.ROOT, {
+ ...currentParams,
+ q: SearchQueryUtils.buildSearchQueryString(queryJSON),
+ });
+
+ setActiveWorkspaceID(action.payload.policyID);
+ return stackRouter.getStateForAction(state, newAction, configOptions);
+ }
+ if (lastRoute?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) {
+ const newAction = StackActions.push(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, {policyID: action.payload.policyID});
+
+ setActiveWorkspaceID(action.payload.policyID);
+ return stackRouter.getStateForAction(state, newAction, configOptions);
+ }
+
+ // We don't have other navigators that should handle switch policy action.
+ return null;
+}
+
+/**
+ * If a new ReportSplitNavigator is opened, it is necessary to check whether workspace is currently selected in the application.
+ * If so, the id of the current policy has to be passed to the new ReportSplitNavigator.
+ */
+function handlePushReportSplitAction(
+ state: StackNavigationState,
+ action: PushActionType,
+ configOptions: RouterConfigOptions,
+ stackRouter: Router, CommonActions.Action | StackActionType>,
+ setActiveWorkspaceID: (workspaceID: string | undefined) => void,
+) {
+ const haveParamsPolicyID = action.payload.params && 'policyID' in action.payload.params;
+ let policyID;
+
+ if (haveParamsPolicyID) {
+ policyID = (action.payload.params as Record)?.policyID;
+ setActiveWorkspaceID(policyID);
+ } else {
+ policyID = getPolicyIDFromState(state as State);
+ }
+
+ const modifiedAction = {
+ ...action,
+ payload: {
+ ...action.payload,
+ params: {
+ ...action.payload.params,
+ policyID,
+ },
+ },
+ };
+
+ const stateWithReportsSplitNavigator = stackRouter.getStateForAction(state, modifiedAction, configOptions);
+
+ if (!stateWithReportsSplitNavigator) {
+ Log.hmmm('[handlePushReportAction] ReportsSplitNavigator has not been found in the navigation state.');
+ return null;
+ }
+
+ const lastFullScreenRoute = stateWithReportsSplitNavigator.routes.at(-1);
+ const actionPayloadScreen = action.payload?.params && 'screen' in action.payload.params ? action.payload?.params?.screen : undefined;
+
+ // ReportScreen should always be opened with an animation
+ if (actionPayloadScreen === SCREENS.REPORT && lastFullScreenRoute?.key) {
+ reportsSplitsWithEnteringAnimation.add(lastFullScreenRoute.key);
+ }
+
+ return stateWithReportsSplitNavigator;
+}
+
+/**
+ * If a new Search page is opened, it is necessary to check whether workspace is currently selected in the application.
+ * If so, the id of the current policy has to be passed to the new Search page
+ */
+function handlePushSearchPageAction(
+ state: StackNavigationState,
+ action: PushActionType,
+ configOptions: RouterConfigOptions,
+ stackRouter: Router, CommonActions.Action | StackActionType>,
+ setActiveWorkspaceID: (workspaceID: string | undefined) => void,
+) {
+ const currentParams = action.payload.params as RootNavigatorParamList[typeof SCREENS.SEARCH.ROOT];
+ const queryJSON = SearchQueryUtils.buildSearchQueryJSON(currentParams.q);
+
+ if (!queryJSON) {
+ return null;
+ }
+
+ if (!queryJSON.policyID) {
+ const policyID = getPolicyIDFromState(state as State);
+
+ if (policyID) {
+ queryJSON.policyID = policyID;
+ } else {
+ delete queryJSON.policyID;
+ }
+ } else {
+ setActiveWorkspaceID(queryJSON.policyID);
+ }
+
+ const modifiedAction = {
+ ...action,
+ payload: {
+ ...action.payload,
+ params: {
+ ...action.payload.params,
+ q: SearchQueryUtils.buildSearchQueryString(queryJSON),
+ },
+ },
+ };
+
+ return stackRouter.getStateForAction(state, modifiedAction, configOptions);
+}
+
+/**
+ * Handles the DISMISS_MODAL action.
+ * If the last route is a modal route, it has to be popped from the navigation stack.
+ */
+function handleDismissModalAction(
+ state: StackNavigationState,
+ configOptions: RouterConfigOptions,
+ stackRouter: Router, CommonActions.Action | StackActionType>,
+) {
+ const lastRoute = state.routes.at(-1);
+ const newAction = StackActions.pop();
+
+ if (!lastRoute?.name || !MODAL_ROUTES_TO_DISMISS.includes(lastRoute?.name)) {
+ Log.hmmm('[Navigation] dismissModal failed because there is no modal stack to dismiss');
+ return null;
+ }
+
+ return stackRouter.getStateForAction(state, newAction, configOptions);
+}
+
+/**
+ * Handles opening a new modal navigator from an existing one.
+ */
+function handleNavigatingToModalFromModal(
+ state: StackNavigationState,
+ action: PushActionType,
+ configOptions: RouterConfigOptions,
+ stackRouter: Router, CommonActions.Action | StackActionType>,
+) {
+ const modifiedState = {...state, routes: state.routes.slice(0, -1), index: state.index !== 0 ? state.index - 1 : 0};
+ return stackRouter.getStateForAction(modifiedState, action, configOptions);
+}
+
+export {
+ handleOpenWorkspaceSplitAction,
+ handleDismissModalAction,
+ handlePushReportSplitAction,
+ handlePushSearchPageAction,
+ handleSwitchPolicyID,
+ handleNavigatingToModalFromModal,
+ workspaceSplitsWithoutEnteringAnimation,
+ reportsSplitsWithEnteringAnimation,
+};
diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts
new file mode 100644
index 000000000000..7fd10aba7ba2
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts
@@ -0,0 +1,104 @@
+import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native';
+import {findFocusedRoute, StackRouter} from '@react-navigation/native';
+import type {ParamListBase} from '@react-navigation/routers';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
+import * as Localize from '@libs/Localize';
+import {isOnboardingFlowName} from '@libs/Navigation/helpers/isNavigatorName';
+import isSideModalNavigator from '@libs/Navigation/helpers/isSideModalNavigator';
+import * as Welcome from '@userActions/Welcome';
+import CONST from '@src/CONST';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import * as GetStateForActionHandlers from './GetStateForActionHandlers';
+import syncBrowserHistory from './syncBrowserHistory';
+import type {DismissModalActionType, OpenWorkspaceSplitActionType, PushActionType, RootStackNavigatorAction, RootStackNavigatorRouterOptions, SwitchPolicyIdActionType} from './types';
+
+function isOpenWorkspaceSplitAction(action: RootStackNavigatorAction): action is OpenWorkspaceSplitActionType {
+ return action.type === CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT;
+}
+
+function isSwitchPolicyIdAction(action: RootStackNavigatorAction): action is SwitchPolicyIdActionType {
+ return action.type === CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID;
+}
+
+function isPushAction(action: RootStackNavigatorAction): action is PushActionType {
+ return action.type === CONST.NAVIGATION.ACTION_TYPE.PUSH;
+}
+
+function isDismissModalAction(action: RootStackNavigatorAction): action is DismissModalActionType {
+ return action.type === CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL;
+}
+
+function shouldPreventReset(state: StackNavigationState, action: CommonActions.Action | StackActionType) {
+ if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) {
+ return false;
+ }
+ const currentFocusedRoute = findFocusedRoute(state);
+ const targetFocusedRoute = findFocusedRoute(action?.payload);
+
+ // We want to prevent the user from navigating back to a non-onboarding screen if they are currently on an onboarding screen
+ if (isOnboardingFlowName(currentFocusedRoute?.name) && !isOnboardingFlowName(targetFocusedRoute?.name)) {
+ Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton'));
+ return true;
+ }
+
+ return false;
+}
+
+function isNavigatingToModalFromModal(state: StackNavigationState, action: CommonActions.Action | StackActionType): action is PushActionType {
+ if (action.type !== CONST.NAVIGATION.ACTION_TYPE.PUSH) {
+ return false;
+ }
+
+ const lastRoute = state.routes.at(-1);
+
+ // If the last route is a side modal navigator and the generated minimal action want's to push a new side modal navigator that means they are different ones.
+ // We want to dismiss the one that is currently on the top.
+ return isSideModalNavigator(lastRoute?.name) && isSideModalNavigator(action.payload.name);
+}
+
+function RootStackRouter(options: RootStackNavigatorRouterOptions) {
+ const stackRouter = StackRouter(options);
+ const {setActiveWorkspaceID} = useActiveWorkspace();
+
+ return {
+ ...stackRouter,
+ getStateForAction(state: StackNavigationState, action: RootStackNavigatorAction, configOptions: RouterConfigOptions) {
+ if (isOpenWorkspaceSplitAction(action)) {
+ return GetStateForActionHandlers.handleOpenWorkspaceSplitAction(state, action, configOptions, stackRouter);
+ }
+
+ if (isSwitchPolicyIdAction(action)) {
+ return GetStateForActionHandlers.handleSwitchPolicyID(state, action, configOptions, stackRouter, setActiveWorkspaceID);
+ }
+
+ if (isDismissModalAction(action)) {
+ return GetStateForActionHandlers.handleDismissModalAction(state, configOptions, stackRouter);
+ }
+
+ if (isPushAction(action)) {
+ if (action.payload.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR) {
+ return GetStateForActionHandlers.handlePushReportSplitAction(state, action, configOptions, stackRouter, setActiveWorkspaceID);
+ }
+
+ if (action.payload.name === SCREENS.SEARCH.ROOT) {
+ return GetStateForActionHandlers.handlePushSearchPageAction(state, action, configOptions, stackRouter, setActiveWorkspaceID);
+ }
+ }
+
+ // Don't let the user navigate back to a non-onboarding screen if they are currently on an onboarding screen and it's not finished.
+ if (shouldPreventReset(state, action)) {
+ syncBrowserHistory(state);
+ return state;
+ }
+
+ if (isNavigatingToModalFromModal(state, action)) {
+ return GetStateForActionHandlers.handleNavigatingToModalFromModal(state, action, configOptions, stackRouter);
+ }
+
+ return stackRouter.getStateForAction(state, action, configOptions);
+ },
+ };
+}
+
+export default RootStackRouter;
diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx
new file mode 100644
index 000000000000..3c4fe0471ef5
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/index.tsx
@@ -0,0 +1,33 @@
+import {createNavigatorFactory} from '@react-navigation/native';
+import type {ParamListBase} from '@react-navigation/native';
+import TopLevelBottomTabBar from '@components/Navigation/TopLevelBottomTabBar';
+import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange';
+import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
+import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent';
+import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions';
+import type {CustomStateHookProps, PlatformStackNavigationEventMap, PlatformStackNavigationOptions, PlatformStackNavigationState} from '@libs/Navigation/PlatformStackNavigation/types';
+import RootStackRouter from './RootStackRouter';
+
+// This is an optimization to keep mounted only last few screens in the stack.
+function useCustomRootStackNavigatorState({state}: CustomStateHookProps) {
+ const lastSplitIndex = state.routes.findLastIndex((route) => isFullScreenName(route.name));
+ const routesToRender = state.routes.slice(Math.max(0, lastSplitIndex - 1), state.routes.length);
+
+ return {...state, routes: routesToRender, index: routesToRender.length - 1};
+}
+
+const RootStackNavigatorComponent = createPlatformStackNavigatorComponent('RootStackNavigator', {
+ createRouter: RootStackRouter,
+ defaultScreenOptions: defaultPlatformStackScreenOptions,
+ useCustomEffects: useNavigationResetOnLayoutChange,
+ useCustomState: useCustomRootStackNavigatorState,
+ ExtraContent: TopLevelBottomTabBar,
+});
+
+function createRootStackNavigator() {
+ return createNavigatorFactory, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, typeof RootStackNavigatorComponent>(
+ RootStackNavigatorComponent,
+ )();
+}
+
+export default createRootStackNavigator;
diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/syncBrowserHistory/index.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.ts
similarity index 100%
rename from src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/syncBrowserHistory/index.ts
rename to src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.ts
diff --git a/src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/syncBrowserHistory/index.web.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.web.ts
similarity index 100%
rename from src/libs/Navigation/AppNavigator/createResponsiveStackNavigator/syncBrowserHistory/index.web.ts
rename to src/libs/Navigation/AppNavigator/createRootStackNavigator/syncBrowserHistory/index.web.ts
diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts
new file mode 100644
index 000000000000..6f6a0a878ab7
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts
@@ -0,0 +1,56 @@
+import type {CommonActions, DefaultNavigatorOptions, ParamListBase, StackActionType, StackNavigationState, StackRouterOptions} from '@react-navigation/native';
+import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack';
+import type CONST from '@src/CONST';
+
+type RootStackNavigatorActionType =
+ | {
+ type: typeof CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID;
+ payload: {
+ policyID: string | undefined;
+ };
+ }
+ | {
+ type: typeof CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL;
+ }
+ | {
+ type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT;
+ payload: {
+ policyID: string;
+ };
+ };
+
+type OpenWorkspaceSplitActionType = RootStackNavigatorActionType & {
+ type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT;
+};
+
+type SwitchPolicyIdActionType = RootStackNavigatorActionType & {
+ type: typeof CONST.NAVIGATION.ACTION_TYPE.SWITCH_POLICY_ID;
+};
+
+type PushActionType = StackActionType & {type: typeof CONST.NAVIGATION.ACTION_TYPE.PUSH};
+
+type DismissModalActionType = RootStackNavigatorActionType & {
+ type: typeof CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL;
+};
+
+type RootStackNavigatorConfig = {
+ isSmallScreenWidth: boolean;
+};
+
+type RootStackNavigatorRouterOptions = StackRouterOptions;
+
+type RootStackNavigatorProps = DefaultNavigatorOptions, StackNavigationOptions, StackNavigationEventMap> & RootStackNavigatorConfig;
+
+type RootStackNavigatorAction = CommonActions.Action | StackActionType | RootStackNavigatorActionType;
+
+export type {
+ OpenWorkspaceSplitActionType,
+ SwitchPolicyIdActionType,
+ PushActionType,
+ DismissModalActionType,
+ RootStackNavigatorAction,
+ RootStackNavigatorActionType,
+ RootStackNavigatorRouterOptions,
+ RootStackNavigatorProps,
+ RootStackNavigatorConfig,
+};
diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/SidebarSpacerWrapper.tsx b/src/libs/Navigation/AppNavigator/createSplitNavigator/SidebarSpacerWrapper.tsx
new file mode 100644
index 000000000000..2d2bc583d59f
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/SidebarSpacerWrapper.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import type {ReactNode} from 'react';
+import {View} from 'react-native';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+
+type SidebarSpacerWrapperProps = {
+ children?: ReactNode;
+};
+
+function SidebarSpacerWrapper({children}: SidebarSpacerWrapperProps) {
+ const styles = useThemeStyles();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+ return {children};
+}
+
+SidebarSpacerWrapper.displayName = 'SidebarSpacerWrapper';
+
+export default SidebarSpacerWrapper;
diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts
new file mode 100644
index 000000000000..f4ecfe3700f1
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/SplitRouter.ts
@@ -0,0 +1,140 @@
+import type {CommonActions, ParamListBase, PartialState, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native';
+import {StackActions, StackRouter} from '@react-navigation/native';
+import pick from 'lodash/pick';
+import getIsNarrowLayout from '@libs/getIsNarrowLayout';
+import getParamsFromRoute from '@libs/Navigation/helpers/getParamsFromRoute';
+import navigationRef from '@libs/Navigation/navigationRef';
+import type {NavigationPartialRoute} from '@libs/Navigation/types';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import CONST from '@src/CONST';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import type {SplitNavigatorRouterOptions} from './types';
+import {getPreservedSplitNavigatorState} from './usePreserveSplitNavigatorState';
+
+type StackState = StackNavigationState | PartialState>;
+
+const isAtLeastOneInState = (state: StackState, screenName: string): boolean => state.routes.some((route) => route.name === screenName);
+
+type AdaptStateIfNecessaryArgs = {
+ state: StackState;
+ options: SplitNavigatorRouterOptions;
+};
+
+function getRoutePolicyID(route: NavigationPartialRoute): string | undefined {
+ return (route?.params as Record | undefined)?.policyID;
+}
+
+function adaptStateIfNecessary({state, options: {sidebarScreen, defaultCentralScreen, parentRoute}}: AdaptStateIfNecessaryArgs) {
+ const isNarrowLayout = getIsNarrowLayout();
+
+ const lastRoute = state.routes.at(-1) as NavigationPartialRoute;
+ const routePolicyID = getRoutePolicyID(lastRoute);
+
+ // If invalid policy page is displayed on narrow layout, sidebar screen should not be pushed to the navigation state to avoid adding reduntant not found page
+ if (isNarrowLayout && !!routePolicyID) {
+ if (PolicyUtils.shouldDisplayPolicyNotFoundPage(routePolicyID)) {
+ return;
+ }
+ }
+
+ // If the screen is wide, there should be at least two screens inside:
+ // - sidebarScreen to cover left pane.
+ // - defaultCentralScreen to cover central pane.
+ if (!isAtLeastOneInState(state, sidebarScreen)) {
+ const paramsFromRoute = getParamsFromRoute(sidebarScreen);
+ const copiedParams = pick(lastRoute?.params, paramsFromRoute);
+
+ // We don't want to get an empty object as params because it breaks some navigation logic when comparing if routes are the same.
+ const params = isEmptyObject(copiedParams) ? undefined : copiedParams;
+
+ // @ts-expect-error Updating read only property
+ // noinspection JSConstantReassignment
+ state.stale = true; // eslint-disable-line
+
+ // @ts-expect-error Updating read only property
+ // Unshift the root screen to fill left pane.
+ state.routes.unshift({
+ name: sidebarScreen,
+ // This handles the case where the sidebar should have params included in the central screen e.g. policyID for workspace initial.
+ params,
+ });
+ }
+
+ // If the screen is wide, there should be at least two screens inside:
+ // - sidebarScreen to cover left pane.
+ // - defaultCentralScreen to cover central pane.
+ if (!isNarrowLayout) {
+ if (state.routes.length === 1 && state.routes[0].name === sidebarScreen) {
+ const rootState = navigationRef.getRootState();
+
+ const previousSameNavigator = rootState?.routes.filter((route) => route.name === parentRoute.name).at(-2);
+
+ // If we have optimization for not rendering all split navigators, then last selected option may not be in the state. In this case state has to be read from the preserved state.
+ const previousSameNavigatorState = previousSameNavigator?.state ?? (previousSameNavigator?.key ? getPreservedSplitNavigatorState(previousSameNavigator.key) : undefined);
+ const previousSelectedCentralScreen =
+ previousSameNavigatorState?.routes && previousSameNavigatorState.routes.length > 1 ? previousSameNavigatorState.routes.at(-1)?.name : undefined;
+
+ // @ts-expect-error Updating read only property
+ // noinspection JSConstantReassignment
+ state.stale = true; // eslint-disable-line
+
+ // @ts-expect-error Updating read only property
+ // Push the default settings central pane screen.
+ state.routes.push({
+ name: previousSelectedCentralScreen ?? defaultCentralScreen,
+ params: state.routes.at(0)?.params,
+ });
+ }
+ }
+ // eslint-disable-next-line no-param-reassign, @typescript-eslint/non-nullable-type-assertion-style
+ (state.index as number) = state.routes.length - 1;
+}
+
+function isPushingSidebarOnCentralPane(state: StackState, action: CommonActions.Action | StackActionType, options: SplitNavigatorRouterOptions) {
+ return action.type === CONST.NAVIGATION.ACTION_TYPE.PUSH && action.payload.name === options.sidebarScreen && state.routes.length > 1;
+}
+
+function SplitRouter(options: SplitNavigatorRouterOptions) {
+ const stackRouter = StackRouter(options);
+ return {
+ ...stackRouter,
+ getStateForAction(state: StackNavigationState, action: CommonActions.Action | StackActionType, configOptions: RouterConfigOptions) {
+ if (isPushingSidebarOnCentralPane(state, action, options)) {
+ if (getIsNarrowLayout()) {
+ const newAction = StackActions.popToTop();
+ return stackRouter.getStateForAction(state, newAction, configOptions);
+ }
+ // On wide screen do nothing as we want to keep the central pane screen and the sidebar is visible.
+ return state;
+ }
+ return stackRouter.getStateForAction(state, action, configOptions);
+ },
+ getInitialState({routeNames, routeParamList, routeGetIdList}: RouterConfigOptions) {
+ const preservedState = getPreservedSplitNavigatorState(options.parentRoute.key);
+ const initialState = preservedState ?? stackRouter.getInitialState({routeNames, routeParamList, routeGetIdList});
+
+ adaptStateIfNecessary({
+ state: initialState,
+ options,
+ });
+
+ // If we needed to modify the state we need to rehydrate it to get keys for new routes.
+ if (initialState.stale) {
+ return stackRouter.getRehydratedState(initialState, {routeNames, routeParamList, routeGetIdList});
+ }
+
+ return initialState;
+ },
+ getRehydratedState(partialState: StackState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): StackNavigationState {
+ adaptStateIfNecessary({
+ state: partialState,
+ options,
+ });
+
+ const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList});
+ return state;
+ },
+ };
+}
+
+export default SplitRouter;
diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState.ts
new file mode 100644
index 000000000000..06ac86f40b2d
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState.ts
@@ -0,0 +1,29 @@
+import type {NavigationState, PartialState} from '@react-navigation/native';
+import {SIDEBAR_TO_SPLIT} from '@libs/Navigation/linkingConfig/RELATIONS';
+import type {NavigationPartialRoute, SplitNavigatorBySidebar, SplitNavigatorParamListType, SplitNavigatorSidebarScreen} from '@libs/Navigation/types';
+
+type ExtractRouteType = Extract;
+
+// The function getPathFromState that we are using in some places isn't working correctly without defined index.
+const getRoutesWithIndex = (routes: NavigationPartialRoute[]): PartialState => ({routes, index: routes.length - 1});
+
+function getInitialSplitNavigatorState(
+ splitNavigatorSidebarRoute: NavigationPartialRoute,
+ route?: NavigationPartialRoute>,
+ splitNavigatorParams?: Record,
+): NavigationPartialRoute> {
+ const routes = [];
+
+ routes.push(splitNavigatorSidebarRoute);
+
+ if (route) {
+ routes.push(route);
+ }
+ return {
+ name: SIDEBAR_TO_SPLIT[splitNavigatorSidebarRoute.name],
+ state: getRoutesWithIndex(routes),
+ params: splitNavigatorParams,
+ };
+}
+
+export default getInitialSplitNavigatorState;
diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx
new file mode 100644
index 000000000000..8a943b40425a
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx
@@ -0,0 +1,52 @@
+import type {ParamListBase} from '@react-navigation/native';
+import {createNavigatorFactory} from '@react-navigation/native';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useNavigationResetOnLayoutChange from '@libs/Navigation/AppNavigator/useNavigationResetOnLayoutChange';
+import createPlatformStackNavigatorComponent from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent';
+import defaultPlatformStackScreenOptions from '@libs/Navigation/PlatformStackNavigation/defaultPlatformStackScreenOptions';
+import type {
+ CustomEffectsHookProps,
+ CustomStateHookProps,
+ PlatformStackNavigationEventMap,
+ PlatformStackNavigationOptions,
+ PlatformStackNavigationState,
+} from '@libs/Navigation/PlatformStackNavigation/types';
+import SidebarSpacerWrapper from './SidebarSpacerWrapper';
+import SplitRouter from './SplitRouter';
+import usePreserveSplitNavigatorState from './usePreserveSplitNavigatorState';
+
+function useCustomEffects(props: CustomEffectsHookProps) {
+ useNavigationResetOnLayoutChange(props);
+ usePreserveSplitNavigatorState(props.state, props.parentRoute);
+}
+
+function useCustomSplitNavigatorState({state}: CustomStateHookProps) {
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+ const sidebarScreenRoute = state.routes.at(0);
+
+ if (!sidebarScreenRoute) {
+ return state;
+ }
+
+ const centralScreenRoutes = state.routes.slice(1);
+ const routesToRender = shouldUseNarrowLayout ? state.routes.slice(-2) : [sidebarScreenRoute, ...centralScreenRoutes.slice(-2)];
+
+ return {...state, routes: routesToRender, index: routesToRender.length - 1};
+}
+
+const SplitNavigatorComponent = createPlatformStackNavigatorComponent('SplitNavigator', {
+ createRouter: SplitRouter,
+ useCustomEffects,
+ defaultScreenOptions: defaultPlatformStackScreenOptions,
+ useCustomState: useCustomSplitNavigatorState,
+ NavigationContentWrapper: SidebarSpacerWrapper,
+});
+
+function createSplitNavigator() {
+ return createNavigatorFactory, PlatformStackNavigationOptions, PlatformStackNavigationEventMap, typeof SplitNavigatorComponent>(
+ SplitNavigatorComponent,
+ )();
+}
+
+export default createSplitNavigator;
diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/types.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/types.ts
new file mode 100644
index 000000000000..2396bf6e4b72
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/types.ts
@@ -0,0 +1,12 @@
+import type {DefaultNavigatorOptions, ParamListBase, RouteProp, StackNavigationState, StackRouterOptions} from '@react-navigation/native';
+import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack';
+
+type SplitNavigatorRouterOptions = StackRouterOptions & {defaultCentralScreen: string; sidebarScreen: string; parentRoute: RouteProp};
+
+type SplitNavigatorProps = DefaultNavigatorOptions, StackNavigationOptions, StackNavigationEventMap> & {
+ persistentScreens?: Array>;
+ defaultCentralScreen: Extract;
+ sidebarScreen: Extract;
+};
+
+export type {SplitNavigatorProps, SplitNavigatorRouterOptions};
diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState.ts b/src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState.ts
new file mode 100644
index 000000000000..789fc27d81fe
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/usePreserveSplitNavigatorState.ts
@@ -0,0 +1,29 @@
+import type {NavigationState, ParamListBase, RouteProp, StackNavigationState} from '@react-navigation/native';
+import {useEffect} from 'react';
+
+const preservedSplitNavigatorStates: Record> = {};
+
+const cleanPreservedSplitNavigatorStates = (state: NavigationState) => {
+ const currentSplitNavigatorKeys = state.routes.map((route) => route.key);
+
+ for (const key of Object.keys(preservedSplitNavigatorStates)) {
+ if (!currentSplitNavigatorKeys.includes(key)) {
+ delete preservedSplitNavigatorStates[key];
+ }
+ }
+};
+
+const getPreservedSplitNavigatorState = (key: string) => preservedSplitNavigatorStates[key];
+
+function usePreserveSplitNavigatorState(state: StackNavigationState, route: RouteProp