diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml
index d2e0ec4f38e5..9eb5bc6eb409 100644
--- a/.github/workflows/failureNotifier.yml
+++ b/.github/workflows/failureNotifier.yml
@@ -88,7 +88,7 @@ jobs:
repo: context.repo.repo,
title: issueTitle,
body: issueBody,
- labels: [failureLabel, 'Daily'],
+ labels: [failureLabel, 'Hourly'],
assignees: [prMerger]
});
}
diff --git a/android/app/build.gradle b/android/app/build.gradle
index e01e62f4b6b9..ea7ea85c1729 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 1001044311
- versionName "1.4.43-11"
+ versionCode 1001044313
+ versionName "1.4.43-13"
}
flavorDimensions "default"
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 8e160e3bcdf2..4ed309467f13 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -25,16 +25,16 @@ https://community.expensify.com/discussion/5194/how-to-assign-a-vacation-delegat
https://community.expensify.com/discussion/5190/how-to-individually-assign-a-vacation-delegate-from-account-settings,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Vacation-Delegate
https://community.expensify.com/discussion/5274/how-to-set-up-an-adp-indirect-integration-with-expensify,https://help.expensify.com/articles/expensify-classic/integrations/HR-integrations/ADP
https://community.expensify.com/discussion/5776/how-to-create-mileage-expenses-in-expensify,https://help.expensify.com/articles/expensify-classic/get-paid-back/Distance-Tracking
-https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details
-https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details
-https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details
-https://community.expensify.com/discussion/4432/how-to-add-a-secondary-login,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details
-https://community.expensify.com/discussion/6794/how-to-change-your-email-in-expensify,https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details
+https://community.expensify.com/discussion/7385/how-to-enable-two-factor-authentication-in-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
+https://community.expensify.com/discussion/5124/how-to-add-your-name-and-photo-to-your-account,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
+https://community.expensify.com/discussion/5149/how-to-manage-your-devices-in-expensify,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
+https://community.expensify.com/discussion/4432/how-to-add-a-secondary-login,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
+https://community.expensify.com/discussion/6794/how-to-change-your-email-in-expensify,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-Card-Perks.html,https://use.expensify.com/company-credit-card
https://help.expensify.com/articles/expensify-classic/expensify-partner-program/How-to-Join-the-ExpensifyApproved!-Partner-Program.html,https://use.expensify.com/accountants-program
https://help.expensify.com/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners, https://use.expensify.com/blog/maximizing-rewards-expensifyapproved-accounting-partners-now-earn-0-5-revenue-share
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursements
-https://community.expensify.com/discussion/4452/how-to-merge-accounts,https://help.expensify.com/articles/expensify-classic/account-settings/Merge-Accounts
+https://community.expensify.com/discussion/4452/how-to-merge-accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts
https://community.expensify.com/discussion/4783/how-to-add-or-remove-a-copilot,https://help.expensify.com/articles/expensify-classic/account-settings/Copilot
https://community.expensify.com/discussion/4343/expensify-anz-partnership-announcement,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ
https://community.expensify.com/discussion/7318/deep-dive-company-credit-card-import-options,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards
@@ -55,3 +55,8 @@ https://help.expensify.com/articles/expensify-classic/getting-started/Using-The-
https://help.expensify.com/articles/expensify-classic/getting-started/support/Expensify-Support,https://use.expensify.com/support
https://help.expensify.com/articles/expensify-classic/getting-started/Plan-Types,https://use.expensify.com/
https://help.expensify.com/articles/new-expensify/payments/Referral-Program,https://help.expensify.com/articles/expensify-classic/get-paid-back/Referral-Program
+https://help.expensify.com/articles/expensify-classic/account-settings/Account-Details,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
+https://help.expensify.com/articles/expensify-classic/account-settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
+https://help.expensify.com/articles/expensify-classic/account-settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts
+https://help.expensify.com/articles/expensify-classic/getting-started/Individual-Users,https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself
+https://help.expensify.com/articles/expensify-classic/getting-started/Invite-Members,https://help.expensify.com/articles/expensify-classic/manage-employees-and-report-approvals/Invite-Members
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 1a2581512eda..637805356a70 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.43.11
+ 1.4.43.13
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 7b789718fd70..78372f479d35 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.43.11
+ 1.4.43.13
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index ad4e309ee295..06d36d7ec496 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
1.4.43
CFBundleVersion
- 1.4.43.11
+ 1.4.43.13
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index cec28a395431..f051c7650d09 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.43-11",
+ "version": "1.4.43-13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.43-11",
+ "version": "1.4.43-13",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 379612854781..27d813e9e1b1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.43-11",
+ "version": "1.4.43-13",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/patches/expo-av+13.10.4.patch b/patches/expo-av+13.10.4.patch
new file mode 100644
index 000000000000..c7b1626e233a
--- /dev/null
+++ b/patches/expo-av+13.10.4.patch
@@ -0,0 +1,17 @@
+diff --git a/node_modules/expo-av/android/build.gradle b/node_modules/expo-av/android/build.gradle
+index 2d68ca6..c3fa3c5 100644
+--- a/node_modules/expo-av/android/build.gradle
++++ b/node_modules/expo-av/android/build.gradle
+@@ -7,10 +7,11 @@ apply plugin: 'maven-publish'
+ group = 'host.exp.exponent'
+ version = '13.10.4'
+
++def REACT_NATIVE_PATH = this.hasProperty('reactNativeProject') ? this.reactNativeProject + '/node_modules/react-native/package.json' : 'react-native/package.json'
+ def REACT_NATIVE_BUILD_FROM_SOURCE = findProject(":ReactAndroid") != null
+ def REACT_NATIVE_DIR = REACT_NATIVE_BUILD_FROM_SOURCE
+ ? findProject(":ReactAndroid").getProjectDir().parent
+- : new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).parent
++ : new File(["node", "--print", "require.resolve('${REACT_NATIVE_PATH}')"].execute(null, rootDir).text.trim()).parent
+
+ def reactNativeArchitectures() {
+ def value = project.getProperties().get("reactNativeArchitectures")
diff --git a/src/CONST.ts b/src/CONST.ts
index 6a57738d06ec..008002a71078 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -895,6 +895,7 @@ const CONST = {
DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'},
DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false},
DEFAULT_CLOSE_ACCOUNT_DATA: {errors: null, success: '', isLoading: false},
+ DEFAULT_NETWORK_DATA: {isOffline: false},
FORMS: {
LOGIN_FORM: 'LoginForm',
VALIDATE_CODE_FORM: 'ValidateCodeForm',
@@ -1555,6 +1556,7 @@ const CONST = {
WORKSPACE_TRAVEL: 'WorkspaceBookTravel',
WORKSPACE_MEMBERS: 'WorkspaceManageMembers',
WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount',
+ WORKSPACE_SETTINGS: 'WorkspaceSettings',
},
get EXPENSIFY_EMAILS() {
return [
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 9d35994875e1..afbcd768b465 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -396,36 +396,35 @@ type AllOnyxKeys = DeepValueOf;
type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: FormTypes.AddDebitCardForm;
[ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm;
- [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceProfileDescriptionForm;
[ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm;
[ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm;
- [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.Form;
+ [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm;
[ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm;
[ONYXKEYS.FORMS.ROOM_NAME_FORM]: FormTypes.RoomNameForm;
- [ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.LEGAL_NAME_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: FormTypes.Form;
+ [ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM]: FormTypes.ReportDescriptionForm;
+ [ONYXKEYS.FORMS.LEGAL_NAME_FORM]: FormTypes.LegalNameForm;
+ [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: FormTypes.WorkspaceInviteMessageForm;
[ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: FormTypes.DateOfBirthForm;
- [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: FormTypes.Form;
+ [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: FormTypes.HomeAddressForm;
[ONYXKEYS.FORMS.NEW_ROOM_FORM]: FormTypes.NewRoomForm;
- [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.NEW_TASK_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.EDIT_TASK_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.MONEY_REQUEST_MERCHANT_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM]: FormTypes.Form;
+ [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: FormTypes.RoomSettingsForm;
+ [ONYXKEYS.FORMS.NEW_TASK_FORM]: FormTypes.NewTaskForm;
+ [ONYXKEYS.FORMS.EDIT_TASK_FORM]: FormTypes.EditTaskForm;
+ [ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM]: FormTypes.MoneyRequestDescriptionForm;
+ [ONYXKEYS.FORMS.MONEY_REQUEST_MERCHANT_FORM]: FormTypes.MoneyRequestMerchantForm;
+ [ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM]: FormTypes.MoneyRequestAmountForm;
+ [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM]: FormTypes.MoneyRequestDateForm;
[ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM]: FormTypes.MoneyRequestHoldReasonForm;
- [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.WAYPOINT_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM]: FormTypes.Form;
- [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: FormTypes.Form;
+ [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: FormTypes.NewContactMethodForm;
+ [ONYXKEYS.FORMS.WAYPOINT_FORM]: FormTypes.WaypointForm;
+ [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.SettingsStatusSetForm;
+ [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM]: FormTypes.SettingsStatusClearDateForm;
+ [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: FormTypes.SettingsStatusSetClearAfterForm;
[ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: FormTypes.PrivateNotesForm;
[ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: FormTypes.IKnowTeacherForm;
[ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: FormTypes.IntroSchoolPrincipalForm;
- [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: FormTypes.Form;
- [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: FormTypes.Form;
+ [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: FormTypes.ReportVirtualCardFraudForm;
+ [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: FormTypes.ReportPhysicalCardForm;
[ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: FormTypes.GetPhysicalCardForm;
[ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: FormTypes.ReportFieldEditForm;
[ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm;
@@ -491,7 +490,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: OnyxTypes.PrivatePersonalDetails;
[ONYXKEYS.TASK]: OnyxTypes.Task;
[ONYXKEYS.WORKSPACE_RATE_AND_UNIT]: OnyxTypes.WorkspaceRateAndUnit;
- [ONYXKEYS.CURRENCY_LIST]: Record;
+ [ONYXKEYS.CURRENCY_LIST]: OnyxTypes.CurrencyList;
[ONYXKEYS.UPDATE_AVAILABLE]: boolean;
[ONYXKEYS.SCREEN_SHARE_REQUEST]: OnyxTypes.ScreenShareRequest;
[ONYXKEYS.COUNTRY_CODE]: number;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index c41ef521661c..4d27c5f5e8cb 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -287,10 +287,6 @@ const ROUTES = {
route: ':iouType/new/currency/:reportID?',
getRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}` as const,
},
- MONEY_REQUEST_CATEGORY: {
- route: ':iouType/new/category/:reportID?',
- getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}` as const,
- },
MONEY_REQUEST_HOLD_REASON: {
route: ':iouType/edit/reason/:transactionID?',
getRoute: (iouType: string, transactionID: string, reportID: string, backTo: string) => `${iouType}/edit/reason/${transactionID}?backTo=${backTo}&reportID=${reportID}` as const,
@@ -338,9 +334,9 @@ const ROUTES = {
getUrlWithBackToParam(`create/${iouType}/taxAmount/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_CATEGORY: {
- route: 'create/:iouType/category/:transactionID/:reportID',
- getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
- getUrlWithBackToParam(`create/${iouType}/category/${transactionID}/${reportID}`, backTo),
+ route: ':action/:iouType/category/:transactionID/:reportID',
+ getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action}/${iouType}/category/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_CURRENCY: {
route: 'create/:iouType/currency/:transactionID/:reportID/:pageIndex?',
@@ -463,6 +459,10 @@ const ROUTES = {
route: 'workspace/:policyID/profile/description',
getRoute: (policyID: string) => `workspace/${policyID}/profile/description` as const,
},
+ WORKSPACE_PROFILE_SHARE: {
+ route: 'workspace/:policyID/profile/share',
+ getRoute: (policyID: string) => `workspace/${policyID}/profile/share` as const,
+ },
WORKSPACE_AVATAR: {
route: 'workspace/:policyID/avatar',
getRoute: (policyID: string) => `workspace/${policyID}/avatar` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 18754a3513c1..da7ea8db5ee6 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -149,7 +149,6 @@ const SCREENS = {
PARTICIPANTS: 'Money_Request_Participants',
CONFIRMATION: 'Money_Request_Confirmation',
CURRENCY: 'Money_Request_Currency',
- CATEGORY: 'Money_Request_Category',
WAYPOINT: 'Money_Request_Waypoint',
EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint',
DISTANCE: 'Money_Request_Distance',
@@ -210,6 +209,7 @@ const SCREENS = {
INVITE_MESSAGE: 'Workspace_Invite_Message',
CURRENCY: 'Workspace_Profile_Currency',
DESCRIPTION: 'Workspace_Profile_Description',
+ SHARE: 'Workspace_Profile_Share',
NAME: 'Workspace_Profile_Name',
},
diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js
index 0e57bcf4db03..2374fc9e5d0c 100644
--- a/src/components/CategoryPicker/index.js
+++ b/src/components/CategoryPicker/index.js
@@ -3,6 +3,7 @@ import React, {useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import * as OptionsListUtils from '@libs/OptionsListUtils';
@@ -67,6 +68,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC
textInputLabel={shouldShowTextInput && translate('common.search')}
onChangeText={setSearchValue}
onSelectRow={onSubmit}
+ ListItem={RadioListItem}
initiallyFocusedOptionKey={selectedOptionKey}
/>
);
diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx
index 584b349c508f..93febc4fd3c0 100644
--- a/src/components/MagicCodeInput.tsx
+++ b/src/components/MagicCodeInput.tsx
@@ -430,4 +430,4 @@ function MagicCodeInput(
MagicCodeInput.displayName = 'MagicCodeInput';
export default forwardRef(MagicCodeInput);
-export type {MagicCodeInputHandle};
+export type {AutoCompleteVariant, MagicCodeInputHandle};
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index 0de601bc9f61..2b18ab9bc003 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -766,11 +766,15 @@ function MoneyRequestConfirmationList(props) {
description={translate('common.category')}
numberOfLinesTitle={2}
onPress={() => {
- if (props.isEditingSplitBill) {
- Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.CATEGORY));
- return;
- }
- Navigation.navigate(ROUTES.MONEY_REQUEST_CATEGORY.getRoute(props.iouType, props.reportID));
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ props.iouType,
+ props.transaction.transactionID,
+ props.reportID,
+ Navigation.getActiveRouteWithoutParams(),
+ ),
+ );
}}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
index 3939e847707d..8609b1b05e4f 100755
--- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
+++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
@@ -747,7 +747,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
title={iouCategory}
description={translate('common.category')}
numberOfLinesTitle={2}
- onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ onPress={() =>
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()),
+ )
+ }
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
disabled={didConfirm}
diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx
index 69728d7be126..b8d4efbd916d 100644
--- a/src/components/PopoverProvider/index.tsx
+++ b/src/components/PopoverProvider/index.tsx
@@ -21,14 +21,15 @@ function PopoverContextProvider(props: PopoverContextProps) {
const [isOpen, setIsOpen] = useState(false);
const activePopoverRef = useRef(null);
- const closePopover = useCallback((anchorRef?: RefObject) => {
+ const closePopover = useCallback((anchorRef?: RefObject): boolean => {
if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) {
- return;
+ return false;
}
activePopoverRef.current.close();
activePopoverRef.current = null;
setIsOpen(false);
+ return true;
}, []);
useEffect(() => {
@@ -63,11 +64,13 @@ function PopoverContextProvider(props: PopoverContextProps) {
if (e.key !== 'Escape') {
return;
}
- closePopover();
+ if (closePopover()) {
+ e.stopImmediatePropagation();
+ }
};
- document.addEventListener('keydown', listener, true);
+ document.addEventListener('keyup', listener, true);
return () => {
- document.removeEventListener('keydown', listener, true);
+ document.removeEventListener('keyup', listener, true);
};
}, [closePopover]);
diff --git a/src/components/Pressable/PressableWithFeedback.tsx b/src/components/Pressable/PressableWithFeedback.tsx
index b717c4890a2d..74ea4596046e 100644
--- a/src/components/Pressable/PressableWithFeedback.tsx
+++ b/src/components/Pressable/PressableWithFeedback.tsx
@@ -2,6 +2,7 @@ import React, {forwardRef, useState} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import type {AnimatedStyle} from 'react-native-reanimated';
import OpacityView from '@components/OpacityView';
+import type {Color} from '@styles/theme/types';
import variables from '@styles/variables';
import GenericPressable from './GenericPressable';
import type {PressableRef} from './GenericPressable/types';
@@ -27,6 +28,9 @@ type PressableWithFeedbackProps = PressableProps & {
/** Whether the view needs to be rendered offscreen (for Android only) */
needsOffscreenAlphaCompositing?: boolean;
+
+ /** The color of the underlay that will show through when the Pressable is active. */
+ underlayColor?: Color;
};
function PressableWithFeedback(
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index f125235affca..67aeeff6163b 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -373,7 +373,11 @@ function MoneyRequestView({
interactive={canEdit}
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
- onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.CATEGORY))}
+ onPress={() =>
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, transaction?.transactionID ?? '', report.reportID),
+ )
+ }
brickRoadIndicator={getErrorForField('category') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
error={getErrorForField('category')}
/>
diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx
index 8b6a894cdd51..198b47cb4259 100644
--- a/src/components/ScreenWrapper.tsx
+++ b/src/components/ScreenWrapper.tsx
@@ -25,7 +25,7 @@ import SafeAreaConsumer from './SafeAreaConsumer';
import TestToolsModal from './TestToolsModal';
type ChildrenProps = {
- insets?: EdgeInsets;
+ insets: EdgeInsets;
safeAreaPaddingBottomStyle?: {
paddingBottom?: DimensionValue;
};
@@ -201,7 +201,17 @@ function ScreenWrapper(
return (
- {({insets, paddingTop, paddingBottom, safeAreaPaddingBottomStyle}) => {
+ {({
+ insets = {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ },
+ paddingTop,
+ paddingBottom,
+ safeAreaPaddingBottomStyle,
+ }) => {
const paddingStyle: StyleProp = {};
if (includePaddingTop) {
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index c39d7a05a4f7..eb9450f6ad98 100644
--- a/src/components/SelectionList/BaseListItem.tsx
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -12,6 +12,7 @@ import type {BaseListItemProps, ListItem} from './types';
function BaseListItem({
item,
+ pressableStyle,
wrapperStyle,
selectMultipleStyle,
isDisabled = false,
@@ -59,6 +60,7 @@ function BaseListItem({
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined}
nativeID={keyForList}
+ style={pressableStyle}
>
{({hovered}) => (
<>
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index b0996a08895a..1c69d00b3910 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -61,6 +61,8 @@ function BaseSelectionList(
rightHandSideComponent,
isLoadingNewOptions = false,
onLayout,
+ customListHeader,
+ listHeaderWrapperStyle,
}: BaseSelectionListProps,
inputRef: ForwardedRef,
) {
@@ -287,7 +289,7 @@ function BaseSelectionList(
showTooltip={showTooltip}
canSelectMultiple={canSelectMultiple}
onSelectRow={() => selectRow(item)}
- onDismissError={onDismissError}
+ onDismissError={() => onDismissError?.(item)}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
rightHandSideComponent={rightHandSideComponent}
keyForList={item.keyForList}
@@ -428,7 +430,7 @@ function BaseSelectionList(
<>
{!headerMessage && canSelectMultiple && shouldShowSelectAll && (
(
onPress={selectAllRow}
disabled={flattenedSections.allOptions.length === flattenedSections.disabledOptionsIndexes.length}
/>
-
- {translate('workspace.people.selectAll')}
-
+ {customListHeader ?? (
+
+ {translate('workspace.people.selectAll')}
+
+ )}
)}
+ {!headerMessage && !canSelectMultiple && customListHeader}
+ {(hovered) => (
+ <>
+ {!!item.icons && (
+
+ )}
+
+
+ {!!item.alternateText && (
+
+ )}
+
+ {!!item.rightElement && item.rightElement}
+ >
+ )}
+
+ );
+}
+
+TableListItem.displayName = 'TableListItem';
+
+export default TableListItem;
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 403ccd91a26b..59f6b14cfb1f 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -4,6 +4,7 @@ import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {ReceiptErrors} from '@src/types/onyx/Transaction';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type RadioListItem from './RadioListItem';
+import type TableListItem from './TableListItem';
import type UserListItem from './UserListItem';
type CommonListItemProps = {
@@ -28,6 +29,9 @@ type CommonListItemProps = {
/** Component to display on the right side */
rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null;
+ /** Styles for the pressable component */
+ pressableStyle?: StyleProp;
+
/** Styles for the wrapper view */
wrapperStyle?: StyleProp;
@@ -121,6 +125,8 @@ type UserListItemProps = ListItemProps & {
type RadioListItemProps = ListItemProps;
+type TableListItemProps = ListItemProps;
+
type Section = {
/** Title of the section */
title?: string;
@@ -143,7 +149,7 @@ type BaseSelectionListProps = Partial & {
sections: Array>>;
/** Default renderer for every item in the list */
- ListItem: typeof RadioListItem | typeof UserListItem;
+ ListItem: typeof RadioListItem | typeof UserListItem | typeof TableListItem;
/** Whether this is a multi-select list */
canSelectMultiple?: boolean;
@@ -155,7 +161,7 @@ type BaseSelectionListProps = Partial & {
onSelectAll?: () => void;
/** Callback to fire when an error is dismissed */
- onDismissError?: () => void;
+ onDismissError?: (item: TItem) => void;
/** Label for the text input */
textInputLabel?: string;
@@ -246,6 +252,12 @@ type BaseSelectionListProps = Partial & {
/** Fired when the list is displayed with the items */
onLayout?: (event: LayoutChangeEvent) => void;
+
+ /** Custom header to show right above list */
+ customListHeader?: ReactNode;
+
+ /** Styles for the list header wrapper */
+ listHeaderWrapperStyle?: StyleProp;
};
type ItemLayout = {
@@ -272,6 +284,7 @@ export type {
BaseListItemProps,
UserListItemProps,
RadioListItemProps,
+ TableListItemProps,
ListItem,
ListItemProps,
FlattenedSectionsReturn,
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.js b/src/components/VideoPlayer/BaseVideoPlayer.js
index 73dbf8407c0c..df79c7ef18da 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.js
+++ b/src/components/VideoPlayer/BaseVideoPlayer.js
@@ -13,6 +13,7 @@ import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import * as Browser from '@libs/Browser';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes';
+import shouldReplayVideo from './shouldReplayVideo';
import VideoPlayerControls from './VideoPlayerControls';
const isMobileSafari = Browser.isMobileSafari();
@@ -95,6 +96,9 @@ function BaseVideoPlayer({
const handlePlaybackStatusUpdate = useCallback(
(e) => {
+ if (shouldReplayVideo(e, isPlaying, duration, position)) {
+ videoPlayerRef.current.setStatusAsync({positionMillis: 0, shouldPlay: true});
+ }
const isVideoPlaying = e.isPlaying || false;
preventPausingWhenExitingFullscreen(isVideoPlaying);
setIsPlaying(isVideoPlaying);
@@ -105,7 +109,7 @@ function BaseVideoPlayer({
onPlaybackStatusUpdate(e);
},
- [onPlaybackStatusUpdate, preventPausingWhenExitingFullscreen, videoDuration],
+ [onPlaybackStatusUpdate, preventPausingWhenExitingFullscreen, videoDuration, isPlaying, duration, position],
);
const handleFullscreenUpdate = useCallback(
diff --git a/src/components/VideoPlayer/shouldReplayVideo.android.ts b/src/components/VideoPlayer/shouldReplayVideo.android.ts
new file mode 100644
index 000000000000..c1c3de034aac
--- /dev/null
+++ b/src/components/VideoPlayer/shouldReplayVideo.android.ts
@@ -0,0 +1,11 @@
+import type {AVPlaybackStatusSuccess} from 'expo-av';
+
+/**
+ * Whether to replay the video when users press play button
+ */
+export default function shouldReplayVideo(e: AVPlaybackStatusSuccess, isPlaying: boolean, duration: number, position: number): boolean {
+ if (!isPlaying && !e.didJustFinish && duration === position) {
+ return true;
+ }
+ return false;
+}
diff --git a/src/components/VideoPlayer/shouldReplayVideo.ios.ts b/src/components/VideoPlayer/shouldReplayVideo.ios.ts
new file mode 100644
index 000000000000..0a923d430699
--- /dev/null
+++ b/src/components/VideoPlayer/shouldReplayVideo.ios.ts
@@ -0,0 +1,11 @@
+import type {AVPlaybackStatusSuccess} from 'expo-av';
+
+/**
+ * Whether to replay the video when users press play button
+ */
+export default function shouldReplayVideo(e: AVPlaybackStatusSuccess, isPlaying: boolean, duration: number, position: number): boolean {
+ if (!isPlaying && e.isPlaying && duration === position) {
+ return true;
+ }
+ return false;
+}
diff --git a/src/components/VideoPlayer/shouldReplayVideo.ts b/src/components/VideoPlayer/shouldReplayVideo.ts
new file mode 100644
index 000000000000..3a55562d4bd2
--- /dev/null
+++ b/src/components/VideoPlayer/shouldReplayVideo.ts
@@ -0,0 +1,9 @@
+import type {AVPlaybackStatusSuccess} from 'expo-av';
+
+/**
+ * Whether to replay the video when users press play button
+ */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export default function shouldReplayVideo(e: AVPlaybackStatusSuccess, isPlaying: boolean, duration: number, position: number): boolean {
+ return false;
+}
diff --git a/src/hooks/useNetwork.ts b/src/hooks/useNetwork.ts
index f9e1a627c5f5..1e4a6d4cf2ca 100644
--- a/src/hooks/useNetwork.ts
+++ b/src/hooks/useNetwork.ts
@@ -1,17 +1,18 @@
import {useContext, useEffect, useRef} from 'react';
import {NetworkContext} from '@components/OnyxProvider';
+import CONST from '@src/CONST';
type UseNetworkProps = {
onReconnect?: () => void;
};
-type UseNetwork = {isOffline?: boolean};
+type UseNetwork = {isOffline: boolean};
export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = {}): UseNetwork {
const callback = useRef(onReconnect);
callback.current = onReconnect;
- const {isOffline} = useContext(NetworkContext) ?? {};
+ const {isOffline} = useContext(NetworkContext) ?? CONST.DEFAULT_NETWORK_DATA;
const prevOfflineStatusRef = useRef(isOffline);
useEffect(() => {
// If we were offline before and now we are not offline then we just reconnected
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 0553d6470ddc..b6a24f33035c 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -205,6 +205,7 @@ export default {
iAcceptThe: 'I accept the ',
remove: 'Remove',
admin: 'Admin',
+ owner: 'Owner',
dateFormat: 'YYYY-MM-DD',
send: 'Send',
notifications: 'Notifications',
@@ -308,6 +309,8 @@ export default {
of: 'of',
default: 'Default',
update: 'Update',
+ member: 'Member',
+ role: 'Role',
},
location: {
useCurrent: 'Use current location',
@@ -1745,6 +1748,7 @@ export default {
},
addedWithPrimary: 'Some users were added with their primary logins.',
invitedBySecondaryLogin: ({secondaryLogin}) => `Added by secondary login ${secondaryLogin}.`,
+ membersListTitle: 'Directory of all workspace members.',
},
card: {
header: 'Unlock free Expensify Cards',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 2a2eb96bd488..fc6755519d6f 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -195,6 +195,7 @@ export default {
iAcceptThe: 'Acepto los ',
remove: 'Eliminar',
admin: 'Administrador',
+ owner: 'Poseedor',
dateFormat: 'AAAA-MM-DD',
send: 'Enviar',
notifications: 'Notificaciones',
@@ -298,6 +299,8 @@ export default {
of: 'de',
default: 'Predeterminado',
update: 'Actualizar',
+ member: 'Miembro',
+ role: 'Role',
},
location: {
useCurrent: 'Usar ubicaciĆ³n actual',
@@ -1769,6 +1772,7 @@ export default {
},
addedWithPrimary: 'Se agregaron algunos usuarios con sus nombres de usuario principales.',
invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`,
+ membersListTitle: 'Directorio de todos los miembros del espacio de trabajo.',
},
card: {
header: 'Desbloquea Tarjetas Expensify gratis',
diff --git a/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts b/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts
index f5cc3f664d12..dedc45d0365f 100644
--- a/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts
+++ b/src/libs/API/parameters/UpdateBeneficialOwnersForBankAccountParams.ts
@@ -1,5 +1,5 @@
-import type {BeneficialOwnersStepProps} from '@src/types/form/ReimbursementAccountForm';
+import type {ACHContractStepProps} from '@src/types/form/ReimbursementAccountForm';
-type UpdateBeneficialOwnersForBankAccountParams = BeneficialOwnersStepProps & {bankAccountID: number; policyID: string; canUseNewVbbaFlow?: boolean};
+type UpdateBeneficialOwnersForBankAccountParams = Partial & {bankAccountID: number; policyID: string; canUseNewVbbaFlow?: boolean};
export default UpdateBeneficialOwnersForBankAccountParams;
diff --git a/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts b/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts
index 21ca49839aec..6421fe02f571 100644
--- a/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts
+++ b/src/libs/API/parameters/UpdateCompanyInformationForBankAccountParams.ts
@@ -1,5 +1,7 @@
-import type {CompanyStepProps} from '@src/types/form/ReimbursementAccountForm';
+import type {BankAccountStepProps, CompanyStepProps, ReimbursementAccountProps} from '@src/types/form/ReimbursementAccountForm';
-type UpdateCompanyInformationForBankAccountParams = CompanyStepProps & {bankAccountID: number; policyID: string; canUseNewVbbaFlow?: boolean};
+type BankAccountCompanyInformation = BankAccountStepProps & CompanyStepProps & ReimbursementAccountProps;
+
+type UpdateCompanyInformationForBankAccountParams = Partial & {bankAccountID: number; policyID: string; canUseNewVbbaFlow?: boolean};
export default UpdateCompanyInformationForBankAccountParams;
diff --git a/src/libs/FormUtils.ts b/src/libs/FormUtils.ts
deleted file mode 100644
index 4d0571ada6f2..000000000000
--- a/src/libs/FormUtils.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type {OnyxFormDraftKey, OnyxFormKey} from '@src/ONYXKEYS';
-
-function getDraftKey(formID: OnyxFormKey): OnyxFormDraftKey {
- return `${formID}Draft`;
-}
-
-export default {getDraftKey};
diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts
index 24437da48953..3f8a7d191f4b 100644
--- a/src/libs/GetPhysicalCardUtils.ts
+++ b/src/libs/GetPhysicalCardUtils.ts
@@ -54,7 +54,11 @@ function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDe
* @param privatePersonalDetails
* @returns
*/
-function getUpdatedDraftValues(draftValues: OnyxEntry, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry): GetPhysicalCardForm {
+function getUpdatedDraftValues(
+ draftValues: OnyxEntry,
+ privatePersonalDetails: OnyxEntry,
+ loginList: OnyxEntry,
+): Partial {
const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {};
return {
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index 9f4edd897f66..cd75a6d31fdb 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -100,7 +100,6 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/iou/steps/MoneyRequestConfirmPage').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType,
[SCREENS.MONEY_REQUEST.HOLD]: () => require('../../../pages/iou/HoldReasonPage').default as React.ComponentType,
- [SCREENS.MONEY_REQUEST.CATEGORY]: () => require('../../../pages/iou/MoneyRequestCategoryPage').default as React.ComponentType,
[SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: () => require('../../../pages/AddPersonalBankAccountPage').default as React.ComponentType,
[SCREENS.IOU_SEND.ADD_DEBIT_CARD]: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType,
@@ -247,6 +246,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceInviteMessagePage').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,
[SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceProfileCurrencyPage').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,
diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts
index 3a4abe225120..371ea89df2e2 100644
--- a/src/libs/Navigation/linkTo.ts
+++ b/src/libs/Navigation/linkTo.ts
@@ -215,7 +215,7 @@ export default function linkTo(navigation: NavigationContainerRef> = {
- [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION],
+ [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],
};
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 2640025efa09..ad3dc305f619 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -235,6 +235,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.DESCRIPTION]: {
path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route,
},
+ [SCREENS.WORKSPACE.SHARE]: {
+ path: ROUTES.WORKSPACE_PROFILE_SHARE.route,
+ },
[SCREENS.WORKSPACE.RATE_AND_UNIT]: {
path: ROUTES.WORKSPACE_RATE_AND_UNIT.route,
},
@@ -428,7 +431,6 @@ const config: LinkingOptions['config'] = {
[SCREENS.MONEY_REQUEST.PARTICIPANTS]: ROUTES.MONEY_REQUEST_PARTICIPANTS.route,
[SCREENS.MONEY_REQUEST.CONFIRMATION]: ROUTES.MONEY_REQUEST_CONFIRMATION.route,
[SCREENS.MONEY_REQUEST.CURRENCY]: ROUTES.MONEY_REQUEST_CURRENCY.route,
- [SCREENS.MONEY_REQUEST.CATEGORY]: ROUTES.MONEY_REQUEST_CATEGORY.route,
[SCREENS.MONEY_REQUEST.RECEIPT]: ROUTES.MONEY_REQUEST_RECEIPT.route,
[SCREENS.MONEY_REQUEST.DISTANCE]: ROUTES.MONEY_REQUEST_DISTANCE.route,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS,
diff --git a/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts b/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts
index 55ccca73a389..02ad78a4c044 100644
--- a/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts
+++ b/src/libs/Navigation/linkingConfig/getMatchingCentralPaneRouteForState.ts
@@ -1,9 +1,11 @@
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
-import type {CentralPaneName, NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types';
+import type {CentralPaneName, CentralPaneNavigatorParamList, NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types';
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
*/
@@ -31,8 +33,47 @@ const getTopMostReportIDFromRHP = (state: State): string => {
return '';
};
+// Check if the given route has a policyID equal to the id provided in the function params
+function hasRouteMatchingPolicyID(route: NavigationPartialRoute, policyID?: string) {
+ if (!route.params) {
+ return false;
+ }
+
+ const params = `params` in route?.params ? (route.params.params as Record) : undefined;
+
+ // If params are not defined, then we need to check if the policyID exists
+ if (!params) {
+ return !policyID;
+ }
+
+ return 'policyID' in params && params.policyID === policyID;
+}
+
+// Get already opened settings screen within the policy
+function getAlreadyOpenedSettingsScreen(rootState?: State, policyID?: string): keyof CentralPaneNavigatorParamList | undefined {
+ if (!rootState) {
+ return undefined;
+ }
+
+ // If one of the screen from WORKSPACES_SCREENS 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))
+ .at(-1);
+
+ if (!hasRouteMatchingPolicyID(alreadyOpenedSettingsTab as NavigationPartialRoute, policyID)) {
+ return undefined;
+ }
+
+ const settingsScreen =
+ alreadyOpenedSettingsTab?.params && 'screen' in alreadyOpenedSettingsTab?.params ? (alreadyOpenedSettingsTab?.params?.screen as keyof CentralPaneNavigatorParamList) : undefined;
+
+ return settingsScreen;
+}
+
// Get matching central pane route for bottom tab navigator. e.g HOME -> REPORT
-function getMatchingCentralPaneRouteForState(state: State): NavigationPartialRoute | undefined {
+function getMatchingCentralPaneRouteForState(state: State, rootState?: State): NavigationPartialRoute | undefined {
const topmostBottomTabRoute = getTopmostBottomTabRoute(state);
if (!topmostBottomTabRoute) {
@@ -42,7 +83,10 @@ function getMatchingCentralPaneRouteForState(state: State):
const centralPaneName = TAB_TO_CENTRAL_PANE_MAPPING[topmostBottomTabRoute.name][0];
if (topmostBottomTabRoute.name === SCREENS.WORKSPACE.INITIAL) {
- return {name: centralPaneName, params: topmostBottomTabRoute.params};
+ // 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;
+ return {name: screen, params: topmostBottomTabRoute.params};
}
if (topmostBottomTabRoute.name === SCREENS.HOME) {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 81229f353e52..1438dfdfaf67 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -92,9 +92,15 @@ type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH]: undefined;
[SCREENS.SETTINGS.PROFILE.ADDRESS]: undefined;
[SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: undefined;
- [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: undefined;
- [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: undefined;
- [SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: undefined;
+ [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: {
+ backTo: Routes;
+ };
+ [SCREENS.SETTINGS.PROFILE.CONTACT_METHOD_DETAILS]: {
+ contactMethod: string;
+ };
+ [SCREENS.SETTINGS.PROFILE.NEW_CONTACT_METHOD]: {
+ backTo: Routes;
+ };
[SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined;
[SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE]: undefined;
[SCREENS.SETTINGS.PREFERENCES.LANGUAGE]: undefined;
@@ -146,6 +152,7 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.CURRENCY]: undefined;
[SCREENS.WORKSPACE.NAME]: undefined;
[SCREENS.WORKSPACE.DESCRIPTION]: undefined;
+ [SCREENS.WORKSPACE.SHARE]: undefined;
[SCREENS.WORKSPACE.RATE_AND_UNIT]: {
policyID: string;
};
@@ -257,9 +264,12 @@ type MoneyRequestNavigatorParamList = {
reportID: string;
backTo: string;
};
- [SCREENS.MONEY_REQUEST.CATEGORY]: {
- iouType: string;
+ [SCREENS.MONEY_REQUEST.STEP_CATEGORY]: {
+ action: ValueOf;
+ iouType: ValueOf;
+ transactionID: string;
reportID: string;
+ backTo: string;
};
[SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: {
iouType: string;
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index a85e97a4cf05..974ce88a03ec 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -93,7 +93,7 @@ function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolea
);
}
-function isExpensifyTeam(email: string): boolean {
+function isExpensifyTeam(email: string | undefined): boolean {
const emailDomain = Str.extractEmailDomain(email ?? '');
return emailDomain === CONST.EXPENSIFY_PARTNER_NAME || emailDomain === CONST.EMAIL.GUIDES_DOMAIN;
}
diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts
index e3a667fd5a44..12b52524f113 100644
--- a/src/libs/UserUtils.ts
+++ b/src/libs/UserUtils.ts
@@ -173,7 +173,7 @@ function getAvatarUrl(avatarSource: AvatarSource | undefined, accountID: number)
* Avatars uploaded by users will have a _128 appended so that the asset server returns a small version.
* This removes that part of the URL so the full version of the image can load.
*/
-function getFullSizeAvatar(avatarSource: AvatarSource | undefined, accountID: number): AvatarSource {
+function getFullSizeAvatar(avatarSource: AvatarSource | undefined, accountID?: number): AvatarSource {
const source = getAvatar(avatarSource, accountID);
if (typeof source !== 'string') {
return source;
diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts
index 02ae638a41d3..56cf1c475812 100644
--- a/src/libs/ValidationUtils.ts
+++ b/src/libs/ValidationUtils.ts
@@ -5,7 +5,7 @@ import isDate from 'lodash/isDate';
import isEmpty from 'lodash/isEmpty';
import isObject from 'lodash/isObject';
import type {OnyxCollection} from 'react-native-onyx';
-import type {FormInputErrors, FormOnyxKeys, FormOnyxValues} from '@components/Form/types';
+import type {FormInputErrors, FormOnyxKeys, FormOnyxValues, FormValue} from '@components/Form/types';
import CONST from '@src/CONST';
import type {OnyxFormKey} from '@src/ONYXKEYS';
import type {Report} from '@src/types/onyx';
@@ -77,7 +77,7 @@ function isValidPastDate(date: string | Date): boolean {
* Used to validate a value that is "required".
* @param value - field value
*/
-function isRequiredFulfilled(value?: string | boolean | Date): boolean {
+function isRequiredFulfilled(value?: FormValue): boolean {
if (!value) {
return false;
}
@@ -103,7 +103,7 @@ function getFieldRequiredErrors(values: FormOnyxVal
const errors: FormInputErrors = {};
requiredFields.forEach((fieldKey) => {
- if (isRequiredFulfilled(values[fieldKey] as keyof FormOnyxValues)) {
+ if (isRequiredFulfilled(values[fieldKey] as FormValue)) {
return;
}
diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts
index 30dd03b6e780..0f4e1aed36a7 100644
--- a/src/libs/actions/BankAccounts.ts
+++ b/src/libs/actions/BankAccounts.ts
@@ -366,7 +366,7 @@ function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subS
* Updates the bank account in the database with the company step data
* @param params - Business step form data
*/
-function updateCompanyInformationForBankAccount(bankAccountID: number, params: CompanyStepProps, policyID: string) {
+function updateCompanyInformationForBankAccount(bankAccountID: number, params: Partial, policyID: string) {
API.write(
WRITE_COMMANDS.UPDATE_COMPANY_INFORMATION_FOR_BANK_ACCOUNT,
{
@@ -383,7 +383,7 @@ function updateCompanyInformationForBankAccount(bankAccountID: number, params: C
* Add beneficial owners for the bank account and verify the accuracy of the information provided
* @param params - Beneficial Owners step form params
*/
-function updateBeneficialOwnersForBankAccount(bankAccountID: number, params: BeneficialOwnersStepProps, policyID: string) {
+function updateBeneficialOwnersForBankAccount(bankAccountID: number, params: Partial, policyID: string) {
API.write(
WRITE_COMMANDS.UPDATE_BENEFICIAL_OWNERS_FOR_BANK_ACCOUNT,
{
diff --git a/src/libs/actions/FormActions.ts b/src/libs/actions/FormActions.ts
index 3a0bdb94d5f5..8207b78e8759 100644
--- a/src/libs/actions/FormActions.ts
+++ b/src/libs/actions/FormActions.ts
@@ -1,6 +1,5 @@
import Onyx from 'react-native-onyx';
import type {NullishDeep} from 'react-native-onyx';
-import FormUtils from '@libs/FormUtils';
import type {OnyxFormDraftKey, OnyxFormKey, OnyxValue} from '@src/ONYXKEYS';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
@@ -25,11 +24,11 @@ function clearErrorFields(formID: OnyxFormKey) {
}
function setDraftValues(formID: OnyxFormKey, draftValues: NullishDeep>) {
- Onyx.merge(FormUtils.getDraftKey(formID), draftValues);
+ Onyx.merge(`${formID}Draft`, draftValues);
}
function clearDraftValues(formID: OnyxFormKey) {
- Onyx.set(FormUtils.getDraftKey(formID), null);
+ Onyx.set(`${formID}Draft`, null);
}
export {setDraftValues, setErrorFields, setErrors, clearErrors, clearErrorFields, setIsLoading, clearDraftValues};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index f39728e7d31c..547c187789eb 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -60,7 +60,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import * as Policy from './Policy';
import * as Report from './Report';
-type MoneyRequestRoute = StackScreenProps['route'];
+type MoneyRequestRoute = StackScreenProps['route'];
type IOURequestType = ValueOf;
@@ -304,16 +304,10 @@ function setMoneyRequestPendingFields(transactionID: string, pendingFields: Pend
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {pendingFields});
}
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function setMoneyRequestCategory_temporaryForRefactor(transactionID: string, category: string) {
+function setMoneyRequestCategory(transactionID: string, category: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {category});
}
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function resetMoneyRequestCategory_temporaryForRefactor(transactionID: string) {
- Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {category: null});
-}
-
function setMoneyRequestTag(transactionID: string, tag: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {tag});
}
@@ -3877,14 +3871,6 @@ function setMoneyRequestCurrency(currency: string) {
Onyx.merge(ONYXKEYS.IOU, {currency});
}
-function setMoneyRequestCategory(category: string) {
- Onyx.merge(ONYXKEYS.IOU, {category});
-}
-
-function resetMoneyRequestCategory() {
- Onyx.merge(ONYXKEYS.IOU, {category: ''});
-}
-
function setMoneyRequestTaxRate(transactionID: string, taxRate: TaxRate) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {taxRate});
}
@@ -3942,7 +3928,6 @@ function navigateToNextPage(iou: OnyxEntry, iouType: string, repo
? [{reportID: chatReport?.reportID, isPolicyExpenseChat: true, selected: true}]
: (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true}));
setMoneyRequestParticipants(participants);
- resetMoneyRequestCategory();
}
Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, report.reportID));
return;
@@ -4105,12 +4090,9 @@ export {
startMoneyRequest,
initMoneyRequest,
startMoneyRequest_temporaryForRefactor,
- resetMoneyRequestCategory,
- resetMoneyRequestCategory_temporaryForRefactor,
resetMoneyRequestInfo,
setMoneyRequestAmount_temporaryForRefactor,
setMoneyRequestBillable_temporaryForRefactor,
- setMoneyRequestCategory_temporaryForRefactor,
setMoneyRequestCreated,
setMoneyRequestCurrency_temporaryForRefactor,
setMoneyRequestDescription,
diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts
index ea675ff6b8f6..13e0a42e839f 100644
--- a/src/libs/actions/PaymentMethods.ts
+++ b/src/libs/actions/PaymentMethods.ts
@@ -12,6 +12,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/AddDebitCardForm';
import type {BankAccountList, FundList} from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type PaymentMethod from '@src/types/onyx/PaymentMethod';
@@ -205,7 +206,15 @@ function clearDebitCardFormErrorAndSubmit() {
Onyx.set(ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, {
isLoading: false,
errors: undefined,
- setupComplete: false,
+ [INPUT_IDS.SETUP_COMPLETE]: false,
+ [INPUT_IDS.NAME_ON_CARD]: '',
+ [INPUT_IDS.CARD_NUMBER]: '',
+ [INPUT_IDS.EXPIRATION_DATE]: '',
+ [INPUT_IDS.SECURITY_CODE]: '',
+ [INPUT_IDS.ADDRESS_STREET]: '',
+ [INPUT_IDS.ADDRESS_ZIP_CODE]: '',
+ [INPUT_IDS.ADDRESS_STATE]: '',
+ [INPUT_IDS.ACCEPT_TERMS]: '',
});
}
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 1276207e37c3..f29f8a4fbaab 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -67,6 +67,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/NewRoomForm';
import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx';
import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage';
import type {NotificationPreference, RoomVisibility, WriteCapability} from '@src/types/onyx/Report';
@@ -2841,6 +2842,11 @@ function clearNewRoomFormError() {
isLoading: false,
errorFields: null,
errors: null,
+ [INPUT_IDS.ROOM_NAME]: '',
+ [INPUT_IDS.REPORT_DESCRIPTION]: '',
+ [INPUT_IDS.POLICY_ID]: '',
+ [INPUT_IDS.WRITE_CAPABILITY]: '',
+ [INPUT_IDS.VISIBILITY]: '',
});
}
diff --git a/src/libs/shouldAllowDownloadQRCode/index.native.ts b/src/libs/shouldAllowDownloadQRCode/index.native.ts
new file mode 100644
index 000000000000..ea9b2b9c8aa1
--- /dev/null
+++ b/src/libs/shouldAllowDownloadQRCode/index.native.ts
@@ -0,0 +1,5 @@
+import type ShouldAllowDownloadQRCode from './types';
+
+const shouldAllowDownloadQRCode: ShouldAllowDownloadQRCode = true;
+
+export default shouldAllowDownloadQRCode;
diff --git a/src/libs/shouldAllowDownloadQRCode/index.ts b/src/libs/shouldAllowDownloadQRCode/index.ts
new file mode 100644
index 000000000000..8331f7d4821f
--- /dev/null
+++ b/src/libs/shouldAllowDownloadQRCode/index.ts
@@ -0,0 +1,5 @@
+import type ShouldAllowDownloadQRCode from './types';
+
+const shouldAllowDownloadQRCode: ShouldAllowDownloadQRCode = false;
+
+export default shouldAllowDownloadQRCode;
diff --git a/src/libs/shouldAllowDownloadQRCode/types.ts b/src/libs/shouldAllowDownloadQRCode/types.ts
new file mode 100644
index 000000000000..3bd6c5dc4dd7
--- /dev/null
+++ b/src/libs/shouldAllowDownloadQRCode/types.ts
@@ -0,0 +1,3 @@
+type ShouldAllowDownloadQRCode = boolean;
+
+export default ShouldAllowDownloadQRCode;
diff --git a/src/pages/EditRequestCategoryPage.js b/src/pages/EditRequestCategoryPage.js
deleted file mode 100644
index 205b4bf66dfa..000000000000
--- a/src/pages/EditRequestCategoryPage.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import CategoryPicker from '@components/CategoryPicker';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import ScreenWrapper from '@components/ScreenWrapper';
-import Text from '@components/Text';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import Navigation from '@libs/Navigation/Navigation';
-
-const propTypes = {
- /** Transaction default category value */
- defaultCategory: PropTypes.string.isRequired,
-
- /** The policyID we are getting categories for */
- policyID: PropTypes.string.isRequired,
-
- /** Callback to fire when the Save button is pressed */
- onSubmit: PropTypes.func.isRequired,
-};
-
-function EditRequestCategoryPage({defaultCategory, policyID, onSubmit}) {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
-
- const selectCategory = (category) => {
- onSubmit({
- category: category.searchText,
- });
- };
-
- return (
-
-
- {translate('iou.categorySelection')}
-
-
- );
-}
-
-EditRequestCategoryPage.propTypes = propTypes;
-EditRequestCategoryPage.displayName = 'EditRequestCategoryPage';
-
-export default EditRequestCategoryPage;
diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js
index 29917154a527..c155fc74e0c3 100644
--- a/src/pages/EditRequestPage.js
+++ b/src/pages/EditRequestPage.js
@@ -1,5 +1,4 @@
import lodashGet from 'lodash/get';
-import lodashValues from 'lodash/values';
import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
@@ -21,7 +20,6 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import EditRequestAmountPage from './EditRequestAmountPage';
-import EditRequestCategoryPage from './EditRequestCategoryPage';
import EditRequestDistancePage from './EditRequestDistancePage';
import EditRequestReceiptPage from './EditRequestReceiptPage';
import EditRequestTagPage from './EditRequestTagPage';
@@ -77,7 +75,7 @@ const defaultProps = {
function EditRequestPage({report, route, policy, policyCategories, policyTags, parentReportActions, transaction}) {
const parentReportActionID = lodashGet(report, 'parentReportActionID', '0');
const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {});
- const {amount: transactionAmount, currency: transactionCurrency, category: transactionCategory, tag: transactionTag} = ReportUtils.getTransactionDetails(transaction);
+ const {amount: transactionAmount, currency: transactionCurrency, tag: transactionTag} = ReportUtils.getTransactionDetails(transaction);
const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency;
const fieldToEdit = lodashGet(route, ['params', 'field'], '');
@@ -90,9 +88,6 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
// A flag for verifying that the current report is a sub-report of a workspace chat
const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report);
- // A flag for showing the categories page
- const shouldShowCategories = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(lodashValues(policyCategories)));
-
// A flag for showing the tags page
const shouldShowTags = useMemo(() => isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)), [isPolicyExpenseChat, policyTagLists, transactionTag]);
@@ -145,16 +140,6 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p
[tag, transaction.transactionID, report.reportID, transactionTag, tagIndex, policy, policyTags, policyCategories],
);
- const saveCategory = useCallback(
- ({category: newCategory}) => {
- // In case the same category has been selected, reset the category.
- const updatedCategory = newCategory === transactionCategory ? '' : newCategory;
- IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory, policy, policyTags, policyCategories);
- Navigation.dismissModal();
- },
- [transactionCategory, transaction.transactionID, report.reportID, policy, policyTags, policyCategories],
- );
-
if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT) {
return (
- );
- }
-
if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAG && shouldShowTags) {
return (
{
- setDraftSplitTransaction({category: transactionChanges.category.trim()});
- }}
- />
- );
- }
-
if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.TAG) {
return (
): FormInputErrors => {
const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS);
- if (values.firstName && !ValidationUtils.isValidPersonName(values.firstName)) {
+ if (values.firstName && !ValidationUtils.isValidLegalName(values.firstName)) {
errors.firstName = 'bankAccount.error.firstName';
}
- if (values.lastName && !ValidationUtils.isValidPersonName(values.lastName)) {
+ if (values.lastName && !ValidationUtils.isValidLegalName(values.lastName)) {
errors.lastName = 'bankAccount.error.lastName';
}
return errors;
diff --git a/src/pages/iou/MoneyRequestCategoryPage.js b/src/pages/iou/MoneyRequestCategoryPage.js
deleted file mode 100644
index ceb2152d2b49..000000000000
--- a/src/pages/iou/MoneyRequestCategoryPage.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React from 'react';
-import {withOnyx} from 'react-native-onyx';
-import CategoryPicker from '@components/CategoryPicker';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import ScreenWrapper from '@components/ScreenWrapper';
-import Text from '@components/Text';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
-import Navigation from '@libs/Navigation/Navigation';
-import reportPropTypes from '@pages/reportPropTypes';
-import * as IOU from '@userActions/IOU';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import {iouDefaultProps, iouPropTypes} from './propTypes';
-
-const propTypes = {
- /** Navigation route context info provided by react navigation */
- route: PropTypes.shape({
- /** Route specific parameters used on this screen via route :iouType/new/category/:reportID? */
- params: PropTypes.shape({
- /** The type of IOU report, i.e. bill, request, send */
- iouType: PropTypes.string,
-
- /** The report ID of the IOU */
- reportID: PropTypes.string,
- }),
- }).isRequired,
-
- /* Onyx Props */
- /** The report currently being used */
- report: reportPropTypes,
-
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- iou: iouPropTypes,
-};
-
-const defaultProps = {
- report: {},
- iou: iouDefaultProps,
-};
-
-function MoneyRequestCategoryPage({route, report, iou}) {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
-
- const reportID = lodashGet(route, 'params.reportID', '');
- const iouType = lodashGet(route, 'params.iouType', '');
-
- const navigateBack = () => {
- Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID));
- };
-
- const updateCategory = (category) => {
- if (category.searchText === iou.category) {
- IOU.resetMoneyRequestCategory();
- } else {
- IOU.setMoneyRequestCategory(category.searchText);
- }
-
- Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID));
- };
-
- return (
-
-
- {translate('iou.categorySelection')}
-
-
- );
-}
-
-MoneyRequestCategoryPage.displayName = 'MoneyRequestCategoryPage';
-MoneyRequestCategoryPage.propTypes = propTypes;
-MoneyRequestCategoryPage.defaultProps = defaultProps;
-
-export default compose(
- withOnyx({
- iou: {
- key: ONYXKEYS.IOU,
- },
- }),
- // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file
- withOnyx({
- report: {
- key: ({route, iou}) => {
- const reportID = IOU.getIOUReportID(iou, route);
-
- return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`;
- },
- },
- }),
-)(MoneyRequestCategoryPage);
diff --git a/src/pages/iou/request/step/IOURequestStepCategory.js b/src/pages/iou/request/step/IOURequestStepCategory.js
index 30f0b9c7a338..3e0feec02854 100644
--- a/src/pages/iou/request/step/IOURequestStepCategory.js
+++ b/src/pages/iou/request/step/IOURequestStepCategory.js
@@ -1,13 +1,24 @@
+import lodashGet from 'lodash/get';
+import PropTypes from 'prop-types';
import React from 'react';
+import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
import CategoryPicker from '@components/CategoryPicker';
+import categoryPropTypes from '@components/categoryPropTypes';
+import tagPropTypes from '@components/tagPropTypes';
import Text from '@components/Text';
import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as ReportUtils from '@libs/ReportUtils';
import reportPropTypes from '@pages/reportPropTypes';
import * as IOU from '@userActions/IOU';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import {policyPropTypes} from '@src/pages/workspace/withPolicy';
import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
import StepScreenWrapper from './StepScreenWrapper';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
@@ -21,24 +32,51 @@ const propTypes = {
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
transaction: transactionPropTypes,
+ /** The draft transaction that holds data to be persisted on the current transaction */
+ splitDraftTransaction: transactionPropTypes,
+
/** The report attached to the transaction */
report: reportPropTypes,
+
+ /** The policy of the report */
+ policy: policyPropTypes.policy,
+
+ /** Collection of categories attached to a policy */
+ policyCategories: PropTypes.objectOf(categoryPropTypes),
+
+ /** Collection of tags attached to a policy */
+ policyTags: tagPropTypes,
};
const defaultProps = {
report: {},
transaction: {},
+ splitDraftTransaction: {},
+ policy: null,
+ policyTags: null,
+ policyCategories: null,
};
function IOURequestStepCategory({
report,
route: {
- params: {transactionID, backTo},
+ params: {transactionID, backTo, action, iouType},
},
transaction,
+ splitDraftTransaction,
+ policy,
+ policyTags,
+ policyCategories,
}) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const isEditing = action === CONST.IOU.ACTION.EDIT;
+ const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT;
+ const {category: transactionCategory} = ReportUtils.getTransactionDetails(isEditingSplitBill ? splitDraftTransaction : transaction);
+
+ const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report);
+ // eslint-disable-next-line rulesdir/no-negated-variables
+ const shouldShowNotFoundPage = !isPolicyExpenseChat || (!transactionCategory && !OptionsListUtils.hasEnabledOptions(_.values(policyCategories)));
const navigateBack = () => {
Navigation.goBack(backTo);
@@ -49,11 +87,23 @@ function IOURequestStepCategory({
* @param {String} category.searchText
*/
const updateCategory = (category) => {
- if (category.searchText === transaction.category) {
- IOU.resetMoneyRequestCategory_temporaryForRefactor(transactionID);
- } else {
- IOU.setMoneyRequestCategory_temporaryForRefactor(transactionID, category.searchText);
+ const isSelectedCategory = category.searchText === transactionCategory;
+ const updatedCategory = isSelectedCategory ? '' : category.searchText;
+
+ // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value
+ if (isEditingSplitBill) {
+ IOU.setDraftSplitTransaction(transaction.transactionID, {category: category.searchText});
+ navigateBack();
+ return;
+ }
+
+ if (isEditing) {
+ IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory, policy, policyTags, policyCategories);
+ navigateBack();
+ return;
}
+
+ IOU.setMoneyRequestCategory(transactionID, updatedCategory);
navigateBack();
};
@@ -62,11 +112,12 @@ function IOURequestStepCategory({
headerTitle={translate('common.category')}
onBackButtonPress={navigateBack}
shouldShowWrapper
+ shouldShowNotFoundPage={shouldShowNotFoundPage}
testID={IOURequestStepCategory.displayName}
>
{translate('iou.categorySelection')}
@@ -78,4 +129,24 @@ IOURequestStepCategory.displayName = 'IOURequestStepCategory';
IOURequestStepCategory.propTypes = propTypes;
IOURequestStepCategory.defaultProps = defaultProps;
-export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepCategory);
+export default compose(
+ withWritableReportOrNotFound,
+ withFullTransactionOrNotFound,
+ withOnyx({
+ splitDraftTransaction: {
+ key: ({route}) => {
+ const transactionID = lodashGet(route, 'params.transactionID', 0);
+ return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`;
+ },
+ },
+ policy: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`,
+ },
+ policyCategories: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`,
+ },
+ policyTags: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`,
+ },
+ }),
+)(IOURequestStepCategory);
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js
index 98bb6851d0de..0744fbd600a7 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.js
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js
@@ -133,7 +133,7 @@ function IOURequestStepConfirmation({
return;
}
if (policyCategories && policyCategories[transaction.category] && !policyCategories[transaction.category].enabled) {
- IOU.resetMoneyRequestCategory_temporaryForRefactor(transactionID);
+ IOU.setMoneyRequestCategory(transactionID, '');
}
}, [policyCategories, transaction.category, transactionID]);
const defaultCategory = lodashGet(
@@ -145,7 +145,7 @@ function IOURequestStepConfirmation({
if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !_.isEmpty(transaction.category)) {
return;
}
- IOU.setMoneyRequestCategory_temporaryForRefactor(transactionID, defaultCategory);
+ IOU.setMoneyRequestCategory(transactionID, defaultCategory);
// Prevent resetting to default when unselect category
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transactionID, requestType, defaultCategory]);
diff --git a/src/pages/iou/request/step/IOURequestStepDate.js b/src/pages/iou/request/step/IOURequestStepDate.js
index e1d156572dc2..f7b8b1ca3869 100644
--- a/src/pages/iou/request/step/IOURequestStepDate.js
+++ b/src/pages/iou/request/step/IOURequestStepDate.js
@@ -19,7 +19,7 @@ import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {policyPropTypes} from '@src/pages/workspace/withPolicy';
-import INPUT_IDS from '@src/types/form/MoneyRequestCreatedForm';
+import INPUT_IDS from '@src/types/form/MoneyRequestDateForm';
import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes';
import StepScreenWrapper from './StepScreenWrapper';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js
index 5f1b22cab128..a6f3563bd486 100644
--- a/src/pages/iou/request/step/IOURequestStepParticipants.js
+++ b/src/pages/iou/request/step/IOURequestStepParticipants.js
@@ -71,7 +71,7 @@ function IOURequestStepParticipants({
const goToNextStep = useCallback(() => {
const nextStepIOUType = numberOfParticipants.current === 1 ? iouType : CONST.IOU.TYPE.SPLIT;
IOU.setMoneyRequestTag(transactionID, '');
- IOU.resetMoneyRequestCategory_temporaryForRefactor(transactionID);
+ IOU.setMoneyRequestCategory(transactionID, '');
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(nextStepIOUType, transactionID, selectedReportID.current || reportID));
}, [iouType, transactionID, reportID]);
diff --git a/src/pages/iou/request/step/StepScreenWrapper.js b/src/pages/iou/request/step/StepScreenWrapper.js
index eae542f0f6f9..1d9129861db0 100644
--- a/src/pages/iou/request/step/StepScreenWrapper.js
+++ b/src/pages/iou/request/step/StepScreenWrapper.js
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import _ from 'underscore';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -23,6 +24,9 @@ const propTypes = {
/** Whether or not the wrapper should be shown (sometimes screens can be embedded inside another screen that already is using a wrapper) */
shouldShowWrapper: PropTypes.bool.isRequired,
+ /** Whether or not to display not found page */
+ shouldShowNotFoundPage: PropTypes.bool,
+
/** An ID used for unit testing */
testID: PropTypes.string.isRequired,
@@ -33,11 +37,16 @@ const propTypes = {
const defaultProps = {
onEntryTransitionEnd: () => {},
includeSafeAreaPaddingBottom: false,
+ shouldShowNotFoundPage: false,
};
-function StepScreenWrapper({testID, headerTitle, onBackButtonPress, onEntryTransitionEnd, children, shouldShowWrapper, includeSafeAreaPaddingBottom}) {
+function StepScreenWrapper({testID, headerTitle, onBackButtonPress, onEntryTransitionEnd, children, shouldShowWrapper, shouldShowNotFoundPage, includeSafeAreaPaddingBottom}) {
const styles = useThemeStyles();
+ if (shouldShowNotFoundPage) {
+ return ;
+ }
+
if (!shouldShowWrapper) {
return children;
}
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
index ea57d88579ae..fc522816b4ce 100644
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
@@ -87,7 +87,6 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) {
const navigateToConfirmationStep = (moneyRequestType) => {
IOU.setMoneyRequestId(moneyRequestType);
- IOU.resetMoneyRequestCategory();
Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(moneyRequestType, reportID));
};
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js
deleted file mode 100644
index a9acf37ae556..000000000000
--- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js
+++ /dev/null
@@ -1,393 +0,0 @@
-import Str from 'expensify-common/lib/str';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {Component} from 'react';
-import {InteractionManager, Keyboard, ScrollView, View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import ConfirmModal from '@components/ConfirmModal';
-import DotIndicatorMessage from '@components/DotIndicatorMessage';
-import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
-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 Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import withTheme, {withThemePropTypes} from '@components/withTheme';
-import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles';
-import compose from '@libs/compose';
-import {canUseTouchScreen} from '@libs/DeviceCapabilities';
-import * as ErrorUtils from '@libs/ErrorUtils';
-import {translatableTextPropTypes} from '@libs/Localize';
-import Navigation from '@libs/Navigation/Navigation';
-import * as Session from '@userActions/Session';
-import * as User from '@userActions/User';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import ValidateCodeForm from './ValidateCodeForm';
-
-const propTypes = {
- /* Onyx Props */
-
- /** Login list for the user that is signed in */
- loginList: PropTypes.shape({
- /** Value of partner name */
- partnerName: PropTypes.string,
-
- /** Phone/Email associated with user */
- partnerUserID: PropTypes.string,
-
- /** Date when login was validated */
- validatedDate: PropTypes.string,
-
- /** Field-specific server side errors keyed by microtime */
- errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
-
- /** Field-specific pending states for offline UI status */
- pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
- }),
-
- /** Current user session */
- session: PropTypes.shape({
- email: PropTypes.string.isRequired,
- }),
-
- /** User's security group IDs by domain */
- myDomainSecurityGroups: PropTypes.objectOf(PropTypes.string),
-
- /** All of the user's security groups and their settings */
- securityGroups: PropTypes.shape({
- hasRestrictedPrimaryLogin: PropTypes.bool,
- }),
-
- /** Route params */
- route: PropTypes.shape({
- params: PropTypes.shape({
- /** Passed via route /settings/profile/contact-methods/:contactMethod/details */
- contactMethod: PropTypes.string,
- }),
- }),
-
- /** Indicated whether the report data is loading */
- isLoadingReportData: PropTypes.bool,
-
- ...withLocalizePropTypes,
- ...withThemeStylesPropTypes,
- ...withThemePropTypes,
-};
-
-const defaultProps = {
- loginList: {},
- session: {
- email: null,
- },
- myDomainSecurityGroups: {},
- securityGroups: {},
- route: {
- params: {
- contactMethod: '',
- },
- },
- isLoadingReportData: true,
-};
-
-class ContactMethodDetailsPage extends Component {
- constructor(props) {
- super(props);
-
- this.deleteContactMethod = this.deleteContactMethod.bind(this);
- this.toggleDeleteModal = this.toggleDeleteModal.bind(this);
- this.confirmDeleteAndHideModal = this.confirmDeleteAndHideModal.bind(this);
- this.getContactMethod = this.getContactMethod.bind(this);
- this.setAsDefault = this.setAsDefault.bind(this);
-
- this.state = {
- isDeleteModalOpen: false,
- };
-
- this.validateCodeFormRef = React.createRef();
- }
-
- componentDidMount() {
- const contactMethod = this.getContactMethod();
- const loginData = lodashGet(this.props.loginList, contactMethod, {});
- if (_.isEmpty(loginData)) {
- return;
- }
- User.resetContactMethodValidateCodeSentState(this.getContactMethod());
- }
-
- componentDidUpdate(prevProps) {
- const contactMethod = this.getContactMethod();
- const validatedDate = lodashGet(this.props.loginList, [contactMethod, 'validatedDate']);
- const prevValidatedDate = lodashGet(prevProps.loginList, [contactMethod, 'validatedDate']);
-
- const loginData = lodashGet(this.props.loginList, contactMethod, {});
- const isDefaultContactMethod = this.props.session.email === loginData.partnerUserID;
- // Navigate to methods page on successful magic code verification
- // validatedDate property is responsible to decide the status of the magic code verification
- if (!prevValidatedDate && validatedDate) {
- // If the selected contactMethod is the current session['login'] and the account is unvalidated,
- // the current authToken is invalid after the successful magic code verification.
- // So we need to sign out the user and redirect to the sign in page.
- if (isDefaultContactMethod) {
- Session.signOutAndRedirectToSignIn();
- return;
- }
- Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route);
- }
- }
-
- /**
- * Gets the current contact method from the route params
- * @returns {string}
- */
- getContactMethod() {
- const contactMethod = lodashGet(this.props.route, 'params.contactMethod');
-
- // We find the number of times the url is encoded based on the last % sign and remove them.
- const lastPercentIndex = contactMethod.lastIndexOf('%');
- const encodePercents = contactMethod.substring(lastPercentIndex).match(new RegExp('25', 'g'));
- let numberEncodePercents = encodePercents ? encodePercents.length : 0;
- const beforeAtSign = contactMethod.substring(0, lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, (match) => {
- if (numberEncodePercents > 0) {
- numberEncodePercents--;
- return '%';
- }
- return match;
- });
- const afterAtSign = contactMethod.substring(lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, '%');
-
- return decodeURIComponent(beforeAtSign + afterAtSign);
- }
-
- /**
- * Attempt to set this contact method as user's "Default contact method"
- */
- setAsDefault() {
- User.setContactMethodAsDefault(this.getContactMethod());
- }
-
- /**
- * Checks if the user is allowed to change their default contact method. This should only be allowed if:
- * 1. The viewed contact method is not already their default contact method
- * 2. The viewed contact method is validated
- * 3. If the user is on a private domain, their security group must allow primary login switching
- *
- * @returns {Boolean}
- */
- canChangeDefaultContactMethod() {
- const contactMethod = this.getContactMethod();
- const loginData = lodashGet(this.props.loginList, contactMethod, {});
- const isDefaultContactMethod = this.props.session.email === loginData.partnerUserID;
-
- // Cannot set this contact method as default if:
- // 1. This contact method is already their default
- // 2. This contact method is not validated
- if (isDefaultContactMethod || !loginData.validatedDate) {
- return false;
- }
-
- const domainName = Str.extractEmailDomain(this.props.session.email);
- const primaryDomainSecurityGroupID = lodashGet(this.props.myDomainSecurityGroups, domainName);
-
- // If there's no security group associated with the user for the primary domain,
- // default to allowing the user to change their default contact method.
- if (!primaryDomainSecurityGroupID) {
- return true;
- }
-
- // Allow user to change their default contact method if they don't have a security group OR if their security group
- // does NOT restrict primary login switching.
- return !lodashGet(this.props.securityGroups, [`${ONYXKEYS.COLLECTION.SECURITY_GROUP}${primaryDomainSecurityGroupID}`, 'hasRestrictedPrimaryLogin'], false);
- }
-
- /**
- * Deletes the contact method if it has errors. Otherwise, it shows the confirmation alert and deletes it only if the user confirms.
- */
- deleteContactMethod() {
- if (!_.isEmpty(lodashGet(this.props.loginList, [this.getContactMethod(), 'errorFields'], {}))) {
- User.deleteContactMethod(this.getContactMethod(), this.props.loginList);
- return;
- }
- this.toggleDeleteModal(true);
- }
-
- /**
- * Toggle delete confirm modal visibility
- * @param {Boolean} isOpen
- */
- toggleDeleteModal(isOpen) {
- if (canUseTouchScreen() && isOpen) {
- InteractionManager.runAfterInteractions(() => {
- this.setState({isDeleteModalOpen: isOpen});
- });
- Keyboard.dismiss();
- } else {
- this.setState({isDeleteModalOpen: isOpen});
- }
- }
-
- /**
- * Delete the contact method and hide the modal
- */
- confirmDeleteAndHideModal() {
- this.toggleDeleteModal(false);
- User.deleteContactMethod(this.getContactMethod(), this.props.loginList);
- }
-
- render() {
- const contactMethod = this.getContactMethod();
-
- // Replacing spaces with "hard spaces" to prevent breaking the number
- const formattedContactMethod = Str.isSMSLogin(contactMethod) ? this.props.formatPhoneNumber(contactMethod).replace(/ /g, '\u00A0') : contactMethod;
-
- if (this.props.isLoadingReportData && _.isEmpty(this.props.loginList)) {
- return ;
- }
-
- const loginData = this.props.loginList[contactMethod];
- if (!contactMethod || !loginData) {
- return (
-
- Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
- onLinkPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
- />
-
- );
- }
-
- const isDefaultContactMethod = this.props.session.email === loginData.partnerUserID;
- const hasMagicCodeBeenSent = lodashGet(this.props.loginList, [contactMethod, 'validateCodeSent'], false);
- const isFailedAddContactMethod = Boolean(lodashGet(loginData, 'errorFields.addedLogin'));
- const isFailedRemovedContactMethod = Boolean(lodashGet(loginData, 'errorFields.deletedLogin'));
-
- return (
- this.validateCodeFormRef.current && this.validateCodeFormRef.current.focus()}
- testID={ContactMethodDetailsPage.displayName}
- >
- Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
- />
-
- this.toggleDeleteModal(false)}
- onModalHide={() => {
- InteractionManager.runAfterInteractions(() => {
- if (!this.validateCodeFormRef.current) {
- return;
- }
- this.validateCodeFormRef.current.focusLastSelected();
- });
- }}
- prompt={this.props.translate('contacts.removeAreYouSure')}
- confirmText={this.props.translate('common.yesContinue')}
- cancelText={this.props.translate('common.cancel')}
- isVisible={this.state.isDeleteModalOpen && !isDefaultContactMethod}
- danger
- />
-
- {isFailedAddContactMethod && (
-
- )}
-
- {!loginData.validatedDate && !isFailedAddContactMethod && (
-
-
-
-
-
- )}
- {this.canChangeDefaultContactMethod() ? (
- User.clearContactMethodErrors(contactMethod, 'defaultLogin')}
- >
-
-
- ) : null}
- {isDefaultContactMethod ? (
- User.clearContactMethodErrors(contactMethod, isFailedRemovedContactMethod ? 'deletedLogin' : 'defaultLogin')}
- >
- {this.props.translate('contacts.yourDefaultContactMethod')}
-
- ) : (
- User.clearContactMethodErrors(contactMethod, 'deletedLogin')}
- >
-
- )}
-
-
- );
- }
-}
-
-ContactMethodDetailsPage.propTypes = propTypes;
-ContactMethodDetailsPage.defaultProps = defaultProps;
-ContactMethodDetailsPage.displayName = 'ContactMethodDetailsPage';
-
-export default compose(
- withLocalize,
- withOnyx({
- loginList: {
- key: ONYXKEYS.LOGIN_LIST,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- myDomainSecurityGroups: {
- key: ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS,
- },
- securityGroups: {
- key: `${ONYXKEYS.COLLECTION.SECURITY_GROUP}`,
- },
- isLoadingReportData: {
- key: `${ONYXKEYS.IS_LOADING_REPORT_DATA}`,
- },
- }),
- withThemeStyles,
- withTheme,
-)(ContactMethodDetailsPage);
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
new file mode 100644
index 000000000000..7de22da728dd
--- /dev/null
+++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx
@@ -0,0 +1,305 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import Str from 'expensify-common/lib/str';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import {InteractionManager, Keyboard, ScrollView, View} from 'react-native';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import ConfirmModal from '@components/ConfirmModal';
+import DotIndicatorMessage from '@components/DotIndicatorMessage';
+import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+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 Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import usePrevious from '@hooks/usePrevious';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {canUseTouchScreen} from '@libs/DeviceCapabilities';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import * as Session from '@userActions/Session';
+import * as User from '@userActions/User';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {LoginList, SecurityGroup, Session as TSession} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import ValidateCodeForm from './ValidateCodeForm';
+import type {ValidateCodeFormHandle} from './ValidateCodeForm/BaseValidateCodeForm';
+
+type ContactMethodDetailsPageOnyxProps = {
+ /** Login list for the user that is signed in */
+ loginList: OnyxEntry;
+
+ /** Current user session */
+ session: OnyxEntry;
+
+ /** User's security group IDs by domain */
+ myDomainSecurityGroups: OnyxEntry>;
+
+ /** All of the user's security groups and their settings */
+ securityGroups: OnyxCollection;
+
+ /** Indicated whether the report data is loading */
+ isLoadingReportData: OnyxEntry;
+};
+
+type ContactMethodDetailsPageProps = ContactMethodDetailsPageOnyxProps & StackScreenProps;
+
+function ContactMethodDetailsPage({loginList, session, myDomainSecurityGroups, securityGroups, isLoadingReportData = true, route}: ContactMethodDetailsPageProps) {
+ const {formatPhoneNumber, translate} = useLocalize();
+ const theme = useTheme();
+ const themeStyles = useThemeStyles();
+
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const validateCodeFormRef = useRef(null);
+
+ /**
+ * Gets the current contact method from the route params
+ */
+ const contactMethod: string = useMemo(() => {
+ const contactMethodParam = route.params.contactMethod;
+
+ // We find the number of times the url is encoded based on the last % sign and remove them.
+ const lastPercentIndex = contactMethodParam.lastIndexOf('%');
+ const encodePercents = contactMethodParam.substring(lastPercentIndex).match(new RegExp('25', 'g'));
+ let numberEncodePercents = encodePercents?.length ?? 0;
+ const beforeAtSign = contactMethodParam.substring(0, lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, (match) => {
+ if (numberEncodePercents > 0) {
+ numberEncodePercents--;
+ return '%';
+ }
+ return match;
+ });
+ const afterAtSign = contactMethodParam.substring(lastPercentIndex).replace(CONST.REGEX.ENCODE_PERCENT_CHARACTER, '%');
+
+ return decodeURIComponent(beforeAtSign + afterAtSign);
+ }, [route.params.contactMethod]);
+ const loginData = useMemo(() => loginList?.[contactMethod], [loginList, contactMethod]);
+ const isDefaultContactMethod = useMemo(() => session?.email === loginData?.partnerUserID, [session?.email, loginData?.partnerUserID]);
+
+ /**
+ * Attempt to set this contact method as user's "Default contact method"
+ */
+ const setAsDefault = useCallback(() => {
+ User.setContactMethodAsDefault(contactMethod);
+ }, [contactMethod]);
+
+ /**
+ * Checks if the user is allowed to change their default contact method. This should only be allowed if:
+ * 1. The viewed contact method is not already their default contact method
+ * 2. The viewed contact method is validated
+ * 3. If the user is on a private domain, their security group must allow primary login switching
+ */
+ const canChangeDefaultContactMethod = useMemo(() => {
+ // Cannot set this contact method as default if:
+ // 1. This contact method is already their default
+ // 2. This contact method is not validated
+ if (isDefaultContactMethod || !loginData?.validatedDate) {
+ return false;
+ }
+
+ const domainName = Str.extractEmailDomain(session?.email ?? '');
+ const primaryDomainSecurityGroupID = myDomainSecurityGroups?.[domainName];
+
+ // If there's no security group associated with the user for the primary domain,
+ // default to allowing the user to change their default contact method.
+ if (!primaryDomainSecurityGroupID) {
+ return true;
+ }
+
+ // Allow user to change their default contact method if they don't have a security group OR if their security group
+ // does NOT restrict primary login switching.
+ return !securityGroups?.[`${ONYXKEYS.COLLECTION.SECURITY_GROUP}${primaryDomainSecurityGroupID}`]?.hasRestrictedPrimaryLogin;
+ }, [isDefaultContactMethod, loginData?.validatedDate, session?.email, myDomainSecurityGroups, securityGroups]);
+
+ /**
+ * Toggle delete confirm modal visibility
+ */
+ const toggleDeleteModal = useCallback((isOpen: boolean) => {
+ if (canUseTouchScreen() && isOpen) {
+ InteractionManager.runAfterInteractions(() => {
+ setIsDeleteModalOpen(isOpen);
+ });
+ Keyboard.dismiss();
+ } else {
+ setIsDeleteModalOpen(isOpen);
+ }
+ }, []);
+
+ /**
+ * Delete the contact method and hide the modal
+ */
+ const confirmDeleteAndHideModal = useCallback(() => {
+ toggleDeleteModal(false);
+ User.deleteContactMethod(contactMethod, loginList ?? {});
+ }, [contactMethod, loginList, toggleDeleteModal]);
+
+ useEffect(() => {
+ if (isEmptyObject(loginData)) {
+ return;
+ }
+ User.resetContactMethodValidateCodeSentState(contactMethod);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const prevValidatedDate = usePrevious(loginData?.validatedDate);
+ useEffect(() => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ if (prevValidatedDate || !loginData?.validatedDate) {
+ return;
+ }
+
+ // If the selected contactMethod is the current session['login'] and the account is unvalidated,
+ // the current authToken is invalid after the successful magic code verification.
+ // So we need to sign out the user and redirect to the sign in page.
+ if (isDefaultContactMethod) {
+ Session.signOutAndRedirectToSignIn();
+ return;
+ }
+ // Navigate to methods page on successful magic code verification
+ // validatedDate property is responsible to decide the status of the magic code verification
+ Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route);
+ }, [prevValidatedDate, loginData?.validatedDate, isDefaultContactMethod]);
+
+ if (isLoadingReportData && isEmptyObject(loginList)) {
+ return ;
+ }
+
+ if (!contactMethod || !loginData) {
+ return (
+
+ Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
+ onLinkPress={() => Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
+ />
+
+ );
+ }
+
+ // Replacing spaces with "hard spaces" to prevent breaking the number
+ const formattedContactMethod = Str.isSMSLogin(contactMethod) ? formatPhoneNumber(contactMethod).replace(/ /g, '\u00A0') : contactMethod;
+ const hasMagicCodeBeenSent = !!loginData.validateCodeSent;
+ const isFailedAddContactMethod = !!loginData.errorFields?.addedLogin;
+ const isFailedRemovedContactMethod = !!loginData.errorFields?.deletedLogin;
+
+ return (
+ validateCodeFormRef.current?.focus?.()}
+ testID={ContactMethodDetailsPage.displayName}
+ >
+ Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route)}
+ />
+
+ toggleDeleteModal(false)}
+ onModalHide={() => {
+ InteractionManager.runAfterInteractions(() => {
+ validateCodeFormRef.current?.focusLastSelected?.();
+ });
+ }}
+ prompt={translate('contacts.removeAreYouSure')}
+ confirmText={translate('common.yesContinue')}
+ cancelText={translate('common.cancel')}
+ isVisible={isDeleteModalOpen && !isDefaultContactMethod}
+ danger
+ />
+
+ {isFailedAddContactMethod && (
+
+ )}
+
+ {!loginData.validatedDate && !isFailedAddContactMethod && (
+
+
+
+
+
+ )}
+ {canChangeDefaultContactMethod ? (
+ User.clearContactMethodErrors(contactMethod, 'defaultLogin')}
+ >
+
+
+ ) : null}
+ {isDefaultContactMethod ? (
+ User.clearContactMethodErrors(contactMethod, isFailedRemovedContactMethod ? 'deletedLogin' : 'defaultLogin')}
+ >
+ {translate('contacts.yourDefaultContactMethod')}
+
+ ) : (
+ User.clearContactMethodErrors(contactMethod, 'deletedLogin')}
+ >
+
+ )}
+
+
+ );
+}
+
+ContactMethodDetailsPage.displayName = 'ContactMethodDetailsPage';
+
+export default withOnyx({
+ loginList: {
+ key: ONYXKEYS.LOGIN_LIST,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ myDomainSecurityGroups: {
+ key: ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS,
+ },
+ securityGroups: {
+ key: `${ONYXKEYS.COLLECTION.SECURITY_GROUP}`,
+ },
+ isLoadingReportData: {
+ key: `${ONYXKEYS.IS_LOADING_REPORT_DATA}`,
+ },
+})(ContactMethodDetailsPage);
diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
similarity index 53%
rename from src/pages/settings/Profile/Contacts/ContactMethodsPage.js
rename to src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
index c85d123ad3fd..5d150e782c44 100644
--- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js
+++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx
@@ -1,10 +1,9 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useCallback} from 'react';
import {ScrollView, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import Button from '@components/Button';
import CopyTextToClipboard from '@components/CopyTextToClipboard';
import FixedFooter from '@components/FixedFooter';
@@ -13,86 +12,64 @@ import MenuItem from '@components/MenuItem';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
-import {translatableTextPropTypes} from '@libs/Localize';
import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {LoginList, Session} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
-const propTypes = {
- /* Onyx Props */
-
+type ContactMethodsPageOnyxProps = {
/** Login list for the user that is signed in */
- loginList: PropTypes.shape({
- /** The partner creating the account. It depends on the source: website, mobile, integrations, ... */
- partnerName: PropTypes.string,
-
- /** Phone/Email associated with user */
- partnerUserID: PropTypes.string,
-
- /** The date when the login was validated, used to show the brickroad status */
- validatedDate: PropTypes.string,
-
- /** Field-specific server side errors keyed by microtime */
- errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
-
- /** Field-specific pending states for offline UI status */
- pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
- }),
+ loginList: OnyxEntry;
/** Current user session */
- session: PropTypes.shape({
- email: PropTypes.string.isRequired,
- }),
-
- ...withLocalizePropTypes,
+ session: OnyxEntry;
};
-const defaultProps = {
- loginList: {},
- session: {
- email: null,
- },
-};
+type ContactMethodsPageProps = ContactMethodsPageOnyxProps & StackScreenProps;
-function ContactMethodsPage(props) {
+function ContactMethodsPage({loginList, session, route}: ContactMethodsPageProps) {
const styles = useThemeStyles();
- const loginNames = _.keys(props.loginList);
- const navigateBackTo = lodashGet(props.route, 'params.backTo', '');
+ const {formatPhoneNumber, translate} = useLocalize();
+ const loginNames = Object.keys(loginList ?? {});
+ const navigateBackTo = route?.params?.backTo || ROUTES.SETTINGS_PROFILE;
// Sort the login names by placing the one corresponding to the default contact method as the first item before displaying the contact methods.
// The default contact method is determined by checking against the session email (the current login).
- const sortedLoginNames = _.sortBy(loginNames, (loginName) => (props.loginList[loginName].partnerUserID === props.session.email ? 0 : 1));
+ const sortedLoginNames = loginNames.sort((loginName) => (loginList?.[loginName].partnerUserID === session?.email ? -1 : 1));
- const loginMenuItems = _.map(sortedLoginNames, (loginName) => {
- const login = props.loginList[loginName];
- const pendingAction = lodashGet(login, 'pendingFields.deletedLogin') || lodashGet(login, 'pendingFields.addedLogin');
- if (!login.partnerUserID && _.isEmpty(pendingAction)) {
+ const loginMenuItems = sortedLoginNames.map((loginName) => {
+ const login = loginList?.[loginName];
+ const pendingAction = login?.pendingFields?.deletedLogin ?? login?.pendingFields?.addedLogin ?? undefined;
+ if (!login?.partnerUserID && !pendingAction) {
return null;
}
let description = '';
- if (props.session.email === login.partnerUserID) {
- description = props.translate('contacts.getInTouch');
- } else if (lodashGet(login, 'errorFields.addedLogin')) {
- description = props.translate('contacts.failedNewContact');
- } else if (!login.validatedDate) {
- description = props.translate('contacts.pleaseVerify');
+ if (session?.email === login?.partnerUserID) {
+ description = translate('contacts.getInTouch');
+ } else if (login?.errorFields?.addedLogin) {
+ description = translate('contacts.failedNewContact');
+ } else if (!login?.validatedDate) {
+ description = translate('contacts.pleaseVerify');
}
- let indicator = null;
- if (_.some(lodashGet(login, 'errorFields', {}), (errorField) => !_.isEmpty(errorField))) {
+ let indicator;
+ if (Object.values(login?.errorFields ?? {}).some((errorField) => !isEmptyObject(errorField))) {
indicator = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR;
- } else if (!login.validatedDate) {
+ } else if (!login?.validatedDate) {
indicator = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO;
}
// Default to using login key if we deleted login.partnerUserID optimistically
// but still need to show the pending login being deleted while offline.
- const partnerUserID = login.partnerUserID || loginName;
- const menuItemTitle = Str.isSMSLogin(partnerUserID) ? props.formatPhoneNumber(partnerUserID) : partnerUserID;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const partnerUserID = login?.partnerUserID || loginName;
+ const menuItemTitle = Str.isSMSLogin(partnerUserID) ? formatPhoneNumber(partnerUserID) : partnerUserID;
return (
);
@@ -126,25 +103,25 @@ function ContactMethodsPage(props) {
testID={ContactMethodsPage.displayName}
>
Navigation.goBack(navigateBackTo)}
/>
- {props.translate('contacts.helpTextBeforeEmail')}
+ {translate('contacts.helpTextBeforeEmail')}
- {props.translate('contacts.helpTextAfterEmail')}
+ {translate('contacts.helpTextAfterEmail')}
{loginMenuItems}
@@ -154,18 +131,13 @@ function ContactMethodsPage(props) {
);
}
-ContactMethodsPage.propTypes = propTypes;
-ContactMethodsPage.defaultProps = defaultProps;
ContactMethodsPage.displayName = 'ContactMethodsPage';
-export default compose(
- withLocalize,
- withOnyx({
- loginList: {
- key: ONYXKEYS.LOGIN_LIST,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- }),
-)(ContactMethodsPage);
+export default withOnyx({
+ loginList: {
+ key: ONYXKEYS.LOGIN_LIST,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+})(ContactMethodsPage);
diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
similarity index 56%
rename from src/pages/settings/Profile/Contacts/NewContactMethodPage.js
rename to src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
index b9d5dee8f4be..20e12f71664e 100644
--- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js
+++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx
@@ -1,57 +1,40 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import Str from 'expensify-common/lib/str';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useCallback, useRef} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
-import {translatableTextPropTypes} from '@libs/Localize';
import * as LoginUtils from '@libs/LoginUtils';
import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import * as User from '@userActions/User';
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/NewContactMethodForm';
+import type {LoginList} from '@src/types/onyx';
+import type {Errors} from '@src/types/onyx/OnyxCommon';
-const propTypes = {
- /* Onyx Props */
-
+type NewContactMethodPageOnyxProps = {
/** Login list for the user that is signed in */
- loginList: PropTypes.shape({
- /** The partner creating the account. It depends on the source: website, mobile, integrations, ... */
- partnerName: PropTypes.string,
-
- /** Phone/Email associated with user */
- partnerUserID: PropTypes.string,
-
- /** The date when the login was validated, used to show the brickroad status */
- validatedDate: PropTypes.string,
-
- /** Field-specific server side errors keyed by microtime */
- errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
-
- /** Field-specific pending states for offline UI status */
- pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
- }),
-
- ...withLocalizePropTypes,
-};
-const defaultProps = {
- loginList: {},
+ loginList: OnyxEntry;
};
-const addNewContactMethod = (values) => {
+type NewContactMethodPageProps = NewContactMethodPageOnyxProps & StackScreenProps;
+
+const addNewContactMethod = (values: FormOnyxValues) => {
const phoneLogin = LoginUtils.getPhoneLogin(values.phoneOrEmail);
const validateIfnumber = LoginUtils.validateNumber(phoneLogin);
const submitDetail = (validateIfnumber || values.phoneOrEmail).trim().toLowerCase();
@@ -59,35 +42,36 @@ const addNewContactMethod = (values) => {
User.addNewContactMethodAndNavigate(submitDetail);
};
-function NewContactMethodPage(props) {
+function NewContactMethodPage({loginList, route}: NewContactMethodPageProps) {
const styles = useThemeStyles();
- const loginInputRef = useRef(null);
+ const {translate} = useLocalize();
+ const loginInputRef = useRef(null);
- const navigateBackTo = lodashGet(props.route, 'params.backTo', ROUTES.SETTINGS_PROFILE);
+ const navigateBackTo = route?.params?.backTo ?? ROUTES.SETTINGS_PROFILE;
const validate = React.useCallback(
- (values) => {
+ (values: FormOnyxValues): Errors => {
const phoneLogin = LoginUtils.getPhoneLogin(values.phoneOrEmail);
const validateIfnumber = LoginUtils.validateNumber(phoneLogin);
const errors = {};
- if (_.isEmpty(values.phoneOrEmail)) {
+ if (!values.phoneOrEmail) {
ErrorUtils.addErrorMessage(errors, 'phoneOrEmail', 'contacts.genericFailureMessages.contactMethodRequired');
}
- if (!_.isEmpty(values.phoneOrEmail) && !(validateIfnumber || Str.isValidEmail(values.phoneOrEmail))) {
+ if (!!values.phoneOrEmail && !(validateIfnumber || Str.isValidEmail(values.phoneOrEmail))) {
ErrorUtils.addErrorMessage(errors, 'phoneOrEmail', 'contacts.genericFailureMessages.invalidContactMethod');
}
- if (!_.isEmpty(values.phoneOrEmail) && lodashGet(props.loginList, validateIfnumber || values.phoneOrEmail.toLowerCase())) {
+ if (!!values.phoneOrEmail && loginList?.[validateIfnumber || values.phoneOrEmail.toLowerCase()]) {
ErrorUtils.addErrorMessage(errors, 'phoneOrEmail', 'contacts.genericFailureMessages.enteredMethodIsAlreadySubmited');
}
return errors;
},
- // We don't need `props.loginList` because when submitting this form
- // the props.loginList gets updated, causing this function to run again.
+ // We don't need `loginList` because when submitting this form
+ // the loginList gets updated, causing this function to run again.
// https://github.com/Expensify/App/issues/20610
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
@@ -103,38 +87,32 @@ function NewContactMethodPage(props) {
return (
{
- if (!loginInputRef.current) {
- return;
- }
-
- loginInputRef.current.focus();
- }}
+ onEntryTransitionEnd={() => loginInputRef.current?.focus()}
includeSafeAreaPaddingBottom={false}
shouldEnableMaxHeight
testID={NewContactMethodPage.displayName}
>
- {props.translate('common.pleaseEnterEmailOrPhoneNumber')}
-
+ {translate('common.pleaseEnterEmailOrPhoneNumber')}
+
(loginInputRef.current = el)}
+ ref={loginInputRef}
inputID={INPUT_IDS.PHONE_OR_EMAIL}
autoCapitalize="none"
enterKeyHint="done"
@@ -146,13 +124,8 @@ function NewContactMethodPage(props) {
);
}
-NewContactMethodPage.propTypes = propTypes;
-NewContactMethodPage.defaultProps = defaultProps;
NewContactMethodPage.displayName = 'NewContactMethodPage';
-export default compose(
- withLocalize,
- withOnyx({
- loginList: {key: ONYXKEYS.LOGIN_LIST},
- }),
-)(NewContactMethodPage);
+export default withOnyx({
+ loginList: {key: ONYXKEYS.LOGIN_LIST},
+})(NewContactMethodPage);
diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx
similarity index 56%
rename from src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js
rename to src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx
index 5c1fa30a88f1..adf2680549c7 100644
--- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.js
+++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/BaseValidateCodeForm.tsx
@@ -1,95 +1,82 @@
import {useFocusEffect} from '@react-navigation/native';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
+import type {ForwardedRef} from 'react';
import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import Button from '@components/Button';
import DotIndicatorMessage from '@components/DotIndicatorMessage';
import MagicCodeInput from '@components/MagicCodeInput';
+import type {AutoCompleteVariant, MagicCodeInputHandle} from '@components/MagicCodeInput';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
-import {withNetwork} from '@components/OnyxProvider';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Text from '@components/Text';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
-import {translatableTextPropTypes} from '@libs/Localize';
import * as ValidationUtils from '@libs/ValidationUtils';
import * as Session from '@userActions/Session';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
+import type {Account, LoginList} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
-const propTypes = {
- ...withLocalizePropTypes,
+type ValidateCodeFormHandle = {
+ focus: () => void;
+ focusLastSelected: () => void;
+};
+
+type ValidateCodeFormError = {
+ validateCode?: TranslationPaths;
+};
+
+type BaseValidateCodeFormOnyxProps = {
+ /** The details about the account that the user is signing in with */
+ account: OnyxEntry;
+};
+type ValidateCodeFormProps = {
/** The contact method being valdiated */
- contactMethod: PropTypes.string.isRequired,
+ contactMethod: string;
/** If the magic code has been resent previously */
- hasMagicCodeBeenSent: PropTypes.bool.isRequired,
+ hasMagicCodeBeenSent: boolean;
/** Login list for the user that is signed in */
- loginList: PropTypes.shape({
- /** Value of partner name */
- partnerName: PropTypes.string,
-
- /** Phone/Email associated with user */
- partnerUserID: PropTypes.string,
-
- /** Date when login was validated */
- validatedDate: PropTypes.string,
+ loginList: LoginList;
- /** Field-specific server side errors keyed by microtime */
- errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
-
- /** Field-specific pending states for offline UI status */
- pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
- }).isRequired,
+ /** Specifies autocomplete hints for the system, so it can provide autofill */
+ autoComplete?: AutoCompleteVariant;
/** Forwarded inner ref */
- innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
-
- /* Onyx Props */
-
- /** The details about the account that the user is signing in with */
- account: PropTypes.shape({
- /** Whether or not a sign on form is loading (being submitted) */
- isLoading: PropTypes.bool,
- }),
-
- /** Specifies autocomplete hints for the system, so it can provide autofill */
- autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code']).isRequired,
+ innerRef?: ForwardedRef;
};
-const defaultProps = {
- account: {},
- innerRef: () => {},
-};
+type BaseValidateCodeFormProps = BaseValidateCodeFormOnyxProps & ValidateCodeFormProps;
-function BaseValidateCodeForm(props) {
+function BaseValidateCodeForm({account = {}, contactMethod, hasMagicCodeBeenSent, loginList, autoComplete = 'one-time-code', innerRef = () => {}}: BaseValidateCodeFormProps) {
+ const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const [formError, setFormError] = useState({});
+ const [formError, setFormError] = useState({});
const [validateCode, setValidateCode] = useState('');
- const loginData = props.loginList[props.contactMethod];
- const inputValidateCodeRef = useRef();
+ const loginData = loginList[contactMethod];
+ const inputValidateCodeRef = useRef(null);
const validateLoginError = ErrorUtils.getEarliestErrorField(loginData, 'validateLogin');
- const shouldDisableResendValidateCode = props.network.isOffline || props.account.isLoading;
- const focusTimeoutRef = useRef(null);
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing doesn't achieve the same result in this case
+ const shouldDisableResendValidateCode = !!isOffline || account?.isLoading;
+ const focusTimeoutRef = useRef(null);
- useImperativeHandle(props.innerRef, () => ({
+ useImperativeHandle(innerRef, () => ({
focus() {
- if (!inputValidateCodeRef.current) {
- return;
- }
- inputValidateCodeRef.current.focus();
+ inputValidateCodeRef.current?.focus();
},
focusLastSelected() {
if (!inputValidateCodeRef.current) {
@@ -98,7 +85,9 @@ function BaseValidateCodeForm(props) {
if (focusTimeoutRef.current) {
clearTimeout(focusTimeoutRef.current);
}
- focusTimeoutRef.current = setTimeout(inputValidateCodeRef.current.focusLastSelected, CONST.ANIMATED_TRANSITION);
+ focusTimeoutRef.current = setTimeout(() => {
+ inputValidateCodeRef.current?.focusLastSelected();
+ }, CONST.ANIMATED_TRANSITION);
},
}));
@@ -110,7 +99,9 @@ function BaseValidateCodeForm(props) {
if (focusTimeoutRef.current) {
clearTimeout(focusTimeoutRef.current);
}
- focusTimeoutRef.current = setTimeout(inputValidateCodeRef.current.focusLastSelected, CONST.ANIMATED_TRANSITION);
+ focusTimeoutRef.current = setTimeout(() => {
+ inputValidateCodeRef.current?.focusLastSelected();
+ }, CONST.ANIMATED_TRANSITION);
return () => {
if (!focusTimeoutRef.current) {
return;
@@ -125,41 +116,39 @@ function BaseValidateCodeForm(props) {
if (!validateLoginError) {
return;
}
- User.clearContactMethodErrors(props.contactMethod, 'validateLogin');
+ User.clearContactMethodErrors(contactMethod, 'validateLogin');
// contactMethod is not added as a dependency since it does not change between renders
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
- if (!props.hasMagicCodeBeenSent) {
+ if (!hasMagicCodeBeenSent) {
return;
}
- inputValidateCodeRef.current.clear();
- }, [props.hasMagicCodeBeenSent]);
+ inputValidateCodeRef.current?.clear();
+ }, [hasMagicCodeBeenSent]);
/**
* Request a validate code / magic code be sent to verify this contact method
*/
const resendValidateCode = () => {
- User.requestContactMethodValidateCode(props.contactMethod);
- inputValidateCodeRef.current.clear();
+ User.requestContactMethodValidateCode(contactMethod);
+ inputValidateCodeRef.current?.clear();
};
/**
* Handle text input and clear formError upon text change
- *
- * @param {String} text
*/
const onTextInput = useCallback(
- (text) => {
+ (text: string) => {
setValidateCode(text);
setFormError({});
if (validateLoginError) {
- User.clearContactMethodErrors(props.contactMethod, 'validateLogin');
+ User.clearContactMethodErrors(contactMethod, 'validateLogin');
}
},
- [validateLoginError, props.contactMethod],
+ [validateLoginError, contactMethod],
);
/**
@@ -177,28 +166,27 @@ function BaseValidateCodeForm(props) {
}
setFormError({});
- User.validateSecondaryLogin(props.contactMethod, validateCode);
- }, [validateCode, props.contactMethod]);
+ User.validateSecondaryLogin(contactMethod, validateCode);
+ }, [validateCode, contactMethod]);
return (
<>
User.clearContactMethodErrors(props.contactMethod, 'validateCodeSent')}
+ onClose={() => User.clearContactMethodErrors(contactMethod, 'validateCodeSent')}
>
- {props.translate('validateCodeForm.magicCodeNotReceived')}
+ {translate('validateCodeForm.magicCodeNotReceived')}
- {props.hasMagicCodeBeenSent && (
+ {hasMagicCodeBeenSent && (
)}
User.clearContactMethodErrors(props.contactMethod, 'validateLogin')}
+ onClose={() => User.clearContactMethodErrors(contactMethod, 'validateLogin')}
>
>
);
}
-BaseValidateCodeForm.propTypes = propTypes;
-BaseValidateCodeForm.defaultProps = defaultProps;
BaseValidateCodeForm.displayName = 'BaseValidateCodeForm';
-export default compose(
- withLocalize,
- withOnyx({
- account: {key: ONYXKEYS.ACCOUNT},
- }),
- withNetwork(),
-)(BaseValidateCodeForm);
+export type {ValidateCodeFormProps, ValidateCodeFormHandle};
+
+export default withOnyx({
+ account: {key: ONYXKEYS.ACCOUNT},
+})(BaseValidateCodeForm);
diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.tsx
similarity index 61%
rename from src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.js
rename to src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.tsx
index a193dc8d2eae..704405f93a2c 100644
--- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.js
+++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.android.tsx
@@ -1,7 +1,8 @@
import React, {forwardRef} from 'react';
import BaseValidateCodeForm from './BaseValidateCodeForm';
+import type {ValidateCodeFormHandle, ValidateCodeFormProps} from './BaseValidateCodeForm';
-const ValidateCodeForm = forwardRef((props, ref) => (
+const ValidateCodeForm = forwardRef((props, ref) => (
(
/>
));
-ValidateCodeForm.displayName = 'ValidateCodeForm';
-
export default ValidateCodeForm;
diff --git a/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.js b/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.tsx
similarity index 62%
rename from src/pages/settings/Profile/Contacts/ValidateCodeForm/index.js
rename to src/pages/settings/Profile/Contacts/ValidateCodeForm/index.tsx
index bb4e5ed36b47..453fc9c3f373 100644
--- a/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.js
+++ b/src/pages/settings/Profile/Contacts/ValidateCodeForm/index.tsx
@@ -1,7 +1,8 @@
import React, {forwardRef} from 'react';
import BaseValidateCodeForm from './BaseValidateCodeForm';
+import type {ValidateCodeFormHandle, ValidateCodeFormProps} from './BaseValidateCodeForm';
-const ValidateCodeForm = forwardRef((props, ref) => (
+const ValidateCodeForm = forwardRef((props, ref) => (
(
/>
));
-ValidateCodeForm.displayName = 'ValidateCodeForm';
-
export default ValidateCodeForm;
diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js
index 38d8b5f13adb..4ff59ad1f9a1 100644
--- a/src/pages/settings/Wallet/ExpensifyCardPage.js
+++ b/src/pages/settings/Wallet/ExpensifyCardPage.js
@@ -17,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import * as FormActions from '@libs/actions/FormActions';
import * as CardUtils from '@libs/CardUtils';
import * as CurrencyUtils from '@libs/CurrencyUtils';
-import FormUtils from '@libs/FormUtils';
import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils';
import {translatableTextPropTypes} from '@libs/Localize';
import Navigation from '@libs/Navigation/Navigation';
@@ -310,6 +309,6 @@ export default withOnyx({
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
},
draftValues: {
- key: FormUtils.getDraftKey(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM),
+ key: ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT,
},
})(ExpensifyCardPage);
diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx
index 43aa32a7d617..bea7789256c9 100644
--- a/src/pages/settings/Wallet/PaymentMethodList.tsx
+++ b/src/pages/settings/Wallet/PaymentMethodList.tsx
@@ -1,4 +1,5 @@
import {FlashList} from '@shopify/flash-list';
+import lodashSortBy from 'lodash/sortBy';
import type {ReactElement, Ref} from 'react';
import React, {useCallback, useMemo} from 'react';
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
@@ -190,20 +191,13 @@ function PaymentMethodList({
if (shouldShowAssignedCards) {
const assignedCards = Object.values(cardList ?? {})
// Filter by physical, active cards associated with a domain
- .filter((card) => !card.isVirtual && !!card.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0))
- .toSorted((card1, card2) => {
- const isExpensifyCard1 = CardUtils.isExpensifyCard(card1.cardID);
- const isExpensifyCard2 = CardUtils.isExpensifyCard(card2.cardID);
- if (isExpensifyCard1 === isExpensifyCard2) {
- return 0;
- }
-
- return isExpensifyCard1 ? -1 : 1;
- });
+ .filter((card) => !card.isVirtual && !!card.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0));
const numberPhysicalExpensifyCards = assignedCards.filter((card) => CardUtils.isExpensifyCard(card.cardID)).length;
- return assignedCards.map((card) => {
+ const assignedCardsSorted = lodashSortBy(assignedCards, (card) => !CardUtils.isExpensifyCard(card.cardID));
+
+ return assignedCardsSorted.map((card) => {
const isExpensifyCard = CardUtils.isExpensifyCard(card.cardID);
const icon = getBankIcon({bankName: card.bank as BankName, isCard: true, styles});
diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js
deleted file mode 100644
index 62b96943453c..000000000000
--- a/src/pages/workspace/WorkspaceMembersPage.js
+++ /dev/null
@@ -1,537 +0,0 @@
-import {useIsFocused} from '@react-navigation/native';
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import {InteractionManager, View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
-import Button from '@components/Button';
-import ConfirmModal from '@components/ConfirmModal';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import * as Expensicons from '@components/Icon/Expensicons';
-import * as Illustrations from '@components/Icon/Illustrations';
-import MessagesRow from '@components/MessagesRow';
-import networkPropTypes from '@components/networkPropTypes';
-import {withNetwork} from '@components/OnyxProvider';
-import ScreenWrapper from '@components/ScreenWrapper';
-import SelectionList from '@components/SelectionList';
-import UserListItem from '@components/SelectionList/UserListItem';
-import Text from '@components/Text';
-import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
-import usePrevious from '@hooks/usePrevious';
-import useThemeStyles from '@hooks/useThemeStyles';
-import useWindowDimensions from '@hooks/useWindowDimensions';
-import compose from '@libs/compose';
-import * as DeviceCapabilities from '@libs/DeviceCapabilities';
-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 personalDetailsPropType from '@pages/personalDetailsPropType';
-import * as Policy from '@userActions/Policy';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import SearchInputManager from './SearchInputManager';
-import {policyDefaultProps, policyPropTypes} from './withPolicy';
-import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
-
-const propTypes = {
- /** All personal details asssociated with user */
- personalDetails: PropTypes.objectOf(personalDetailsPropType),
-
- /** URL Route params */
- route: PropTypes.shape({
- /** Params from the URL path */
- params: PropTypes.shape({
- /** policyID passed via route: /workspace/:policyID/members */
- policyID: PropTypes.string,
- }),
- }).isRequired,
-
- /** Session info for the currently logged in user. */
- session: PropTypes.shape({
- /** Currently logged in user accountID */
- accountID: PropTypes.number,
- }),
-
- isLoadingReportData: PropTypes.bool,
- ...policyPropTypes,
- ...withLocalizePropTypes,
- ...windowDimensionsPropTypes,
- ...withCurrentUserPersonalDetailsPropTypes,
- network: networkPropTypes.isRequired,
-};
-
-const defaultProps = {
- personalDetails: {},
- session: {
- accountID: 0,
- },
- isLoadingReportData: true,
- ...policyDefaultProps,
- ...withCurrentUserPersonalDetailsDefaultProps,
-};
-
-function WorkspaceMembersPage(props) {
- const styles = useThemeStyles();
- const [selectedEmployees, setSelectedEmployees] = useState([]);
- const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false);
- const [errors, setErrors] = useState({});
- const [searchValue, setSearchValue] = useState('');
- const prevIsOffline = usePrevious(props.network.isOffline);
- const accountIDs = useMemo(() => _.map(_.keys(props.policyMembers), (accountID) => Number(accountID)), [props.policyMembers]);
- const prevAccountIDs = usePrevious(accountIDs);
- const textInputRef = useRef(null);
- const isOfflineAndNoMemberDataAvailable = _.isEmpty(props.policyMembers) && props.network.isOffline;
- const prevPersonalDetails = usePrevious(props.personalDetails);
- const {isSmallScreenWidth} = useWindowDimensions();
-
- const isFocusedScreen = useIsFocused();
-
- useEffect(() => {
- setSearchValue(SearchInputManager.searchInput);
- }, [isFocusedScreen]);
-
- useEffect(() => () => (SearchInputManager.searchInput = ''), []);
-
- /**
- * Get filtered personalDetails list with current policyMembers
- * @param {Object} policyMembers
- * @param {Object} personalDetails
- * @returns {Object}
- */
- const filterPersonalDetails = (policyMembers, personalDetails) =>
- _.reduce(
- _.keys(policyMembers),
- (result, key) => {
- if (personalDetails[key]) {
- return {
- ...result,
- [key]: personalDetails[key],
- };
- }
- return result;
- },
- {},
- );
-
- /**
- * Get members for the current workspace
- */
- const getWorkspaceMembers = useCallback(() => {
- Policy.openWorkspaceMembersPage(props.route.params.policyID, _.keys(PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails)));
- }, [props.route.params.policyID, props.policyMembers, props.personalDetails]);
-
- /**
- * Check if the current selection includes members that cannot be removed
- */
- const validateSelection = useCallback(() => {
- const newErrors = {};
- const ownerAccountID = _.first(PersonalDetailsUtils.getAccountIDsByLogins(props.policy.owner ? [props.policy.owner] : []));
- _.each(selectedEmployees, (member) => {
- if (member !== ownerAccountID && member !== props.session.accountID) {
- return;
- }
- newErrors[member] = props.translate('workspace.people.error.cannotRemove');
- });
- setErrors(newErrors);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [selectedEmployees, props.policy.owner, props.session.accountID]);
-
- useEffect(() => {
- getWorkspaceMembers();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- useEffect(() => {
- validateSelection();
- }, [props.preferredLocale, validateSelection]);
-
- useEffect(() => {
- if (removeMembersConfirmModalVisible && !_.isEqual(accountIDs, prevAccountIDs)) {
- setRemoveMembersConfirmModalVisible(false);
- }
- setSelectedEmployees((prevSelected) => {
- // Filter all personal details in order to use the elements needed for the current workspace
- const currentPersonalDetails = filterPersonalDetails(props.policyMembers, props.personalDetails);
- // We need to filter the previous selected employees by the new personal details, since unknown/new user id's change when transitioning from offline to online
- const prevSelectedElements = _.map(prevSelected, (id) => {
- const prevItem = lodashGet(prevPersonalDetails, id);
- const res = _.find(_.values(currentPersonalDetails), (item) => lodashGet(prevItem, 'login') === lodashGet(item, 'login'));
- return lodashGet(res, 'accountID', id);
- });
- return _.intersection(prevSelectedElements, _.values(PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails)));
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.policyMembers]);
-
- useEffect(() => {
- const isReconnecting = prevIsOffline && !props.network.isOffline;
- if (!isReconnecting) {
- return;
- }
- getWorkspaceMembers();
- }, [props.network.isOffline, prevIsOffline, getWorkspaceMembers]);
-
- /**
- * Open the modal to invite a user
- */
- const inviteUser = () => {
- setSearchValue('');
- Navigation.navigate(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID));
- };
-
- /**
- * Remove selected users from the workspace
- * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
- */
- const removeUsers = () => {
- if (!_.isEmpty(errors)) {
- return;
- }
-
- // Remove the admin from the list
- const accountIDsToRemove = _.without(selectedEmployees, props.session.accountID);
-
- Policy.removeMembers(accountIDsToRemove, props.route.params.policyID);
- setSelectedEmployees([]);
- setRemoveMembersConfirmModalVisible(false);
- };
-
- /**
- * Show the modal to confirm removal of the selected members
- */
- const askForConfirmationToRemove = () => {
- if (!_.isEmpty(errors)) {
- return;
- }
- setRemoveMembersConfirmModalVisible(true);
- };
-
- /**
- * Add or remove all users passed from the selectedEmployees list
- * @param {Object} memberList
- */
- const toggleAllUsers = (memberList) => {
- const enabledAccounts = _.filter(memberList, (member) => !member.isDisabled);
- const everyoneSelected = _.every(enabledAccounts, (member) => _.contains(selectedEmployees, member.accountID));
-
- if (everyoneSelected) {
- setSelectedEmployees([]);
- } else {
- const everyAccountId = _.map(enabledAccounts, (member) => member.accountID);
- setSelectedEmployees(everyAccountId);
- }
-
- validateSelection();
- };
-
- /**
- * Add user from the selectedEmployees list
- *
- * @param {String} login
- */
- const addUser = useCallback(
- (accountID) => {
- setSelectedEmployees((prevSelected) => [...prevSelected, accountID]);
- validateSelection();
- },
- [validateSelection],
- );
-
- /**
- * Remove user from the selectedEmployees list
- *
- * @param {String} login
- */
- const removeUser = useCallback(
- (accountID) => {
- setSelectedEmployees((prevSelected) => _.without(prevSelected, accountID));
- validateSelection();
- },
- [validateSelection],
- );
-
- /**
- * Toggle user from the selectedEmployees list
- *
- * @param {String} accountID
- * @param {String} pendingAction
- *
- */
- const toggleUser = useCallback(
- (accountID, pendingAction) => {
- if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
- return;
- }
-
- // Add or remove the user if the checkbox is enabled
- if (_.contains(selectedEmployees, accountID)) {
- removeUser(accountID);
- } else {
- addUser(accountID);
- }
- },
- [selectedEmployees, addUser, removeUser],
- );
-
- /**
- * Dismisses the errors on one item
- *
- * @param {Object} item
- */
- const dismissError = useCallback(
- (item) => {
- if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
- Policy.clearDeleteMemberError(props.route.params.policyID, item.accountID);
- } else {
- Policy.clearAddMemberError(props.route.params.policyID, item.accountID);
- }
- },
- [props.route.params.policyID],
- );
-
- /**
- * Check if the policy member is deleted from the workspace
- *
- * @param {Object} policyMember
- * @returns {Boolean}
- */
- const isDeletedPolicyMember = (policyMember) => !props.network.isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && _.isEmpty(policyMember.errors);
- const policyOwner = lodashGet(props.policy, 'owner');
- const currentUserLogin = lodashGet(props.currentUserPersonalDetails, 'login');
- const policyID = lodashGet(props.route, 'params.policyID');
- const invitedPrimaryToSecondaryLogins = _.invert(props.policy.primaryLoginsInvited);
-
- const getMemberOptions = () => {
- let result = [];
-
- _.each(props.policyMembers, (policyMember, accountIDKey) => {
- const accountID = Number(accountIDKey);
- if (isDeletedPolicyMember(policyMember)) {
- return;
- }
-
- const details = props.personalDetails[accountID];
-
- if (!details) {
- Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`);
- return;
- }
-
- // If search value is provided, filter out members that don't match the search value
- if (searchValue.trim()) {
- let memberDetails = '';
- if (details.login) {
- memberDetails += ` ${details.login.toLowerCase()}`;
- }
- if (details.firstName) {
- memberDetails += ` ${details.firstName.toLowerCase()}`;
- }
- if (details.lastName) {
- memberDetails += ` ${details.lastName.toLowerCase()}`;
- }
- if (details.displayName) {
- memberDetails += ` ${details.displayName.toLowerCase()}`;
- }
- if (details.phoneNumber) {
- memberDetails += ` ${details.phoneNumber.toLowerCase()}`;
- }
-
- if (!OptionsListUtils.isSearchStringMatch(searchValue.trim(), memberDetails)) {
- return;
- }
- }
-
- // If this policy is owned by Expensify then show all support (expensify.com or team.expensify.com) emails
- // We don't want to show guides as policy members unless the user is a guide. Some customers get confused when they
- // see random people added to their policy, but guides having access to the policies help set them up.
- if (PolicyUtils.isExpensifyTeam(details.login || details.displayName)) {
- if (policyOwner && currentUserLogin && !PolicyUtils.isExpensifyTeam(policyOwner) && !PolicyUtils.isExpensifyTeam(currentUserLogin)) {
- return;
- }
- }
-
- const isAdmin = props.session.email === details.login || policyMember.role === CONST.POLICY.ROLE.ADMIN;
-
- result.push({
- keyForList: accountIDKey,
- accountID,
- isSelected: _.contains(selectedEmployees, accountID),
- isDisabled:
- accountID === props.session.accountID ||
- details.login === props.policy.owner ||
- policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ||
- !_.isEmpty(policyMember.errors),
- text: props.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)),
- alternateText: props.formatPhoneNumber(details.login),
- rightElement: isAdmin ? (
-
- {props.translate('common.admin')}
-
- ) : null,
- icons: [
- {
- source: UserUtils.getAvatar(details.avatar, accountID),
- name: props.formatPhoneNumber(details.login),
- type: CONST.ICON_TYPE_AVATAR,
- id: accountID,
- },
- ],
- errors: policyMember.errors,
- pendingAction: policyMember.pendingAction,
-
- // Note which secondary login was used to invite this primary login
- invitedSecondaryLogin: invitedPrimaryToSecondaryLogins[details.login] || '',
- });
- });
-
- result = _.sortBy(result, (value) => value.text.toLowerCase());
-
- return result;
- };
- const data = getMemberOptions();
-
- const getHeaderMessage = () => {
- if (isOfflineAndNoMemberDataAvailable) {
- return props.translate('workspace.common.mustBeOnlineToViewMembers');
- }
- return searchValue.trim() && !data.length ? props.translate('workspace.common.memberNotFound') : '';
- };
-
- const getHeaderContent = () => {
- if (_.isEmpty(invitedPrimaryToSecondaryLogins)) {
- return null;
- }
- return (
- Policy.dismissAddedWithPrimaryLoginMessages(policyID)}
- />
- );
- };
-
- const getHeaderButtons = () => (
-
-
-
-
- );
-
- return (
-
- Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
- >
- {
- setSearchValue('');
- Navigation.goBack();
- }}
- shouldShowBackButton={isSmallScreenWidth}
- guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
- >
- {!isSmallScreenWidth && getHeaderButtons()}
-
- {isSmallScreenWidth && {getHeaderButtons()}}
- setRemoveMembersConfirmModalVisible(false)}
- prompt={props.translate('workspace.people.removeMembersPrompt')}
- confirmText={props.translate('common.remove')}
- cancelText={props.translate('common.cancel')}
- onModalHide={() =>
- InteractionManager.runAfterInteractions(() => {
- if (!textInputRef.current) {
- return;
- }
- textInputRef.current.focus();
- })
- }
- />
-
- {
- SearchInputManager.searchInput = value;
- setSearchValue(value);
- }}
- disableKeyboardShortcuts={removeMembersConfirmModalVisible}
- headerMessage={getHeaderMessage()}
- headerContent={getHeaderContent()}
- onSelectRow={(item) => toggleUser(item.accountID)}
- onSelectAll={() => toggleAllUsers(data)}
- onDismissError={dismissError}
- showLoadingPlaceholder={!isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(props.personalDetails) || _.isEmpty(props.policyMembers))}
- showScrollIndicator
- shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
- inputRef={textInputRef}
- />
-
-
-
- );
-}
-
-WorkspaceMembersPage.propTypes = propTypes;
-WorkspaceMembersPage.defaultProps = defaultProps;
-WorkspaceMembersPage.displayName = 'WorkspaceMembersPage';
-
-export default compose(
- withLocalize,
- withWindowDimensions,
- withPolicyAndFullscreenLoading,
- withNetwork(),
- withOnyx({
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- isLoadingReportData: {
- key: ONYXKEYS.IS_LOADING_REPORT_DATA,
- },
- }),
- withCurrentUserPersonalDetails,
-)(WorkspaceMembersPage);
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
new file mode 100644
index 000000000000..f28a435d26d9
--- /dev/null
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -0,0 +1,487 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import lodashIsEqual from 'lodash/isEqual';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import type {TextInput} from 'react-native';
+import {InteractionManager, View} 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 Button from '@components/Button';
+import ConfirmModal from '@components/ConfirmModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import * as Illustrations from '@components/Icon/Illustrations';
+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 Text from '@components/Text';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import usePrevious from '@hooks/usePrevious';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+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 * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as UserUtils from '@libs/UserUtils';
+import * as Policy from '@userActions/Policy';
+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 {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
+import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
+
+type WorkspaceMembersPageOnyxProps = {
+ /** Personal details of all users */
+ personalDetails: OnyxEntry;
+ /** Session info for the currently logged in user. */
+ session: OnyxEntry;
+};
+
+type WorkspaceMembersPageProps = WithPolicyAndFullscreenLoadingProps &
+ WithCurrentUserPersonalDetailsProps &
+ WorkspaceMembersPageOnyxProps &
+ StackScreenProps;
+
+/**
+ * Inverts an object, equivalent of _.invert
+ */
+function invertObject(object: Record): Record {
+ const invertedEntries = Object.entries(object).map(([key, value]) => [value, key]);
+ const inverted: Record = Object.fromEntries(invertedEntries);
+ return inverted;
+}
+
+type MemberOption = Omit & {accountID: number};
+
+function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, session, currentUserPersonalDetails, isLoadingReportData = true}: WorkspaceMembersPageProps) {
+ const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+ const [selectedEmployees, setSelectedEmployees] = useState([]);
+ const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false);
+ const [errors, setErrors] = useState({});
+ const {isOffline} = useNetwork();
+ const prevIsOffline = usePrevious(isOffline);
+ const accountIDs = useMemo(() => Object.keys(policyMembers ?? {}).map((accountID) => Number(accountID)), [policyMembers]);
+ const prevAccountIDs = usePrevious(accountIDs);
+ const textInputRef = useRef(null);
+ const isOfflineAndNoMemberDataAvailable = isEmptyObject(policyMembers) && isOffline;
+ const prevPersonalDetails = usePrevious(personalDetails);
+ const {translate, formatPhoneNumber, preferredLocale} = useLocalize();
+ const {isSmallScreenWidth} = useWindowDimensions();
+
+ /**
+ * Get filtered personalDetails list with current policyMembers
+ */
+ const filterPersonalDetails = (members: OnyxEntry, details: OnyxEntry): PersonalDetailsList =>
+ Object.keys(members ?? {}).reduce((result, key) => {
+ if (details?.[key]) {
+ return {
+ ...result,
+ [key]: details[key],
+ };
+ }
+ return result;
+ }, {});
+
+ /**
+ * Get members for the current workspace
+ */
+ const getWorkspaceMembers = useCallback(() => {
+ Policy.openWorkspaceMembersPage(route.params.policyID, Object.keys(PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetails)));
+ }, [route.params.policyID, policyMembers, personalDetails]);
+
+ /**
+ * Check if the current selection includes members that cannot be removed
+ */
+ const validateSelection = useCallback(() => {
+ const newErrors: Errors = {};
+ const ownerAccountID = PersonalDetailsUtils.getAccountIDsByLogins(policy?.owner ? [policy.owner] : [])[0];
+ selectedEmployees.forEach((member) => {
+ if (member !== ownerAccountID && member !== session?.accountID) {
+ return;
+ }
+ newErrors[member] = translate('workspace.people.error.cannotRemove');
+ });
+ setErrors(newErrors);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedEmployees, policy?.owner, session?.accountID]);
+
+ useEffect(() => {
+ getWorkspaceMembers();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ validateSelection();
+ }, [preferredLocale, validateSelection]);
+
+ useEffect(() => {
+ if (removeMembersConfirmModalVisible && !lodashIsEqual(accountIDs, prevAccountIDs)) {
+ setRemoveMembersConfirmModalVisible(false);
+ }
+ setSelectedEmployees((prevSelected) => {
+ // Filter all personal details in order to use the elements needed for the current workspace
+ const currentPersonalDetails = filterPersonalDetails(policyMembers, personalDetails);
+ // We need to filter the previous selected employees by the new personal details, since unknown/new user id's change when transitioning from offline to online
+ const prevSelectedElements = prevSelected.map((id) => {
+ const prevItem = prevPersonalDetails?.id;
+ const res = Object.values(currentPersonalDetails).find((item) => prevItem?.login === item?.login);
+ return res?.accountID ?? id;
+ });
+ // This is an equivalent of the lodash intersection function. The reduce method below is used to filter the items that exist in both arrays.
+ return [prevSelectedElements, Object.values(PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetails))].reduce((prev, members) =>
+ prev.filter((item) => members.includes(item)),
+ );
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [policyMembers]);
+
+ useEffect(() => {
+ const isReconnecting = prevIsOffline && !isOffline;
+ if (!isReconnecting) {
+ return;
+ }
+ getWorkspaceMembers();
+ }, [isOffline, prevIsOffline, getWorkspaceMembers]);
+
+ /**
+ * Open the modal to invite a user
+ */
+ const inviteUser = () => {
+ Navigation.navigate(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID));
+ };
+
+ /**
+ * Remove selected users from the workspace
+ * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details
+ */
+ const removeUsers = () => {
+ if (!isEmptyObject(errors)) {
+ return;
+ }
+
+ // Remove the admin from the list
+ const accountIDsToRemove = session?.accountID ? selectedEmployees.filter((id) => id !== session.accountID) : selectedEmployees;
+
+ Policy.removeMembers(accountIDsToRemove, route.params.policyID);
+ setSelectedEmployees([]);
+ setRemoveMembersConfirmModalVisible(false);
+ };
+
+ /**
+ * Show the modal to confirm removal of the selected members
+ */
+ const askForConfirmationToRemove = () => {
+ if (!isEmptyObject(errors)) {
+ return;
+ }
+ setRemoveMembersConfirmModalVisible(true);
+ };
+
+ /**
+ * Add or remove all users passed from the selectedEmployees list
+ */
+ const toggleAllUsers = (memberList: MemberOption[]) => {
+ const enabledAccounts = memberList.filter((member) => !member.isDisabled);
+ const everyoneSelected = enabledAccounts.every((member) => selectedEmployees.includes(member.accountID));
+
+ if (everyoneSelected) {
+ setSelectedEmployees([]);
+ } else {
+ const everyAccountId = enabledAccounts.map((member) => member.accountID);
+ setSelectedEmployees(everyAccountId);
+ }
+
+ validateSelection();
+ };
+
+ /**
+ * Add user from the selectedEmployees list
+ */
+ const addUser = useCallback(
+ (accountID: number) => {
+ setSelectedEmployees((prevSelected) => [...prevSelected, accountID]);
+ validateSelection();
+ },
+ [validateSelection],
+ );
+
+ /**
+ * Remove user from the selectedEmployees list
+ */
+ const removeUser = useCallback(
+ (accountID: number) => {
+ setSelectedEmployees((prevSelected) => prevSelected.filter((id) => id !== accountID));
+ validateSelection();
+ },
+ [validateSelection],
+ );
+
+ /**
+ * Toggle user from the selectedEmployees list
+ */
+ const toggleUser = useCallback(
+ (accountID: number, pendingAction?: PendingAction) => {
+ if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ return;
+ }
+
+ // Add or remove the user if the checkbox is enabled
+ if (selectedEmployees.includes(accountID)) {
+ removeUser(accountID);
+ } else {
+ addUser(accountID);
+ }
+ },
+ [selectedEmployees, addUser, removeUser],
+ );
+
+ /**
+ * Dismisses the errors on one item
+ */
+ const dismissError = useCallback(
+ (item: MemberOption) => {
+ if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ Policy.clearDeleteMemberError(route.params.policyID, item.accountID);
+ } else {
+ Policy.clearAddMemberError(route.params.policyID, item.accountID);
+ }
+ },
+ [route.params.policyID],
+ );
+
+ /**
+ * 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 policyOwner = policy?.owner;
+ const currentUserLogin = currentUserPersonalDetails.login;
+ const policyID = route.params.policyID;
+
+ const invitedPrimaryToSecondaryLogins = invertObject(policy?.primaryLoginsInvited ?? {});
+
+ const getUsers = (): MemberOption[] => {
+ let result: MemberOption[] = [];
+
+ Object.entries(policyMembers ?? {}).forEach(([accountIDKey, policyMember]) => {
+ const accountID = Number(accountIDKey);
+ if (isDeletedPolicyMember(policyMember)) {
+ return;
+ }
+
+ const details = personalDetails?.[accountID];
+
+ if (!details) {
+ Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`);
+ return;
+ }
+
+ // If this policy is owned by Expensify then show all support (expensify.com or team.expensify.com) emails
+ // We don't want to show guides as policy members unless the user is a guide. Some customers get confused when they
+ // see random people added to their policy, but guides having access to the policies help set them up.
+ if (PolicyUtils.isExpensifyTeam(details?.login ?? details?.displayName)) {
+ if (policyOwner && currentUserLogin && !PolicyUtils.isExpensifyTeam(policyOwner) && !PolicyUtils.isExpensifyTeam(currentUserLogin)) {
+ return;
+ }
+ }
+
+ const isOwner = policy?.owner === details.login;
+ const isAdmin = session?.email === details.login || policyMember.role === CONST.POLICY.ROLE.ADMIN;
+
+ let roleBadge = null;
+ if (isOwner || isAdmin) {
+ roleBadge = (
+
+ );
+ }
+
+ result.push({
+ keyForList: accountIDKey,
+ accountID,
+ isSelected: selectedEmployees.includes(accountID),
+ isDisabled:
+ accountID === session?.accountID ||
+ details.login === policy?.owner ||
+ 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,
+
+ // Note which secondary login was used to invite this primary login
+ invitedSecondaryLogin: details?.login ? invitedPrimaryToSecondaryLogins[details.login] ?? '' : '',
+ });
+ });
+
+ result = result.sort((a, b) => a.text.toLowerCase().localeCompare(b.text.toLowerCase()));
+
+ return result;
+ };
+ const data = getUsers();
+
+ const getHeaderMessage = () => {
+ if (isOfflineAndNoMemberDataAvailable) {
+ return translate('workspace.common.mustBeOnlineToViewMembers');
+ }
+ return !data.length ? translate('workspace.common.memberNotFound') : '';
+ };
+
+ const getHeaderContent = () => (
+ <>
+ {translate('workspace.people.membersListTitle')}
+ {!isEmptyObject(invitedPrimaryToSecondaryLogins) && (
+ Policy.dismissAddedWithPrimaryLoginMessages(policyID)}
+ />
+ )}
+ >
+ );
+
+ const getCustomListHeader = () => (
+
+
+ {translate('common.member')}
+
+
+ {translate('common.role')}
+
+
+ );
+
+ const getHeaderButtons = () => (
+
+
+
+
+ );
+
+ return (
+
+ Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
+ >
+ {
+ Navigation.goBack();
+ }}
+ shouldShowBackButton={isSmallScreenWidth}
+ guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
+ >
+ {!isSmallScreenWidth && getHeaderButtons()}
+
+ {isSmallScreenWidth && {getHeaderButtons()}}
+ setRemoveMembersConfirmModalVisible(false)}
+ prompt={translate('workspace.people.removeMembersPrompt')}
+ confirmText={translate('common.remove')}
+ cancelText={translate('common.cancel')}
+ onModalHide={() => {
+ InteractionManager.runAfterInteractions(() => {
+ if (!textInputRef.current) {
+ return;
+ }
+ textInputRef.current.focus();
+ });
+ }}
+ />
+
+ toggleUser(item.accountID)}
+ onSelectAll={() => toggleAllUsers(data)}
+ onDismissError={dismissError}
+ showLoadingPlaceholder={!isOfflineAndNoMemberDataAvailable && (!OptionsListUtils.isPersonalDetailsReady(personalDetails) || isEmptyObject(policyMembers))}
+ showScrollIndicator
+ shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
+ ref={textInputRef}
+ customListHeader={getCustomListHeader()}
+ listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
+ />
+
+
+
+ );
+}
+
+WorkspaceMembersPage.displayName = 'WorkspaceMembersPage';
+
+export default withCurrentUserPersonalDetails(
+ withPolicyAndFullscreenLoading(
+ withOnyx({
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ })(WorkspaceMembersPage),
+ ),
+);
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.tsx
similarity index 71%
rename from src/pages/workspace/WorkspaceNewRoomPage.js
rename to src/pages/workspace/WorkspaceNewRoomPage.tsx
index 36f874e8919d..73107d7e3eba 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.js
+++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx
@@ -1,12 +1,14 @@
-import PropTypes from 'prop-types';
+import {useIsFocused} from '@react-navigation/core';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
import BlockingView from '@components/BlockingViews/BlockingView';
import Button from '@components/Button';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
+import type {FormOnyxValues} from '@components/Form/types';
import * as Illustrations from '@components/Icon/Illustrations';
import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
import OfflineIndicator from '@components/OfflineIndicator';
@@ -14,16 +16,13 @@ import RoomNameInput from '@components/RoomNameInput';
import ScreenWrapper from '@components/ScreenWrapper';
import TextInput from '@components/TextInput';
import ValuePicker from '@components/ValuePicker';
-import withNavigationFocus from '@components/withNavigationFocus';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import compose from '@libs/compose';
import * as ErrorUtils from '@libs/ErrorUtils';
-import {translatableTextPropTypes} from '@libs/Localize';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -33,93 +32,57 @@ import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {NewRoomForm} from '@src/types/form/NewRoomForm';
import INPUT_IDS from '@src/types/form/NewRoomForm';
+import type {Account, Policy, Report as ReportType, Session} from '@src/types/onyx';
+import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
-const propTypes = {
- /** All reports shared with the user */
- reports: PropTypes.shape({
- /** The report name */
- reportName: PropTypes.string,
-
- /** The report type */
- type: PropTypes.string,
-
- /** ID of the policy */
- policyID: PropTypes.string,
- }),
-
+type WorkspaceNewRoomPageOnyxProps = {
/** The list of policies the user has access to. */
- policies: PropTypes.objectOf(
- PropTypes.shape({
- /** The policy type */
- type: PropTypes.oneOf(_.values(CONST.POLICY.TYPE)),
+ policies: OnyxCollection;
- /** The name of the policy */
- name: PropTypes.string,
-
- /** The ID of the policy */
- id: PropTypes.string,
- }),
- ),
-
- /** Whether navigation is focused */
- isFocused: PropTypes.bool.isRequired,
+ /** All reports shared with the user */
+ reports: OnyxCollection;
/** Form state for NEW_ROOM_FORM */
- formState: PropTypes.shape({
- /** Loading state for the form */
- isLoading: PropTypes.bool,
-
- /** Field errors in the form */
- errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
- }),
+ formState: OnyxEntry;
/** Session details for the user */
- session: PropTypes.shape({
- /** accountID of current user */
- accountID: PropTypes.number,
- }),
+ session: OnyxEntry;
/** policyID for main workspace */
- activePolicyID: PropTypes.string,
-};
-const defaultProps = {
- reports: {},
- policies: {},
- formState: {
- isLoading: false,
- errorFields: {},
- },
- session: {
- accountID: 0,
- },
- activePolicyID: null,
+ activePolicyID: OnyxEntry['activePolicyID']>;
};
-function WorkspaceNewRoomPage(props) {
+type WorkspaceNewRoomPageProps = WorkspaceNewRoomPageOnyxProps;
+
+function WorkspaceNewRoomPage({policies, reports, formState, session, activePolicyID}: WorkspaceNewRoomPageProps) {
const styles = useThemeStyles();
+ const isFocused = useIsFocused();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const {isSmallScreenWidth} = useWindowDimensions();
- const [visibility, setVisibility] = useState(CONST.REPORT.VISIBILITY.RESTRICTED);
- const [writeCapability, setWriteCapability] = useState(CONST.REPORT.WRITE_CAPABILITIES.ALL);
- const wasLoading = usePrevious(props.formState.isLoading);
+ const [visibility, setVisibility] = useState>(CONST.REPORT.VISIBILITY.RESTRICTED);
+ const [writeCapability, setWriteCapability] = useState>(CONST.REPORT.WRITE_CAPABILITIES.ALL);
+ const wasLoading = usePrevious(!!formState?.isLoading);
const visibilityDescription = useMemo(() => translate(`newRoomPage.${visibility}Description`), [translate, visibility]);
+ const {isLoading = false, errorFields = {}} = formState ?? {};
const workspaceOptions = useMemo(
() =>
- _.map(
- _.filter(PolicyUtils.getActivePolicies(props.policies), (policy) => policy.type !== CONST.POLICY.TYPE.PERSONAL),
- (policy) => ({
+ PolicyUtils.getActivePolicies(policies)
+ ?.filter((policy) => policy.type !== CONST.POLICY.TYPE.PERSONAL)
+ .map((policy) => ({
label: policy.name,
value: policy.id,
- }),
- ).sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())),
- [props.policies],
+ }))
+ .sort((a, b) => a.label.toLowerCase().localeCompare(b.label.toLowerCase())) ?? [],
+ [policies],
);
- const [policyID, setPolicyID] = useState(() => {
- if (_.some(workspaceOptions, (option) => option.value === props.activePolicyID)) {
- return props.activePolicyID;
+ const [policyID, setPolicyID] = useState(() => {
+ if (!!activePolicyID && workspaceOptions.some((option) => option.value === activePolicyID)) {
+ return activePolicyID;
}
return '';
});
@@ -128,16 +91,16 @@ function WorkspaceNewRoomPage(props) {
return false;
}
- return ReportUtils.isPolicyAdmin(policyID, props.policies);
- }, [policyID, props.policies]);
- const [newRoomReportID, setNewRoomReportID] = useState(undefined);
+ return ReportUtils.isPolicyAdmin(policyID, policies);
+ }, [policyID, policies]);
+ const [newRoomReportID, setNewRoomReportID] = useState();
/**
- * @param {Object} values - form input values passed by the Form component
+ * @param values - form input values passed by the Form component
*/
- const submit = (values) => {
- const participants = [props.session.accountID];
- const parsedDescription = ReportUtils.getParsedComment(values.reportDescription);
+ const submit = (values: FormOnyxValues) => {
+ const participants = [session?.accountID ?? 0];
+ const parsedDescription = ReportUtils.getParsedComment(values.reportDescription ?? '');
const policyReport = ReportUtils.buildOptimisticChatReport(
participants,
values.roomName,
@@ -163,25 +126,25 @@ function WorkspaceNewRoomPage(props) {
useEffect(() => {
if (policyID) {
- if (!_.some(workspaceOptions, (opt) => opt.value === policyID)) {
+ if (!workspaceOptions.some((opt) => opt.value === policyID)) {
setPolicyID('');
}
return;
}
- if (_.some(workspaceOptions, (opt) => opt.value === props.activePolicyID)) {
- setPolicyID(props.activePolicyID);
+ if (!!activePolicyID && workspaceOptions.some((opt) => opt.value === activePolicyID)) {
+ setPolicyID(activePolicyID);
} else {
setPolicyID('');
}
- }, [props.activePolicyID, policyID, workspaceOptions]);
+ }, [activePolicyID, policyID, workspaceOptions]);
useEffect(() => {
- if (!(((wasLoading && !props.formState.isLoading) || (isOffline && props.formState.isLoading)) && _.isEmpty(props.formState.errorFields))) {
+ if (!(((wasLoading && !isLoading) || (isOffline && isLoading)) && isEmptyObject(errorFields))) {
return;
}
Navigation.dismissModal(newRoomReportID);
// eslint-disable-next-line react-hooks/exhaustive-deps -- we just want this to update on changing the form State
- }, [props.formState]);
+ }, [isLoading, errorFields]);
useEffect(() => {
if (isPolicyAdmin) {
@@ -192,12 +155,12 @@ function WorkspaceNewRoomPage(props) {
}, [isPolicyAdmin]);
/**
- * @param {Object} values - form input values passed by the Form component
- * @returns {Boolean}
+ * @param values - form input values passed by the Form component
+ * @returns an object containing validation errors, if any were found during validation
*/
const validate = useCallback(
- (values) => {
- const errors = {};
+ (values: FormOnyxValues): OnyxCommon.Errors => {
+ const errors: {policyID?: string; roomName?: string} = {};
if (!values.roomName || values.roomName === CONST.POLICY.ROOM_PREFIX) {
// We error if the user doesn't enter a room name or left blank
@@ -208,7 +171,7 @@ function WorkspaceNewRoomPage(props) {
} else if (ValidationUtils.isReservedRoomName(values.roomName)) {
// Certain names are reserved for default rooms and should not be used for policy rooms.
ErrorUtils.addErrorMessage(errors, 'roomName', ['newRoomPage.roomNameReservedError', {reservedName: values.roomName}]);
- } else if (ValidationUtils.isExistingRoomName(values.roomName, props.reports, values.policyID)) {
+ } else if (ValidationUtils.isExistingRoomName(values.roomName, reports, values.policyID ?? '')) {
// Certain names are reserved for default rooms and should not be used for policy rooms.
ErrorUtils.addErrorMessage(errors, 'roomName', 'newRoomPage.roomAlreadyExistsError');
} else if (values.roomName.length > CONST.TITLE_CHARACTER_LIMIT) {
@@ -221,12 +184,12 @@ function WorkspaceNewRoomPage(props) {
return errors;
},
- [props.reports],
+ [reports],
);
const writeCapabilityOptions = useMemo(
() =>
- _.map(CONST.REPORT.WRITE_CAPABILITIES, (value) => ({
+ Object.values(CONST.REPORT.WRITE_CAPABILITIES).map((value) => ({
value,
label: translate(`writeCapabilityPage.writeCapability.${value}`),
})),
@@ -235,14 +198,13 @@ function WorkspaceNewRoomPage(props) {
const visibilityOptions = useMemo(
() =>
- _.map(
- _.filter(_.values(CONST.REPORT.VISIBILITY), (visibilityOption) => visibilityOption !== CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE),
- (visibilityOption) => ({
+ Object.values(CONST.REPORT.VISIBILITY)
+ .filter((visibilityOption) => visibilityOption !== CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE)
+ .map((visibilityOption) => ({
label: translate(`newRoomPage.visibilityOptions.${visibilityOption}`),
value: visibilityOption,
description: translate(`newRoomPage.${visibilityOption}Description`),
- }),
- ),
+ })),
[translate],
);
@@ -302,7 +264,8 @@ function WorkspaceNewRoomPage(props) {
InputComponent={RoomNameInput}
ref={inputCallbackRef}
inputID={INPUT_IDS.ROOM_NAME}
- isFocused={props.isFocused}
+ isFocused={isFocused}
+ // @ts-expect-error TODO: Remove this once RoomNameInput (https://github.com/Expensify/App/issues/25090) is migrated to TypeScript.
shouldDelayFocus
autoFocus
/>
@@ -313,7 +276,7 @@ function WorkspaceNewRoomPage(props) {
inputID={INPUT_IDS.REPORT_DESCRIPTION}
label={translate('reportDescriptionPage.roomDescriptionOptional')}
accessibilityLabel={translate('reportDescriptionPage.roomDescriptionOptional')}
- role={CONST.ACCESSIBILITY_ROLE.TEXT}
+ role={CONST.ROLE.PRESENTATION}
autoGrowHeight
maxLength={CONST.REPORT_DESCRIPTION.MAX_LENGTH}
autoCapitalize="none"
@@ -327,7 +290,7 @@ function WorkspaceNewRoomPage(props) {
label={translate('workspace.common.workspace')}
items={workspaceOptions}
value={policyID}
- onValueChange={setPolicyID}
+ onValueChange={(value) => setPolicyID(value as typeof policyID)}
/>
{isPolicyAdmin && (
@@ -338,7 +301,7 @@ function WorkspaceNewRoomPage(props) {
label={translate('writeCapabilityPage.label')}
items={writeCapabilityOptions}
value={writeCapability}
- onValueChange={setWriteCapability}
+ onValueChange={(value) => setWriteCapability(value as typeof writeCapability)}
/>
)}
@@ -348,7 +311,7 @@ function WorkspaceNewRoomPage(props) {
inputID={INPUT_IDS.VISIBILITY}
label={translate('newRoomPage.visibility')}
items={visibilityOptions}
- onValueChange={setVisibility}
+ onValueChange={(value) => setVisibility(value as typeof visibility)}
value={visibility}
furtherDetails={visibilityDescription}
shouldShowTooltips={false}
@@ -363,32 +326,24 @@ function WorkspaceNewRoomPage(props) {
);
}
-WorkspaceNewRoomPage.propTypes = propTypes;
-WorkspaceNewRoomPage.defaultProps = defaultProps;
WorkspaceNewRoomPage.displayName = 'WorkspaceNewRoomPage';
-export default compose(
- withNavigationFocus,
- withOnyx({
- betas: {
- key: ONYXKEYS.BETAS,
- },
- policies: {
- key: ONYXKEYS.COLLECTION.POLICY,
- },
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- },
- formState: {
- key: ONYXKEYS.FORMS.NEW_ROOM_FORM,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- activePolicyID: {
- key: ONYXKEYS.ACCOUNT,
- selector: (account) => (account && account.activePolicyID) || null,
- initialValue: null,
- },
- }),
-)(WorkspaceNewRoomPage);
+export default withOnyx({
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
+ reports: {
+ key: ONYXKEYS.COLLECTION.REPORT,
+ },
+ formState: {
+ key: ONYXKEYS.FORMS.NEW_ROOM_FORM,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ activePolicyID: {
+ key: ONYXKEYS.ACCOUNT,
+ selector: (account) => account?.activePolicyID ?? null,
+ initialValue: null,
+ },
+})(WorkspaceNewRoomPage);
diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.js b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx
similarity index 53%
rename from src/pages/workspace/WorkspaceProfileCurrencyPage.js
rename to src/pages/workspace/WorkspaceProfileCurrencyPage.tsx
index bd13ce4687f5..e5824ef8a9f9 100644
--- a/src/pages/workspace/WorkspaceProfileCurrencyPage.js
+++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx
@@ -1,65 +1,62 @@
-import PropTypes from 'prop-types';
import React, {useState} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
-import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as Policy from '@userActions/Policy';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import {policyDefaultProps, policyPropTypes} from './withPolicy';
+import type {CurrencyList} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
-const propTypes = {
+type WorkspaceProfileCurrentPageOnyxProps = {
/** Constant, list of available currencies */
- currencyList: PropTypes.objectOf(
- PropTypes.shape({
- /** Symbol of the currency */
- symbol: PropTypes.string.isRequired,
- }),
- ),
- isLoadingReportData: PropTypes.bool,
- ...policyPropTypes,
+ currencyList: OnyxEntry;
};
-const defaultProps = {
- currencyList: {},
- isLoadingReportData: true,
- ...policyDefaultProps,
+type WorkspaceProfileCurrentPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceProfileCurrentPageOnyxProps;
+
+type WorkspaceProfileCurrencyPageSectionItem = {
+ text: string;
+ keyForList: string;
+ isSelected: boolean;
};
-const getDisplayText = (currencyCode, currencySymbol) => `${currencyCode} - ${currencySymbol}`;
+const getDisplayText = (currencyCode: string, currencySymbol: string) => `${currencyCode} - ${currencySymbol}`;
-function WorkspaceSettingsCurrencyPage({currencyList, policy, isLoadingReportData}) {
+function WorkspaceProfileCurrencyPage({currencyList = {}, policy, isLoadingReportData = true}: WorkspaceProfileCurrentPageProps) {
const {translate} = useLocalize();
const [searchText, setSearchText] = useState('');
const trimmedText = searchText.trim().toLowerCase();
- const currencyListKeys = _.keys(currencyList);
+ const currencyListKeys = Object.keys(currencyList ?? {});
- const filteredItems = _.filter(currencyListKeys, (currencyCode) => {
- const currency = currencyList[currencyCode];
- return getDisplayText(currencyCode, currency.symbol).toLowerCase().includes(trimmedText);
+ const filteredItems = currencyListKeys.filter((currencyCode: string) => {
+ const currency = currencyList?.[currencyCode];
+ return getDisplayText(currencyCode, currency?.symbol ?? '')
+ .toLowerCase()
+ .includes(trimmedText);
});
let initiallyFocusedOptionKey;
- const currencyItems = _.map(filteredItems, (currencyCode) => {
- const currency = currencyList[currencyCode];
- const isSelected = policy.outputCurrency === currencyCode;
+ const currencyItems: WorkspaceProfileCurrencyPageSectionItem[] = filteredItems.map((currencyCode: string) => {
+ const currency = currencyList?.[currencyCode];
+ const isSelected = policy?.outputCurrency === currencyCode;
if (isSelected) {
initiallyFocusedOptionKey = currencyCode;
}
return {
- text: getDisplayText(currencyCode, currency.symbol),
+ text: getDisplayText(currencyCode, currency?.symbol ?? ''),
keyForList: currencyCode,
isSelected,
};
@@ -69,20 +66,20 @@ function WorkspaceSettingsCurrencyPage({currencyList, policy, isLoadingReportDat
const headerMessage = searchText.trim() && !currencyItems.length ? translate('common.noResultsFound') : '';
- const onSelectCurrency = (item) => {
- Policy.updateGeneralSettings(policy.id, policy.name, item.keyForList);
+ const onSelectCurrency = (item: WorkspaceProfileCurrencyPageSectionItem) => {
+ Policy.updateGeneralSettings(policy?.id ?? '', policy?.name ?? '', item.keyForList);
Navigation.goBack();
};
return (
Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
- shouldShow={(_.isEmpty(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)}
- subtitleKey={_.isEmpty(policy) ? undefined : 'workspace.common.notAuthorized'}
+ shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)}
+ subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'}
>
({
currencyList: {key: ONYXKEYS.CURRENCY_LIST},
- }),
-)(WorkspaceSettingsCurrencyPage);
+ })(WorkspaceProfileCurrencyPage),
+);
diff --git a/src/pages/workspace/WorkspaceProfilePage.js b/src/pages/workspace/WorkspaceProfilePage.tsx
similarity index 67%
rename from src/pages/workspace/WorkspaceProfilePage.js
rename to src/pages/workspace/WorkspaceProfilePage.tsx
index c91f7ed8fb44..4ce5822dfaa0 100644
--- a/src/pages/workspace/WorkspaceProfilePage.js
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -1,12 +1,12 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
import React, {useCallback} from 'react';
+import type {ImageStyle, StyleProp} from 'react-native';
import {Image, ScrollView, StyleSheet, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import WorkspaceProfile from '@assets/images/workspace-profile.png';
import Avatar from '@components/Avatar';
import AvatarWithImagePicker from '@components/AvatarWithImagePicker';
+import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
@@ -16,59 +16,47 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
+import StringUtils from '@libs/StringUtils';
import * as UserUtils from '@libs/UserUtils';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import withPolicy, {policyDefaultProps, policyPropTypes} from './withPolicy';
+import type {CurrencyList} from '@src/types/onyx';
+import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import withPolicy from './withPolicy';
+import type {WithPolicyProps} from './withPolicy';
import WorkspacePageWithSections from './WorkspacePageWithSections';
-const propTypes = {
+type WorkSpaceProfilePageOnyxProps = {
/** Constant, list of available currencies */
- currencyList: PropTypes.objectOf(
- PropTypes.shape({
- /** Symbol of the currency */
- symbol: PropTypes.string.isRequired,
- }),
- ),
-
- /** The route object passed to this page from the navigator */
- route: PropTypes.shape({
- /** Each parameter passed via the URL */
- params: PropTypes.shape({
- /** The policyID that is being configured */
- policyID: PropTypes.string.isRequired,
- }).isRequired,
- }).isRequired,
-
- ...policyPropTypes,
+ currencyList: OnyxEntry;
};
-const defaultProps = {
- currencyList: {},
- ...policyDefaultProps,
-};
+type WorkSpaceProfilePageProps = WithPolicyProps & WorkSpaceProfilePageOnyxProps;
-function WorkspaceProfilePage({policy, currencyList, route}) {
+function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfilePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
- const formattedCurrency = !_.isEmpty(policy) && !_.isEmpty(currencyList) && !!policy.outputCurrency ? `${policy.outputCurrency} - ${currencyList[policy.outputCurrency].symbol}` : '';
+ const outputCurrency = policy?.outputCurrency ?? '';
+ const currencySymbol = currencyList?.[outputCurrency]?.symbol ?? '';
+ const formattedCurrency = !isEmptyObject(policy) && !isEmptyObject(currencyList) ? `${outputCurrency} - ${currencySymbol}` : '';
- const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_CURRENCY.getRoute(policy.id)), [policy.id]);
- const onPressName = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_NAME.getRoute(policy.id)), [policy.id]);
- const onPressDescription = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy.id)), [policy.id]);
+ const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_CURRENCY.getRoute(policy?.id ?? '')), [policy?.id]);
+ const onPressName = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_NAME.getRoute(policy?.id ?? '')), [policy?.id]);
+ const onPressDescription = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy?.id ?? '')), [policy?.id]);
+ const onPressShare = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_SHARE.getRoute(policy?.id ?? '')), [policy?.id]);
- const policyName = lodashGet(policy, 'name', '');
- const policyDescription = lodashGet(policy, 'description', '');
+ const policyName = policy?.name ?? '';
+ const policyDescription = policy?.description ?? '';
const readOnly = !PolicyUtils.isPolicyAdmin(policy);
- const imageStyle = isSmallScreenWidth ? [styles.mhv12, styles.mhn5] : [styles.mhv8, styles.mhn8];
+ const imageStyle: StyleProp = isSmallScreenWidth ? [styles.mhv12, styles.mhn5] : [styles.mhv8, styles.mhn8];
return (
- {(hasVBA) => (
+ {(hasVBA?: boolean) => (
-
+
Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(policy.id))}
- source={lodashGet(policy, 'avatar')}
+ onViewPhotoPress={() => Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(policy?.id ?? ''))}
+ source={policy?.avatar ?? ''}
size={CONST.AVATAR_SIZE.XLARGE}
avatarStyle={styles.avatarXLarge}
enablePreview
@@ -100,7 +91,7 @@ function WorkspaceProfilePage({policy, currencyList, route}) {
Policy.updateWorkspaceAvatar(lodashGet(policy, 'id', ''), file)}
- onImageRemoved={() => Policy.deleteWorkspaceAvatar(lodashGet(policy, 'id', ''))}
+ isUsingDefaultAvatar={!policy?.avatar ?? null}
+ onImageSelected={(file: File) => Policy.updateWorkspaceAvatar(policy?.id ?? '', file)}
+ onImageRemoved={() => Policy.deleteWorkspaceAvatar(policy?.id ?? '')}
editorMaskImage={Expensicons.ImageCropSquareMask}
- pendingAction={lodashGet(policy, 'pendingFields.avatar', null)}
- errors={lodashGet(policy, 'errorFields.avatar', null)}
- onErrorClose={() => Policy.clearAvatarErrors(policy.id)}
- previewSource={UserUtils.getFullSizeAvatar(policy.avatar, '')}
+ pendingAction={policy?.pendingFields?.avatar ?? null}
+ errors={policy?.errorFields?.avatar ?? null}
+ onErrorClose={() => Policy.clearAvatarErrors(policy?.id ?? '')}
+ previewSource={UserUtils.getFullSizeAvatar(policy?.avatar ?? '')}
headerTitle={translate('workspace.common.workspaceAvatar')}
- originalFileName={policy.originalFileName}
+ originalFileName={policy?.originalFileName}
disabled={readOnly}
disabledStyle={styles.cursorDefault}
+ errorRowStyles={undefined}
/>
-
+
- {(!_.isEmpty(policy.description) || !readOnly) && (
-
+ {(!StringUtils.isEmptyString(policy?.description ?? '') || !readOnly) && (
+
)}
-
+
+ {!readOnly && (
+
+
+
+ )}
@@ -176,13 +179,10 @@ function WorkspaceProfilePage({policy, currencyList, route}) {
);
}
-WorkspaceProfilePage.propTypes = propTypes;
-WorkspaceProfilePage.defaultProps = defaultProps;
WorkspaceProfilePage.displayName = 'WorkspaceProfilePage';
-export default compose(
- withPolicy,
- withOnyx({
+export default withPolicy(
+ withOnyx({
currencyList: {key: ONYXKEYS.CURRENCY_LIST},
- }),
-)(WorkspaceProfilePage);
+ })(WorkspaceProfilePage),
+);
diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx
new file mode 100644
index 000000000000..dd03436042ca
--- /dev/null
+++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx
@@ -0,0 +1,88 @@
+import React, {useRef} from 'react';
+import {ScrollView, View} from 'react-native';
+import type {ImageSourcePropType} from 'react-native';
+import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png';
+import ContextMenuItem from '@components/ContextMenuItem';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItem from '@components/MenuItem';
+import QRShareWithDownload from '@components/QRShare/QRShareWithDownload';
+import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useEnvironment from '@hooks/useEnvironment';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import Clipboard from '@libs/Clipboard';
+import Navigation from '@libs/Navigation/Navigation';
+import shouldAllowDownloadQRCode from '@libs/shouldAllowDownloadQRCode';
+import * as Url from '@libs/Url';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import withPolicy from './withPolicy';
+import type {WithPolicyProps} from './withPolicy';
+
+function WorkspaceProfileSharePage({policy}: WithPolicyProps) {
+ const themeStyles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {environmentURL} = useEnvironment();
+ const qrCodeRef = useRef(null);
+ const {isSmallScreenWidth} = useWindowDimensions();
+
+ const policyName = policy?.name ?? '';
+ const id = policy?.id ?? '';
+ const urlWithTrailingSlash = Url.addTrailingForwardSlash(environmentURL);
+
+ const url = `${urlWithTrailingSlash}${ROUTES.WORKSPACE_PROFILE.getRoute(id)}`;
+ return (
+
+
+
+
+
+
+
+
+
+ Clipboard.setString(url)}
+ shouldLimitWidth={false}
+ wrapperStyle={themeStyles.sectionMenuItemTopDescription}
+ />
+ {shouldAllowDownloadQRCode && (
+
+
+
+
+ );
+}
+
+WorkspaceProfileSharePage.displayName = 'WorkspaceProfileSharePage';
+
+export default withPolicy(WorkspaceProfileSharePage);
diff --git a/src/setup/index.ts b/src/setup/index.ts
index fe9d80ec5fb1..285b01792cc6 100644
--- a/src/setup/index.ts
+++ b/src/setup/index.ts
@@ -34,7 +34,7 @@ export default function () {
// Clear any loading and error messages so they do not appear on app startup
[ONYXKEYS.SESSION]: {loading: false},
[ONYXKEYS.ACCOUNT]: CONST.DEFAULT_ACCOUNT_DATA,
- [ONYXKEYS.NETWORK]: {isOffline: false},
+ [ONYXKEYS.NETWORK]: CONST.DEFAULT_NETWORK_DATA,
[ONYXKEYS.IS_SIDEBAR_LOADED]: false,
[ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: true,
[ONYXKEYS.MODAL]: {
diff --git a/src/stories/SelectionList.stories.js b/src/stories/SelectionList.stories.js
index dcd639119886..6c289097552b 100644
--- a/src/stories/SelectionList.stories.js
+++ b/src/stories/SelectionList.stories.js
@@ -1,9 +1,8 @@
import React, {useMemo, useState} from 'react';
-import {View} from 'react-native';
import _ from 'underscore';
+import Badge from '@components/Badge';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
-import Text from '@components/Text';
// eslint-disable-next-line no-restricted-imports
import {defaultStyles} from '@styles/index';
import CONST from '@src/CONST';
@@ -232,9 +231,11 @@ function MultipleSelection(args) {
accountID: item.keyForList,
login: item.text,
rightElement: isAdmin && (
-
- Admin
-
+
),
};
});
@@ -295,9 +296,11 @@ function WithSectionHeader(args) {
accountID: item.keyForList,
login: item.text,
rightElement: isAdmin && (
-
- Admin
-
+
),
};
});
@@ -356,9 +359,11 @@ function WithConfirmButton(args) {
accountID: item.keyForList,
login: item.text,
rightElement: isAdmin && (
-
- Admin
-
+
),
};
});
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 13b2015d2c9c..238ba1afc781 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -3115,19 +3115,6 @@ const styles = (theme: ThemeColors) =>
...spacing.pb2,
},
- peopleBadge: {
- backgroundColor: theme.icon,
- ...spacing.ph3,
- ...spacing.ml3,
- },
-
- peopleBadgeText: {
- color: theme.textReversed,
- fontSize: variables.fontSizeSmall,
- lineHeight: variables.lineHeightNormal,
- ...whiteSpace.noWrap,
- },
-
offlineFeedback: {
deleted: {
textDecorationLine: 'line-through',
@@ -4231,6 +4218,17 @@ const styles = (theme: ThemeColors) =>
marginHorizontal: 20,
},
+ selectionListPressableItemWrapper: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ paddingHorizontal: 16,
+ paddingVertical: 16,
+ marginHorizontal: 20,
+ marginBottom: 12,
+ backgroundColor: theme.highlightBG,
+ borderRadius: 8,
+ },
+
draggableTopBar: {
height: 30,
width: '100%',
diff --git a/src/styles/utils/borders.ts b/src/styles/utils/borders.ts
index 26fdf6415fc7..2e20091e3fae 100644
--- a/src/styles/utils/borders.ts
+++ b/src/styles/utils/borders.ts
@@ -8,6 +8,10 @@ export default {
borderRadius: 0,
},
+ br1: {
+ borderRadius: 4,
+ },
+
br2: {
borderRadius: 8,
},
diff --git a/src/types/form/AddDebitCardForm.ts b/src/types/form/AddDebitCardForm.ts
index 4a73766fb8f1..b5badf0bb7f3 100644
--- a/src/types/form/AddDebitCardForm.ts
+++ b/src/types/form/AddDebitCardForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -12,19 +13,23 @@ const INPUT_IDS = {
ACCEPT_TERMS: 'acceptTerms',
} as const;
-type AddDebitCardForm = Form<{
- /** Whether the form has been submitted */
- [INPUT_IDS.SETUP_COMPLETE]: boolean;
+type InputID = ValueOf;
- [INPUT_IDS.NAME_ON_CARD]?: string;
- [INPUT_IDS.CARD_NUMBER]?: string;
- [INPUT_IDS.EXPIRATION_DATE]?: string;
- [INPUT_IDS.SECURITY_CODE]?: string;
- [INPUT_IDS.ADDRESS_STREET]?: string;
- [INPUT_IDS.ADDRESS_ZIP_CODE]?: string;
- [INPUT_IDS.ADDRESS_STATE]?: string;
- [INPUT_IDS.ACCEPT_TERMS]?: string;
-}>;
+type AddDebitCardForm = Form<
+ InputID,
+ {
+ /** Whether the form has been submitted */
+ [INPUT_IDS.SETUP_COMPLETE]: boolean;
+ [INPUT_IDS.NAME_ON_CARD]: string;
+ [INPUT_IDS.CARD_NUMBER]: string;
+ [INPUT_IDS.EXPIRATION_DATE]: string;
+ [INPUT_IDS.SECURITY_CODE]: string;
+ [INPUT_IDS.ADDRESS_STREET]: string;
+ [INPUT_IDS.ADDRESS_ZIP_CODE]: string;
+ [INPUT_IDS.ADDRESS_STATE]: string;
+ [INPUT_IDS.ACCEPT_TERMS]: string;
+ }
+>;
export type {AddDebitCardForm};
export default INPUT_IDS;
diff --git a/src/types/form/CloseAccountForm.ts b/src/types/form/CloseAccountForm.ts
index 054dcd71e442..dd852ded653f 100644
--- a/src/types/form/CloseAccountForm.ts
+++ b/src/types/form/CloseAccountForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -6,11 +7,16 @@ const INPUT_IDS = {
SUCCESS: 'success',
} as const;
-type CloseAccountForm = Form<{
- [INPUT_IDS.REASON_FOR_LEAVING]: string;
- [INPUT_IDS.PHONE_OR_EMAIL]: string;
- [INPUT_IDS.SUCCESS]: string;
-}>;
+type InputID = ValueOf;
+
+type CloseAccountForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.REASON_FOR_LEAVING]: string;
+ [INPUT_IDS.PHONE_OR_EMAIL]: string;
+ [INPUT_IDS.SUCCESS]: string;
+ }
+>;
export type {CloseAccountForm};
export default INPUT_IDS;
diff --git a/src/types/form/DateOfBirthForm.ts b/src/types/form/DateOfBirthForm.ts
index 01678669f176..999b4e44459d 100644
--- a/src/types/form/DateOfBirthForm.ts
+++ b/src/types/form/DateOfBirthForm.ts
@@ -1,13 +1,19 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
DOB: 'dob',
} as const;
-type DateOfBirthForm = Form<{
- /** Date of birth */
- [INPUT_IDS.DOB]: string;
-}>;
+type InputID = ValueOf;
+
+type DateOfBirthForm = Form<
+ InputID,
+ {
+ /** Date of birth */
+ [INPUT_IDS.DOB]: string;
+ }
+>;
export type {DateOfBirthForm};
export default INPUT_IDS;
diff --git a/src/types/form/DisplayNameForm.ts b/src/types/form/DisplayNameForm.ts
index 3f9738c45a34..ae40e6020fd7 100644
--- a/src/types/form/DisplayNameForm.ts
+++ b/src/types/form/DisplayNameForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -5,10 +6,15 @@ const INPUT_IDS = {
LAST_NAME: 'lastName',
} as const;
-type DisplayNameForm = Form<{
- [INPUT_IDS.FIRST_NAME]: string;
- [INPUT_IDS.LAST_NAME]: string;
-}>;
+type InputID = ValueOf;
+
+type DisplayNameForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.FIRST_NAME]: string;
+ [INPUT_IDS.LAST_NAME]: string;
+ }
+>;
export type {DisplayNameForm};
export default INPUT_IDS;
diff --git a/src/types/form/EditTaskForm.ts b/src/types/form/EditTaskForm.ts
index 05de0310c784..86a1c8198fec 100644
--- a/src/types/form/EditTaskForm.ts
+++ b/src/types/form/EditTaskForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -5,10 +6,15 @@ const INPUT_IDS = {
DESCRIPTION: 'description',
} as const;
-type EditTaskForm = Form<{
- [INPUT_IDS.TITLE]: string;
- [INPUT_IDS.DESCRIPTION]: string;
-}>;
+type InputID = ValueOf;
+
+type EditTaskForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.TITLE]: string;
+ [INPUT_IDS.DESCRIPTION]: string;
+ }
+>;
export type {EditTaskForm};
export default INPUT_IDS;
diff --git a/src/types/form/Form.ts b/src/types/form/Form.ts
index d80eb65f4215..cf35e84646b1 100644
--- a/src/types/form/Form.ts
+++ b/src/types/form/Form.ts
@@ -12,8 +12,8 @@ type BaseForm = {
errorFields?: OnyxCommon.ErrorFields | null;
};
-type FormValues = Record;
-type Form = TFormValues & BaseForm;
+type FormValues = Record;
+type Form = FormValues> = TFormValues & BaseForm;
export default Form;
export type {BaseForm};
diff --git a/src/types/form/GetPhysicalCardForm.ts b/src/types/form/GetPhysicalCardForm.ts
index 091113eb4763..c8fc6f3cce9e 100644
--- a/src/types/form/GetPhysicalCardForm.ts
+++ b/src/types/form/GetPhysicalCardForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
import ADDRESS_INPUT_IDS from './HomeAddressForm';
@@ -8,17 +9,22 @@ const INPUT_IDS = {
PHONE_NUMBER: 'phoneNumber',
} as const;
-type GetPhysicalCardForm = Form<{
- [INPUT_IDS.ADDRESS_LINE_1]?: string;
- [INPUT_IDS.ADDRESS_LINE_2]?: string;
- [INPUT_IDS.COUNTRY]?: string;
- [INPUT_IDS.STATE]?: string;
- [INPUT_IDS.CITY]?: string;
- [INPUT_IDS.ZIP_POST_CODE]?: string;
- [INPUT_IDS.LEGAL_FIRST_NAME]?: string;
- [INPUT_IDS.LEGAL_LAST_NAME]?: string;
- [INPUT_IDS.PHONE_NUMBER]?: string;
-}>;
+type InputID = ValueOf;
+
+type GetPhysicalCardForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.ADDRESS_LINE_1]: string;
+ [INPUT_IDS.ADDRESS_LINE_2]: string;
+ [INPUT_IDS.COUNTRY]: string;
+ [INPUT_IDS.STATE]: string;
+ [INPUT_IDS.CITY]: string;
+ [INPUT_IDS.ZIP_POST_CODE]: string;
+ [INPUT_IDS.LEGAL_FIRST_NAME]: string;
+ [INPUT_IDS.LEGAL_LAST_NAME]: string;
+ [INPUT_IDS.PHONE_NUMBER]: string;
+ }
+>;
export type {GetPhysicalCardForm};
export default INPUT_IDS;
diff --git a/src/types/form/HomeAddressForm.ts b/src/types/form/HomeAddressForm.ts
index 4e819146faf7..6d9ef8580078 100644
--- a/src/types/form/HomeAddressForm.ts
+++ b/src/types/form/HomeAddressForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -9,14 +10,19 @@ const INPUT_IDS = {
ZIP_POST_CODE: 'zipPostCode',
} as const;
-type HomeAddressForm = Form<{
- [INPUT_IDS.ADDRESS_LINE_1]: string;
- [INPUT_IDS.ADDRESS_LINE_2]: string;
- [INPUT_IDS.COUNTRY]: string;
- [INPUT_IDS.STATE]: string;
- [INPUT_IDS.CITY]: string;
- [INPUT_IDS.ZIP_POST_CODE]: string;
-}>;
+type InputID = ValueOf;
+
+type HomeAddressForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.ADDRESS_LINE_1]: string;
+ [INPUT_IDS.ADDRESS_LINE_2]: string;
+ [INPUT_IDS.COUNTRY]: string;
+ [INPUT_IDS.STATE]: string;
+ [INPUT_IDS.CITY]: string;
+ [INPUT_IDS.ZIP_POST_CODE]: string;
+ }
+>;
export type {HomeAddressForm};
export default INPUT_IDS;
diff --git a/src/types/form/IKnowTeacherForm.ts b/src/types/form/IKnowTeacherForm.ts
index 7d6ba4b9079a..d406dd57de56 100644
--- a/src/types/form/IKnowTeacherForm.ts
+++ b/src/types/form/IKnowTeacherForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -6,11 +7,16 @@ const INPUT_IDS = {
PARTNER_USER_ID: 'partnerUserID',
} as const;
-type IKnowTeacherForm = Form<{
- [INPUT_IDS.FIRST_NAME]: string;
- [INPUT_IDS.LAST_NAME]: string;
- [INPUT_IDS.PARTNER_USER_ID]: string;
-}>;
+type InputID = ValueOf;
+
+type IKnowTeacherForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.FIRST_NAME]: string;
+ [INPUT_IDS.LAST_NAME]: string;
+ [INPUT_IDS.PARTNER_USER_ID]: string;
+ }
+>;
export type {IKnowTeacherForm};
export default INPUT_IDS;
diff --git a/src/types/form/IntroSchoolPrincipalForm.ts b/src/types/form/IntroSchoolPrincipalForm.ts
index 61742a98c6fd..6cb4a74d0f7e 100644
--- a/src/types/form/IntroSchoolPrincipalForm.ts
+++ b/src/types/form/IntroSchoolPrincipalForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -6,11 +7,16 @@ const INPUT_IDS = {
PARTNER_USER_ID: 'partnerUserID',
} as const;
-type IntroSchoolPrincipalForm = Form<{
- [INPUT_IDS.FIRST_NAME]: string;
- [INPUT_IDS.LAST_NAME]: string;
- [INPUT_IDS.PARTNER_USER_ID]: string;
-}>;
+type InputID = ValueOf;
+
+type IntroSchoolPrincipalForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.FIRST_NAME]: string;
+ [INPUT_IDS.LAST_NAME]: string;
+ [INPUT_IDS.PARTNER_USER_ID]: string;
+ }
+>;
export type {IntroSchoolPrincipalForm};
export default INPUT_IDS;
diff --git a/src/types/form/LegalNameForm.ts b/src/types/form/LegalNameForm.ts
index 8ca9e44a7754..808d3f68639a 100644
--- a/src/types/form/LegalNameForm.ts
+++ b/src/types/form/LegalNameForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -5,10 +6,15 @@ const INPUT_IDS = {
LEGAL_LAST_NAME: 'legalLastName',
} as const;
-type LegalNameForm = Form<{
- [INPUT_IDS.LEGAL_FIRST_NAME]: string;
- [INPUT_IDS.LEGAL_LAST_NAME]: string;
-}>;
+type InputID = ValueOf;
+
+type LegalNameForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.LEGAL_FIRST_NAME]: string;
+ [INPUT_IDS.LEGAL_LAST_NAME]: string;
+ }
+>;
export type {LegalNameForm};
export default INPUT_IDS;
diff --git a/src/types/form/MoneyRequestCreatedForm.ts b/src/types/form/MoneyRequestCreatedForm.ts
deleted file mode 100644
index b7df7362a888..000000000000
--- a/src/types/form/MoneyRequestCreatedForm.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type Form from './Form';
-
-const INPUT_IDS = {
- CREATED: 'created',
- MONEY_REQUEST_CREATED: 'moneyRequestCreated',
-} as const;
-
-type MoneyRequestCreatedForm = Form<{
- [INPUT_IDS.CREATED]: string;
- [INPUT_IDS.MONEY_REQUEST_CREATED]: string;
-}>;
-
-export type {MoneyRequestCreatedForm};
-export default INPUT_IDS;
diff --git a/src/types/form/MoneyRequestDateForm.ts b/src/types/form/MoneyRequestDateForm.ts
new file mode 100644
index 000000000000..64e9336d0659
--- /dev/null
+++ b/src/types/form/MoneyRequestDateForm.ts
@@ -0,0 +1,20 @@
+import type {ValueOf} from 'type-fest';
+import type Form from './Form';
+
+const INPUT_IDS = {
+ CREATED: 'created',
+ MONEY_REQUEST_CREATED: 'moneyRequestCreated',
+} as const;
+
+type InputID = ValueOf;
+
+type MoneyRequestDateForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.CREATED]: string;
+ [INPUT_IDS.MONEY_REQUEST_CREATED]: string;
+ }
+>;
+
+export type {MoneyRequestDateForm};
+export default INPUT_IDS;
diff --git a/src/types/form/MoneyRequestDescriptionForm.ts b/src/types/form/MoneyRequestDescriptionForm.ts
index df8ae5d225f3..e64e183c57b2 100644
--- a/src/types/form/MoneyRequestDescriptionForm.ts
+++ b/src/types/form/MoneyRequestDescriptionForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -5,10 +6,15 @@ const INPUT_IDS = {
MONEY_REQUEST_COMMENT: 'moneyRequestComment',
} as const;
-type MoneyRequestDescriptionForm = Form<{
- [INPUT_IDS.COMMENT]: string;
- [INPUT_IDS.MONEY_REQUEST_COMMENT]: string;
-}>;
+type InputID = ValueOf;
+
+type MoneyRequestDescriptionForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.COMMENT]: string;
+ [INPUT_IDS.MONEY_REQUEST_COMMENT]: string;
+ }
+>;
export type {MoneyRequestDescriptionForm};
export default INPUT_IDS;
diff --git a/src/types/form/MoneyRequestHoldReasonForm.ts b/src/types/form/MoneyRequestHoldReasonForm.ts
index 0d80810eaf69..26ce5e2a88a9 100644
--- a/src/types/form/MoneyRequestHoldReasonForm.ts
+++ b/src/types/form/MoneyRequestHoldReasonForm.ts
@@ -1,12 +1,18 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
COMMENT: 'comment',
} as const;
-type MoneyRequestHoldReasonForm = Form<{
- [INPUT_IDS.COMMENT]: string;
-}>;
+type InputID = ValueOf;
+
+type MoneyRequestHoldReasonForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.COMMENT]: string;
+ }
+>;
export type {MoneyRequestHoldReasonForm};
export default INPUT_IDS;
diff --git a/src/types/form/MoneyRequestMerchantForm.ts b/src/types/form/MoneyRequestMerchantForm.ts
index 0dd194037b7d..589915e55694 100644
--- a/src/types/form/MoneyRequestMerchantForm.ts
+++ b/src/types/form/MoneyRequestMerchantForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -5,10 +6,15 @@ const INPUT_IDS = {
MONEY_REQUEST_MERCHANT: 'moneyRequestMerchant',
} as const;
-type MoneyRequestMerchantForm = Form<{
- [INPUT_IDS.MERCHANT]: string;
- [INPUT_IDS.MONEY_REQUEST_MERCHANT]: string;
-}>;
+type InputID = ValueOf;
+
+type MoneyRequestMerchantForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.MERCHANT]: string;
+ [INPUT_IDS.MONEY_REQUEST_MERCHANT]: string;
+ }
+>;
export type {MoneyRequestMerchantForm};
export default INPUT_IDS;
diff --git a/src/types/form/NewContactMethodForm.ts b/src/types/form/NewContactMethodForm.ts
index baa67cec1cbf..cea8ffad1992 100644
--- a/src/types/form/NewContactMethodForm.ts
+++ b/src/types/form/NewContactMethodForm.ts
@@ -1,12 +1,18 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
PHONE_OR_EMAIL: 'phoneOrEmail',
} as const;
-type NewContactMethodForm = Form<{
- [INPUT_IDS.PHONE_OR_EMAIL]: string;
-}>;
+type InputID = ValueOf;
+
+type NewContactMethodForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.PHONE_OR_EMAIL]: string;
+ }
+>;
export type {NewContactMethodForm};
export default INPUT_IDS;
diff --git a/src/types/form/NewRoomForm.ts b/src/types/form/NewRoomForm.ts
index 8fe047f8a7e4..3e21e97140ef 100644
--- a/src/types/form/NewRoomForm.ts
+++ b/src/types/form/NewRoomForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -8,13 +9,18 @@ const INPUT_IDS = {
VISIBILITY: 'visibility',
} as const;
-type NewRoomForm = Form<{
- [INPUT_IDS.ROOM_NAME]?: string;
- [INPUT_IDS.REPORT_DESCRIPTION]?: string;
- [INPUT_IDS.POLICY_ID]?: string;
- [INPUT_IDS.WRITE_CAPABILITY]?: string;
- [INPUT_IDS.VISIBILITY]?: string;
-}>;
+type InputID = ValueOf;
+
+type NewRoomForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.ROOM_NAME]: string;
+ [INPUT_IDS.REPORT_DESCRIPTION]: string;
+ [INPUT_IDS.POLICY_ID]: string;
+ [INPUT_IDS.WRITE_CAPABILITY]: string;
+ [INPUT_IDS.VISIBILITY]: string;
+ }
+>;
export type {NewRoomForm};
export default INPUT_IDS;
diff --git a/src/types/form/NewTaskForm.ts b/src/types/form/NewTaskForm.ts
index b281a79321fb..a9ef7155a9d1 100644
--- a/src/types/form/NewTaskForm.ts
+++ b/src/types/form/NewTaskForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -5,10 +6,15 @@ const INPUT_IDS = {
TASK_DESCRIPTION: 'taskDescription',
} as const;
-type NewTaskForm = Form<{
- [INPUT_IDS.TASK_TITLE]: string;
- [INPUT_IDS.TASK_DESCRIPTION]: string;
-}>;
+type InputID = ValueOf;
+
+type NewTaskForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.TASK_TITLE]: string;
+ [INPUT_IDS.TASK_DESCRIPTION]: string;
+ }
+>;
export type {NewTaskForm};
export default INPUT_IDS;
diff --git a/src/types/form/PrivateNotesForm.ts b/src/types/form/PrivateNotesForm.ts
index 2961b693ef6b..13fe312a1514 100644
--- a/src/types/form/PrivateNotesForm.ts
+++ b/src/types/form/PrivateNotesForm.ts
@@ -1,12 +1,18 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
PRIVATE_NOTES: 'privateNotes',
} as const;
-type PrivateNotesForm = Form<{
- [INPUT_IDS.PRIVATE_NOTES]: string;
-}>;
+type InputID = ValueOf;
+
+type PrivateNotesForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.PRIVATE_NOTES]: string;
+ }
+>;
export type {PrivateNotesForm};
export default INPUT_IDS;
diff --git a/src/types/form/ReimbursementAccountForm.ts b/src/types/form/ReimbursementAccountForm.ts
index 7bc1c52e8025..7860d5a066f1 100644
--- a/src/types/form/ReimbursementAccountForm.ts
+++ b/src/types/form/ReimbursementAccountForm.ts
@@ -1,3 +1,4 @@
+import type DeepValueOf from '@src/types/utils/DeepValueOf';
import type Form from './Form';
const INPUT_IDS = {
@@ -50,73 +51,79 @@ const INPUT_IDS = {
AMOUNT3: 'amount3',
} as const;
+type InputID = DeepValueOf;
+
+type BeneficialOwnersStepBaseProps = {
+ [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.OWNS_MORE_THAN_25_PERCENT]: boolean;
+ [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.HAS_OTHER_BENEFICIAL_OWNERS]: boolean;
+ [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNERS]: string;
+};
+
// BeneficialOwnerDraftData is saved under dynamic key which consists of prefix, beneficial owner ID and input key
type BeneficialOwnerDataKey = `beneficialOwner_${string}_${string}`;
-type AdditionalDraftData = {selectedPlaidAccountID?: string; bankAccountID?: number};
+type ReimbursementAccountFormExtraProps = BeneficialOwnersStepExtraProps & {bankAccountID?: number};
+
+type BeneficialOwnersStepExtraProps = {
+ [key: BeneficialOwnerDataKey]: string;
+ beneficialOwnerKeys?: string[];
+};
+
+type BeneficialOwnersStepProps = BeneficialOwnersStepBaseProps & BeneficialOwnersStepExtraProps;
type BankAccountStepProps = {
- [INPUT_IDS.BANK_INFO_STEP.ACCOUNT_NUMBER]?: string;
- [INPUT_IDS.BANK_INFO_STEP.ROUTING_NUMBER]?: string;
- [INPUT_IDS.BANK_INFO_STEP.PLAID_ACCOUNT_ID]?: string;
- [INPUT_IDS.BANK_INFO_STEP.PLAID_MASK]?: string;
+ [INPUT_IDS.BANK_INFO_STEP.ACCOUNT_NUMBER]: string;
+ [INPUT_IDS.BANK_INFO_STEP.ROUTING_NUMBER]: string;
+ [INPUT_IDS.BANK_INFO_STEP.PLAID_ACCOUNT_ID]: string;
+ [INPUT_IDS.BANK_INFO_STEP.PLAID_MASK]: string;
};
type CompanyStepProps = {
- [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_NAME]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.STREET]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.CITY]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.STATE]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.ZIP_CODE]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_PHONE]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_WEBSITE]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_TAX_ID]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_TYPE]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_DATE]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_STATE]?: string;
- [INPUT_IDS.BUSINESS_INFO_STEP.HAS_NO_CONNECTION_TO_CANNABIS]?: boolean;
+ [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_NAME]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.STREET]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.CITY]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.STATE]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.ZIP_CODE]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_PHONE]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_WEBSITE]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_TAX_ID]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_TYPE]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_DATE]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.INCORPORATION_STATE]: string;
+ [INPUT_IDS.BUSINESS_INFO_STEP.HAS_NO_CONNECTION_TO_CANNABIS]: boolean;
};
type RequestorStepProps = {
- [INPUT_IDS.PERSONAL_INFO_STEP.FIRST_NAME]?: string;
- [INPUT_IDS.PERSONAL_INFO_STEP.LAST_NAME]?: string;
- [INPUT_IDS.PERSONAL_INFO_STEP.STREET]?: string;
- [INPUT_IDS.PERSONAL_INFO_STEP.CITY]?: string;
- [INPUT_IDS.PERSONAL_INFO_STEP.STATE]?: string;
- [INPUT_IDS.PERSONAL_INFO_STEP.ZIP_CODE]?: string;
- [INPUT_IDS.PERSONAL_INFO_STEP.DOB]?: string;
- [INPUT_IDS.PERSONAL_INFO_STEP.SSN_LAST_4]?: string;
-};
-
-type BeneficialOwnersStepProps = {
- [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.OWNS_MORE_THAN_25_PERCENT]?: boolean;
- [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.HAS_OTHER_BENEFICIAL_OWNERS]?: boolean;
- [INPUT_IDS.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNERS]?: string;
- [key: BeneficialOwnerDataKey]: string;
- beneficialOwnerKeys?: string[];
+ [INPUT_IDS.PERSONAL_INFO_STEP.FIRST_NAME]: string;
+ [INPUT_IDS.PERSONAL_INFO_STEP.LAST_NAME]: string;
+ [INPUT_IDS.PERSONAL_INFO_STEP.STREET]: string;
+ [INPUT_IDS.PERSONAL_INFO_STEP.CITY]: string;
+ [INPUT_IDS.PERSONAL_INFO_STEP.STATE]: string;
+ [INPUT_IDS.PERSONAL_INFO_STEP.ZIP_CODE]: string;
+ [INPUT_IDS.PERSONAL_INFO_STEP.DOB]: string;
+ [INPUT_IDS.PERSONAL_INFO_STEP.SSN_LAST_4]: string;
};
type ACHContractStepProps = {
- [INPUT_IDS.COMPLETE_VERIFICATION.ACCEPT_TERMS_AND_CONDITIONS]?: boolean;
- [INPUT_IDS.COMPLETE_VERIFICATION.CERTIFY_TRUE_INFORMATION]?: boolean;
- [INPUT_IDS.COMPLETE_VERIFICATION.IS_AUTHORIZED_TO_USE_BANK_ACCOUNT]?: boolean;
+ [INPUT_IDS.COMPLETE_VERIFICATION.ACCEPT_TERMS_AND_CONDITIONS]: boolean;
+ [INPUT_IDS.COMPLETE_VERIFICATION.CERTIFY_TRUE_INFORMATION]: boolean;
+ [INPUT_IDS.COMPLETE_VERIFICATION.IS_AUTHORIZED_TO_USE_BANK_ACCOUNT]: boolean;
};
type ReimbursementAccountProps = {
- [INPUT_IDS.BANK_INFO_STEP.IS_SAVINGS]?: boolean;
- [INPUT_IDS.BANK_INFO_STEP.BANK_NAME]?: string;
- [INPUT_IDS.BANK_INFO_STEP.PLAID_ACCESS_TOKEN]?: string;
- [INPUT_IDS.AMOUNT1]?: string;
- [INPUT_IDS.AMOUNT2]?: string;
- [INPUT_IDS.AMOUNT3]?: string;
+ [INPUT_IDS.BANK_INFO_STEP.IS_SAVINGS]: boolean;
+ [INPUT_IDS.BANK_INFO_STEP.BANK_NAME]: string;
+ [INPUT_IDS.BANK_INFO_STEP.PLAID_ACCESS_TOKEN]: string;
+ [INPUT_IDS.BANK_INFO_STEP.SELECTED_PLAID_ACCOUNT_ID]: string;
+ [INPUT_IDS.AMOUNT1]: string;
+ [INPUT_IDS.AMOUNT2]: string;
+ [INPUT_IDS.AMOUNT3]: string;
};
-type ReimbursementAccountForm = AdditionalDraftData &
- BeneficialOwnersStepProps &
- Form;
+type ReimbursementAccountForm = ReimbursementAccountFormExtraProps &
+ Form;
export type {
ReimbursementAccountForm,
- AdditionalDraftData,
BeneficialOwnerDataKey,
BankAccountStepProps,
CompanyStepProps,
diff --git a/src/types/form/ReportDescriptionForm.ts b/src/types/form/ReportDescriptionForm.ts
index 18f42ad80182..b3300051d90c 100644
--- a/src/types/form/ReportDescriptionForm.ts
+++ b/src/types/form/ReportDescriptionForm.ts
@@ -1,12 +1,18 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
REPORT_DESCRIPTION: 'reportDescription',
} as const;
-type ReportDescriptionForm = Form<{
- [INPUT_IDS.REPORT_DESCRIPTION]: string;
-}>;
+type InputID = ValueOf;
+
+type ReportDescriptionForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.REPORT_DESCRIPTION]: string;
+ }
+>;
export type {ReportDescriptionForm};
export default INPUT_IDS;
diff --git a/src/types/form/ReportFieldEditForm.ts b/src/types/form/ReportFieldEditForm.ts
index 3dce5456875c..7befa2cb6502 100644
--- a/src/types/form/ReportFieldEditForm.ts
+++ b/src/types/form/ReportFieldEditForm.ts
@@ -1,6 +1,6 @@
import type Form from './Form';
-type ReportFieldEditForm = Form>;
+type ReportFieldEditForm = Form>;
// eslint-disable-next-line import/prefer-default-export
export type {ReportFieldEditForm};
diff --git a/src/types/form/ReportPhysicalCardForm.ts b/src/types/form/ReportPhysicalCardForm.ts
new file mode 100644
index 000000000000..2daafb974c2c
--- /dev/null
+++ b/src/types/form/ReportPhysicalCardForm.ts
@@ -0,0 +1,6 @@
+import type Form from './Form';
+
+type ReportPhysicalCardForm = Form;
+
+// eslint-disable-next-line import/prefer-default-export
+export type {ReportPhysicalCardForm};
diff --git a/src/types/form/RoomNameForm.ts b/src/types/form/RoomNameForm.ts
index 0a128d2c175f..b9fb996986b9 100644
--- a/src/types/form/RoomNameForm.ts
+++ b/src/types/form/RoomNameForm.ts
@@ -1,12 +1,18 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
ROOM_NAME: 'roomName',
} as const;
-type RoomNameForm = Form<{
- [INPUT_IDS.ROOM_NAME]: string;
-}>;
+type InputID = ValueOf;
+
+type RoomNameForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.ROOM_NAME]: string;
+ }
+>;
export type {RoomNameForm};
export default INPUT_IDS;
diff --git a/src/types/form/SettingsStatusClearDateForm.ts b/src/types/form/SettingsStatusClearDateForm.ts
index fdb191cb4bc5..65f2e046d272 100644
--- a/src/types/form/SettingsStatusClearDateForm.ts
+++ b/src/types/form/SettingsStatusClearDateForm.ts
@@ -1,12 +1,18 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
DATE_TIME: 'dateTime',
} as const;
-type SettingsStatusClearDateForm = Form<{
- [INPUT_IDS.DATE_TIME]: string;
-}>;
+type InputID = ValueOf;
+
+type SettingsStatusClearDateForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.DATE_TIME]: string;
+ }
+>;
export type {SettingsStatusClearDateForm};
export default INPUT_IDS;
diff --git a/src/types/form/WorkspaceDescriptionForm.ts b/src/types/form/WorkspaceDescriptionForm.ts
index b06a37abf36b..e6f9596e79a1 100644
--- a/src/types/form/WorkspaceDescriptionForm.ts
+++ b/src/types/form/WorkspaceDescriptionForm.ts
@@ -1,12 +1,18 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
DESCRIPTION: 'description',
} as const;
-type WorkspaceDescriptionForm = Form<{
- [INPUT_IDS.DESCRIPTION]: string;
-}>;
+type InputID = ValueOf;
+
+type WorkspaceDescriptionForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.DESCRIPTION]: string;
+ }
+>;
export type {WorkspaceDescriptionForm};
export default INPUT_IDS;
diff --git a/src/types/form/WorkspaceInviteMessageForm.ts b/src/types/form/WorkspaceInviteMessageForm.ts
index 268e29224f35..7f36b3281703 100644
--- a/src/types/form/WorkspaceInviteMessageForm.ts
+++ b/src/types/form/WorkspaceInviteMessageForm.ts
@@ -1,12 +1,18 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
WELCOME_MESSAGE: 'welcomeMessage',
} as const;
-type WorkspaceInviteMessageForm = Form<{
- [INPUT_IDS.WELCOME_MESSAGE]: string;
-}>;
+type InputID = ValueOf;
+
+type WorkspaceInviteMessageForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.WELCOME_MESSAGE]: string;
+ }
+>;
export type {WorkspaceInviteMessageForm};
export default INPUT_IDS;
diff --git a/src/types/form/WorkspaceProfileDescriptionForm.ts b/src/types/form/WorkspaceProfileDescriptionForm.ts
deleted file mode 100644
index 2fc84334c03e..000000000000
--- a/src/types/form/WorkspaceProfileDescriptionForm.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type Form from './Form';
-
-const INPUT_IDS = {
- DESCRIPTION: 'description',
-} as const;
-
-type WorkspaceProfileDescriptionForm = Form<{
- [INPUT_IDS.DESCRIPTION]: string;
-}>;
-
-export type {WorkspaceProfileDescriptionForm};
-export default INPUT_IDS;
diff --git a/src/types/form/WorkspaceRateAndUnitForm.ts b/src/types/form/WorkspaceRateAndUnitForm.ts
index 52f13d3a1415..566f10df9d9d 100644
--- a/src/types/form/WorkspaceRateAndUnitForm.ts
+++ b/src/types/form/WorkspaceRateAndUnitForm.ts
@@ -1,3 +1,4 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
@@ -5,10 +6,15 @@ const INPUT_IDS = {
UNIT: 'unit',
} as const;
-type WorkspaceRateAndUnitForm = Form<{
- [INPUT_IDS.RATE]: string;
- [INPUT_IDS.UNIT]: string;
-}>;
+type InputID = ValueOf;
+
+type WorkspaceRateAndUnitForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.RATE]: string;
+ [INPUT_IDS.UNIT]: string;
+ }
+>;
export type {WorkspaceRateAndUnitForm};
export default INPUT_IDS;
diff --git a/src/types/form/WorkspaceSettingsForm.ts b/src/types/form/WorkspaceSettingsForm.ts
index a3714583477a..8e2f580942c9 100644
--- a/src/types/form/WorkspaceSettingsForm.ts
+++ b/src/types/form/WorkspaceSettingsForm.ts
@@ -1,12 +1,18 @@
+import type {ValueOf} from 'type-fest';
import type Form from './Form';
const INPUT_IDS = {
NAME: 'name',
} as const;
-type WorkspaceSettingsForm = Form<{
- [INPUT_IDS.NAME]: string;
-}>;
+type InputID = ValueOf;
+
+type WorkspaceSettingsForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.NAME]: string;
+ }
+>;
export type {WorkspaceSettingsForm};
export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index f188dee6ab4f..aa21aeb274fc 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -9,7 +9,7 @@ export type {IKnowTeacherForm} from './IKnowTeacherForm';
export type {IntroSchoolPrincipalForm} from './IntroSchoolPrincipalForm';
export type {LegalNameForm} from './LegalNameForm';
export type {MoneyRequestAmountForm} from './MoneyRequestAmountForm';
-export type {MoneyRequestCreatedForm} from './MoneyRequestCreatedForm';
+export type {MoneyRequestDateForm} from './MoneyRequestDateForm';
export type {MoneyRequestDescriptionForm} from './MoneyRequestDescriptionForm';
export type {MoneyRequestMerchantForm} from './MoneyRequestMerchantForm';
export type {MoneyRequestHoldReasonForm} from './MoneyRequestHoldReasonForm';
@@ -33,6 +33,6 @@ export type {WaypointForm} from './WaypointForm';
export type {WorkspaceInviteMessageForm} from './WorkspaceInviteMessageForm';
export type {WorkspaceRateAndUnitForm} from './WorkspaceRateAndUnitForm';
export type {WorkspaceSettingsForm} from './WorkspaceSettingsForm';
+export type {ReportPhysicalCardForm} from './ReportPhysicalCardForm';
export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm';
-export type {WorkspaceProfileDescriptionForm} from './WorkspaceProfileDescriptionForm';
export type {default as Form} from './Form';
diff --git a/src/types/onyx/Currency.ts b/src/types/onyx/Currency.ts
index a4767403381f..b8d6f8dda88b 100644
--- a/src/types/onyx/Currency.ts
+++ b/src/types/onyx/Currency.ts
@@ -21,4 +21,7 @@ type Currency = {
cacheBurst?: number;
};
+type CurrencyList = Record;
+
export default Currency;
+export type {CurrencyList};
diff --git a/src/types/onyx/Network.ts b/src/types/onyx/Network.ts
index 32b084bbf2f7..173ca486b53c 100644
--- a/src/types/onyx/Network.ts
+++ b/src/types/onyx/Network.ts
@@ -1,6 +1,6 @@
type Network = {
/** Is the network currently offline or not */
- isOffline?: boolean;
+ isOffline: boolean;
/** Should the network be forced offline */
shouldForceOffline?: boolean;
diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts
index 173200ba681a..5b16d15bf5cc 100644
--- a/src/types/onyx/ReimbursementAccount.ts
+++ b/src/types/onyx/ReimbursementAccount.ts
@@ -8,37 +8,34 @@ type BankAccountStep = ValueOf;
type BankAccountSubStep = ValueOf;
-type ACHData = BeneficialOwnersStepProps &
- CompanyStepProps &
- RequestorStepProps &
- ACHContractStepProps & {
- /** Step of the setup flow that we are on. Determines which view is presented. */
- currentStep?: BankAccountStep;
+type ACHData = Partial & {
+ /** Step of the setup flow that we are on. Determines which view is presented. */
+ currentStep?: BankAccountStep;
- /** Optional subStep we would like the user to start back on */
- subStep?: BankAccountSubStep;
+ /** Optional subStep we would like the user to start back on */
+ subStep?: BankAccountSubStep;
- /** Bank account state */
- state?: string;
+ /** Bank account state */
+ state?: string;
- /** Bank account ID of the VBA that we are validating is required */
- bankAccountID?: number;
+ /** Bank account ID of the VBA that we are validating is required */
+ bankAccountID?: number;
- /** Bank account routing number */
- routingNumber?: string;
+ /** Bank account routing number */
+ routingNumber?: string;
- /** Bank account number */
- accountNumber?: string;
+ /** Bank account number */
+ accountNumber?: string;
- /** Bank account name */
- bankName?: BankName;
+ /** Bank account name */
+ bankName?: BankName;
- /** Bank account owner name */
- addressName?: string;
+ /** Bank account owner name */
+ addressName?: string;
- /** Policy ID of the workspace the bank account is being set up on */
- policyID?: string;
- };
+ /** Policy ID of the workspace the bank account is being set up on */
+ policyID?: string;
+};
type ReimbursementAccount = {
/** Whether we are loading the data via the API */
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 1b2ecdbdce12..c24e0871e0ed 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -9,6 +9,7 @@ import type {CardList} from './Card';
import type Log from './Console';
import type Credentials from './Credentials';
import type Currency from './Currency';
+import type {CurrencyList} from './Currency';
import type CustomStatusDraft from './CustomStatusDraft';
import type Download from './Download';
import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji';
@@ -81,6 +82,7 @@ export type {
CardList,
Credentials,
Currency,
+ CurrencyList,
CustomStatusDraft,
Download,
FrequentlyUsedEmoji,
diff --git a/tests/e2e/measure/writeTestStats.js b/tests/e2e/measure/writeTestStats.js
deleted file mode 100644
index 6de9dcc79db4..000000000000
--- a/tests/e2e/measure/writeTestStats.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import fs from 'fs';
-import config from '../config';
-
-/**
- * Writes the results of `getStats` to the {@link OUTPUT_FILE_CURRENT} file.
- *
- * @param {Object} stats
- * @param {string} stats.name - The name for the test, used in outputs.
- * @param {number} stats.mean - The average time for the test to run.
- * @param {number} stats.stdev - The standard deviation of the test.
- * @param {number} stats.entries - The data points
- * @param {number} stats.runs - The number of times the test was run.
- * @param {string} [path] - The path to write to. Defaults to {@link OUTPUT_FILE_CURRENT}.
- */
-export default (stats, path = config.OUTPUT_FILE_CURRENT) => {
- if (!stats.name || stats.mean == null || stats.stdev == null || !stats.entries || !stats.runs) {
- throw new Error(`Invalid stats object:\n${JSON.stringify(stats, null, 2)}\n\n`);
- }
-
- if (!fs.existsSync(path)) {
- fs.writeFileSync(path, '[]');
- }
-
- try {
- const content = JSON.parse(fs.readFileSync(path, 'utf8'));
- const line = `${JSON.stringify(content.concat([stats]))}\n`;
- fs.writeFileSync(path, line);
- } catch (error) {
- console.error(`Error writing ${path}`, error);
- throw error;
- }
-};
diff --git a/tests/e2e/utils/killApp.js b/tests/e2e/utils/killApp.ts
similarity index 68%
rename from tests/e2e/utils/killApp.js
rename to tests/e2e/utils/killApp.ts
index 32cc75d3ef8a..889f345cf222 100644
--- a/tests/e2e/utils/killApp.js
+++ b/tests/e2e/utils/killApp.ts
@@ -1,11 +1,13 @@
import config from '../config';
import execAsync from './execAsync';
-export default function (platform = 'android', packageName = config.APP_PACKAGE) {
+const killApp = function (platform = 'android', packageName = config.MAIN_APP_PACKAGE): Promise {
if (platform !== 'android') {
throw new Error(`killApp() missing implementation for platform: ${platform}`);
}
// Use adb to kill the app
return execAsync(`adb shell am force-stop ${packageName}`);
-}
+};
+
+export default killApp;
diff --git a/tests/unit/isStagingDeployLockedTest.js b/tests/unit/isStagingDeployLockedTest.ts
similarity index 100%
rename from tests/unit/isStagingDeployLockedTest.js
rename to tests/unit/isStagingDeployLockedTest.ts