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')} - > - this.toggleDeleteModal(true)} - /> - - )} - - - ); - } -} - -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')} + > + toggleDeleteModal(true)} + /> + + )} + + + ); +} + +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}