diff --git a/.eslintrc.js b/.eslintrc.js index 5451cfff6534..0661183101ab 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -194,6 +194,7 @@ module.exports = { { selector: ['parameter', 'method'], format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allow', }, ], '@typescript-eslint/ban-types': [ diff --git a/android/app/build.gradle b/android/app/build.gradle index ee8aa986ddf2..b792f7830ea4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001045005 - versionName "1.4.50-5" + versionCode 1001045103 + versionName "1.4.51-3" } flavorDimensions "default" diff --git a/assets/fonts/web/ExpensifyNewKansas-Medium.woff b/assets/fonts/web/ExpensifyNewKansas-Medium.woff index bd842c5ecb1d..9e4258763f58 100644 Binary files a/assets/fonts/web/ExpensifyNewKansas-Medium.woff and b/assets/fonts/web/ExpensifyNewKansas-Medium.woff differ diff --git a/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 b/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 index dba1df7e971e..1f65d0df0fcb 100644 Binary files a/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 and b/assets/fonts/web/ExpensifyNewKansas-Medium.woff2 differ diff --git a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff index d3e7d9e82e15..5bab939ee71d 100644 Binary files a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff and b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff differ diff --git a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 index 94a0e04fa3b2..589edf3bc922 100644 Binary files a/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 and b/assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2 differ diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index ea18acef7c23..ec0f76801bc7 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -508,6 +508,7 @@ button { .info { padding: 12px; + margin-bottom: 20px; border-radius: 8px; background-color: $color-highlightBG; color: $color-text; diff --git a/docs/redirects.csv b/docs/redirects.csv index 3c89e920e3f7..df4e2a45dce3 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -65,3 +65,4 @@ https://help.expensify.com/articles/expensify-classic/expensify-billing/Pay-Per- https://help.expensify.com/articles/expensify-classic/expensify-billing/Individual-Subscription,https://use.expensify.com/ https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings +https://help.expensify.com/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager,https://use.expensify.com/support diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg index c4412cf650ee..2c5350cec2aa 100644 Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg index 999442b550da..bae3cd9f3e21 100644 Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 127f98edc070..ab5d359a5460 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.50 + 1.4.51 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.50.5 + 1.4.51.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 8a2fef8c99c6..ca9200c78376 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.50 + 1.4.51 CFBundleSignature ???? CFBundleVersion - 1.4.50.5 + 1.4.51.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 34ef35759e15..f20b520b1480 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.50 + 1.4.51 CFBundleVersion - 1.4.50.5 + 1.4.51.3 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 5cf96414d5f1..3083c15c0196 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.50-5", + "version": "1.4.51-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.50-5", + "version": "1.4.51-3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -52,7 +52,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -30570,8 +30570,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", - "integrity": "sha512-3d/JHWgeS+LFPRahCAXdLwnBYQk4XUYybtgCm7VsdmMDtCeGUTksLsEY7F1Zqm+ULqZjmCtYwAi8IPKy0fsSOw==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9", + "integrity": "sha512-v6UnN9yAW6p2996Fvd4AZnMRnisVfjg6ijWzUQue/6JsjSY+MW10oP74hSjD6x32fRrNmMctjy6d5a79bQFdPA==", "license": "MIT", "dependencies": { "classnames": "2.5.0", diff --git a/package.json b/package.json index 717a13e1c110..dc5b261c3b40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.50-5", + "version": "1.4.51-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -103,7 +103,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7bfd55f0ce75a37423119029fde58cfbe57086d9", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", diff --git a/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch b/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch index 877521094cd4..c65ebbb98007 100644 --- a/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch +++ b/patches/@react-navigation+stack+6.3.16+002+dontDetachScreen.patch @@ -43,7 +43,7 @@ index 7558eb3..b7bb75e 100644 }) : STATE_TRANSITIONING_OR_BELOW_TOP; } + -+ const isHomeScreenAndNotOnTop = (route.name === 'BottomTabNavigator' || route.name === 'Settings_Root') && isScreenActive !== STATE_ON_TOP; ++ const isHomeScreenAndNotOnTop = (route.name === 'BottomTabNavigator' || route.name === 'Workspace_Initial') && isScreenActive !== STATE_ON_TOP; + const { headerShown = true, diff --git a/patches/react-native-reanimated+3.7.1+001+fix-boost-dependency.patch b/patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch similarity index 100% rename from patches/react-native-reanimated+3.7.1+001+fix-boost-dependency.patch rename to patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch diff --git a/patches/react-native-reanimated+3.7.1.patch b/patches/react-native-reanimated+3.7.2.patch similarity index 100% rename from patches/react-native-reanimated+3.7.1.patch rename to patches/react-native-reanimated+3.7.2.patch diff --git a/src/CONST.ts b/src/CONST.ts index ae572d8c6e31..c8cafb889b72 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1661,6 +1661,8 @@ const CONST = { LOGIN_CHARACTER_LIMIT: 254, CATEGORY_NAME_LIMIT: 256, + TAG_NAME_LIMIT: 256, + TITLE_CHARACTER_LIMIT: 100, DESCRIPTION_LIMIT: 500, diff --git a/src/Expensify.tsx b/src/Expensify.tsx index f822862ec434..5681be838ca8 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -183,12 +183,12 @@ function Expensify({ // If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report Linking.getInitialURL().then((url) => { setInitialUrl(url); - Report.openReportFromDeepLink(url ?? '', isAuthenticated); + Report.openReportFromDeepLink(url ?? ''); }); // Open chat report from a deep link (only mobile native) Linking.addEventListener('url', (state) => { - Report.openReportFromDeepLink(state.url, isAuthenticated); + Report.openReportFromDeepLink(state.url); }); return () => { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b9e7c4d5d274..8c48cbad561f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -332,6 +332,8 @@ const ONYXKEYS = { WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', WORKSPACE_CATEGORY_CREATE_FORM: 'workspaceCategoryCreate', WORKSPACE_CATEGORY_CREATE_FORM_DRAFT: 'workspaceCategoryCreateDraft', + WORKSPACE_TAG_CREATE_FORM: 'workspaceTagCreate', + WORKSPACE_TAG_CREATE_FORM_DRAFT: 'workspaceTagCreateDraft', WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft', WORKSPACE_DESCRIPTION_FORM: 'workspaceDescriptionForm', WORKSPACE_DESCRIPTION_FORM_DRAFT: 'workspaceDescriptionFormDraft', @@ -416,6 +418,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_CREATE_FORM]: FormTypes.WorkspaceCategoryCreateForm; + [ONYXKEYS.FORMS.WORKSPACE_TAG_CREATE_FORM]: FormTypes.WorkspaceTagCreateForm; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d9f0c6658a2b..defb945ba8c2 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -450,140 +450,148 @@ const ROUTES = { WORKSPACE_NEW: 'workspace/new', WORKSPACE_NEW_ROOM: 'workspace/new-room', WORKSPACE_INITIAL: { - route: 'workspace/:policyID', - getRoute: (policyID: string) => `workspace/${policyID}` as const, + route: 'settings/workspaces/:policyID', + getRoute: (policyID: string) => `settings/workspaces/${policyID}` as const, }, WORKSPACE_INVITE: { - route: 'workspace/:policyID/invite', - getRoute: (policyID: string) => `workspace/${policyID}/invite` as const, + route: 'settings/workspaces/:policyID/invite', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/invite` as const, }, WORKSPACE_INVITE_MESSAGE: { - route: 'workspace/:policyID/invite-message', - getRoute: (policyID: string) => `workspace/${policyID}/invite-message` as const, + route: 'settings/workspaces/:policyID/invite-message', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/invite-message` as const, }, WORKSPACE_PROFILE: { - route: 'workspace/:policyID/profile', - getRoute: (policyID: string) => `workspace/${policyID}/profile` as const, + route: 'settings/workspaces/:policyID/profile', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile` as const, }, WORKSPACE_PROFILE_CURRENCY: { - route: 'workspace/:policyID/profile/currency', - getRoute: (policyID: string) => `workspace/${policyID}/profile/currency` as const, + route: 'settings/workspaces/:policyID/profile/currency', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/currency` as const, }, WORKSPACE_PROFILE_NAME: { - route: 'workspace/:policyID/profile/name', - getRoute: (policyID: string) => `workspace/${policyID}/profile/name` as const, + route: 'settings/workspaces/:policyID/profile/name', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/name` as const, }, WORKSPACE_PROFILE_DESCRIPTION: { - route: 'workspace/:policyID/profile/description', - getRoute: (policyID: string) => `workspace/${policyID}/profile/description` as const, + route: 'settings/workspaces/:policyID/profile/description', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/description` as const, }, WORKSPACE_PROFILE_SHARE: { - route: 'workspace/:policyID/profile/share', - getRoute: (policyID: string) => `workspace/${policyID}/profile/share` as const, + route: 'settings/workspaces/:policyID/profile/share', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/share` as const, }, WORKSPACE_AVATAR: { - route: 'workspace/:policyID/avatar', - getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const, + route: 'settings/workspaces/:policyID/avatar', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/avatar` as const, }, WORKSPACE_JOIN_USER: { - route: 'workspace/:policyID/join', - getRoute: (policyID: string, inviterEmail: string) => `workspace/${policyID}/join?email=${inviterEmail}` as const, + route: 'settings/workspaces/:policyID/join', + getRoute: (policyID: string, inviterEmail: string) => `settings/workspaces/${policyID}/join?email=${inviterEmail}` as const, }, WORKSPACE_SETTINGS_CURRENCY: { - route: 'workspace/:policyID/settings/currency', - getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const, + route: 'settings/workspaces/:policyID/settings/currency', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/currency` as const, }, WORKSPACE_WORKFLOWS: { - route: 'workspace/:policyID/workflows', - getRoute: (policyID: string) => `workspace/${policyID}/workflows` as const, + route: 'settings/workspaces/:policyID/workflows', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/workflows` as const, + }, + WORKSPACE_WORKFLOWS_PAYER: { + route: 'workspace/:policyID/settings/workflows/payer', + getRoute: (policyId: string) => `workspace/${policyId}/settings/workflows/payer` as const, }, WORKSPACE_WORKFLOWS_APPROVER: { - route: 'workspace/:policyID/settings/workflows/approver', - getRoute: (policyId: string) => `workspace/${policyId}/settings/workflows/approver` as const, + route: 'settings/workspaces/:policyID/settings/workflows/approver', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/workflows/approver` as const, }, WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY: { - route: 'workspace/:policyID/settings/workflows/auto-reporting-frequency', - getRoute: (policyID: string) => `workspace/${policyID}/settings/workflows/auto-reporting-frequency` as const, + route: 'settings/workspaces/:policyID/settings/workflows/auto-reporting-frequency', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/workflows/auto-reporting-frequency` as const, }, WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET: { - route: 'workspace/:policyID/settings/workflows/auto-reporting-frequency/monthly-offset', - getRoute: (policyID: string) => `workspace/${policyID}/settings/workflows/auto-reporting-frequency/monthly-offset` as const, + route: 'settings/workspaces/:policyID/settings/workflows/auto-reporting-frequency/monthly-offset', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/workflows/auto-reporting-frequency/monthly-offset` as const, }, WORKSPACE_CARD: { - route: 'workspace/:policyID/card', - getRoute: (policyID: string) => `workspace/${policyID}/card` as const, + route: 'settings/workspaces/:policyID/card', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/card` as const, }, WORKSPACE_REIMBURSE: { - route: 'workspace/:policyID/reimburse', - getRoute: (policyID: string) => `workspace/${policyID}/reimburse` as const, + route: 'settings/workspaces/:policyID/reimburse', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/reimburse` as const, }, WORKSPACE_RATE_AND_UNIT: { - route: 'workspace/:policyID/rateandunit', - getRoute: (policyID: string) => `workspace/${policyID}/rateandunit` as const, + route: 'settings/workspaces/:policyID/rateandunit', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/rateandunit` as const, }, WORKSPACE_RATE_AND_UNIT_RATE: { - route: 'workspace/:policyID/rateandunit/rate', - getRoute: (policyID: string) => `workspace/${policyID}/rateandunit/rate` as const, + route: 'settings/workspaces/:policyID/rateandunit/rate', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/rateandunit/rate` as const, }, WORKSPACE_RATE_AND_UNIT_UNIT: { - route: 'workspace/:policyID/rateandunit/unit', - getRoute: (policyID: string) => `workspace/${policyID}/rateandunit/unit` as const, + route: 'settings/workspaces/:policyID/rateandunit/unit', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/rateandunit/unit` as const, }, WORKSPACE_BILLS: { - route: 'workspace/:policyID/bills', - getRoute: (policyID: string) => `workspace/${policyID}/bills` as const, + route: 'settings/workspaces/:policyID/bills', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/bills` as const, }, WORKSPACE_INVOICES: { - route: 'workspace/:policyID/invoices', - getRoute: (policyID: string) => `workspace/${policyID}/invoices` as const, + route: 'settings/workspaces/:policyID/invoices', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/invoices` as const, }, WORKSPACE_TRAVEL: { - route: 'workspace/:policyID/travel', - getRoute: (policyID: string) => `workspace/${policyID}/travel` as const, + route: 'settings/workspaces/:policyID/travel', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/travel` as const, }, WORKSPACE_MEMBERS: { - route: 'workspace/:policyID/members', - getRoute: (policyID: string) => `workspace/${policyID}/members` as const, + route: 'settings/workspaces/:policyID/members', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/members` as const, }, WORKSPACE_CATEGORIES: { - route: 'workspace/:policyID/categories', - getRoute: (policyID: string) => `workspace/${policyID}/categories` as const, + route: 'settings/workspaces/:policyID/categories', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const, }, WORKSPACE_CATEGORY_SETTINGS: { - route: 'workspace/:policyID/categories/:categoryName', - getRoute: (policyID: string, categoryName: string) => `workspace/${policyID}/categories/${encodeURI(categoryName)}` as const, + route: 'settings/workspaces/:policyID/categories/:categoryName', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURI(categoryName)}` as const, }, WORKSPACE_CATEGORIES_SETTINGS: { - route: 'workspace/:policyID/categories/settings', - getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, + route: 'settings/workspaces/:policyID/categories/settings', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/settings` as const, }, WORKSPACE_MORE_FEATURES: { route: 'workspace/:policyID/more-features', getRoute: (policyID: string) => `workspace/${policyID}/more-features` as const, }, WORKSPACE_CATEGORY_CREATE: { - route: 'workspace/:policyID/categories/new', - getRoute: (policyID: string) => `workspace/${policyID}/categories/new` as const, + route: 'settings/workspaces/:policyID/categories/new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories/new` as const, }, WORKSPACE_TAGS: { - route: 'workspace/:policyID/tags', - getRoute: (policyID: string) => `workspace/${policyID}/tags` as const, + route: 'settings/workspaces/:policyID/tags', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags` as const, + }, + WORKSPACE_TAG_CREATE: { + route: 'settings/workspaces/:policyID/tags/new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/new` as const, }, WORKSPACE_TAGS_SETTINGS: { - route: 'workspace/:policyID/tags/settings', - getRoute: (policyID: string) => `workspace/${policyID}/tags/settings` as const, + route: 'settings/workspaces/:policyID/tags/settings', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/settings` as const, }, WORKSPACE_EDIT_TAGS: { - route: 'workspace/:policyID/tags/edit', - getRoute: (policyID: string) => `workspace/${policyID}/tags/edit` as const, + route: 'settings/workspaces/:policyID/tags/edit', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/edit` as const, }, WORKSPACE_MEMBER_DETAILS: { - route: 'workspace/:policyID/members/:accountID', - getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}`, backTo), + route: 'settings/workspaces/:policyID/members/:accountID', + getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/members/${accountID}`, backTo), }, WORKSPACE_MEMBER_ROLE_SELECTION: { - route: 'workspace/:policyID/members/:accountID/role-selection', - getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/members/${accountID}/role-selection`, backTo), + route: 'settings/workspaces/:policyID/members/:accountID/role-selection', + getRoute: (policyID: string, accountID: number, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/members/${accountID}/role-selection`, backTo), }, WORKSPACE_DISTANCE_RATES: { route: 'workspace/:policyID/distance-rates', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index a0e06b98da2b..4db5fd9115a5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -22,6 +22,7 @@ const SCREENS = { VALIDATE_LOGIN: 'ValidateLogin', UNLINK_LOGIN: 'UnlinkLogin', SETTINGS_CENTRAL_PANE: 'SettingsCentralPane', + WORKSPACES_CENTRAL_PANE: 'WorkspacesCentralPane', SETTINGS: { ROOT: 'Settings_Root', SHARE_CODE: 'Settings_Share_Code', @@ -218,8 +219,10 @@ const SCREENS = { TAGS: 'Workspace_Tags', TAGS_SETTINGS: 'Tags_Settings', TAGS_EDIT: 'Tags_Edit', + TAG_CREATE: 'Tag_Create', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', + WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver', WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', diff --git a/src/components/AvatarSkeleton.tsx b/src/components/AvatarSkeleton.tsx index 0887830aa07a..a6781448c3ba 100644 --- a/src/components/AvatarSkeleton.tsx +++ b/src/components/AvatarSkeleton.tsx @@ -1,21 +1,24 @@ import React from 'react'; import {Circle} from 'react-native-svg'; import useTheme from '@hooks/useTheme'; +import variables from '@styles/variables'; import SkeletonViewContentLoader from './SkeletonViewContentLoader'; function AvatarSkeleton() { const theme = useTheme(); + const skeletonCircleRadius = variables.componentSizeSmall / 2; + return ( ); diff --git a/src/components/AvatarWithIndicator.tsx b/src/components/AvatarWithIndicator.tsx index 2fd733d4b072..42b91b3d2d71 100644 --- a/src/components/AvatarWithIndicator.tsx +++ b/src/components/AvatarWithIndicator.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import * as UserUtils from '@libs/UserUtils'; +import CONST from '@src/CONST'; import Avatar from './Avatar'; import AvatarSkeleton from './AvatarSkeleton'; import * as Expensicons from './Icon/Expensicons'; @@ -33,6 +34,7 @@ function AvatarWithIndicator({source, tooltipText = '', fallbackIcon = Expensico ) : ( <> diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index 88938f31cd79..b9c52ad397ec 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -104,13 +104,13 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo }; return ( - - - + + + { fabPressable.current = el ?? null; @@ -136,9 +136,9 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo /> - - - + + + ); } diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index ad09b68a5f39..ee3b3607401e 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -394,3 +394,5 @@ export default withOnyx({ key: (props) => `${props.formID}Draft` as any, }, })(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode; + +export type {FormProviderProps}; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js b/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js deleted file mode 100644 index 2432d1b1748c..000000000000 --- a/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; -import {Animated} from 'react-native'; - -const propTypes = { - /** GrowlNotification content */ - children: PropTypes.node.isRequired, - - /** GrowlNotification Y postion, required to show or hide with fling animation */ - translateY: PropTypes.instanceOf(Animated.Value).isRequired, -}; - -export default propTypes; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.js deleted file mode 100644 index ccc404d415d7..000000000000 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useThemeStyles from '@hooks/useThemeStyles'; -import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes'; - -const propTypes = { - ...growlNotificationContainerPropTypes, - ...windowDimensionsPropTypes, -}; - -function GrowlNotificationContainer(props) { - const styles = useThemeStyles(); - return ( - - {props.children} - - ); -} - -GrowlNotificationContainer.propTypes = propTypes; -GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; - -export default withWindowDimensions(GrowlNotificationContainer); diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js deleted file mode 100644 index 207033f8fac2..000000000000 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes'; - -const propTypes = { - ...growlNotificationContainerPropTypes, -}; - -function GrowlNotificationContainer(props) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const insets = useSafeAreaInsets; - - return ( - - {props.children} - - ); -} - -GrowlNotificationContainer.propTypes = propTypes; -GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; - -export default GrowlNotificationContainer; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx new file mode 100644 index 000000000000..efd143c9487c --- /dev/null +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import {Animated} from 'react-native'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type GrowlNotificationContainerProps from './types'; + +function GrowlNotificationContainer({children, translateY}: GrowlNotificationContainerProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const insets = useSafeAreaInsets(); + + return {children}; +} + +GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; + +export default GrowlNotificationContainer; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx new file mode 100644 index 000000000000..3bbd0303906d --- /dev/null +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import {Animated} from 'react-native'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type GrowlNotificationContainerProps from './types'; + +function GrowlNotificationContainer({children, translateY}: GrowlNotificationContainerProps) { + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); + + return ( + + {children} + + ); +} + +GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; + +export default GrowlNotificationContainer; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/types.ts b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts new file mode 100644 index 000000000000..91a48437dbd9 --- /dev/null +++ b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts @@ -0,0 +1,8 @@ +import type {Animated} from 'react-native'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type GrowlNotificationContainerProps = ChildrenProps & { + translateY: Animated.Value; +}; + +export default GrowlNotificationContainerProps; diff --git a/src/components/GrowlNotification/index.js b/src/components/GrowlNotification/index.tsx similarity index 82% rename from src/components/GrowlNotification/index.js rename to src/components/GrowlNotification/index.tsx index ed0dd302f705..d0846dcf7a42 100644 --- a/src/components/GrowlNotification/index.js +++ b/src/components/GrowlNotification/index.tsx @@ -1,6 +1,8 @@ +import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {Animated, View} from 'react-native'; import {Directions, Gesture, GestureDetector} from 'react-native-gesture-handler'; +import type {SvgProps} from 'react-native-svg'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Pressables from '@components/Pressable'; @@ -8,6 +10,7 @@ import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Growl from '@libs/Growl'; +import type {GrowlRef} from '@libs/Growl'; import useNativeDriver from '@libs/useNativeDriver'; import CONST from '@src/CONST'; import GrowlNotificationContainer from './GrowlNotificationContainer'; @@ -16,15 +19,29 @@ const INACTIVE_POSITION_Y = -255; const PressableWithoutFeedback = Pressables.PressableWithoutFeedback; -function GrowlNotification(_, ref) { +function GrowlNotification(_: unknown, ref: ForwardedRef) { const translateY = useRef(new Animated.Value(INACTIVE_POSITION_Y)).current; const [bodyText, setBodyText] = useState(''); const [type, setType] = useState('success'); - const [duration, setDuration] = useState(); + const [duration, setDuration] = useState(); const theme = useTheme(); const styles = useThemeStyles(); - const types = { + type GrowlIconTypes = Record< + /** String representing the growl type, all type strings + * for growl notifications are stored in CONST.GROWL + */ + string, + { + /** Expensicon for the page */ + icon: React.FC; + + /** Color for the icon (should be from theme) */ + iconColor: string; + } + >; + + const types: GrowlIconTypes = { [CONST.GROWL.SUCCESS]: { icon: Expensicons.Checkmark, iconColor: theme.success, @@ -46,7 +63,7 @@ function GrowlNotification(_, ref) { * @param {String} type * @param {Number} duration */ - const show = useCallback((text, growlType, growlDuration) => { + const show = useCallback((text: string, growlType: string, growlDuration: number) => { setBodyText(text); setType(growlType); setDuration(growlDuration); @@ -61,7 +78,6 @@ function GrowlNotification(_, ref) { (val = INACTIVE_POSITION_Y) => { Animated.spring(translateY, { toValue: val, - duration: 80, useNativeDriver, }).start(); }, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index 5d8c0f6ef81e..0327b6bc6f56 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -1,3 +1,4 @@ +import Str from 'expensify-common/lib/str'; import cloneDeep from 'lodash/cloneDeep'; import isEmpty from 'lodash/isEmpty'; import React from 'react'; @@ -65,10 +66,11 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona // We need to remove the LTR unicode and leading @ from data as it is not part of the login displayNameOrLogin = tnodeClone.data.replace(CONST.UNICODE.LTR, '').slice(1); // We need to replace tnode.data here because we will pass it to TNodeChildrenRenderer below - asMutable(tnodeClone).data = tnodeClone.data.replace(displayNameOrLogin, getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID)); + asMutable(tnodeClone).data = tnodeClone.data.replace(displayNameOrLogin, Str.removeSMSDomain(getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID))); accountID = PersonalDetailsUtils.getAccountIDsByLogins([displayNameOrLogin])?.[0]; navigationRoute = ROUTES.DETAILS.getRoute(displayNameOrLogin); + displayNameOrLogin = Str.removeSMSDomain(displayNameOrLogin); } else { // If neither an account ID or email is provided, don't render anything return null; diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 3a1c35d46c94..21f3e9a3b605 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {Keyboard, StyleSheet, View} from 'react-native'; +import Avatar from '@components/Avatar'; import AvatarWithDisplayName from '@components/AvatarWithDisplayName'; import Header from '@components/Header'; import Icon from '@components/Icon'; @@ -32,7 +33,8 @@ function HeaderWithBackButton({ onThreeDotsButtonPress = () => {}, report = null, policy, - shouldShowAvatarWithDisplay = false, + policyAvatar, + shouldShowReportAvatarWithDisplay = false, shouldShowBackButton = true, shouldShowBorderBottom = false, shouldShowCloseButton = false, @@ -58,6 +60,7 @@ function HeaderWithBackButton({ shouldOverlay = false, singleExecution = (func) => func, shouldNavigateToTopMostReport = false, + style, }: HeaderWithBackButtonProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -81,6 +84,7 @@ function HeaderWithBackButton({ shouldShowBorderBottom && styles.borderBottom, shouldShowBackButton && styles.pl2, shouldOverlay && StyleSheet.absoluteFillObject, + style, ]} > @@ -118,7 +122,15 @@ function HeaderWithBackButton({ additionalStyles={[styles.mr2]} /> )} - {shouldShowAvatarWithDisplay ? ( + {policyAvatar && ( + + )} + {shouldShowReportAvatarWithDisplay ? ( & { /** Data to display a step counter in the header */ stepCounter?: StepCounterParams; - /** Whether we should show an avatar */ - shouldShowAvatarWithDisplay?: boolean; + /** Whether we should show a report avatar */ + shouldShowReportAvatarWithDisplay?: boolean; /** Parent report, if provided it will override props.report for AvatarWithDisplay */ parentReport?: OnyxEntry; @@ -121,6 +123,12 @@ type HeaderWithBackButtonProps = Partial & { /** Whether we should overlay the 3 dots menu */ shouldOverlayDots?: boolean; + + /** Policy avatar to display in the header */ + policyAvatar?: Icon; + + /** Additional styles to add to the component */ + style?: StyleProp; }; export type {ThreeDotsMenuItem}; diff --git a/src/components/Indicator.tsx b/src/components/Indicator.tsx index 1420a6abe189..e3d226a17999 100644 --- a/src/components/Indicator.tsx +++ b/src/components/Indicator.tsx @@ -1,17 +1,24 @@ import React from 'react'; import {StyleSheet, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as UserUtils from '@libs/UserUtils'; import * as PaymentMethods from '@userActions/PaymentMethods'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {BankAccountList, FundList, LoginList, UserWallet, WalletTerms} from '@src/types/onyx'; +import type {BankAccountList, FundList, LoginList, Policy, PolicyMembers, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx'; type CheckingMethod = () => boolean; type IndicatorOnyxProps = { + /** The employee list of all policies (coming from Onyx) */ + allPolicyMembers: OnyxCollection; + + /** All the user's policies (from Onyx via withFullPolicy) */ + policies: OnyxCollection; + /** List of bank accounts */ bankAccountList: OnyxEntry; @@ -21,6 +28,9 @@ type IndicatorOnyxProps = { /** The user's wallet (coming from Onyx) */ userWallet: OnyxEntry; + /** Bank account attached to free plan */ + reimbursementAccount: OnyxEntry; + /** Information about the user accepting the terms for payments */ walletTerms: OnyxEntry; @@ -30,16 +40,25 @@ type IndicatorOnyxProps = { type IndicatorProps = IndicatorOnyxProps; -function Indicator({bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) { +function Indicator({reimbursementAccount, allPolicyMembers, policies, bankAccountList, fundList, userWallet, walletTerms, loginList}: IndicatorOnyxProps) { const theme = useTheme(); const styles = useThemeStyles(); + // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and + // those should be cleaned out before doing any error checking + const cleanPolicies = Object.fromEntries(Object.entries(policies ?? {}).filter(([, policy]) => policy?.id)); + const cleanAllPolicyMembers = Object.fromEntries(Object.entries(allPolicyMembers ?? {}).filter(([, policyMembers]) => !!policyMembers)); + // All of the error & info-checking methods are put into an array. This is so that using _.some() will return // early as soon as the first error / info condition is returned. This makes the checks very efficient since // we only care if a single error / info condition exists anywhere. const errorCheckingMethods: CheckingMethod[] = [ () => Object.keys(userWallet?.errors ?? {}).length > 0, () => PaymentMethods.hasPaymentMethodError(bankAccountList, fundList), + () => Object.values(cleanPolicies).some(PolicyUtils.hasPolicyError), + () => Object.values(cleanPolicies).some(PolicyUtils.hasCustomUnitsError), + () => Object.values(cleanAllPolicyMembers).some(PolicyUtils.hasPolicyMemberError), + () => Object.keys(reimbursementAccount?.errors ?? {}).length > 0, () => !!loginList && UserUtils.hasLoginListError(loginList), // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) @@ -58,9 +77,19 @@ function Indicator({bankAccountList, fundList, userWallet, walletTerms, loginLis Indicator.displayName = 'Indicator'; export default withOnyx({ + allPolicyMembers: { + key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, bankAccountList: { key: ONYXKEYS.BANK_ACCOUNT_LIST, }, + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, fundList: { key: ONYXKEYS.FUND_LIST, }, diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx index 5c672cf7cab6..08e7613dc7a9 100644 --- a/src/components/Lottie/index.tsx +++ b/src/components/Lottie/index.tsx @@ -4,6 +4,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef} from 'react'; import {View} from 'react-native'; import type DotLottieAnimation from '@components/LottieAnimations/types'; +import useAppState from '@hooks/useAppState'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -12,6 +13,7 @@ type Props = { } & Omit; function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef) { + const appState = useAppState(); const styles = useThemeStyles(); const [isError, setIsError] = React.useState(false); @@ -19,8 +21,10 @@ function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef; } diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index e0d6c39623ed..74fec2c606af 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -97,7 +97,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money return ( { // We don't want to activate pinch gesture when we are swiping in the pager if (!shouldDisableTransformationGestures.value) { diff --git a/src/components/MultiGestureCanvas/useTapGestures.ts b/src/components/MultiGestureCanvas/useTapGestures.ts index a28333725d6e..f550e93d6be2 100644 --- a/src/components/MultiGestureCanvas/useTapGestures.ts +++ b/src/components/MultiGestureCanvas/useTapGestures.ts @@ -121,7 +121,6 @@ const useTapGestures = ({ const doubleTapGesture = Gesture.Tap() // The first argument is not used, but must be defined - // eslint-disable-next-line @typescript-eslint/naming-convention .onTouchesDown((_evt, state) => { if (!shouldDisableTransformationGestures.value) { return; @@ -156,7 +155,6 @@ const useTapGestures = ({ .onBegin(() => { stopAnimation(); }) - // eslint-disable-next-line @typescript-eslint/naming-convention .onFinalize((_evt, success) => { if (!success || onTap === undefined) { return; diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 575df128894a..3844080c6f5d 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -133,7 +133,6 @@ function BaseOptionsList( * * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] */ - // eslint-disable-next-line @typescript-eslint/naming-convention const getItemLayout = (_data: OptionsListData[] | null, flatDataArrayIndex: number) => { if (!flattenedData.current[flatDataArrayIndex]) { flattenedData.current = buildFlatSectionArray(); diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 2694e2b83f7f..40fb1115ac36 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -655,10 +655,9 @@ class BaseOptionsSelector extends Component { )} {this.props.shouldShowReferralCTA && ( - + + + )} {shouldShowFooter && ( diff --git a/src/components/RadioButtonWithLabel.tsx b/src/components/RadioButtonWithLabel.tsx index cfcd6acba41f..9b93d7900772 100644 --- a/src/components/RadioButtonWithLabel.tsx +++ b/src/components/RadioButtonWithLabel.tsx @@ -72,3 +72,5 @@ function RadioButtonWithLabel({LabelComponent, style, label = '', hasError = fal RadioButtonWithLabel.displayName = 'RadioButtonWithLabel'; export default RadioButtonWithLabel; + +export type {RadioButtonWithLabelProps}; diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index c5a91b3fdceb..6db37ce1320a 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; @@ -26,11 +25,9 @@ type ReferralProgramCTAProps = ReferralProgramCTAOnyxProps & { | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND; - - style?: StyleProp; }; -function ReferralProgramCTA({referralContentType, style, dismissedReferralBanners}: ReferralProgramCTAProps) { +function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: ReferralProgramCTAProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); @@ -48,7 +45,7 @@ function ReferralProgramCTA({referralContentType, style, dismissedReferralBanner onPress={() => { Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(referralContentType, Navigation.getActiveRouteWithoutParams())); }} - style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5, style]} + style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5]} accessibilityLabel="referral" role={CONST.ACCESSIBILITY_ROLE.BUTTON} > diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 743bfd8fff88..f1aa1751dd84 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -1,8 +1,8 @@ import React, {useMemo} from 'react'; -import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -29,7 +29,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy, Report, ReportAction, Transaction, TransactionViolations} from '@src/types/onyx'; +import type {Policy, Report, ReportAction, Transaction, TransactionViolations, UserWallet} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ReportActionItemImages from './ReportActionItemImages'; @@ -48,6 +48,9 @@ type ReportPreviewOnyxProps = { /** All of the transaction violations */ transactionViolations: OnyxCollection; + + /** The user's wallet account */ + userWallet: OnyxEntry; }; type ReportPreviewProps = ReportPreviewOnyxProps & { @@ -94,6 +97,7 @@ function ReportPreview({ isHovered = false, isWhisper = false, checkIfContextMenuActive = () => {}, + userWallet, }: ReportPreviewProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -206,6 +210,9 @@ function ReportPreview({ const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID); + const shouldShowRBR = !iouSettled && hasErrors; + /* Show subtitle if at least one of the money requests is not being smart scanned, and either: - There is more than one money request – in this case, the "X requests, Y scanning" subtitle is shown; @@ -251,12 +258,19 @@ function ReportPreview({ {getPreviewMessage()} - {!iouSettled && hasErrors && ( + {shouldShowRBR && ( )} + + {!shouldShowRBR && shouldPromptUserToAddBankAccount && ( + + )} @@ -338,4 +352,7 @@ export default withOnyx({ transactionViolations: { key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, })(ReportPreview); diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 306846ad7d99..827eec8088a6 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -148,7 +148,6 @@ function ScreenWrapper( const panResponder = useRef( PanResponder.create({ - // eslint-disable-next-line @typescript-eslint/naming-convention onStartShouldSetPanResponderCapture: (_e, gestureState) => gestureState.numberActiveTouches === CONST.TEST_TOOL.NUMBER_OF_TAPS, onPanResponderRelease: toggleTestToolsModal, }), @@ -156,7 +155,6 @@ function ScreenWrapper( const keyboardDissmissPanResponder = useRef( PanResponder.create({ - // eslint-disable-next-line @typescript-eslint/naming-convention onMoveShouldSetPanResponderCapture: (_e, gestureState) => { const isHorizontalSwipe = Math.abs(gestureState.dx) > Math.abs(gestureState.dy); const shouldDismissKeyboard = shouldDismissKeyboardBeforeClose && isKeyboardShown && Browser.isMobile(); diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 9dc3e4aa2c7e..db6204c8c1ef 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -1,7 +1,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import isEmpty from 'lodash/isEmpty'; import type {ForwardedRef} from 'react'; -import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native'; import {View} from 'react-native'; import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; @@ -23,7 +23,7 @@ import Log from '@libs/Log'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, ListItem, Section, SectionListDataType} from './types'; +import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, ListItem, Section, SectionListDataType, SelectionListHandle} from './types'; function BaseSelectionList( { @@ -66,13 +66,14 @@ function BaseSelectionList( customListHeader, listHeaderWrapperStyle, isRowMultilineSupported = false, + textInputRef, }: BaseSelectionListProps, - inputRef: ForwardedRef, + ref: ForwardedRef, ) { const styles = useThemeStyles(); const {translate} = useLocalize(); const listRef = useRef>>(null); - const textInputRef = useRef(null); + const innerTextInputRef = useRef(null); const focusTimeoutRef = useRef(null); const shouldShowTextInput = !!textInputLabel; const shouldShowSelectAll = !!onSelectAll; @@ -80,6 +81,8 @@ function BaseSelectionList( const isFocused = useIsFocused(); const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); const [isInitialSectionListRender, setIsInitialSectionListRender] = useState(true); + const [itemsToHighlight, setItemsToHighlight] = useState | null>(null); + const itemFocusTimeoutRef = useRef(null); const [currentPage, setCurrentPage] = useState(1); const incrementPage = () => setCurrentPage((prev) => prev + 1); @@ -235,16 +238,16 @@ function BaseSelectionList( onSelectRow(item); - if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && textInputRef.current) { - textInputRef.current.focus(); + if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && innerTextInputRef.current) { + innerTextInputRef.current.focus(); } }; const selectAllRow = () => { onSelectAll?.(); - if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && textInputRef.current) { - textInputRef.current.focus(); + if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && innerTextInputRef.current) { + innerTextInputRef.current.focus(); } }; @@ -310,7 +313,7 @@ function BaseSelectionList( const indexOffset = section.indexOffset ? section.indexOffset : 0; const normalizedIndex = index + indexOffset; const isDisabled = !!section.isDisabled || item.isDisabled; - const isItemFocused = !isDisabled && focusedIndex === normalizedIndex; + const isItemFocused = !isDisabled && (focusedIndex === normalizedIndex || itemsToHighlight?.has(item.keyForList ?? '')); // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const showTooltip = shouldShowTooltips && normalizedIndex < 10; @@ -370,10 +373,10 @@ function BaseSelectionList( useCallback(() => { if (shouldShowTextInput) { focusTimeoutRef.current = setTimeout(() => { - if (!textInputRef.current) { + if (!innerTextInputRef.current) { return; } - textInputRef.current.focus(); + innerTextInputRef.current.focus(); }, CONST.ANIMATED_TRANSITION); } return () => { @@ -399,6 +402,46 @@ function BaseSelectionList( updateAndScrollToFocusedIndex(newSelectedIndex); }, [canSelectMultiple, flattenedSections.allOptions.length, prevTextInputValue, textInputValue, updateAndScrollToFocusedIndex]); + useEffect( + () => () => { + if (!itemFocusTimeoutRef.current) { + return; + } + clearTimeout(itemFocusTimeoutRef.current); + }, + [], + ); + + /** + * Highlights the items and scrolls to the first item present in the items list. + * + * @param items - The list of items to highlight. + * @param timeout - The timeout in milliseconds before removing the highlight. + */ + const scrollAndHighlightItem = useCallback( + (items: string[], timeout: number) => { + const newItemsToHighlight = new Set(); + items.forEach((item) => { + newItemsToHighlight.add(item); + }); + const index = flattenedSections.allOptions.findIndex((option) => newItemsToHighlight.has(option.keyForList ?? '')); + updateAndScrollToFocusedIndex(index); + setItemsToHighlight(newItemsToHighlight); + + if (itemFocusTimeoutRef.current) { + clearTimeout(itemFocusTimeoutRef.current); + } + + itemFocusTimeoutRef.current = setTimeout(() => { + setFocusedIndex(-1); + setItemsToHighlight(null); + }, timeout); + }, + [flattenedSections.allOptions, updateAndScrollToFocusedIndex], + ); + + useImperativeHandle(ref, () => ({scrollAndHighlightItem}), [scrollAndHighlightItem]); + /** Selects row when pressing Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { captureOnInputs: true, @@ -428,15 +471,14 @@ function BaseSelectionList( { - textInputRef.current = element as RNTextInput; + innerTextInputRef.current = element as RNTextInput; - if (!inputRef) { + if (!textInputRef) { return; } - if (typeof inputRef === 'function') { - inputRef(element as RNTextInput); - } + // eslint-disable-next-line no-param-reassign + textInputRef.current = element as RNTextInput; }} label={textInputLabel} accessibilityLabel={textInputLabel} diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx index 882ca56076bd..2a3a8dd04a79 100644 --- a/src/components/SelectionList/UserListItem.tsx +++ b/src/components/SelectionList/UserListItem.tsx @@ -1,3 +1,4 @@ +import Str from 'expensify-common/lib/str'; import React from 'react'; import {View} from 'react-native'; import MultipleAvatars from '@components/MultipleAvatars'; @@ -81,7 +82,7 @@ function UserListItem({ )} diff --git a/src/components/SelectionList/index.android.tsx b/src/components/SelectionList/index.android.tsx index 46f2af8356f6..f8e54b219f5b 100644 --- a/src/components/SelectionList/index.android.tsx +++ b/src/components/SelectionList/index.android.tsx @@ -1,11 +1,10 @@ import React, {forwardRef} from 'react'; import type {ForwardedRef} from 'react'; import {Keyboard} from 'react-native'; -import type {TextInput} from 'react-native'; import BaseSelectionList from './BaseSelectionList'; -import type {BaseSelectionListProps, ListItem} from './types'; +import type {BaseSelectionListProps, ListItem, SelectionListHandle} from './types'; -function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { return ( (props: BaseSelectionListProps, ref: ForwardedRef) { +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { return ( // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/SelectionList/index.tsx b/src/components/SelectionList/index.tsx index 2446e1b4f5c1..a6fd636cc215 100644 --- a/src/components/SelectionList/index.tsx +++ b/src/components/SelectionList/index.tsx @@ -1,12 +1,11 @@ import React, {forwardRef, useEffect, useState} from 'react'; import type {ForwardedRef} from 'react'; import {Keyboard} from 'react-native'; -import type {TextInput} from 'react-native'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import BaseSelectionList from './BaseSelectionList'; -import type {BaseSelectionListProps, ListItem} from './types'; +import type {BaseSelectionListProps, ListItem, SelectionListHandle} from './types'; -function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { const [isScreenTouched, setIsScreenTouched] = useState(false); const touchStart = () => setIsScreenTouched(true); diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index a9cd3dacc1a7..152a44996fea 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,5 +1,5 @@ -import type {ReactElement, ReactNode} from 'react'; -import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {MutableRefObject, ReactElement, ReactNode} from 'react'; +import type {GestureResponderEvent, InputModeOptions, LayoutChangeEvent, SectionListData, StyleProp, TextInput, TextStyle, ViewStyle} from 'react-native'; import type {MaybePhraseKey} from '@libs/Localize'; import type CONST from '@src/CONST'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -284,6 +284,13 @@ type BaseSelectionListProps = Partial & { /** Whether to wrap long text up to 2 lines */ isRowMultilineSupported?: boolean; + + /** Ref for textInput */ + textInputRef?: MutableRefObject; +}; + +type SelectionListHandle = { + scrollAndHighlightItem?: (items: string[], timeout: number) => void; }; type ItemLayout = { @@ -317,4 +324,5 @@ export type { ItemLayout, ButtonOrCheckBoxRoles, SectionListDataType, + SelectionListHandle, }; diff --git a/src/components/SwipeableView/index.native.tsx b/src/components/SwipeableView/index.native.tsx index 67c6695c1a7f..e5b6d371e606 100644 --- a/src/components/SwipeableView/index.native.tsx +++ b/src/components/SwipeableView/index.native.tsx @@ -9,7 +9,6 @@ function SwipeableView({children, onSwipeDown}: SwipeableViewProps) { const panResponder = useRef( PanResponder.create({ // The PanResponder gets focus only when the y-axis movement is over minimumPixelDistance & swipe direction is downwards - // eslint-disable-next-line @typescript-eslint/naming-convention onMoveShouldSetPanResponderCapture: (_event, gestureState) => { if (gestureState.dy - oldYRef.current > 0 && gestureState.dy > minimumPixelDistance) { return true; diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx index 963782bb50d5..a94f54682c85 100644 --- a/src/components/WorkspaceSwitcherButton.tsx +++ b/src/components/WorkspaceSwitcherButton.tsx @@ -1,13 +1,11 @@ import React, {useMemo} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; import * as Expensicons from './Icon/Expensicons'; @@ -19,14 +17,14 @@ type WorkspaceSwitcherButtonOnyxProps = { policy: OnyxEntry; }; -type WorkspaceSwitcherButtonProps = {activeWorkspaceID?: string} & WorkspaceSwitcherButtonOnyxProps; +type WorkspaceSwitcherButtonProps = WorkspaceSwitcherButtonOnyxProps; -function WorkspaceSwitcherButton({activeWorkspaceID, policy}: WorkspaceSwitcherButtonProps) { +function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { const {translate} = useLocalize(); const theme = useTheme(); const {source, name, type} = useMemo(() => { - if (!activeWorkspaceID) { + if (!policy) { return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR}; } @@ -36,7 +34,7 @@ function WorkspaceSwitcherButton({activeWorkspaceID, policy}: WorkspaceSwitcherB name: policy?.name ?? '', type: CONST.ICON_TYPE_WORKSPACE, }; - }, [policy, activeWorkspaceID]); + }, [policy]); return ( @@ -71,8 +69,4 @@ function WorkspaceSwitcherButton({activeWorkspaceID, policy}: WorkspaceSwitcherB WorkspaceSwitcherButton.displayName = 'WorkspaceSwitcherButton'; -export default withOnyx({ - policy: { - key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`, - }, -})(WorkspaceSwitcherButton); +export default WorkspaceSwitcherButton; diff --git a/src/hooks/useAppState/index.native.ts b/src/hooks/useAppState/index.native.ts new file mode 100644 index 000000000000..39c1dff65e7c --- /dev/null +++ b/src/hooks/useAppState/index.native.ts @@ -0,0 +1,28 @@ +import React from 'react'; +import type {AppStateStatus} from 'react-native'; +import {AppState} from 'react-native'; +import type AppStateType from './types'; + +function useAppState() { + const [appState, setAppState] = React.useState({ + isForeground: AppState.currentState === 'active', + isInactive: AppState.currentState === 'inactive', + isBackground: AppState.currentState === 'background', + }); + + React.useEffect(() => { + function handleAppStateChange(nextAppState: AppStateStatus) { + setAppState({ + isForeground: nextAppState === 'active', + isInactive: nextAppState === 'inactive', + isBackground: nextAppState === 'background', + }); + } + const subscription = AppState.addEventListener('change', handleAppStateChange); + return () => subscription.remove(); + }, []); + + return appState; +} + +export default useAppState; diff --git a/src/hooks/useAppState/index.ts b/src/hooks/useAppState/index.ts new file mode 100644 index 000000000000..74d68535d1d1 --- /dev/null +++ b/src/hooks/useAppState/index.ts @@ -0,0 +1,8 @@ +import type AppStateType from './types'; + +function useAppState(): AppStateType { + // Since there's no AppState in web, we'll always return isForeground as true + return {isForeground: true, isInactive: false, isBackground: false}; +} + +export default useAppState; diff --git a/src/hooks/useAppState/types.ts b/src/hooks/useAppState/types.ts new file mode 100644 index 000000000000..9093b89fc4dc --- /dev/null +++ b/src/hooks/useAppState/types.ts @@ -0,0 +1,7 @@ +type AppStateType = { + isForeground: boolean; + isInactive: boolean; + isBackground: boolean; +}; + +export default AppStateType; diff --git a/src/hooks/useStyledSafeAreaInsets.ts b/src/hooks/useStyledSafeAreaInsets.ts deleted file mode 100644 index bfd9c32a46ae..000000000000 --- a/src/hooks/useStyledSafeAreaInsets.ts +++ /dev/null @@ -1,37 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import useStyleUtils from './useStyleUtils'; - -/** - * Custom hook to get the styled safe area insets. - * This hook utilizes the `SafeAreaInsetsContext` to retrieve the current safe area insets - * and applies styling adjustments using the `useStyleUtils` hook. - * - * @returns An object containing the styled safe area insets and additional styles. - * @returns .paddingTop The top padding adjusted for safe area. - * @returns .paddingBottom The bottom padding adjusted for safe area. - * @returns .insets The safe area insets object or undefined if not available. - * @returns .safeAreaPaddingBottomStyle An object containing the bottom padding style adjusted for safe area. - * - * @example - * // How to use this hook in a component - * function MyComponent() { - * const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useStyledSafeAreaInsets(); - * - * // Use these values to style your component accordingly - * } - */ -function useStyledSafeAreaInsets() { - const StyleUtils = useStyleUtils(); - const insets = useSafeAreaInsets(); - - const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets ?? undefined); - return { - paddingTop, - paddingBottom, - insets: insets ?? undefined, - safeAreaPaddingBottomStyle: {paddingBottom}, - }; -} - -export default useStyledSafeAreaInsets; diff --git a/src/languages/en.ts b/src/languages/en.ts index ff91a4f6f205..7e442eee2236 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1072,6 +1072,11 @@ export default { }, }, }, + workflowsPayerPage: { + title: 'Authorized payer', + genericErrorMessage: 'The authorized payer could not be changed. Please try again.', + admins: 'Admins', + }, reportFraudPage: { title: 'Report virtual card fraud', description: 'If your virtual card details have been stolen or compromised, we’ll permanently deactivate your existing card and provide you with a new virtual card and number.', @@ -1829,11 +1834,14 @@ export default { requiresTag: 'Members must tag all spend', customTagName: 'Custom tag name', enableTag: 'Enable tag', + addTag: 'Add tag', subtitle: 'Tags add more detailed ways to classify costs.', emptyTags: { title: "You haven't created any tags", subtitle: 'Add a tag to track projects, locations, departments, and more.', }, + tagRequiredError: 'Tag name is required.', + existingTagError: 'A tag with this name already exists.', genericFailureMessage: 'An error occurred while updating the tag, please try again.', }, emptyWorkspace: { diff --git a/src/languages/es.ts b/src/languages/es.ts index c21f46ed8853..267581f043ee 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1068,6 +1068,11 @@ export default { }, }, }, + workflowsPayerPage: { + title: 'Pagador autorizado', + genericErrorMessage: 'El pagador autorizado no se pudo cambiar. Por favor, inténtalo mas tarde.', + admins: 'Administradores', + }, reportFraudPage: { title: 'Reportar fraude con la tarjeta virtual', description: @@ -1853,11 +1858,14 @@ export default { requiresTag: 'Los miembros deben etiquetar todos los gastos', customTagName: 'Nombre de etiqueta personalizada', enableTag: 'Habilitar etiqueta', + addTag: 'Añadir etiqueta', subtitle: 'Las etiquetas añaden formas más detalladas de clasificar los costos.', emptyTags: { title: 'No has creado ninguna etiqueta', subtitle: 'Añade una etiqueta para realizar el seguimiento de proyectos, ubicaciones, departamentos y otros.', }, + tagRequiredError: 'Lo nombre de la etiqueta es obligatorio.', + existingTagError: 'Ya existe una etiqueta con este nombre.', genericFailureMessage: 'Se produjo un error al actualizar la etiqueta, inténtelo nuevamente.', }, emptyWorkspace: { diff --git a/src/libs/API/parameters/CreatePolicyTagsParams.ts b/src/libs/API/parameters/CreatePolicyTagsParams.ts new file mode 100644 index 000000000000..6fd16d9ca87b --- /dev/null +++ b/src/libs/API/parameters/CreatePolicyTagsParams.ts @@ -0,0 +1,10 @@ +type CreatePolicyTagsParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array<{name: string;}> + */ + tags: string; +}; + +export default CreatePolicyTagsParams; diff --git a/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts b/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts new file mode 100644 index 000000000000..eea0788b3927 --- /dev/null +++ b/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts @@ -0,0 +1,5 @@ +type OpenPolicyWorkflowsPageParams = { + policyID: string; +}; + +export default OpenPolicyWorkflowsPageParams; diff --git a/src/libs/API/parameters/ReconnectAppParams.ts b/src/libs/API/parameters/ReconnectAppParams.ts index 8c5b7d6c0da9..d8c1da4f0887 100644 --- a/src/libs/API/parameters/ReconnectAppParams.ts +++ b/src/libs/API/parameters/ReconnectAppParams.ts @@ -2,6 +2,7 @@ type ReconnectAppParams = { mostRecentReportActionLastModified?: string; updateIDFrom?: number; policyIDList: string[]; + idempotencyKey?: string; }; export default ReconnectAppParams; diff --git a/src/libs/API/parameters/SetWorkspacePayerParams.ts b/src/libs/API/parameters/SetWorkspacePayerParams.ts new file mode 100644 index 000000000000..d1c976c31dd3 --- /dev/null +++ b/src/libs/API/parameters/SetWorkspacePayerParams.ts @@ -0,0 +1,6 @@ +type SetWorkspacePayerParams = { + policyID: string; + reimburserEmail: string; +}; + +export default SetWorkspacePayerParams; diff --git a/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts b/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts new file mode 100644 index 000000000000..f96f6385f541 --- /dev/null +++ b/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts @@ -0,0 +1,9 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type SetWorkspaceReimbursementParams = { + policyID: string; + reimbursementChoice: ValueOf; +}; + +export default SetWorkspaceReimbursementParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 7e0e9b6e4a96..687f32f5b6de 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -156,6 +156,8 @@ export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAut export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams'; export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams'; export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; +export type {default as SetWorkspacePayerParams} from './SetWorkspacePayerParams'; +export type {default as SetWorkspaceReimbursementParams} from './SetWorkspaceReimbursementParams'; export type {default as SetPolicyRequiresTag} from './SetPolicyRequiresTag'; export type {default as RenamePolicyTaglist} from './RenamePolicyTaglist'; export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams'; @@ -169,4 +171,6 @@ export type {default as EnablePolicyReportFieldsParams} from './EnablePolicyRepo export type {default as AcceptJoinRequestParams} from './AcceptJoinRequest'; export type {default as DeclineJoinRequestParams} from './DeclineJoinRequest'; export type {default as JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink'; +export type {default as OpenPolicyWorkflowsPageParams} from './OpenPolicyWorkflowsPageParams'; export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams'; +export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index bf215e09e37d..17c38b252e0b 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -12,6 +12,8 @@ const WRITE_COMMANDS = { SET_WORKSPACE_AUTO_REPORTING_FREQUENCY: 'SetWorkspaceAutoReportingFrequency', SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET: 'UpdatePolicy', SET_WORKSPACE_APPROVAL_MODE: 'SetWorkspaceApprovalMode', + SET_WORKSPACE_PAYER: 'SetWorkspacePayer', + SET_WORKSPACE_REIMBURSEMENT: 'SetWorkspaceReimbursement', DISMISS_REFERRAL_BANNER: 'DismissReferralBanner', UPDATE_PREFERRED_LOCALE: 'UpdatePreferredLocale', OPEN_APP: 'OpenApp', @@ -116,6 +118,7 @@ const WRITE_COMMANDS = { CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment', SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled', CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories', + CREATE_POLICY_TAG: 'CreatePolicyTag', SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory', SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag', RENAME_POLICY_TAG_LIST: 'RenamePolicyTaglist', @@ -281,6 +284,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag; [WRITE_COMMANDS.RENAME_POLICY_TAG_LIST]: Parameters.RenamePolicyTaglist; + [WRITE_COMMANDS.CREATE_POLICY_TAG]: Parameters.CreatePolicyTagsParams; [WRITE_COMMANDS.CREATE_TASK]: Parameters.CreateTaskParams; [WRITE_COMMANDS.CANCEL_TASK]: Parameters.CancelTaskParams; [WRITE_COMMANDS.EDIT_TASK_ASSIGNEE]: Parameters.EditTaskAssigneeParams; @@ -325,6 +329,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_FREQUENCY]: Parameters.SetWorkspaceAutoReportingFrequencyParams; [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams; [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams; + [WRITE_COMMANDS.SET_WORKSPACE_PAYER]: Parameters.SetWorkspacePayerParams; + [WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT]: Parameters.SetWorkspaceReimbursementParams; [WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams; [WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES]: Parameters.EnablePolicyCategoriesParams; [WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS]: Parameters.EnablePolicyConnectionsParams; @@ -370,6 +376,7 @@ const READ_COMMANDS = { OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage', OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage', OPEN_DRAFT_WORKSPACE_REQUEST: 'OpenDraftWorkspaceRequest', + OPEN_POLICY_WORKFLOWS_PAGE: 'OpenPolicyWorkflowsPage', OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage', } as const; @@ -407,6 +414,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_TAGS_PAGE]: Parameters.OpenPolicyTagsPageParams; [READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams; [READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams; + [READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE]: Parameters.OpenPolicyWorkflowsPageParams; [READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams; }; diff --git a/src/libs/DoInteractionTask/index.ts b/src/libs/DoInteractionTask/index.ts index c53f08be88dc..0eadb8f7dbee 100644 --- a/src/libs/DoInteractionTask/index.ts +++ b/src/libs/DoInteractionTask/index.ts @@ -1,8 +1,7 @@ -import DomUtils from '@libs/DomUtils'; import type DoInteractionTask from './types'; const doInteractionTask: DoInteractionTask = (callback) => { - DomUtils.requestAnimationFrame(callback); + callback(); return null; }; diff --git a/src/libs/Growl.ts b/src/libs/Growl.ts index 55bcf88206e9..3812a155ba1f 100644 --- a/src/libs/Growl.ts +++ b/src/libs/Growl.ts @@ -50,4 +50,6 @@ export default { success, }; +export type {GrowlRef}; + export {growlRef, setIsReady}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index d56e38564149..7df1da23d068 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -186,13 +186,20 @@ const NewTeachersUniteNavigator = createModalStackNavigator require('../../../pages/TeachersUnite/ImTeacherPage').default as React.ComponentType, }); -const AccountSettingsModalStackNavigator = createModalStackNavigator( +const WorkspaceSettingsModalStackNavigator = createModalStackNavigator( { - [SCREENS.SETTINGS.PREFERENCES.ROOT]: () => require('../../../pages/settings/Preferences/PreferencesPage').default as React.ComponentType, - [SCREENS.SETTINGS.SECURITY]: () => require('../../../pages/settings/Security/SecuritySettingsPage').default as React.ComponentType, - [SCREENS.SETTINGS.PROFILE.ROOT]: () => require('../../../pages/settings/Profile/ProfilePage').default as React.ComponentType, - [SCREENS.SETTINGS.WALLET.ROOT]: () => require('../../../pages/settings/Wallet/WalletPage').default as React.ComponentType, - [SCREENS.SETTINGS.ABOUT]: () => require('../../../pages/settings/AboutPage/AboutPage').default as React.ComponentType, + [SCREENS.WORKSPACE.PROFILE]: () => require('../../../pages/workspace/WorkspaceProfilePage').default as React.ComponentType, + [SCREENS.WORKSPACE.CARD]: () => require('../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType, + [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType, + [SCREENS.WORKSPACE.BILLS]: () => require('../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.INVOICES]: () => require('../../../pages/workspace/invoices/WorkspaceInvoicesPage').default as React.ComponentType, + [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType, + [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType, + [SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesPage').default as React.ComponentType, + [SCREENS.WORKSPACE.MORE_FEATURES]: () => require('../../../pages/workspace/WorkspaceMoreFeaturesPage').default as React.ComponentType, + [SCREENS.WORKSPACE.TAGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default as React.ComponentType, }, (styles) => ({cardStyle: styles.navigationScreenCardStyle, headerShown: false}), ); @@ -245,6 +252,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceInvitePage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_APPROVER]: () => require('../../../pages/workspace/workflows/WorkspaceWorkflowsApproverPage').default as React.ComponentType, [SCREENS.WORKSPACE.INVITE_MESSAGE]: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').default as React.ComponentType, + [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: () => require('../../../pages/workspace/workflows/WorkspaceWorkflowsPayerPage').default as React.ComponentType, [SCREENS.WORKSPACE.NAME]: () => require('../../../pages/workspace/WorkspaceNamePage').default as React.ComponentType, [SCREENS.WORKSPACE.DESCRIPTION]: () => require('../../../pages/workspace/WorkspaceProfileDescriptionPage').default as React.ComponentType, [SCREENS.WORKSPACE.SHARE]: () => require('../../../pages/workspace/WorkspaceProfileSharePage').default as React.ComponentType, @@ -256,6 +264,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS_EDIT]: () => require('../../../pages/workspace/tags/WorkspaceEditTagsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.TAG_CREATE]: () => require('../../../pages/workspace/tags/WorkspaceCreateTagPage').default as React.ComponentType, [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, [SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType, [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType, @@ -312,7 +321,6 @@ const ProcessMoneyRequestHoldStackNavigator = createModalStackNavigator({ }); export { - AccountSettingsModalStackNavigator, AddPersonalBankAccountModalStackNavigator, DetailsModalStackNavigator, OnboardEngagementModalStackNavigator, @@ -341,4 +349,5 @@ export { TaskModalStackNavigator, WalletStatementStackNavigator, ProcessMoneyRequestHoldStackNavigator, + WorkspaceSettingsModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx index ce03a8d5bcba..87a441f16ddb 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/BottomTabNavigator.tsx @@ -4,12 +4,11 @@ import React from 'react'; import createCustomBottomTabNavigator from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import type {BottomTabNavigatorParamList} from '@libs/Navigation/types'; -import AllSettingsScreen from '@pages/home/sidebar/AllSettingsScreen'; import SidebarScreen from '@pages/home/sidebar/SidebarScreen'; import SCREENS from '@src/SCREENS'; import ActiveRouteContext from './ActiveRouteContext'; -const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default as React.ComponentType; +const loadInitialSettingsPage = () => require('../../../../pages/settings/InitialSettingsPage').default as React.ComponentType; const Tab = createCustomBottomTabNavigator(); @@ -20,6 +19,7 @@ const screenOptions: StackNavigationOptions = { function BottomTabNavigator() { const activeRoute = useNavigationState(getTopmostCentralPaneRoute); + return ( @@ -28,12 +28,8 @@ function BottomTabNavigator() { component={SidebarScreen} /> - diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx index 5a3af07a3d5a..16f403342a58 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx @@ -13,20 +13,13 @@ const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : type Screens = Partial React.ComponentType>>; -const workspaceSettingsScreens = { +const settingsScreens = { [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../../../pages/workspace/WorkspacesListPage').default as React.ComponentType, - [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../../pages/workspace/WorkspaceProfilePage').default as React.ComponentType, - [SCREENS.WORKSPACE.CARD]: () => require('../../../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType, - [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default as React.ComponentType, - [SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType, - [SCREENS.WORKSPACE.BILLS]: () => require('../../../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType, - [SCREENS.WORKSPACE.INVOICES]: () => require('../../../../../pages/workspace/invoices/WorkspaceInvoicesPage').default as React.ComponentType, - [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType, - [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType, - [SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../../../pages/workspace/categories/WorkspaceCategoriesPage').default as React.ComponentType, - [SCREENS.WORKSPACE.MORE_FEATURES]: () => require('../../../../../pages/workspace/WorkspaceMoreFeaturesPage').default as React.ComponentType, - [SCREENS.WORKSPACE.TAGS]: () => require('../../../../../pages/workspace/tags/WorkspaceTagsPage').default as React.ComponentType, - [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default as React.ComponentType, + [SCREENS.SETTINGS.PREFERENCES.ROOT]: () => require('../../../../../pages/settings/Preferences/PreferencesPage').default as React.ComponentType, + [SCREENS.SETTINGS.SECURITY]: () => require('../../../../../pages/settings/Security/SecuritySettingsPage').default as React.ComponentType, + [SCREENS.SETTINGS.PROFILE.ROOT]: () => require('../../../../../pages/settings/Profile/ProfilePage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET.ROOT]: () => require('../../../../../pages/settings/Wallet/WalletPage').default as React.ComponentType, + [SCREENS.SETTINGS.ABOUT]: () => require('../../../../../pages/settings/AboutPage/AboutPage').default as React.ComponentType, } satisfies Screens; function BaseCentralPaneNavigator() { @@ -46,8 +39,7 @@ function BaseCentralPaneNavigator() { initialParams={{openOnAdminRoom: openOnAdminRoom === 'true' || undefined}} component={ReportScreenWrapper} /> - - {Object.entries(workspaceSettingsScreens).map(([screenName, componentGetter]) => ( + {Object.entries(settingsScreens).map(([screenName, componentGetter]) => ( require('../../../../pages/settings/InitialSettingsPage').default as React.ComponentType; +const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default as React.ComponentType; const RootStack = createCustomFullScreenNavigator(); @@ -22,14 +22,14 @@ function FullScreenNavigator() { diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 58d9efb43df5..3a59e42bcca1 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -12,11 +12,11 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Session from '@libs/actions/Session'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {RootStackParamList, State} from '@libs/Navigation/types'; -import {checkIfWorkspaceSettingsTabHasRBR, getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils'; +import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils'; +import BottomTabAvatar from '@pages/home/sidebar/BottomTabAvatar'; import BottomTabBarFloatingActionButton from '@pages/home/sidebar/BottomTabBarFloatingActionButton'; import variables from '@styles/variables'; import * as Welcome from '@userActions/Welcome'; @@ -43,14 +43,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps const navigationState = navigation.getState() as State | undefined; const routes = navigationState?.routes; const currentRoute = routes?.[navigationState?.index ?? 0]; - const bottomTabRoute = getTopmostBottomTabRoute(navigationState); - if ( - // When we are redirected to the Settings tab from the OldDot, we don't want to call the Welcome.show() method. - // To prevent this, the value of the bottomTabRoute?.name is checked here - bottomTabRoute?.name === SCREENS.WORKSPACE.INITIAL || - Boolean(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) || - Session.isAnonymousUser() - ) { + if (Boolean(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) || Session.isAnonymousUser()) { return; } @@ -64,22 +57,20 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps return topmostBottomTabRoute?.name ?? SCREENS.HOME; }); - const shouldShowWorkspaceRedBrickRoad = checkIfWorkspaceSettingsTabHasRBR(activeWorkspaceID) && currentTabName === SCREENS.HOME; - - const chatTabBrickRoad = currentTabName !== SCREENS.HOME ? getChatTabBrickRoad(activeWorkspaceID) : undefined; + const chatTabBrickRoad = getChatTabBrickRoad(activeWorkspaceID); return ( - - { - Navigation.navigate(ROUTES.HOME); - }} - role={CONST.ROLE.BUTTON} - accessibilityLabel={translate('common.chats')} - wrapperStyle={styles.flexGrow1} - style={styles.bottomTabBarItem} - > + { + Navigation.navigate(ROUTES.HOME); + }} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('common.chats')} + wrapperStyle={styles.flex1} + style={styles.bottomTabBarItem} + > + )} - - + + + - - - interceptAnonymousUser(() => - activeWorkspaceID ? Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(activeWorkspaceID)) : Navigation.navigate(ROUTES.ALL_SETTINGS), - ) - } - role={CONST.ROLE.BUTTON} - accessibilityLabel={translate('common.settings')} - wrapperStyle={styles.flexGrow1} - style={styles.bottomTabBarItem} - > - - - {shouldShowWorkspaceRedBrickRoad && } - - - + + + ); } diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx index 4ed8869c1eaa..38bfe4af9ab6 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar.tsx @@ -1,36 +1,87 @@ import React from 'react'; import {View} from 'react-native'; -import Search from '@components/Search'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import Breadcrumbs from '@components/Breadcrumbs'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithoutFeedback} from '@components/Pressable'; +import Tooltip from '@components/Tooltip'; import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton'; -import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import SignInOrAvatarWithOptionalStatus from '@pages/home/sidebar/SignInOrAvatarWithOptionalStatus'; +import SignInButton from '@pages/home/sidebar/SignInButton'; import * as Session from '@userActions/Session'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Policy} from '@src/types/onyx'; -function TopBar() { +type TopBarOnyxProps = { + policy: OnyxEntry; +}; + +// eslint-disable-next-line react/no-unused-prop-types +type TopBarProps = {activeWorkspaceID?: string} & TopBarOnyxProps; + +function TopBar({policy}: TopBarProps) { const styles = useThemeStyles(); + const theme = useTheme(); const {translate} = useLocalize(); - const {activeWorkspaceID} = useActiveWorkspace(); + + const headerBreadcrumb = policy?.name + ? {type: CONST.BREADCRUMB_TYPE.STRONG, text: policy.name} + : { + type: CONST.BREADCRUMB_TYPE.ROOT, + }; return ( - - - Navigation.navigate(ROUTES.SEARCH))} - containerStyle={[styles.flex1]} - /> - + + + + + + + + + + {Session.isAnonymousUser() ? ( + + ) : ( + + Navigation.navigate(ROUTES.SEARCH))} + > + + + + )} + ); } TopBar.displayName = 'TopBar'; -export default TopBar; +export default withOnyx({ + policy: { + key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`, + }, +})(TopBar); diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx index bd32c6cab73c..8c53027cf713 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/index.tsx @@ -9,7 +9,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import type {NavigationStateRoute} from '@libs/Navigation/types'; import SCREENS from '@src/SCREENS'; import BottomTabBar from './BottomTabBar'; -import TopBar from './TopBar'; type CustomNavigatorProps = DefaultNavigatorOptions, StackNavigationOptions, StackNavigationEventMap> & { initialRouteName: string; @@ -52,7 +51,6 @@ function CustomBottomTabNavigator({initialRouteName, children, screenOptions, .. shouldShowOfflineIndicator={false} > - | PartialState>; -const isAtLeastOneInState = (state: StackState, screenName: string): boolean => !!state.routes.find((route) => route.name === screenName); +const isAtLeastOneInState = (state: StackState, screenName: string): boolean => state.routes.some((route) => route.name === screenName); function adaptStateIfNecessary(state: StackState) { const isNarrowLayout = getIsNarrowLayout(); + const workspaceCentralPane = state.routes.at(-1); + const topmostWorkspaceCentralPaneRoute = workspaceCentralPane?.state?.routes[0]; - // There should always be SETTINGS.ROOT screen in the state to make sure go back works properly if we deeplinkg to a subpage of settings. - if (!isAtLeastOneInState(state, SCREENS.SETTINGS.ROOT)) { + // When a screen from the FullScreenNavigator is opened from the deeplink then params should be passed to SCREENS.WORKSPACE.INITIAL from the variable defined below. + const workspacesCentralPaneParams = + workspaceCentralPane?.params && 'params' in workspaceCentralPane.params ? (workspaceCentralPane.params.params as Record) : undefined; + + // There should always be WORKSPACE.INITIAL screen in the state to make sure go back works properly if we deeplinkg to a subpage of settings. + if (!isAtLeastOneInState(state, SCREENS.WORKSPACE.INITIAL)) { // @ts-expect-error Updating read only property // noinspection JSConstantReassignment state.stale = true; // eslint-disable-line @@ -20,27 +26,30 @@ function adaptStateIfNecessary(state: StackState) { // This is necessary for ts to narrow type down to PartialState. if (state.stale === true) { // Unshift the root screen to fill left pane. - state.routes.unshift({name: SCREENS.SETTINGS.ROOT}); + state.routes.unshift({ + name: SCREENS.WORKSPACE.INITIAL, + params: topmostWorkspaceCentralPaneRoute?.params ?? workspacesCentralPaneParams, + }); } } // If the screen is wide, there should be at least two screens inside: - // - SETINGS.ROOT to cover left pane. - // - SETTINGS_CENTRAL_PANE to cover central pane. + // - WORKSPACE.INITIAL to cover left pane. + // - WORKSPACES_CENTRAL_PANE to cover central pane. if (!isNarrowLayout) { - if (!isAtLeastOneInState(state, SCREENS.SETTINGS_CENTRAL_PANE)) { + if (!isAtLeastOneInState(state, SCREENS.WORKSPACES_CENTRAL_PANE)) { // @ts-expect-error Updating read only property // noinspection JSConstantReassignment state.stale = true; // eslint-disable-line - // Push the default settings central pane screen. if (state.stale === true) { state.routes.push({ - name: SCREENS.SETTINGS_CENTRAL_PANE, + name: SCREENS.WORKSPACES_CENTRAL_PANE, state: { routes: [ { - name: SCREENS.SETTINGS.PROFILE.ROOT, + name: SCREENS.WORKSPACE.PROFILE, + params: state.routes[0]?.params, }, ], }, diff --git a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx index fb7ae24947c2..f35c609402b0 100644 --- a/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomFullScreenNavigator/index.tsx @@ -2,34 +2,12 @@ import type {ParamListBase, StackActionHelpers, StackNavigationState} from '@rea import {createNavigatorFactory, useNavigationBuilder} from '@react-navigation/native'; import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack'; import {StackView} from '@react-navigation/stack'; -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect} from 'react'; import useWindowDimensions from '@hooks/useWindowDimensions'; import navigationRef from '@libs/Navigation/navigationRef'; -import SCREENS from '@src/SCREENS'; import CustomFullScreenRouter from './CustomFullScreenRouter'; import type {FullScreenNavigatorProps, FullScreenNavigatorRouterOptions} from './types'; -type Routes = StackNavigationState['routes']; -function reduceReportRoutes(routes: Routes): Routes { - const result: Routes = []; - let count = 0; - const reverseRoutes = [...routes].reverse(); - - reverseRoutes.forEach((route) => { - if (route.name === SCREENS.SETTINGS_CENTRAL_PANE) { - // Remove all report routes except the last 3. This will improve performance. - if (count < 3) { - result.push(route); - count++; - } - } else { - result.push(route); - } - }); - - return result.reverse(); -} - function CustomFullScreenNavigator(props: FullScreenNavigatorProps) { const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder< StackNavigationState, @@ -45,16 +23,6 @@ function CustomFullScreenNavigator(props: FullScreenNavigatorProps) { const {isSmallScreenWidth} = useWindowDimensions(); - const stateToRender = useMemo(() => { - const result = reduceReportRoutes(state.routes); - - return { - ...state, - index: result.length - 1, - routes: [...result], - }; - }, [state]); - useEffect(() => { if (!navigationRef.isReady()) { return; @@ -69,7 +37,7 @@ function CustomFullScreenNavigator(props: FullScreenNavigatorProps) { diff --git a/src/libs/Navigation/FreezeWrapper.tsx b/src/libs/Navigation/FreezeWrapper.tsx index 9bb72a34588b..bb6ab107373b 100644 --- a/src/libs/Navigation/FreezeWrapper.tsx +++ b/src/libs/Navigation/FreezeWrapper.tsx @@ -1,7 +1,6 @@ import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native'; import React, {useEffect, useRef, useState} from 'react'; import {Freeze} from 'react-freeze'; -import {InteractionManager} from 'react-native'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; type FreezeWrapperProps = ChildrenProps & { @@ -29,7 +28,7 @@ function FreezeWrapper({keepVisible = false, children}: FreezeWrapperProps) { // we don't want to freeze the screen if it's the previous screen because the freeze placeholder // would be visible at the beginning of the back animation then if ((navigation.getState()?.index ?? 0) - (screenIndexRef.current ?? 0) > 1) { - InteractionManager.runAfterInteractions(() => setIsScreenBlurred(true)); + setIsScreenBlurred(true); } else { setIsScreenBlurred(false); } diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 4cd6a141bd3b..c55145a5d580 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -19,7 +19,7 @@ import linkingConfig from './linkingConfig'; import linkTo from './linkTo'; import navigationRef from './navigationRef'; import switchPolicyID from './switchPolicyID'; -import type {State, StateOrRoute, SwitchPolicyIDParams} from './types'; +import type {NavigationStateRoute, State, StateOrRoute, SwitchPolicyIDParams} from './types'; let resolveNavigationIsReadyPromise: () => void; const navigationIsReadyPromise = new Promise((resolve) => { @@ -234,6 +234,18 @@ function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopT navigationRef.current.goBack(); } +/** + * Reset the navigation state to Home page + */ +function resetToHome() { + const rootState = navigationRef.getRootState(); + const bottomTabKey = rootState.routes.find((item: NavigationStateRoute) => item.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR)?.state?.key; + if (bottomTabKey) { + navigationRef.dispatch({...StackActions.popToTop(), target: bottomTabKey}); + } + navigationRef.dispatch({...StackActions.popToTop(), target: rootState.key}); +} + /** * Close the full screen modal. */ @@ -366,6 +378,7 @@ export default { parseHybridAppUrl, closeFullScreen, navigateWithSwitchPolicyID, + resetToHome, }; export {navigationRef}; diff --git a/src/libs/Navigation/getTopmostSettingsCentralPaneName.ts b/src/libs/Navigation/getTopmostWorkspacesCentralPaneName.ts similarity index 76% rename from src/libs/Navigation/getTopmostSettingsCentralPaneName.ts rename to src/libs/Navigation/getTopmostWorkspacesCentralPaneName.ts index 0ddea6588ef6..db11368c1345 100644 --- a/src/libs/Navigation/getTopmostSettingsCentralPaneName.ts +++ b/src/libs/Navigation/getTopmostWorkspacesCentralPaneName.ts @@ -2,12 +2,12 @@ import type {NavigationState, PartialState} from '@react-navigation/native'; import SCREENS from '@src/SCREENS'; // Get the name of topmost report in the navigation stack. -function getTopmostSettingsCentralPaneName(state: NavigationState | PartialState): string | undefined { +function getTopmostWorkspacesCentralPaneName(state: NavigationState | PartialState): string | undefined { if (!state) { return; } - const topmostCentralPane = state.routes.filter((route) => typeof route !== 'number' && 'name' in route && route.name === SCREENS.SETTINGS_CENTRAL_PANE).at(-1); + const topmostCentralPane = state.routes.filter((route) => typeof route !== 'number' && 'name' in route && route.name === SCREENS.WORKSPACES_CENTRAL_PANE).at(-1); if (!topmostCentralPane) { return; @@ -24,4 +24,4 @@ function getTopmostSettingsCentralPaneName(state: NavigationState | PartialState return topmostCentralPane.state?.routes.at(-1)?.name; } -export default getTopmostSettingsCentralPaneName; +export default getTopmostWorkspacesCentralPaneName; diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts index 371ea89df2e2..2a00895f0492 100644 --- a/src/libs/Navigation/linkTo.ts +++ b/src/libs/Navigation/linkTo.ts @@ -6,7 +6,6 @@ import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import type {Route} from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import getActionsFromPartialDiff from './AppNavigator/getActionsFromPartialDiff'; import getPartialStateDiff from './AppNavigator/getPartialStateDiff'; import dismissModal from './dismissModal'; @@ -119,7 +118,6 @@ export default function linkTo(navigation: NavigationContainerRef)?.name === SCREENS.WORKSPACE.INITIAL && path.includes('workspace'); + const isFullScreenOnTop = rootState.routes?.at(-1)?.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR; - if (policyID && !isWorkspaceSettingsOpened) { + if (policyID && !isFullScreenOnTop) { // The stateFromPath doesn't include proper path if there is a policy passed with /w/id. // We need to replace the path in the state with the proper one. // To avoid this hacky solution we may want to create custom getActionFromState function in the future. diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 743bf2e0cff1..95233bfed079 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -2,12 +2,40 @@ import type {CentralPaneName} from '@libs/Navigation/types'; import SCREENS from '@src/SCREENS'; const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = { - [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE], - [SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT], - [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION], - [SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET], - [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT], - [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], + [SCREENS.SETTINGS.PROFILE.ROOT]: [ + SCREENS.SETTINGS.PROFILE.DISPLAY_NAME, + SCREENS.SETTINGS.PROFILE.CONTACT_METHODS, + SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS, + SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD, + SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER, + SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE, + SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME, + SCREENS.SETTINGS.PROFILE.STATUS, + SCREENS.SETTINGS.PROFILE.PRONOUNS, + SCREENS.SETTINGS.PROFILE.TIMEZONE, + SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT, + SCREENS.SETTINGS.PROFILE.LEGAL_NAME, + SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH, + SCREENS.SETTINGS.PROFILE.ADDRESS, + SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY, + SCREENS.SETTINGS.SHARE_CODE, + ], + [SCREENS.SETTINGS.PREFERENCES.ROOT]: [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE, SCREENS.SETTINGS.PREFERENCES.LANGUAGE, SCREENS.SETTINGS.PREFERENCES.THEME], + [SCREENS.SETTINGS.WALLET.ROOT]: [ + SCREENS.SETTINGS.WALLET.DOMAIN_CARD, + SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME, + SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE, + SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS, + SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM, + SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE, + SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT, + SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS, + SCREENS.SETTINGS.WALLET.CARD_ACTIVATE, + SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD, + SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS, + ], + [SCREENS.SETTINGS.SECURITY]: [SCREENS.SETTINGS.TWO_FACTOR_AUTH, SCREENS.SETTINGS.CLOSE], + [SCREENS.SETTINGS.ABOUT]: [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS, SCREENS.SETTINGS.TROUBLESHOOT], }; export default CENTRAL_PANE_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index f79e275007d7..fd108f2c95f3 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -2,39 +2,17 @@ import type {FullScreenName} from '@libs/Navigation/types'; import SCREENS from '@src/SCREENS'; const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { - [SCREENS.SETTINGS.PROFILE.ROOT]: [ - SCREENS.SETTINGS.PROFILE.DISPLAY_NAME, - SCREENS.SETTINGS.PROFILE.CONTACT_METHODS, - SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS, - SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD, - SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER, - SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE, - SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME, - SCREENS.SETTINGS.PROFILE.STATUS, - SCREENS.SETTINGS.PROFILE.PRONOUNS, - SCREENS.SETTINGS.PROFILE.TIMEZONE, - SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT, - SCREENS.SETTINGS.PROFILE.LEGAL_NAME, - SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH, - SCREENS.SETTINGS.PROFILE.ADDRESS, - SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY, + [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE], + [SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT], + [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION], + [SCREENS.WORKSPACE.WORKFLOWS]: [ + SCREENS.WORKSPACE.WORKFLOWS_APPROVER, + SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, + SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET, + SCREENS.WORKSPACE.WORKFLOWS_PAYER, ], - [SCREENS.SETTINGS.PREFERENCES.ROOT]: [SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE, SCREENS.SETTINGS.PREFERENCES.LANGUAGE, SCREENS.SETTINGS.PREFERENCES.THEME], - [SCREENS.SETTINGS.WALLET.ROOT]: [ - SCREENS.SETTINGS.WALLET.DOMAIN_CARD, - SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME, - SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE, - SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS, - SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM, - SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE, - SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT, - SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS, - SCREENS.SETTINGS.WALLET.CARD_ACTIVATE, - SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD, - SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS, - ], - [SCREENS.SETTINGS.SECURITY]: [SCREENS.SETTINGS.TWO_FACTOR_AUTH, SCREENS.SETTINGS.CLOSE], - [SCREENS.SETTINGS.ABOUT]: [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS, SCREENS.SETTINGS.TROUBLESHOOT], + [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE], + [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts index b2939cf38d9f..78a644ab4aee 100755 --- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts @@ -3,20 +3,13 @@ import SCREENS from '@src/SCREENS'; const TAB_TO_CENTRAL_PANE_MAPPING: Record = { [SCREENS.HOME]: [SCREENS.REPORT], - [SCREENS.ALL_SETTINGS]: [SCREENS.SETTINGS.WORKSPACES], - [SCREENS.WORKSPACE.INITIAL]: [ - SCREENS.WORKSPACE.PROFILE, - SCREENS.WORKSPACE.CARD, - SCREENS.WORKSPACE.WORKFLOWS, - SCREENS.WORKSPACE.REIMBURSE, - SCREENS.WORKSPACE.BILLS, - SCREENS.WORKSPACE.INVOICES, - SCREENS.WORKSPACE.TRAVEL, - SCREENS.WORKSPACE.MEMBERS, - SCREENS.WORKSPACE.CATEGORIES, - SCREENS.WORKSPACE.TAGS, - SCREENS.WORKSPACE.MORE_FEATURES, - SCREENS.WORKSPACE.DISTANCE_RATES, + [SCREENS.SETTINGS.ROOT]: [ + SCREENS.SETTINGS.PROFILE.ROOT, + SCREENS.SETTINGS.PREFERENCES.ROOT, + SCREENS.SETTINGS.SECURITY, + SCREENS.SETTINGS.WALLET.ROOT, + SCREENS.SETTINGS.ABOUT, + SCREENS.SETTINGS.WORKSPACES, ], }; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 97d7650a9043..1461c27e03e0 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -31,10 +31,8 @@ const config: LinkingOptions['config'] = { initialRouteName: SCREENS.HOME, screens: { [SCREENS.HOME]: ROUTES.HOME, - [SCREENS.ALL_SETTINGS]: ROUTES.ALL_SETTINGS, - [SCREENS.WORKSPACE.INITIAL]: { - path: ROUTES.WORKSPACE_INITIAL.route, - exact: true, + [SCREENS.SETTINGS.ROOT]: { + path: ROUTES.SETTINGS, }, }, }, @@ -42,42 +40,27 @@ const config: LinkingOptions['config'] = { [NAVIGATORS.CENTRAL_PANE_NAVIGATOR]: { screens: { [SCREENS.REPORT]: ROUTES.REPORT_WITH_ID.route, - - [SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES, - [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_PROFILE.route, - [SCREENS.WORKSPACE.CARD]: { - path: ROUTES.WORKSPACE_CARD.route, - }, - [SCREENS.WORKSPACE.WORKFLOWS]: { - path: ROUTES.WORKSPACE_WORKFLOWS.route, - }, - [SCREENS.WORKSPACE.REIMBURSE]: { - path: ROUTES.WORKSPACE_REIMBURSE.route, - }, - [SCREENS.WORKSPACE.BILLS]: { - path: ROUTES.WORKSPACE_BILLS.route, - }, - [SCREENS.WORKSPACE.INVOICES]: { - path: ROUTES.WORKSPACE_INVOICES.route, - }, - [SCREENS.WORKSPACE.TRAVEL]: { - path: ROUTES.WORKSPACE_TRAVEL.route, - }, - [SCREENS.WORKSPACE.MEMBERS]: { - path: ROUTES.WORKSPACE_MEMBERS.route, + [SCREENS.SETTINGS.PROFILE.ROOT]: { + path: ROUTES.SETTINGS_PROFILE, + exact: true, }, - [SCREENS.WORKSPACE.CATEGORIES]: { - path: ROUTES.WORKSPACE_CATEGORIES.route, + [SCREENS.SETTINGS.PREFERENCES.ROOT]: { + path: ROUTES.SETTINGS_PREFERENCES, + exact: true, }, - [SCREENS.WORKSPACE.MORE_FEATURES]: { - path: ROUTES.WORKSPACE_MORE_FEATURES.route, + [SCREENS.SETTINGS.SECURITY]: { + path: ROUTES.SETTINGS_SECURITY, + exact: true, }, - [SCREENS.WORKSPACE.TAGS]: { - path: ROUTES.WORKSPACE_TAGS.route, + [SCREENS.SETTINGS.WALLET.ROOT]: { + path: ROUTES.SETTINGS_WALLET, + exact: true, }, - [SCREENS.WORKSPACE.DISTANCE_RATES]: { - path: ROUTES.WORKSPACE_DISTANCE_RATES.route, + [SCREENS.SETTINGS.ABOUT]: { + path: ROUTES.SETTINGS_ABOUT, + exact: true, }, + [SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES, }, }, [SCREENS.NOT_FOUND]: '*', @@ -288,6 +271,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route, }, + [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: { + path: ROUTES.WORKSPACE_WORKFLOWS_PAYER.route, + }, [SCREENS.WORKSPACE.MEMBER_DETAILS]: { path: ROUTES.WORKSPACE_MEMBER_DETAILS.route, }, @@ -303,6 +289,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.TAGS_EDIT]: { path: ROUTES.WORKSPACE_EDIT_TAGS.route, }, + [SCREENS.WORKSPACE.TAG_CREATE]: { + path: ROUTES.WORKSPACE_TAG_CREATE.route, + }, [SCREENS.REIMBURSEMENT_ACCOUNT]: { path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route, exact: true, @@ -557,30 +546,44 @@ const config: LinkingOptions['config'] = { [NAVIGATORS.FULL_SCREEN_NAVIGATOR]: { screens: { - [SCREENS.SETTINGS.ROOT]: { - path: ROUTES.SETTINGS, + [SCREENS.WORKSPACE.INITIAL]: { + path: ROUTES.WORKSPACE_INITIAL.route, }, - [SCREENS.SETTINGS_CENTRAL_PANE]: { + [SCREENS.WORKSPACES_CENTRAL_PANE]: { screens: { - [SCREENS.SETTINGS.PROFILE.ROOT]: { - path: ROUTES.SETTINGS_PROFILE, - exact: true, + [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_PROFILE.route, + [SCREENS.WORKSPACE.CARD]: { + path: ROUTES.WORKSPACE_CARD.route, }, - [SCREENS.SETTINGS.PREFERENCES.ROOT]: { - path: ROUTES.SETTINGS_PREFERENCES, - exact: true, + [SCREENS.WORKSPACE.WORKFLOWS]: { + path: ROUTES.WORKSPACE_WORKFLOWS.route, }, - [SCREENS.SETTINGS.SECURITY]: { - path: ROUTES.SETTINGS_SECURITY, - exact: true, + [SCREENS.WORKSPACE.REIMBURSE]: { + path: ROUTES.WORKSPACE_REIMBURSE.route, }, - [SCREENS.SETTINGS.WALLET.ROOT]: { - path: ROUTES.SETTINGS_WALLET, - exact: true, + [SCREENS.WORKSPACE.BILLS]: { + path: ROUTES.WORKSPACE_BILLS.route, }, - [SCREENS.SETTINGS.ABOUT]: { - path: ROUTES.SETTINGS_ABOUT, - exact: true, + [SCREENS.WORKSPACE.INVOICES]: { + path: ROUTES.WORKSPACE_INVOICES.route, + }, + [SCREENS.WORKSPACE.TRAVEL]: { + path: ROUTES.WORKSPACE_TRAVEL.route, + }, + [SCREENS.WORKSPACE.MEMBERS]: { + path: ROUTES.WORKSPACE_MEMBERS.route, + }, + [SCREENS.WORKSPACE.CATEGORIES]: { + path: ROUTES.WORKSPACE_CATEGORIES.route, + }, + [SCREENS.WORKSPACE.MORE_FEATURES]: { + path: ROUTES.WORKSPACE_MORE_FEATURES.route, + }, + [SCREENS.WORKSPACE.TAGS]: { + path: ROUTES.WORKSPACE_TAGS.route, + }, + [SCREENS.WORKSPACE.DISTANCE_RATES]: { + path: ROUTES.WORKSPACE_DISTANCE_RATES.route, }, }, }, diff --git a/src/libs/Navigation/linkingConfig/customGetPathFromState.ts b/src/libs/Navigation/linkingConfig/customGetPathFromState.ts index 4f7023d14db4..4017b1b2b17c 100644 --- a/src/libs/Navigation/linkingConfig/customGetPathFromState.ts +++ b/src/libs/Navigation/linkingConfig/customGetPathFromState.ts @@ -8,20 +8,19 @@ import SCREENS from '@src/SCREENS'; const removePolicyIDParamFromState = (state: State) => { const stateCopy = _.cloneDeep(state); const bottomTabRoute = getTopmostBottomTabRoute(stateCopy); - if (bottomTabRoute?.name === SCREENS.HOME && bottomTabRoute?.params && 'policyID' in bottomTabRoute.params) { + if (bottomTabRoute?.params && 'policyID' in bottomTabRoute.params) { delete bottomTabRoute.params.policyID; } return stateCopy; }; const customGetPathFromState: typeof getPathFromState = (state, options) => { + // For the Home and Settings pages we should remove policyID from the params, because on small screens it's displayed twice in the URL const stateWithoutPolicyID = removePolicyIDParamFromState(state as State); - - // For the Home page we should remove policyID from the params, const path = getPathFromState(stateWithoutPolicyID, options); const policyIDFromState = getPolicyIDFromState(state as State); - const isWorkspaceSettingsOpened = getTopmostBottomTabRoute(state as State)?.name === SCREENS.WORKSPACE.INITIAL && path.includes('workspace'); - return `${policyIDFromState && !isWorkspaceSettingsOpened ? `/w/${policyIDFromState}` : ''}${path}`; + const isHomeOpened = getTopmostBottomTabRoute(state as State)?.name === SCREENS.HOME; + return `${policyIDFromState && isHomeOpened ? `/w/${policyIDFromState}` : ''}${path}`; }; export default customGetPathFromState; diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index 5f89b2cd4630..e4a8464d7ddd 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -14,6 +14,10 @@ import getMatchingBottomTabRouteForState from './getMatchingBottomTabRouteForSta import getMatchingCentralPaneRouteForState from './getMatchingCentralPaneRouteForState'; import replacePathInNestedState from './replacePathInNestedState'; +const RHP_SCREENS_OPENED_FROM_LHN = [SCREENS.SETTINGS.SHARE_CODE, SCREENS.SETTINGS.PROFILE.STATUS] as const; + +type RHPScreenOpenedFromLHN = (typeof RHP_SCREENS_OPENED_FROM_LHN)[number]; + type Metainfo = { // Sometimes modal screens don't have information about what should be visible under the overlay. // That means such screen can have different screens under the overlay depending on what was already in the state. @@ -73,14 +77,21 @@ function createCentralPaneNavigator(route: NavigationPartialRoute): NavigationPartialRoute { const routes = []; - routes.push({name: SCREENS.SETTINGS.ROOT}); + const policyID = route?.params && 'policyID' in route.params ? route.params.policyID : undefined; + + // Both routes in FullScreenNavigator should store a policyID in params, so here this param is also passed to the screen displayed in LHN in FullScreenNavigator + routes.push({ + name: SCREENS.WORKSPACE.INITIAL, + params: { + policyID, + }, + }); if (route) { routes.push({ - name: SCREENS.SETTINGS_CENTRAL_PANE, + name: SCREENS.WORKSPACES_CENTRAL_PANE, state: getRoutesWithIndex([route]), }); } - return { name: NAVIGATORS.FULL_SCREEN_NAVIGATOR, state: getRoutesWithIndex(routes), @@ -131,11 +142,6 @@ function getMatchingRootRouteForRHPRoute( return createFullScreenNavigator({name: fullScreenName as FullScreenName, params: route.params}); } } - - // This screen is opened from the LHN of the FullStackNavigator, so in this case we shouldn't push any CentralPane screen - if (route.name === SCREENS.SETTINGS.SHARE_CODE) { - return createFullScreenNavigator(); - } } function getAdaptedState(state: PartialState>, policyID?: string): GetAdaptedStateReturnType { @@ -165,18 +171,25 @@ function getAdaptedState(state: PartialState if (topmostNestedRHPRoute) { let matchingRootRoute = getMatchingRootRouteForRHPRoute(topmostNestedRHPRoute); - + const isRHPScreenOpenedFromLHN = topmostNestedRHPRoute?.name && RHP_SCREENS_OPENED_FROM_LHN.includes(topmostNestedRHPRoute?.name as RHPScreenOpenedFromLHN); // This may happen if this RHP doens't have a route that should be under the overlay defined. - if (!matchingRootRoute) { + if (!matchingRootRoute || isRHPScreenOpenedFromLHN) { metainfo.isCentralPaneAndBottomTabMandatory = false; metainfo.isFullScreenNavigatorMandatory = false; - matchingRootRoute = createCentralPaneNavigator({name: SCREENS.REPORT}); + matchingRootRoute = matchingRootRoute ?? createCentralPaneNavigator({name: SCREENS.REPORT}); } // If the root route is type of FullScreenNavigator, the default bottom tab will be added. const matchingBottomTabRoute = getMatchingBottomTabRouteForState({routes: [matchingRootRoute]}); routes.push(createBottomTabNavigator(matchingBottomTabRoute, policyID)); - routes.push(matchingRootRoute); + // When we open a screen in RHP from FullScreenNavigator, we need to add the appropriate screen in CentralPane. + // Then, when we close FullScreenNavigator, we will be redirected to the correct page in CentralPane. + if (matchingRootRoute.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR) { + routes.push(createCentralPaneNavigator({name: SCREENS.SETTINGS.WORKSPACES})); + } + if (!isNarrowLayout || !isRHPScreenOpenedFromLHN) { + routes.push(matchingRootRoute); + } } routes.push(rhpNavigator); @@ -230,14 +243,18 @@ function getAdaptedState(state: PartialState routes.push( createBottomTabNavigator( { - name: SCREENS.HOME, + name: SCREENS.SETTINGS.ROOT, }, policyID, ), ); - if (!isNarrowLayout) { - routes.push(createCentralPaneNavigator({name: SCREENS.REPORT})); - } + + routes.push( + createCentralPaneNavigator({ + name: SCREENS.SETTINGS.WORKSPACES, + }), + ); + routes.push(fullScreenNavigator); return { @@ -318,7 +335,6 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options) => { const state = getStateFromPath(pathWithoutPolicyID, options) as PartialState>; replacePathInNestedState(state, path); - if (state === undefined) { throw new Error('Unable to parse path'); } diff --git a/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts b/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts index ef4cd65942b0..fd45685acf23 100644 --- a/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts +++ b/src/libs/Navigation/linkingConfig/getMatchingBottomTabRouteForState.ts @@ -1,5 +1,6 @@ import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import type {BottomTabName, NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types'; +import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import {CENTRAL_PANE_TO_TAB_MAPPING} from './TAB_TO_CENTRAL_PANE_MAPPING'; @@ -7,6 +8,12 @@ import {CENTRAL_PANE_TO_TAB_MAPPING} from './TAB_TO_CENTRAL_PANE_MAPPING'; function getMatchingBottomTabRouteForState(state: State, policyID?: string): NavigationPartialRoute { const paramsWithPolicyID = policyID ? {policyID} : undefined; const defaultRoute = {name: SCREENS.HOME, params: paramsWithPolicyID}; + const isFullScreenNavigatorOpened = state.routes.some((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR); + + if (isFullScreenNavigatorOpened) { + return {name: SCREENS.SETTINGS.ROOT, params: paramsWithPolicyID}; + } + const topmostCentralPaneRoute = getTopmostCentralPaneRoute(state); if (topmostCentralPaneRoute === undefined) { @@ -14,9 +21,6 @@ function getMatchingBottomTabRouteForState(state: State, pol } const tabName = CENTRAL_PANE_TO_TAB_MAPPING[topmostCentralPaneRoute.name]; - if (tabName === SCREENS.WORKSPACE.INITIAL) { - return {name: tabName, params: topmostCentralPaneRoute.params}; - } return {name: tabName, params: paramsWithPolicyID}; } diff --git a/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts b/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts index d4d558708b63..51cb9e3aa9a5 100644 --- a/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts +++ b/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts @@ -4,8 +4,6 @@ import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import TAB_TO_CENTRAL_PANE_MAPPING from './TAB_TO_CENTRAL_PANE_MAPPING'; -const WORKSPACES_SCREENS = TAB_TO_CENTRAL_PANE_MAPPING[SCREENS.WORKSPACE.INITIAL].concat(TAB_TO_CENTRAL_PANE_MAPPING[SCREENS.ALL_SETTINGS]); - /** * @param state - react-navigation state */ @@ -55,11 +53,11 @@ function getAlreadyOpenedSettingsScreen(rootState?: State, policyID?: string): k return undefined; } - // If one of the screen from WORKSPACES_SCREENS is now in the navigation state, we can decide which screen we should display. + // If one of the screen from TAB_TO_CENTRAL_PANE_MAPPING[SCREENS.SETTINGS.ROOT] is now in the navigation state, we can decide which screen we should display. // A screen from the navigation state can be pushed to the navigation state again only if it has a matching policyID with the currently selected workspace. // Otherwise, when we switch the workspace, we want to display the initial screen in the settings tab. const alreadyOpenedSettingsTab = rootState.routes - .filter((item) => item.params && 'screen' in item.params && WORKSPACES_SCREENS.includes(item.params.screen as keyof CentralPaneNavigatorParamList)) + .filter((item) => item.params && 'screen' in item.params && TAB_TO_CENTRAL_PANE_MAPPING[SCREENS.SETTINGS.ROOT].includes(item.params.screen as keyof CentralPaneNavigatorParamList)) .at(-1); if (!hasRouteMatchingPolicyID(alreadyOpenedSettingsTab as NavigationPartialRoute, policyID)) { @@ -82,7 +80,7 @@ function getMatchingCentralPaneRouteForState(state: State, r const centralPaneName = TAB_TO_CENTRAL_PANE_MAPPING[topmostBottomTabRoute.name][0]; - if (topmostBottomTabRoute.name === SCREENS.WORKSPACE.INITIAL) { + if (topmostBottomTabRoute.name === SCREENS.SETTINGS.ROOT) { // When we go back to the settings tab without switching the workspace id, we want to return to the previously opened screen const policyID = topmostBottomTabRoute?.params && 'policyID' in topmostBottomTabRoute.params ? (topmostBottomTabRoute.params.policyID as string) : undefined; const screen = getAlreadyOpenedSettingsScreen(rootState, policyID) ?? centralPaneName; diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts index c425beca73fd..685c21d88e79 100644 --- a/src/libs/Navigation/switchPolicyID.ts +++ b/src/libs/Navigation/switchPolicyID.ts @@ -10,7 +10,6 @@ import SCREENS from '@src/SCREENS'; import getStateFromPath from './getStateFromPath'; import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute'; import linkingConfig from './linkingConfig'; -import TAB_TO_CENTRAL_PANE_MAPPING from './linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING'; import type {NavigationRoot, RootStackParamList, StackNavigationAction, State, SwitchPolicyIDParams} from './types'; type ActionPayloadParams = { @@ -62,7 +61,7 @@ function getActionForBottomTabNavigator(action: StackNavigationAction, state: Na }; } -export default function switchPolicyID(navigation: NavigationContainerRef | null, {policyID, route, isPolicyAdmin = false}: SwitchPolicyIDParams) { +export default function switchPolicyID(navigation: NavigationContainerRef | null, {policyID, route}: SwitchPolicyIDParams) { if (!navigation) { throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); } @@ -110,7 +109,7 @@ export default function switchPolicyID(navigation: NavigationContainerRef); @@ -122,23 +121,12 @@ export default function switchPolicyID(navigation: NavigationContainerRef; }; -type SettingsCentralPaneNavigatorParamList = { - [SCREENS.SETTINGS.PROFILE.ROOT]: undefined; - [SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined; - [SCREENS.SETTINGS.SECURITY]: undefined; - [SCREENS.SETTINGS.WALLET.ROOT]: undefined; - [SCREENS.SETTINGS.ABOUT]: undefined; +type WorkspacesCentralPaneNavigatorParamList = { + [SCREENS.WORKSPACE.PROFILE]: { + policyID: string; + }; + [SCREENS.WORKSPACE.CARD]: { + policyID: string; + }; + [SCREENS.WORKSPACE.WORKFLOWS]: { + policyID: string; + }; + [SCREENS.WORKSPACE.WORKFLOWS_APPROVER]: { + policyID: string; + }; + [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: { + policyID: string; + }; + [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: { + policyID: string; + }; + [SCREENS.WORKSPACE.REIMBURSE]: { + policyID: string; + }; + [SCREENS.WORKSPACE.BILLS]: { + policyID: string; + }; + [SCREENS.WORKSPACE.INVOICES]: { + policyID: string; + }; + [SCREENS.WORKSPACE.TRAVEL]: { + policyID: string; + }; + [SCREENS.WORKSPACE.MEMBERS]: { + policyID: string; + }; + [SCREENS.WORKSPACE.CATEGORIES]: { + policyID: string; + }; + [SCREENS.WORKSPACE.MORE_FEATURES]: { + policyID: string; + }; + [SCREENS.WORKSPACE.TAGS]: { + policyID: string; + categoryName: string; + }; + [SCREENS.WORKSPACE.DISTANCE_RATES]: { + policyID: string; + }; }; type FullScreenNavigatorParamList = { - [SCREENS.SETTINGS.ROOT]: undefined; - [SCREENS.SETTINGS_CENTRAL_PANE]: NavigatorScreenParams; + [SCREENS.WORKSPACE.INITIAL]: { + policyID: string; + }; + [SCREENS.WORKSPACES_CENTRAL_PANE]: NavigatorScreenParams; }; type BottomTabNavigatorParamList = { [SCREENS.HOME]: undefined; - [SCREENS.ALL_SETTINGS]: undefined; - [SCREENS.WORKSPACE.INITIAL]: undefined; + [SCREENS.SETTINGS.ROOT]: undefined; }; type SharedScreensParamList = { @@ -618,7 +624,7 @@ type BottomTabName = keyof BottomTabNavigatorParamList; type CentralPaneName = keyof CentralPaneNavigatorParamList; -type FullScreenName = keyof SettingsCentralPaneNavigatorParamList; +type FullScreenName = keyof WorkspacesCentralPaneNavigatorParamList; type SwitchPolicyIDParams = { policyID?: string; @@ -672,5 +678,7 @@ export type { WorkspaceSwitcherNavigatorParamList, OnboardEngagementNavigatorParamList, SwitchPolicyIDParams, + FullScreenNavigatorParamList, + WorkspacesCentralPaneNavigatorParamList, BackToParams, }; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f53f2a66167a..3de3d1622405 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -977,7 +977,17 @@ function getCategoryListSections( } if (searchInputValue) { - const searchCategories = enabledCategories.filter((category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase())); + const searchCategories: Category[] = []; + + enabledCategories.forEach((category) => { + if (!category.name.toLowerCase().includes(searchInputValue.toLowerCase())) { + return; + } + searchCategories.push({ + ...category, + isSelected: selectedOptions.some((selectedOption) => selectedOption.name === category.name), + }); + }); categorySections.push({ // "Search" section diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 9dd60eeebcef..65aadd440010 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -26,6 +26,13 @@ Onyx.connect({ function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true, shouldAddCurrentUserPostfix = false): string { let displayName = passedPersonalDetails?.displayName ? passedPersonalDetails.displayName.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '') : ''; + + // If the displayName is not set by the user, the backend sets the diplayName same as the login so + // we need to remove the sms domain from the displayName if it is an sms login. + if (displayName === passedPersonalDetails?.login && Str.isSMSLogin(passedPersonalDetails?.login)) { + displayName = Str.removeSMSDomain(displayName); + } + if (shouldAddCurrentUserPostfix && !!displayName) { displayName = `${displayName} (${Localize.translateLocal('common.you').toLowerCase()})`; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 97b7b576b303..8dc1c9967f13 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -31,6 +31,7 @@ import type { Task, Transaction, TransactionViolation, + UserWallet, } from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -51,6 +52,7 @@ import type {Receipt, TransactionChanges, WaypointCollection} from '@src/types/o import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; +import * as store from './actions/ReimbursementAccount/store'; import * as CollectionUtils from './CollectionUtils'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; @@ -424,6 +426,8 @@ type AncestorIDs = { reportActionsIDs: string[]; }; +type MissingPaymentMethod = 'bankAccount' | 'wallet'; + let currentUserEmail: string | undefined; let currentUserPrivateDomain: string | undefined; let currentUserAccountID: number | undefined; @@ -1177,7 +1181,7 @@ function hasOnlyTransactionsWithPendingRoutes(iouReportID: string | undefined): * If the report is a thread and has a chat type set, it is a workspace chat. */ function isWorkspaceThread(report: OnyxEntry): boolean { - return isThread(report) && isChatReport(report) && !isDM(report); + return isThread(report) && isChatReport(report) && !!getChatType(report); } /** @@ -5247,6 +5251,30 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry, reportId: string, reportAction: ReportAction): MissingPaymentMethod | undefined { + const isSubmitterOfUnsettledReport = isCurrentUserSubmitter(reportId) && !isSettled(reportId); + if (!isSubmitterOfUnsettledReport || reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { + return undefined; + } + const paymentType = reportAction.originalMessage?.paymentType; + if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + return isEmpty(userWallet) || userWallet.tierName === CONST.WALLET.TIER_NAME.SILVER ? 'wallet' : undefined; + } + + return !store.hasCreditBankAccount() ? 'bankAccount' : undefined; +} + +/** + * Checks if report chat contains missing payment method + */ +function hasMissingPaymentMethod(userWallet: OnyxEntry, iouReportID: string): boolean { + const reportActions = ReportActionsUtils.getAllReportActions(iouReportID); + return Object.values(reportActions).some((action) => getIndicatedMissingPaymentMethod(userWallet, iouReportID, action) !== undefined); +} + /** * Used from money request actions to decide if we need to build an optimistic money request report. Create a new report if: @@ -5454,6 +5482,7 @@ export { isValidReport, getReportDescriptionText, isReportFieldOfTypeTitle, + hasMissingPaymentMethod, isIOUReportUsingReport, hasUpdatedTotal, isReportFieldDisabled, @@ -5464,6 +5493,7 @@ export { canEditRoomVisibility, canEditPolicyDescription, getPolicyDescriptionText, + getIndicatedMissingPaymentMethod, isJoinRequestInAdminRoom, canAddOrDeleteTransactions, shouldCreateNewMoneyRequestReport, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index ff1b96601951..7bf163416054 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -304,7 +304,8 @@ function getOptionData({ const lastActorDisplayName = OptionsListUtils.getLastActorDisplayName(lastActorDetails, hasMultipleParticipants); const lastMessageTextFromReport = OptionsListUtils.getLastMessageTextForReport(report, lastActorDetails, policy); - let lastMessageText = lastMessageTextFromReport; + // We need to remove sms domain in case the last message text has a phone number mention with sms domain. + let lastMessageText = Str.removeSMSDomain(lastMessageTextFromReport); const lastAction = visibleReportActionItems[report.reportID]; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 6442f2ec0eef..302e4048d0e8 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -222,7 +222,10 @@ function openApp() { function reconnectApp(updateIDFrom: OnyxEntry = 0) { console.debug(`[OnyxUpdates] App reconnecting with updateIDFrom: ${updateIDFrom}`); getPolicyParamsForOpenOrReconnect().then((policyParams) => { - const params: ReconnectAppParams = {...policyParams}; + const params: ReconnectAppParams = { + ...policyParams, + idempotencyKey: `${WRITE_COMMANDS.RECONNECT_APP}`, + }; // When the app reconnects we do a fast "sync" of the LHN and only return chats that have new messages. We achieve this by sending the most recent reportActionID. // we have locally. And then only update the user about chats with messages that have occurred after that reportActionID. diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index df46aa82bc74..c9759c6c7995 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -26,6 +26,7 @@ import type { OpenPolicyCategoriesPageParams, OpenPolicyDistanceRatesPageParams, OpenPolicyTagsPageParams, + OpenPolicyWorkflowsPageParams, OpenWorkspaceInvitePageParams, OpenWorkspaceMembersPageParams, OpenWorkspaceParams, @@ -34,6 +35,8 @@ import type { SetWorkspaceAutoReportingFrequencyParams, SetWorkspaceAutoReportingMonthlyOffsetParams, SetWorkspaceAutoReportingParams, + SetWorkspacePayerParams, + SetWorkspaceReimbursementParams, UpdateWorkspaceAvatarParams, UpdateWorkspaceCustomUnitAndRateParams, UpdateWorkspaceDescriptionParams, @@ -589,6 +592,106 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo API.write(WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, params, {optimisticData, failureData, successData}); } +function setWorkspacePayer(policyID: string, reimburserEmail: string, reimburserAccountID: number) { + const policy = ReportUtils.getPolicy(policyID); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + reimburserEmail, + reimburserAccountID, + errorFields: {reimburserEmail: null}, + pendingFields: {reimburserEmail: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + errorFields: {reimburserEmail: null}, + pendingFields: {reimburserEmail: null}, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + reimburserEmail: policy.reimburserEmail, + reimburserAccountID: policy.reimburserAccountID, + errorFields: {reimburserEmail: ErrorUtils.getMicroSecondOnyxError('workflowsPayerPage.genericErrorMessage')}, + pendingFields: {reimburserEmail: null}, + }, + }, + ]; + + const params: SetWorkspacePayerParams = {policyID, reimburserEmail}; + + API.write(WRITE_COMMANDS.SET_WORKSPACE_PAYER, params, {optimisticData, failureData, successData}); +} + +function clearWorkspacePayerError(policyID: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {reimburserEmail: null}}); +} + +function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueOf, reimburserAccountID: number, reimburserEmail: string) { + const policy = ReportUtils.getPolicy(policyID); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + reimbursementChoice, + reimburserAccountID, + reimburserEmail, + errorFields: {reimbursementChoice: null}, + pendingFields: {reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + errorFields: {reimbursementChoice: null}, + pendingFields: {reimbursementChoice: null}, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + reimbursementChoice: policy.reimbursementChoice, + reimburserAccountID: policy.reimburserAccountID, + reimburserEmail: policy.reimburserEmail, + errorFields: {reimbursementChoice: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + pendingFields: {reimbursementChoice: null}, + }, + }, + ]; + + const params: SetWorkspaceReimbursementParams = {policyID, reimbursementChoice}; + + API.write(WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT, params, {optimisticData, failureData, successData}); +} + +function clearWorkspaceReimbursementErrors(policyID: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {reimbursementChoice: null}}); +} + /** * Build optimistic data for removing users from the announcement room */ @@ -1888,6 +1991,50 @@ function openWorkspaceReimburseView(policyID: string) { API.read(READ_COMMANDS.OPEN_WORKSPACE_REIMBURSE_VIEW, params, {successData, failureData}); } +function openPolicyWorkflowsPage(policyID: string) { + if (!policyID) { + Log.warn('openPolicyWorkflowsPage invalid params', {policyID}); + return; + } + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + key: `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${policyID}`, + value: { + isLoading: true, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + key: `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${policyID}`, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + key: `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${policyID}`, + value: { + isLoading: false, + }, + }, + ], + }; + + const params: OpenPolicyWorkflowsPageParams = {policyID}; + + API.read(READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE, params, onyxData); +} + function setPolicyIDForReimburseView(policyID: string) { Onyx.merge(ONYXKEYS.WORKSPACE_RATE_AND_UNIT, {policyID, rate: null, unit: null}); } @@ -2600,6 +2747,70 @@ function createPolicyCategory(policyID: string, categoryName: string) { API.write(WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES, parameters, onyxData); } +function createPolicyTag(policyID: string, tagName: string) { + const tagListName = Object.keys(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})[0]; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + value: { + [tagListName]: { + tags: { + [tagName]: { + name: tagName, + enabled: false, + errors: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + value: { + [tagListName]: { + tags: { + [tagName]: { + errors: null, + pendingAction: null, + }, + }, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + value: { + [tagListName]: { + tags: { + [tagName]: { + errors: ErrorUtils.getMicroSecondOnyxError('workspace.tags.genericFailureMessage'), + pendingAction: null, + }, + }, + }, + }, + }, + ], + }; + + const parameters = { + policyID, + tags: JSON.stringify([{name: tagName}]), + }; + + API.write(WRITE_COMMANDS.CREATE_POLICY_TAG, parameters, onyxData); +} + function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) { const onyxData: OnyxData = { optimisticData: [ @@ -3293,6 +3504,10 @@ export { declineJoinRequest, createPolicyCategory, clearCategoryErrors, + setWorkspacePayer, + clearWorkspacePayerError, + setWorkspaceReimbursement, + openPolicyWorkflowsPage, setPolicyRequiresTag, renamePolicyTaglist, enablePolicyCategories, @@ -3303,4 +3518,6 @@ export { enablePolicyTaxes, enablePolicyWorkflows, openPolicyDistanceRatesPage, + createPolicyTag, + clearWorkspaceReimbursementErrors, }; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 06958c5ddaf7..425616376db3 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1847,7 +1847,7 @@ function addPolicyReport(policyReport: ReportUtils.OptimisticChatReport) { }; API.write(WRITE_COMMANDS.ADD_WORKSPACE_ROOM, parameters, {optimisticData, successData, failureData}); - Navigation.dismissModal(policyReport.reportID); + Navigation.dismissModalWithReport(policyReport); } /** Deletes a report, along with its reportActions, any linked reports, and any linked IOU report. */ @@ -2219,10 +2219,10 @@ function toggleEmojiReaction( addEmojiReaction(originalReportID, reportAction.reportActionID, emoji, skinTone); } -function openReportFromDeepLink(url: string, isAuthenticated: boolean) { +function openReportFromDeepLink(url: string) { const reportID = ReportUtils.getReportIDFromLink(url); - if (reportID && !isAuthenticated) { + if (reportID && !Session.hasAuthToken()) { // Call the OpenReport command to check in the server if it's a public room. If so, we'll open it as an anonymous user openReport(reportID, '', [], {}, '0', true); diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 619281ac7ecf..2c2baee9b96e 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -184,7 +184,14 @@ function hasStashedSession(): boolean { return Boolean(stashedSession.authToken && stashedCredentials.autoGeneratedLogin && stashedCredentials.autoGeneratedLogin !== ''); } -function signOutAndRedirectToSignIn(shouldReplaceCurrentScreen?: boolean, shouldStashSession?: boolean) { +/** + * Checks if the user has authToken + */ +function hasAuthToken(): boolean { + return !!session.authToken; +} + +function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean) { Log.info('Redirecting to Sign In because signOut() was called'); hideContextMenu(false); if (!isAnonymousUser()) { @@ -233,11 +240,10 @@ function signOutAndRedirectToSignIn(shouldReplaceCurrentScreen?: boolean, should if (Navigation.isActiveRoute(ROUTES.SIGN_IN_MODAL)) { return; } - if (shouldReplaceCurrentScreen) { - Navigation.navigate(ROUTES.SIGN_IN_MODAL, CONST.NAVIGATION.TYPE.UP); - } else { - Navigation.navigate(ROUTES.SIGN_IN_MODAL); + if (shouldResetToHome) { + Navigation.resetToHome(); } + Navigation.navigate(ROUTES.SIGN_IN_MODAL); Linking.getInitialURL().then((url) => { const reportID = ReportUtils.getReportIDFromLink(url); if (reportID) { @@ -987,6 +993,7 @@ export { toggleTwoFactorAuth, validateTwoFactorAuth, waitForUserSignIn, + hasAuthToken, canAnonymousUserAccessRoute, signInWithSupportAuthToken, isSupportAuthToken, diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 3e00d8084825..48ab7cce9186 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -689,8 +689,8 @@ function clearOutTaskInfoAndNavigate(reportID: string) { /** * Get the assignee data */ -function getAssignee(assigneeAccountID: number, personalDetails: OnyxTypes.PersonalDetailsList): Assignee { - const details = personalDetails[assigneeAccountID]; +function getAssignee(assigneeAccountID: number, personalDetails: OnyxEntry): Assignee { + const details = personalDetails?.[assigneeAccountID]; if (!details) { return { @@ -710,7 +710,7 @@ function getAssignee(assigneeAccountID: number, personalDetails: OnyxTypes.Perso /** * Get the share destination data * */ -function getShareDestination(reportID: string, reports: OnyxCollection, personalDetails: OnyxTypes.PersonalDetailsList): ShareDestination { +function getShareDestination(reportID: string, reports: OnyxCollection, personalDetails: OnyxEntry): ShareDestination { const report = reports?.[`report_${reportID}`] ?? null; const participantAccountIDs = report?.participantAccountIDs ?? []; @@ -721,8 +721,8 @@ function getShareDestination(reportID: string, reports: OnyxCollection; + /** All of the personal details for everyone */ + personalDetails: OnyxEntry; + + betas: OnyxEntry; + + /** An object that holds data about which referral banners have been dismissed */ + dismissedReferralBanners: DismissedReferralBanners; + /** Whether we are searching for reports in the server */ isSearchingForReports: OnyxEntry; }; @@ -46,126 +46,119 @@ type NewChatPageProps = NewChatPageWithOnyxProps & { const excludedGroupEmails = CONST.EXPENSIFY_EMAILS.filter((value) => value !== CONST.EMAIL.CONCIERGE); -const EMPTY_ARRAY: Array>> = []; - -function useOptions({reports, isGroupChat}: Omit) { - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const personalDetails = usePersonalDetails(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); - const [selectedOptions, setSelectedOptions] = useState>([]); - const betas = useBetas(); - - const options = useMemo(() => { - const filteredOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, - betas, - debouncedSearchTerm, - selectedOptions, - isGroupChat ? excludedGroupEmails : [], - false, - true, - false, - {}, - [], - false, - {}, - [], - true, - false, - ); - const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; - - const headerMessage = OptionsListUtils.getHeaderMessage( - filteredOptions.personalDetails.length + filteredOptions.recentReports.length !== 0, - Boolean(filteredOptions.userToInvite), - debouncedSearchTerm.trim(), - maxParticipantsReached, - selectedOptions.some((participant) => participant?.searchText?.toLowerCase?.().includes(debouncedSearchTerm.trim().toLowerCase())), - ); - return {...filteredOptions, headerMessage, maxParticipantsReached}; - }, [betas, debouncedSearchTerm, isGroupChat, personalDetails, reports, selectedOptions]); - - useEffect(() => { - if (!debouncedSearchTerm.length || options.maxParticipantsReached) { - return; - } - - Report.searchInServer(debouncedSearchTerm); - }, [debouncedSearchTerm, options.maxParticipantsReached]); - - return {...options, searchTerm, debouncedSearchTerm, setSearchTerm, isOptionsDataReady, selectedOptions, setSelectedOptions}; -} - -function NewChatPage({isGroupChat, reports, isSearchingForReports}: NewChatPageProps) { +function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingForReports, dismissedReferralBanners}: NewChatPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [filteredRecentReports, setFilteredRecentReports] = useState([]); + const [filteredPersonalDetails, setFilteredPersonalDetails] = useState([]); + const [filteredUserToInvite, setFilteredUserToInvite] = useState(); + const [selectedOptions, setSelectedOptions] = useState([]); const {isOffline} = useNetwork(); const {isSmallScreenWidth} = useWindowDimensions(); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + + const maxParticipantsReached = selectedOptions.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; + const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); - const { - headerMessage, + const headerMessage = OptionsListUtils.getHeaderMessage( + filteredPersonalDetails.length + filteredRecentReports.length !== 0, + Boolean(filteredUserToInvite), + searchTerm.trim(), maxParticipantsReached, - searchTerm, - debouncedSearchTerm, - setSearchTerm, - selectedOptions, - setSelectedOptions, - recentReports, - personalDetails, - userToInvite, - isOptionsDataReady, - } = useOptions({ - reports, - isGroupChat, - }); - - const sections = useMemo(() => { - const sectionsList = []; + selectedOptions.some((participant) => participant?.searchText?.toLowerCase().includes(searchTerm.trim().toLowerCase())), + ); + + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); + + const sections = useMemo((): OptionsListUtils.CategorySection[] => { + const sectionsList: OptionsListUtils.CategorySection[] = []; let indexOffset = 0; - const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(debouncedSearchTerm, selectedOptions, recentReports, personalDetails, true, indexOffset); + const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(searchTerm, selectedOptions, filteredRecentReports, filteredPersonalDetails, maxParticipantsReached, indexOffset); sectionsList.push(formatResults.section); + indexOffset = formatResults.newIndexOffset; if (maxParticipantsReached) { - return sectionsList as unknown as Array>>; + return sectionsList; } sectionsList.push({ title: translate('common.recents'), - data: recentReports, - shouldShow: !isEmpty(recentReports), + data: filteredRecentReports, + shouldShow: filteredRecentReports.length > 0, indexOffset, }); - indexOffset += recentReports.length; + indexOffset += filteredRecentReports.length; sectionsList.push({ title: translate('common.contacts'), - data: personalDetails, - shouldShow: !isEmpty(personalDetails), + data: filteredPersonalDetails, + shouldShow: filteredPersonalDetails.length > 0, indexOffset, }); - indexOffset += personalDetails.length; + indexOffset += filteredPersonalDetails.length; - if (userToInvite) { + if (filteredUserToInvite) { sectionsList.push({ title: undefined, - data: [userToInvite], + data: [filteredUserToInvite], shouldShow: true, indexOffset, }); } - return sectionsList as unknown as Array>>; - }, [debouncedSearchTerm, selectedOptions, recentReports, personalDetails, maxParticipantsReached, translate, userToInvite]); + return sectionsList; + }, [translate, filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, maxParticipantsReached, selectedOptions, searchTerm]); + + /** + * Removes a selected option from list if already selected. If not already selected add this option to the list. + */ + const toggleOption = (option: OptionData) => { + const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option.login); + + let newSelectedOptions; + + if (isOptionInList) { + newSelectedOptions = selectedOptions.filter((selectedOption) => selectedOption.login !== option.login); + } else { + newSelectedOptions = [...selectedOptions, option]; + } + + const { + recentReports, + personalDetails: newChatPersonalDetails, + userToInvite, + } = OptionsListUtils.getFilteredOptions( + reports, + personalDetails, + betas ?? [], + searchTerm, + newSelectedOptions, + isGroupChat ? excludedGroupEmails : [], + false, + true, + false, + {}, + [], + false, + {}, + [], + true, + ); + + setSelectedOptions(newSelectedOptions); + setFilteredRecentReports(recentReports); + setFilteredPersonalDetails(newChatPersonalDetails); + setFilteredUserToInvite(userToInvite); + }; /** * Creates a new 1:1 chat with the option and the current user, * or navigates to the existing chat if one with those participants already exists. */ - const createChat = (option: ListItem) => { + const createChat = (option: OptionData) => { if (!option.login) { return; } @@ -173,141 +166,139 @@ function NewChatPage({isGroupChat, reports, isSearchingForReports}: NewChatPageP }; /** - * This hook is used to set the state of didScreenTransitionEnd to true after the screen has transitioned. - * This is used to prevent the screen from rendering sections until transition has ended. + * Creates a new group chat with all the selected options and the current user, + * or navigates to the existing chat if one with those participants already exists. */ - useFocusEffect( - React.useCallback(() => { - const task = InteractionManager.runAfterInteractions(() => { - setDidScreenTransitionEnd(true); - }); - - return () => task.cancel(); - }, []), - ); + const createGroup = () => { + const logins = selectedOptions.map((option) => option.login).filter((login): login is string => typeof login === 'string'); - const itemRightSideComponent = useCallback( - (listItem: ListItem) => { - const item = listItem as ListItem & OptionData; - /** - * Removes a selected option from list if already selected. If not already selected add this option to the list. - * @param option - */ - function toggleOption(option: ListItem & OptionData) { - const isOptionInList = !!option.isSelected; - - let newSelectedOptions: Array; - - if (isOptionInList) { - newSelectedOptions = reject(selectedOptions, (selectedOption) => selectedOption.login === option.login); - } else { - newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true}]; - } - - setSelectedOptions(newSelectedOptions); - } + if (logins.length < 1) { + return; + } - if (item.isSelected) { - return ( - toggleOption(item)} - disabled={item.isDisabled} - role={CONST.ACCESSIBILITY_ROLE.CHECKBOX} - accessibilityLabel={CONST.ACCESSIBILITY_ROLE.CHECKBOX} - style={[styles.flexRow, styles.alignItemsCenter, styles.ml3]} - > - - - ); - } + Report.navigateToAndOpenReport(logins); + }; - return ( -