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 (
-
diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx
index 82402d62a074..89d20287f66c 100644
--- a/src/pages/RoomMembersPage.tsx
+++ b/src/pages/RoomMembersPage.tsx
@@ -224,7 +224,9 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) {
testID={RoomMembersPage.displayName}
>
{
Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/SearchPage/SearchPageFooter.tsx
index a9369ff9f0b7..3d5ebfd2c193 100644
--- a/src/pages/SearchPage/SearchPageFooter.tsx
+++ b/src/pages/SearchPage/SearchPageFooter.tsx
@@ -1,9 +1,17 @@
import React from 'react';
+import {View} from 'react-native';
import ReferralProgramCTA from '@components/ReferralProgramCTA';
+import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
function SearchPageFooter() {
- return ;
+ const themeStyles = useThemeStyles();
+
+ return (
+
+
+
+ );
}
SearchPageFooter.displayName = 'SearchPageFooter';
diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx
index 3d46ede8b31e..2eb5ecaf373f 100644
--- a/src/pages/WorkspaceSwitcherPage.tsx
+++ b/src/pages/WorkspaceSwitcherPage.tsx
@@ -18,6 +18,7 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -102,7 +103,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) {
return;
}
- const {policyID, isPolicyAdmin} = option;
+ const {policyID} = option;
if (policyID) {
setSelectedOption(option);
@@ -112,7 +113,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) {
setActiveWorkspaceID(policyID);
Navigation.goBack();
if (policyID !== activeWorkspaceID) {
- Navigation.navigateWithSwitchPolicyID({policyID, isPolicyAdmin});
+ Navigation.navigateWithSwitchPolicyID({policyID});
}
},
[activeWorkspaceID, setActiveWorkspaceID],
@@ -219,7 +220,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) {
role={CONST.ROLE.BUTTON}
onPress={() => {
Navigation.goBack();
- App.createWorkspaceWithPolicyDraftAndNavigateToIt();
+ interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt());
}}
>
{({hovered}) => (
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 7561f02e9aaa..d046bce95d2b 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -464,7 +464,7 @@ function ReportScreen({
}, [report, didSubscribeToReportLeavingEvents, reportIDFromRoute]);
const onListLayout = useCallback((event: LayoutChangeEvent) => {
- setListHeight((prev) => event.nativeEvent.layout.height ?? prev);
+ setListHeight((prev) => event.nativeEvent?.layout?.height ?? prev);
if (!markReadyForHydration) {
return;
}
diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
index 931b87704ce5..793fbf9b1e7e 100644
--- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
@@ -3,7 +3,7 @@ import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef,
/* eslint-disable no-restricted-imports */
import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native';
-import {Dimensions} from 'react-native';
+import {DeviceEventEmitter, Dimensions} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import ConfirmModal from '@components/ConfirmModal';
import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent';
@@ -30,7 +30,6 @@ function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEv
return event;
}
-// eslint-disable-next-line @typescript-eslint/naming-convention
function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef) {
const {translate} = useLocalize();
const reportIDRef = useRef('0');
@@ -262,6 +261,8 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef
<>
- {shouldShowAddCreditBankAccountButton && (
+ {missingPaymentMethod === 'bankAccount' && (
)}
-
- {shouldShowEnableWalletButton && (
+ {missingPaymentMethod === 'wallet' && (
Navigation.navigate(ROUTES.ENABLE_PAYMENTS)}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx
index 8419df479bef..7b617e7a3863 100644
--- a/src/pages/home/report/ReportActionsList.tsx
+++ b/src/pages/home/report/ReportActionsList.tsx
@@ -283,9 +283,18 @@ function ReportActionsList({
setMessageManuallyMarkedUnread(0);
});
+ const deletedReportActionSubscription = DeviceEventEmitter.addListener(`deletedReportAction_${report.reportID}`, (reportActionID) => {
+ if (cacheUnreadMarkers.get(report.reportID) !== reportActionID) {
+ return;
+ }
+
+ setMessageManuallyMarkedUnread(new Date().getTime());
+ });
+
return () => {
unreadActionSubscription.remove();
readNewestActionSubscription.remove();
+ deletedReportActionSubscription.remove();
};
}, [report.reportID]);
diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.js b/src/pages/home/sidebar/AvatarWithOptionalStatus.js
index e1ff3982a0cc..942d5c1da1f2 100644
--- a/src/pages/home/sidebar/AvatarWithOptionalStatus.js
+++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.js
@@ -1,61 +1,53 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
import PropTypes from 'prop-types';
-import React, {useCallback} from 'react';
+import React from 'react';
import {View} from 'react-native';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Text from '@components/Text';
-import Tooltip from '@components/Tooltip';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
-import ROUTES from '@src/ROUTES';
import PressableAvatarWithIndicator from './PressableAvatarWithIndicator';
const propTypes = {
- /** Whether the create menu is open or not */
- isCreateMenuOpen: PropTypes.bool,
-
/** Emoji status */
emojiStatus: PropTypes.string,
+
+ /** Whether the avatar is selected */
+ isSelected: PropTypes.bool,
+
+ /** Callback called when the avatar or status icon is pressed */
+ onPress: PropTypes.func,
};
const defaultProps = {
- isCreateMenuOpen: false,
emojiStatus: '',
+ isSelected: false,
+ onPress: () => {},
};
-function AvatarWithOptionalStatus({emojiStatus, isCreateMenuOpen}) {
+function AvatarWithOptionalStatus({emojiStatus, isSelected, onPress}) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const showStatusPage = useCallback(() => {
- if (isCreateMenuOpen) {
- // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon
- return;
- }
-
- Navigation.setShouldPopAllStateOnUP();
- Navigation.navigate(ROUTES.SETTINGS_STATUS);
- }, [isCreateMenuOpen]);
-
return (
-
+
-
-
- {emojiStatus}
-
-
+
+ {emojiStatus}
+
);
diff --git a/src/pages/home/sidebar/BottomTabAvatar.tsx b/src/pages/home/sidebar/BottomTabAvatar.tsx
new file mode 100644
index 000000000000..15134b762161
--- /dev/null
+++ b/src/pages/home/sidebar/BottomTabAvatar.tsx
@@ -0,0 +1,49 @@
+/* eslint-disable rulesdir/onyx-props-must-have-default */
+import React, {useCallback} from 'react';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import interceptAnonymousUser from '@libs/interceptAnonymousUser';
+import Navigation from '@libs/Navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+import AvatarWithOptionalStatus from './AvatarWithOptionalStatus';
+import PressableAvatarWithIndicator from './PressableAvatarWithIndicator';
+
+type BottomTabAvatarProps = {
+ /** Whether the create menu is open or not */
+ isCreateMenuOpen?: boolean;
+
+ /** Whether the avatar is selected */
+ isSelected?: boolean;
+};
+
+function BottomTabAvatar({isCreateMenuOpen = false, isSelected = false}: BottomTabAvatarProps) {
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const emojiStatus = currentUserPersonalDetails?.status?.emojiCode ?? '';
+
+ const showSettingsPage = useCallback(() => {
+ if (isCreateMenuOpen) {
+ // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon
+ return;
+ }
+
+ interceptAnonymousUser(() => Navigation.navigate(ROUTES.SETTINGS));
+ }, [isCreateMenuOpen]);
+
+ if (emojiStatus) {
+ return (
+
+ );
+ }
+ return (
+
+ );
+}
+
+BottomTabAvatar.displayName = 'BottomTabAvatar';
+export default BottomTabAvatar;
diff --git a/src/pages/home/sidebar/PressableAvatarWithIndicator.js b/src/pages/home/sidebar/PressableAvatarWithIndicator.js
index 63c5936e957b..a7345ff6c14a 100644
--- a/src/pages/home/sidebar/PressableAvatarWithIndicator.js
+++ b/src/pages/home/sidebar/PressableAvatarWithIndicator.js
@@ -1,67 +1,65 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useCallback} from 'react';
+import React from 'react';
+import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import AvatarWithIndicator from '@components/AvatarWithIndicator';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
-import Navigation from '@libs/Navigation/Navigation';
import * as UserUtils from '@libs/UserUtils';
import personalDetailsPropType from '@pages/personalDetailsPropType';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
const propTypes = {
- /** Whether the create menu is open or not */
- isCreateMenuOpen: PropTypes.bool,
-
/** The personal details of the person who is logged in */
currentUserPersonalDetails: personalDetailsPropType,
/** Indicates whether the app is loading initial data */
isLoading: PropTypes.bool,
+
+ /** Whether the avatar is selected */
+ isSelected: PropTypes.bool,
+
+ /** Callback called when the avatar is pressed */
+ onPress: PropTypes.func,
};
const defaultProps = {
- isCreateMenuOpen: false,
currentUserPersonalDetails: {
pendingFields: {avatar: ''},
accountID: '',
avatar: '',
},
isLoading: true,
+ isSelected: false,
+ onPress: () => {},
};
-function PressableAvatarWithIndicator({isCreateMenuOpen, currentUserPersonalDetails, isLoading}) {
+function PressableAvatarWithIndicator({currentUserPersonalDetails, isLoading, isSelected, onPress}) {
const {translate} = useLocalize();
-
- const showSettingsPage = useCallback(() => {
- if (isCreateMenuOpen) {
- // Prevent opening Settings page when click profile avatar quickly after clicking FAB icon
- return;
- }
-
- Navigation.navigate(ROUTES.SETTINGS);
- }, [isCreateMenuOpen]);
+ const styles = useThemeStyles();
return (
-
+
+
+
);
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index e872bbad008a..4d1585cd424a 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -1,11 +1,8 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
-import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import {InteractionManager, StyleSheet, View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
-import Breadcrumbs from '@components/Breadcrumbs';
import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList';
import OptionsListSkeletonView from '@components/OptionsListSkeletonView';
import useLocalize from '@hooks/useLocalize';
@@ -43,11 +40,11 @@ const propTypes = {
isActiveReport: PropTypes.func.isRequired,
};
-function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen, activePolicy}) {
+function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen}) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const modal = useRef({});
- const {translate, updateLocale} = useLocalize();
+ const {updateLocale} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
useEffect(() => {
@@ -129,22 +126,6 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
return (
-
`${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`,
- },
-})(SidebarLinks);
+export default SidebarLinks;
export {basePropTypes};
diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
index 9188a859d175..2c2d28a0edbc 100644
--- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
+++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js
@@ -1,8 +1,10 @@
import React, {useEffect} from 'react';
import {View} from 'react-native';
import ScreenWrapper from '@components/ScreenWrapper';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Browser from '@libs/Browser';
+import TopBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator/TopBar';
import Performance from '@libs/Performance';
import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData';
import Timing from '@userActions/Timing';
@@ -19,6 +21,7 @@ const startTimer = () => {
function BaseSidebarScreen(props) {
const styles = useThemeStyles();
+ const {activeWorkspaceID} = useActiveWorkspace();
useEffect(() => {
Performance.markStart(CONST.TIMING.SIDEBAR_LOADED);
Timing.start(CONST.TIMING.SIDEBAR_LOADED, true);
@@ -33,13 +36,16 @@ function BaseSidebarScreen(props) {
includePaddingTop={false}
>
{({insets}) => (
-
-
-
+ <>
+
+
+
+
+ >
)}
);
diff --git a/src/pages/home/sidebar/SidebarScreen/index.js b/src/pages/home/sidebar/SidebarScreen/index.js
index 7086e8a8561a..dfb5db7c15d3 100755
--- a/src/pages/home/sidebar/SidebarScreen/index.js
+++ b/src/pages/home/sidebar/SidebarScreen/index.js
@@ -1,14 +1,11 @@
import React from 'react';
-import useWindowDimensions from '@hooks/useWindowDimensions';
import FreezeWrapper from '@libs/Navigation/FreezeWrapper';
import BaseSidebarScreen from './BaseSidebarScreen';
import sidebarPropTypes from './sidebarPropTypes';
function SidebarScreen(props) {
- const {isSmallScreenWidth} = useWindowDimensions();
-
return (
-
+
;
- }
- if (emojiStatus) {
- return (
-
- );
- }
- return ;
-}
-
-SignInOrAvatarWithOptionalStatus.propTypes = propTypes;
-SignInOrAvatarWithOptionalStatus.defaultProps = defaultProps;
-SignInOrAvatarWithOptionalStatus.displayName = 'SignInOrAvatarWithOptionalStatus';
-export default SignInOrAvatarWithOptionalStatus;
diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
index 01b6b0f4d21c..fb3a4d9457d5 100644
--- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
+++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js
@@ -28,6 +28,9 @@ const propTypes = {
/** Beta features list */
betas: PropTypes.arrayOf(PropTypes.string),
+ /** An object that holds data about which referral banners have been dismissed */
+ dismissedReferralBanners: PropTypes.objectOf(PropTypes.bool),
+
/** Callback to request parent modal to go to next step, which should be split */
onFinish: PropTypes.func.isRequired,
@@ -66,6 +69,7 @@ const defaultProps = {
safeAreaPaddingBottomStyle: {},
reports: {},
betas: [],
+ dismissedReferralBanners: {},
didScreenTransitionEnd: false,
};
@@ -78,6 +82,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
safeAreaPaddingBottomStyle,
iouType,
iouRequestType,
+ dismissedReferralBanners,
didScreenTransitionEnd,
}) {
const {translate} = useLocalize();
@@ -263,10 +268,11 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
const footerContent = useMemo(
() => (
-
+ {!dismissedReferralBanners[referralContentType] && (
+
+
+
+ )}
{shouldShowSplitBillErrorMessage && (
),
- [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, styles, translate],
+ [handleConfirmSelection, participants.length, dismissedReferralBanners, referralContentType, shouldShowSplitBillErrorMessage, styles, translate],
);
const itemRightSideComponent = useCallback(
@@ -352,6 +358,10 @@ MoneyTemporaryForRefactorRequestParticipantsSelector.defaultProps = defaultProps
MoneyTemporaryForRefactorRequestParticipantsSelector.displayName = 'MoneyTemporaryForRefactorRequestParticipantsSelector';
export default withOnyx({
+ dismissedReferralBanners: {
+ key: ONYXKEYS.ACCOUNT,
+ selector: (data) => data.dismissedReferralBanners || {},
+ },
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
},
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
index ca52053eca33..1ad6488aeee9 100755
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
@@ -36,6 +36,9 @@ const propTypes = {
/** Callback to add participants in MoneyRequestModal */
onAddParticipants: PropTypes.func.isRequired,
+ /** An object that holds data about which referral banners have been dismissed */
+ dismissedReferralBanners: PropTypes.objectOf(PropTypes.bool),
+
/** Selected participants from MoneyRequestModal with login */
participants: PropTypes.arrayOf(
PropTypes.shape({
@@ -64,6 +67,7 @@ const propTypes = {
};
const defaultProps = {
+ dismissedReferralBanners: {},
participants: [],
safeAreaPaddingBottomStyle: {},
reports: {},
@@ -74,6 +78,7 @@ const defaultProps = {
function MoneyRequestParticipantsSelector({
betas,
+ dismissedReferralBanners,
participants,
reports,
navigateToRequest,
@@ -281,10 +286,11 @@ function MoneyRequestParticipantsSelector({
const footerContent = useMemo(
() => (
-
+ {!dismissedReferralBanners[referralContentType] && (
+
+
+
+ )}
{shouldShowSplitBillErrorMessage && (
),
- [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, styles, translate],
+ [handleConfirmSelection, participants.length, dismissedReferralBanners, referralContentType, shouldShowSplitBillErrorMessage, styles, translate],
);
const itemRightSideComponent = useCallback(
@@ -365,6 +371,10 @@ MoneyRequestParticipantsSelector.displayName = 'MoneyRequestParticipantsSelector
MoneyRequestParticipantsSelector.defaultProps = defaultProps;
export default withOnyx({
+ dismissedReferralBanners: {
+ key: ONYXKEYS.ACCOUNT,
+ selector: (data) => data.dismissedReferralBanners || {},
+ },
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
},
diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx
index 2f2343027cf0..a57f308b5623 100755
--- a/src/pages/settings/InitialSettingsPage.tsx
+++ b/src/pages/settings/InitialSettingsPage.tsx
@@ -1,23 +1,24 @@
-import {useNavigationState} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
import ConfirmModal from '@components/ConfirmModal';
import CurrentUserPersonalDetailsSkeletonView from '@components/CurrentUserPersonalDetailsSkeletonView';
-import HeaderPageLayout from '@components/HeaderPageLayout';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {PressableWithFeedback} from '@components/Pressable';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
+import useActiveRoute from '@hooks/useActiveRoute';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useSingleExecution from '@hooks/useSingleExecution';
@@ -25,9 +26,10 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import * as CurrencyUtils from '@libs/CurrencyUtils';
-import getTopmostSettingsCentralPaneName from '@libs/Navigation/getTopmostSettingsCentralPaneName';
import Navigation from '@libs/Navigation/Navigation';
+import shouldShowSubscriptionsMenu from '@libs/shouldShowSubscriptionsMenu';
import * as UserUtils from '@libs/UserUtils';
+import {hasGlobalWorkspaceSettingsRBR} from '@libs/WorkspacesSettingsUtils';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import variables from '@styles/variables';
import * as Link from '@userActions/Link';
@@ -40,7 +42,6 @@ import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import type {Icon as TIcon} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -64,6 +65,12 @@ type InitialSettingsPageOnyxProps = {
/** Login list for the user that is signed in */
loginList: OnyxEntry;
+
+ /** The policies which the user has access to */
+ policies: OnyxCollection;
+
+ /** Members of all the workspaces the user is member of */
+ policyMembers: OnyxCollection;
};
type InitialSettingsPageProps = InitialSettingsPageOnyxProps & WithCurrentUserPersonalDetailsProps;
@@ -88,7 +95,7 @@ type MenuData = {
type Menu = {sectionStyle: StyleProp; sectionTranslationKey: TranslationPaths; items: MenuData[]};
-function InitialSettingsPage({session, userWallet, bankAccountList, fundList, walletTerms, loginList, currentUserPersonalDetails}: InitialSettingsPageProps) {
+function InitialSettingsPage({session, userWallet, bankAccountList, fundList, walletTerms, loginList, currentUserPersonalDetails, policies, policyMembers}: InitialSettingsPageProps) {
const network = useNetwork();
const theme = useTheme();
const styles = useThemeStyles();
@@ -96,7 +103,7 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
const waitForNavigate = useWaitForNavigation();
const popoverAnchor = useRef(null);
const {translate, formatPhoneNumber} = useLocalize();
- const activeRoute = useNavigationState(getTopmostSettingsCentralPaneName);
+ const activeRoute = useActiveRoute();
const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? '';
const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false);
@@ -129,7 +136,6 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
const accountMenuItemsData: Menu = useMemo(() => {
const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(loginList);
const paymentCardList = fundList;
- const signOutTranslationKey = Session.isSupportAuthToken() && Session.hasStashedSession() ? 'initialSettingsPage.restoreStashed' : 'initialSettingsPage.signOut';
const defaultMenu: Menu = {
sectionStyle: styles.accountSettingsSectionContainer,
sectionTranslationKey: 'initialSettingsPage.account',
@@ -164,32 +170,70 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
icon: Expensicons.Lock,
routeName: ROUTES.SETTINGS_SECURITY,
},
- {
- translationKey: signOutTranslationKey,
- icon: Expensicons.Exit,
- action: () => {
- signOut(false);
- },
- },
],
};
return defaultMenu;
- }, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors, signOut]);
+ }, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors]);
+
+ /**
+ * Retuns a list of menu items data for workspace section
+ * @returns object with translationKey, style and items for the workspace section
+ */
+ const workspaceMenuItemsData: Menu = useMemo(() => {
+ const items: MenuData[] = [
+ {
+ translationKey: 'common.workspaces',
+ icon: Expensicons.Building,
+ routeName: ROUTES.SETTINGS_WORKSPACES,
+ brickRoadIndicator: hasGlobalWorkspaceSettingsRBR(policies, policyMembers) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
+ },
+ {
+ translationKey: 'allSettingsScreen.cardsAndDomains',
+ icon: Expensicons.CardsAndDomains,
+ action: () => {
+ Link.openOldDotLink(CONST.OLDDOT_URLS.ADMIN_DOMAINS_URL);
+ },
+ shouldShowRightIcon: true,
+ iconRight: Expensicons.NewWindow,
+ link: () => Link.buildOldDotURL(CONST.OLDDOT_URLS.ADMIN_DOMAINS_URL),
+ },
+ ];
+
+ if (shouldShowSubscriptionsMenu) {
+ items.splice(1, 0, {
+ translationKey: 'allSettingsScreen.subscriptions',
+ icon: Expensicons.MoneyBag,
+ action: () => {
+ Link.openOldDotLink(CONST.OLDDOT_URLS.ADMIN_POLICIES_URL);
+ },
+ shouldShowRightIcon: true,
+ iconRight: Expensicons.NewWindow,
+ link: () => Link.buildOldDotURL(CONST.OLDDOT_URLS.ADMIN_POLICIES_URL),
+ });
+ }
+
+ return {
+ sectionStyle: styles.workspaceSettingsSectionContainer,
+ sectionTranslationKey: 'common.workspaces',
+ items,
+ };
+ }, [policies, policyMembers, styles.workspaceSettingsSectionContainer]);
/**
* Retuns a list of menu items data for general section
* @returns object with translationKey, style and items for the general section
*/
- const generalMenuItemsData: Menu = useMemo(
- () => ({
+ const generalMenuItemsData: Menu = useMemo(() => {
+ const signOutTranslationKey = Session.isSupportAuthToken() && Session.hasStashedSession() ? 'initialSettingsPage.restoreStashed' : 'initialSettingsPage.signOut';
+ const defaultMenu: Menu = {
sectionStyle: {
...styles.pt4,
},
- sectionTranslationKey: 'initialSettingsPage.general' as const,
+ sectionTranslationKey: 'initialSettingsPage.general',
items: [
{
- translationKey: 'initialSettingsPage.help' as const,
+ translationKey: 'initialSettingsPage.help',
icon: Expensicons.QuestionMark,
action: () => {
Link.openExternalLink(CONST.NEWHELP_URL);
@@ -199,14 +243,22 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
link: CONST.NEWHELP_URL,
},
{
- translationKey: 'initialSettingsPage.about' as const,
+ translationKey: 'initialSettingsPage.about',
icon: Expensicons.Info,
routeName: ROUTES.SETTINGS_ABOUT,
},
+ {
+ translationKey: signOutTranslationKey,
+ icon: Expensicons.Exit,
+ action: () => {
+ signOut(false);
+ },
+ },
],
- }),
- [styles.pt4],
- );
+ };
+
+ return defaultMenu;
+ }, [styles.pt4, signOut]);
/**
* Retuns JSX.Element with menu items
@@ -291,13 +343,14 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
const accountMenuItems = useMemo(() => getMenuItemsSection(accountMenuItemsData), [accountMenuItemsData, getMenuItemsSection]);
const generalMenuItems = useMemo(() => getMenuItemsSection(generalMenuItemsData), [generalMenuItemsData, getMenuItemsSection]);
+ const workspaceMenuItems = useMemo(() => getMenuItemsSection(workspaceMenuItemsData), [workspaceMenuItemsData, getMenuItemsSection]);
const currentUserDetails = currentUserPersonalDetails;
const avatarURL = currentUserDetails?.avatar ?? '';
const accountID = currentUserDetails?.accountID ?? '';
const headerContent = (
-
+
{isEmptyObject(currentUserPersonalDetails) || currentUserPersonalDetails.displayName === undefined ? (
) : (
@@ -384,17 +437,15 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa
);
return (
- Navigation.closeFullScreen()}
- backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.ROOT].backgroundColor}
- childrenContainerStyles={[styles.m0, styles.p0]}
+
-
+
+ {headerContent}
{accountMenuItems}
+ {workspaceMenuItems}
{generalMenuItems}
signOut(true)}
onCancel={() => toggleSignoutConfirmModal(false)}
/>
-
-
+
+
);
}
@@ -433,5 +484,11 @@ export default withCurrentUserPersonalDetails(
session: {
key: ONYXKEYS.SESSION,
},
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
+ policyMembers: {
+ key: ONYXKEYS.COLLECTION.POLICY_MEMBERS,
+ },
})(InitialSettingsPage),
);
diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
index 88236e06f9a9..c28a0f6649e4 100644
--- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
+++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx
@@ -396,7 +396,6 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi
)}
navigateToWalletOrTransferBalancePage(source)}
onSelectPaymentMethod={(selectedPaymentMethod: string) => {
if (hasActivatedWallet || selectedPaymentMethod !== CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
diff --git a/src/pages/tasks/NewTaskDescriptionPage.js b/src/pages/tasks/NewTaskDescriptionPage.tsx
similarity index 60%
rename from src/pages/tasks/NewTaskDescriptionPage.js
rename to src/pages/tasks/NewTaskDescriptionPage.tsx
index dbcb10d47f39..a5eb79497707 100644
--- a/src/pages/tasks/NewTaskDescriptionPage.js
+++ b/src/pages/tasks/NewTaskDescriptionPage.tsx
@@ -1,58 +1,50 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapperWithRef from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
+import type {NewTaskNavigatorParamList} from '@libs/Navigation/types';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
-import * as Task from '@userActions/Task';
+import * as TaskActions from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/NewTaskForm';
+import type {Task} from '@src/types/onyx';
-const propTypes = {
- /** Grab the Share description of the Task */
- task: PropTypes.shape({
- /** Description of the Task */
- description: PropTypes.string,
- }),
-
- ...withLocalizePropTypes,
+type NewTaskDescriptionPageOnyxProps = {
+ /** Task Creation Data */
+ task: OnyxEntry;
};
-const defaultProps = {
- task: {
- description: '',
- },
-};
+type NewTaskDescriptionPageProps = NewTaskDescriptionPageOnyxProps & StackScreenProps;
const parser = new ExpensiMark();
-function NewTaskDescriptionPage(props) {
+function NewTaskDescriptionPage({task}: NewTaskDescriptionPageProps) {
const styles = useThemeStyles();
+ const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
- const onSubmit = (values) => {
- Task.setDescriptionValue(values.taskDescription);
+ const onSubmit = (values: FormOnyxValues) => {
+ TaskActions.setDescriptionValue(values.taskDescription);
Navigation.goBack(ROUTES.NEW_TASK);
};
- /**
- * @param {Object} values - form input values passed by the Form component
- * @returns {Boolean}
- */
- function validate(values) {
+ const validate = (values: FormOnyxValues): FormInputErrors => {
const errors = {};
if (values.taskDescription.length > CONST.DESCRIPTION_LIMIT) {
@@ -60,7 +52,7 @@ function NewTaskDescriptionPage(props) {
}
return errors;
- }
+ };
return (
<>
Task.dismissModalAndClearOutTaskInfo()}
+ title={translate('task.description')}
+ onCloseButtonPress={() => TaskActions.dismissModalAndClearOutTaskInfo()}
onBackButtonPress={() => Navigation.goBack(ROUTES.NEW_TASK)}
/>
validate(values)}
- onSubmit={(values) => onSubmit(values)}
+ validate={validate}
+ onSubmit={onSubmit}
enabledWhenOffline
>
{
inputCallbackRef(el);
@@ -96,7 +88,7 @@ function NewTaskDescriptionPage(props) {
}}
autoGrowHeight
shouldSubmitForm
- containerStyles={[styles.autoGrowHeightMultilineInput]}
+ containerStyles={styles.autoGrowHeightMultilineInput}
/>
@@ -106,14 +98,9 @@ function NewTaskDescriptionPage(props) {
}
NewTaskDescriptionPage.displayName = 'NewTaskDescriptionPage';
-NewTaskDescriptionPage.propTypes = propTypes;
-NewTaskDescriptionPage.defaultProps = defaultProps;
-export default compose(
- withOnyx({
- task: {
- key: ONYXKEYS.TASK,
- },
- }),
- withLocalize,
-)(NewTaskDescriptionPage);
+export default withOnyx({
+ task: {
+ key: ONYXKEYS.TASK,
+ },
+})(NewTaskDescriptionPage);
diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.tsx
similarity index 58%
rename from src/pages/tasks/NewTaskDetailsPage.js
rename to src/pages/tasks/NewTaskDetailsPage.tsx
index e4533b637dee..15612e20afd7 100644
--- a/src/pages/tasks/NewTaskDetailsPage.js
+++ b/src/pages/tasks/NewTaskDetailsPage.tsx
@@ -1,58 +1,52 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
-import PropTypes from 'prop-types';
import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as Task from '@userActions/Task';
+import type {NewTaskNavigatorParamList} from '@libs/Navigation/types';
+import * as TaskActions from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/NewTaskForm';
+import type {Task} from '@src/types/onyx';
-const propTypes = {
- /** Task title and description data */
- task: PropTypes.shape({
- title: PropTypes.string,
- description: PropTypes.string,
- }),
-
- ...withLocalizePropTypes,
+type NewTaskDetailsPageOnyxProps = {
+ /** Task Creation Data */
+ task: OnyxEntry;
};
-const defaultProps = {
- task: {},
-};
+type NewTaskDetailsPageProps = NewTaskDetailsPageOnyxProps & StackScreenProps;
const parser = new ExpensiMark();
-function NewTaskDetailsPage(props) {
+function NewTaskDetailsPage({task}: NewTaskDetailsPageProps) {
const styles = useThemeStyles();
- const [taskTitle, setTaskTitle] = useState(props.task.title);
- const [taskDescription, setTaskDescription] = useState(props.task.description || '');
+ const {translate} = useLocalize();
+ const [taskTitle, setTaskTitle] = useState(task?.title ?? '');
+ const [taskDescription, setTaskDescription] = useState(task?.description ?? '');
const {inputCallbackRef} = useAutoFocusInput();
useEffect(() => {
- setTaskTitle(props.task.title);
- setTaskDescription(parser.htmlToMarkdown(parser.replace(props.task.description || '')));
- }, [props.task]);
+ setTaskTitle(task?.title ?? '');
+ setTaskDescription(parser.htmlToMarkdown(parser.replace(task?.description ?? '')));
+ }, [task]);
- /**
- * @param {Object} values - form input values passed by the Form component
- * @returns {Boolean}
- */
- function validate(values) {
+ const validate = (values: FormOnyxValues): FormInputErrors => {
const errors = {};
if (!values.taskTitle) {
@@ -66,14 +60,14 @@ function NewTaskDetailsPage(props) {
}
return errors;
- }
+ };
// On submit, we want to call the assignTask function and wait to validate
// the response
- function onSubmit(values) {
- Task.setDetailsValue(values.taskTitle, values.taskDescription);
+ const onSubmit = (values: FormOnyxValues) => {
+ TaskActions.setDetailsValue(values.taskTitle, values.taskDescription);
Navigation.navigate(ROUTES.NEW_TASK);
- }
+ };
return (
Task.dismissModalAndClearOutTaskInfo()}
+ title={translate('newTaskPage.assignTask')}
+ onCloseButtonPress={() => TaskActions.dismissModalAndClearOutTaskInfo()}
shouldShowBackButton
- onBackButtonPress={() => Task.dismissModalAndClearOutTaskInfo()}
+ onBackButtonPress={() => TaskActions.dismissModalAndClearOutTaskInfo()}
/>
validate(values)}
- onSubmit={(values) => onSubmit(values)}
+ validate={validate}
+ onSubmit={onSubmit}
enabledWhenOffline
>
setTaskTitle(value)}
+ onValueChange={setTaskTitle}
autoCorrect={false}
/>
setTaskDescription(value)}
+ onValueChange={setTaskDescription}
/>
@@ -129,14 +125,9 @@ function NewTaskDetailsPage(props) {
}
NewTaskDetailsPage.displayName = 'NewTaskDetailsPage';
-NewTaskDetailsPage.propTypes = propTypes;
-NewTaskDetailsPage.defaultProps = defaultProps;
-export default compose(
- withOnyx({
- task: {
- key: ONYXKEYS.TASK,
- },
- }),
- withLocalize,
-)(NewTaskDetailsPage);
+export default withOnyx({
+ task: {
+ key: ONYXKEYS.TASK,
+ },
+})(NewTaskDetailsPage);
diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js
deleted file mode 100644
index 352c08115114..000000000000
--- a/src/pages/tasks/NewTaskPage.js
+++ /dev/null
@@ -1,231 +0,0 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useEffect, useMemo, useState} from 'react';
-import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import MenuItem from '@components/MenuItem';
-import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
-import ScreenWrapper from '@components/ScreenWrapper';
-import ScrollView from '@components/ScrollView';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
-import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
-import Navigation from '@libs/Navigation/Navigation';
-import * as OptionsListUtils from '@libs/OptionsListUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import playSound, {SOUNDS} from '@libs/Sound';
-import reportPropTypes from '@pages/reportPropTypes';
-import * as Task from '@userActions/Task';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-
-const propTypes = {
- /** Task Creation Data */
- task: PropTypes.shape({
- assignee: PropTypes.string,
- shareDestination: PropTypes.string,
- title: PropTypes.string,
- description: PropTypes.string,
- parentReportID: PropTypes.string,
- }),
-
- /** All of the personal details for everyone */
- personalDetails: PropTypes.objectOf(
- PropTypes.shape({
- /** Display name of the person */
- displayName: PropTypes.string,
-
- /** Avatar URL of the person */
- avatar: PropTypes.string,
-
- /** Login of the person */
- login: PropTypes.string,
- }),
- ),
-
- /** All reports shared with the user */
- reports: PropTypes.objectOf(reportPropTypes),
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- task: {},
- personalDetails: {},
- reports: {},
-};
-
-function NewTaskPage(props) {
- const styles = useThemeStyles();
- const [assignee, setAssignee] = useState({});
- const assigneeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs([props.task.assigneeAccountID], props.personalDetails), false);
- const [shareDestination, setShareDestination] = useState({});
- const [title, setTitle] = useState('');
- const [description, setDescription] = useState('');
- const [errorMessage, setErrorMessage] = useState('');
- const [parentReport, setParentReport] = useState({});
-
- const isAllowedToCreateTask = useMemo(() => _.isEmpty(parentReport) || ReportUtils.isAllowedToComment(parentReport), [parentReport]);
-
- useEffect(() => {
- setErrorMessage('');
-
- // If we have an assignee, we want to set the assignee data
- // If there's an issue with the assignee chosen, we want to notify the user
- if (props.task.assignee) {
- const displayDetails = Task.getAssignee(props.task.assigneeAccountID, props.personalDetails);
- setAssignee(displayDetails);
- }
-
- // We only set the parentReportID if we are creating a task from a report
- // this allows us to go ahead and set that report as the share destination
- // and disable the share destination selector
- if (props.task.parentReportID) {
- Task.setShareDestinationValue(props.task.parentReportID);
- }
-
- // If we have a share destination, we want to set the parent report and
- // the share destination data
- if (props.task.shareDestination) {
- setParentReport(lodashGet(props.reports, `report_${props.task.shareDestination}`, {}));
- const displayDetails = Task.getShareDestination(props.task.shareDestination, props.reports, props.personalDetails);
- setShareDestination(displayDetails);
- }
-
- // If we have a title, we want to set the title
- if (!_.isUndefined(props.task.title)) {
- setTitle(props.task.title);
- }
-
- // If we have a description, we want to set the description
- if (!_.isUndefined(props.task.description)) {
- setDescription(props.task.description);
- }
- }, [props]);
-
- // On submit, we want to call the createTask function and wait to validate
- // the response
- function onSubmit() {
- if (!props.task.title && !props.task.shareDestination) {
- setErrorMessage('newTaskPage.confirmError');
- return;
- }
-
- if (!props.task.title) {
- setErrorMessage('newTaskPage.pleaseEnterTaskName');
- return;
- }
-
- if (!props.task.shareDestination) {
- setErrorMessage('newTaskPage.pleaseEnterTaskDestination');
- return;
- }
-
- playSound(SOUNDS.DONE);
- Task.createTaskAndNavigate(
- parentReport.reportID,
- props.task.title,
- props.task.description,
- props.task.assignee,
- props.task.assigneeAccountID,
- props.task.assigneeChatReport,
- parentReport.policyID,
- );
- }
-
- return (
-
- Task.dismissModalAndClearOutTaskInfo()}
- shouldShowLink={false}
- >
- Task.dismissModalAndClearOutTaskInfo()}
- shouldShowBackButton
- onBackButtonPress={() => {
- Navigation.goBack(ROUTES.NEW_TASK_DETAILS);
- }}
- />
-
-
-
- Navigation.navigate(ROUTES.NEW_TASK_TITLE)}
- shouldShowRightIcon
- />
- Navigation.navigate(ROUTES.NEW_TASK_DESCRIPTION)}
- shouldShowRightIcon
- shouldParseTitle
- numberOfLinesTitle={2}
- titleStyle={styles.flex1}
- />
-
-
-
- onSubmit()}
- enabledWhenOffline
- buttonText={props.translate('newTaskPage.confirmTask')}
- containerStyles={[styles.mh0, styles.mt5, styles.flex1, styles.ph5]}
- />
-
-
-
-
- );
-}
-
-NewTaskPage.displayName = 'NewTaskPage';
-NewTaskPage.propTypes = propTypes;
-NewTaskPage.defaultProps = defaultProps;
-
-export default compose(
- withOnyx({
- task: {
- key: ONYXKEYS.TASK,
- },
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- },
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- }),
- withLocalize,
-)(NewTaskPage);
diff --git a/src/pages/tasks/NewTaskPage.tsx b/src/pages/tasks/NewTaskPage.tsx
new file mode 100644
index 000000000000..64c46e75c91d
--- /dev/null
+++ b/src/pages/tasks/NewTaskPage.tsx
@@ -0,0 +1,215 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useEffect, useMemo, useState} from 'react';
+import {View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import MenuItem from '@components/MenuItem';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
+import Navigation from '@libs/Navigation/Navigation';
+import type {NewTaskNavigatorParamList} from '@libs/Navigation/types';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import playSound, {SOUNDS} from '@libs/Sound';
+import * as TaskActions from '@userActions/Task';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {PersonalDetailsList, Report, Task} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type NewTaskPageOnyxProps = {
+ /** Task Creation Data */
+ task: OnyxEntry;
+
+ /** All of the personal details for everyone */
+ personalDetails: OnyxEntry;
+
+ /** All reports shared with the user */
+ reports: OnyxCollection;
+};
+
+type NewTaskPageProps = NewTaskPageOnyxProps & StackScreenProps;
+
+function NewTaskPage({task, reports, personalDetails}: NewTaskPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const [assignee, setAssignee] = useState();
+ const assigneeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips(
+ OptionsListUtils.getPersonalDetailsForAccountIDs(task?.assigneeAccountID ? [task.assigneeAccountID] : [], personalDetails),
+ false,
+ );
+ const [shareDestination, setShareDestination] = useState();
+ const [title, setTitle] = useState('');
+ const [description, setDescription] = useState('');
+ const [errorMessage, setErrorMessage] = useState('');
+ const [parentReport, setParentReport] = useState>(null);
+
+ const isAllowedToCreateTask = useMemo(() => isEmptyObject(parentReport) || ReportUtils.isAllowedToComment(parentReport), [parentReport]);
+
+ useEffect(() => {
+ setErrorMessage('');
+
+ // If we have an assignee, we want to set the assignee data
+ // If there's an issue with the assignee chosen, we want to notify the user
+ if (task?.assignee) {
+ const displayDetails = TaskActions.getAssignee(task?.assigneeAccountID ?? -1, personalDetails);
+ setAssignee(displayDetails);
+ }
+
+ // We only set the parentReportID if we are creating a task from a report
+ // this allows us to go ahead and set that report as the share destination
+ // and disable the share destination selector
+ if (task?.parentReportID) {
+ TaskActions.setShareDestinationValue(task.parentReportID);
+ }
+
+ // If we have a share destination, we want to set the parent report and
+ // the share destination data
+ if (task?.shareDestination) {
+ setParentReport(reports?.[`report_${task.shareDestination}`] ?? null);
+ const displayDetails = TaskActions.getShareDestination(task.shareDestination, reports, personalDetails);
+ setShareDestination(displayDetails);
+ }
+
+ // If we have a title, we want to set the title
+ if (task?.title !== undefined) {
+ setTitle(task.title);
+ }
+
+ // If we have a description, we want to set the description
+ if (task?.description !== undefined) {
+ setDescription(task.description);
+ }
+ }, [personalDetails, reports, task?.assignee, task?.assigneeAccountID, task?.description, task?.parentReportID, task?.shareDestination, task?.title]);
+
+ // On submit, we want to call the createTask function and wait to validate
+ // the response
+ const onSubmit = () => {
+ if (!task?.title && !task?.shareDestination) {
+ setErrorMessage('newTaskPage.confirmError');
+ return;
+ }
+
+ if (!task.title) {
+ setErrorMessage('newTaskPage.pleaseEnterTaskName');
+ return;
+ }
+
+ if (!task.shareDestination) {
+ setErrorMessage('newTaskPage.pleaseEnterTaskDestination');
+ return;
+ }
+
+ playSound(SOUNDS.DONE);
+ TaskActions.createTaskAndNavigate(
+ parentReport?.reportID ?? '',
+ task.title,
+ task?.description ?? '',
+ task?.assignee ?? '',
+ task.assigneeAccountID,
+ task.assigneeChatReport,
+ parentReport?.policyID,
+ );
+ };
+
+ return (
+
+ TaskActions.dismissModalAndClearOutTaskInfo()}
+ shouldShowLink={false}
+ >
+ TaskActions.dismissModalAndClearOutTaskInfo()}
+ shouldShowBackButton
+ onBackButtonPress={() => {
+ Navigation.goBack(ROUTES.NEW_TASK_DETAILS);
+ }}
+ />
+
+
+
+ Navigation.navigate(ROUTES.NEW_TASK_TITLE)}
+ shouldShowRightIcon
+ />
+ Navigation.navigate(ROUTES.NEW_TASK_DESCRIPTION)}
+ shouldShowRightIcon
+ shouldParseTitle
+ numberOfLinesTitle={2}
+ titleStyle={styles.flex1}
+ />
+ Navigation.navigate(ROUTES.NEW_TASK_ASSIGNEE)}
+ shouldShowRightIcon
+ titleWithTooltips={assigneeTooltipDetails}
+ />
+ Navigation.navigate(ROUTES.NEW_TASK_SHARE_DESTINATION)}
+ interactive={!task?.parentReportID}
+ shouldShowRightIcon={!task?.parentReportID}
+ titleWithTooltips={shareDestination?.shouldUseFullTitleToDisplay ? shareDestination?.displayNamesWithTooltips : []}
+ />
+
+
+
+
+
+
+
+
+ );
+}
+
+NewTaskPage.displayName = 'NewTaskPage';
+
+export default withOnyx({
+ task: {
+ key: ONYXKEYS.TASK,
+ },
+ reports: {
+ key: ONYXKEYS.COLLECTION.REPORT,
+ },
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+})(NewTaskPage);
diff --git a/src/pages/tasks/NewTaskTitlePage.js b/src/pages/tasks/NewTaskTitlePage.tsx
similarity index 60%
rename from src/pages/tasks/NewTaskTitlePage.js
rename to src/pages/tasks/NewTaskTitlePage.tsx
index 5b1f180c97d6..582d2a5c6500 100644
--- a/src/pages/tasks/NewTaskTitlePage.js
+++ b/src/pages/tasks/NewTaskTitlePage.tsx
@@ -1,49 +1,41 @@
-import PropTypes from 'prop-types';
+import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
import FormProvider from '@components/Form/FormProvider';
import InputWrapperWithRef from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import * as Task from '@userActions/Task';
+import type {NewTaskNavigatorParamList} from '@libs/Navigation/types';
+import * as TaskActions from '@userActions/Task';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/NewTaskForm';
+import type {Task} from '@src/types/onyx';
-const propTypes = {
- /** Grab the Share title of the Task */
- task: PropTypes.shape({
- /** Title of the Task */
- title: PropTypes.string,
- }),
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- task: {
- title: '',
- },
+type NewTaskTitlePageOnyxProps = {
+ /** Task Creation Data */
+ task: OnyxEntry;
};
+type NewTaskTitlePageProps = NewTaskTitlePageOnyxProps & StackScreenProps;
-function NewTaskTitlePage(props) {
+function NewTaskTitlePage({task}: NewTaskTitlePageProps) {
const styles = useThemeStyles();
const {inputCallbackRef} = useAutoFocusInput();
- /**
- * @param {Object} values - form input values passed by the Form component
- * @returns {Boolean}
- */
- function validate(values) {
+ const {translate} = useLocalize();
+
+ const validate = (values: FormOnyxValues): FormInputErrors => {
const errors = {};
if (!values.taskTitle) {
@@ -54,14 +46,14 @@ function NewTaskTitlePage(props) {
}
return errors;
- }
+ };
// On submit, we want to call the assignTask function and wait to validate
// the response
- function onSubmit(values) {
- Task.setTitleValue(values.taskTitle);
+ const onSubmit = (values: FormOnyxValues) => {
+ TaskActions.setTitleValue(values.taskTitle);
Navigation.goBack(ROUTES.NEW_TASK);
- }
+ };
return (
Task.dismissModalAndClearOutTaskInfo()}
+ title={translate('task.title')}
+ onCloseButtonPress={() => TaskActions.dismissModalAndClearOutTaskInfo()}
shouldShowBackButton
onBackButtonPress={() => Navigation.goBack(ROUTES.NEW_TASK)}
/>
validate(values)}
- onSubmit={(values) => onSubmit(values)}
+ validate={validate}
+ onSubmit={onSubmit}
enabledWhenOffline
>
@@ -100,14 +92,9 @@ function NewTaskTitlePage(props) {
}
NewTaskTitlePage.displayName = 'NewTaskTitlePage';
-NewTaskTitlePage.propTypes = propTypes;
-NewTaskTitlePage.defaultProps = defaultProps;
-export default compose(
- withOnyx({
- task: {
- key: ONYXKEYS.TASK,
- },
- }),
- withLocalize,
-)(NewTaskTitlePage);
+export default withOnyx({
+ task: {
+ key: ONYXKEYS.TASK,
+ },
+})(NewTaskTitlePage);
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 240a148110f7..38828a0406ef 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -1,26 +1,28 @@
+import {useNavigationState} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import Breadcrumbs from '@components/Breadcrumbs';
import ConfirmModal from '@components/ConfirmModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
-import useActiveRoute from '@hooks/useActiveRoute';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
+import getTopmostWorkspacesCentralPaneName from '@libs/Navigation/getTopmostWorkspacesCentralPaneName';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
-import type {BottomTabNavigatorParamList} from '@navigation/types';
+import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';
+import type {FullScreenNavigatorParamList} from '@navigation/types';
import * as App from '@userActions/App';
import * as Policy from '@userActions/Policy';
import * as ReimbursementAccount from '@userActions/ReimbursementAccount';
@@ -48,7 +50,7 @@ type WorkspaceInitialPageOnyxProps = {
reimbursementAccount: OnyxEntry;
};
-type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInitialPageOnyxProps & StackScreenProps;
+type WorkspaceInitialPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInitialPageOnyxProps & StackScreenProps;
function dismissError(policyID: string) {
PolicyUtils.goBackFromInvalidPolicy();
@@ -62,8 +64,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && policy.errors);
const waitForNavigate = useWaitForNavigation();
const {singleExecution, isExecuting} = useSingleExecution();
- const activeRoute = useActiveRoute();
-
+ const activeRoute = useNavigationState(getTopmostWorkspacesCentralPaneName);
const {translate} = useLocalize();
const policyID = policy?.id ?? '';
@@ -160,6 +161,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
icon: Expensicons.Workflows,
action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)))),
routeName: SCREENS.WORKSPACE.WORKFLOWS,
+ brickRoadIndicator: !isEmptyObject(policy?.errorFields?.reimburserEmail ?? {}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
});
}
@@ -215,31 +217,38 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
// We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace
(PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy));
+ const policyAvatar = useMemo(() => {
+ if (!policy) {
+ return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR};
+ }
+
+ const avatar = policy?.avatar ? policy.avatar : getDefaultWorkspaceAvatar(policy?.name);
+ return {
+ source: avatar,
+ name: policy?.name ?? '',
+ type: CONST.ICON_TYPE_WORKSPACE,
+ };
+ }, [policy]);
+
return (
-
+
-
+
{/*
Ideally we should use MenuList component for MenuItems with singleExecution/Navigation actions.
In this case where user can click on workspace avatar or menu items, we need to have a check for `isExecuting`. So, we are directly mapping menuItems.
diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
index 72f08095b58a..df1d3cd63011 100644
--- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx
+++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx
@@ -85,7 +85,6 @@ function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsT
Keyboard.dismiss();
// Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
Policy.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, welcomeNote ?? '', route.params.policyID);
- Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {});
SearchInputManager.searchInput = '';
// Pop the invite message page before navigating to the members page.
Navigation.goBack();
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index 1aae3294be0d..20be7913e31e 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -1,3 +1,4 @@
+import {useIsFocused} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import lodashIsEqual from 'lodash/isEqual';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
@@ -18,7 +19,7 @@ import MessagesRow from '@components/MessagesRow';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import TableListItem from '@components/SelectionList/TableListItem';
-import type {ListItem} from '@components/SelectionList/types';
+import type {ListItem, SelectionListHandle} from '@components/SelectionList/types';
import Text from '@components/Text';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
@@ -31,7 +32,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
-import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
@@ -41,7 +42,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
-import type {PersonalDetailsList, PolicyMember, PolicyMembers, Session} from '@src/types/onyx';
+import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyMember, PolicyMembers, Session} from '@src/types/onyx';
import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
@@ -50,14 +51,18 @@ import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
type WorkspaceMembersPageOnyxProps = {
/** Personal details of all users */
personalDetails: OnyxEntry;
+
/** Session info for the currently logged in user. */
session: OnyxEntry;
+
+ /** An object containing the accountID for every invited user email */
+ invitedEmailsToAccountIDsDraft: OnyxEntry;
};
type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps &
WithCurrentUserPersonalDetailsProps &
WorkspaceMembersPageOnyxProps &
- StackScreenProps;
+ StackScreenProps;
/**
* Inverts an object, equivalent of _.invert
@@ -70,7 +75,16 @@ function invertObject(object: Record): Record {
type MemberOption = Omit & {accountID: number};
-function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, session, currentUserPersonalDetails, isLoadingReportData = true}: WorkspaceMembersPageProps) {
+function WorkspaceMembersPage({
+ policyMembers,
+ personalDetails,
+ invitedEmailsToAccountIDsDraft,
+ route,
+ policy,
+ session,
+ currentUserPersonalDetails,
+ isLoadingReportData = true,
+}: WorkspaceMembersPageProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [selectedEmployees, setSelectedEmployees] = useState([]);
@@ -91,6 +105,8 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
() => !isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers)),
[isOfflineAndNoMemberDataAvailable, personalDetails, policyMembers],
);
+ const selectionListRef = useRef(null);
+ const isFocused = useIsFocused();
/**
* Get filtered personalDetails list with current policyMembers
@@ -288,15 +304,17 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
/**
* Check if the policy member is deleted from the workspace
*/
- const isDeletedPolicyMember = (policyMember: PolicyMember): boolean =>
- !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyMember.errors);
+ const isDeletedPolicyMember = useCallback(
+ (policyMember: PolicyMember): boolean => !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyMember.errors),
+ [isOffline],
+ );
const policyOwner = policy?.owner;
const currentUserLogin = currentUserPersonalDetails.login;
const policyID = route.params.policyID;
const invitedPrimaryToSecondaryLogins = invertObject(policy?.primaryLoginsInvited ?? {});
- const getUsers = (): MemberOption[] => {
+ const getUsers = useCallback((): MemberOption[] => {
let result: MemberOption[] = [];
Object.entries(policyMembers ?? {}).forEach(([accountIDKey, policyMember]) => {
@@ -369,8 +387,40 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
result = result.sort((a, b) => (a.text ?? '').toLowerCase().localeCompare((b.text ?? '').toLowerCase()));
return result;
- };
- const data = getUsers();
+ }, [
+ StyleUtils,
+ currentUserLogin,
+ formatPhoneNumber,
+ invitedPrimaryToSecondaryLogins,
+ isDeletedPolicyMember,
+ isPolicyAdmin,
+ personalDetails,
+ policy?.owner,
+ policy?.ownerAccountID,
+ policyMembers,
+ policyOwner,
+ selectedEmployees,
+ session?.accountID,
+ styles.activeItemBadge,
+ styles.badgeBordered,
+ styles.justifyContentCenter,
+ styles.textStrong,
+ translate,
+ ]);
+
+ const data = useMemo(() => getUsers(), [getUsers]);
+
+ useEffect(() => {
+ if (!isFocused) {
+ return;
+ }
+ if (isEmptyObject(invitedEmailsToAccountIDsDraft) || accountIDs === prevAccountIDs) {
+ return;
+ }
+ const invitedEmails = Object.values(invitedEmailsToAccountIDsDraft).map(String);
+ selectionListRef.current?.scrollAndHighlightItem?.(invitedEmails, 1500);
+ Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {});
+ }, [invitedEmailsToAccountIDsDraft, route.params.policyID, isFocused, accountIDs, prevAccountIDs]);
const getHeaderMessage = () => {
if (isOfflineAndNoMemberDataAvailable) {
@@ -537,6 +587,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, se
/>
@@ -568,6 +619,9 @@ export default withCurrentUserPersonalDetails(
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
+ invitedEmailsToAccountIDsDraft: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`,
+ },
session: {
key: ONYXKEYS.SESSION,
},
diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
index 45a950e0fafb..000ba0db7bc7 100644
--- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
+++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
@@ -9,7 +9,7 @@ import Section from '@components/Section';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types';
import * as Policy from '@userActions/Policy';
import type {TranslationPaths} from '@src/languages/types';
import type SCREENS from '@src/SCREENS';
@@ -21,7 +21,7 @@ import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscree
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
import ToggleSettingOptionRow from './workflows/ToggleSettingsOptionRow';
-type WorkspaceMoreFeaturesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+type WorkspaceMoreFeaturesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
type Item = {
icon: IconAsset;
diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx
index 32b76ef46202..4904a4f35193 100644
--- a/src/pages/workspace/WorkspacePageWithSections.tsx
+++ b/src/pages/workspace/WorkspacePageWithSections.tsx
@@ -19,7 +19,6 @@ import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimburs
import * as BankAccounts from '@userActions/BankAccounts';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
-import ROUTES from '@src/ROUTES';
import type {Policy, ReimbursementAccount, User} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
@@ -81,6 +80,9 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps &
* taller header on desktop and different font of the title.
* */
icon?: IconAsset;
+
+ /** Whether the page is loading, example any other API call in progres */
+ isLoading?: boolean;
};
function fetchData(policyID: string, skipVBBACal?: boolean) {
@@ -110,12 +112,14 @@ function WorkspacePageWithSections({
shouldShowOfflineIndicatorInWideScreen = false,
shouldShowNonAdmin = false,
shouldShowNotFoundPage = false,
+ isLoading: isPageLoading = false,
}: WorkspacePageWithSectionsProps) {
const styles = useThemeStyles();
const policyID = route.params?.policyID ?? '';
useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)});
- const isLoading = reimbursementAccount?.isLoading ?? true;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true;
const achState = reimbursementAccount?.achData?.state ?? '';
const isUsingECard = user?.isUsingExpensifyCard ?? false;
const hasVBA = achState === BankAccount.STATE.OPEN;
@@ -152,8 +156,8 @@ function WorkspacePageWithSections({
shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow}
>
Navigation.goBack(backButtonRoute ?? ROUTES.WORKSPACE_INITIAL.getRoute(policyID))}
+ onBackButtonPress={() => Navigation.goBack(backButtonRoute)}
icon={icon ?? undefined}
+ style={styles.headerBarDesktopHeight}
/>
{(isLoading || firstRender.current) && shouldShowLoading && isFocused ? (
diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx
index 9d90557b1d37..ddebc9d4b368 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -102,7 +102,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi
>
{(hasVBA?: boolean) => (
-
+
App.createWorkspaceWithPolicyDraftAndNavigateToIt()}
+ onPress={() => interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt())}
/>
@@ -366,7 +367,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
subtitle={translate('workspace.emptyWorkspace.subtitle')}
ctaText={translate('workspace.new.newWorkspace')}
ctaAccessibilityLabel={translate('workspace.new.newWorkspace')}
- onCtaPress={() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()}
+ onCtaPress={() => interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt())}
illustration={LottieAnimations.WorkspacePlanet}
// We use this style to vertically center the illustration, as the original illustration is not centered
illustrationStyle={styles.emptyWorkspaceIllustrationStyle}
@@ -394,7 +395,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
success
medium
text={translate('workspace.new.newWorkspace')}
- onPress={() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()}
+ onPress={() => interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt())}
/>
;
+type WorkspaceBillsPageProps = StackScreenProps;
function WorkspaceBillsPage({route}: WorkspaceBillsPageProps) {
const {translate} = useLocalize();
diff --git a/src/pages/workspace/card/WorkspaceCardPage.tsx b/src/pages/workspace/card/WorkspaceCardPage.tsx
index 710ef3735026..079c715bffd6 100644
--- a/src/pages/workspace/card/WorkspaceCardPage.tsx
+++ b/src/pages/workspace/card/WorkspaceCardPage.tsx
@@ -4,7 +4,7 @@ import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import CONST from '@src/CONST';
import type SCREENS from '@src/SCREENS';
@@ -12,7 +12,7 @@ import WorkspaceCardNoVBAView from './WorkspaceCardNoVBAView';
import WorkspaceCardVBANoECardView from './WorkspaceCardVBANoECardView';
import WorkspaceCardVBAWithECardView from './WorkspaceCardVBAWithECardView';
-type WorkspaceCardPageProps = StackScreenProps;
+type WorkspaceCardPageProps = StackScreenProps;
function WorkspaceCardPage({route}: WorkspaceCardPageProps) {
const {translate} = useLocalize();
diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx
index cfe28ba292b0..8de0e3a07980 100644
--- a/src/pages/workspace/categories/CreateCategoryPage.tsx
+++ b/src/pages/workspace/categories/CreateCategoryPage.tsx
@@ -9,6 +9,7 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
@@ -34,6 +35,7 @@ type CreateCategoryPageProps = WorkspaceCreateCategoryPageOnyxProps & StackScree
function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
const validate = useCallback(
(values: FormOnyxValues) => {
@@ -72,6 +74,7 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps)
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={CreateCategoryPage.displayName}
+ shouldEnableMaxHeight
>
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index 17afefd1cea4..3f2ef8ce6aa6 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -21,7 +21,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import localeCompare from '@libs/LocaleCompare';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
-import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
@@ -47,7 +47,7 @@ type WorkspaceCategoriesOnyxProps = {
policyCategories: OnyxEntry;
};
-type WorkspaceCategoriesPageProps = WorkspaceCategoriesOnyxProps & StackScreenProps;
+type WorkspaceCategoriesPageProps = WorkspaceCategoriesOnyxProps & StackScreenProps;
function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCategoriesPageProps) {
const {isSmallScreenWidth} = useWindowDimensions();
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
index fd6466da1758..ca508791c028 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
@@ -20,7 +20,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CurrencyUtils from '@libs/CurrencyUtils';
-import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
@@ -44,7 +44,7 @@ type PolicyDistanceRatesPageOnyxProps = {
policy: OnyxEntry;
};
-type PolicyDistanceRatesPageProps = PolicyDistanceRatesPageOnyxProps & StackScreenProps;
+type PolicyDistanceRatesPageProps = PolicyDistanceRatesPageOnyxProps & StackScreenProps;
function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) {
const {isSmallScreenWidth} = useWindowDimensions();
diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
index 96aa350496b5..a00c4959cedb 100644
--- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
+++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx
@@ -4,14 +4,14 @@ import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import CONST from '@src/CONST';
import type SCREENS from '@src/SCREENS';
import WorkspaceInvoicesNoVBAView from './WorkspaceInvoicesNoVBAView';
import WorkspaceInvoicesVBAView from './WorkspaceInvoicesVBAView';
-type WorkspaceInvoicesPageProps = StackScreenProps;
+type WorkspaceInvoicesPageProps = StackScreenProps;
function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) {
const {translate} = useLocalize();
diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx
new file mode 100644
index 000000000000..2df7621c17d3
--- /dev/null
+++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx
@@ -0,0 +1,113 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback} from 'react';
+import {Keyboard} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/WorkspaceTagCreateForm';
+import type {PolicyTagList} from '@src/types/onyx';
+
+type WorkspaceCreateTagPageOnyxProps = {
+ /** All policy tags */
+ policyTags: OnyxEntry;
+};
+
+type CreateTagPageProps = WorkspaceCreateTagPageOnyxProps & StackScreenProps;
+
+function CreateTagPage({route, policyTags}: CreateTagPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const validate = useCallback(
+ (values: FormOnyxValues) => {
+ const errors: FormInputErrors = {};
+ const tagName = values.tagName.trim();
+ const {tags} = PolicyUtils.getTagList(policyTags, 0);
+
+ if (!ValidationUtils.isRequiredFulfilled(tagName)) {
+ errors.tagName = 'workspace.tags.tagRequiredError';
+ } else if (tags?.[tagName]) {
+ errors.tagName = 'workspace.tags.existingTagError';
+ } else if ([...tagName].length > CONST.TAG_NAME_LIMIT) {
+ // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units.
+ ErrorUtils.addErrorMessage(errors, 'tagName', ['common.error.characterLimitExceedCounter', {length: [...tagName].length, limit: CONST.TAG_NAME_LIMIT}]);
+ }
+
+ return errors;
+ },
+ [policyTags],
+ );
+
+ const createTag = useCallback(
+ (values: FormOnyxValues) => {
+ Policy.createPolicyTag(route.params.policyID, values.tagName.trim());
+ Keyboard.dismiss();
+ Navigation.goBack();
+ },
+ [route.params.policyID],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+CreateTagPage.displayName = 'CreateTagPage';
+
+export default withOnyx({
+ policyTags: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${route?.params?.policyID}`,
+ },
+})(CreateTagPage);
diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
index cc809892d45e..4ea8ba669b9b 100644
--- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
@@ -20,7 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
-import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
@@ -43,7 +43,7 @@ type WorkspaceTagsOnyxProps = {
policyTags: OnyxEntry;
};
-type WorkspaceTagsPageProps = WorkspaceTagsOnyxProps & StackScreenProps;
+type WorkspaceTagsPageProps = WorkspaceTagsOnyxProps & StackScreenProps;
function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
const {isSmallScreenWidth} = useWindowDimensions();
@@ -115,18 +115,32 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
Navigation.navigate(ROUTES.WORKSPACE_TAGS_SETTINGS.getRoute(route.params.policyID));
};
+ const navigateToCreateTagPage = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_TAG_CREATE.getRoute(route.params.policyID));
+ };
+
const isLoading = !isOffline && policyTags === undefined;
- const settingsButton = (
+ const headerButtons = (
+ {policyTags && (
+
+ )}
);
@@ -144,9 +158,9 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
title={translate('workspace.common.tags')}
shouldShowBackButton={isSmallScreenWidth}
>
- {!isSmallScreenWidth && policyTags && settingsButton}
+ {!isSmallScreenWidth && headerButtons}
- {isSmallScreenWidth && policyTags && {settingsButton}}
+ {isSmallScreenWidth && {headerButtons}}
{translate('workspace.tags.subtitle')}
diff --git a/src/pages/workspace/travel/WorkspaceTravelPage.tsx b/src/pages/workspace/travel/WorkspaceTravelPage.tsx
index 88dfe5254fcf..c03bcc7cfb9b 100644
--- a/src/pages/workspace/travel/WorkspaceTravelPage.tsx
+++ b/src/pages/workspace/travel/WorkspaceTravelPage.tsx
@@ -4,14 +4,14 @@ import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import CONST from '@src/CONST';
import type SCREENS from '@src/SCREENS';
import WorkspaceTravelNoVBAView from './WorkspaceTravelNoVBAView';
import WorkspaceTravelVBAView from './WorkspaceTravelVBAView';
-type WorkspaceTravelPageProps = StackScreenProps;
+type WorkspaceTravelPageProps = StackScreenProps;
function WorkspaceTravelPage({route}: WorkspaceTravelPageProps) {
const {translate} = useLocalize();
diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx
index 76126040652b..16af069fa0a2 100644
--- a/src/pages/workspace/withPolicy.tsx
+++ b/src/pages/workspace/withPolicy.tsx
@@ -8,7 +8,7 @@ import {withOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import taxPropTypes from '@components/taxPropTypes';
import {translatableTextPropTypes} from '@libs/Localize';
-import type {BottomTabNavigatorParamList, CentralPaneNavigatorParamList, SettingsNavigatorParamList} from '@navigation/types';
+import type {CentralPaneNavigatorParamList, FullScreenNavigatorParamList, SettingsNavigatorParamList, WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import policyMemberPropType from '@pages/policyMemberPropType';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
@@ -16,7 +16,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
-type WorkspaceParamList = BottomTabNavigatorParamList & CentralPaneNavigatorParamList & SettingsNavigatorParamList;
+type WorkspaceParamList = WorkspacesCentralPaneNavigatorParamList & FullScreenNavigatorParamList & CentralPaneNavigatorParamList & SettingsNavigatorParamList;
type PolicyRoute = RouteProp>;
function getPolicyIDFromRoute(route: PolicyRoute): string {
@@ -148,5 +148,5 @@ export default function (WrappedComponent:
})(forwardRef(WithPolicy));
}
-export {policyPropTypes, policyDefaultProps};
-export type {WithPolicyOnyxProps, WithPolicyProps, PolicyRoute};
+export {policyDefaultProps, policyPropTypes};
+export type {PolicyRoute, WithPolicyOnyxProps, WithPolicyProps};
diff --git a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx
index c5c4465937b9..da995de1d5d5 100644
--- a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx
+++ b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx
@@ -5,7 +5,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Switch from '@components/Switch';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
-import type {PendingAction} from '@src/types/onyx/OnyxCommon';
+import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
import type IconAsset from '@src/types/utils/IconAsset';
type ToggleSettingOptionRowProps = {
@@ -23,14 +23,23 @@ type ToggleSettingOptionRowProps = {
subMenuItems?: React.ReactNode;
/** If there is a pending action, we will grey out the option */
pendingAction?: PendingAction;
+ /** Any error message to show */
+ errors?: Errors;
+ /** Callback to close the error messages */
+ onCloseError?: () => void;
};
const ICON_SIZE = 48;
-function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction}: ToggleSettingOptionRowProps) {
+function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction, errors, onCloseError}: ToggleSettingOptionRowProps) {
const styles = useThemeStyles();
return (
-
+
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index ee3934cacc06..c6ace2b0856e 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -1,31 +1,34 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import React, {useCallback, useMemo} from 'react';
+import React, {useCallback, useEffect, useMemo} from 'react';
import {FlatList, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import * as Illustrations from '@components/Icon/Illustrations';
import MenuItem from '@components/MenuItem';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Section from '@components/Section';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
-import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import BankAccount from '@libs/models/BankAccount';
import Navigation from '@libs/Navigation/Navigation';
import Permissions from '@libs/Permissions';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
-import type {CentralPaneNavigatorParamList} from '@navigation/types';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import type {WithPolicyProps} from '@pages/workspace/withPolicy';
import withPolicy from '@pages/workspace/withPolicy';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import * as Policy from '@userActions/Policy';
+import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
-import type {Beta} from '@src/types/onyx';
+import type {Beta, ReimbursementAccount, Session} from '@src/types/onyx';
import ToggleSettingOptionRow from './ToggleSettingsOptionRow';
import type {ToggleSettingOptionRowProps} from './ToggleSettingsOptionRow';
import {getAutoReportingFrequencyDisplayNames} from './WorkspaceAutoReportingFrequencyPage';
@@ -34,25 +37,49 @@ import type {AutoReportingFrequencyKey} from './WorkspaceAutoReportingFrequencyP
type WorkspaceWorkflowsPageOnyxProps = {
/** Beta features list */
betas: OnyxEntry;
+ /** Reimbursement account details */
+ reimbursementAccount: OnyxEntry;
+ /** Policy details */
+ session: OnyxEntry;
};
-type WorkspaceWorkflowsPageProps = WithPolicyProps & WorkspaceWorkflowsPageOnyxProps & StackScreenProps;
+type WorkspaceWorkflowsPageProps = WithPolicyProps & WorkspaceWorkflowsPageOnyxProps & StackScreenProps;
-function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPageProps) {
+function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, session}: WorkspaceWorkflowsPageProps) {
const {translate, preferredLocale} = useLocalize();
const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
const {isSmallScreenWidth} = useWindowDimensions();
- const {isOffline} = useNetwork();
const policyApproverEmail = policy?.approver;
const policyApproverName = useMemo(() => PersonalDetailsUtils.getPersonalDetailByEmail(policyApproverEmail ?? '')?.displayName ?? policyApproverEmail, [policyApproverEmail]);
const containerStyle = useMemo(() => [styles.ph8, styles.mhn8, styles.ml11, styles.pv3, styles.pr0, styles.pl4, styles.mr0, styles.widthAuto, styles.mt4], [styles]);
const canUseDelayedSubmission = Permissions.canUseWorkflowsDelayedSubmission(betas);
+ const displayNameForAuthorizedPayer = useMemo(() => {
+ const personalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([policy?.reimburserAccountID ?? 0], session?.accountID ?? 0);
+ const displayNameFromReimburserEmail = PersonalDetailsUtils.getPersonalDetailByEmail(policy?.reimburserEmail ?? '')?.displayName ?? policy?.reimburserEmail;
+ return displayNameFromReimburserEmail ?? personalDetails?.[0]?.displayName;
+ }, [policy?.reimburserAccountID, policy?.reimburserEmail, session?.accountID]);
+
const onPressAutoReportingFrequency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY.getRoute(policy?.id ?? '')), [policy?.id]);
- const items: ToggleSettingOptionRowProps[] = useMemo(
- () => [
+ const fetchData = () => {
+ Policy.openPolicyWorkflowsPage(policy?.id ?? route.params.policyID);
+ };
+
+ useNetwork({onReconnect: fetchData});
+
+ useEffect(() => {
+ fetchData();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const optionItems: ToggleSettingOptionRowProps[] = useMemo(() => {
+ const {accountNumber, state, bankName} = reimbursementAccount?.achData ?? {};
+ const hasVBA = state === BankAccount.STATE.OPEN;
+ const bankDisplayName = bankName ? `${bankName} ${accountNumber ? `${accountNumber.slice(-5)}` : ''}` : '';
+ const hasReimburserEmailError = !!policy?.errorFields?.reimburserEmail;
+
+ return [
...(canUseDelayedSubmission
? [
{
@@ -115,41 +142,71 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr
title: translate('workflowsPage.makeOrTrackPaymentsTitle'),
subtitle: translate('workflowsPage.makeOrTrackPaymentsDescription'),
onToggle: () => {
- // TODO will be done in https://github.com/Expensify/Expensify/issues/368335
+ const isActive = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES;
+ const newReimbursementChoice = isActive ? CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL : CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES;
+ const newReimburserAccountID =
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ PersonalDetailsUtils.getPersonalDetailByEmail(policy?.reimburserEmail ?? '')?.accountID || policy?.reimburserAccountID || policy?.ownerAccountID;
+ const newReimburserEmail = PersonalDetailsUtils.getPersonalDetailsByIDs([newReimburserAccountID ?? 0], session?.accountID ?? 0)?.[0]?.login;
+ Policy.setWorkspaceReimbursement(policy?.id ?? '', newReimbursementChoice, newReimburserAccountID ?? 0, newReimburserEmail ?? '');
},
subMenuItems: (
- Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_CONNECT_BANK_ACCOUNT.getRoute(route.params.policyID))}
- // TODO will be done in https://github.com/Expensify/Expensify/issues/368335
- shouldShowRightIcon
- wrapperStyle={containerStyle}
- hoverAndPressStyle={[styles.mr0, styles.br2]}
- />
+ <>
+ navigateToBankAccountRoute(route.params.policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID))}
+ shouldShowRightIcon
+ wrapperStyle={containerStyle}
+ hoverAndPressStyle={[styles.mr0, styles.br2]}
+ />
+ {hasVBA && (
+ Policy.clearWorkspacePayerError(policy?.id ?? '')}
+ errorRowStyles={[styles.ml7]}
+ >
+ Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_PAYER.getRoute(route.params.policyID))}
+ shouldShowRightIcon
+ wrapperStyle={containerStyle}
+ hoverAndPressStyle={[styles.mr0, styles.br2]}
+ brickRoadIndicator={hasReimburserEmailError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ />
+
+ )}
+ >
),
isEndOptionRow: true,
- isActive: false, // TODO will be done in https://github.com/Expensify/Expensify/issues/368335
+ isActive: policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES,
+ pendingAction: policy?.pendingFields?.reimbursementChoice,
+ errors: ErrorUtils.getLatestErrorField(policy ?? {}, 'reimbursementChoice'),
+ onCloseError: () => Policy.clearWorkspaceReimbursementErrors(policy?.id ?? ''),
},
- ],
- [
- policyApproverName,
- policy,
- route.params.policyID,
- styles,
- translate,
- containerStyle,
- isOffline,
- StyleUtils,
- onPressAutoReportingFrequency,
- preferredLocale,
- canUseDelayedSubmission,
- ],
- );
+ ];
+ }, [
+ policy,
+ route.params.policyID,
+ styles,
+ translate,
+ policyApproverName,
+ containerStyle,
+ onPressAutoReportingFrequency,
+ preferredLocale,
+ canUseDelayedSubmission,
+ reimbursementAccount?.achData,
+ displayNameForAuthorizedPayer,
+ session?.accountID,
+ ]);
- const renderItem = ({item}: {item: ToggleSettingOptionRowProps}) => (
+ const renderOptionItem = ({item}: {item: ToggleSettingOptionRowProps}) => (
);
const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy);
const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy);
+ const isLoading = reimbursementAccount?.isLoading ?? true;
return (
{translate('workflowsPage.workflowDescription')}
item.title}
/>
@@ -202,5 +264,12 @@ export default withPolicy(
betas: {
key: ONYXKEYS.BETAS,
},
+ reimbursementAccount: {
+ // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM
+ key: ({route}) => `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${route.params.policyID}`,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
})(WorkspaceWorkflowsPage),
);
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx
new file mode 100644
index 000000000000..9934de95878b
--- /dev/null
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx
@@ -0,0 +1,238 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useMemo, useState} from 'react';
+import type {SectionListData} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import Badge from '@components/Badge';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SelectionList from '@components/SelectionList';
+import type {ListItem, Section} from '@components/SelectionList/types';
+import UserListItem from '@components/SelectionList/UserListItem';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import compose from '@libs/compose';
+import {formatPhoneNumber} from '@libs/LocalePhoneNumber';
+import Log from '@libs/Log';
+import Navigation from '@libs/Navigation/Navigation';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as UserUtils from '@libs/UserUtils';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {PersonalDetailsList, PolicyMember} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type WorkspaceWorkflowsPayerPageOnyxProps = {
+ /** All of the personal details for everyone */
+ personalDetails: OnyxEntry;
+};
+
+type WorkspaceWorkflowsPayerPageProps = WorkspaceWorkflowsPayerPageOnyxProps &
+ WithPolicyAndFullscreenLoadingProps &
+ StackScreenProps;
+type MemberOption = Omit & {accountID: number};
+type MembersSection = SectionListData>;
+
+function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDetails, isLoadingReportData = true}: WorkspaceWorkflowsPayerPageProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+ const policyName = policy?.name ?? '';
+ const {isOffline} = useNetwork();
+
+ const [searchTerm, setSearchTerm] = useState('');
+
+ const isDeletedPolicyMember = useCallback(
+ (policyMember: PolicyMember) => !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyMember.errors),
+ [isOffline],
+ );
+
+ const [formattedPolicyAdmins, formattedAuthorizedPayer] = useMemo(() => {
+ const policyAdminDetails: MemberOption[] = [];
+ const authorizedPayerDetails: MemberOption[] = [];
+
+ Object.entries(policyMembers ?? {}).forEach(([accountIDKey, policyMember]) => {
+ const accountID = Number(accountIDKey);
+ const details = personalDetails?.[accountID];
+ if (!details) {
+ Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`);
+ return;
+ }
+
+ const isOwner = policy?.owner === details?.login;
+ const isAdmin = policyMember.role === CONST.POLICY.ROLE.ADMIN;
+ const shouldSkipMember = isDeletedPolicyMember(policyMember) || PolicyUtils.isExpensifyTeam(details?.login) || (!isOwner && !isAdmin);
+
+ if (shouldSkipMember) {
+ return;
+ }
+
+ const roleBadge = (
+
+ );
+
+ const isAuthorizedPayer = policy?.reimburserEmail === details?.login ?? policy?.reimburserAccountID === accountID;
+
+ const formattedMember = {
+ keyForList: accountIDKey,
+ accountID,
+ isSelected: isAuthorizedPayer,
+ isDisabled: policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyMember.errors),
+ text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)),
+ alternateText: formatPhoneNumber(details?.login ?? ''),
+ rightElement: roleBadge,
+ icons: [
+ {
+ source: UserUtils.getAvatar(details?.avatar, accountID),
+ name: formatPhoneNumber(details?.login ?? ''),
+ type: CONST.ICON_TYPE_AVATAR,
+ id: accountID,
+ },
+ ],
+ errors: policyMember.errors,
+ pendingAction: policyMember.pendingAction ?? isAuthorizedPayer ? policy?.pendingFields?.reimburserEmail : null,
+ };
+
+ if (policy?.reimburserEmail === details?.login ?? policy?.reimburserAccountID === accountID) {
+ authorizedPayerDetails.push(formattedMember);
+ } else {
+ policyAdminDetails.push(formattedMember);
+ }
+ });
+ return [policyAdminDetails, authorizedPayerDetails];
+ }, [
+ personalDetails,
+ policyMembers,
+ translate,
+ policy?.reimburserEmail,
+ isDeletedPolicyMember,
+ policy?.owner,
+ styles,
+ StyleUtils,
+ policy?.reimburserAccountID,
+ policy?.pendingFields?.reimburserEmail,
+ ]);
+
+ const sections: MembersSection[] = useMemo(() => {
+ const sectionsArray: MembersSection[] = [];
+
+ if (searchTerm !== '') {
+ const filteredOptions = [...formattedPolicyAdmins, ...formattedAuthorizedPayer].filter((option) => {
+ const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm);
+ return !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue);
+ });
+ return [
+ {
+ title: undefined,
+ data: filteredOptions,
+ shouldShow: true,
+ },
+ ];
+ }
+
+ sectionsArray.push({
+ data: formattedAuthorizedPayer,
+ shouldShow: true,
+ indexOffset: formattedPolicyAdmins.length,
+ });
+
+ sectionsArray.push({
+ title: translate('workflowsPayerPage.admins'),
+ data: formattedPolicyAdmins,
+ shouldShow: true,
+ indexOffset: 0,
+ });
+ return sectionsArray;
+ }, [formattedPolicyAdmins, formattedAuthorizedPayer, translate, searchTerm]);
+
+ const headerMessage = useMemo(
+ () => (searchTerm && !sections[0].data.length ? translate('common.noResultsFound') : ''),
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [translate, sections],
+ );
+
+ const setPolicyAuthorizedPayer = (member: MemberOption) => {
+ const authorizedPayerEmail = personalDetails?.[member.accountID]?.login ?? '';
+ if (policy?.reimburserEmail === authorizedPayerEmail) {
+ return;
+ }
+
+ if (policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES) {
+ return;
+ }
+ const authorizedPayerAccountID = member.accountID;
+ Policy.setWorkspacePayer(policy?.id ?? '', authorizedPayerEmail, authorizedPayerAccountID);
+ Navigation.goBack();
+ };
+
+ // eslint-disable-next-line rulesdir/no-negated-variables
+ const shouldShowNotFoundPage = useMemo(
+ () =>
+ (isEmptyObject(policy) && !isLoadingReportData) ||
+ PolicyUtils.isPendingDeletePolicy(policy) ||
+ policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES,
+ [policy, isLoadingReportData],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+WorkspaceWorkflowsPayerPage.displayName = 'WorkspaceWorkflowsPayerPage';
+
+export default compose(
+ withOnyx({
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ }),
+ withPolicyAndFullscreenLoading,
+)(WorkspaceWorkflowsPayerPage);
diff --git a/src/stories/Composer.stories.js b/src/stories/Composer.stories.tsx
similarity index 78%
rename from src/stories/Composer.stories.js
rename to src/stories/Composer.stories.tsx
index e4051a4ab72a..8cb3f297684e 100644
--- a/src/stories/Composer.stories.js
+++ b/src/stories/Composer.stories.tsx
@@ -1,16 +1,16 @@
+import type {ComponentMeta} from '@storybook/react';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import React, {useState} from 'react';
import {Image, View} from 'react-native';
import Composer from '@components/Composer';
+import type {ComposerProps} from '@components/Composer/types';
import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import withNavigationFallback from '@components/withNavigationFallback';
import useStyleUtils from '@hooks/useStyleUtils';
// eslint-disable-next-line no-restricted-imports
-import {defaultStyles} from '@styles/index';
-// eslint-disable-next-line no-restricted-imports
import {defaultTheme} from '@styles/theme';
-import CONST from '@src/CONST';
+import {defaultStyles} from '@src/styles';
const ComposerWithNavigation = withNavigationFallback(Composer);
@@ -19,25 +19,25 @@ const ComposerWithNavigation = withNavigationFallback(Composer);
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/Composer',
component: ComposerWithNavigation,
};
const parser = new ExpensiMark();
-function Default(args) {
+function Default(props: ComposerProps) {
const StyleUtils = useStyleUtils();
- const [pastedFile, setPastedFile] = useState(null);
- const [comment, setComment] = useState(args.defaultValue);
- const renderedHTML = parser.replace(comment);
+ const [pastedFile, setPastedFile] = useState(null);
+ const [comment, setComment] = useState(props.defaultValue);
+ const renderedHTML = parser.replace(comment ?? '');
return (
-
+
Entered Comment (Drop Enabled)
{comment}
Rendered Comment
- {Boolean(renderedHTML) && }
- {Boolean(pastedFile) && (
+ {!!renderedHTML && }
+ {!!pastedFile && (
= {
title: 'Components/DragAndDrop',
component: DragAndDropConsumer,
};
function Default() {
const [fileURL, setFileURL] = useState('');
+
return (
{
- const file = lodashGet(e, ['dataTransfer', 'files', 0]);
- if (file && file.type.includes('image')) {
+ onDrop={(event) => {
+ const file = event.dataTransfer?.files?.[0];
+ if (file?.type.includes('image')) {
const reader = new FileReader();
- reader.addEventListener('load', () => setFileURL(reader.result));
+ reader.addEventListener('load', () => setFileURL(reader.result as string));
reader.readAsDataURL(file);
}
}}
diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.tsx
similarity index 76%
rename from src/stories/Form.stories.js
rename to src/stories/Form.stories.tsx
index 62f9a35d89e2..8eeab971ea88 100644
--- a/src/stories/Form.stories.js
+++ b/src/stories/Form.stories.tsx
@@ -1,27 +1,47 @@
+import type {ComponentMeta, Story} from '@storybook/react';
import React, {useState} from 'react';
import {View} from 'react-native';
import AddressSearch from '@components/AddressSearch';
import CheckboxWithLabel from '@components/CheckboxWithLabel';
import DatePicker from '@components/DatePicker';
import FormProvider from '@components/Form/FormProvider';
+import type {FormProviderProps} from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import Picker from '@components/Picker';
import StatePicker from '@components/StatePicker';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
+import type {MaybePhraseKey} from '@libs/Localize';
import NetworkConnection from '@libs/NetworkConnection';
import * as ValidationUtils from '@libs/ValidationUtils';
-// eslint-disable-next-line no-restricted-imports
-import {defaultStyles} from '@styles/index';
import * as FormActions from '@userActions/FormActions';
import CONST from '@src/CONST';
+import type {OnyxFormValuesMapping} from '@src/ONYXKEYS';
+import {defaultStyles} from '@src/styles';
+
+type FormStory = Story;
+
+type StorybookFormValues = {
+ routingNumber?: string;
+ accountNumber?: string;
+ street?: string;
+ dob?: string;
+ pickFruit?: string;
+ pickAnotherFruit?: string;
+ state?: string;
+ checkbox?: boolean;
+};
+
+type StorybookFormErrors = Partial>;
+
+const STORYBOOK_FORM_ID = 'TestForm' as keyof OnyxFormValuesMapping;
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/Form',
component: FormProvider,
subcomponents: {
@@ -35,16 +55,21 @@ const story = {
},
};
-function Template(args) {
+function Template(props: FormProviderProps) {
// Form consumes data from Onyx, so we initialize Onyx with the necessary data here
NetworkConnection.setOfflineStatus(false);
- FormActions.setIsLoading(args.formID, args.formState.isLoading);
- FormActions.setErrors(args.formID, args.formState.error);
- FormActions.setDraftValues(args.formID, args.draftValues);
+ FormActions.setIsLoading(props.formID, !!props.formState?.isLoading);
+ FormActions.setDraftValues(props.formID, props.draftValues);
+
+ if (props.formState?.error) {
+ FormActions.setErrors(props.formID, {error: props.formState.error as MaybePhraseKey});
+ } else {
+ FormActions.clearErrors(props.formID);
+ }
return (
// eslint-disable-next-line react/jsx-props-no-spreading
-
+
{}}
+ containerStyles={defaultStyles.mt4}
shouldSaveDraft
items={[
{
@@ -103,7 +129,8 @@ function Template(args) {
InputComponent={Picker}
label="Another Fruit"
inputID="pickAnotherFruit"
- containerStyles={[defaultStyles.mt4]}
+ onInputChange={() => {}}
+ containerStyles={defaultStyles.mt4}
items={[
{
label: 'Select a Fruit',
@@ -139,21 +166,24 @@ function Template(args) {
/**
* Story to exhibit the native event handlers for TextInput in the Form Component
- * @param {Object} args
- * @returns {JSX}
*/
-function WithNativeEventHandler(args) {
+function WithNativeEventHandler(props: FormProviderProps) {
const [log, setLog] = useState('');
// Form consumes data from Onyx, so we initialize Onyx with the necessary data here
NetworkConnection.setOfflineStatus(false);
- FormActions.setIsLoading(args.formID, args.formState.isLoading);
- FormActions.setErrors(args.formID, args.formState.error);
- FormActions.setDraftValues(args.formID, args.draftValues);
+ FormActions.setIsLoading(props.formID, !!props.formState?.isLoading);
+ FormActions.setDraftValues(props.formID, props.draftValues);
+
+ if (props.formState?.error) {
+ FormActions.setErrors(props.formID, {error: props.formState.error as MaybePhraseKey});
+ } else {
+ FormActions.clearErrors(props.formID);
+ }
return (
// eslint-disable-next-line react/jsx-props-no-spreading
-
+
{
- const errors = {};
+ validate: (values: StorybookFormValues) => {
+ const errors: StorybookFormErrors = {};
if (!ValidationUtils.isRequiredFulfilled(values.routingNumber)) {
errors.routingNumber = 'Please enter a routing number';
}
@@ -206,10 +236,10 @@ const defaultArgs = {
}
return errors;
},
- onSubmit: (values) => {
+ onSubmit: (values: StorybookFormValues) => {
setTimeout(() => {
alert(`Form submitted!\n\nInput values: ${JSON.stringify(values, null, 4)}`);
- FormActions.setIsLoading('TestForm', false);
+ FormActions.setIsLoading(STORYBOOK_FORM_ID, false);
}, 1000);
},
formState: {
diff --git a/src/stories/RadioButtonWithLabel.stories.js b/src/stories/RadioButtonWithLabel.stories.tsx
similarity index 60%
rename from src/stories/RadioButtonWithLabel.stories.js
rename to src/stories/RadioButtonWithLabel.stories.tsx
index af5d6ec15a8c..3280864b8fdb 100644
--- a/src/stories/RadioButtonWithLabel.stories.js
+++ b/src/stories/RadioButtonWithLabel.stories.tsx
@@ -1,35 +1,37 @@
+import type {ComponentMeta, ComponentStory} from '@storybook/react';
import React from 'react';
import RadioButtonWithLabel from '@components/RadioButtonWithLabel';
+import type {RadioButtonWithLabelProps} from '@components/RadioButtonWithLabel';
+
+type RadioButtonWithLabelStory = ComponentStory;
/**
* We use the Component Story Format for writing stories. Follow the docs here:
*
* https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format
*/
-const story = {
+const story: ComponentMeta = {
title: 'Components/RadioButtonWithLabel',
component: RadioButtonWithLabel,
};
-function Template(args) {
+function Template(props: RadioButtonWithLabelProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
- return ;
+ return ;
}
// Arguments can be passed to the component by binding
// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
-const Default = Template.bind({});
-const Checked = Template.bind({});
+const Default: RadioButtonWithLabelStory = Template.bind({});
+const Checked: RadioButtonWithLabelStory = Template.bind({});
Default.args = {
isChecked: false,
label: 'This radio button is unchecked',
- onInputChange: () => {},
};
Checked.args = {
isChecked: true,
label: 'This radio button is checked',
- onInputChange: () => {},
};
export default story;
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 0f129d898758..3cfb38ef4bab 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -1442,10 +1442,16 @@ const styles = (theme: ThemeColors) =>
},
sidebarAvatar: {
- backgroundColor: theme.icon,
+ borderRadius: 28,
+ height: variables.componentSizeSmall,
+ width: variables.componentSizeSmall,
+ },
+
+ selectedAvatarBorder: {
+ padding: 2,
+ borderWidth: 2,
borderRadius: 20,
- height: variables.componentSizeNormal,
- width: variables.componentSizeNormal,
+ borderColor: theme.success,
},
statusIndicator: (backgroundColor = theme.danger) =>
@@ -1455,10 +1461,10 @@ const styles = (theme: ThemeColors) =>
borderRadius: 8,
borderWidth: 2,
position: 'absolute',
- right: -2,
- top: -1,
- height: 16,
- width: 16,
+ right: -4,
+ top: -3,
+ height: 12,
+ width: 12,
zIndex: 10,
} satisfies ViewStyle),
@@ -2648,6 +2654,12 @@ const styles = (theme: ThemeColors) =>
...spacing.pt0,
},
+ workspaceSettingsSectionContainer: {
+ borderBottomWidth: 1,
+ borderBottomColor: theme.border,
+ ...spacing.pt4,
+ },
+
centralPaneAnimation: {
height: CONST.CENTRAL_PANE_ANIMATION_HEIGHT,
},
@@ -3086,7 +3098,7 @@ const styles = (theme: ThemeColors) =>
smallEditIcon: {
alignItems: 'center',
backgroundColor: theme.buttonDefaultBG,
- borderColor: theme.cardBG,
+ borderColor: theme.appBG,
borderRadius: 20,
borderWidth: 3,
color: theme.textReversed,
diff --git a/src/styles/utils/addOutlineWidth/index.native.ts b/src/styles/utils/addOutlineWidth/index.native.ts
index 9a9942951cd0..fdc338caaffd 100644
--- a/src/styles/utils/addOutlineWidth/index.native.ts
+++ b/src/styles/utils/addOutlineWidth/index.native.ts
@@ -4,7 +4,6 @@
*/
import type AddOutlineWidth from './types';
-// eslint-disable-next-line @typescript-eslint/naming-convention
const addOutlineWidth: AddOutlineWidth = (_theme, obj) => obj;
export default addOutlineWidth;
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index f32d166d06e4..31a19904b81c 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -453,7 +453,7 @@ function getBackgroundColorWithOpacityStyle(backgroundColor: string, opacity: nu
return {};
}
-function getWidthAndHeightStyle(width: number, height?: number): ViewStyle {
+function getWidthAndHeightStyle(width: number, height?: number): Pick {
return {
width,
height: height ?? width,
diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts
index 4f4ea821e794..56000a851e7b 100644
--- a/src/styles/utils/spacing.ts
+++ b/src/styles/utils/spacing.ts
@@ -167,6 +167,10 @@ export default {
marginLeft: 24,
},
+ ml7: {
+ marginLeft: 28,
+ },
+
ml8: {
marginLeft: 32,
},
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index d63fb5e9f339..61c16a8c2fd7 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -12,7 +12,7 @@ function getValueUsingPixelRatio(defaultValue: number, maxValue: number): number
}
export default {
- bottomTabHeight: 80,
+ bottomTabHeight: 72,
contentHeaderHeight: getValueUsingPixelRatio(72, 100),
contentHeaderDesktopHeight: getValueUsingPixelRatio(80, 100),
componentSizeSmall: getValueUsingPixelRatio(28, 32),
diff --git a/src/types/form/WorkspaceTagCreateForm.ts b/src/types/form/WorkspaceTagCreateForm.ts
new file mode 100644
index 000000000000..9a9670d84ae8
--- /dev/null
+++ b/src/types/form/WorkspaceTagCreateForm.ts
@@ -0,0 +1,18 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ TAG_NAME: 'tagName',
+} as const;
+
+type InputID = ValueOf;
+
+type WorkspaceTagCreateForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.TAG_NAME]: string;
+ }
+>;
+
+export type {WorkspaceTagCreateForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index 5fe6eea5c3af..5a574de3db54 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -39,4 +39,5 @@ export type {WorkspaceSettingsForm} from './WorkspaceSettingsForm';
export type {ReportPhysicalCardForm} from './ReportPhysicalCardForm';
export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm';
export type {PolicyTagNameForm} from './PolicyTagNameForm';
+export type {WorkspaceTagCreateForm} from './WorkspaceTagCreateForm';
export type {default as Form} from './Form';
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index cf959194be5f..ed30f4b7d201 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -321,6 +321,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback<
/** Email of the reimburser when reimbursement is set direct */
reimburserEmail?: string;
+ /** AccountID of the reimburser when reimbursement is set direct */
+ reimburserAccountID?: number;
+
/** ReportID of the admins room for this workspace */
chatReportIDAdmins?: number;
diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts
index a66fb731269e..70a0884c30bd 100644
--- a/src/types/onyx/PolicyTag.ts
+++ b/src/types/onyx/PolicyTag.ts
@@ -10,9 +10,12 @@ type PolicyTag = {
/** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */
// eslint-disable-next-line @typescript-eslint/naming-convention
'GL Code': string;
+
+ /** A list of errors keyed by microtime */
+ errors?: OnyxCommon.Errors | null;
};
-type PolicyTags = Record;
+type PolicyTags = Record>;
type PolicyTagList = Record<
T,
diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts
index 1f3b49ff77b0..c34534c0f420 100644
--- a/src/types/onyx/Report.ts
+++ b/src/types/onyx/Report.ts
@@ -32,6 +32,9 @@ type Participants = Record;
type Report = OnyxCommon.OnyxValueWithOfflineFeedback<
{
+ /** The URL of the Group Chat report custom avatar */
+ avatarUrl?: string;
+
/** The specific type of chat */
chatType?: ValueOf;
diff --git a/src/utils/times.ts b/src/utils/times.ts
index 1dc97eb74659..0f2a0766a8c3 100644
--- a/src/utils/times.ts
+++ b/src/utils/times.ts
@@ -1,5 +1,4 @@
function times(n: number, func: (index: number) => TReturnType = (i) => i as TReturnType): TReturnType[] {
- // eslint-disable-next-line @typescript-eslint/naming-convention
return Array.from({length: n}).map((_, i) => func(i));
}
diff --git a/tests/unit/checkDeployBlockersTest.js b/tests/unit/checkDeployBlockersTest.ts
similarity index 63%
rename from tests/unit/checkDeployBlockersTest.js
rename to tests/unit/checkDeployBlockersTest.ts
index 354ab132f601..5a35fdf4f681 100644
--- a/tests/unit/checkDeployBlockersTest.js
+++ b/tests/unit/checkDeployBlockersTest.ts
@@ -2,12 +2,18 @@
* @jest-environment node
*/
import * as core from '@actions/core';
-import _ from 'underscore';
+import asMutable from '@src/types/utils/asMutable';
import run from '../../.github/actions/javascript/checkDeployBlockers/checkDeployBlockers';
import GithubUtils from '../../.github/libs/GithubUtils';
+type CommentData = {body: string};
+
+type Comment = {data?: CommentData[]};
+
+type PullRequest = {url: string; isQASuccess: boolean};
+
// Static mock function for core.getInput
-const mockGetInput = jest.fn().mockImplementation((arg) => {
+const mockGetInput = jest.fn().mockImplementation((arg: string): string | number | undefined => {
if (arg === 'GITHUB_TOKEN') {
return 'fake_token';
}
@@ -23,8 +29,8 @@ const mockListComments = jest.fn();
beforeAll(() => {
// Mock core module
- core.getInput = mockGetInput;
- core.setOutput = mockSetOutput;
+ asMutable(core).getInput = mockGetInput;
+ asMutable(core).setOutput = mockSetOutput;
// Mock octokit module
const moctokit = {
@@ -35,10 +41,12 @@ beforeAll(() => {
},
},
};
+
+ // @ts-expect-error TODO: Remove this once GithubUtils (https://github.com/Expensify/App/issues/25382) is migrated to TypeScript.
GithubUtils.internalOctokit = moctokit;
});
-let baseComments = [];
+let baseComments: Comment = {};
beforeEach(() => {
baseComments = {
data: [
@@ -65,11 +73,11 @@ afterAll(() => {
jest.clearAllMocks();
});
-function checkbox(isClosed) {
+function checkbox(isClosed: boolean): string {
return isClosed ? '[x]' : '[ ]';
}
-function mockIssue(prList, deployBlockerList) {
+function mockIssue(prList: PullRequest[], deployBlockerList?: PullRequest[]) {
return {
data: {
number: 1,
@@ -79,25 +87,27 @@ function mockIssue(prList, deployBlockerList) {
**Compare Changes:** https://github.com/Expensify/App/compare/production...staging
**This release contains changes from the following pull requests:**
-${_.map(
- prList,
- ({url, isQASuccess}) => `
+${prList
+ .map(
+ ({url, isQASuccess}) => `
- ${checkbox(isQASuccess)} ${url}
`,
-)}
+ )
+ .join('\n')}
${
- !_.isEmpty(deployBlockerList)
+ !deployBlockerList || deployBlockerList.length < 0
? `
**Deploy Blockers:**`
: ''
-}
-${_.map(
- deployBlockerList,
- ({url, isQASuccess}) => `
+}
+${deployBlockerList
+ ?.map(
+ ({url, isQASuccess}) => `
- ${checkbox(isQASuccess)} ${url}
`,
-)}
+ )
+ .join('\n')}
cc @Expensify/applauseleads
`,
},
@@ -108,51 +118,46 @@ describe('checkDeployBlockers', () => {
const allClearIssue = mockIssue([{url: 'https://github.com/Expensify/App/pull/6882', isQASuccess: true}]);
describe('checkDeployBlockers', () => {
- test('Test an issue with all checked items and :shipit:', () => {
+ test('Test an issue with all checked items and :shipit:', async () => {
mockGetIssue.mockResolvedValue(allClearIssue);
mockListComments.mockResolvedValue(baseComments);
- return run().then(() => {
- expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', false);
- });
+ await expect(run()).resolves.toBeUndefined();
+ expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', false);
});
- test('Test an issue with all boxes checked but no :shipit:', () => {
+ test('Test an issue with all boxes checked but no :shipit:', async () => {
mockGetIssue.mockResolvedValue(allClearIssue);
const extraComments = {
- data: [...baseComments.data, {body: 'This issue either has unchecked QA steps or has not yet been stamped with a :shipit: comment. Reopening!'}],
+ data: [...(baseComments?.data ?? []), {body: 'This issue either has unchecked QA steps or has not yet been stamped with a :shipit: comment. Reopening!'}],
};
mockListComments.mockResolvedValue(extraComments);
- return run().then(() => {
- expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
- });
+ await expect(run()).resolves.toBeUndefined();
+ expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
});
- test('Test an issue with all boxes checked but no comments', () => {
+ test('Test an issue with all boxes checked but no comments', async () => {
mockGetIssue.mockResolvedValue(allClearIssue);
mockListComments.mockResolvedValue({data: []});
- return run().then(() => {
- expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
- });
+ await expect(run()).resolves.toBeUndefined();
+ expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
});
- test('Test an issue with all QA checked but not all deploy blockers', () => {
+ test('Test an issue with all QA checked but not all deploy blockers', async () => {
mockGetIssue.mockResolvedValue(
mockIssue([{url: 'https://github.com/Expensify/App/pull/6882', isQASuccess: true}], [{url: 'https://github.com/Expensify/App/pull/6883', isQASuccess: false}]),
);
mockListComments.mockResolvedValue(baseComments);
- return run().then(() => {
- expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
- });
+ await expect(run()).resolves.toBeUndefined();
+ expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', true);
});
- test('Test an issue with all QA checked and all deploy blockers resolved', () => {
+ test('Test an issue with all QA checked and all deploy blockers resolved', async () => {
mockGetIssue.mockResolvedValue(
mockIssue([{url: 'https://github.com/Expensify/App/pull/6882', isQASuccess: true}], [{url: 'https://github.com/Expensify/App/pull/6883', isQASuccess: true}]),
);
mockListComments.mockResolvedValue(baseComments);
- return run().then(() => {
- expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', false);
- });
+ await expect(run()).resolves.toBeUndefined();
+ expect(mockSetOutput).toHaveBeenCalledWith('HAS_DEPLOY_BLOCKERS', false);
});
});
});