diff --git a/.github/workflows/deployExpensifyHelp.yml b/.github/workflows/deployExpensifyHelp.yml index 82cd62c5e832..975f1f69e219 100644 --- a/.github/workflows/deployExpensifyHelp.yml +++ b/.github/workflows/deployExpensifyHelp.yml @@ -52,6 +52,14 @@ jobs: projectName: helpdot directory: ./docs/_site + - name: Setup Cloudflare CLI + run: pip3 install cloudflare + + - name: Purge Cloudflare cache + run: /home/runner/.local/bin/cli4 --delete hosts=["help.expensify.com"] /zones/:9ee042e6cfc7fd45e74aa7d2f78d617b/purge_cache + env: + CF_API_KEY: ${{ secrets.CLOUDFLARE_TOKEN }} + - name: Leave a comment on the PR uses: actions-cool/maintain-one-comment@de04bd2a3750d86b324829a3ff34d47e48e16f4b if: ${{ github.event_name == 'pull_request' }} diff --git a/android/app/build.gradle b/android/app/build.gradle index 685e9f206eb4..40961ac9957d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -91,8 +91,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001041200 - versionName "1.4.12-0" + versionCode 1001041304 + versionName "1.4.13-4" } flavorDimensions "default" diff --git a/android/app/src/main/java/com/expensify/chat/MainApplication.java b/android/app/src/main/java/com/expensify/chat/MainApplication.java index a4f2bc97416d..bc76be739949 100644 --- a/android/app/src/main/java/com/expensify/chat/MainApplication.java +++ b/android/app/src/main/java/com/expensify/chat/MainApplication.java @@ -3,7 +3,6 @@ import android.content.Context; import android.database.CursorWindow; -import androidx.appcompat.app.AppCompatDelegate; import androidx.multidex.MultiDexApplication; import com.expensify.chat.bootsplash.BootSplashPackage; @@ -67,9 +66,6 @@ public ReactNativeHost getReactNativeHost() { public void onCreate() { super.onCreate(); - // Use night (dark) mode so native UI defaults to dark theme. - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); - SoLoader.init(this, /* native exopackage */ false); if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 39b186beb022..6f62e6a5ba00 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -8,7 +8,7 @@ "license": "MIT", "dependencies": { "electron-context-menu": "^2.3.0", - "electron-log": "^4.4.7", + "electron-log": "^4.4.8", "electron-serve": "^1.2.0", "electron-updater": "^6.1.6", "node-machine-id": "^1.1.12" @@ -140,9 +140,9 @@ "integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==" }, "node_modules/electron-log": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.7.tgz", - "integrity": "sha512-uFZQdgevOp9Fn5lDOrJMU/bmmYxDLZitbIHJM7VXN+cpB59ZnPt1FQL4bOf/Dl2gaIMPYJEfXx38GvJma5iV6A==" + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.8.tgz", + "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==" }, "node_modules/electron-serve": { "version": "1.2.0", @@ -531,9 +531,9 @@ "integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==" }, "electron-log": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.7.tgz", - "integrity": "sha512-uFZQdgevOp9Fn5lDOrJMU/bmmYxDLZitbIHJM7VXN+cpB59ZnPt1FQL4bOf/Dl2gaIMPYJEfXx38GvJma5iV6A==" + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-4.4.8.tgz", + "integrity": "sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==" }, "electron-serve": { "version": "1.2.0", diff --git a/desktop/package.json b/desktop/package.json index 7689c18f0dbd..7545e4b57dba 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -5,7 +5,7 @@ "scripts": {}, "dependencies": { "electron-context-menu": "^2.3.0", - "electron-log": "^4.4.7", + "electron-log": "^4.4.8", "electron-serve": "^1.2.0", "electron-updater": "^6.1.6", "node-machine-id": "^1.1.12" diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md new file mode 100644 index 000000000000..65361ba1af9a --- /dev/null +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md @@ -0,0 +1,150 @@ +--- +title: Certinia +description: Guide to connecting Expensify and Certinia FFA and PSA/SRP (formerly known as FinancialForce) +--- +# Overview +[Cetinia](https://use.expensify.com/financialforce) (Formerly known as FinancialForce)is a cloud-based software solution that provides a range of financial management and accounting applications built on the Salesforce platform. There are two versions: PSA/SRP and FFA and we support both. + +# Before connecting to Certinia +Install the Expensify bundle in Certinia using the relevant installer: +* [PSA/SRP](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t2M000002J0BHD%252Fpackaging%252FinstallPackage.apexp%253Fp0%253D04t2M000002J0BH) +* [FFA](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t4p000001UQVj) + +## Check contact details in Certinia +First, make sure you have a user and contact in Certinia that match your main email in Expensify. Then, create contacts for all employees who will be sending expense reports. Ensure that each contact's email matches the one they use in their Expensify account. + +## If you use PSA/SRP +Each report approver needs both a User and a Contact. The user does not need to have a SalesForce license. These can be free chatter users. +Set permission controls in Certinia for your user for each contact/resource. +* Go to Permission Controls + - Create a new permission control + - Set yourself (exporter) as the user + - Select the resource (report submitter) + - Grant all available permissions +* Set permissions on any project you are exporting to + - Go to **Projects** > _select a project_ > **Project Attributes** > **Allow Expenses Without Assignment** + - Select the project > **Edit** + - Under the Project Attributes section, check **Allow Expenses Without Assignment** +* Set up Expense Types (categories in Expensify - _SRP only_) + - Go to **Main Menu** > _+ symbol_ > **Expense Type GLA Mappings** + - Click **New** to add new mappings + +# How to connect to Certinia +1. Go to **Settings** > **Workspaces** > **Groups** > _[Workspace Name]_ > **Connections** in Expensify +2. Click **Create a New Certinia (FinancialForce) Connection** +3. Log into your Certinia account +4. Expensify and Certinia will begin to sync (in Expensify) + +# How to configure export settings for Certinia +## Preferred Exporter +The preferred exporter is the user who will be the main exporter of reports. This person will receive the notifications for errors. + +## Payable Invoice Status and Date +Reports can be exported as Complete or In Progress, using date of last expense, submitted date or exported date. + +## Reimbursable and non-reimbursable exports +Both reimbursable and non-reimbursable reports are exported as payable invoices (FFA) or expense reports (PSA/SRP). If you have both Reimbursable and Non-Reimbursable expenses on a single report, we will create a separate payable invoice/expense report for each type. + +## Default Vendor (FFA) +Choose from the full list of vendors from your Certinia FFA account, this will be applied to the non-reimbursable payable invoices. + +# How to Configure coding for Certinia +## Company +Select which FinancialForce company to import from/export to. + +## Chart of Accounts (FFA) +Prepaid Expense Type and Profit & Loss accounts are imported to be used as categories on each expense. + +## Expense Type GLA Mappings (PSA/SRP) +Your Expense Type GLA Mappings are enabled in Expensify to use as categories on each expense when using both PSA and SRP; however, PSA will not import or export categories, while SRP will. + +## Dimensions (FFA) +We import four dimension levels and each has three options to select from: + +* Do not map: FinancialForce defaults will apply to the payable invoice, without importing into Expensify +* Tags: These are shown in the Tag section of your workspace, and employees can select them on each expense created +* Report fields: These will show in the Reports section of your workspace. Employees can select one to be applied at the header level i.e. the entire report. + +## Projects, Assignments, or Projects & Assignments (PSA/SRP) +These can be imported as tags with **Milestones** being optional. When selecting to import only projects, we will derive the account from the project. If an assignment is selected, we will derive both the account and project from the assignment. + +Note: If you are using a project that does not have an assignment, the box **Allow Expenses Without Assignment** must be checked on the project in FinancialForce. + +## Tax +Import tax rates from Certinia to apply to expenses. + +# How to configure advanced settings for Certinia +## Auto Sync +Auto Sync in Certinia performs daily updates to your coding. Additionally, it automatically exports reports after they receive final approval. For Non-Reimbursable expenses, syncing happens immediately upon final approval of the report. In the case of Reimbursable expenses, syncing occurs as soon as the report is reimbursed or marked as reimbursed. + +## Export tax as non-billable +When exporting Billable expenses, this dictates whether you will also bill the tax component to your clients/customers. + +# Deep Dive +## Multi-Currency in Certinia PSA/SRP +When exporting to Certinia PSA/SRP you may see up to three different currencies on the expense report in Certinia, if employees are submitting expenses in more than one original currency. +* Summary Total Reimbursement Amount: this currency is derived from the currency of the project selected on the expense. +* Amount field on the Expense line: this currency is derived from the Expensify workspace default report currency. +* Reimbursable Amount on the Expense line: this currency is derived from the currency of the resource with an email matching the report submitter. + +# FAQ +## What happens if the report can’t be exported to Certinia? +* The preferred exporter will receive an email outlining the issue and any specific error messages +* Any error messages preventing the export from taking place will be recorded in the report’s history +* The report will be listed in the exporter’s Expensify Inbox as awaiting export. + +## If I enable Auto Sync, what happens to existing approved and reimbursed reports? +You can activate Auto Sync without worry because it relies on Final Approval to trigger auto-export. Existing Approved reports won't be affected. However, for Approved reports that haven't been exported to Certinia, you'll need to either manually export them or mark them as manually entered. + +## How do I export tax? +Tax rates are created in Expensify through the tax tracking feature under **Settings** > **Workspaces** > **Groups** > _[Workspace Name]_ > **Tax**. We export the tax amount calculated on the expenses. + +## How do reports map to Payable Invoices in Certinia FFA? +* Account Name - Account associated with Expensify submitter’s email address +* Reference 1 - Report URL +* Invoice Description - Report title + +## How do reports map to Expense Reports in Certinia PSA/SRP? +* Expense report name - Report title +* Resource - User associated with Expensify submitter’s email address +* Description - Report URL +* Approver - Expensify report approver + +# Sync and Export Errors + +## ExpensiError FF0047: You must have an Ops Edit permission to edit approved records. +This error indicates that the permission control setup between the connected user and the report submitter or region is missing Ops Edit permission. + +In Certinia go to Permission Controls and click the one you need to edit. Make sure that Expense Ops Edit is selected under Permissions. + +## ExpensiError FF0076: Could not find employee in Certinia +Go to Contacts in Certinia and add the report creator/submitter's Expensify email address to their employee record, or create a record with that email listed. + +If a record already exists then search for their email address to confirm it is not associated with multiple records. + +## ExpensiError FF0089: Expense Reports for this Project require an Assignment +This error indicates that the project needs to have the permissions adjusted in Certinia + +Go to Projects > [project name] > Project Attributes and check Allow Expense Without Assignment. + +## ExpensiError FF0091: Bad Field Name — [field] is invalid for [object] +This means the field in question is not accessible to the user profile in Certinia for the user whose credentials were used to make the connection within Expensify. + +To correct this: +* Go to Setup > Build > expand Create > Object within Certinia +* Then go to Payable Invoice > Custom Fields and Relationships +* Click View Field Accessibility +* Find the employee profile in the list and select Hidden +* Make sure both checkboxes for Visible are selected + +Once this step has been completed, sync the connection within Expensify by going to **Settings** > **Workspaces** > **Groups** > _[Workspace Name]_ > **Connections** > **Sync Now** and then attempt to export the report again. + +## ExpensiError FF0132: Insufficient access. Make sure you are connecting to Certinia with a user that has the 'Modify All Data' permission + +Log into Certinia and go to Setup > Manage Users > Users and find the user whose credentials made the connection. + +* Click on their profile on the far right side of the page +* Go to System > System Permissions +* Enable Modify All Data and save + +Sync the connection within Expensify by going to **Settings** > **Workspaces** > **Groups** > _[Workspace Name]_ > **Connections** > **Sync Now** and then attempt to export the report again diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/FinancalForce.md b/docs/articles/expensify-classic/integrations/accounting-integrations/FinancalForce.md deleted file mode 100644 index 18c78ac7fc10..000000000000 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/FinancalForce.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Financial Force -description: Guide to connecting Expensify and FinancialForce FFA and PSA/SRP ---- -# Overview -[FinancialForce](https://use.expensify.com/financialforce) is a cloud-based software solution that provides a range of financial management and accounting applications built on the Salesforce platform. There are two versions: PSA/SRP and FFA and we support both. - -# Before connecting to FinancialForce -Install the Expensify bundle in SalesForce using the relevant installer: -* [PSA/SRP](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t2M000002J0BHD%252Fpackaging%252FinstallPackage.apexp%253Fp0%253D04t2M000002J0BH) -* [FFA](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t4p000001UQVj) - -## Check contact details in FF -First, make sure you have a user and contact in FinancialForce that match your main email in Expensify. Then, create contacts for all employees who will be sending expense reports. Ensure that each contact's email matches the one they use in their Expensify account. - -## If you use PSA/SRP -Each report approver needs both a User and a Contact. The user does not need to have a SalesForce license. These can be free chatter users. -Set permission controls in FinancialForce for your user for each contact/resource. -* Go to Permission Controls - - Create a new permission control - - Set yourself (exporter) as the user - - Select the resource (report submitter) - - Grant all available permissions -* Set permissions on any project you are exporting to - - Go to **Projects** > _select a project_ > **Project Attributes** > **Allow Expenses Without Assignment** - - Select the project > **Edit** - - Under the Project Attributes section, check **Allow Expenses Without Assignment** -* Set up Expense Types (categories in Expensify - _SRP only_) - - Go to **Main Menu** > _+ symbol_ > **Expense Type GLA Mappings** - - Click **New** to add new mappings - -# How to connect to FinancialForce -1. Go to **Settings** > **Policies** > **Groups** > _[Policy Name]_ > **Connections in Expensify** -2. Click **Create a New FinancialForce Connection** -3. Log into your FinancialForce account -4. Expensify and FinancialForce will begin to sync (in Expensify) - -# How to configure export settings for FinancialForce -## Preferred Exporter -The preferred exporter is the user who will be the main exporter of reports. This person will receive the notifications for errors. - -## Payable Invoice Status and Date -Reports can be exported as Complete or In Progress, using date of last expense, submitted date or exported date. - -## Reimbursable and non-reimbursable exports -Both reimbursable and non-reimbursable reports are exported as payable invoices (FFA) or expense reports (PSA/SRP). If you have both Reimbursable and Non-Reimbursable expenses on a single report, we will create a separate payable invoice/expense report for each type. - -## Default Vendor (FFA) -Choose from the full list of vendors from your FinancialForce FFA account, this will be applied to the non-reimbursable payable invoices. - -# How to Configure coding for Financial Force -## Company -Select which FinancialForce company to import from/export to. - -## Chart of Accounts (FFA) -Prepaid Expense Type and Profit & Loss accounts are imported to be used as categories on each expense. - -## Expense Type GLA Mappings (PSA/SRP) -Your Expense Type GLA Mappings are enabled in Expensify to use as categories on each expense when using both PSA and SRP; however, PSA will not import or export categories, while SRP will. - -## Dimensions (FFA) -We import four dimension levels and each has three options to select from: - -* Do not map: FinancialForce defaults will apply to the payable invoice, without importing into Expensify -* Tags: These are shown in the Tag section of your policy, and employees can select them on each expense created -* Report fields: These will show in the Reports section of your policy. Employees can select one to be applied at the header level i.e. the entire report. - -## Projects, Assignments, or Projects & Assignments (PSA/SRP) -These can be imported as tags with **Milestones** being optional. When selecting to import only projects, we will derive the account from the project. If an assignment is selected, we will derive both the account and project from the assignment. - -Note: If you are using a project that does not have an assignment, the box **Allow Expenses Without Assignment** must be checked on the project in FinancialForce. - -## Tax -Import tax rates from FinancialForce to apply to expenses. - -# How to configure advanced settings for Financial Force -## Auto Sync -Auto Sync in FinancialForce performs daily updates to your coding. Additionally, it automatically exports reports after they receive final approval. For Non-Reimbursable expenses, syncing happens immediately upon final approval of the report. In the case of Reimbursable expenses, syncing occurs as soon as the report is reimbursed or marked as reimbursed. - -## Export tax as non-billable -When exporting Billable expenses, this dictates whether you will also bill the tax component to your clients/customers. - -# Deep Dive -## Multi-Currency in FinancialForce PSA/SRP -When exporting to FinancialForce PSA/SRP you may see up to three different currencies on the expense report in FinancialForce, if employees are submitting expenses in more than one original currency. -* Summary Total Reimbursement Amount: this currency is derived from the currency of the project selected on the expense. -* Amount field on the Expense line: this currency is derived from the Expensify policy default report currency. -* Reimbursable Amount on the Expense line: this currency is derived from the currency of the resource with an email matching the report submitter. - -# FAQ -## What happens if the report can’t be exported to FinancialForce? -* The preferred exporter will receive an email outlining the issue and any specific error messages -* Any error messages preventing the export from taking place will be recorded in the report’s history -* The report will be listed in the exporter’s Expensify Inbox as awaiting export. - -## If I enable Auto Sync, what happens to existing approved and reimbursed reports? -You can activate Auto Sync without worry because it relies on Final Approval to trigger auto-export. Existing Approved reports won't be affected. However, for Approved reports that haven't been exported to FinancialForce, you'll need to either manually export them or mark them as manually entered. - -## How do I export tax? -Tax rates are created in Expensify through the tax tracking feature under **Settings** > **Policies** > **Group** > _[Policy Name]_ > **Tax**. We export the tax amount calculated on the expenses. - -## How do reports map to Payable Invoices in FinancialForce FFA? -* Account Name - Account associated with Expensify submitter’s email address -* Reference 1 - Report URL -* Invoice Description - Report title - -## How do reports map to Expense Reports in FinancialForce PSA/SRP? -* Expense report name - Report title -* Resource - User associated with Expensify submitter’s email address -* Description - Report URL -* Approver - Expensify report approver diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f4ef6d22bea6..f372dd6f9cba 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.12 + 1.4.13 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.12.0 + 1.4.13.4 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes @@ -120,8 +120,6 @@ UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIUserInterfaceStyle - Dark UIViewControllerBasedStatusBarAppearance diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index c7fb13979540..b8c64b921e23 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.12 + 1.4.13 CFBundleSignature ???? CFBundleVersion - 1.4.12.0 + 1.4.13.4 diff --git a/package-lock.json b/package-lock.json index c03809caece0..9ad0c00b7bfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.12-0", + "version": "1.4.13-4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.12-0", + "version": "1.4.13-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index c6a9d9fde386..33882d533cae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.12-0", + "version": "1.4.13-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/App.js b/src/App.js index 12fc6a9426e1..3553900bbc7f 100644 --- a/src/App.js +++ b/src/App.js @@ -8,8 +8,8 @@ import {SafeAreaProvider} from 'react-native-safe-area-context'; import '../wdyr'; import ColorSchemeWrapper from './components/ColorSchemeWrapper'; import ComposeProviders from './components/ComposeProviders'; -import CustomStatusBar from './components/CustomStatusBar'; -import CustomStatusBarContextProvider from './components/CustomStatusBar/CustomStatusBarContextProvider'; +import CustomStatusBarAndBackground from './components/CustomStatusBarAndBackground'; +import CustomStatusBarAndBackgroundContextProvider from './components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContextProvider'; import ErrorBoundary from './components/ErrorBoundary'; import HTMLEngineProvider from './components/HTMLEngineProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; @@ -68,10 +68,10 @@ function App() { ReportAttachmentsProvider, PickerStateProvider, EnvironmentProvider, - CustomStatusBarContextProvider, + CustomStatusBarAndBackgroundContextProvider, ]} > - + diff --git a/src/CONST.ts b/src/CONST.ts index c0e3d64b5eee..2733da56e597 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -533,11 +533,13 @@ const CONST = { DELETE_TAG: 'POLICYCHANGELOG_DELETE_TAG', IMPORT_CUSTOM_UNIT_RATES: 'POLICYCHANGELOG_IMPORT_CUSTOM_UNIT_RATES', IMPORT_TAGS: 'POLICYCHANGELOG_IMPORT_TAGS', + INDIVIDUAL_BUDGET_NOTIFICATION: 'POLICYCHANGELOG_INDIVIDUAL_BUDGET_NOTIFICATION', INVITE_TO_ROOM: 'POLICYCHANGELOG_INVITETOROOM', REMOVE_FROM_ROOM: 'POLICYCHANGELOG_REMOVEFROMROOM', SET_AUTOREIMBURSEMENT: 'POLICYCHANGELOG_SET_AUTOREIMBURSEMENT', SET_AUTO_JOIN: 'POLICYCHANGELOG_SET_AUTO_JOIN', SET_CATEGORY_NAME: 'POLICYCHANGELOG_SET_CATEGORY_NAME', + SHARED_BUDGET_NOTIFICATION: 'POLICYCHANGELOG_SHARED_BUDGET_NOTIFICATION', UPDATE_ACH_ACCOUNT: 'POLICYCHANGELOG_UPDATE_ACH_ACCOUNT', UPDATE_APPROVER_RULE: 'POLICYCHANGELOG_UPDATE_APPROVER_RULE', UPDATE_AUDIT_RATE: 'POLICYCHANGELOG_UPDATE_AUDIT_RATE', @@ -688,6 +690,7 @@ const CONST = { TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', SEARCH_RENDER: 'search_render', + CHAT_RENDER: 'chat_render', HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', REPORT_INITIAL_RENDER: 'report_initial_render', SWITCH_REPORT: 'switch_report', diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index 4d01fa108e2a..4abe5655e307 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -48,6 +48,9 @@ const propTypes = { /** Currently logged in user accountID */ accountID: PropTypes.number, }), + + /** Whether the personal bank account option should be shown */ + shouldShowPersonalBankAccountOption: PropTypes.bool, }; const defaultProps = { @@ -59,9 +62,10 @@ const defaultProps = { }, anchorRef: () => {}, session: {}, + shouldShowPersonalBankAccountOption: false, }; -function AddPaymentMethodMenu({isVisible, onClose, anchorPosition, anchorAlignment, anchorRef, iouReport, onItemSelected, session}) { +function AddPaymentMethodMenu({isVisible, onClose, anchorPosition, anchorAlignment, anchorRef, iouReport, onItemSelected, session, shouldShowPersonalBankAccountOption}) { const {translate} = useLocalize(); // Users can choose to pay with business bank account in case of Expense reports or in case of P2P IOU report @@ -70,6 +74,8 @@ function AddPaymentMethodMenu({isVisible, onClose, anchorPosition, anchorAlignme ReportUtils.isExpenseReport(iouReport) || (ReportUtils.isIOUReport(iouReport) && !ReportActionsUtils.hasRequestFromCurrentAccount(lodashGet(iouReport, 'reportID', 0), lodashGet(session, 'accountID', 0))); + const canUsePersonalBankAccount = shouldShowPersonalBankAccountOption || ReportUtils.isIOUReport(iouReport); + return ( ( style={[styles.autoCompleteSuggestionsContainer, animatedStyles]} exiting={FadeOutDown.duration(100).easing(Easing.inOut(Easing.ease))} > - rowHeight.value} - extraData={highlightedSuggestionIndex} - /> + + rowHeight.value} + extraData={highlightedSuggestionIndex} + /> + ); } diff --git a/src/components/AutoUpdateTime.js b/src/components/AutoUpdateTime.tsx similarity index 63% rename from src/components/AutoUpdateTime.js rename to src/components/AutoUpdateTime.tsx index b9aa3446fa12..258bdb281eb2 100644 --- a/src/components/AutoUpdateTime.js +++ b/src/components/AutoUpdateTime.tsx @@ -2,42 +2,35 @@ * Displays the user's local time and updates it every minute. * The time auto-update logic is extracted to this component to avoid re-rendering a more complex component, e.g. DetailsPage. */ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import {Timezone} from '@src/types/onyx/PersonalDetails'; import Text from './Text'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import withLocalize, {WithLocalizeProps} from './withLocalize'; -const propTypes = { +type AutoUpdateTimeProps = WithLocalizeProps & { /** Timezone of the user from their personal details */ - timezone: PropTypes.shape({ - /** Value of selected timezone */ - selected: PropTypes.string, - - /** Whether timezone is automatically set */ - automatic: PropTypes.bool, - }).isRequired, - ...withLocalizePropTypes, + timezone: Timezone; }; -function AutoUpdateTime(props) { +function AutoUpdateTime({timezone, preferredLocale, translate}: AutoUpdateTimeProps) { const styles = useThemeStyles(); - /** - * @returns {Date} Returns the locale Date object - */ - const getCurrentUserLocalTime = useCallback( - () => DateUtils.getLocalDateFromDatetime(props.preferredLocale, null, props.timezone.selected), - [props.preferredLocale, props.timezone.selected], - ); + /** @returns Returns the locale Date object */ + const getCurrentUserLocalTime = useCallback(() => DateUtils.getLocalDateFromDatetime(preferredLocale, undefined, timezone.selected), [preferredLocale, timezone.selected]); const [currentUserLocalTime, setCurrentUserLocalTime] = useState(getCurrentUserLocalTime); const minuteRef = useRef(new Date().getMinutes()); - const timezoneName = useMemo(() => DateUtils.getZoneAbbreviation(currentUserLocalTime, props.timezone.selected), [currentUserLocalTime, props.timezone.selected]); + const timezoneName = useMemo(() => { + if (timezone.selected) { + return DateUtils.getZoneAbbreviation(currentUserLocalTime, timezone.selected); + } + return ''; + }, [currentUserLocalTime, timezone.selected]); useEffect(() => { - // If the any of the props that getCurrentUserLocalTime depends on change, we want to update the displayed time immediately + // If any of the props that getCurrentUserLocalTime depends on change, we want to update the displayed time immediately setCurrentUserLocalTime(getCurrentUserLocalTime()); // Also, if the user leaves this page open, we want to make sure the displayed time is updated every minute when the clock changes @@ -58,7 +51,7 @@ function AutoUpdateTime(props) { style={[styles.textLabelSupporting, styles.mb1]} numberOfLines={1} > - {props.translate('detailsPage.localTime')} + {translate('detailsPage.localTime')} {DateUtils.formatToLocalTime(currentUserLocalTime)} {timezoneName} @@ -67,6 +60,5 @@ function AutoUpdateTime(props) { ); } -AutoUpdateTime.propTypes = propTypes; AutoUpdateTime.displayName = 'AutoUpdateTime'; export default withLocalize(AutoUpdateTime); diff --git a/src/components/CustomStatusBar/CustomStatusBarContext.tsx b/src/components/CustomStatusBar/CustomStatusBarContext.tsx deleted file mode 100644 index b2c317b05c25..000000000000 --- a/src/components/CustomStatusBar/CustomStatusBarContext.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import {createContext} from 'react'; - -type CustomStatusBarContextType = { - isRootStatusBarDisabled: boolean; - disableRootStatusBar: (isDisabled: boolean) => void; -}; - -const CustomStatusBarContext = createContext({isRootStatusBarDisabled: false, disableRootStatusBar: () => undefined}); - -export default CustomStatusBarContext; -export {type CustomStatusBarContextType}; diff --git a/src/components/CustomStatusBar/CustomStatusBarContextProvider.tsx b/src/components/CustomStatusBar/CustomStatusBarContextProvider.tsx deleted file mode 100644 index 27a5cac5d8cc..000000000000 --- a/src/components/CustomStatusBar/CustomStatusBarContextProvider.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, {useMemo, useState} from 'react'; -import CustomStatusBarContext from './CustomStatusBarContext'; - -function CustomStatusBarContextProvider({children}: React.PropsWithChildren) { - const [isRootStatusBarDisabled, disableRootStatusBar] = useState(false); - const value = useMemo( - () => ({ - isRootStatusBarDisabled, - disableRootStatusBar, - }), - [isRootStatusBarDisabled], - ); - - return {children}; -} - -export default CustomStatusBarContextProvider; diff --git a/src/components/CustomStatusBar/index.tsx b/src/components/CustomStatusBar/index.tsx deleted file mode 100644 index 2c1af898786d..000000000000 --- a/src/components/CustomStatusBar/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import {EventListenerCallback, NavigationContainerEventMap} from '@react-navigation/native'; -import React, {useCallback, useContext, useEffect} from 'react'; -import useTheme from '@hooks/useTheme'; -import {navigationRef} from '@libs/Navigation/Navigation'; -import StatusBar from '@libs/StatusBar'; -import CustomStatusBarContext from './CustomStatusBarContext'; -import updateStatusBarAppearance from './updateStatusBarAppearance'; - -type CustomStatusBarProps = { - /** Whether the CustomStatusBar is nested within another CustomStatusBar. - * A nested CustomStatusBar will disable the "root" CustomStatusBar. */ - isNested: boolean; -}; - -// eslint-disable-next-line react/function-component-definition -function CustomStatusBar({isNested = false}: CustomStatusBarProps) { - const {isRootStatusBarDisabled, disableRootStatusBar} = useContext(CustomStatusBarContext); - const theme = useTheme(); - - const isDisabled = !isNested && isRootStatusBarDisabled; - - useEffect(() => { - if (isNested) { - disableRootStatusBar(true); - } - - return () => { - if (!isNested) { - return; - } - disableRootStatusBar(false); - }; - }, [disableRootStatusBar, isNested]); - - const updateStatusBarStyle = useCallback>(() => { - if (isDisabled) { - return; - } - - // Set the status bar colour depending on the current route. - // If we don't have any colour defined for a route, fall back to - // appBG color. - const currentRoute = navigationRef.getCurrentRoute(); - - let currentScreenBackgroundColor = theme.appBG; - let statusBarStyle = theme.statusBarStyle; - if (currentRoute && 'name' in currentRoute && currentRoute.name in theme.PAGE_THEMES) { - const screenTheme = theme.PAGE_THEMES[currentRoute.name]; - currentScreenBackgroundColor = screenTheme.backgroundColor; - statusBarStyle = screenTheme.statusBarStyle; - } - - updateStatusBarAppearance({backgroundColor: currentScreenBackgroundColor, statusBarStyle}); - }, [isDisabled, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle]); - - useEffect(() => { - navigationRef.addListener('state', updateStatusBarStyle); - - return () => navigationRef.removeListener('state', updateStatusBarStyle); - }, [updateStatusBarStyle]); - - useEffect(() => { - if (isDisabled) { - return; - } - - updateStatusBarAppearance({statusBarStyle: theme.statusBarStyle}); - }, [isDisabled, theme.statusBarStyle]); - - if (isDisabled) { - return null; - } - - return ; -} - -CustomStatusBar.displayName = 'CustomStatusBar'; - -export default CustomStatusBar; diff --git a/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext.tsx b/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext.tsx new file mode 100644 index 000000000000..4a1a1cd2f964 --- /dev/null +++ b/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext.tsx @@ -0,0 +1,11 @@ +import {createContext} from 'react'; + +type CustomStatusBarAndBackgroundContextType = { + isRootStatusBarDisabled: boolean; + disableRootStatusBar: (isDisabled: boolean) => void; +}; + +const CustomStatusBarAndBackgroundContext = createContext({isRootStatusBarDisabled: false, disableRootStatusBar: () => undefined}); + +export default CustomStatusBarAndBackgroundContext; +export {type CustomStatusBarAndBackgroundContextType}; diff --git a/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContextProvider.tsx b/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContextProvider.tsx new file mode 100644 index 000000000000..b4d553b60d0f --- /dev/null +++ b/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContextProvider.tsx @@ -0,0 +1,17 @@ +import React, {useMemo, useState} from 'react'; +import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackgroundContext'; + +function CustomStatusBarAndBackgroundContextProvider({children}: React.PropsWithChildren) { + const [isRootStatusBarDisabled, disableRootStatusBar] = useState(false); + const value = useMemo( + () => ({ + isRootStatusBarDisabled, + disableRootStatusBar, + }), + [isRootStatusBarDisabled], + ); + + return {children}; +} + +export default CustomStatusBarAndBackgroundContextProvider; diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx new file mode 100644 index 000000000000..b84f9c6a6630 --- /dev/null +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -0,0 +1,113 @@ +import React, {useCallback, useContext, useEffect, useRef, useState} from 'react'; +import useTheme from '@hooks/useTheme'; +import {navigationRef} from '@libs/Navigation/Navigation'; +import StatusBar from '@libs/StatusBar'; +import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackgroundContext'; +import updateGlobalBackgroundColor from './updateGlobalBackgroundColor'; +import updateStatusBarAppearance from './updateStatusBarAppearance'; + +type CustomStatusBarAndBackgroundProps = { + /** Whether the CustomStatusBar is nested within another CustomStatusBar. + * A nested CustomStatusBar will disable the "root" CustomStatusBar. */ + isNested: boolean; +}; + +function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBackgroundProps) { + const {isRootStatusBarDisabled, disableRootStatusBar} = useContext(CustomStatusBarAndBackgroundContext); + const theme = useTheme(); + const [statusBarStyle, setStatusBarStyle] = useState(theme.statusBarStyle); + + const isDisabled = !isNested && isRootStatusBarDisabled; + + // Disable the root status bar when a nested status bar is rendered + useEffect(() => { + if (isNested) { + disableRootStatusBar(true); + } + + return () => { + if (!isNested) { + return; + } + disableRootStatusBar(false); + }; + }, [disableRootStatusBar, isNested]); + + const listenerCount = useRef(0); + const updateStatusBarStyle = useCallback( + (listenerId?: number) => { + // Check if this function is either called through the current navigation listener or the general useEffect which listens for theme changes. + if (listenerId !== undefined && listenerId !== listenerCount.current) { + return; + } + + // Set the status bar colour depending on the current route. + // If we don't have any colour defined for a route, fall back to + // appBG color. + let currentRoute: ReturnType | undefined; + if (navigationRef.isReady()) { + currentRoute = navigationRef.getCurrentRoute(); + } + + let currentScreenBackgroundColor = theme.appBG; + let newStatusBarStyle = theme.statusBarStyle; + if (currentRoute && 'name' in currentRoute && currentRoute.name in theme.PAGE_THEMES) { + const screenTheme = theme.PAGE_THEMES[currentRoute.name]; + currentScreenBackgroundColor = screenTheme.backgroundColor; + newStatusBarStyle = screenTheme.statusBarStyle; + } + + // Don't update the status bar style if it's the same as the current one, to prevent flashing. + if (newStatusBarStyle === statusBarStyle) { + updateStatusBarAppearance({backgroundColor: currentScreenBackgroundColor}); + } else { + updateStatusBarAppearance({backgroundColor: currentScreenBackgroundColor, statusBarStyle: newStatusBarStyle}); + setStatusBarStyle(newStatusBarStyle); + } + }, + [statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], + ); + + // Add navigation state listeners to update the status bar every time the route changes + // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properyl + useEffect(() => { + if (isDisabled) { + return; + } + + const listenerId = ++listenerCount.current; + const listener = () => updateStatusBarStyle(listenerId); + + navigationRef.addListener('state', listener); + return () => navigationRef.removeListener('state', listener); + }, [isDisabled, theme.appBG, updateStatusBarStyle]); + + // Update the status bar style everytime the theme changes + useEffect(() => { + if (isDisabled) { + return; + } + + updateStatusBarStyle(); + }, [isDisabled, theme, updateStatusBarStyle]); + + // Update the global background (on web) everytime the theme changes. + // The background of the html element needs to be updated, otherwise you will see a big contrast when resizing the window or when the keyboard is open on iOS web. + useEffect(() => { + if (isDisabled) { + return; + } + + updateGlobalBackgroundColor(theme); + }, [isDisabled, theme]); + + if (isDisabled) { + return null; + } + + return ; +} + +CustomStatusBarAndBackground.displayName = 'CustomStatusBarAndBackground'; + +export default CustomStatusBarAndBackground; diff --git a/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/index.ts b/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/index.ts new file mode 100644 index 000000000000..dac994ba8597 --- /dev/null +++ b/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/index.ts @@ -0,0 +1,5 @@ +import type UpdateGlobalBackgroundColor from './types'; + +const updateGlobalBackgroundColor: UpdateGlobalBackgroundColor = () => undefined; + +export default updateGlobalBackgroundColor; diff --git a/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/index.website.ts b/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/index.website.ts new file mode 100644 index 000000000000..481d866dbe4f --- /dev/null +++ b/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/index.website.ts @@ -0,0 +1,8 @@ +import UpdateGlobalBackgroundColor from './types'; + +const updateGlobalBackgroundColor: UpdateGlobalBackgroundColor = (theme) => { + const htmlElement = document.getElementsByTagName('html')[0]; + htmlElement.style.setProperty('background-color', theme.appBG); +}; + +export default updateGlobalBackgroundColor; diff --git a/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/types.ts b/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/types.ts new file mode 100644 index 000000000000..83bd36a9428a --- /dev/null +++ b/src/components/CustomStatusBarAndBackground/updateGlobalBackgroundColor/types.ts @@ -0,0 +1,5 @@ +import {ThemeColors} from '@styles/theme/types'; + +type UpdateGlobalBackgroundColor = (theme: ThemeColors) => void; + +export default UpdateGlobalBackgroundColor; diff --git a/src/components/CustomStatusBar/updateStatusBarAppearance/index.android.ts b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.android.ts similarity index 100% rename from src/components/CustomStatusBar/updateStatusBarAppearance/index.android.ts rename to src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.android.ts diff --git a/src/components/CustomStatusBar/updateStatusBarAppearance/index.ios.ts b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ios.ts similarity index 100% rename from src/components/CustomStatusBar/updateStatusBarAppearance/index.ios.ts rename to src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ios.ts diff --git a/src/components/CustomStatusBar/updateStatusBarAppearance/index.ts b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ts similarity index 100% rename from src/components/CustomStatusBar/updateStatusBarAppearance/index.ts rename to src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/index.ts diff --git a/src/components/CustomStatusBar/updateStatusBarAppearance/types.ts b/src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/types.ts similarity index 100% rename from src/components/CustomStatusBar/updateStatusBarAppearance/types.ts rename to src/components/CustomStatusBarAndBackground/updateStatusBarAppearance/types.ts diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 0d6dcb001091..50b24e368fc6 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -126,7 +126,7 @@ const FormProvider = forwardRef( } FormActions.setErrorFields(formID, null); - const validateErrors = validate(values) || {}; + const validateErrors = validate(trimmedStringValues) || {}; // Validate the input for html tags. It should supercede any other error _.each(trimmedStringValues, (inputValue, inputID) => { @@ -155,6 +155,11 @@ const FormProvider = forwardRef( } } } + + if (isMatch && leadingSpaceIndex === -1) { + return; + } + // Add a validation error here because it is a string value that contains HTML characters validateErrors[inputID] = 'common.error.invalidCharacter'; }); diff --git a/src/components/KYCWall/BaseKYCWall.js b/src/components/KYCWall/BaseKYCWall.js index e9eb1e4c54c8..5ad25d23f484 100644 --- a/src/components/KYCWall/BaseKYCWall.js +++ b/src/components/KYCWall/BaseKYCWall.js @@ -42,6 +42,7 @@ function KYCWall({ source, userWallet, walletTerms, + shouldShowPersonalBankAccountOption, }) { const anchorRef = useRef(null); const transferBalanceButtonRef = useRef(null); @@ -213,6 +214,7 @@ function KYCWall({ setShouldShowAddPaymentMenu(false); selectPaymentMethod(item); }} + shouldShowPersonalBankAccountOption={shouldShowPersonalBankAccountOption} /> {children(continueAction, anchorRef)} diff --git a/src/components/KYCWall/kycWallPropTypes.js b/src/components/KYCWall/kycWallPropTypes.js index 58db2c1c1940..2f14f1e12e11 100644 --- a/src/components/KYCWall/kycWallPropTypes.js +++ b/src/components/KYCWall/kycWallPropTypes.js @@ -62,6 +62,9 @@ const propTypes = { /** Callback for when a payment method has been selected */ onSelectPaymentMethod: PropTypes.func, + + /** Whether the personal bank account option should be shown */ + shouldShowPersonalBankAccountOption: PropTypes.bool, }; const defaultProps = { @@ -82,6 +85,7 @@ const defaultProps = { }, shouldIncludeDebitCard: true, onSelectPaymentMethod: () => {}, + shouldShowPersonalBankAccountOption: false, }; export {propTypes, defaultProps}; diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 65bffa9bffd6..fc3e4b095873 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -521,6 +521,7 @@ function MoneyRequestConfirmationList(props) { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }} + shouldShowPersonalBankAccountOption /> ) : ( >>) { + const theme = useTheme(); + return ( { if (typeof ref !== 'function') { return; diff --git a/src/components/ReportActionItem/TaskAction.js b/src/components/ReportActionItem/TaskAction.js deleted file mode 100644 index 5825d623a5ca..000000000000 --- a/src/components/ReportActionItem/TaskAction.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as TaskUtils from '@libs/TaskUtils'; - -const propTypes = { - /** Name of the reportAction action */ - actionName: PropTypes.string.isRequired, - - /** The ID of the associated taskReport */ - // eslint-disable-next-line react/no-unused-prop-types -- This is used in the withOnyx HOC - taskReportID: PropTypes.string.isRequired, - - ...withLocalizePropTypes, -}; - -function TaskAction(props) { - const styles = useThemeStyles(); - return ( - <> - - {TaskUtils.getTaskReportActionMessage(props.actionName)} - - - ); -} - -TaskAction.propTypes = propTypes; -TaskAction.displayName = 'TaskAction'; - -export default withLocalize(TaskAction); diff --git a/src/components/ReportActionItem/TaskAction.tsx b/src/components/ReportActionItem/TaskAction.tsx new file mode 100644 index 000000000000..b10be4e86fe8 --- /dev/null +++ b/src/components/ReportActionItem/TaskAction.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as TaskUtils from '@libs/TaskUtils'; + +type TaskActionProps = { + /** Name of the reportAction action */ + actionName: string; +}; + +function TaskAction({actionName}: TaskActionProps) { + const styles = useThemeStyles(); + + return ( + + {TaskUtils.getTaskReportActionMessage(actionName)} + + ); +} + +TaskAction.displayName = 'TaskAction'; + +export default TaskAction; diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 7f8292f0123e..60be8430b056 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import refPropTypes from '@components/refPropTypes'; const propTypes = { /** Callback to execute when the text input is modified correctly */ @@ -14,7 +15,7 @@ const propTypes = { errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** A ref forwarded to the TextInput */ - forwardedRef: PropTypes.func, + forwardedRef: refPropTypes, /** The ID used to uniquely identify the input in a Form */ inputID: PropTypes.string, diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js index 1ffdfd78664d..0c8e193af4cc 100644 --- a/src/components/SettlementButton.js +++ b/src/components/SettlementButton.js @@ -81,6 +81,9 @@ const propTypes = { horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), }), + + /** Whether the personal bank account option should be shown */ + shouldShowPersonalBankAccountOption: PropTypes.bool, }; const defaultProps = { @@ -110,6 +113,7 @@ const defaultProps = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, // caret for dropdown is at right, so horizontal anchor is at RIGHT vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP }, + shouldShowPersonalBankAccountOption: false, }; function SettlementButton({ @@ -132,6 +136,7 @@ function SettlementButton({ shouldHidePaymentOptions, shouldShowApproveButton, style, + shouldShowPersonalBankAccountOption, }) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -220,6 +225,7 @@ function SettlementButton({ chatReportID={chatReportID} iouReport={iouReport} anchorAlignment={kycWallAnchorAlignment} + shouldShowPersonalBankAccountOption={shouldShowPersonalBankAccountOption} > {(triggerKYCFlow, buttonRef) => ( { + // check for login (if already logged in the action will simply resolve) + console.debug('[E2E] Logging in for chat opening'); + const report = mockReport() as MockReportResponse; + + const {reportID} = report.onyxData[0].value; + + E2ELogin().then((neededLogin) => { + if (neededLogin) { + // we don't want to submit the first login to the results + return E2EClient.submitTestDone(); + } + + console.debug('[E2E] Logged in, getting chat opening metrics and submitting them…'); + Performance.subscribeToMeasurements((entry) => { + if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { + console.debug(`[E2E] Sidebar loaded, navigating to report…`); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + return; + } + console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`); + if (entry.name !== CONST.TIMING.CHAT_RENDER) { + return; + } + + console.debug(`[E2E] Submitting!`); + E2EClient.submitTestResults({ + name: 'Chat opening', + duration: entry.duration, + }) + .then(() => { + console.debug('[E2E] Done with chat opening, exiting…'); + E2EClient.submitTestDone(); + }) + .catch((err) => { + console.debug('[E2E] Error while submitting test results:', err); + }); + }); + }); +}; + +export default test; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5431292f1ab6..e805f5a023a0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1211,7 +1211,8 @@ function getDefaultWorkspaceAvatar(workspaceName?: string): React.FC { function getWorkspaceAvatar(report: OnyxEntry): UserUtils.AvatarSource { const workspaceName = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]); - return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar ?? getDefaultWorkspaceAvatar(workspaceName); + const avatar = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]?.avatar ?? ''; + return !isEmpty(avatar) ? avatar : getDefaultWorkspaceAvatar(workspaceName); } /** @@ -1812,6 +1813,10 @@ function canEditMoneyRequest(reportAction: OnyxEntry, fieldToEdit return true; } + if (reportAction.originalMessage.type !== CONST.IOU.REPORT_ACTION_TYPE.CREATE) { + return false; + } + const moneyRequestReportID = reportAction?.originalMessage?.IOUReportID ?? 0; if (!moneyRequestReportID) { diff --git a/src/libs/SessionUtils.ts b/src/libs/SessionUtils.ts index 1afa3a75e081..c73513c747af 100644 --- a/src/libs/SessionUtils.ts +++ b/src/libs/SessionUtils.ts @@ -35,8 +35,9 @@ Onyx.connect({ if (loggedInDuringSession) { return; } - - if (session?.authToken) { + // We are incorporating a check for 'signedInWithShortLivedAuthToken' to handle cases where login is performed using a ShortLivedAuthToken + // This check is necessary because, with ShortLivedAuthToken, 'authToken' gets populated, leading to 'loggedInDuringSession' being assigned a false value + if (session?.authToken && !session?.signedInWithShortLivedAuthToken) { loggedInDuringSession = false; } else { loggedInDuringSession = true; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index bea4ab8aed77..9977693896ee 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2054,29 +2054,22 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal // If a workspace member is leaving a workspace room, they don't actually lose the room from Onyx. // Instead, their notification preference just gets set to "hidden". const optimisticData: OnyxUpdate[] = [ - isWorkspaceMemberLeavingWorkspaceRoom - ? { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: isWorkspaceMemberLeavingWorkspaceRoom + ? { notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, - }, - } - : { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - reportID, + } + : { + reportID: null, stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS.CLOSED, - chatType: report.chatType, - parentReportID: report.parentReportID, - parentReportActionID: report.parentReportActionID, - policyID: report.policyID, - type: report.type, + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, }, - }, + }, ]; + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 82b51651cacc..d7043ee7b3eb 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -4,14 +4,20 @@ import {ChannelAuthorizationCallback} from 'pusher-js/with-encryption'; import {Linking} from 'react-native'; import Onyx, {OnyxUpdate} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; +import * as PersistedRequests from '@libs/actions/PersistedRequests'; import * as API from '@libs/API'; import * as Authentication from '@libs/Authentication'; import * as ErrorUtils from '@libs/ErrorUtils'; +import HttpUtils from '@libs/HttpUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; +import navigationRef from '@libs/Navigation/navigationRef'; +import * as MainQueue from '@libs/Network/MainQueue'; import * as NetworkStore from '@libs/Network/NetworkStore'; +import NetworkConnection from '@libs/NetworkConnection'; import * as Pusher from '@libs/Pusher/pusher'; import * as ReportUtils from '@libs/ReportUtils'; +import * as SessionUtils from '@libs/SessionUtils'; import Timers from '@libs/Timers'; import {hideContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as Device from '@userActions/Device'; @@ -23,6 +29,7 @@ import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import Credentials from '@src/types/onyx/Credentials'; import {AutoAuthState} from '@src/types/onyx/Session'; import clearCache from './clearCache'; @@ -332,19 +339,19 @@ function signInWithShortLivedAuthToken(email: string, authToken: string) { isLoading: true, }, }, - ]; - - const successData: OnyxUpdate[] = [ + // We are making a temporary modification to 'signedInWithShortLivedAuthToken' to ensure that 'App.openApp' will be called at least once { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.ACCOUNT, + key: ONYXKEYS.SESSION, value: { - isLoading: false, + signedInWithShortLivedAuthToken: true, }, }, ]; - const failureData: OnyxUpdate[] = [ + // Subsequently, we revert it back to the default value of 'signedInWithShortLivedAuthToken' in 'successData' or 'failureData' to ensure the user is logged out on refresh + // We are combining both success and failure data params into one const as they are identical + const resolutionData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.ACCOUNT, @@ -352,8 +359,18 @@ function signInWithShortLivedAuthToken(email: string, authToken: string) { isLoading: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.SESSION, + value: { + signedInWithShortLivedAuthToken: null, + }, + }, ]; + const successData = resolutionData; + const failureData = resolutionData; + // If the user is signing in with a different account from the current app, should not pass the auto-generated login as it may be tied to the old account. // scene 1: the user is transitioning to newDot from a different account on oldDot. // scene 2: the user is transitioning to desktop app from a different account on web app. @@ -583,14 +600,40 @@ function clearSignInData() { }); } +/** + * Reset all current params of the Home route + */ +function resetHomeRouteParams() { + Navigation.isNavigationReady().then(() => { + const routes = navigationRef.current?.getState().routes; + const homeRoute = routes?.find((route) => route.name === SCREENS.HOME); + + const emptyParams: Record = {}; + Object.keys(homeRoute?.params ?? {}).forEach((paramKey) => { + emptyParams[paramKey] = undefined; + }); + + Navigation.setParams(emptyParams, homeRoute?.key ?? ''); + Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false); + }); +} + /** * Put any logic that needs to run when we are signed out here. This can be triggered when the current tab or another tab signs out. + * - Cancels pending network calls - any lingering requests are discarded to prevent unwanted storage writes + * - Clears all current params of the Home route - the login page URL should not contain any parameter */ function cleanupSession() { Pusher.disconnect(); Timers.clearAll(); Welcome.resetReadyCheck(); PriorityMode.resetHasReadRequiredDataFromStorage(); + MainQueue.clear(); + HttpUtils.cancelPendingRequests(); + PersistedRequests.clear(); + NetworkConnection.clearReconnectionCallbacks(); + SessionUtils.resetDidUserLogInDuringSession(); + resetHomeRouteParams(); } function clearAccountMessages() { diff --git a/src/libs/actions/SignInRedirect.ts b/src/libs/actions/SignInRedirect.ts index 9ca4e6813567..6c9e7f55d887 100644 --- a/src/libs/actions/SignInRedirect.ts +++ b/src/libs/actions/SignInRedirect.ts @@ -1,14 +1,6 @@ import Onyx from 'react-native-onyx'; import * as ErrorUtils from '@libs/ErrorUtils'; -import HttpUtils from '@libs/HttpUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import navigationRef from '@libs/Navigation/navigationRef'; -import * as MainQueue from '@libs/Network/MainQueue'; -import NetworkConnection from '@libs/NetworkConnection'; -import * as SessionUtils from '@libs/SessionUtils'; import ONYXKEYS, {OnyxKey} from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; -import * as PersistedRequests from './PersistedRequests'; let currentIsOffline: boolean | undefined; let currentShouldForceOffline: boolean | undefined; @@ -45,42 +37,16 @@ function clearStorageAndRedirect(errorMessage?: string) { }); } -/** - * Reset all current params of the Home route - */ -function resetHomeRouteParams() { - Navigation.isNavigationReady().then(() => { - const routes = navigationRef.current?.getState().routes; - const homeRoute = routes?.find((route) => route.name === SCREENS.HOME); - - const emptyParams: Record = {}; - Object.keys(homeRoute?.params ?? {}).forEach((paramKey) => { - emptyParams[paramKey] = undefined; - }); - - Navigation.setParams(emptyParams, homeRoute?.key ?? ''); - Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false); - }); -} - /** * Cleanup actions resulting in the user being redirected to the Sign-in page * - Clears the Onyx store - removing the authToken redirects the user to the Sign-in page - * - Cancels pending network calls - any lingering requests are discarded to prevent unwanted storage writes - * - Clears all current params of the Home route - the login page URL should not contain any parameter * * Normally this method would live in Session.js, but that would cause a circular dependency with Network.js. * * @param [errorMessage] error message to be displayed on the sign in page */ function redirectToSignIn(errorMessage?: string) { - MainQueue.clear(); - HttpUtils.cancelPendingRequests(); - PersistedRequests.clear(); - NetworkConnection.clearReconnectionCallbacks(); clearStorageAndRedirect(errorMessage); - resetHomeRouteParams(); - SessionUtils.resetDidUserLogInDuringSession(); } export default redirectToSignIn; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 8e24a2a92310..b1e46ec40861 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -17,7 +17,6 @@ import * as OnyxUpdates from './OnyxUpdates'; import * as PersonalDetails from './PersonalDetails'; import * as Report from './Report'; import * as Session from './Session'; -import redirectToSignIn from './SignInRedirect'; let currentUserAccountID = ''; let currentEmail = ''; @@ -69,8 +68,6 @@ function closeAccount(message) { ], }, ); - // Run cleanup actions to prevent reconnection callbacks from blocking logging in again - redirectToSignIn(); } /** diff --git a/src/pages/ReimbursementAccount/ACHContractStep.js b/src/pages/ReimbursementAccount/ACHContractStep.js index d10d440a0af5..806e438d0397 100644 --- a/src/pages/ReimbursementAccount/ACHContractStep.js +++ b/src/pages/ReimbursementAccount/ACHContractStep.js @@ -219,7 +219,7 @@ function ACHContractStep(props) { key={index} style={[styles.p5, styles.border, styles.mb2]} > - {props.translate('beneficialOwnersStep.additionalOwner')} + {props.translate('beneficialOwnersStep.additionalOwner')} !_.isEmpty(props.policy), [props.policy]); @@ -134,7 +135,7 @@ function HeaderView(props) { } if ((isChatThread && !isEmptyChat) || isUserCreatedPolicyRoom || canLeaveRoom) { - if (props.report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { + if (!isWhisperAction && props.report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { threeDotMenuItems.push({ icon: Expensicons.ChatBubbles, text: translate('common.join'), diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index c910f170cc2b..fd5caeea24f4 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -20,9 +20,11 @@ import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import Timing from '@libs/actions/Timing'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; +import Performance from '@libs/Performance'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import personalDetailsPropType from '@pages/personalDetailsPropType'; @@ -164,6 +166,11 @@ function ReportScreen({ const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); + if (firstRenderRef.current) { + Timing.start(CONST.TIMING.CHAT_RENDER); + Performance.markStart(CONST.TIMING.CHAT_RENDER); + } + const reportID = getReportID(route); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -283,6 +290,9 @@ function ReportScreen({ ); useEffect(() => { + Timing.end(CONST.TIMING.CHAT_RENDER); + Performance.markEnd(CONST.TIMING.CHAT_RENDER); + fetchReportIfNeeded(); ComposerActions.setShouldShowComposeInput(true); return () => { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 5e6f2d46abda..6f26ce0cba1c 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -131,7 +131,12 @@ export default [ const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); const isModifiedExpenseAction = ReportActionsUtils.isModifiedExpenseAction(reportAction); const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); - return (isCommentAction || isReportPreviewAction || isIOUAction || isModifiedExpenseAction || isTaskAction) && !ReportUtils.isThreadFirstChat(reportAction, reportID); + const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction); + return ( + !isWhisperAction && + (isCommentAction || isReportPreviewAction || isIOUAction || isModifiedExpenseAction || isTaskAction) && + !ReportUtils.isThreadFirstChat(reportAction, reportID) + ); }, onPress: (closePopover, {reportAction, reportID}) => { if (closePopover) { @@ -162,7 +167,8 @@ export default [ const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); - return !subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); + const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction); + return !subscribed && !isWhisperAction && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 2e888a5471b8..a08f025e0530 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -358,12 +358,7 @@ function ReportActionItem(props) { props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED || props.action.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED ) { - children = ( - - ); + children = ; } else if (ReportActionsUtils.isCreatedTaskReportAction(props.action)) { children = ( diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index b468607ed8f3..e3aee93a6911 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -21,6 +21,7 @@ import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeSt import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as Session from '@userActions/Session'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -119,12 +120,22 @@ class ContactMethodDetailsPage extends Component { } componentDidUpdate(prevProps) { - const validatedDate = lodashGet(this.props.loginList, [this.getContactMethod(), 'validatedDate']); - const prevValidatedDate = lodashGet(prevProps.loginList, [this.getContactMethod(), 'validatedDate']); + const contactMethod = this.getContactMethod(); + const validatedDate = lodashGet(this.props.loginList, [contactMethod, 'validatedDate']); + const prevValidatedDate = lodashGet(prevProps.loginList, [contactMethod, 'validatedDate']); + const loginData = lodashGet(this.props.loginList, contactMethod, {}); + const isDefaultContactMethod = this.props.session.email === loginData.partnerUserID; // Navigate to methods page on successful magic code verification // validatedDate property is responsible to decide the status of the magic code verification if (!prevValidatedDate && validatedDate) { + // If the selected contactMethod is the current session['login'] and the account is unvalidated, + // the current authToken is invalid after the successful magic code verification. + // So we need to sign out the user and redirect to the sign in page. + if (isDefaultContactMethod) { + Session.signOutAndRedirectToSignIn(); + return; + } Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route); } } diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js index fa2bee325e8a..5f64faca50fc 100644 --- a/src/pages/settings/Report/RoomNamePage.js +++ b/src/pages/settings/Report/RoomNamePage.js @@ -4,7 +4,8 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import Form from '@components/Form'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import RoomNameInput from '@components/RoomNameInput'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -42,13 +43,8 @@ const defaultProps = { policy: {}, }; -function RoomNamePage(props) { +function RoomNamePage({policy, report, reports, translate}) { const styles = useThemeStyles(); - const policy = props.policy; - const report = props.report; - const reports = props.reports; - const translate = props.translate; - const roomNameInputRef = useRef(null); const isFocused = useIsFocused(); @@ -91,7 +87,7 @@ function RoomNamePage(props) { title={translate('newRoomPage.roomName')} onBackButtonPress={() => Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID))} /> -
Report.updatePolicyRoomNameAndNavigate(report, values.roomName)} @@ -100,14 +96,15 @@ function RoomNamePage(props) { enabledWhenOffline > - (roomNameInputRef.current = ref)} + -
+ ); diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index 23c5b09a0909..8cb0ef9907af 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -5,7 +5,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; -import CustomStatusBar from '@components/CustomStatusBar'; +import CustomStatusBarAndBackground from '@components/CustomStatusBarAndBackground'; import ThemeProvider from '@components/ThemeProvider'; import ThemeStylesProvider from '@components/ThemeStylesProvider'; import useLocalize from '@hooks/useLocalize'; @@ -288,7 +288,7 @@ function SignInPage(props) { - + _.map(PolicyUtils.getActivePolicies(props.policies), (policy) => ({label: policy.name, key: policy.id, value: policy.id})), [props.policies]); + const workspaceOptions = useMemo( + () => + _.map(PolicyUtils.getActivePolicies(props.policies), (policy) => ({ + label: policy.name, + key: policy.id, + value: policy.id, + })), + [props.policies], + ); const writeCapabilityOptions = useMemo( () => @@ -216,13 +228,28 @@ function WorkspaceNewRoomPage(props) { const {inputCallbackRef} = useAutoFocusInput(); + const renderEmptyWorkspaceView = () => ( + <> + +