diff --git a/.eslintrc.changed.js b/.eslintrc.changed.js
index b2c9a46df078..d0a7f9a27d21 100644
--- a/.eslintrc.changed.js
+++ b/.eslintrc.changed.js
@@ -21,13 +21,7 @@ module.exports = {
},
overrides: [
{
- files: [
- 'src/libs/actions/IOU.ts',
- 'src/libs/actions/Report.ts',
- 'src/pages/workspace/WorkspaceInitialPage.tsx',
- 'src/pages/home/report/PureReportActionItem.tsx',
- 'src/libs/SidebarUtils.ts',
- ],
+ files: ['src/libs/actions/IOU.ts', 'src/pages/workspace/WorkspaceInitialPage.tsx', 'src/pages/home/report/PureReportActionItem.tsx', 'src/libs/SidebarUtils.ts'],
rules: {
'rulesdir/no-default-id-values': 'off',
},
diff --git a/.github/actions/javascript/proposalPoliceComment/index.js b/.github/actions/javascript/proposalPoliceComment/index.js
index fa07a1063e2e..16c3f7c1b0cd 100644
--- a/.github/actions/javascript/proposalPoliceComment/index.js
+++ b/.github/actions/javascript/proposalPoliceComment/index.js
@@ -18007,6 +18007,11 @@ async function run() {
console.log('Comment body is either empty or doesn\'t contain the keyword "Proposal": ', payload.comment?.body);
return;
}
+ // If event is `edited` and comment was already edited by the bot, return early
+ if (isCommentEditedEvent(payload) && payload.comment?.body.trim().includes('Edited by **proposal-police**')) {
+ console.log('Comment was already edited by proposal-police once.\n', payload.comment?.body);
+ return;
+ }
console.log('ProposalPolice™ Action triggered for comment:', payload.comment?.body);
console.log('-> GitHub Action Type: ', payload.action?.toUpperCase());
if (!isCommentCreatedEvent(payload) && !isCommentEditedEvent(payload)) {
@@ -18033,12 +18038,7 @@ async function run() {
if (isCommentCreatedEvent(payload) && isActionRequired) {
const formattedResponse = message
// replace {user} from response template with @username
- .replaceAll('{user}', `@${payload.comment?.user.login}`)
- // replace {proposalLink} from response template with the link to the comment
- .replaceAll('{proposalLink}', payload.comment?.html_url)
- // remove any double quotes from the final comment because sometimes the assistant's
- // response contains double quotes / sometimes it doesn't
- .replaceAll('"', '');
+ .replaceAll('{user}', `@${payload.comment?.user.login}`);
// Create a comment with the assistant's response
console.log('ProposalPolice™ commenting on issue...');
await GithubUtils_1.default.createComment(CONST_1.default.APP_REPO, github_1.context.issue.number, formattedResponse);
diff --git a/.github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts b/.github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts
index afa57d7c58ef..56847eb90afe 100644
--- a/.github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts
+++ b/.github/actions/javascript/proposalPoliceComment/proposalPoliceComment.ts
@@ -41,6 +41,12 @@ async function run() {
return;
}
+ // If event is `edited` and comment was already edited by the bot, return early
+ if (isCommentEditedEvent(payload) && payload.comment?.body.trim().includes('Edited by **proposal-police**')) {
+ console.log('Comment was already edited by proposal-police once.\n', payload.comment?.body);
+ return;
+ }
+
console.log('ProposalPolice™ Action triggered for comment:', payload.comment?.body);
console.log('-> GitHub Action Type: ', payload.action?.toUpperCase());
@@ -73,14 +79,7 @@ async function run() {
if (isCommentCreatedEvent(payload) && isActionRequired) {
const formattedResponse = message
// replace {user} from response template with @username
- .replaceAll('{user}', `@${payload.comment?.user.login}`)
-
- // replace {proposalLink} from response template with the link to the comment
- .replaceAll('{proposalLink}', payload.comment?.html_url)
-
- // remove any double quotes from the final comment because sometimes the assistant's
- // response contains double quotes / sometimes it doesn't
- .replaceAll('"', '');
+ .replaceAll('{user}', `@${payload.comment?.user.login}`);
// Create a comment with the assistant's response
console.log('ProposalPolice™ commenting on issue...');
diff --git a/Mobile-Expensify b/Mobile-Expensify
index 734d4adb43ca..20be9a151000 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 734d4adb43cab0e2c2cd6e44e6381dc49d270037
+Subproject commit 20be9a151000b756fcbf7c028bd59405695e41d6
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 3671f189765b..4f8d6e5a8d60 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 1009008700
- versionName "9.0.87-0"
+ versionCode 1009008804
+ versionName "9.0.88-4"
// 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/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md b/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md
index 664e41677cbc..d74f9108d6d9 100644
--- a/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md
+++ b/docs/articles/new-expensify/getting-started/Create-a-company-workspace.md
@@ -21,6 +21,8 @@ You can get support any time by locating your chat with Concierge in your chat i
# 2. Create a new workspace
+Members can submit and manage their expenses in a workspace in Expensify. Each workspace has its own set of rules, settings, and integrations.
+
Click your profile photo or icon in the bottom left menu.
Scroll down and click Workspaces in the left menu.
diff --git a/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md b/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md
index 1e8ae38b3991..3a06ef247212 100644
--- a/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md
+++ b/docs/articles/new-expensify/getting-started/Join-your-company's-workspace.md
@@ -16,7 +16,7 @@ Get started by downloading Expensify mobile or desktop apps and ensure you’re
- **Mobile Devices:** Download the Expensify app for [Android](https://play.google.com/store/apps/details?id=com.expensify.chat) or [iOS](https://apps.apple.com/us/app/expensify-cash/id1530278510).
- **Desktop Devices:** Download the Expensify app for [macOS](https://new.expensify.com/NewExpensify.dmg).
-## Use Expensify on the Web:
+## Use Expensify on the web:
Expensify is also accessible via the web and supports the following browsers:
@@ -76,9 +76,9 @@ By using a compatible device or browser, you’ll ensure the best experience wit
# 3. Meet Concierge
Concierge is your personal assistant that walks you through setting up your account and also provides:
-- Reminders to do things like submit your expenses
-- Alerts when more information is needed on an expense report
-- Updates on new and improved account features
+- Reminders to do things like submit your expenses.
+- Alerts when more information is needed on an expense report.
+- Updates on new and improved account features.
You can get support any time by locating your chat with Concierge in your chat inbox. You can ask questions and receive direct support in this thread.
@@ -92,7 +92,7 @@ You can create an expense by SmartScanning a receipt to automatically capture th
{% include option.html value="desktop" %}
-
Click the + icon in the bottom left menu and select Submit Expense.
+
Click the + icon in the bottom left menu and select Create Expense.
Click Scan.
Drag and drop the receipt into Expensify, or click Choose File to select it from your saved files. Note: The SmartScan process will auto-populate the merchant, date, and amount.
Use the search field to find the desired workspace or an individual’s name, email, or phone number.
@@ -104,7 +104,7 @@ You can create an expense by SmartScanning a receipt to automatically capture th
{% include option.html value="mobile" %}
-
Tap the + icon at the bottom of the screen and select Submit Expense.
+
Tap the + icon at the bottom of the screen and select Create Expense.
Tap Scan.
Tap the green button to take a photo of a receipt, or tap the Image icon to the left of it to upload a receipt from your phone. Note: The SmartScan process will auto-populate the merchant, date, and amount.
Use the search field to find the desired workspace or an individual’s name, email, or phone number.
diff --git a/docs/assets/images/commfeed-03.png b/docs/assets/images/commfeed-03.png
new file mode 100644
index 000000000000..41b32803ef40
Binary files /dev/null and b/docs/assets/images/commfeed-03.png differ
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index a04d8eac5873..ee97600b65e8 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -23,7 +23,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 9.0.87
+ 9.0.88CFBundleSignature????CFBundleURLTypes
@@ -44,7 +44,7 @@
CFBundleVersion
- 9.0.87.0
+ 9.0.88.4FullStoryOrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 3245342658cb..699de6dc5993 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 9.0.87
+ 9.0.88CFBundleSignature????CFBundleVersion
- 9.0.87.0
+ 9.0.88.4
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 314309978cff..101c7c26d830 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -11,9 +11,9 @@
CFBundleName$(PRODUCT_NAME)CFBundleShortVersionString
- 9.0.87
+ 9.0.88CFBundleVersion
- 9.0.87.0
+ 9.0.88.4NSExtensionNSExtensionPointIdentifier
diff --git a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt
index 395f822645b3..47e4196f37c1 100644
--- a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt
+++ b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt
@@ -11,6 +11,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.os.Build
import android.os.PersistableBundle
import android.util.Log
@@ -27,7 +28,13 @@ class ReactNativeBackgroundTaskModule internal constructor(context: ReactApplica
init {
val filter = IntentFilter("com.expensify.reactnativebackgroundtask.TASK_ACTION")
- reactApplicationContext.registerReceiver(taskReceiver, filter)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ reactApplicationContext.registerReceiver(
+ taskReceiver,
+ filter,
+ Context.RECEIVER_EXPORTED
+ )
+ }
}
override fun getName(): String {
diff --git a/package-lock.json b/package-lock.json
index 79abe870d375..33892559a235 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.87-0",
+ "version": "9.0.88-4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.87-0",
+ "version": "9.0.88-4",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 43e6944cc236..9a9898bde0be 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.87-0",
+ "version": "9.0.88-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/scripts/applyPatches.sh b/scripts/applyPatches.sh
index fa87b4540b38..7319d72ffe39 100755
--- a/scripts/applyPatches.sh
+++ b/scripts/applyPatches.sh
@@ -15,12 +15,18 @@ function patchPackage {
OS="$(uname)"
if [[ "$OS" == "Darwin" || "$OS" == "Linux" ]]; then
- npx patch-package --error-on-fail --color=always
- EXIT_CODE=$?
if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then
- echo -e "\n${GREEN}Applying HybridApp patches!${NC}"
- npx patch-package --patch-dir 'Mobile-Expensify/patches' --error-on-fail --color=always
- EXIT_CODE+=$?
+ TEMP_PATCH_DIR=$(mktemp -d ./tmp-patches-XXX)
+ cp -r ./patches/* "$TEMP_PATCH_DIR"
+ cp -r ./Mobile-Expensify/patches/* "$TEMP_PATCH_DIR"
+
+ npx patch-package --patch-dir "$TEMP_PATCH_DIR" --error-on-fail --color=always
+ EXIT_CODE=$?
+
+ rm -rf "$TEMP_PATCH_DIR"
+ else
+ npx patch-package --error-on-fail --color=always
+ EXIT_CODE=$?
fi
exit $EXIT_CODE
else
diff --git a/src/CONST.ts b/src/CONST.ts
index c62b564939b8..01a1c6387ce3 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -661,6 +661,7 @@ const CONST = {
PER_DIEM: 'newDotPerDiem',
NEWDOT_MERGE_ACCOUNTS: 'newDotMergeAccounts',
NEWDOT_MANAGER_MCTEST: 'newDotManagerMcTest',
+ NSQS: 'nsqs',
},
BUTTON_STATES: {
DEFAULT: 'default',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 6b26ecd73700..d39213943c82 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -558,6 +558,8 @@ const ONYXKEYS = {
ADD_PAYMENT_CARD_FORM_DRAFT: 'addPaymentCardFormDraft',
WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm',
WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm',
+ WORKSPACE_CONFIRMATION_FORM: 'workspaceConfirmationForm',
+ WORKSPACE_CONFIRMATION_FORM_DRAFT: 'workspaceConfirmationFormDraft',
WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft',
WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM: 'workspaceCategoryDescriptionHintForm',
WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM_DRAFT: 'workspaceCategoryDescriptionHintFormDraft',
@@ -743,6 +745,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM]: FormTypes.AddPaymentCardForm;
[ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm;
[ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM]: FormTypes.WorkspaceCategoryForm;
+ [ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM]: FormTypes.WorkspaceConfirmationForm;
[ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm;
[ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName;
[ONYXKEYS.FORMS.WORKSPACE_COMPANY_CARD_FEED_NAME]: FormTypes.WorkspaceCompanyCardFeedName;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 7bf1bf1c9e07..5c0d0b4b376f 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -13,8 +13,8 @@ import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';
/**
* Builds a URL with an encoded URI component for the `backTo` param which can be added to the end of URLs
*/
-function getUrlWithBackToParam(url: TUrl, backTo?: string): `${TUrl}` {
- const backToParam = backTo ? (`${url.includes('?') ? '&' : '?'}backTo=${encodeURIComponent(backTo)}` as const) : '';
+function getUrlWithBackToParam(url: TUrl, backTo?: string, shouldEncodeURIComponent = true): `${TUrl}` {
+ const backToParam = backTo ? (`${url.includes('?') ? '&' : '?'}backTo=${shouldEncodeURIComponent ? encodeURIComponent(backTo) : backTo}` as const) : '';
return `${url}${backToParam}` as `${TUrl}`;
}
@@ -656,8 +656,12 @@ const ROUTES = {
},
MONEY_REQUEST_STEP_SCAN: {
route: ':action/:iouType/scan/:transactionID/:reportID',
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`${action as string}/${iouType as string}/scan/${transactionID}/${reportID}`, backTo),
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => {
+ if (!transactionID || !reportID) {
+ Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_SCAN route');
+ }
+ return getUrlWithBackToParam(`${action as string}/${iouType as string}/scan/${transactionID}/${reportID}`, backTo);
+ },
},
MONEY_REQUEST_STEP_TAG: {
route: ':action/:iouType/tag/:orderWeight/:transactionID/:reportID/:reportActionID?',
@@ -745,7 +749,12 @@ const ROUTES = {
WORKSPACE_NEW_ROOM: 'workspace/new-room',
WORKSPACE_INITIAL: {
route: 'settings/workspaces/:policyID',
- getRoute: (policyID: string, backTo?: string) => `${getUrlWithBackToParam(`settings/workspaces/${policyID}`, backTo)}` as const,
+ getRoute: (policyID: string | undefined, backTo?: string) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID while building route WORKSPACE_INITIAL');
+ }
+ return `${getUrlWithBackToParam(`settings/workspaces/${policyID}`, backTo)}` as const;
+ },
},
WORKSPACE_INVITE: {
route: 'settings/workspaces/:policyID/invite',
@@ -777,7 +786,7 @@ const ROUTES = {
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export',
- getRoute: (policyID: string, backTo?: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export?backTo=${backTo}` as const,
+ getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/accounting/quickbooks-online/export` as const, backTo, false),
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account',
@@ -979,7 +988,12 @@ const ROUTES = {
},
WORKSPACE_MEMBERS: {
route: 'settings/workspaces/:policyID/members',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/members` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID while building route WORKSPACE_MEMBERS');
+ }
+ return `settings/workspaces/${policyID}/members` as const;
+ },
},
WORKSPACE_MEMBERS_IMPORT: {
route: 'settings/workspaces/:policyID/members/import',
@@ -991,7 +1005,10 @@ const ROUTES = {
},
POLICY_ACCOUNTING: {
route: 'settings/workspaces/:policyID/accounting',
- getRoute: (policyID: string, newConnectionName?: ConnectionName, integrationToDisconnect?: ConnectionName, shouldDisconnectIntegrationBeforeConnecting?: boolean) => {
+ getRoute: (policyID: string | undefined, newConnectionName?: ConnectionName, integrationToDisconnect?: ConnectionName, shouldDisconnectIntegrationBeforeConnecting?: boolean) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID while building route POLICY_ACCOUNTING');
+ }
let queryParams = '';
if (newConnectionName) {
queryParams += `?newConnectionName=${newConnectionName}`;
@@ -1029,7 +1046,12 @@ const ROUTES = {
},
WORKSPACE_CATEGORIES: {
route: 'settings/workspaces/:policyID/categories',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID while building route WORKSPACE_CATEGORIES');
+ }
+ return `settings/workspaces/${policyID}/categories` as const;
+ },
},
WORKSPACE_CATEGORY_SETTINGS: {
route: 'settings/workspaces/:policyID/category/:categoryName',
@@ -1094,7 +1116,12 @@ const ROUTES = {
},
WORKSPACE_MORE_FEATURES: {
route: 'settings/workspaces/:policyID/more-features',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/more-features` as const,
+ getRoute: (policyID: string | undefined) => {
+ if (!policyID) {
+ Log.warn('Invalid policyID while building route WORKSPACE_MORE_FEATURES');
+ }
+ return `settings/workspaces/${policyID}/more-features` as const;
+ },
},
WORKSPACE_TAGS: {
route: 'settings/workspaces/:policyID/tags',
@@ -1270,7 +1297,7 @@ const ROUTES = {
WORKSPACE_COMPANY_CARD_EXPORT: {
route: 'settings/workspaces/:policyID/company-cards/:bank/:cardID/edit/export',
getRoute: (policyID: string, cardID: string, bank: string, backTo?: string) =>
- `settings/workspaces/${policyID}/company-cards/${bank}/${cardID}/edit/export?backTo=${backTo}` as const,
+ getUrlWithBackToParam(`settings/workspaces/${policyID}/company-cards/${bank}/${cardID}/edit/export`, backTo, false),
},
WORKSPACE_EXPENSIFY_CARD: {
route: 'settings/workspaces/:policyID/expensify-card',
@@ -1487,6 +1514,10 @@ const ROUTES = {
},
WELCOME_VIDEO_ROOT: 'onboarding/welcome-video',
EXPLANATION_MODAL_ROOT: 'onboarding/explanation',
+ WORKSPACE_CONFIRMATION: {
+ route: 'workspace/confirmation',
+ getRoute: (backTo?: string) => getUrlWithBackToParam(`workspace/confirmation`, backTo),
+ },
MIGRATED_USER_WELCOME_MODAL: 'onboarding/migrated-user-welcome',
TRANSACTION_RECEIPT: {
@@ -1562,7 +1593,7 @@ const ROUTES = {
},
POLICY_ACCOUNTING_XERO_EXPORT: {
route: 'settings/workspaces/:policyID/accounting/xero/export',
- getRoute: (policyID: string, backTo?: string) => `settings/workspaces/${policyID}/accounting/xero/export?backTo=${backTo}` as const,
+ getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/accounting/xero/export` as const, backTo, false),
},
POLICY_ACCOUNTING_XERO_PREFERRED_EXPORTER_SELECT: {
route: 'settings/workspaces/:policyID/connections/xero/export/preferred-exporter/select',
@@ -1687,7 +1718,7 @@ const ROUTES = {
},
POLICY_ACCOUNTING_NETSUITE_EXPORT: {
route: 'settings/workspaces/:policyID/connections/netsuite/export/',
- getRoute: (policyID: string, backTo?: string) => `settings/workspaces/${policyID}/connections/netsuite/export?backTo=${backTo}` as const,
+ getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/connections/netsuite/export/` as const, backTo, false),
},
POLICY_ACCOUNTING_NETSUITE_PREFERRED_EXPORTER_SELECT: {
route: 'settings/workspaces/:policyID/connections/netsuite/export/preferred-exporter/select',
@@ -1825,7 +1856,7 @@ const ROUTES = {
},
POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT: {
route: 'settings/workspaces/:policyID/accounting/sage-intacct/export',
- getRoute: (policyID: string, backTo?: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export?backTo=${backTo}` as const,
+ getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/accounting/sage-intacct/export` as const, backTo, false),
},
POLICY_ACCOUNTING_SAGE_INTACCT_PREFERRED_EXPORTER: {
route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/preferred-exporter',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index c204b05c90de..5a5635037d02 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -159,6 +159,7 @@ const SCREENS = {
DETAILS: 'Details',
PROFILE: 'Profile',
REPORT_DETAILS: 'Report_Details',
+ WORKSPACE_CONFIRMATION: 'Workspace_Confirmation',
REPORT_SETTINGS: 'Report_Settings',
REPORT_DESCRIPTION: 'Report_Description',
PARTICIPANTS: 'Participants',
@@ -326,6 +327,8 @@ const SCREENS = {
EXPORT: 'Report_Details_Export',
},
+ WORKSPACE_CONFIRMATION: {ROOT: 'Workspace_Confirmation_Root'},
+
WORKSPACE: {
ACCOUNTING: {
ROOT: 'Policy_Accounting',
diff --git a/src/components/Accordion/index.native.tsx b/src/components/Accordion/index.native.tsx
deleted file mode 100644
index 0fcaa7294dd7..000000000000
--- a/src/components/Accordion/index.native.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import type {ReactNode} from 'react';
-import React from 'react';
-import type {StyleProp, ViewStyle} from 'react-native';
-import {View} from 'react-native';
-import type {SharedValue} from 'react-native-reanimated';
-import Animated, {Easing, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated';
-import useThemeStyles from '@hooks/useThemeStyles';
-
-type AccordionProps = {
- /** Giving information whether the component is open */
- isExpanded: SharedValue;
-
- /** Element that is inside Accordion */
- children: ReactNode;
-
- /** Duration of expansion animation */
- duration?: number;
-
- /** Additional external style */
- style?: StyleProp;
-
- /** Was toggle triggered */
- isToggleTriggered: SharedValue;
-};
-
-function Accordion({isExpanded, children, duration = 300, isToggleTriggered, style}: AccordionProps) {
- const height = useSharedValue(0);
- const styles = useThemeStyles();
-
- const derivedHeight = useDerivedValue(() => {
- if (!isToggleTriggered.get()) {
- return isExpanded.get() ? height.get() : 0;
- }
-
- return withTiming(height.get() * Number(isExpanded.get()), {
- duration,
- easing: Easing.inOut(Easing.quad),
- });
- });
-
- const derivedOpacity = useDerivedValue(() => {
- if (!isToggleTriggered.get()) {
- return isExpanded.get() ? 1 : 0;
- }
-
- return withTiming(isExpanded.get() ? 1 : 0, {
- duration,
- easing: Easing.inOut(Easing.quad),
- });
- });
-
- const animatedStyle = useAnimatedStyle(() => {
- if (!isToggleTriggered.get() && !isExpanded.get()) {
- return {
- height: 0,
- opacity: 0,
- };
- }
- return {
- height: !isToggleTriggered.get() ? height.get() : derivedHeight.get(),
- opacity: derivedOpacity.get(),
- };
- });
-
- return (
-
- {
- height.set(e.nativeEvent.layout.height);
- }}
- style={[styles.pAbsolute, styles.l0, styles.r0, styles.t0]}
- >
- {children}
-
-
- );
-}
-
-Accordion.displayName = 'Accordion';
-
-export default Accordion;
diff --git a/src/components/Accordion/index.tsx b/src/components/Accordion/index.tsx
deleted file mode 100644
index 9715f3902c03..000000000000
--- a/src/components/Accordion/index.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import type {ReactNode} from 'react';
-import React from 'react';
-import type {StyleProp, ViewStyle} from 'react-native';
-import {View} from 'react-native';
-import type {SharedValue} from 'react-native-reanimated';
-import Animated, {Easing, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated';
-
-type AccordionProps = {
- /** Giving information whether the component is open */
- isExpanded: SharedValue;
-
- /** Element that is inside Accordion */
- children: ReactNode;
-
- /** Duration of expansion animation */
- duration?: number;
-
- /** Additional external style */
- style?: StyleProp;
-
- /** Was toggle triggered */
- isToggleTriggered: SharedValue;
-};
-
-function Accordion({isExpanded, children, duration = 300, isToggleTriggered, style}: AccordionProps) {
- const height = useSharedValue(0);
-
- const derivedHeight = useDerivedValue(() => {
- if (!isToggleTriggered.get()) {
- return isExpanded.get() ? height.get() : 0;
- }
-
- return withTiming(height.get() * Number(isExpanded.get()), {
- duration,
- easing: Easing.inOut(Easing.quad),
- });
- });
-
- const derivedOpacity = useDerivedValue(() => {
- if (!isToggleTriggered.get()) {
- return isExpanded.get() ? 1 : 0;
- }
-
- return withTiming(isExpanded.get() ? 1 : 0, {
- duration,
- easing: Easing.inOut(Easing.quad),
- });
- });
-
- const animatedStyle = useAnimatedStyle(() => {
- if (!isToggleTriggered.get() && !isExpanded.get()) {
- return {
- height: 0,
- opacity: 0,
- };
- }
-
- return {
- height: !isToggleTriggered.get() ? undefined : derivedHeight.get(),
- opacity: derivedOpacity.get(),
- };
- });
-
- return (
-
- {
- height.set(e.nativeEvent.layout.height);
- }}
- >
- {children}
-
-
- );
-}
-Accordion.displayName = 'Accordion';
-
-export default Accordion;
diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx
index 0ad6dfbb8f7f..74ff19f21a46 100644
--- a/src/components/AttachmentModal.tsx
+++ b/src/components/AttachmentModal.tsx
@@ -14,13 +14,13 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import attachmentModalHandler from '@libs/AttachmentModalHandler';
import fileDownload from '@libs/fileDownload';
-import * as FileUtils from '@libs/fileDownload/FileUtils';
+import {cleanFileName, getFileName, validateImageForCorruption} from '@libs/fileDownload/FileUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as TransactionUtils from '@libs/TransactionUtils';
+import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {hasEReceipt, hasMissingSmartscanFields, hasReceipt, hasReceiptSource, isReceiptBeingScanned} from '@libs/TransactionUtils';
import type {AvatarSource} from '@libs/UserUtils';
import variables from '@styles/variables';
-import * as IOU from '@userActions/IOU';
+import {detachReceipt} from '@userActions/IOU';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -183,8 +183,9 @@ function AttachmentModal({
const nope = useSharedValue(false);
const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid);
const iouType = useMemo(() => (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction]);
- const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1');
- const transactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1';
+ const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID);
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const transactionID = (isMoneyRequestAction(parentReportAction) && getOriginalMessage(parentReportAction)?.IOUTransactionID) || CONST.DEFAULT_NUMBER_ID;
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
const [currentAttachmentLink, setCurrentAttachmentLink] = useState(attachmentLink);
@@ -249,7 +250,7 @@ function AttachmentModal({
}
if (typeof sourceURL === 'string') {
- const fileName = type === CONST.ATTACHMENT_TYPE.SEARCH ? FileUtils.getFileName(`${sourceURL}`) : file?.name;
+ const fileName = type === CONST.ATTACHMENT_TYPE.SEARCH ? getFileName(`${sourceURL}`) : file?.name;
fileDownload(sourceURL, fileName ?? '');
}
@@ -288,14 +289,14 @@ function AttachmentModal({
* Detach the receipt and close the modal.
*/
const deleteAndCloseModal = useCallback(() => {
- IOU.detachReceipt(transaction?.transactionID ?? '-1');
+ detachReceipt(transaction?.transactionID);
setIsDeleteReceiptConfirmModalVisible(false);
- Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '-1'));
+ Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID));
}, [transaction, report]);
const isValidFile = useCallback(
(fileObject: FileObject) =>
- FileUtils.validateImageForCorruption(fileObject)
+ validateImageForCorruption(fileObject)
.then(() => {
if (fileObject.size && fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setIsAttachmentInvalid(true);
@@ -355,7 +356,7 @@ function AttachmentModal({
* upload, drag and drop, copy-paste
*/
let updatedFile = fileObject;
- const cleanName = FileUtils.cleanFileName(updatedFile.name);
+ const cleanName = cleanFileName(updatedFile.name);
if (updatedFile.name !== cleanName) {
updatedFile = new File([updatedFile], cleanName, {type: updatedFile.type});
}
@@ -432,13 +433,7 @@ function AttachmentModal({
onSelected: () => {
closeModal(true);
Navigation.navigate(
- ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(
- CONST.IOU.ACTION.EDIT,
- iouType,
- transaction?.transactionID ?? '-1',
- report?.reportID ?? '-1',
- Navigation.getActiveRouteWithoutParams(),
- ),
+ ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, report?.reportID, Navigation.getActiveRouteWithoutParams()),
);
},
});
@@ -450,13 +445,9 @@ function AttachmentModal({
onSelected: () => downloadAttachment(),
});
}
- if (
- !TransactionUtils.hasEReceipt(transaction) &&
- TransactionUtils.hasReceipt(transaction) &&
- !TransactionUtils.isReceiptBeingScanned(transaction) &&
- canEditReceipt &&
- !TransactionUtils.hasMissingSmartscanFields(transaction)
- ) {
+
+ const hasOnlyEReceipt = hasEReceipt(transaction) && !hasReceiptSource(transaction);
+ if (!hasOnlyEReceipt && hasReceipt(transaction) && !isReceiptBeingScanned(transaction) && canEditReceipt && !hasMissingSmartscanFields(transaction)) {
menuItems.push({
icon: Expensicons.Trashcan,
text: translate('receipt.deleteReceipt'),
diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx
index 9171132964f6..39ce908cb2a9 100755
--- a/src/components/Composer/implementation/index.tsx
+++ b/src/components/Composer/implementation/index.tsx
@@ -16,9 +16,9 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
-import * as Browser from '@libs/Browser';
-import * as EmojiUtils from '@libs/EmojiUtils';
-import * as FileUtils from '@libs/fileDownload/FileUtils';
+import {isMobileSafari, isSafari} from '@libs/Browser';
+import {containsOnlyEmojis} from '@libs/EmojiUtils';
+import {base64ToFile} from '@libs/fileDownload/FileUtils';
import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition';
import variables from '@styles/variables';
import CONST from '@src/CONST';
@@ -55,7 +55,7 @@ function Composer(
}: ComposerProps,
ref: ForwardedRef,
) {
- const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]);
+ const textContainsOnlyEmojis = useMemo(() => containsOnlyEmojis(value ?? ''), [value]);
const theme = useTheme();
const styles = useThemeStyles();
const markdownStyle = useMarkdownStyle(value, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
@@ -78,7 +78,7 @@ function Composer(
// On mobile safari, the cursor will move from right to left with inputMode set to none during report transition
// To avoid that we should hide the cursor util the transition is finished
- const [shouldTransparentCursor, setShouldTransparentCursor] = useState(!showSoftInputOnFocus && Browser.isMobileSafari());
+ const [shouldTransparentCursor, setShouldTransparentCursor] = useState(!showSoftInputOnFocus && isMobileSafari());
const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? '');
const [prevScroll, setPrevScroll] = useState();
@@ -180,7 +180,7 @@ function Composer(
if (embeddedImages.length > 0 && embeddedImages[0].src) {
const src = embeddedImages[0].src;
- const file = FileUtils.base64ToFile(src, 'image.png');
+ const file = base64ToFile(src, 'image.png');
onPasteFile(file);
return true;
}
@@ -241,6 +241,12 @@ function Composer(
e.preventDefault();
return;
}
+
+ // When the composer has no scrollable content, the stopPropagation will prevent the inverted wheel event handler on the Chat body
+ // which defaults to the browser wheel behavior. This causes the chat body to scroll in the opposite direction creating jerky behavior.
+ if (textInput.current && textInput.current.scrollHeight <= textInput.current.clientHeight) {
+ return;
+ }
e.stopPropagation();
};
textInput.current?.addEventListener('wheel', handleWheel, {passive: false});
@@ -344,7 +350,7 @@ function Composer(
() => [
StyleSheet.flatten([style, {outline: 'none'}]),
StyleUtils.getComposeTextAreaPadding(isComposerFullSize),
- Browser.isMobileSafari() || Browser.isSafari() ? styles.rtlTextRenderForSafari : {},
+ isMobileSafari() || isSafari() ? styles.rtlTextRenderForSafari : {},
scrollStyleMemo,
StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize),
isComposerFullSize ? {height: '100%', maxHeight: 'none'} : undefined,
@@ -358,7 +364,7 @@ function Composer(
(textInput.current = el)}
selection={selection}
diff --git a/src/components/CurrencyPicker.tsx b/src/components/CurrencyPicker.tsx
new file mode 100644
index 000000000000..f382542c6538
--- /dev/null
+++ b/src/components/CurrencyPicker.tsx
@@ -0,0 +1,89 @@
+import React, {forwardRef, useState} from 'react';
+import type {ForwardedRef} from 'react';
+import {View} from 'react-native';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import CurrencySelectionListWithOnyx from './CurrencySelectionList';
+import HeaderWithBackButton from './HeaderWithBackButton';
+import MenuItemWithTopDescription from './MenuItemWithTopDescription';
+import Modal from './Modal';
+import ScreenWrapper from './ScreenWrapper';
+import type {ValuePickerItem, ValuePickerProps} from './ValuePicker/types';
+
+type CurrencyPickerProps = {
+ selectedCurrency?: string;
+};
+function CurrencyPicker({selectedCurrency, label = '', errorText = '', value, onInputChange, furtherDetails}: ValuePickerProps & CurrencyPickerProps, forwardedRef: ForwardedRef) {
+ const StyleUtils = useStyleUtils();
+ const styles = useThemeStyles();
+ const [isPickerVisible, setIsPickerVisible] = useState(false);
+ const {translate} = useLocalize();
+
+ const showPickerModal = () => {
+ setIsPickerVisible(true);
+ };
+
+ const hidePickerModal = () => {
+ setIsPickerVisible(false);
+ };
+
+ const updateInput = (item: ValuePickerItem) => {
+ if (item.value !== selectedCurrency) {
+ onInputChange?.(item.value);
+ }
+ hidePickerModal();
+ };
+
+ const descStyle = !selectedCurrency || selectedCurrency.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null;
+
+ return (
+
+
+
+ hidePickerModal}
+ onModalHide={hidePickerModal}
+ hideModalContentWhileAnimating
+ useNativeDriver
+ onBackdropPress={hidePickerModal}
+ >
+
+
+ updateInput({value: item.currencyCode})}
+ searchInputLabel={translate('common.currency')}
+ initiallySelectedCurrencyCode={selectedCurrency}
+ />
+
+
+
+ );
+}
+
+CurrencyPicker.displayName = 'CurrencyPicker';
+
+export default forwardRef(CurrencyPicker);
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 01539b0d5776..36b11ff58b40 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -11,7 +11,20 @@ import {MouseProvider} from '@hooks/useMouseContext';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import blurActiveElement from '@libs/Accessibility/blurActiveElement';
-//
+import {
+ adjustRemainingSplitShares,
+ resetSplitShares,
+ setCustomUnitRateID,
+ setIndividualShare,
+ setMoneyRequestAmount,
+ setMoneyRequestCategory,
+ setMoneyRequestMerchant,
+ setMoneyRequestPendingFields,
+ setMoneyRequestTag,
+ setMoneyRequestTaxAmount,
+ setMoneyRequestTaxRate,
+ setSplitShares,
+} from '@libs/actions/IOU';
import {convertToBackendAmount, convertToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencyDecimals} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import {calculateAmount, insertTagIntoTransactionTagsString, isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseUtil} from '@libs/IOUUtils';
@@ -36,20 +49,6 @@ import {
isMerchantMissing,
isScanRequest as isScanRequestUtil,
} from '@libs/TransactionUtils';
-import {
- adjustRemainingSplitShares,
- resetSplitShares,
- setCustomUnitRateID,
- setIndividualShare,
- setMoneyRequestAmount,
- setMoneyRequestCategory,
- setMoneyRequestMerchant,
- setMoneyRequestPendingFields,
- setMoneyRequestTag,
- setMoneyRequestTaxAmount,
- setMoneyRequestTaxRate,
- setSplitShares,
-} from '@userActions/IOU';
import {hasInvoicingDetails} from '@userActions/Policy/Policy';
import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index 0333ee5fb11d..ee217c8616b5 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -710,6 +710,7 @@ function MoneyRequestConfirmationListFooter({
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
disabledStyle={styles.cursorDefault}
+ style={styles.flex1}
>
{
- InteractionManager.runAfterInteractions(() => {
+ requestAnimationFrame(() => {
if (disabled) {
disabledAction?.();
return;
diff --git a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
index 7e2d150c2fc4..80861eb16dfb 100644
--- a/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
+++ b/src/components/ValidateCodeActionModal/ValidateCodeForm/BaseValidateCodeForm.tsx
@@ -1,6 +1,6 @@
import {useFocusEffect} from '@react-navigation/native';
import type {ForwardedRef} from 'react';
-import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import type {StyleProp, ViewStyle} from 'react-native';
import {useOnyx} from 'react-native-onyx';
@@ -16,10 +16,10 @@ import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as Browser from '@libs/Browser';
-import * as ErrorUtils from '@libs/ErrorUtils';
-import * as ValidationUtils from '@libs/ValidationUtils';
-import * as User from '@userActions/User';
+import {isMobileSafari} from '@libs/Browser';
+import {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils';
+import {isValidValidateCode} from '@libs/ValidationUtils';
+import {clearValidateCodeActionError} from '@userActions/User';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -101,6 +101,7 @@ function BaseValidateCodeForm({
const shouldDisableResendValidateCode = !!isOffline || account?.isLoading;
const focusTimeoutRef = useRef(null);
const [timeRemaining, setTimeRemaining] = useState(CONST.REQUEST_CODE_DELAY as number);
+ const [canShowError, setCanShowError] = useState(false);
const timerRef = useRef();
@@ -131,7 +132,7 @@ function BaseValidateCodeForm({
}
// Keyboard won't show if we focus the input with a delay, so we need to focus immediately.
- if (!Browser.isMobileSafari()) {
+ if (!isMobileSafari()) {
focusTimeoutRef.current = setTimeout(() => {
inputValidateCodeRef.current?.focusLastSelected();
}, CONST.ANIMATED_TRANSITION);
@@ -185,7 +186,7 @@ function BaseValidateCodeForm({
if (!isEmptyObject(validateError)) {
clearError();
- User.clearValidateCodeActionError('actionVerified');
+ clearValidateCodeActionError('actionVerified');
}
},
[validateError, clearError],
@@ -195,12 +196,13 @@ function BaseValidateCodeForm({
* Check that all the form fields are valid, then trigger the submit callback
*/
const validateAndSubmitForm = useCallback(() => {
+ setCanShowError(true);
if (!validateCode.trim()) {
setFormError({validateCode: 'validateCodeForm.error.pleaseFillMagicCode'});
return;
}
- if (!ValidationUtils.isValidValidateCode(validateCode)) {
+ if (!isValidValidateCode(validateCode)) {
setFormError({validateCode: 'validateCodeForm.error.incorrectMagicCode'});
return;
}
@@ -209,6 +211,16 @@ function BaseValidateCodeForm({
handleSubmitForm(validateCode);
}, [validateCode, handleSubmitForm]);
+ const errorText = useMemo(() => {
+ if (!canShowError) {
+ return '';
+ }
+ if (formError?.validateCode) {
+ return translate(formError?.validateCode);
+ }
+ return getLatestErrorMessage(account ?? {});
+ }, [canShowError, formError, account, translate]);
+
const shouldShowTimer = timeRemaining > 0 && !isOffline;
return (
<>
@@ -218,8 +230,8 @@ function BaseValidateCodeForm({
name="validateCode"
value={validateCode}
onChangeText={onTextInput}
- errorText={formError?.validateCode ? translate(formError?.validateCode) : ErrorUtils.getLatestErrorMessage(account ?? {})}
- hasError={!isEmptyObject(validateError)}
+ errorText={errorText}
+ hasError={canShowError ? !isEmptyObject(validateError) : false}
onFulfill={validateAndSubmitForm}
autoFocus={false}
/>
@@ -231,9 +243,9 @@ function BaseValidateCodeForm({
)}
User.clearValidateCodeActionError('actionVerified')}
+ onClose={() => clearValidateCodeActionError('actionVerified')}
>
{!shouldShowTimer && (
@@ -263,7 +275,7 @@ function BaseValidateCodeForm({
clearError()}
style={buttonStyles}
diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx
index 1f2b1db5b0a8..e7d0fcd75089 100644
--- a/src/hooks/useReportIDs.tsx
+++ b/src/hooks/useReportIDs.tsx
@@ -56,7 +56,7 @@ function ReportIDsContextProvider({
const [chatReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (c) => mapOnyxCollectionItems(c, policySelector)});
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
- const [reportsDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
+ const [reportsDrafts] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {initialValue: {}});
const draftAmount = Object.keys(reportsDrafts ?? {}).length;
const [betas] = useOnyx(ONYXKEYS.BETAS);
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 0110e185e3bf..8bfaf4deb8d1 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -312,7 +312,7 @@ const translations = {
addressLine: ({lineNumber}: AddressLineParams) => `Address line ${lineNumber}`,
personalAddress: 'Personal address',
companyAddress: 'Company address',
- noPO: 'PO boxes and mail drop addresses are not allowed',
+ noPO: 'No PO boxes or mail-drop addresses, please.',
city: 'City',
state: 'State',
streetAddress: 'Street address',
@@ -360,7 +360,6 @@ const translations = {
invalidRateError: 'Please enter a valid rate.',
lowRateError: 'Rate must be greater than 0.',
email: 'Please enter a valid email address.',
- login: 'An error occurred while logging in. Please try again.',
},
comma: 'comma',
semicolon: 'semicolon',
@@ -848,7 +847,7 @@ const translations = {
assignTask: 'Assign task',
header: 'Quick action',
trackManual: 'Create expense',
- trackScan: 'Create expense for receipt',
+ trackScan: 'Scan receipt',
trackDistance: 'Track distance',
noLongerHaveReportAccess: 'You no longer have access to your previous quick action destination. Pick a new one below.',
updateDestination: 'Update destination',
@@ -1080,7 +1079,6 @@ const translations = {
bookingArchivedDescription: 'This booking is archived because the trip date has passed. Add an expense for the final amount if needed.',
attendees: 'Attendees',
paymentComplete: 'Payment complete',
- justTrackIt: 'Just track it (don’t submit it)',
time: 'Time',
startDate: 'Start date',
endDate: 'End date',
@@ -2216,7 +2214,6 @@ const translations = {
whatsYourLegalName: 'What’s your legal name?',
whatsYourDOB: 'What’s your date of birth?',
whatsYourAddress: 'What’s your address?',
- noPOBoxesPlease: 'No PO boxes or mail-drop addresses, please.',
whatsYourSSN: 'What are the last four digits of your Social Security Number?',
noPersonalChecks: 'Don’t worry, no personal credit checks here!',
whatsYourPhoneNumber: 'What’s your phone number?',
@@ -2519,6 +2516,7 @@ const translations = {
carType: 'Car type',
cancellation: 'Cancellation policy',
cancellationUntil: 'Free cancellation until',
+ freeCancellation: 'Free cancellation',
confirmation: 'Confirmation number',
},
train: 'Rail',
@@ -3562,7 +3560,7 @@ const translations = {
},
earnSection: {
title: 'Earn',
- subtitle: 'Enable optional functionality to streamline your revenue and get paid faster.',
+ subtitle: 'Streamline your revenue and get paid faster.',
},
organizeSection: {
title: 'Organize',
@@ -3819,7 +3817,7 @@ const translations = {
},
emptyWorkspace: {
title: 'Create a workspace',
- subtitle: 'Create a workspace to track receipts, reimburse expenses, send invoices, and more -- all at the speed of chat.',
+ subtitle: 'Create a workspace to track receipts, reimburse expenses, send invoices, and more — all at the speed of chat.',
createAWorkspaceCTA: 'Get Started',
features: {
trackAndCollect: 'Track and collect receipts',
@@ -3837,6 +3835,7 @@ const translations = {
new: {
newWorkspace: 'New workspace',
getTheExpensifyCardAndMore: 'Get the Expensify Card and more',
+ confirmWorkspace: 'Confirm Workspace',
},
people: {
genericFailureMessage: 'An error occurred removing a member from the workspace, please try again.',
@@ -4559,6 +4558,7 @@ const translations = {
description: 'Choose from the support options below:',
chatWithConcierge: 'Chat with Concierge',
scheduleSetupCall: 'Schedule a setup call',
+ scheduleADemo: 'Schedule a demo',
questionMarkButtonTooltip: 'Get assistance from our team',
exploreHelpDocs: 'Explore help docs',
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 07b9620d6efd..5af6b4768e44 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -303,7 +303,7 @@ const translations = {
addressLine: ({lineNumber}: AddressLineParams) => `Dirección línea ${lineNumber}`,
personalAddress: 'Dirección física personal',
companyAddress: 'Dirección física de la empresa',
- noPO: 'No se aceptan apartados ni direcciones postales',
+ noPO: 'Nada de apartados de correos ni direcciones de envío, por favor.',
city: 'Ciudad',
state: 'Estado',
streetAddress: 'Dirección',
@@ -351,7 +351,6 @@ const translations = {
invalidRateError: 'Por favor, introduce una tarifa válida.',
lowRateError: 'La tarifa debe ser mayor que 0.',
email: 'Por favor, introduzca una dirección de correo electrónico válida.',
- login: 'Se produjo un error al iniciar sesión. Por favor intente nuevamente.',
},
comma: 'la coma',
semicolon: 'el punto y coma',
@@ -843,7 +842,7 @@ const translations = {
assignTask: 'Assignar tarea',
header: 'Acción rápida',
trackManual: 'Crear gasto',
- trackScan: 'Crear gasto por recibo',
+ trackScan: 'Escanear recibo',
trackDistance: 'Crear gasto por desplazamiento',
noLongerHaveReportAccess: 'Ya no tienes acceso al destino previo de esta acción rápida. Escoge uno nuevo a continuación.',
updateDestination: 'Actualiza el destino',
@@ -1078,7 +1077,6 @@ const translations = {
bookingArchivedDescription: 'Esta reserva está archivada porque la fecha del viaje ha pasado. Agregue un gasto por el monto final si es necesario.',
attendees: 'Asistentes',
paymentComplete: 'Pago completo',
- justTrackIt: 'Solo guardarlo (no enviarlo)',
time: 'Tiempo',
startDate: 'Fecha de inicio',
endDate: 'Fecha de finalización',
@@ -2240,7 +2238,6 @@ const translations = {
whatsYourLegalName: '¿Cuál es tu nombre legal?',
whatsYourDOB: '¿Cuál es tu fecha de nacimiento?',
whatsYourAddress: '¿Cuál es tu dirección?',
- noPOBoxesPlease: 'Nada de apartados de correos ni direcciones de envío, por favor.',
whatsYourSSN: '¿Cuáles son los últimos 4 dígitos de tu número de la seguridad social?',
noPersonalChecks: 'No te preocupes, no hacemos verificaciones de crédito personales.',
whatsYourPhoneNumber: '¿Cuál es tu número de teléfono?',
@@ -2543,6 +2540,7 @@ const translations = {
carType: 'Tipo de coche',
cancellation: 'Política de cancelación',
cancellationUntil: 'Cancelación gratuita hasta el',
+ freeCancellation: 'Cancelación gratuita',
confirmation: 'Número de confirmación',
},
train: 'Tren',
@@ -3604,7 +3602,7 @@ const translations = {
},
earnSection: {
title: 'Gane',
- subtitle: 'Habilita funciones opcionales para agilizar tus ingresos y recibir pagos más rápido.',
+ subtitle: 'Agiliza tus ingresos y recibe pagos más rápido.',
},
organizeSection: {
title: 'Organizar',
@@ -3881,6 +3879,7 @@ const translations = {
new: {
newWorkspace: 'Nuevo espacio de trabajo',
getTheExpensifyCardAndMore: 'Consigue la Tarjeta Expensify y más',
+ confirmWorkspace: 'Confirmar espacio de trabajo',
},
people: {
genericFailureMessage: 'Se ha producido un error al intentar eliminar a un miembro del espacio de trabajo. Por favor, inténtalo más tarde.',
@@ -4606,6 +4605,7 @@ const translations = {
description: 'Elige una de las siguientes opciones:',
chatWithConcierge: 'Chatear con Concierge',
scheduleSetupCall: 'Concertar una llamada',
+ scheduleADemo: 'Programa una demostración',
questionMarkButtonTooltip: 'Obtén ayuda de nuestro equipo',
exploreHelpDocs: 'Explorar la documentación de ayuda',
},
diff --git a/src/libs/API/parameters/CreateWorkspaceParams.ts b/src/libs/API/parameters/CreateWorkspaceParams.ts
index 91c1039169aa..313ef1bd6268 100644
--- a/src/libs/API/parameters/CreateWorkspaceParams.ts
+++ b/src/libs/API/parameters/CreateWorkspaceParams.ts
@@ -11,6 +11,8 @@ type CreateWorkspaceParams = {
customUnitID: string;
customUnitRateID: string;
engagementChoice?: string;
+ currency: string;
+ file?: File;
};
export default CreateWorkspaceParams;
diff --git a/src/libs/API/parameters/SearchForRoomsToMentionParams.ts b/src/libs/API/parameters/SearchForRoomsToMentionParams.ts
index 6a4efe7aed6b..adc2bdd2e4eb 100644
--- a/src/libs/API/parameters/SearchForRoomsToMentionParams.ts
+++ b/src/libs/API/parameters/SearchForRoomsToMentionParams.ts
@@ -1,6 +1,6 @@
type SearchForRoomsToMentionParams = {
query: string;
- policyID: string;
+ policyID?: string;
};
export default SearchForRoomsToMentionParams;
diff --git a/src/libs/API/parameters/ShareTrackedExpenseParams.ts b/src/libs/API/parameters/ShareTrackedExpenseParams.ts
index cee4bc40d9ac..dac75ae5c5d5 100644
--- a/src/libs/API/parameters/ShareTrackedExpenseParams.ts
+++ b/src/libs/API/parameters/ShareTrackedExpenseParams.ts
@@ -20,6 +20,10 @@ type ShareTrackedExpenseParams = {
taxCode: string;
taxAmount: number;
billable?: boolean;
+ policyExpenseChatReportID?: string;
+ policyExpenseCreatedReportActionID?: string;
+ adminsChatReportID?: string;
+ adminsCreatedReportActionID?: string;
};
export default ShareTrackedExpenseParams;
diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts
index 26a5d209b0ff..cb5363225a4c 100644
--- a/src/libs/CurrencyUtils.ts
+++ b/src/libs/CurrencyUtils.ts
@@ -2,8 +2,9 @@ import Onyx from 'react-native-onyx';
import CONST from '@src/CONST';
import type {OnyxValues} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {Currency} from '@src/types/onyx';
import BaseLocaleListener from './Localize/LocaleListener/BaseLocaleListener';
-import * as NumberFormatUtils from './NumberFormatUtils';
+import {format, formatToParts} from './NumberFormatUtils';
let currencyList: OnyxValues[typeof ONYXKEYS.CURRENCY_LIST] = {};
@@ -30,6 +31,11 @@ function getCurrencyDecimals(currency: string = CONST.CURRENCY.USD): number {
return decimals ?? 2;
}
+function getCurrency(currency: string = CONST.CURRENCY.USD): Currency | null {
+ const currencyItem = currencyList?.[currency];
+ return currencyItem;
+}
+
/**
* Returns the currency's minor unit quantity
* e.g. Cent in USD
@@ -44,7 +50,7 @@ function getCurrencyUnit(currency: string = CONST.CURRENCY.USD): number {
* Get localized currency symbol for currency(ISO 4217) Code
*/
function getLocalizedCurrencySymbol(currencyCode: string): string | undefined {
- const parts = NumberFormatUtils.formatToParts(BaseLocaleListener.getPreferredLocale(), 0, {
+ const parts = formatToParts(BaseLocaleListener.getPreferredLocale(), 0, {
style: 'currency',
currency: currencyCode,
});
@@ -62,7 +68,7 @@ function getCurrencySymbol(currencyCode: string): string | undefined {
* Whether the currency symbol is left-to-right.
*/
function isCurrencySymbolLTR(currencyCode: string): boolean {
- const parts = NumberFormatUtils.formatToParts(BaseLocaleListener.getPreferredLocale(), 0, {
+ const parts = formatToParts(BaseLocaleListener.getPreferredLocale(), 0, {
style: 'currency',
currency: currencyCode,
});
@@ -121,7 +127,7 @@ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURR
if (!currency) {
currencyWithFallback = CONST.CURRENCY.USD;
}
- return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
+ return format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
style: 'currency',
currency: currencyWithFallback,
@@ -143,7 +149,7 @@ function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURR
function convertToShortDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD): string {
const convertedAmount = convertToFrontendAmountAsInteger(amountInCents, currency);
- return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
+ return format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
style: 'currency',
currency,
@@ -161,7 +167,7 @@ function convertToShortDisplayString(amountInCents = 0, currency: string = CONST
*/
function convertAmountToDisplayString(amount = 0, currency: string = CONST.CURRENCY.USD): string {
const convertedAmount = amount / 100.0;
- return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
+ return format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
style: 'currency',
currency,
minimumFractionDigits: CONST.MIN_TAX_RATE_DECIMAL_PLACES,
@@ -174,7 +180,7 @@ function convertAmountToDisplayString(amount = 0, currency: string = CONST.CURRE
*/
function convertToDisplayStringWithoutCurrency(amountInCents: number, currency: string = CONST.CURRENCY.USD) {
const convertedAmount = convertToFrontendAmountAsInteger(amountInCents, currency);
- return NumberFormatUtils.formatToParts(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
+ return formatToParts(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
style: 'currency',
currency,
@@ -216,5 +222,6 @@ export {
convertToDisplayStringWithoutCurrency,
isValidCurrencyCode,
convertToShortDisplayString,
+ getCurrency,
sanitizeCurrencyCode,
};
diff --git a/src/libs/GoogleTagManager/index.native.ts b/src/libs/GoogleTagManager/index.native.ts
index 267591d6ef40..aa10ffc4fe3c 100644
--- a/src/libs/GoogleTagManager/index.native.ts
+++ b/src/libs/GoogleTagManager/index.native.ts
@@ -1,11 +1,12 @@
+/* eslint-disable @typescript-eslint/naming-convention */
import analytics from '@react-native-firebase/analytics';
import Log from '@libs/Log';
import type {GoogleTagManagerEvent} from './types';
import type GoogleTagManagerModule from './types';
function publishEvent(event: GoogleTagManagerEvent, accountID: number) {
- analytics().logEvent(event, {accountID});
- Log.info('[GTM] event published', false, {event, accountID});
+ analytics().logEvent(event, {user_id: accountID});
+ Log.info('[GTM] event published', false, {event, user_id: accountID});
}
const GoogleTagManager: GoogleTagManagerModule = {
diff --git a/src/libs/GoogleTagManager/index.ts b/src/libs/GoogleTagManager/index.ts
index e859da8c12e9..c02fa899c9b4 100644
--- a/src/libs/GoogleTagManager/index.ts
+++ b/src/libs/GoogleTagManager/index.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/naming-convention */
import Log from '@libs/Log';
import type {GoogleTagManagerEvent} from './types';
import type GoogleTagManagerModule from './types';
@@ -14,7 +15,7 @@ type WindowWithDataLayer = Window & {
type DataLayerPushParams = {
event: GoogleTagManagerEvent;
- accountID: number;
+ user_id: number;
};
declare const window: WindowWithDataLayer;
@@ -24,8 +25,12 @@ function publishEvent(event: GoogleTagManagerEvent, accountID: number) {
return;
}
- window.dataLayer.push({event, accountID});
- Log.info('[GTM] event published', false, {event, accountID});
+ const params = {event, user_id: accountID};
+
+ // Pass a copy of params here since the dataLayer modifies the object
+ window.dataLayer.push({...params});
+
+ Log.info('[GTM] event published', false, params);
}
const GoogleTagManager: GoogleTagManagerModule = {
diff --git a/src/libs/LoginUtils.ts b/src/libs/LoginUtils.ts
index f496c9de0e6e..191fd72db4e9 100644
--- a/src/libs/LoginUtils.ts
+++ b/src/libs/LoginUtils.ts
@@ -1,11 +1,7 @@
import {PUBLIC_DOMAINS, Str} from 'expensify-common';
import Onyx from 'react-native-onyx';
-import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import * as Session from './actions/Session';
-import Navigation from './Navigation/Navigation';
import {parsePhoneNumber} from './PhoneNumber';
let countryCodeByIP: number;
@@ -79,26 +75,4 @@ function areEmailsFromSamePrivateDomain(email1: string, email2: string): boolean
return Str.extractEmailDomain(email1).toLowerCase() === Str.extractEmailDomain(email2).toLowerCase();
}
-function postSAMLLogin(body: FormData): Promise {
- return fetch(CONFIG.EXPENSIFY.SAML_URL, {
- method: CONST.NETWORK.METHOD.POST,
- body,
- credentials: 'omit',
- }).then((response) => {
- if (!response.ok) {
- throw new Error('An error occurred while logging in. Please try again');
- }
- return response.json() as Promise;
- });
-}
-
-function handleSAMLLoginError(errorMessage: string, cleanSignInData: boolean) {
- if (cleanSignInData) {
- Session.clearSignInData();
- }
-
- Session.setAccountError(errorMessage);
- Navigation.goBack(ROUTES.HOME);
-}
-
-export {getPhoneNumberWithoutSpecialChars, appendCountryCode, isEmailPublicDomain, validateNumber, getPhoneLogin, areEmailsFromSamePrivateDomain, postSAMLLogin, handleSAMLLoginError};
+export {getPhoneNumberWithoutSpecialChars, appendCountryCode, isEmailPublicDomain, validateNumber, getPhoneLogin, areEmailsFromSamePrivateDomain};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index e05d316bd8ca..cf6bb5eae810 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -31,6 +31,7 @@ import type {
TransactionDuplicateNavigatorParamList,
TravelNavigatorParamList,
WalletStatementNavigatorParamList,
+ WorkspaceConfirmationNavigatorParamList,
} from '@navigation/types';
import type {Screen} from '@src/SCREENS';
import SCREENS from '@src/SCREENS';
@@ -137,6 +138,10 @@ const ReportSettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Report/VisibilityPage').default,
});
+const WorkspaceConfirmationModalStackNavigator = createModalStackNavigator({
+ [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: () => require('../../../../pages/workspace/WorkspaceConfirmationPage').default,
+});
+
const TaskModalStackNavigator = createModalStackNavigator({
[SCREENS.TASK.TITLE]: () => require('../../../../pages/tasks/TaskTitlePage').default,
[SCREENS.TASK.ASSIGNEE]: () => require('../../../../pages/tasks/TaskAssigneeSelectorModal').default,
@@ -730,4 +735,5 @@ export {
SearchSavedSearchModalStackNavigator,
MissingPersonalDetailsModalStackNavigator,
DebugModalStackNavigator,
+ WorkspaceConfirmationModalStackNavigator,
};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index 95f82f4a2fdf..ac53ad3b64d2 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -138,6 +138,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
name={SCREENS.RIGHT_MODAL.MONEY_REQUEST}
component={ModalStackNavigators.MoneyRequestModalStackNavigator}
/>
+ ['config'] = {
},
},
},
+ [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: {
+ screens: {
+ [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: ROUTES.WORKSPACE_CONFIRMATION.route,
+ },
+ },
[SCREENS.RIGHT_MODAL.NEW_TASK]: {
screens: {
[SCREENS.NEW_TASK.ROOT]: ROUTES.NEW_TASK.route,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 994178139675..41e8c8cc7824 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1282,6 +1282,12 @@ type MoneyRequestNavigatorParamList = {
};
};
+type WorkspaceConfirmationNavigatorParamList = {
+ [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: {
+ backTo?: Routes;
+ };
+};
+
type NewTaskNavigatorParamList = {
[SCREENS.NEW_TASK.ROOT]: {
backTo?: Routes;
@@ -1455,6 +1461,7 @@ type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.PARTICIPANTS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.ROOM_MEMBERS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.MONEY_REQUEST]: NavigatorScreenParams;
+ [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.NEW_TASK]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.TEACHERS_UNITE]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.TASK_DETAILS]: NavigatorScreenParams;
@@ -1863,4 +1870,5 @@ export type {
MissingPersonalDetailsParamList,
DebugParamList,
MigratedUserModalNavigatorParamList,
+ WorkspaceConfirmationNavigatorParamList,
};
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 5314391e2166..75808914d72f 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -1,3 +1,5 @@
+/* eslint-disable @typescript-eslint/prefer-for-of */
+
/* eslint-disable no-continue */
import {Str} from 'expensify-common';
import lodashOrderBy from 'lodash/orderBy';
@@ -29,22 +31,105 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import Timing from './actions/Timing';
import filterArrayByMatch from './filterArrayByMatch';
-import * as LocalePhoneNumber from './LocalePhoneNumber';
-import * as Localize from './Localize';
-import * as LoginUtils from './LoginUtils';
+import {formatPhoneNumber} from './LocalePhoneNumber';
+import {translate, translateLocal} from './Localize';
+import {appendCountryCode, getPhoneNumberWithoutSpecialChars} from './LoginUtils';
import ModifiedExpenseMessage from './ModifiedExpenseMessage';
import Navigation from './Navigation/Navigation';
import Parser from './Parser';
import Performance from './Performance';
-import * as PersonalDetailsUtils from './PersonalDetailsUtils';
-import * as PhoneNumber from './PhoneNumber';
-import * as PolicyUtils from './PolicyUtils';
-import * as ReportActionUtils from './ReportActionsUtils';
-import * as ReportUtils from './ReportUtils';
-import * as TaskUtils from './TaskUtils';
-import * as UserUtils from './UserUtils';
-
-type SearchOption = ReportUtils.OptionData & {
+import {getDisplayNameOrDefault} from './PersonalDetailsUtils';
+import {addSMSDomainIfPhoneNumber, parsePhoneNumber} from './PhoneNumber';
+import {canSendInvoiceFromWorkspace} from './PolicyUtils';
+import {
+ getCombinedReportActions,
+ getExportIntegrationLastMessageText,
+ getIOUReportIDFromReportActionPreview,
+ getMessageOfOldDotReportAction,
+ getOneTransactionThreadReportID,
+ getOriginalMessage,
+ getReportActionMessageText,
+ getSortedReportActions,
+ isActionableAddPaymentCard,
+ isActionOfType,
+ isClosedAction,
+ isCreatedTaskReportAction,
+ isDeletedParentAction,
+ isModifiedExpenseAction,
+ isMoneyRequestAction,
+ isOldDotReportAction,
+ isPendingRemove,
+ isReimbursementDeQueuedAction,
+ isReimbursementQueuedAction,
+ isReportPreviewAction,
+ isResolvedActionTrackExpense,
+ isTaskAction,
+ isThreadParentMessage,
+ isUnapprovedAction,
+ isWhisperAction,
+ shouldReportActionBeVisible,
+} from './ReportActionsUtils';
+import {
+ canUserPerformWriteAction,
+ formatReportLastMessageText,
+ getAllReportErrors,
+ getChatRoomSubtitle,
+ getDeletedParentActionMessageForChatReport,
+ getDisplayNameForParticipant,
+ getDowngradeWorkspaceMessage,
+ getIcons,
+ getIOUApprovedMessage,
+ getIOUForwardedMessage,
+ getIOUSubmittedMessage,
+ getIOUUnapprovedMessage,
+ getMoneyRequestSpendBreakdown,
+ getParticipantsAccountIDsForDisplay,
+ getPolicyName,
+ getReimbursementDeQueuedActionMessage,
+ getReimbursementQueuedActionMessage,
+ getRejectedReportMessage,
+ getReportAutomaticallyApprovedMessage,
+ getReportAutomaticallyForwardedMessage,
+ getReportAutomaticallySubmittedMessage,
+ getReportLastMessage,
+ getReportName,
+ getReportNotificationPreference,
+ getReportOrDraftReport,
+ getReportParticipantsTitle,
+ getReportPreviewMessage,
+ getUpgradeWorkspaceMessage,
+ hasIOUWaitingOnCurrentUserBankAccount,
+ isArchivedNonExpenseReport,
+ isChatThread,
+ isDefaultRoom,
+ isDraftReport,
+ isExpenseReport,
+ isHiddenForCurrentUser,
+ isInvoiceRoom,
+ isIOUOwnedByCurrentUser,
+ isMoneyRequest,
+ isPolicyAdmin,
+ isReportMessageAttachment,
+ isUnread,
+ isAdminRoom as reportUtilsIsAdminRoom,
+ isAnnounceRoom as reportUtilsIsAnnounceRoom,
+ isChatReport as reportUtilsIsChatReport,
+ isChatRoom as reportUtilsIsChatRoom,
+ isGroupChat as reportUtilsIsGroupChat,
+ isMoneyRequestReport as reportUtilsIsMoneyRequestReport,
+ isOneOnOneChat as reportUtilsIsOneOnOneChat,
+ isPolicyExpenseChat as reportUtilsIsPolicyExpenseChat,
+ isSelfDM as reportUtilsIsSelfDM,
+ isTaskReport as reportUtilsIsTaskReport,
+ shouldDisplayViolationsRBRInLHN,
+ shouldReportBeInOptionList,
+ shouldReportShowSubscript,
+} from './ReportUtils';
+import type {OptionData} from './ReportUtils';
+import {getTaskCreatedMessage, getTaskReportActionMessage} from './TaskUtils';
+import {generateAccountID} from './UserUtils';
+
+type SearchOption = OptionData & {
item: T;
};
@@ -53,7 +138,7 @@ type OptionList = {
personalDetails: Array>;
};
-type Option = Partial;
+type Option = Partial;
/**
* A narrowed version of `Option` is used when we have a guarantee that given values exist.
@@ -87,32 +172,41 @@ type Section = SectionBase & {
data: Option[];
};
-type GetOptionsConfig = {
- betas?: OnyxEntry;
+type GetValidOptionsSharedConfig = {
+ includeP2P?: boolean;
+ transactionViolations?: OnyxCollection;
+ action?: IOUAction;
+ shouldBoldTitleByDefault?: boolean;
selectedOptions?: Option[];
- excludeLogins?: string[];
+};
+
+type GetValidReportsConfig = {
+ betas?: OnyxEntry;
includeMultipleParticipantReports?: boolean;
- includeRecentReports?: boolean;
- includeSelfDM?: boolean;
showChatPreviewLine?: boolean;
forcePolicyNamePreview?: boolean;
+ includeSelfDM?: boolean;
includeOwnedWorkspaceChats?: boolean;
includeThreads?: boolean;
includeTasks?: boolean;
includeMoneyRequests?: boolean;
- includeP2P?: boolean;
- includeSelectedOptions?: boolean;
- transactionViolations?: OnyxCollection;
includeInvoiceRooms?: boolean;
includeDomainEmail?: boolean;
- action?: IOUAction;
+ optionsToExclude?: Array>;
+} & GetValidOptionsSharedConfig;
+
+type GetOptionsConfig = {
+ excludeLogins?: string[];
+ includeRecentReports?: boolean;
+ includeSelectedOptions?: boolean;
recentAttendees?: Attendee[];
- shouldBoldTitleByDefault?: boolean;
-};
+ shouldSeparateWorkspaceChat?: boolean;
+ shouldSeparateSelfDMChat?: boolean;
+} & GetValidReportsConfig;
type GetUserToInviteConfig = {
searchValue: string | undefined;
- optionsToExclude?: Array>;
+ optionsToExclude?: Array>;
reportActions?: ReportActions;
shouldAcceptName?: boolean;
} & Pick;
@@ -134,10 +228,12 @@ type SectionForSearchTerm = {
section: Section;
};
type Options = {
- recentReports: ReportUtils.OptionData[];
- personalDetails: ReportUtils.OptionData[];
- userToInvite: ReportUtils.OptionData | null;
- currentUserOption: ReportUtils.OptionData | null | undefined;
+ recentReports: OptionData[];
+ personalDetails: OptionData[];
+ userToInvite: OptionData | null;
+ currentUserOption: OptionData | null | undefined;
+ workspaceChats?: OptionData[];
+ selfDMChat?: OptionData | undefined;
};
type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean};
@@ -165,7 +261,7 @@ type OrderReportOptionsConfig = {
preferRecentExpenseReports?: boolean;
};
-type ReportAndPersonalDetailOptions = Pick;
+type ReportAndPersonalDetailOptions = Pick;
/**
* OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can
@@ -255,15 +351,15 @@ Onyx.connect({
}
const reportActionsArray = Object.values(reportActions[1] ?? {});
- let sortedReportActions = ReportActionUtils.getSortedReportActions(reportActionsArray, true);
+ let sortedReportActions = getSortedReportActions(reportActionsArray, true);
allSortedReportActions[reportID] = sortedReportActions;
// If the report is a one-transaction report and has , we need to return the combined reportActions so that the LHN can display modifications
// to the transaction thread or the report itself
- const transactionThreadReportID = ReportActionUtils.getOneTransactionThreadReportID(reportID, actions[reportActions[0]]);
+ const transactionThreadReportID = getOneTransactionThreadReportID(reportID, actions[reportActions[0]]);
if (transactionThreadReportID) {
const transactionThreadReportActionsArray = Object.values(actions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`] ?? {});
- sortedReportActions = ReportActionUtils.getCombinedReportActions(sortedReportActions, transactionThreadReportID, transactionThreadReportActionsArray, reportID, false);
+ sortedReportActions = getCombinedReportActions(sortedReportActions, transactionThreadReportID, transactionThreadReportActionsArray, reportID, false);
}
const firstReportAction = sortedReportActions.at(0);
@@ -274,17 +370,17 @@ Onyx.connect({
}
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
- const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report);
+ const isWriteActionAllowed = canUserPerformWriteAction(report);
// The report is only visible if it is the last action not deleted that
// does not match a closed or created state.
const reportActionsForDisplay = sortedReportActions.filter(
(reportAction, actionKey) =>
- ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey, canUserPerformWriteAction) &&
- !ReportActionUtils.isWhisperAction(reportAction) &&
+ shouldReportActionBeVisible(reportAction, actionKey, isWriteActionAllowed) &&
+ !isWhisperAction(reportAction) &&
reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED &&
reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE &&
- !ReportActionUtils.isResolvedActionTrackExpense(reportAction),
+ !isResolvedActionTrackExpense(reportAction),
);
const reportActionForDisplay = reportActionsForDisplay.at(0);
if (!reportActionForDisplay) {
@@ -366,11 +462,11 @@ function isPersonalDetailsReady(personalDetails: OnyxEntry)
/**
* Get the participant option for a report.
*/
-function getParticipantsOption(participant: ReportUtils.OptionData | Participant, personalDetails: OnyxEntry): Participant {
+function getParticipantsOption(participant: OptionData | Participant, personalDetails: OnyxEntry): Participant {
const detail = participant.accountID ? getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID] : undefined;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const login = detail?.login || participant.login || '';
- const displayName = LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(detail, login || participant.text));
+ const displayName = formatPhoneNumber(getDisplayNameOrDefault(detail, login || participant.text));
return {
keyForList: String(detail?.accountID),
@@ -379,7 +475,7 @@ function getParticipantsOption(participant: ReportUtils.OptionData | Participant
text: displayName,
firstName: detail?.firstName ?? '',
lastName: detail?.lastName ?? '',
- alternateText: LocalePhoneNumber.formatPhoneNumber(login) || displayName,
+ alternateText: formatPhoneNumber(login) || displayName,
icons: [
{
source: detail?.avatar ?? FallbackAvatar,
@@ -420,27 +516,27 @@ function uniqFast(items: string[]): string[] {
function getLastActorDisplayName(lastActorDetails: Partial | null, hasMultipleParticipants: boolean) {
return hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID
? // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- lastActorDetails.firstName || LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails))
+ lastActorDetails.firstName || formatPhoneNumber(getDisplayNameOrDefault(lastActorDetails))
: '';
}
/**
* Update alternate text for the option when applicable
*/
-function getAlternateText(option: ReportUtils.OptionData, {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig) {
- const report = ReportUtils.getReportOrDraftReport(option.reportID);
- const isAdminRoom = ReportUtils.isAdminRoom(report);
- const isAnnounceRoom = ReportUtils.isAnnounceRoom(report);
- const isGroupChat = ReportUtils.isGroupChat(report);
- const isExpenseThread = ReportUtils.isMoneyRequest(report);
- const formattedLastMessageText = ReportUtils.formatReportLastMessageText(Parser.htmlToText(option.lastMessageText ?? ''));
+function getAlternateText(option: OptionData, {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig) {
+ const report = getReportOrDraftReport(option.reportID);
+ const isAdminRoom = reportUtilsIsAdminRoom(report);
+ const isAnnounceRoom = reportUtilsIsAnnounceRoom(report);
+ const isGroupChat = reportUtilsIsGroupChat(report);
+ const isExpenseThread = isMoneyRequest(report);
+ const formattedLastMessageText = formatReportLastMessageText(Parser.htmlToText(option.lastMessageText ?? ''));
if (isExpenseThread || option.isMoneyRequestReport) {
- return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : Localize.translate(preferredLocale, 'iou.expense');
+ return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : translate(preferredLocale, 'iou.expense');
}
if (option.isThread) {
- return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : Localize.translate(preferredLocale, 'threads.thread');
+ return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : translate(preferredLocale, 'threads.thread');
}
if (option.isChatRoom && !isAdminRoom && !isAnnounceRoom) {
@@ -452,16 +548,16 @@ function getAlternateText(option: ReportUtils.OptionData, {showChatPreviewLine =
}
if (option.isTaskReport) {
- return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : Localize.translate(preferredLocale, 'task.task');
+ return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : translate(preferredLocale, 'task.task');
}
if (isGroupChat) {
- return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : Localize.translate(preferredLocale, 'common.group');
+ return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageText : translate(preferredLocale, 'common.group');
}
return showChatPreviewLine && formattedLastMessageText
? formattedLastMessageText
- : LocalePhoneNumber.formatPhoneNumber(option.participantsList && option.participantsList.length > 0 ? option.participantsList.at(0)?.login ?? '' : '');
+ : formatPhoneNumber(option.participantsList && option.participantsList.length > 0 ? option.participantsList.at(0)?.login ?? '' : '');
}
function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchValue: string) {
@@ -476,7 +572,7 @@ function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchV
memberDetails += ` ${personalDetail.lastName}`;
}
if (personalDetail.displayName) {
- memberDetails += ` ${PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail)}`;
+ memberDetails += ` ${getDisplayNameOrDefault(personalDetail)}`;
}
if (personalDetail.phoneNumber) {
memberDetails += ` ${personalDetail.phoneNumber}`;
@@ -492,10 +588,10 @@ function getIOUReportIDOfLastAction(report: OnyxEntry): string | undefin
return;
}
const lastAction = lastVisibleReportActions[report.reportID];
- if (!ReportActionUtils.isReportPreviewAction(lastAction)) {
+ if (!isReportPreviewAction(lastAction)) {
return;
}
- return ReportUtils.getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastAction))?.reportID;
+ return getReportOrDraftReport(getIOUReportIDFromReportActionPreview(lastAction))?.reportID;
}
/**
@@ -509,117 +605,114 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
const lastOriginalReportAction = reportID ? lastReportActions[reportID] : undefined;
let lastMessageTextFromReport = '';
- if (ReportUtils.isArchivedNonExpenseReport(report)) {
+ if (isArchivedNonExpenseReport(report)) {
const archiveReason =
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- (ReportActionUtils.isClosedAction(lastOriginalReportAction) && ReportActionUtils.getOriginalMessage(lastOriginalReportAction)?.reason) || CONST.REPORT.ARCHIVE_REASON.DEFAULT;
+ (isClosedAction(lastOriginalReportAction) && getOriginalMessage(lastOriginalReportAction)?.reason) || CONST.REPORT.ARCHIVE_REASON.DEFAULT;
switch (archiveReason) {
case CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED:
case CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY:
case CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED: {
- lastMessageTextFromReport = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, {
- displayName: LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails)),
- policyName: ReportUtils.getPolicyName(report, false, policy),
+ lastMessageTextFromReport = translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, {
+ displayName: formatPhoneNumber(getDisplayNameOrDefault(lastActorDetails)),
+ policyName: getPolicyName(report, false, policy),
});
break;
}
case CONST.REPORT.ARCHIVE_REASON.BOOKING_END_DATE_HAS_PASSED: {
- lastMessageTextFromReport = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`);
+ lastMessageTextFromReport = translate(preferredLocale, `reportArchiveReasons.${archiveReason}`);
break;
}
default: {
- lastMessageTextFromReport = Localize.translate(preferredLocale, `reportArchiveReasons.default`);
+ lastMessageTextFromReport = translate(preferredLocale, `reportArchiveReasons.default`);
}
}
- } else if (ReportActionUtils.isMoneyRequestAction(lastReportAction)) {
- const properSchemaForMoneyRequestMessage = ReportUtils.getReportPreviewMessage(report, lastReportAction, true, false, null, true);
- lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForMoneyRequestMessage);
- } else if (ReportActionUtils.isReportPreviewAction(lastReportAction)) {
- const iouReport = ReportUtils.getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction));
+ } else if (isMoneyRequestAction(lastReportAction)) {
+ const properSchemaForMoneyRequestMessage = getReportPreviewMessage(report, lastReportAction, true, false, null, true);
+ lastMessageTextFromReport = formatReportLastMessageText(properSchemaForMoneyRequestMessage);
+ } else if (isReportPreviewAction(lastReportAction)) {
+ const iouReport = getReportOrDraftReport(getIOUReportIDFromReportActionPreview(lastReportAction));
const lastIOUMoneyReportAction = iouReport?.reportID
? allSortedReportActions[iouReport.reportID]?.find(
(reportAction, key): reportAction is ReportAction =>
- ReportActionUtils.shouldReportActionBeVisible(reportAction, key, ReportUtils.canUserPerformWriteAction(report)) &&
+ shouldReportActionBeVisible(reportAction, key, canUserPerformWriteAction(report)) &&
reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE &&
- ReportActionUtils.isMoneyRequestAction(reportAction),
+ isMoneyRequestAction(reportAction),
)
: undefined;
- const reportPreviewMessage = ReportUtils.getReportPreviewMessage(
+ const reportPreviewMessage = getReportPreviewMessage(
!isEmptyObject(iouReport) ? iouReport : null,
lastIOUMoneyReportAction,
true,
- ReportUtils.isChatReport(report),
+ reportUtilsIsChatReport(report),
null,
true,
lastReportAction,
);
- lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(reportPreviewMessage);
- } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) {
- lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report);
- } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) {
- lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report, true);
- } else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) {
- lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction);
- } else if (ReportActionUtils.isPendingRemove(lastReportAction) && report?.reportID && ReportActionUtils.isThreadParentMessage(lastReportAction, report.reportID)) {
- lastMessageTextFromReport = Localize.translateLocal('parentReportAction.hiddenMessage');
- } else if (ReportUtils.isReportMessageAttachment({text: report?.lastMessageText ?? '', html: report?.lastMessageHtml, type: ''})) {
- lastMessageTextFromReport = `[${Localize.translateLocal('common.attachment')}]`;
- } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) {
+ lastMessageTextFromReport = formatReportLastMessageText(reportPreviewMessage);
+ } else if (isReimbursementQueuedAction(lastReportAction)) {
+ lastMessageTextFromReport = getReimbursementQueuedActionMessage(lastReportAction, report);
+ } else if (isReimbursementDeQueuedAction(lastReportAction)) {
+ lastMessageTextFromReport = getReimbursementDeQueuedActionMessage(lastReportAction, report, true);
+ } else if (isDeletedParentAction(lastReportAction) && reportUtilsIsChatReport(report)) {
+ lastMessageTextFromReport = getDeletedParentActionMessageForChatReport(lastReportAction);
+ } else if (isPendingRemove(lastReportAction) && report?.reportID && isThreadParentMessage(lastReportAction, report.reportID)) {
+ lastMessageTextFromReport = translateLocal('parentReportAction.hiddenMessage');
+ } else if (isReportMessageAttachment({text: report?.lastMessageText ?? '', html: report?.lastMessageHtml, type: ''})) {
+ lastMessageTextFromReport = `[${translateLocal('common.attachment')}]`;
+ } else if (isModifiedExpenseAction(lastReportAction)) {
const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(report?.reportID, lastReportAction);
- lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true);
- } else if (ReportActionUtils.isTaskAction(lastReportAction)) {
- lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(TaskUtils.getTaskReportActionMessage(lastReportAction).text);
- } else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) {
- lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction);
- } else if (
- ReportActionUtils.isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED) ||
- ReportActionUtils.isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED)
- ) {
- const wasSubmittedViaHarvesting = ReportActionUtils.getOriginalMessage(lastReportAction)?.harvesting ?? false;
+ lastMessageTextFromReport = formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true);
+ } else if (isTaskAction(lastReportAction)) {
+ lastMessageTextFromReport = formatReportLastMessageText(getTaskReportActionMessage(lastReportAction).text);
+ } else if (isCreatedTaskReportAction(lastReportAction)) {
+ lastMessageTextFromReport = getTaskCreatedMessage(lastReportAction);
+ } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED) || isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED)) {
+ const wasSubmittedViaHarvesting = getOriginalMessage(lastReportAction)?.harvesting ?? false;
if (wasSubmittedViaHarvesting) {
- lastMessageTextFromReport = ReportUtils.getReportAutomaticallySubmittedMessage(lastReportAction);
+ lastMessageTextFromReport = getReportAutomaticallySubmittedMessage(lastReportAction);
} else {
- lastMessageTextFromReport = ReportUtils.getIOUSubmittedMessage(lastReportAction);
+ lastMessageTextFromReport = getIOUSubmittedMessage(lastReportAction);
}
- } else if (ReportActionUtils.isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.APPROVED)) {
- const {automaticAction} = ReportActionUtils.getOriginalMessage(lastReportAction) ?? {};
+ } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.APPROVED)) {
+ const {automaticAction} = getOriginalMessage(lastReportAction) ?? {};
if (automaticAction) {
- lastMessageTextFromReport = ReportUtils.getReportAutomaticallyApprovedMessage(lastReportAction);
+ lastMessageTextFromReport = getReportAutomaticallyApprovedMessage(lastReportAction);
} else {
- lastMessageTextFromReport = ReportUtils.getIOUApprovedMessage(lastReportAction);
+ lastMessageTextFromReport = getIOUApprovedMessage(lastReportAction);
}
- } else if (ReportActionUtils.isUnapprovedAction(lastReportAction)) {
- lastMessageTextFromReport = ReportUtils.getIOUUnapprovedMessage(lastReportAction);
- } else if (ReportActionUtils.isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.FORWARDED)) {
- const {automaticAction} = ReportActionUtils.getOriginalMessage(lastReportAction) ?? {};
+ } else if (isUnapprovedAction(lastReportAction)) {
+ lastMessageTextFromReport = getIOUUnapprovedMessage(lastReportAction);
+ } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.FORWARDED)) {
+ const {automaticAction} = getOriginalMessage(lastReportAction) ?? {};
if (automaticAction) {
- lastMessageTextFromReport = ReportUtils.getReportAutomaticallyForwardedMessage(lastReportAction, reportID);
+ lastMessageTextFromReport = getReportAutomaticallyForwardedMessage(lastReportAction, reportID);
} else {
- lastMessageTextFromReport = ReportUtils.getIOUForwardedMessage(lastReportAction, report);
+ lastMessageTextFromReport = getIOUForwardedMessage(lastReportAction, report);
}
} else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED) {
- lastMessageTextFromReport = ReportUtils.getRejectedReportMessage();
+ lastMessageTextFromReport = getRejectedReportMessage();
} else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.CORPORATE_UPGRADE) {
- lastMessageTextFromReport = ReportUtils.getUpgradeWorkspaceMessage();
+ lastMessageTextFromReport = getUpgradeWorkspaceMessage();
} else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.TEAM_DOWNGRADE) {
- lastMessageTextFromReport = ReportUtils.getDowngradeWorkspaceMessage();
- } else if (ReportActionUtils.isActionableAddPaymentCard(lastReportAction)) {
- lastMessageTextFromReport = ReportActionUtils.getReportActionMessageText(lastReportAction);
+ lastMessageTextFromReport = getDowngradeWorkspaceMessage();
+ } else if (isActionableAddPaymentCard(lastReportAction)) {
+ lastMessageTextFromReport = getReportActionMessageText(lastReportAction);
} else if (lastReportAction?.actionName === 'EXPORTINTEGRATION') {
- lastMessageTextFromReport = ReportActionUtils.getExportIntegrationLastMessageText(lastReportAction);
- } else if (lastReportAction?.actionName && ReportActionUtils.isOldDotReportAction(lastReportAction)) {
- lastMessageTextFromReport = ReportActionUtils.getMessageOfOldDotReportAction(lastReportAction, false);
+ lastMessageTextFromReport = getExportIntegrationLastMessageText(lastReportAction);
+ } else if (lastReportAction?.actionName && isOldDotReportAction(lastReportAction)) {
+ lastMessageTextFromReport = getMessageOfOldDotReportAction(lastReportAction, false);
}
// we do not want to show report closed in LHN for non archived report so use getReportLastMessage as fallback instead of lastMessageText from report
if (reportID && !report.private_isArchived && report.lastActionType === CONST.REPORT.ACTIONS.TYPE.CLOSED) {
- return lastMessageTextFromReport || (ReportUtils.getReportLastMessage(reportID).lastMessageText ?? '');
+ return lastMessageTextFromReport || (getReportLastMessage(reportID).lastMessageText ?? '');
}
return lastMessageTextFromReport || (report?.lastMessageText ?? '');
}
function hasReportErrors(report: Report, reportActions: OnyxEntry) {
- return !isEmptyObject(ReportUtils.getAllReportErrors(report, reportActions));
+ return !isEmptyObject(getAllReportErrors(report, reportActions));
}
/**
@@ -631,9 +724,9 @@ function createOption(
report: OnyxInputOrEntry,
reportActions: ReportActions,
config?: PreviewConfig,
-): ReportUtils.OptionData {
+): OptionData {
const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false} = config ?? {};
- const result: ReportUtils.OptionData = {
+ const result: OptionData = {
text: undefined,
alternateText: undefined,
pendingAction: undefined,
@@ -676,39 +769,39 @@ function createOption(
result.participantsList = personalDetailList;
result.isOptimisticPersonalDetail = personalDetail?.isOptimisticPersonalDetail;
if (report) {
- result.isChatRoom = ReportUtils.isChatRoom(report);
- result.isDefaultRoom = ReportUtils.isDefaultRoom(report);
+ result.isChatRoom = reportUtilsIsChatRoom(report);
+ result.isDefaultRoom = isDefaultRoom(report);
// eslint-disable-next-line @typescript-eslint/naming-convention
result.private_isArchived = report.private_isArchived;
- result.isExpenseReport = ReportUtils.isExpenseReport(report);
- result.isInvoiceRoom = ReportUtils.isInvoiceRoom(report);
- result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report);
- result.isThread = ReportUtils.isChatThread(report);
- result.isTaskReport = ReportUtils.isTaskReport(report);
- result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report);
- result.isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report);
+ result.isExpenseReport = isExpenseReport(report);
+ result.isInvoiceRoom = isInvoiceRoom(report);
+ result.isMoneyRequestReport = reportUtilsIsMoneyRequestReport(report);
+ result.isThread = isChatThread(report);
+ result.isTaskReport = reportUtilsIsTaskReport(report);
+ result.shouldShowSubscript = shouldReportShowSubscript(report);
+ result.isPolicyExpenseChat = reportUtilsIsPolicyExpenseChat(report);
result.isOwnPolicyExpenseChat = report.isOwnPolicyExpenseChat ?? false;
- result.allReportErrors = ReportUtils.getAllReportErrors(report, reportActions);
+ result.allReportErrors = getAllReportErrors(report, reportActions);
result.brickRoadIndicator = hasReportErrors(report, reportActions) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '';
result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom ?? report.pendingFields.createChat : undefined;
result.ownerAccountID = report.ownerAccountID;
result.reportID = report.reportID;
- result.isUnread = ReportUtils.isUnread(report);
+ result.isUnread = isUnread(report);
result.isPinned = report.isPinned;
result.iouReportID = report.iouReportID;
result.keyForList = String(report.reportID);
result.isWaitingOnBankAccount = report.isWaitingOnBankAccount;
result.policyID = report.policyID;
- result.isSelfDM = ReportUtils.isSelfDM(report);
- result.notificationPreference = ReportUtils.getReportNotificationPreference(report);
+ result.isSelfDM = reportUtilsIsSelfDM(report);
+ result.notificationPreference = getReportNotificationPreference(report);
result.lastVisibleActionCreated = report.lastVisibleActionCreated;
- const visibleParticipantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, true);
+ const visibleParticipantAccountIDs = getParticipantsAccountIDsForDisplay(report, true);
- result.tooltipText = ReportUtils.getReportParticipantsTitle(visibleParticipantAccountIDs);
+ result.tooltipText = getReportParticipantsTitle(visibleParticipantAccountIDs);
- hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat || ReportUtils.isGroupChat(report);
- subtitle = ReportUtils.getChatRoomSubtitle(report);
+ hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat || reportUtilsIsGroupChat(report);
+ subtitle = getChatRoomSubtitle(report);
const lastActorDetails = report.lastActorAccountID ? personalDetailMap[report.lastActorAccountID] : null;
const lastActorDisplayName = getLastActorDisplayName(lastActorDetails, hasMultipleParticipants);
@@ -727,28 +820,26 @@ function createOption(
// If displaying chat preview line is needed, let's overwrite the default alternate text
result.alternateText = showPersonalDetails && personalDetail?.login ? personalDetail.login : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview});
- reportName = showPersonalDetails
- ? ReportUtils.getDisplayNameForParticipant(accountIDs.at(0)) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '')
- : ReportUtils.getReportName(report);
+ reportName = showPersonalDetails ? getDisplayNameForParticipant(accountIDs.at(0)) || formatPhoneNumber(personalDetail?.login ?? '') : getReportName(report);
} else {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- reportName = ReportUtils.getDisplayNameForParticipant(accountIDs.at(0)) || LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '');
+ reportName = getDisplayNameForParticipant(accountIDs.at(0)) || formatPhoneNumber(personalDetail?.login ?? '');
result.keyForList = String(accountIDs.at(0));
- result.alternateText = LocalePhoneNumber.formatPhoneNumber(personalDetails?.[accountIDs[0]]?.login ?? '');
+ result.alternateText = formatPhoneNumber(personalDetails?.[accountIDs[0]]?.login ?? '');
}
- result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result);
- result.iouReportAmount = ReportUtils.getMoneyRequestSpendBreakdown(result).totalDisplaySpend;
+ result.isIOUReportOwner = isIOUOwnedByCurrentUser(result);
+ result.iouReportAmount = getMoneyRequestSpendBreakdown(result).totalDisplaySpend;
- if (!hasMultipleParticipants && (!report || (report && !ReportUtils.isGroupChat(report) && !ReportUtils.isChatRoom(report)))) {
+ if (!hasMultipleParticipants && (!report || (report && !reportUtilsIsGroupChat(report) && !reportUtilsIsChatRoom(report)))) {
result.login = personalDetail?.login;
result.accountID = Number(personalDetail?.accountID);
result.phoneNumber = personalDetail?.phoneNumber;
}
result.text = reportName;
- result.icons = ReportUtils.getIcons(report, personalDetails, personalDetail?.avatar, personalDetail?.login, personalDetail?.accountID, null);
+ result.icons = getIcons(report, personalDetails, personalDetail?.avatar, personalDetail?.login, personalDetail?.accountID, null);
result.subtitle = subtitle;
return result;
@@ -757,9 +848,9 @@ function createOption(
/**
* Get the option for a given report.
*/
-function getReportOption(participant: Participant): ReportUtils.OptionData {
- const report = ReportUtils.getReportOrDraftReport(participant.reportID);
- const visibleParticipantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, true);
+function getReportOption(participant: Participant): OptionData {
+ const report = getReportOrDraftReport(participant.reportID);
+ const visibleParticipantAccountIDs = getParticipantsAccountIDsForDisplay(report, true);
const option = createOption(
visibleParticipantAccountIDs,
@@ -774,15 +865,15 @@ function getReportOption(participant: Participant): ReportUtils.OptionData {
// Update text & alternateText because createOption returns workspace name only if report is owned by the user
if (option.isSelfDM) {
- option.alternateText = Localize.translateLocal('reportActionsView.yourSpace');
+ option.alternateText = translateLocal('reportActionsView.yourSpace');
} else if (option.isInvoiceRoom) {
- option.text = ReportUtils.getReportName(report);
- option.alternateText = Localize.translateLocal('workspace.common.invoices');
+ option.text = getReportName(report);
+ option.alternateText = translateLocal('workspace.common.invoices');
} else {
- option.text = ReportUtils.getPolicyName(report);
- option.alternateText = Localize.translateLocal('workspace.common.workspace');
+ option.text = getPolicyName(report);
+ option.alternateText = translateLocal('workspace.common.workspace');
}
- option.isDisabled = ReportUtils.isDraftReport(participant.reportID);
+ option.isDisabled = isDraftReport(participant.reportID);
option.selected = participant.selected;
option.isSelected = participant.selected;
return option;
@@ -791,11 +882,11 @@ function getReportOption(participant: Participant): ReportUtils.OptionData {
/**
* Get the option for a policy expense report.
*/
-function getPolicyExpenseReportOption(participant: Participant | ReportUtils.OptionData): ReportUtils.OptionData {
- const expenseReport = ReportUtils.isPolicyExpenseChat(participant) ? ReportUtils.getReportOrDraftReport(participant.reportID) : null;
+function getPolicyExpenseReportOption(participant: Participant | OptionData): OptionData {
+ const expenseReport = reportUtilsIsPolicyExpenseChat(participant) ? getReportOrDraftReport(participant.reportID) : null;
const visibleParticipantAccountIDs = Object.entries(expenseReport?.participants ?? {})
- .filter(([, reportParticipant]) => reportParticipant && !ReportUtils.isHiddenForCurrentUser(reportParticipant.notificationPreference))
+ .filter(([, reportParticipant]) => reportParticipant && !isHiddenForCurrentUser(reportParticipant.notificationPreference))
.map(([accountID]) => Number(accountID));
const option = createOption(
@@ -810,8 +901,8 @@ function getPolicyExpenseReportOption(participant: Participant | ReportUtils.Opt
);
// Update text & alternateText because createOption returns workspace name only if report is owned by the user
- option.text = ReportUtils.getPolicyName(expenseReport);
- option.alternateText = Localize.translateLocal('workspace.common.workspace');
+ option.text = getPolicyName(expenseReport);
+ option.alternateText = translateLocal('workspace.common.workspace');
option.selected = participant.selected;
option.isSelected = participant.selected;
return option;
@@ -846,7 +937,7 @@ function isCurrentUser(userDetails: PersonalDetails): boolean {
}
// If user login is a mobile number, append sms domain if not appended already.
- const userDetailsLogin = PhoneNumber.addSMSDomainIfPhoneNumber(userDetails.login ?? '');
+ const userDetailsLogin = addSMSDomainIfPhoneNumber(userDetails.login ?? '');
if (currentUserLogin?.toLowerCase() === userDetailsLogin.toLowerCase()) {
return true;
@@ -864,7 +955,7 @@ function getEnabledCategoriesCount(options: PolicyCategories): number {
}
function getSearchValueForPhoneOrEmail(searchTerm: string) {
- const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchTerm)));
+ const parsedPhoneNumber = parsePhoneNumber(appendCountryCode(Str.removeSMSDomain(searchTerm)));
return parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 ?? '' : searchTerm.toLowerCase();
}
@@ -882,7 +973,7 @@ function hasEnabledOptions(options: PolicyCategories | PolicyTag[]): boolean {
* @param selectedOptions - Array of selected options to compare with.
* @returns true if the report option matches any of the selected options by accountID or reportID, false otherwise.
*/
-function isReportSelected(reportOption: ReportUtils.OptionData, selectedOptions: Array>) {
+function isReportSelected(reportOption: OptionData, selectedOptions: Array>) {
if (!selectedOptions || selectedOptions.length === 0) {
return false;
}
@@ -901,10 +992,10 @@ function createOptionList(personalDetails: OnyxEntry, repor
return;
}
- const isOneOnOneChat = ReportUtils.isOneOnOneChat(report);
- const accountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report);
+ const isOneOnOneChat = reportUtilsIsOneOnOneChat(report);
+ const accountIDs = getParticipantsAccountIDsForDisplay(report);
- const isChatRoom = ReportUtils.isChatRoom(report);
+ const isChatRoom = reportUtilsIsChatRoom(report);
if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) {
return;
}
@@ -941,7 +1032,7 @@ function createOptionList(personalDetails: OnyxEntry, repor
}
function createOptionFromReport(report: Report, personalDetails: OnyxEntry) {
- const accountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report);
+ const accountIDs = getParticipantsAccountIDsForDisplay(report);
return {
item: report,
@@ -949,7 +1040,7 @@ function createOptionFromReport(report: Report, personalDetails: OnyxEntry personalDetail.text?.toLowerCase()], 'asc');
}
@@ -958,7 +1049,7 @@ function orderPersonalDetailsOptions(options: ReportUtils.OptionData[]) {
* Orders report options without grouping them by kind.
* Usually used when there is no search value
*/
-function orderReportOptions(options: ReportUtils.OptionData[]) {
+function orderReportOptions(options: OptionData[]) {
return lodashOrderBy(options, [sortComparatorReportOptionByArchivedStatus, sortComparatorReportOptionByDate], ['asc', 'desc']);
}
@@ -969,7 +1060,7 @@ function orderReportOptions(options: ReportUtils.OptionData[]) {
* @returns a sorted list of options
*/
function orderReportOptionsWithSearch(
- options: ReportUtils.OptionData[],
+ options: OptionData[],
searchValue: string,
{preferChatroomsOverThreads = false, preferPolicyExpenseChat = false, preferRecentExpenseReports = false}: OrderReportOptionsConfig = {},
) {
@@ -1017,11 +1108,27 @@ function orderReportOptionsWithSearch(
);
}
-function sortComparatorReportOptionByArchivedStatus(option: ReportUtils.OptionData) {
+function orderWorkspaceOptions(options: OptionData[]): OptionData[] {
+ return options.sort((a, b) => {
+ // Check if `a` is the default workspace
+ if (a.isPolicyExpenseChat && a.policyID === activePolicyID) {
+ return -1;
+ }
+
+ // Check if `b` is the default workspace
+ if (b.isPolicyExpenseChat && b.policyID === activePolicyID) {
+ return 1;
+ }
+
+ return 0;
+ });
+}
+
+function sortComparatorReportOptionByArchivedStatus(option: OptionData) {
return option.private_isArchived ? 1 : 0;
}
-function sortComparatorReportOptionByDate(options: ReportUtils.OptionData) {
+function sortComparatorReportOptionByDate(options: OptionData) {
// If there is no date (ie. a personal detail option), the option will be sorted to the bottom
// (comparing a dateString > '' returns true, and we are sorting descending, so the dateString will come before '')
return options.lastVisibleActionCreated ?? '';
@@ -1037,17 +1144,19 @@ function orderOptions(options: ReportAndPersonalDetailOptions): ReportAndPersona
*/
function orderOptions(options: ReportAndPersonalDetailOptions, searchValue: string, config?: OrderReportOptionsConfig): ReportAndPersonalDetailOptions;
function orderOptions(options: ReportAndPersonalDetailOptions, searchValue?: string, config?: OrderReportOptionsConfig): ReportAndPersonalDetailOptions {
- let orderedReportOptions: ReportUtils.OptionData[];
+ let orderedReportOptions: OptionData[];
if (searchValue) {
orderedReportOptions = orderReportOptionsWithSearch(options.recentReports, searchValue, config);
} else {
orderedReportOptions = orderReportOptions(options.recentReports);
}
const orderedPersonalDetailsOptions = orderPersonalDetailsOptions(options.personalDetails);
+ const orderedWorkspaceChats = orderWorkspaceOptions(options?.workspaceChats ?? []);
return {
recentReports: orderedReportOptions,
personalDetails: orderedPersonalDetailsOptions,
+ workspaceChats: orderedWorkspaceChats,
};
}
@@ -1057,9 +1166,9 @@ function canCreateOptimisticPersonalDetailOption({
currentUserOption,
searchValue,
}: {
- recentReportOptions: ReportUtils.OptionData[];
- personalDetailsOptions: ReportUtils.OptionData[];
- currentUserOption?: ReportUtils.OptionData | null;
+ recentReportOptions: OptionData[];
+ personalDetailsOptions: OptionData[];
+ currentUserOption?: OptionData | null;
searchValue: string;
}) {
if (recentReportOptions.length + personalDetailsOptions.length > 0) {
@@ -1068,7 +1177,7 @@ function canCreateOptimisticPersonalDetailOption({
if (!currentUserOption) {
return true;
}
- return currentUserOption.login !== PhoneNumber.addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() && currentUserOption.login !== searchValue?.toLowerCase();
+ return currentUserOption.login !== addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase() && currentUserOption.login !== searchValue?.toLowerCase();
}
/**
@@ -1085,25 +1194,25 @@ function getUserToInviteOption({
reportActions = {},
showChatPreviewLine = false,
shouldAcceptName = false,
-}: GetUserToInviteConfig): ReportUtils.OptionData | null {
+}: GetUserToInviteConfig): OptionData | null {
if (!searchValue) {
return null;
}
- const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchValue)));
+ const parsedPhoneNumber = parsePhoneNumber(appendCountryCode(Str.removeSMSDomain(searchValue)));
const isCurrentUserLogin = isCurrentUser({login: searchValue} as PersonalDetails);
const isInSelectedOption = selectedOptions.some((option) => 'login' in option && option.login === searchValue);
const isValidEmail = Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN);
- const isValidPhoneNumber = parsedPhoneNumber.possible && Str.isValidE164Phone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? ''));
+ const isValidPhoneNumber = parsedPhoneNumber.possible && Str.isValidE164Phone(getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? ''));
const isInOptionToExclude =
- optionsToExclude.findIndex((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) !== -1;
+ optionsToExclude.findIndex((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) !== -1;
if (isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber && !shouldAcceptName) || isInOptionToExclude) {
return null;
}
// Generates an optimistic account ID for new users not yet saved in Onyx
- const optimisticAccountID = UserUtils.generateAccountID(searchValue);
+ const optimisticAccountID = generateAccountID(searchValue);
const personalDetailsExtended = {
...allPersonalDetails,
[optimisticAccountID]: {
@@ -1134,42 +1243,40 @@ function getUserToInviteOption({
return userToInvite;
}
-/**
- * Options are reports and personal details. This function filters out the options that are not valid to be displayed.
- */
-function getValidOptions(
- options: OptionList,
+function getValidReports(
+ reports: OptionList['reports'],
{
betas = [],
- selectedOptions = [],
- excludeLogins = [],
includeMultipleParticipantReports = false,
- includeRecentReports = true,
showChatPreviewLine = false,
forcePolicyNamePreview = false,
includeOwnedWorkspaceChats = false,
includeThreads = false,
includeTasks = false,
includeMoneyRequests = false,
- includeP2P = true,
- includeSelectedOptions = false,
transactionViolations = {},
includeSelfDM = false,
includeInvoiceRooms = false,
- includeDomainEmail = false,
action,
- recentAttendees,
+ selectedOptions = [],
+ includeP2P = true,
+ includeDomainEmail = false,
shouldBoldTitleByDefault = true,
- }: GetOptionsConfig = {},
-): Options {
+ optionsToExclude = [],
+ }: GetValidReportsConfig,
+) {
const topmostReportId = Navigation.getTopmostReportId();
- // Filter out all the reports that shouldn't be displayed
- const filteredReportOptions = options.reports.filter((option) => {
+ const validReportOptions: OptionData[] = [];
+ const preferRecentExpenseReports = action === CONST.IOU.ACTION.CREATE;
+
+ for (let i = 0; i < reports.length; i++) {
+ // eslint-disable-next-line rulesdir/prefer-at
+ const option = reports[i];
const report = option.item;
- const doesReportHaveViolations = ReportUtils.shouldDisplayViolationsRBRInLHN(report, transactionViolations);
+ const doesReportHaveViolations = shouldDisplayViolationsRBRInLHN(report, transactionViolations);
- return ReportUtils.shouldReportBeInOptionList({
+ const shouldBeInOptionList = shouldReportBeInOptionList({
report,
currentReportId: topmostReportId,
betas,
@@ -1181,13 +1288,9 @@ function getValidOptions(
login: option.login,
includeDomainEmail,
});
- });
-
- const allReportOptions = filteredReportOptions.filter((option) => {
- const report = option.item;
- if (!report) {
- return false;
+ if (!shouldBeInOptionList) {
+ continue;
}
const isThread = option.isThread;
@@ -1196,163 +1299,198 @@ function getValidOptions(
const isMoneyRequestReport = option.isMoneyRequestReport;
const isSelfDM = option.isSelfDM;
const isChatRoom = option.isChatRoom;
- const accountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report);
+ const accountIDs = getParticipantsAccountIDsForDisplay(report);
if (isPolicyExpenseChat && report.isOwnPolicyExpenseChat && !includeOwnedWorkspaceChats) {
- return false;
+ continue;
}
// When passing includeP2P false we are trying to hide features from users that are not ready for P2P and limited to workspace chats only.
if (!includeP2P && !isPolicyExpenseChat) {
- return false;
+ continue;
}
if (isSelfDM && !includeSelfDM) {
- return false;
+ continue;
}
if (isThread && !includeThreads) {
- return false;
+ continue;
}
if (isTaskReport && !includeTasks) {
- return false;
+ continue;
}
if (isMoneyRequestReport && !includeMoneyRequests) {
- return false;
+ continue;
}
// In case user needs to add credit bank account, don't allow them to submit an expense from the workspace.
- if (includeOwnedWorkspaceChats && ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(report)) {
- return false;
+ if (includeOwnedWorkspaceChats && hasIOUWaitingOnCurrentUserBankAccount(report)) {
+ continue;
}
if ((!accountIDs || accountIDs.length === 0) && !isChatRoom) {
- return false;
+ continue;
}
- return true;
- });
+ if (option.login === CONST.EMAIL.NOTIFICATIONS) {
+ continue;
+ }
- const allPersonalDetailsOptions = includeP2P
- ? options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail && (includeDomainEmail || !Str.isDomainEmail(detail.login)))
- : [];
+ const isCurrentUserOwnedPolicyExpenseChatThatCouldShow =
+ option.isPolicyExpenseChat && option.ownerAccountID === currentUserAccountID && includeOwnedWorkspaceChats && !option.private_isArchived;
- const optionsToExclude: Option[] = [{login: CONST.EMAIL.NOTIFICATIONS}];
+ const shouldShowInvoiceRoom =
+ includeInvoiceRooms && isInvoiceRoom(option.item) && isPolicyAdmin(option.policyID, policies) && !option.private_isArchived && canSendInvoiceFromWorkspace(option.policyID);
+
+ /*
+ Exclude the report option if it doesn't meet any of the following conditions:
+ - It is not an owned policy expense chat that could be shown
+ - Multiple participant reports are not included
+ - It doesn't have a login
+ - It is not an invoice room that should be shown
+ */
+ if (!isCurrentUserOwnedPolicyExpenseChatThatCouldShow && !includeMultipleParticipantReports && !option.login && !shouldShowInvoiceRoom) {
+ continue;
+ }
+
+ // If we're excluding threads, check the report to see if it has a single participant and if the participant is already selected
+ if (!includeThreads && (!!option.login || option.reportID) && optionsToExclude.some((x) => x.login === option.login || x.reportID === option.reportID)) {
+ continue;
+ }
+
+ if (action === CONST.IOU.ACTION.CATEGORIZE) {
+ const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${option.policyID}`];
+ if (!reportPolicy?.areCategoriesEnabled) {
+ continue;
+ }
+ }
+ /**
+ * By default, generated options does not have the chat preview line enabled.
+ * If showChatPreviewLine or forcePolicyNamePreview are true, let's generate and overwrite the alternate text.
+ */
+ const alternateText = getAlternateText(option, {showChatPreviewLine, forcePolicyNamePreview});
+ const isSelected = isReportSelected(option, selectedOptions);
+ const isBold = shouldBoldTitleByDefault || shouldUseBoldText(option);
+ let lastIOUCreationDate;
+
+ // Add a field to sort the recent reports by the time of last IOU request for create actions
+ if (preferRecentExpenseReports) {
+ const reportPreviewAction = allSortedReportActions[option.reportID]?.find((reportAction) => isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW));
+
+ if (reportPreviewAction) {
+ const iouReportID = getIOUReportIDFromReportActionPreview(reportPreviewAction);
+ const iouReportActions = iouReportID ? allSortedReportActions[iouReportID] ?? [] : [];
+ const lastIOUAction = iouReportActions.find((iouAction) => iouAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
+ if (lastIOUAction) {
+ lastIOUCreationDate = lastIOUAction.lastModified;
+ }
+ }
+ }
+
+ const newReportOption = {
+ ...option,
+ alternateText,
+ isSelected,
+ isBold,
+ lastIOUCreationDate,
+ };
+
+ validReportOptions.push(newReportOption);
+ }
+
+ return validReportOptions;
+}
+
+/**
+ * Options are reports and personal details. This function filters out the options that are not valid to be displayed.
+ */
+function getValidOptions(
+ options: OptionList,
+ {
+ excludeLogins = [],
+ includeSelectedOptions = false,
+ includeRecentReports = true,
+ recentAttendees,
+ selectedOptions = [],
+ shouldSeparateSelfDMChat = false,
+ shouldSeparateWorkspaceChat = false,
+ ...config
+ }: GetOptionsConfig = {},
+): Options {
+ // Gather shared configs:
+ const optionsToExclude: Option[] = [{login: CONST.EMAIL.NOTIFICATIONS}];
// If we're including selected options from the search results, we only want to exclude them if the search input is empty
// This is because on certain pages, we show the selected options at the top when the search input is empty
// This prevents the issue of seeing the selected option twice if you have them as a recent chat and select them
if (!includeSelectedOptions) {
optionsToExclude.push(...selectedOptions);
}
-
excludeLogins.forEach((login) => {
optionsToExclude.push({login});
});
+ const {includeP2P = true, shouldBoldTitleByDefault = true, includeDomainEmail = false, ...getValidReportsConfig} = config;
- let recentReportOptions: ReportUtils.OptionData[] = [];
- const personalDetailsOptions: ReportUtils.OptionData[] = [];
-
- const preferRecentExpenseReports = action === CONST.IOU.ACTION.CREATE;
-
+ // Get valid recent reports:
+ let recentReportOptions: OptionData[] = [];
if (includeRecentReports) {
- for (const reportOption of allReportOptions) {
- // Skip notifications@expensify.com
- if (reportOption.login === CONST.EMAIL.NOTIFICATIONS) {
+ recentReportOptions = getValidReports(options.reports, {
+ ...getValidReportsConfig,
+ includeP2P,
+ includeDomainEmail,
+ selectedOptions,
+ optionsToExclude,
+ shouldBoldTitleByDefault,
+ });
+ } else if (recentAttendees && recentAttendees?.length > 0) {
+ recentAttendees.filter((attendee) => attendee.login ?? attendee.displayName).forEach((a) => optionsToExclude.push({login: a.login ?? a.displayName}));
+ recentReportOptions = recentAttendees as OptionData[];
+ }
+
+ // Get valid personal details and check if we can find the current user:
+ const personalDetailsOptions: OptionData[] = [];
+ let currentUserOption: OptionData | undefined;
+ if (includeP2P) {
+ const personalDetailsOptionsToExclude = [...optionsToExclude, {login: currentUserLogin}];
+ for (let i = 0; i < options.personalDetails.length; i++) {
+ // eslint-disable-next-line rulesdir/prefer-at
+ const detail = options.personalDetails[i];
+ if (!detail?.login || !detail.accountID || !!detail?.isOptimisticPersonalDetail || (!includeDomainEmail && Str.isDomainEmail(detail.login))) {
continue;
}
- const isCurrentUserOwnedPolicyExpenseChatThatCouldShow =
- reportOption.isPolicyExpenseChat && reportOption.ownerAccountID === currentUserAccountID && includeOwnedWorkspaceChats && !reportOption.private_isArchived;
-
- const shouldShowInvoiceRoom =
- includeInvoiceRooms &&
- ReportUtils.isInvoiceRoom(reportOption.item) &&
- ReportUtils.isPolicyAdmin(reportOption.policyID, policies) &&
- !reportOption.private_isArchived &&
- PolicyUtils.canSendInvoiceFromWorkspace(reportOption.policyID);
-
- /**
- Exclude the report option if it doesn't meet any of the following conditions:
- - It is not an owned policy expense chat that could be shown
- - Multiple participant reports are not included
- - It doesn't have a login
- - It is not an invoice room that should be shown
- */
- if (!isCurrentUserOwnedPolicyExpenseChatThatCouldShow && !includeMultipleParticipantReports && !reportOption.login && !shouldShowInvoiceRoom) {
- continue;
+ if (!!currentUserLogin && detail.login === currentUserLogin) {
+ currentUserOption = detail;
}
- // If we're excluding threads, check the report to see if it has a single participant and if the participant is already selected
- if (
- !includeThreads &&
- (!!reportOption.login || reportOption.reportID) &&
- optionsToExclude.some((option) => option.login === reportOption.login || option.reportID === reportOption.reportID)
- ) {
+ if (personalDetailsOptionsToExclude.some((optionToExclude) => optionToExclude.login === detail.login)) {
continue;
}
- /**
- * By default, generated options does not have the chat preview line enabled.
- * If showChatPreviewLine or forcePolicyNamePreview are true, let's generate and overwrite the alternate text.
- */
- const alternateText = getAlternateText(reportOption, {showChatPreviewLine, forcePolicyNamePreview});
- const isSelected = isReportSelected(reportOption, selectedOptions);
- const isBold = shouldBoldTitleByDefault || shouldUseBoldText(reportOption);
- let lastIOUCreationDate;
-
- // Add a field to sort the recent reports by the time of last IOU request for create actions
- if (preferRecentExpenseReports) {
- const reportPreviewAction = allSortedReportActions[reportOption.reportID]?.find((reportAction) =>
- ReportActionUtils.isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW),
- );
-
- if (reportPreviewAction) {
- const iouReportID = ReportActionUtils.getIOUReportIDFromReportActionPreview(reportPreviewAction);
- const iouReportActions = iouReportID ? allSortedReportActions[iouReportID] ?? [] : [];
- const lastIOUAction = iouReportActions.find((iouAction) => iouAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
- if (lastIOUAction) {
- lastIOUCreationDate = lastIOUAction.lastModified;
- }
- }
- }
+ detail.isBold = shouldBoldTitleByDefault;
- const newReportOption = {
- ...reportOption,
- alternateText,
- isSelected,
- isBold,
- lastIOUCreationDate,
- };
-
- if (action === CONST.IOU.ACTION.CATEGORIZE) {
- const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${newReportOption.policyID}`];
- if (reportPolicy?.areCategoriesEnabled) {
- recentReportOptions.push(newReportOption);
- }
- } else {
- recentReportOptions.push(newReportOption);
- }
+ personalDetailsOptions.push(detail);
}
- } else if (recentAttendees && recentAttendees?.length > 0) {
- recentAttendees.filter((attendee) => attendee.login ?? attendee.displayName).forEach((a) => optionsToExclude.push({login: a.login ?? a.displayName}));
- recentReportOptions = recentAttendees as ReportUtils.OptionData[];
}
- const personalDetailsOptionsToExclude = [...optionsToExclude, {login: currentUserLogin}];
- // Next loop over all personal details removing any that are selectedUsers or recentChats
- for (const personalDetailOption of allPersonalDetailsOptions) {
- if (personalDetailsOptionsToExclude.some((optionToExclude) => optionToExclude.login === personalDetailOption.login)) {
- continue;
- }
- personalDetailOption.isBold = shouldBoldTitleByDefault;
+ let workspaceChats: OptionData[] = [];
- personalDetailsOptions.push(personalDetailOption);
+ if (shouldSeparateWorkspaceChat) {
+ workspaceChats = recentReportOptions.filter((option) => option.isOwnPolicyExpenseChat && !option.private_isArchived);
}
- const currentUserOption = allPersonalDetailsOptions.find((personalDetailsOption) => personalDetailsOption.login === currentUserLogin);
+ let selfDMChat: OptionData | undefined;
+
+ if (shouldSeparateWorkspaceChat) {
+ recentReportOptions = recentReportOptions.filter((option) => !option.isPolicyExpenseChat);
+ }
+ if (shouldSeparateSelfDMChat) {
+ selfDMChat = recentReportOptions.find((option) => option.isSelfDM);
+ recentReportOptions = recentReportOptions.filter((option) => !option.isSelfDM);
+ }
return {
personalDetails: personalDetailsOptions,
@@ -1361,6 +1499,8 @@ function getValidOptions(
// User to invite is generated by the search input of a user.
// As this function isn't concerned with any search input yet, this is null (will be set when using filterOptions).
userToInvite: null,
+ workspaceChats,
+ selfDMChat,
};
}
@@ -1411,8 +1551,8 @@ function getShareLogOptions(options: OptionList, betas: Beta[] = []): Options {
function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: OnyxEntry, amountText?: string): PayeePersonalDetails {
const login = personalDetail?.login ?? '';
return {
- text: LocalePhoneNumber.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, login)),
- alternateText: LocalePhoneNumber.formatPhoneNumber(login || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false)),
+ text: formatPhoneNumber(getDisplayNameOrDefault(personalDetail, login)),
+ alternateText: formatPhoneNumber(login || getDisplayNameOrDefault(personalDetail, '', false)),
icons: [
{
source: personalDetail?.avatar ?? FallbackAvatar,
@@ -1465,7 +1605,7 @@ function getShareDestinationOptions(
reports: Array> = [],
personalDetails: Array> = [],
betas: OnyxEntry = [],
- selectedOptions: Array> = [],
+ selectedOptions: Array> = [],
excludeLogins: string[] = [],
includeOwnedWorkspaceChats = true,
) {
@@ -1493,7 +1633,7 @@ function getShareDestinationOptions(
* @param member - personalDetails or userToInvite
* @param config - keys to overwrite the default values
*/
-function formatMemberForList(member: ReportUtils.OptionData): MemberForList {
+function formatMemberForList(member: OptionData): MemberForList {
const accountID = member.accountID;
return {
@@ -1547,27 +1687,27 @@ function getMemberInviteOptions(
* Helper method that returns the text to be used for the header's message and title (if any)
*/
function getHeaderMessage(hasSelectableOptions: boolean, hasUserToInvite: boolean, searchValue: string, hasMatchedParticipant = false): string {
- const isValidPhone = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(searchValue)).possible;
+ const isValidPhone = parsePhoneNumber(appendCountryCode(searchValue)).possible;
const isValidEmail = Str.isValidEmail(searchValue);
if (searchValue && CONST.REGEX.DIGITS_AND_PLUS.test(searchValue) && !isValidPhone && !hasSelectableOptions) {
- return Localize.translate(preferredLocale, 'messages.errorMessageInvalidPhone');
+ return translate(preferredLocale, 'messages.errorMessageInvalidPhone');
}
// Without a search value, it would be very confusing to see a search validation message.
// Therefore, this skips the validation when there is no search value.
if (searchValue && !hasSelectableOptions && !hasUserToInvite) {
if (/^\d+$/.test(searchValue) && !isValidPhone) {
- return Localize.translate(preferredLocale, 'messages.errorMessageInvalidPhone');
+ return translate(preferredLocale, 'messages.errorMessageInvalidPhone');
}
if (/@/.test(searchValue) && !isValidEmail) {
- return Localize.translate(preferredLocale, 'messages.errorMessageInvalidEmail');
+ return translate(preferredLocale, 'messages.errorMessageInvalidEmail');
}
if (hasMatchedParticipant && (isValidEmail || isValidPhone)) {
return '';
}
- return Localize.translate(preferredLocale, 'common.noResultsFound');
+ return translate(preferredLocale, 'common.noResultsFound');
}
return '';
@@ -1578,7 +1718,7 @@ function getHeaderMessage(hasSelectableOptions: boolean, hasUserToInvite: boolea
*/
function getHeaderMessageForNonUserList(hasSelectableOptions: boolean, searchValue: string): string {
if (searchValue && !hasSelectableOptions) {
- return Localize.translate(preferredLocale, 'common.noResultsFound');
+ return translate(preferredLocale, 'common.noResultsFound');
}
return '';
}
@@ -1586,7 +1726,7 @@ function getHeaderMessageForNonUserList(hasSelectableOptions: boolean, searchVal
/**
* Helper method to check whether an option can show tooltip or not
*/
-function shouldOptionShowTooltip(option: ReportUtils.OptionData): boolean {
+function shouldOptionShowTooltip(option: OptionData): boolean {
return !option.private_isArchived;
}
@@ -1595,11 +1735,12 @@ function shouldOptionShowTooltip(option: ReportUtils.OptionData): boolean {
*/
function formatSectionsFromSearchTerm(
searchTerm: string,
- selectedOptions: ReportUtils.OptionData[],
- filteredRecentReports: ReportUtils.OptionData[],
- filteredPersonalDetails: ReportUtils.OptionData[],
+ selectedOptions: OptionData[],
+ filteredRecentReports: OptionData[],
+ filteredPersonalDetails: OptionData[],
personalDetails: OnyxEntry = {},
shouldGetOptionDetails = false,
+ filteredWorkspaceChats: OptionData[] = [],
): SectionForSearchTerm {
// We show the selected participants at the top of the list when there is no search term or maximum number of participants has already been selected
// However, if there is a search term we remove the selected participants from the top of the list unless they are part of the search results
@@ -1625,8 +1766,9 @@ function formatSectionsFromSearchTerm(
const selectedParticipantsWithoutDetails = selectedOptions.filter((participant) => {
const accountID = participant.accountID ?? null;
const isPartOfSearchTerm = getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm);
- const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID);
+ const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID) || filteredWorkspaceChats.some((report) => report.accountID === accountID);
const isReportInPersonalDetails = filteredPersonalDetails.some((personalDetail) => personalDetail.accountID === accountID);
+
return isPartOfSearchTerm && !isReportInRecentReports && !isReportInPersonalDetails;
});
@@ -1657,18 +1799,18 @@ function getFirstKeyForList(data?: Option[] | null) {
return firstNonEmptyDataObj?.keyForList ? firstNonEmptyDataObj?.keyForList : '';
}
-function getPersonalDetailSearchTerms(item: Partial) {
+function getPersonalDetailSearchTerms(item: Partial) {
return [item.participantsList?.[0]?.displayName ?? '', item.login ?? '', item.login?.replace(CONST.EMAIL_SEARCH_REGEX, '') ?? ''];
}
-function getCurrentUserSearchTerms(item: ReportUtils.OptionData) {
+function getCurrentUserSearchTerms(item: OptionData) {
return [item.text ?? '', item.login ?? '', item.login?.replace(CONST.EMAIL_SEARCH_REGEX, '') ?? ''];
}
/**
* Remove the personal details for the DMs that are already in the recent reports so that we don't show duplicates.
*/
-function filteredPersonalDetailsOfRecentReports(recentReports: ReportUtils.OptionData[], personalDetails: ReportUtils.OptionData[]) {
+function filteredPersonalDetailsOfRecentReports(recentReports: OptionData[], personalDetails: OptionData[]) {
const excludedLogins = new Set(recentReports.map((report) => report.login));
return personalDetails.filter((personalDetail) => !excludedLogins.has(personalDetail.login));
}
@@ -1676,7 +1818,7 @@ function filteredPersonalDetailsOfRecentReports(recentReports: ReportUtils.Optio
/**
* Filters options based on the search input value
*/
-function filterReports(reports: ReportUtils.OptionData[], searchTerms: string[]): ReportUtils.OptionData[] {
+function filterReports(reports: OptionData[], searchTerms: string[]): OptionData[] {
// We search eventually for multiple whitespace separated search terms.
// We start with the search term at the end, and then narrow down those filtered search results with the next search term.
// We repeat (reduce) this until all search terms have been used:
@@ -1712,7 +1854,24 @@ function filterReports(reports: ReportUtils.OptionData[], searchTerms: string[])
return filteredReports;
}
-function filterPersonalDetails(personalDetails: ReportUtils.OptionData[], searchTerms: string[]): ReportUtils.OptionData[] {
+function filterWorkspaceChats(reports: OptionData[], searchTerms: string[]): OptionData[] {
+ const filteredReports = searchTerms.reduceRight(
+ (items, term) =>
+ filterArrayByMatch(items, term, (item) => {
+ const values: string[] = [];
+ if (item.text) {
+ values.push(item.text);
+ }
+ return uniqFast(values);
+ }),
+ // We start from all unfiltered reports:
+ reports,
+ );
+
+ return filteredReports;
+}
+
+function filterPersonalDetails(personalDetails: OptionData[], searchTerms: string[]): OptionData[] {
return searchTerms.reduceRight(
(items, term) =>
filterArrayByMatch(items, term, (item) => {
@@ -1723,7 +1882,7 @@ function filterPersonalDetails(personalDetails: ReportUtils.OptionData[], search
);
}
-function filterCurrentUserOption(currentUserOption: ReportUtils.OptionData | null | undefined, searchTerms: string[]): ReportUtils.OptionData | null | undefined {
+function filterCurrentUserOption(currentUserOption: OptionData | null | undefined, searchTerms: string[]): OptionData | null | undefined {
return searchTerms.reduceRight((item, term) => {
if (!item) {
return null;
@@ -1734,7 +1893,7 @@ function filterCurrentUserOption(currentUserOption: ReportUtils.OptionData | nul
}, currentUserOption);
}
-function filterUserToInvite(options: Omit, searchValue: string, config?: FilterUserToInviteConfig): ReportUtils.OptionData | null {
+function filterUserToInvite(options: Omit, searchValue: string, config?: FilterUserToInviteConfig): OptionData | null {
const {canInviteUser = true, excludeLogins = []} = config ?? {};
if (!canInviteUser) {
return null;
@@ -1763,8 +1922,36 @@ function filterUserToInvite(options: Omit, searchValue:
});
}
+function filterSelfDMChat(report: OptionData, searchTerms: string[]): OptionData | undefined {
+ const isMatch = searchTerms.every((term) => {
+ const values: string[] = [];
+
+ if (report.text) {
+ values.push(report.text);
+ }
+ if (report.login) {
+ values.push(report.login);
+ values.push(report.login.replace(CONST.EMAIL_SEARCH_REGEX, ''));
+ }
+ if (report.isThread) {
+ if (report.alternateText) {
+ values.push(report.alternateText);
+ }
+ } else if (!!report.isChatRoom || !!report.isPolicyExpenseChat) {
+ if (report.subtitle) {
+ values.push(report.subtitle);
+ }
+ }
+
+ // Remove duplicate values and check if the term matches any value
+ return uniqFast(values).some((value) => value.includes(term));
+ });
+
+ return isMatch ? report : undefined;
+}
+
function filterOptions(options: Options, searchInputValue: string, config?: FilterUserToInviteConfig): Options {
- const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue)));
+ const parsedPhoneNumber = parsePhoneNumber(appendCountryCode(Str.removeSMSDomain(searchInputValue)));
const searchValue = parsedPhoneNumber.possible && parsedPhoneNumber.number?.e164 ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase();
const searchTerms = searchValue ? searchValue.split(' ') : [];
@@ -1780,12 +1967,17 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
searchValue,
config,
);
+ const workspaceChats = filterWorkspaceChats(options.workspaceChats ?? [], searchTerms);
+
+ const selfDMChat = options.selfDMChat ? filterSelfDMChat(options.selfDMChat, searchTerms) : undefined;
return {
personalDetails,
recentReports,
userToInvite,
currentUserOption,
+ workspaceChats,
+ selfDMChat,
};
}
@@ -1857,9 +2049,9 @@ function getEmptyOptions(): Options {
};
}
-function shouldUseBoldText(report: ReportUtils.OptionData): boolean {
- const notificationPreference = report.notificationPreference ?? ReportUtils.getReportNotificationPreference(report);
- return report.isUnread === true && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE && !ReportUtils.isHiddenForCurrentUser(notificationPreference);
+function shouldUseBoldText(report: OptionData): boolean {
+ const notificationPreference = report.notificationPreference ?? getReportNotificationPreference(report);
+ return report.isUnread === true && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE && !isHiddenForCurrentUser(notificationPreference);
}
export {
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index a98d5e7d4009..423eafb3f122 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -41,6 +41,10 @@ function canUseManagerMcTest(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.NEWDOT_MANAGER_MCTEST) || canUseAllBetas(betas);
}
+function canUseNSQS(betas: OnyxEntry): boolean {
+ return !!betas?.includes(CONST.BETAS.NSQS) || canUseAllBetas(betas);
+}
+
export default {
canUseDefaultRooms,
canUseLinkPreviews,
@@ -50,4 +54,5 @@ export default {
canUsePerDiem,
canUseMergeAccounts,
canUseManagerMcTest,
+ canUseNSQS,
};
diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts
index 1dd1628e53d2..1b7eb8664939 100644
--- a/src/libs/Pusher/pusher.ts
+++ b/src/libs/Pusher/pusher.ts
@@ -40,7 +40,7 @@ type EventData = {chunk?: string; id?: string; index?:
? PusherEventMap[EventName]
: OnyxUpdatesFromServer);
-type EventCallbackError = {type: ValueOf; data: {code: number}};
+type EventCallbackError = {type: ValueOf; data: {code: number; message?: string}};
type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean};
diff --git a/src/libs/PusherConnectionManager.ts b/src/libs/PusherConnectionManager.ts
index 597670d3f5ad..69ffa8339f5c 100644
--- a/src/libs/PusherConnectionManager.ts
+++ b/src/libs/PusherConnectionManager.ts
@@ -1,9 +1,9 @@
import type {ChannelAuthorizationCallback} from 'pusher-js/with-encryption';
import CONST from '@src/CONST';
-import * as Session from './actions/Session';
+import {authenticatePusher} from './actions/Session';
import Log from './Log';
import type {SocketEventName} from './Pusher/library/types';
-import * as Pusher from './Pusher/pusher';
+import {reconnect, registerCustomAuthorizer, registerSocketEventCallback} from './Pusher/pusher';
import type {EventCallbackError, States} from './Pusher/pusher';
function init() {
@@ -13,22 +13,30 @@ function init() {
* current valid token to generate the signed auth response
* needed to subscribe to Pusher channels.
*/
- Pusher.registerCustomAuthorizer((channel) => ({
+ registerCustomAuthorizer((channel) => ({
authorize: (socketId: string, callback: ChannelAuthorizationCallback) => {
- Session.authenticatePusher(socketId, channel.name, callback);
+ authenticatePusher(socketId, channel.name, callback);
},
}));
- Pusher.registerSocketEventCallback((eventName: SocketEventName, error?: EventCallbackError | States) => {
+ registerSocketEventCallback((eventName: SocketEventName, error?: EventCallbackError | States) => {
switch (eventName) {
case 'error': {
if (error && 'type' in error) {
const errorType = error?.type;
const code = error?.data?.code;
+ const errorMessage = error?.data?.message ?? '';
if (errorType === CONST.ERROR.PUSHER_ERROR && code === 1006) {
// 1006 code happens when a websocket connection is closed. There may or may not be a reason attached indicating why the connection was closed.
// https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
Log.hmmm('[PusherConnectionManager] Channels Error 1006', {error});
+
+ // The 1006 errors don't always have a message, but when they do, it seems that it prevents the pusher client from reconnecting.
+ // On the advice from Pusher directly, they suggested to manually reconnect in those scenarios.
+ if (errorMessage) {
+ Log.hmmm('[PusherConnectionManager] Channels Error 1006 message', {errorMessage});
+ reconnect();
+ }
} else if (errorType === CONST.ERROR.PUSHER_ERROR && code === 4201) {
// This means the connection was closed because Pusher did not receive a reply from the client when it pinged them for a response
// https://pusher.com/docs/channels/library_auth_reference/pusher-websockets-protocol/#4200-4299
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index ffd78ff4355e..fa5f0519ca6c 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -10,7 +10,7 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {SvgProps} from 'react-native-svg';
import type {OriginalMessageIOU, OriginalMessageModifiedExpense} from 'src/types/onyx/OriginalMessage';
-import type {TupleToUnion, ValueOf} from 'type-fest';
+import type {SetRequired, TupleToUnion, ValueOf} from 'type-fest';
import type {FileObject} from '@components/AttachmentModal';
import {FallbackAvatar, IntacctSquare, NetSuiteSquare, QBOSquare, XeroSquare} from '@components/Icon/Expensicons';
import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars';
@@ -513,22 +513,25 @@ type OptimisticModifiedExpenseReportAction = Pick<
| 'delegateAccountID'
> & {reportID?: string};
-type OptimisticTaskReport = Pick<
- Report,
- | 'reportID'
- | 'reportName'
- | 'description'
- | 'ownerAccountID'
- | 'participants'
- | 'managerID'
- | 'type'
- | 'parentReportID'
- | 'policyID'
- | 'stateNum'
- | 'statusNum'
- | 'parentReportActionID'
- | 'lastVisibleActionCreated'
- | 'hasParentAccess'
+type OptimisticTaskReport = SetRequired<
+ Pick<
+ Report,
+ | 'reportID'
+ | 'reportName'
+ | 'description'
+ | 'ownerAccountID'
+ | 'participants'
+ | 'managerID'
+ | 'type'
+ | 'parentReportID'
+ | 'policyID'
+ | 'stateNum'
+ | 'statusNum'
+ | 'parentReportActionID'
+ | 'lastVisibleActionCreated'
+ | 'hasParentAccess'
+ >,
+ 'parentReportID'
>;
type TransactionDetails = {
@@ -6356,8 +6359,8 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string, exp
function buildOptimisticTaskReport(
ownerAccountID: number,
+ parentReportID: string,
assigneeAccountID = 0,
- parentReportID?: string,
title?: string,
description?: string,
policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE,
@@ -7603,7 +7606,10 @@ function getWorkspaceChats(policyID: string, accountIDs: number[], reports: Onyx
*
* @param policyID - the workspace ID to get all associated reports
*/
-function getAllWorkspaceReports(policyID: string): Array> {
+function getAllWorkspaceReports(policyID?: string): Array> {
+ if (!policyID) {
+ return [];
+ }
return Object.values(allReports ?? {}).filter((report) => report?.policyID === policyID);
}
diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy
index 732bb430d526..f0f67d4bb729 100644
--- a/src/libs/SearchParser/searchParser.peggy
+++ b/src/libs/SearchParser/searchParser.peggy
@@ -66,7 +66,7 @@ filterList
const keywordFilter = buildFilter(
"eq",
"keyword",
- keywords.map((filter) => filter.right).flat()
+ keywords.map((filter) => filter.right.replace(/^(['"])(.*)\1$/, '$2')).flat()
);
if (keywordFilter.right.length > 0) {
nonKeywords.push(keywordFilter);
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 49d791dc604d..749236638045 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -951,6 +951,10 @@ function isOnHoldByTransactionID(transactionID: string | undefined | null): bool
* Checks if any violations for the provided transaction are of type 'violation'
*/
function hasViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean {
+ const transaction = getTransaction(transactionID);
+ if (isExpensifyCardTransaction(transaction) && isPending(transaction)) {
+ return false;
+ }
return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some(
(violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION && (showInReview === undefined || showInReview === (violation.showInReview ?? false)),
);
@@ -960,6 +964,10 @@ function hasViolation(transactionID: string | undefined, transactionViolations:
* Checks if any violations for the provided transaction are of type 'notice'
*/
function hasNoticeTypeViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean {
+ const transaction = getTransaction(transactionID);
+ if (isExpensifyCardTransaction(transaction) && isPending(transaction)) {
+ return false;
+ }
return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some(
(violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.NOTICE && (showInReview === undefined || showInReview === (violation.showInReview ?? false)),
);
@@ -969,6 +977,10 @@ function hasNoticeTypeViolation(transactionID: string | undefined, transactionVi
* Checks if any violations for the provided transaction are of type 'warning'
*/
function hasWarningTypeViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean {
+ const transaction = getTransaction(transactionID);
+ if (isExpensifyCardTransaction(transaction) && isPending(transaction)) {
+ return false;
+ }
const violations = transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID];
const warningTypeViolations =
violations?.filter(
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 51093e759e1a..ddf9701a6c8e 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -384,10 +384,22 @@ function endSignOnTransition() {
* @param [transitionFromOldDot] Optional, if the user is transitioning from old dot
* @param [makeMeAdmin] Optional, leave the calling account as an admin on the policy
* @param [backTo] An optional return path. If provided, it will be URL-encoded and appended to the resulting URL.
+ * @param [policyID] Optional, Policy id.
+ * @param [currency] Optional, selected currency for the workspace
+ * @param [file], avatar file for workspace
*/
-function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', policyName = '', transitionFromOldDot = false, makeMeAdmin = false, backTo = '') {
- const policyID = generatePolicyID();
- createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID, makeMeAdmin);
+function createWorkspaceWithPolicyDraftAndNavigateToIt(
+ policyOwnerEmail = '',
+ policyName = '',
+ transitionFromOldDot = false,
+ makeMeAdmin = false,
+ backTo = '',
+ policyID = '',
+ currency?: string,
+ file?: File,
+) {
+ const policyIDWithDefault = policyID || generatePolicyID();
+ createDraftInitialWorkspace(policyOwnerEmail, policyName, policyIDWithDefault, makeMeAdmin, currency, file);
Navigation.isNavigationReady()
.then(() => {
@@ -395,8 +407,8 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', po
// We must call goBack() to remove the /transition route from history
Navigation.goBack();
}
- savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail, makeMeAdmin);
- Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID, backTo));
+ savePolicyDraftByNewWorkspace(policyIDWithDefault, policyName, policyOwnerEmail, makeMeAdmin, currency, file);
+ Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyIDWithDefault, backTo));
})
.then(endSignOnTransition);
}
@@ -408,9 +420,11 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', po
* @param [policyName] custom policy name we will use for created workspace
* @param [policyOwnerEmail] Optional, the email of the account to make the owner of the policy
* @param [makeMeAdmin] Optional, leave the calling account as an admin on the policy
+ * @param [currency] Optional, selected currency for the workspace
+ * @param [file] Optional, avatar file for workspace
*/
-function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false) {
- createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID);
+function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, currency = '', file?: File) {
+ createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, '', currency, file);
}
/**
diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts
index f99400d87d3e..320f91f8a677 100644
--- a/src/libs/actions/Delegate.ts
+++ b/src/libs/actions/Delegate.ts
@@ -1,13 +1,12 @@
import {NativeModules} from 'react-native';
import Onyx from 'react-native-onyx';
-import type {OnyxUpdate} from 'react-native-onyx';
+import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import * as API from '@libs/API';
import type {AddDelegateParams, RemoveDelegateParams, UpdateDelegateRoleParams} from '@libs/API/parameters';
import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import Log from '@libs/Log';
import * as NetworkStore from '@libs/Network/NetworkStore';
-import {getCurrentUserEmail} from '@libs/Network/NetworkStore';
import * as SequentialQueue from '@libs/Network/SequentialQueue';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -16,6 +15,7 @@ import type Credentials from '@src/types/onyx/Credentials';
import type Response from '@src/types/onyx/Response';
import type Session from '@src/types/onyx/Session';
import {confirmReadyToOpenApp, openApp} from './App';
+import {getCurrentUserAccountID} from './Report';
import updateSessionAuthTokens from './Session/updateSessionAuthTokens';
import updateSessionUser from './Session/updateSessionUser';
@@ -51,6 +51,14 @@ Onyx.connect({
callback: (value) => (stashedSession = value ?? {}),
});
+let activePolicyID: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
+ callback: (newActivePolicyID) => {
+ activePolicyID = newActivePolicyID;
+ },
+});
+
const KEYS_TO_PRESERVE_DELEGATE_ACCESS = [
ONYXKEYS.NVP_TRY_FOCUS_MODE,
ONYXKEYS.PREFERRED_THEME,
@@ -73,6 +81,8 @@ function connect(email: string) {
Onyx.set(ONYXKEYS.STASHED_CREDENTIALS, credentials);
Onyx.set(ONYXKEYS.STASHED_SESSION, session);
+ const previousAccountID = getCurrentUserAccountID();
+
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -130,6 +140,14 @@ function connect(email: string) {
Onyx.update(failureData);
return;
}
+ if (!activePolicyID) {
+ Log.alert('[Delegate] Unable to access activePolicyID');
+ Onyx.update(failureData);
+ return;
+ }
+ const restrictedToken = response.restrictedToken;
+ const policyID = activePolicyID;
+
return SequentialQueue.waitForIdle()
.then(() => Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS))
.then(() => {
@@ -138,9 +156,7 @@ function connect(email: string) {
NetworkStore.setAuthToken(response?.restrictedToken ?? null);
confirmReadyToOpenApp();
- openApp();
-
- NativeModules.HybridAppModule.switchAccount(email);
+ openApp().then(() => NativeModules.HybridAppModule.switchAccount(email, restrictedToken, policyID, String(previousAccountID)));
});
})
.catch((error) => {
@@ -195,22 +211,34 @@ function disconnect() {
return;
}
+ if (!response?.requesterID || !response?.requesterEmail) {
+ Log.alert('[Delegate] No requester data returned while disconnecting as a delegate');
+ return;
+ }
+
+ const requesterEmail = response.requesterEmail;
+ const authToken = response.authToken;
return SequentialQueue.waitForIdle()
.then(() => Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS))
.then(() => {
- // Update authToken in Onyx and in our local variables so that API requests will use the new authToken
- updateSessionAuthTokens(response?.authToken, response?.encryptedAuthToken);
+ Onyx.set(ONYXKEYS.CREDENTIALS, {
+ ...stashedCredentials,
+ accountID: response.requesterID,
+ });
+ Onyx.set(ONYXKEYS.SESSION, {
+ ...stashedSession,
+ accountID: response.requesterID,
+ email: requesterEmail,
+ authToken,
+ encryptedAuthToken: response.encryptedAuthToken,
+ });
+ Onyx.set(ONYXKEYS.STASHED_CREDENTIALS, {});
+ Onyx.set(ONYXKEYS.STASHED_SESSION, {});
NetworkStore.setAuthToken(response?.authToken ?? null);
- Onyx.set(ONYXKEYS.CREDENTIALS, stashedCredentials);
- Onyx.set(ONYXKEYS.SESSION, stashedSession);
- Onyx.set(ONYXKEYS.STASHED_CREDENTIALS, {});
- Onyx.set(ONYXKEYS.STASHED_SESSION, {});
confirmReadyToOpenApp();
- openApp();
-
- NativeModules.HybridAppModule.switchAccount(getCurrentUserEmail() ?? '');
+ openApp().then(() => NativeModules.HybridAppModule.switchAccount(requesterEmail, authToken, '', ''));
});
})
.catch((error) => {
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 26ebf5e37a8e..143590b7169d 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -22,6 +22,7 @@ import type {
SendInvoiceParams,
SendMoneyParams,
SetNameValuePairParams,
+ ShareTrackedExpenseParams,
SplitBillParams,
StartSplitBillParams,
SubmitReportParams,
@@ -208,7 +209,7 @@ type TrackExpenseInformation = {
actionableWhisperReportActionIDParam?: string;
onyxData: OnyxData;
};
-type CategorizeTrackedExpenseTransactionParams = {
+type TrackedExpenseTransactionParams = {
transactionID: string;
amount: number;
currency: string;
@@ -222,11 +223,11 @@ type CategorizeTrackedExpenseTransactionParams = {
billable?: boolean;
receipt?: Receipt;
};
-type CategorizeTrackedExpensePolicyParams = {
+type TrackedExpensePolicyParams = {
policyID: string;
- isDraftPolicy: boolean;
+ isDraftPolicy?: boolean;
};
-type CategorizeTrackedExpenseReportInformation = {
+type TrackedExpenseReportInformation = {
moneyRequestPreviewReportActionID: string;
moneyRequestReportID: string;
moneyRequestCreatedReportActionID: string;
@@ -236,13 +237,14 @@ type CategorizeTrackedExpenseReportInformation = {
transactionThreadReportID: string;
reportPreviewReportActionID: string;
};
-type CategorizeTrackedExpenseParams = {
- onyxData: OnyxData | undefined;
- reportInformation: CategorizeTrackedExpenseReportInformation;
- transactionParams: CategorizeTrackedExpenseTransactionParams;
- policyParams: CategorizeTrackedExpensePolicyParams;
+type TrackedExpenseParams = {
+ onyxData?: OnyxData;
+ reportInformation: TrackedExpenseReportInformation;
+ transactionParams: TrackedExpenseTransactionParams;
+ policyParams: TrackedExpensePolicyParams;
createdWorkspaceParams?: CreateWorkspaceParams;
};
+
type SendInvoiceInformation = {
senderWorkspaceID: string;
receiver: Partial;
@@ -4118,7 +4120,7 @@ function convertTrackedExpenseToRequest(
API.write(WRITE_COMMANDS.CONVERT_TRACKED_EXPENSE_TO_REQUEST, parameters, {optimisticData, successData, failureData});
}
-function categorizeTrackedExpense(trackedExpenseParams: CategorizeTrackedExpenseParams) {
+function categorizeTrackedExpense(trackedExpenseParams: TrackedExpenseParams) {
const {onyxData, reportInformation, transactionParams, policyParams, createdWorkspaceParams} = trackedExpenseParams;
const {optimisticData, successData, failureData} = onyxData ?? {};
const {transactionID} = transactionParams;
@@ -4163,32 +4165,20 @@ function categorizeTrackedExpense(trackedExpenseParams: CategorizeTrackedExpense
}
}
-function shareTrackedExpense(
- policyID: string,
- transactionID: string,
- moneyRequestPreviewReportActionID: string,
- moneyRequestReportID: string,
- moneyRequestCreatedReportActionID: string,
- actionableWhisperReportActionID: string,
- linkedTrackedExpenseReportAction: OnyxTypes.ReportAction,
- linkedTrackedExpenseReportID: string,
- transactionThreadReportID: string,
- reportPreviewReportActionID: string,
- onyxData: OnyxData | undefined,
- amount: number,
- currency: string,
- comment: string,
- merchant: string,
- created: string,
- category?: string,
- tag?: string,
- taxCode = '',
- taxAmount = 0,
- billable?: boolean,
- receipt?: Receipt,
- createdWorkspaceParams?: CreateWorkspaceParams,
-) {
+function shareTrackedExpense(trackedExpenseParams: TrackedExpenseParams) {
+ const {onyxData, reportInformation, transactionParams, policyParams, createdWorkspaceParams} = trackedExpenseParams;
const {optimisticData, successData, failureData} = onyxData ?? {};
+ const {transactionID} = transactionParams;
+ const {
+ actionableWhisperReportActionID,
+ moneyRequestPreviewReportActionID,
+ moneyRequestCreatedReportActionID,
+ reportPreviewReportActionID,
+ moneyRequestReportID,
+ linkedTrackedExpenseReportAction,
+ linkedTrackedExpenseReportID,
+ transactionThreadReportID,
+ } = reportInformation;
const {
optimisticData: moveTransactionOptimisticData,
@@ -4209,26 +4199,15 @@ function shareTrackedExpense(
successData?.push(...moveTransactionSuccessData);
failureData?.push(...moveTransactionFailureData);
- const parameters = {
- policyID,
- transactionID,
+ const parameters: ShareTrackedExpenseParams = {
+ ...transactionParams,
+ policyID: policyParams?.policyID,
moneyRequestPreviewReportActionID,
moneyRequestReportID,
moneyRequestCreatedReportActionID,
actionableWhisperReportActionID,
modifiedExpenseReportActionID,
reportPreviewReportActionID,
- amount,
- currency,
- comment,
- merchant,
- created,
- category,
- tag,
- taxCode,
- taxAmount,
- billable,
- receipt: receipt instanceof Blob ? receipt : undefined,
policyExpenseChatReportID: createdWorkspaceParams?.expenseChatReportID,
policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID,
adminsChatReportID: createdWorkspaceParams?.adminsChatReportID,
@@ -4595,7 +4574,7 @@ function trackExpense(
if (!linkedTrackedExpenseReportAction || !actionableWhisperReportActionID || !linkedTrackedExpenseReportID) {
return;
}
- const transactionParams: CategorizeTrackedExpenseTransactionParams = {
+ const transactionParams: TrackedExpenseTransactionParams = {
transactionID: transaction?.transactionID ?? '-1',
amount,
currency,
@@ -4609,11 +4588,11 @@ function trackExpense(
billable,
receipt: trackedReceipt instanceof Blob ? trackedReceipt : undefined,
};
- const policyParams: CategorizeTrackedExpensePolicyParams = {
+ const policyParams: TrackedExpensePolicyParams = {
policyID: chatReport?.policyID ?? '-1',
isDraftPolicy,
};
- const reportInformation: CategorizeTrackedExpenseReportInformation = {
+ const reportInformation: TrackedExpenseReportInformation = {
moneyRequestPreviewReportActionID: iouAction?.reportActionID ?? '-1',
moneyRequestReportID: iouReport?.reportID ?? '-1',
moneyRequestCreatedReportActionID: createdIOUReportActionID ?? '-1',
@@ -4623,7 +4602,7 @@ function trackExpense(
transactionThreadReportID: transactionThreadReportID ?? '-1',
reportPreviewReportActionID: reportPreviewAction?.reportActionID ?? '-1',
};
- const trackedExpenseParams: CategorizeTrackedExpenseParams = {
+ const trackedExpenseParams: TrackedExpenseParams = {
onyxData,
reportInformation,
transactionParams,
@@ -4638,31 +4617,41 @@ function trackExpense(
if (!linkedTrackedExpenseReportAction || !actionableWhisperReportActionID || !linkedTrackedExpenseReportID) {
return;
}
- shareTrackedExpense(
- chatReport?.policyID ?? '-1',
- transaction?.transactionID ?? '-1',
- iouAction?.reportActionID ?? '-1',
- iouReport?.reportID ?? '-1',
- createdIOUReportActionID ?? '-1',
- actionableWhisperReportActionID,
- linkedTrackedExpenseReportAction,
- linkedTrackedExpenseReportID,
- transactionThreadReportID ?? '-1',
- reportPreviewAction?.reportActionID ?? '-1',
- onyxData,
+ const transactionParams = {
+ transactionID: transaction?.transactionID ?? '-1',
amount,
currency,
comment,
merchant,
created,
+ taxCode: taxCode ?? '',
+ taxAmount: taxAmount ?? 0,
category,
tag,
- taxCode,
- taxAmount,
billable,
- trackedReceipt,
+ receipt: trackedReceipt instanceof Blob ? trackedReceipt : undefined,
+ };
+ const policyParams = {
+ policyID: chatReport?.policyID ?? '-1',
+ };
+ const reportInformation = {
+ moneyRequestPreviewReportActionID: iouAction?.reportActionID ?? '-1',
+ moneyRequestReportID: iouReport?.reportID ?? '-1',
+ moneyRequestCreatedReportActionID: createdIOUReportActionID ?? '-1',
+ actionableWhisperReportActionID,
+ linkedTrackedExpenseReportAction,
+ linkedTrackedExpenseReportID,
+ transactionThreadReportID: transactionThreadReportID ?? '-1',
+ reportPreviewReportActionID: reportPreviewAction?.reportActionID ?? '-1',
+ };
+ const trackedExpenseParams = {
+ onyxData,
+ reportInformation,
+ transactionParams,
+ policyParams,
createdWorkspaceParams,
- );
+ };
+ shareTrackedExpense(trackedExpenseParams);
break;
}
default: {
@@ -8521,7 +8510,10 @@ function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.
API.write(WRITE_COMMANDS.PAY_INVOICE, params, {optimisticData, successData, failureData});
}
-function detachReceipt(transactionID: string) {
+function detachReceipt(transactionID: string | undefined) {
+ if (!transactionID) {
+ return;
+ }
const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
const newTransaction = transaction
? {
diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts
index a09159993ad8..5a57e5622c53 100644
--- a/src/libs/actions/OnyxUpdates.ts
+++ b/src/libs/actions/OnyxUpdates.ts
@@ -9,7 +9,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {OnyxUpdateEvent, OnyxUpdatesFromServer, Request} from '@src/types/onyx';
import type Response from '@src/types/onyx/Response';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import * as QueuedOnyxUpdates from './QueuedOnyxUpdates';
+import {queueOnyxUpdates} from './QueuedOnyxUpdates';
// This key needs to be separate from ONYXKEYS.ONYX_UPDATES_FROM_SERVER so that it can be updated without triggering the callback when the server IDs are updated. If that
// callback were triggered it would lead to duplicate processing of server updates.
@@ -30,7 +30,7 @@ function applyHTTPSOnyxUpdates(request: Request, response: Response) {
console.debug('[OnyxUpdateManager] Applying https update');
// For most requests we can immediately update Onyx. For write requests we queue the updates and apply them after the sequential queue has flushed to prevent a replay effect in
// the UI. See https://github.com/Expensify/App/issues/12775 for more info.
- const updateHandler: (updates: OnyxUpdate[]) => Promise = request?.data?.apiRequestType === CONST.API_REQUEST_TYPE.WRITE ? QueuedOnyxUpdates.queueOnyxUpdates : Onyx.update;
+ const updateHandler: (updates: OnyxUpdate[]) => Promise = request?.data?.apiRequestType === CONST.API_REQUEST_TYPE.WRITE ? queueOnyxUpdates : Onyx.update;
// First apply any onyx data updates that are being sent back from the API. We wait for this to complete and then
// apply successData or failureData. This ensures that we do not update any pending, loading, or other UI states contained
@@ -114,7 +114,10 @@ function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFrom
Log.info(`[OnyxUpdateManager] Applying update type: ${type} with lastUpdateID: ${lastUpdateID}`, false, {command: request?.command});
if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) <= lastUpdateIDAppliedToClient) {
- Log.info('[OnyxUpdateManager] Update received was older than or the same as current state, returning without applying the updates other than successData and failureData');
+ Log.info('[OnyxUpdateManager] Update received was older than or the same as current state, returning without applying the updates other than successData and failureData', false, {
+ lastUpdateID,
+ lastUpdateIDAppliedToClient,
+ });
// In this case, we're already received the OnyxUpdate included in the response, so we don't need to apply it again.
// However, we do need to apply the successData and failureData from the request
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index f28a82bea9bb..eb302fd7fda5 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -64,6 +64,7 @@ import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
import * as ErrorUtils from '@libs/ErrorUtils';
+import {createFile} from '@libs/fileDownload/FileUtils';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import GoogleTagManager from '@libs/GoogleTagManager';
import Log from '@libs/Log';
@@ -1596,7 +1597,8 @@ function generateCustomUnitID(): string {
}
function buildOptimisticDistanceRateCustomUnits(reportCurrency?: string): OptimisticCustomUnits {
- const currency = reportCurrency ?? allPersonalDetails?.[sessionAccountID]?.localCurrencyCode ?? CONST.CURRENCY.USD;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null
+ const currency = reportCurrency || (allPersonalDetails?.[sessionAccountID]?.localCurrencyCode ?? CONST.CURRENCY.USD);
const customUnitID = generateCustomUnitID();
const customUnitRateID = generateCustomUnitID();
@@ -1634,10 +1636,12 @@ function buildOptimisticDistanceRateCustomUnits(reportCurrency?: string): Optimi
* @param [policyName] custom policy name we will use for created workspace
* @param [policyID] custom policy id we will use for created workspace
* @param [makeMeAdmin] leave the calling account as an admin on the policy
+ * @param [currency] Optional, selected currency for the workspace
+ * @param [file], avatar file for workspace
*/
-function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false) {
+function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false, currency = '', file?: File) {
const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail);
- const {customUnits, outputCurrency} = buildOptimisticDistanceRateCustomUnits();
+ const {customUnits, outputCurrency} = buildOptimisticDistanceRateCustomUnits(currency);
const optimisticData: OnyxUpdate[] = [
{
@@ -1658,6 +1662,8 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol
makeMeAdmin,
autoReporting: true,
autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT,
+ avatarURL: file?.uri ?? null,
+ originalFileName: file?.name,
employeeList: {
[sessionEmail]: {
role: CONST.POLICY.ROLE.ADMIN,
@@ -1687,12 +1693,24 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol
* @param [makeMeAdmin] leave the calling account as an admin on the policy
* @param [policyName] custom policy name we will use for created workspace
* @param [policyID] custom policy id we will use for created workspace
- * @param [expenseReportId] the reportID of the expense report that is being used to create the workspace
+ * @param [expenseReportId] Optional, Purpose of using application selected by user in guided setup flow
+ * @param [engagementChoice] Purpose of using application selected by user in guided setup flow
+ * @param [currency] Optional, selected currency for the workspace
+ * @param [file] Optional, avatar file for workspace
*/
-function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: string) {
+function buildPolicyData(
+ policyOwnerEmail = '',
+ makeMeAdmin = false,
+ policyName = '',
+ policyID = generatePolicyID(),
+ expenseReportId?: string,
+ engagementChoice?: string,
+ currency = '',
+ file?: File,
+) {
const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail);
- const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticDistanceRateCustomUnits();
+ const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticDistanceRateCustomUnits(currency);
const {
adminsChatReportID,
@@ -1754,6 +1772,8 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
type: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
+ avatarURL: file?.uri,
+ originalFileName: file?.name,
},
},
{
@@ -1939,6 +1959,9 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
successData.push(...optimisticCategoriesData.successData);
}
+ // We need to clone the file to prevent non-indexable errors.
+ const clonedFile = file ? (createFile(file) as File) : undefined;
+
const params: CreateWorkspaceParams = {
policyID,
adminsChatReportID,
@@ -1952,6 +1975,8 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
customUnitID,
customUnitRateID,
engagementChoice,
+ currency: outputCurrency,
+ file: clonedFile,
};
return {successData, optimisticData, failureData, params};
@@ -1965,9 +1990,19 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName
* @param [policyName] custom policy name we will use for created workspace
* @param [policyID] custom policy id we will use for created workspace
* @param [engagementChoice] Purpose of using application selected by user in guided setup flow
+ * @param [currency] Optional, selected currency for the workspace
+ * @param [file], avatar file for workspace
*/
-function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), engagementChoice = ''): CreateWorkspaceParams {
- const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice);
+function createWorkspace(
+ policyOwnerEmail = '',
+ makeMeAdmin = false,
+ policyName = '',
+ policyID = generatePolicyID(),
+ engagementChoice = '',
+ currency = '',
+ file?: File,
+): CreateWorkspaceParams {
+ const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, currency, file);
API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData});
// Publish a workspace created event if this is their first policy
@@ -1986,10 +2021,10 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName
* @param [policyName] custom policy name we will use for created workspace
* @param [policyID] custom policy id we will use for created workspace
*/
-function createDraftWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID()): CreateWorkspaceParams {
+function createDraftWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), currency = '', file?: File): CreateWorkspaceParams {
const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail);
- const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticDistanceRateCustomUnits();
+ const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticDistanceRateCustomUnits(currency);
const {expenseChatData, adminsChatReportID, adminsCreatedReportActionID, expenseChatReportID, expenseCreatedReportActionID} = ReportUtils.buildOptimisticWorkspaceChats(
policyID,
@@ -2056,6 +2091,9 @@ function createDraftWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policy
},
];
+ // We need to clone the file to prevent non-indexable errors.
+ const clonedFile = file ? (createFile(file) as File) : undefined;
+
const params: CreateWorkspaceParams = {
policyID,
adminsChatReportID,
@@ -2068,6 +2106,8 @@ function createDraftWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policy
expenseCreatedReportActionID,
customUnitID,
customUnitRateID,
+ currency: outputCurrency,
+ file: clonedFile,
};
Onyx.update(optimisticData);
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index a469af82dd1c..f0d2d2a08ac9 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -193,7 +193,7 @@ type TaskForParameters =
taskReportID: string;
parentReportID: string;
parentReportActionID: string;
- assigneeChatReportID: string;
+ assigneeChatReportID?: string;
createdTaskReportActionID: string;
completedTaskReportActionID?: string;
title: string;
@@ -844,7 +844,7 @@ function openReport(
reportActionID?: string,
participantLoginList: string[] = [],
newReportObject?: OptimisticChatReport,
- parentReportActionID = '-1',
+ parentReportActionID?: string,
isFromDeepLink = false,
participantAccountIDList: number[] = [],
avatar?: File | CustomRNImageManipulatorResult,
@@ -934,19 +934,21 @@ function openReport(
const onboardingData = prepareOnboardingOptimisticData(choice, onboardingMessage);
- optimisticData.push(...onboardingData.optimisticData, {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.NVP_INTRO_SELECTED,
- value: {
- isInviteOnboardingComplete: true,
- },
- });
+ if (onboardingData) {
+ optimisticData.push(...onboardingData.optimisticData, {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.NVP_INTRO_SELECTED,
+ value: {
+ isInviteOnboardingComplete: true,
+ },
+ });
- successData.push(...onboardingData.successData);
+ successData.push(...onboardingData.successData);
- failureData.push(...onboardingData.failureData);
+ failureData.push(...onboardingData.failureData);
- parameters.guidedSetupData = JSON.stringify(onboardingData.guidedSetupData);
+ parameters.guidedSetupData = JSON.stringify(onboardingData.guidedSetupData);
+ }
}
}
@@ -1100,7 +1102,7 @@ function openReport(
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newReportObject.parentReportID}`,
- value: {[parentReportActionID]: {childReportID: '-1', childType: ''}},
+ value: {[parentReportActionID]: {childType: ''}},
});
}
}
@@ -1177,12 +1179,12 @@ function navigateToAndOpenReport(
const report = isEmptyObject(chat) ? newChat : chat;
// We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server
- openReport(report?.reportID ?? '', '', userLogins, newChat, undefined, undefined, undefined, avatarFile);
+ openReport(report?.reportID, '', userLogins, newChat, undefined, undefined, undefined, avatarFile);
if (shouldDismissModal) {
Navigation.dismissModalWithReport(report);
} else {
Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME});
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID ?? '-1'), actionType);
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report?.reportID), actionType);
}
}
@@ -1200,7 +1202,7 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[])
const report = chat ?? newChat;
// We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server
- openReport(report?.reportID ?? '', '', [], newChat, '0', false, participantAccountIDs);
+ openReport(report?.reportID, '', [], newChat, '0', false, participantAccountIDs);
Navigation.dismissModalWithReport(report);
}
@@ -1211,8 +1213,8 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[])
* @param parentReportAction the parent comment of a thread
* @param parentReportID The reportID of the parent
*/
-function navigateToAndOpenChildReport(childReportID = '-1', parentReportAction: Partial = {}, parentReportID = '0') {
- if (childReportID !== '-1' && childReportID !== '0') {
+function navigateToAndOpenChildReport(childReportID: string | undefined, parentReportAction: Partial = {}, parentReportID?: string) {
+ if (childReportID) {
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID));
} else {
const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction.actorAccountID)])];
@@ -1506,36 +1508,38 @@ function handleReportChanged(report: OnyxEntry) {
return;
}
+ const {reportID, preexistingReportID, parentReportID, parentReportActionID} = report;
+
// Handle cleanup of stale optimistic IOU report and its report preview separately
- if (report?.reportID && report.preexistingReportID && isMoneyRequestReport(report) && report?.parentReportActionID) {
- Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, {
- [report.parentReportActionID]: null,
+ if (reportID && preexistingReportID && isMoneyRequestReport(report) && parentReportActionID) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, {
+ [parentReportActionID]: null,
});
- Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null);
+ Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null);
return;
}
// It is possible that we optimistically created a DM/group-DM for a set of users for which a report already exists.
// In this case, the API will let us know by returning a preexistingReportID.
// We should clear out the optimistically created report and re-route the user to the preexisting report.
- if (report?.reportID && report.preexistingReportID) {
+ if (reportID && preexistingReportID) {
let callback = () => {
- Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null);
- Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.preexistingReportID}`, {...report, reportID: report.preexistingReportID, preexistingReportID: null});
- Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`, null);
+ Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null);
+ Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`, {...report, reportID: preexistingReportID, preexistingReportID: null});
+ Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null);
};
// Only re-route them if they are still looking at the optimistically created report
- if (Navigation.getActiveRoute().includes(`/r/${report.reportID}`)) {
+ if (Navigation.getActiveRoute().includes(`/r/${reportID}`)) {
const currCallback = callback;
callback = () => {
currCallback();
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.preexistingReportID ?? '-1'), CONST.NAVIGATION.TYPE.UP);
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(preexistingReportID), CONST.NAVIGATION.TYPE.UP);
};
// The report screen will listen to this event and transfer the draft comment to the existing report
// This will allow the newest draft comment to be transferred to the existing report
- DeviceEventEmitter.emit(`switchToPreExistingReport_${report.reportID}`, {
- preexistingReportID: report.preexistingReportID,
+ DeviceEventEmitter.emit(`switchToPreExistingReport_${reportID}`, {
+ preexistingReportID,
callback,
});
@@ -1544,20 +1548,20 @@ function handleReportChanged(report: OnyxEntry) {
// In case the user is not on the report screen, we will transfer the report draft comment directly to the existing report
// after that clear the optimistically created report
- const draftReportComment = allReportDraftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`];
+ const draftReportComment = allReportDraftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`];
if (!draftReportComment) {
callback();
return;
}
- saveReportDraftComment(report.preexistingReportID ?? '-1', draftReportComment, callback);
+ saveReportDraftComment(preexistingReportID, draftReportComment, callback);
return;
}
- if (report?.reportID) {
+ if (reportID) {
if (isConciergeChatReport(report)) {
- conciergeChatReportID = report.reportID;
+ conciergeChatReportID = reportID;
}
}
}
@@ -1972,10 +1976,15 @@ function updateRoomVisibility(reportID: string, previousValue: RoomVisibility |
* @param parentReportID The reportID of the parent
* @param prevNotificationPreference The previous notification preference for the child report
*/
-function toggleSubscribeToChildReport(childReportID = '-1', parentReportAction: Partial = {}, parentReportID = '-1', prevNotificationPreference?: NotificationPreference) {
- if (childReportID !== '-1') {
+function toggleSubscribeToChildReport(
+ childReportID: string | undefined,
+ parentReportAction: Partial = {},
+ parentReportID?: string,
+ prevNotificationPreference?: NotificationPreference,
+) {
+ if (childReportID) {
openReport(childReportID);
- const parentReportActionID = parentReportAction?.reportActionID ?? '-1';
+ const parentReportActionID = parentReportAction?.reportActionID;
if (!prevNotificationPreference || isHiddenForCurrentUser(prevNotificationPreference)) {
updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, parentReportID, parentReportActionID);
} else {
@@ -2946,7 +2955,7 @@ function navigateToMostRecentReport(currentReport: OnyxEntry) {
const lastAccessedReportID = findLastAccessedReport(false, false, undefined, currentReport?.reportID)?.reportID;
if (lastAccessedReportID) {
- const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? '-1');
+ const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID);
Navigation.goBack(lastAccessedReportRoute);
} else {
const isChatThread = isChatThreadReportUtils(currentReport);
@@ -3121,7 +3130,7 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`,
value: {
[report.parentReportActionID]: {
- childReportNotificationPreference: report?.participants?.[currentUserAccountID ?? -1]?.notificationPreference ?? getDefaultNotificationPreferenceForReport(report),
+ childReportNotificationPreference: report?.participants?.[currentUserAccountID]?.notificationPreference ?? getDefaultNotificationPreferenceForReport(report),
},
},
});
@@ -3613,10 +3622,16 @@ function prepareOnboardingOptimisticData(
// Guides are assigned and tasks are posted in the #admins room for the MANAGE_TEAM onboarding action, except for emails that have a '+'.
const shouldPostTasksInAdminsRoom = engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !currentUserEmail?.includes('+');
- const integrationName = userReportedIntegration ? CONST.ONBOARDING_ACCOUNTING_MAPPING[userReportedIntegration] : '';
const adminsChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`];
const targetChatReport = shouldPostTasksInAdminsRoom ? adminsChatReport : getChatByParticipants([CONST.ACCOUNT_ID.CONCIERGE, currentUserAccountID]);
- const {reportID: targetChatReportID = '', policyID: targetChatPolicyID = ''} = targetChatReport ?? {};
+ const {reportID: targetChatReportID, policyID: targetChatPolicyID} = targetChatReport ?? {};
+
+ if (!targetChatReportID) {
+ Log.warn('Missing reportID for onboarding optimistic data');
+ return;
+ }
+
+ const integrationName = userReportedIntegration ? CONST.ONBOARDING_ACCOUNTING_MAPPING[userReportedIntegration] : '';
const assignedGuideEmail = getPolicy(targetChatPolicyID)?.assignedGuide?.email ?? 'Setup Specialist';
const assignedGuidePersonalDetail = Object.values(allPersonalDetails ?? {}).find((personalDetail) => personalDetail?.login === assignedGuideEmail);
let assignedGuideAccountID: number;
@@ -3668,14 +3683,14 @@ function prepareOnboardingOptimisticData(
const taskDescription =
typeof task.description === 'function'
? task.description({
- adminsRoomLink: `${environmentURL}/${ROUTES.REPORT_WITH_ID.getRoute(adminsChatReportID ?? '-1')}`,
- workspaceCategoriesLink: `${environmentURL}/${ROUTES.WORKSPACE_CATEGORIES.getRoute(onboardingPolicyID ?? '-1')}`,
- workspaceMembersLink: `${environmentURL}/${ROUTES.WORKSPACE_MEMBERS.getRoute(onboardingPolicyID ?? '-1')}`,
- workspaceMoreFeaturesLink: `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(onboardingPolicyID ?? '-1')}`,
+ adminsRoomLink: `${environmentURL}/${ROUTES.REPORT_WITH_ID.getRoute(adminsChatReportID)}`,
+ workspaceCategoriesLink: `${environmentURL}/${ROUTES.WORKSPACE_CATEGORIES.getRoute(onboardingPolicyID)}`,
+ workspaceMembersLink: `${environmentURL}/${ROUTES.WORKSPACE_MEMBERS.getRoute(onboardingPolicyID)}`,
+ workspaceMoreFeaturesLink: `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(onboardingPolicyID)}`,
navatticURL: getNavatticURL(environment, engagementChoice),
integrationName,
- workspaceAccountingLink: `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(onboardingPolicyID ?? '-1')}`,
- workspaceSettingsLink: `${environmentURL}/${ROUTES.WORKSPACE_INITIAL.getRoute(onboardingPolicyID ?? '-1')}`,
+ workspaceAccountingLink: `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(onboardingPolicyID)}`,
+ workspaceSettingsLink: `${environmentURL}/${ROUTES.WORKSPACE_INITIAL.getRoute(onboardingPolicyID)}`,
})
: task.description;
const taskTitle =
@@ -3686,8 +3701,8 @@ function prepareOnboardingOptimisticData(
: task.title;
const currentTask = buildOptimisticTaskReport(
actorAccountID,
- currentUserAccountID,
targetChatReportID,
+ currentUserAccountID,
taskTitle,
taskDescription,
targetChatPolicyID,
@@ -3726,11 +3741,10 @@ function prepareOnboardingOptimisticData(
type: 'task',
task: task.type,
taskReportID: currentTask.reportID,
- parentReportID: currentTask.parentReportID ?? '-1',
+ parentReportID: currentTask.parentReportID,
parentReportActionID: taskReportAction.reportAction.reportActionID,
- assigneeChatReportID: '',
createdTaskReportActionID: taskCreatedAction.reportActionID,
- completedTaskReportActionID: completedTaskReportAction?.reportActionID ?? undefined,
+ completedTaskReportActionID: completedTaskReportAction?.reportActionID,
title: currentTask.reportName ?? '',
description: taskDescription ?? '',
}));
@@ -4165,14 +4179,12 @@ function completeOnboarding(
userReportedIntegration?: OnboardingAccounting,
wasInvited?: boolean,
) {
- const {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters} = prepareOnboardingOptimisticData(
- engagementChoice,
- data,
- adminsChatReportID,
- onboardingPolicyID,
- userReportedIntegration,
- wasInvited,
- );
+ const onboardingData = prepareOnboardingOptimisticData(engagementChoice, data, adminsChatReportID, onboardingPolicyID, userReportedIntegration, wasInvited);
+ if (!onboardingData) {
+ return;
+ }
+
+ const {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters} = onboardingData;
const parameters: CompleteGuidedSetupParams = {
engagementChoice,
@@ -4248,7 +4260,7 @@ function searchForReports(searchInput: string, policyID?: string) {
},
];
- const searchForRoomToMentionParams: SearchForRoomsToMentionParams = {query: searchInput, policyID: policyID ?? '-1'};
+ const searchForRoomToMentionParams: SearchForRoomsToMentionParams = {query: searchInput, policyID};
const searchForReportsParams: SearchForReportsParams = {searchInput, canCancel: true};
// We want to cancel all pending SearchForReports API calls before making another one
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index 85aeebcd69a7..728353365a43 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -34,6 +34,7 @@ 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 {getCurrentUserEmail} from '@libs/Network/NetworkStore';
import NetworkConnection from '@libs/NetworkConnection';
import * as Pusher from '@libs/Pusher/pusher';
import {getReportIDFromLink, parseReportRouteParams as parseReportRouteParamsReportUtils} from '@libs/ReportUtils';
@@ -271,6 +272,9 @@ function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSess
[ONYXKEYS.SESSION]: stashedSession,
};
}
+ if (isSupportal && !shouldStashSession && !hasStashedSession()) {
+ Log.info('No stashed session found for supportal access, clearing the session');
+ }
redirectToSignIn().then(() => {
Onyx.multiSet(onyxSetParams);
});
@@ -525,7 +529,7 @@ function signInAfterTransitionFromOldDot(transitionURL: string) {
nudgeMigrationTimestamp,
isSingleNewDotEntry,
primaryLogin,
- shouldRemoveDelegatedAccess,
+ oldDotOriginalAccountEmail,
} = Object.fromEntries(
queryParams.split('&').map((param) => {
const [key, value] = param.split('=');
@@ -544,22 +548,39 @@ function signInAfterTransitionFromOldDot(transitionURL: string) {
const setSessionDataAndOpenApp = new Promise((resolve) => {
clearOnyxForNewAccount()
.then(() => {
- if (!shouldRemoveDelegatedAccess) {
+ // This section controls copilot changes
+ const currentUserEmail = getCurrentUserEmail();
+
+ // If ND and OD account are the same - do nothing
+ if (email === currentUserEmail) {
return;
}
- return Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS);
+
+ // If account was changed to original one on OD side - clear onyx
+ if (!oldDotOriginalAccountEmail) {
+ return Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS);
+ }
+
+ // If we're already logged in - do nothing, data will be set in next step
+ if (currentUserEmail) {
+ return;
+ }
+
+ // If we're not logged in - set stashed data
+ return Onyx.multiSet({
+ [ONYXKEYS.STASHED_CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword},
+ });
})
.then(() =>
Onyx.multiSet({
[ONYXKEYS.SESSION]: {email, authToken, encryptedAuthToken: decodeURIComponent(encryptedAuthToken), accountID: Number(accountID)},
- [ONYXKEYS.ACCOUNT]: {primaryLogin},
[ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword},
[ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: isSingleNewDotEntry === 'true',
[ONYXKEYS.NVP_TRYNEWDOT]: {
classicRedirect: {completedHybridAppOnboarding: completedHybridAppOnboarding === 'true'},
nudgeMigration: nudgeMigrationTimestamp ? {timestamp: new Date(nudgeMigrationTimestamp)} : undefined,
},
- }),
+ }).then(() => Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin})),
)
.then(() => {
if (clearOnyxOnStart === 'true') {
diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts
index 2a13ff69b769..e67ad4831d40 100644
--- a/src/libs/actions/Task.ts
+++ b/src/libs/actions/Task.ts
@@ -123,7 +123,7 @@ function createTaskAndNavigate(
policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE,
isCreatedUsingMarkdown = false,
) {
- const optimisticTaskReport = ReportUtils.buildOptimisticTaskReport(currentUserAccountID, assigneeAccountID, parentReportID, title, description, policyID);
+ const optimisticTaskReport = ReportUtils.buildOptimisticTaskReport(currentUserAccountID, parentReportID, assigneeAccountID, title, description, policyID);
const assigneeChatReportID = assigneeChatReport?.reportID;
const taskReportID = optimisticTaskReport.reportID;
diff --git a/src/libs/actions/connections/index.ts b/src/libs/actions/connections/index.ts
index b2cb6ffffe94..9087b9fb00c8 100644
--- a/src/libs/actions/connections/index.ts
+++ b/src/libs/actions/connections/index.ts
@@ -11,6 +11,7 @@ import type {
} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
+import * as PolicyUtils from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
@@ -42,11 +43,50 @@ function removePolicyConnection(policyID: string, connectionName: PolicyConnecti
},
];
+ const successData: OnyxUpdate[] = [];
+ const failureData: OnyxUpdate[] = [];
+ const policy = PolicyUtils.getPolicy(policyID);
+ const supportedConnections: PolicyConnectionName[] = [CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.POLICY.CONNECTIONS.NAME.XERO];
+
+ if (PolicyUtils.isCollectPolicy(policy) && supportedConnections.includes(connectionName)) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ areReportFieldsEnabled: false,
+ pendingFields: {
+ areReportFieldsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ },
+ });
+
+ successData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ pendingFields: {
+ areReportFieldsEnabled: null,
+ },
+ },
+ });
+
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ areReportFieldsEnabled: policy?.areReportFieldsEnabled,
+ pendingFields: {
+ areReportFieldsEnabled: null,
+ },
+ },
+ });
+ }
+
const parameters: RemovePolicyConnectionParams = {
policyID,
connectionName,
};
- API.write(WRITE_COMMANDS.REMOVE_POLICY_CONNECTION, parameters, {optimisticData});
+ API.write(WRITE_COMMANDS.REMOVE_POLICY_CONNECTION, parameters, {optimisticData, successData, failureData});
}
function createPendingFields(
diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts
index f209e46930cb..cc43850f12f3 100644
--- a/src/libs/fileDownload/FileUtils.ts
+++ b/src/libs/fileDownload/FileUtils.ts
@@ -3,6 +3,7 @@ import {Alert, Linking, Platform} from 'react-native';
import ImageSize from 'react-native-image-size';
import type {FileObject} from '@components/AttachmentModal';
import DateUtils from '@libs/DateUtils';
+import getPlatform from '@libs/getPlatform';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
import saveLastRoute from '@libs/saveLastRoute';
@@ -330,6 +331,21 @@ const resizeImageIfNeeded = (file: FileObject) => {
}
return getImageDimensionsAfterResize(file).then(({width, height}) => getImageManipulator({fileUri: file.uri ?? '', width, height, fileName: file.name ?? '', type: file.type}));
};
+
+const createFile = (file: File): FileObject => {
+ if (getPlatform() === CONST.PLATFORM.ANDROID || getPlatform() === CONST.PLATFORM.IOS) {
+ return {
+ uri: file.uri,
+ name: file.name,
+ type: file.type,
+ };
+ }
+ return new File([file], file.name, {
+ type: file.type,
+ lastModified: file.lastModified,
+ });
+};
+
export {
showGeneralErrorAlert,
showSuccessAlert,
@@ -350,4 +366,5 @@ export {
verifyFileFormat,
getImageDimensionsAfterResize,
resizeImageIfNeeded,
+ createFile,
};
diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx
index 342fb041a6c9..b7d1705f96e4 100644
--- a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx
+++ b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx
@@ -43,7 +43,7 @@ function AddressStep({onNext, onMove, isEditing}: SubStepProps) {
onMove={onMove}
formID={ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS}
formTitle={translate('personalInfoStep.whatsYourAddress')}
- formPOBoxDisclaimer={translate('personalInfoStep.noPOBoxesPlease')}
+ formPOBoxDisclaimer={translate('common.noPO')}
onSubmit={handleSubmit}
stepFields={STEP_FIELDS}
inputFieldsIDs={INPUT_KEYS}
diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx
index 5669a98fd484..845722909b2c 100644
--- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx
+++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx
@@ -49,8 +49,6 @@ function LogInWithShortLivedAuthTokenPage({route}: LogInWithShortLivedAuthTokenP
// For HybridApp we have separate logic to handle transitions.
if (!NativeModules.HybridAppModule && exitTo) {
Navigation.isNavigationReady().then(() => {
- // We must call goBack() to remove the /transition route from history
- Navigation.goBack();
Navigation.navigate(exitTo as Route);
});
}
diff --git a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx
index a14f30216051..3f52722fc30e 100644
--- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx
+++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx
@@ -18,13 +18,13 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import {createWorkspace, generatePolicyID} from '@libs/actions/Policy/Policy';
+import {completeOnboarding} from '@libs/actions/Report';
+import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@libs/actions/Welcome';
import navigateAfterOnboarding from '@libs/navigateAfterOnboarding';
import Navigation from '@libs/Navigation/Navigation';
-import * as PolicyUtils from '@libs/PolicyUtils';
+import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils';
import variables from '@styles/variables';
-import * as Policy from '@userActions/Policy/Policy';
-import * as Report from '@userActions/Report';
-import * as Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
import type {OnboardingAccounting} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -53,6 +53,7 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount
const [onboardingCompanySize] = useOnyx(ONYXKEYS.ONBOARDING_COMPANY_SIZE);
const {canUseDefaultRooms} = usePermissions();
const {activeWorkspaceID} = useActiveWorkspace();
+ const [session] = useOnyx(ONYXKEYS.SESSION);
const [userReportedIntegration, setUserReportedIntegration] = useState(undefined);
const [error, setError] = useState('');
@@ -60,16 +61,16 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount
// If the signupQualifier is VSB, the company size step is skip.
// So we need to create the new workspace in the accounting step
- const paidGroupPolicy = Object.values(allPolicies ?? {}).find(PolicyUtils.isPaidGroupPolicy);
+ const paidGroupPolicy = Object.values(allPolicies ?? {}).find((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, session?.email));
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (!isVsb || paidGroupPolicy || isLoadingOnyxValue(allPoliciesResult)) {
return;
}
- const {adminsChatReportID, policyID} = Policy.createWorkspace(undefined, true, '', Policy.generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM);
- Welcome.setOnboardingAdminsChatReportID(adminsChatReportID);
- Welcome.setOnboardingPolicyID(policyID);
+ const {adminsChatReportID, policyID} = createWorkspace(undefined, true, '', generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM);
+ setOnboardingAdminsChatReportID(adminsChatReportID);
+ setOnboardingPolicyID(policyID);
}, [isVsb, paidGroupPolicy, allPolicies, allPoliciesResult]);
// Set onboardingPolicyID and onboardingAdminsChatReportID if a workspace is created by the backend for OD signups
@@ -77,8 +78,8 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount
if (!paidGroupPolicy || onboardingPolicyID) {
return;
}
- Welcome.setOnboardingAdminsChatReportID(paidGroupPolicy.chatReportIDAdmins?.toString());
- Welcome.setOnboardingPolicyID(paidGroupPolicy.id);
+ setOnboardingAdminsChatReportID(paidGroupPolicy.chatReportIDAdmins?.toString());
+ setOnboardingPolicyID(paidGroupPolicy.id);
}, [paidGroupPolicy, onboardingPolicyID]);
const accountingOptions: OnboardingListItem[] = useMemo(() => {
@@ -166,7 +167,7 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount
return;
}
- Report.completeOnboarding(
+ completeOnboarding(
onboardingPurposeSelected,
CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected],
undefined,
@@ -179,8 +180,8 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount
);
// Avoid creating new WS because onboardingPolicyID is cleared before unmounting
InteractionManager.runAfterInteractions(() => {
- Welcome.setOnboardingAdminsChatReportID();
- Welcome.setOnboardingPolicyID();
+ setOnboardingAdminsChatReportID();
+ setOnboardingPolicyID();
});
navigateAfterOnboarding(isSmallScreenWidth, canUseDefaultRooms, onboardingPolicyID, activeWorkspaceID);
}}
diff --git a/src/pages/Travel/CarTripDetails.tsx b/src/pages/Travel/CarTripDetails.tsx
index 09ffd3d2cad1..e840e1a56da7 100644
--- a/src/pages/Travel/CarTripDetails.tsx
+++ b/src/pages/Travel/CarTripDetails.tsx
@@ -22,9 +22,15 @@ function CarTripDetails({reservation, personalDetails}: CarTripDetailsProps) {
const pickUpDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.start.date));
const dropOffDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.end.date));
- const cancellationText = reservation.cancellationDeadline
- ? `${translate('travel.carDetails.cancellationUntil')} ${DateUtils.getFormattedTransportDateAndHour(new Date(reservation.cancellationDeadline)).date}`
- : reservation.cancellationPolicy;
+
+ let cancellationText = reservation.cancellationPolicy;
+ if (reservation.cancellationDeadline) {
+ cancellationText = `${translate('travel.carDetails.cancellationUntil')} ${DateUtils.getFormattedTransportDateAndHour(new Date(reservation.cancellationDeadline)).date}`;
+ }
+
+ if (reservation.cancellationPolicy === null && reservation.cancellationDeadline === null) {
+ cancellationText = translate('travel.carDetails.freeCancellation');
+ }
const displayName = personalDetails?.displayName ?? reservation.travelerPersonalInfo?.name;
diff --git a/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx b/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx
index 4a6b6a473188..0fa006601097 100644
--- a/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx
+++ b/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx
@@ -5,7 +5,7 @@ import Section, {CARD_LAYOUT} from '@components/Section';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import * as App from '@userActions/App';
+import ROUTES from '@src/ROUTES';
function WorkspaceCardCreateAWorkspace() {
const styles = useThemeStyles();
@@ -22,8 +22,7 @@ function WorkspaceCardCreateAWorkspace() {
>
);
diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts
index dbbdc52d5b9a..d4f3a9ca2049 100644
--- a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts
+++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/index.ts
@@ -10,20 +10,12 @@ const GUTTER_WIDTH = variables.gutterWidth;
* Compute the amount the tooltip needs to be horizontally shifted in order to keep it from displaying in the gutters.
*
* @param windowWidth - The width of the window.
- * @param xOffset - The distance between the left edge of the window
+ * @param tooltipLeftEdge - The distance between the left edge of the tooltip
* and the left edge of the wrapped component.
- * @param componentWidth - The width of the wrapped component.
* @param tooltipWidth - The width of the tooltip itself.
- * @param [manualShiftHorizontal] - Any additional amount to manually shift the tooltip to the left or right.
- * A positive value shifts it to the right,
- * and a negative value shifts it to the left.
*/
-const computeHorizontalShift: ComputeHorizontalShift = (windowWidth, xOffset, componentWidth, tooltipWidth, manualShiftHorizontal) => {
- // First find the left and right edges of the tooltip (by default, it is centered on the component).
- const componentCenter = xOffset + componentWidth / 2 + manualShiftHorizontal;
- const tooltipLeftEdge = componentCenter - tooltipWidth / 2;
- const tooltipRightEdge = componentCenter + tooltipWidth / 2;
-
+const computeHorizontalShift: ComputeHorizontalShift = (windowWidth, tooltipLeftEdge, tooltipWidth) => {
+ const tooltipRightEdge = tooltipLeftEdge + tooltipWidth;
if (tooltipLeftEdge < GUTTER_WIDTH) {
// Tooltip is in left gutter, shift right by a multiple of four.
return roundToNearestMultipleOfFour(GUTTER_WIDTH - tooltipLeftEdge);
diff --git a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts
index bc82a9b4fbe4..892da822ebc6 100644
--- a/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts
+++ b/src/styles/utils/generators/TooltipStyleUtils/computeHorizontalShift/types.ts
@@ -1,4 +1,4 @@
-type ComputeHorizontalShift = (windowWidth: number, xOffset: number, componentWidth: number, tooltipWidth: number, manualShiftHorizontal: number) => number;
+type ComputeHorizontalShift = (windowWidth: number, tooltipLeftEdge: number, componentWidth: number) => number;
// eslint-disable-next-line import/prefer-default-export
export type {ComputeHorizontalShift};
diff --git a/src/styles/utils/generators/TooltipStyleUtils/index.ts b/src/styles/utils/generators/TooltipStyleUtils/index.ts
index d169b0da0f25..e313f140d48f 100644
--- a/src/styles/utils/generators/TooltipStyleUtils/index.ts
+++ b/src/styles/utils/generators/TooltipStyleUtils/index.ts
@@ -142,18 +142,6 @@ const createTooltipStyleUtils: StyleUtilGenerator = (
!!(tooltip && isOverlappingAtTop(tooltip, xOffset, yOffset, tooltipTargetWidth, tooltipTargetHeight)) ||
anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP;
- // Determine if we need to shift the tooltip horizontally to prevent it
- // from displaying too near to the edge of the screen.
- horizontalShift = computeHorizontalShift(windowWidth, xOffset, tooltipTargetWidth, tooltipWidth, manualShiftHorizontal);
-
- // Determine if we need to shift the pointer horizontally to prevent it from being too near to the edge of the tooltip
- // We shift it to the right a bit if the tooltip is positioned on the extreme left
- // and shift it to left a bit if the tooltip is positioned on the extreme right.
- horizontalShiftPointer =
- horizontalShift > 0
- ? Math.max(-horizontalShift, -(tooltipWidth / 2) + pointerWidth / 2 + variables.componentBorderRadiusSmall)
- : Math.min(-horizontalShift, tooltipWidth / 2 - pointerWidth / 2 - variables.componentBorderRadiusSmall);
-
// Because it uses fixed positioning, the top-left corner of the tooltip is aligned
// with the top-left corner of the window by default.
// we will use yOffset to position the tooltip relative to the Wrapped Component
@@ -184,10 +172,9 @@ const createTooltipStyleUtils: StyleUtilGenerator = (
// To shift the tooltip left, we'll give `left` a negative value.
//
// So we'll:
- // 1) Add the horizontal shift (left or right) computed above to keep it out of the gutters.
- // 2) Add the manual horizontal shift passed in as a parameter.
- // 3a) Horizontally align left: No need for shifting.
- // 3b) Horizontally align center:
+ // 1) Add the manual horizontal shift passed in as a parameter.
+ // 2a) Horizontally align left: No need for shifting.
+ // 2b) Horizontally align center:
// - Shift the tooltip right (+) to the center of the component,
// so the left edge lines up with the component center.
// - Shift it left (-) to by half the tooltip's width,
@@ -203,21 +190,38 @@ const createTooltipStyleUtils: StyleUtilGenerator = (
// so the pointer's center lines up with the tooltipWidth's center.
// - Remove the wrapper's horizontalShift to maintain the pointer
// at the center of the hovered component.
- rootWrapperLeft = xOffset + horizontalShift + manualShiftHorizontal;
+
+ rootWrapperLeft = xOffset + manualShiftHorizontal;
switch (anchorAlignment.horizontal) {
case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT:
pointerWrapperLeft = pointerWidth / 2;
break;
case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT:
- pointerWrapperLeft = horizontalShiftPointer + (tooltipWidth - pointerWidth * 1.5);
+ pointerWrapperLeft = tooltipWidth - pointerWidth * 1.5;
rootWrapperLeft += tooltipTargetWidth - tooltipWidth;
break;
case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER:
default:
- pointerWrapperLeft = horizontalShiftPointer + (tooltipWidth / 2 - pointerWidth / 2);
+ pointerWrapperLeft = tooltipWidth / 2 - pointerWidth / 2;
rootWrapperLeft += tooltipTargetWidth / 2 - tooltipWidth / 2;
}
+ // Determine if we need to shift the tooltip horizontally to prevent it
+ // from displaying too near to the edge of the screen.
+ horizontalShift = computeHorizontalShift(windowWidth, rootWrapperLeft, tooltipWidth);
+ // Add the horizontal shift (left or right) computed above to keep it out of the gutters.
+ rootWrapperLeft += horizontalShift;
+
+ // Determine if we need to shift the pointer horizontally to prevent it from being too near to the edge of the tooltip
+ // We shift it to the right a bit if the tooltip is positioned on the extreme left
+ // and shift it to left a bit if the tooltip is positioned on the extreme right.
+ horizontalShiftPointer =
+ horizontalShift > 0
+ ? Math.max(-horizontalShift, -(tooltipWidth / 2) + pointerWidth / 2 + variables.componentBorderRadiusSmall)
+ : Math.min(-horizontalShift, tooltipWidth / 2 - pointerWidth / 2 - variables.componentBorderRadiusSmall);
+
+ // Horizontally align left: No need for shifting.
+ pointerWrapperLeft += anchorAlignment.horizontal === CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT ? 0 : horizontalShiftPointer;
pointerAdditionalStyle = shouldShowBelow ? styles.flipUpsideDown : {};
// React Native's measure() is asynchronous, we temporarily hide the tooltip until its bound is calculated
diff --git a/src/types/form/WorkspaceConfirmationForm.tsx b/src/types/form/WorkspaceConfirmationForm.tsx
new file mode 100644
index 000000000000..8ae2261d018a
--- /dev/null
+++ b/src/types/form/WorkspaceConfirmationForm.tsx
@@ -0,0 +1,20 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ NAME: 'name',
+ CURRENCY: 'currency',
+} as const;
+
+type InputID = ValueOf;
+
+type WorkspaceConfirmationForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.NAME]: string;
+ [INPUT_IDS.CURRENCY]: string;
+ }
+>;
+
+export type {WorkspaceConfirmationForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index 162f84c7c861..51c1d545aacf 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -87,6 +87,7 @@ export type {WorkspaceCompanyCardFeedName} from './WorkspaceCompanyCardFeedName'
export type {SearchSavedSearchRenameForm} from './SearchSavedSearchRenameForm';
export type {WorkspaceCompanyCardEditName} from './WorkspaceCompanyCardEditName';
export type {PersonalDetailsForm} from './PersonalDetailsForm';
+export type {WorkspaceConfirmationForm} from './WorkspaceConfirmationForm';
export type {MoneyRequestTimeForm} from './MoneyRequestTimeForm';
export type {MoneyRequestSubrateForm} from './MoneyRequestSubrateForm';
export type {WorkspacePerDiemForm} from './WorkspacePerDiemForm';
diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts
index c72d4bf2a653..cbe3f10cbae8 100644
--- a/src/types/modules/react-native.d.ts
+++ b/src/types/modules/react-native.d.ts
@@ -8,7 +8,7 @@ import type StartupTimer from '@libs/StartupTimer/types';
type HybridAppModule = {
closeReactNativeApp: (shouldSignOut: boolean, shouldSetNVP: boolean) => void;
completeOnboarding: (status: boolean) => void;
- switchAccount: (newDotCurrentAccount: string) => void;
+ switchAccount: (newDotCurrentAccountEmail: string, authToken: string, policyID: string, accountID: string) => void;
exitApp: () => void;
};
diff --git a/src/types/onyx/Response.ts b/src/types/onyx/Response.ts
index ce9acbaaccc6..146cf327dd31 100644
--- a/src/types/onyx/Response.ts
+++ b/src/types/onyx/Response.ts
@@ -83,6 +83,12 @@ type Response = {
/** If there is newer data to load for pagination commands */
hasNewerActions?: boolean;
+
+ /** The email of the original user (returned when in delegate mode) */
+ requesterEmail?: string;
+
+ /** The ID of the original user (returned when in delegate mode) */
+ requesterID?: number;
};
export default Response;