diff --git a/android/app/build.gradle b/android/app/build.gradle index 491c810cb350..0594d6afc211 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009003902 - versionName "9.0.39-2" + versionCode 1009003904 + versionName "9.0.39-4" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/android/build.gradle b/android/build.gradle index 85e547439cc1..fd3f9997612e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -29,7 +29,7 @@ buildscript { classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1") classpath("com.google.firebase:perf-plugin:1.4.1") // Fullstory integration - classpath ("com.fullstory:gradle-plugin-local:1.49.0") + classpath ("com.fullstory:gradle-plugin-local:1.52.0") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/assets/images/expensify-card.svg b/assets/images/expensify-card.svg index 52f55778b2bd..2989f5025ae4 100644 --- a/assets/images/expensify-card.svg +++ b/assets/images/expensify-card.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/babel.config.js b/babel.config.js index 3721edaa7afb..663eb29d5d2f 100644 --- a/babel.config.js +++ b/babel.config.js @@ -21,6 +21,7 @@ const defaultPlugins = [ '@babel/transform-runtime', '@babel/plugin-proposal-class-properties', + ['@babel/plugin-transform-object-rest-spread', {useBuiltIns: true, loose: true}], // We use `@babel/plugin-transform-class-properties` for transforming ReactNative libraries and do not use it for our own // source code transformation as we do not use class property assignment. diff --git a/docs/articles/new-expensify/expenses-&-payments/Connect-a-Personal-Bank-Account.md b/docs/articles/new-expensify/expenses-&-payments/Connect-a-Personal-Bank-Account.md index b8e66c937a0a..2d33552f3e3a 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Connect-a-Personal-Bank-Account.md +++ b/docs/articles/new-expensify/expenses-&-payments/Connect-a-Personal-Bank-Account.md @@ -8,7 +8,10 @@ Connecting a personal bank account to Expensify allows you to get reimbursed for 1. Click your profile image or icon in the bottom left menu. 2. Click **Wallet**. 3. Click **Add Bank Account**. -4. Click **Continue** (this will open a new window and redirect you to Plaid). + + ![Wallet Settings, showing where to connect a bank account](https://help.expensify.com/assets/images/addbankaccount_01.png){:width="100%"} + +5. Click **Continue** (this will open a new window and redirect you to Plaid). {% include info.html %} Plaid is an encrypted third-party financial data platform that Expensify uses to securely verify your banking information. @@ -19,4 +22,6 @@ Plaid is an encrypted third-party financial data platform that Expensify uses to 7. Choose which account you want to connect to Expensify. 8. Click **Save & continue**. + ![Wallet Settings, showing bank account connected](https://help.expensify.com/assets/images/addbankaccount_03.png){:width="100%"} + Once connected, all payments and reimbursements will be deposited directly into that bank account. diff --git a/docs/assets/images/invoices_01.png b/docs/assets/images/invoices_01.png new file mode 100644 index 000000000000..fc6d5587bb03 Binary files /dev/null and b/docs/assets/images/invoices_01.png differ diff --git a/docs/assets/images/invoices_02.png b/docs/assets/images/invoices_02.png new file mode 100644 index 000000000000..29038987c18a Binary files /dev/null and b/docs/assets/images/invoices_02.png differ diff --git a/docs/assets/images/invoices_03.png b/docs/assets/images/invoices_03.png new file mode 100644 index 000000000000..fd78aa731784 Binary files /dev/null and b/docs/assets/images/invoices_03.png differ diff --git a/docs/assets/images/invoices_04.png b/docs/assets/images/invoices_04.png new file mode 100644 index 000000000000..d2e301a9d1a5 Binary files /dev/null and b/docs/assets/images/invoices_04.png differ diff --git a/docs/assets/images/invoices_05.png b/docs/assets/images/invoices_05.png new file mode 100644 index 000000000000..8eae5efaa9df Binary files /dev/null and b/docs/assets/images/invoices_05.png differ diff --git a/docs/assets/images/invoices_06.png b/docs/assets/images/invoices_06.png new file mode 100644 index 000000000000..2858227891eb Binary files /dev/null and b/docs/assets/images/invoices_06.png differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7bed47c032af..5f6051c85745 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.39.2 + 9.0.39.4 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 6e01c0d2f912..c29a15f26438 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.39.2 + 9.0.39.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 74158e9d574a..2b43cf12b38a 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.39 CFBundleVersion - 9.0.39.2 + 9.0.39.4 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile b/ios/Podfile index 2ed1752abf4f..e807089c26b9 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -121,4 +121,4 @@ target 'NotificationServiceExtension' do pod 'AirshipServiceExtension' end -pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz' \ No newline at end of file +pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz' \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0f1a42791d1e..a801a7c4de1c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -143,8 +143,8 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - fmt (9.1.0) - - FullStory (1.49.0) - - fullstory_react-native (1.4.2): + - FullStory (1.52.0) + - fullstory_react-native (1.7.1): - DoubleConversion - FullStory (~> 1.14) - glog @@ -2700,7 +2700,7 @@ DEPENDENCIES: - ExpoModulesCore (from `../node_modules/expo-modules-core`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz\"}`)" + - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz\"}`)" - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)" - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) @@ -2874,7 +2874,7 @@ EXTERNAL SOURCES: fmt: :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" FullStory: - :http: https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz + :http: https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz fullstory_react-native: :path: "../node_modules/@fullstory/react-native" glog: @@ -3089,7 +3089,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: FullStory: - :http: https://ios-releases.fullstory.com/fullstory-1.49.0-xcframework.tar.gz + :http: https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz SPEC CHECKSUMS: Airship: bb32ff2c5a811352da074480357d9f02dbb8f327 @@ -3116,8 +3116,8 @@ SPEC CHECKSUMS: FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 - FullStory: c95f74445f871bc344cdc4a4e4ece61b5554e55d - fullstory_react-native: 1818ee93dc38801665f26869f7ad68abb698a89a + FullStory: c8a10b2358c0d33c57be84d16e4c440b0434b33d + fullstory_react-native: 44dc2c85a6316df2713e6cb0048ce5719c3b0bab glog: 69ef571f3de08433d766d614c73a9838a06bf7eb GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a @@ -3248,6 +3248,6 @@ SPEC CHECKSUMS: VisionCamera: c6c8aa4b028501fc87644550fbc35a537d4da3fb Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8 -PODFILE CHECKSUM: e479ec84cb53e5fd463486d71dfee91708d3fd9a +PODFILE CHECKSUM: a07e55247056ec5d84d1af31d694506efff3cfe2 COCOAPODS: 1.15.2 diff --git a/package-lock.json b/package-lock.json index 3c27c44a3bd7..c1896b581645 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.39-2", + "version": "9.0.39-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.39-2", + "version": "9.0.39-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6f4980f04ee0..577eac08c6d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.39-2", + "version": "9.0.39-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index c8cbe72bc39e..9ee9ec4d9147 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2452,6 +2452,7 @@ const CONST = { }, }, EXPENSIFY_CARD: { + NAME: 'expensifyCard', BANK: 'Expensify Card', FRAUD_TYPES: { DOMAIN: 'domain', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 64ad1f660c12..7fcb675dc191 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -207,8 +207,8 @@ const ONYXKEYS = { /** The NVP containing all information related to educational tooltip in workspace chat */ NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip', - /** Whether to hide save search rename tooltip */ - NVP_SHOULD_HIDE_SAVED_SEARCH_RENAME_TOOLTIP: 'nvp_should_hide_saved_search_rename_tooltip', + /** Whether to show save search rename tooltip */ + SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP: 'shouldShowSavedSearchRenameTooltip', /** Whether to hide gbr tooltip */ NVP_SHOULD_HIDE_GBR_TOOLTIP: 'nvp_should_hide_gbr_tooltip', @@ -983,7 +983,7 @@ type OnyxValuesMapping = { [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; [ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet; [ONYXKEYS.LAST_ROUTE]: string; - [ONYXKEYS.NVP_SHOULD_HIDE_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; + [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9b351bd31899..c0ec944b71e1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -878,6 +878,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/members/:accountID', getRoute: (policyID: string, accountID: number) => `settings/workspaces/${policyID}/members/${accountID}` as const, }, + WORKSPACE_MEMBER_NEW_CARD: { + route: 'settings/workspaces/:policyID/members/:accountID/new-card', + getRoute: (policyID: string, accountID: number) => `settings/workspaces/${policyID}/members/${accountID}/new-card` as const, + }, WORKSPACE_MEMBER_ROLE_SELECTION: { route: 'settings/workspaces/:policyID/members/:accountID/role-selection', getRoute: (policyID: string, accountID: number) => `settings/workspaces/${policyID}/members/${accountID}/role-selection` as const, @@ -960,10 +964,6 @@ const ROUTES = { route: 'settings/workspaces/:policyID/company-cards/select-feed', getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards/select-feed` as const, }, - WORKSPACE_EXPENSIFY_CARD: { - route: 'settings/workspaces/:policyID/expensify-card', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const, - }, WORKSPACE_COMPANY_CARDS_ASSIGN_CARD: { route: 'settings/workspaces/:policyID/company-cards/:feed/assign-card', getRoute: (policyID: string, feed: string) => `settings/workspaces/${policyID}/company-cards/${feed}/assign-card` as const, @@ -980,6 +980,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/company-cards/:bank/:cardID/edit/export', getRoute: (policyID: string, cardID: string, bank: string) => `settings/workspaces/${policyID}/company-cards/${bank}/${cardID}/edit/export` as const, }, + WORKSPACE_EXPENSIFY_CARD: { + route: 'settings/workspaces/:policyID/expensify-card', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const, + }, WORKSPACE_EXPENSIFY_CARD_DETAILS: { route: 'settings/workspaces/:policyID/expensify-card/:cardID', getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/${cardID}`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index da92d2b0940d..920bd48dd42e 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -463,6 +463,7 @@ const SCREENS = { CATEGORIES_IMPORTED: 'Categories_Imported', MORE_FEATURES: 'Workspace_More_Features', MEMBER_DETAILS: 'Workspace_Member_Details', + MEMBER_NEW_CARD: 'Workspace_Member_NewCard', OWNER_CHANGE_CHECK: 'Workspace_Owner_Change_Check', OWNER_CHANGE_SUCCESS: 'Workspace_Owner_Change_Success', OWNER_CHANGE_ERROR: 'Workspace_Owner_Change_Error', diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx index a51a7d7456e1..14f14aee8c73 100644 --- a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx +++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx @@ -6,7 +6,6 @@ import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; import TOP_TAB_SCREENS from '@components/FocusTrap/TOP_TAB_SCREENS'; import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import CONST from '@src/CONST'; import type FocusTrapProps from './FocusTrapProps'; @@ -42,30 +41,15 @@ function FocusTrapForScreen({children, focusTrapSettings}: FocusTrapProps) { paused={!isFocused} containerElements={focusTrapSettings?.containerElements?.length ? focusTrapSettings.containerElements : undefined} focusTrapOptions={{ + onActivate: () => { + (document?.activeElement as HTMLElement)?.blur(); + }, trapStack: sharedTrapStack, allowOutsideClick: true, fallbackFocus: document.body, delayInitialFocus: CONST.ANIMATED_TRANSITION, - initialFocus: (focusTrapContainers) => { - if (!canFocusInputOnScreenFocus()) { - return false; - } - - const isFocusedElementInsideContainer = !!focusTrapContainers?.some((container) => container.contains(document.activeElement)); - const hasButtonWithEnterListener = !!focusTrapContainers?.some( - (container) => !!container.querySelector(`button[data-listener="${CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey}"]`), - ); - if (isFocusedElementInsideContainer || hasButtonWithEnterListener) { - return false; - } - return undefined; - }, - setReturnFocus: (element) => { - if (document.activeElement && document.activeElement !== document.body) { - return false; - } - return element; - }, + initialFocus: false, + setReturnFocus: false, ...(focusTrapSettings?.focusTrapOptions ?? {}), }} > diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index f1e715bface8..eb04ad5540eb 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -7,6 +7,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PinButton from '@components/PinButton'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import SearchButton from '@components/Search/SearchRouter/SearchButton'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import Tooltip from '@components/Tooltip'; import useKeyboardState from '@hooks/useKeyboardState'; @@ -60,6 +61,7 @@ function HeaderWithBackButton({ shouldOverlayDots = false, shouldOverlay = false, shouldNavigateToTopMostReport = false, + shouldDisplaySearchRouter = false, progressBarPercentage, style, }: HeaderWithBackButtonProps) { @@ -261,6 +263,7 @@ function HeaderWithBackButton({ )} + {shouldDisplaySearchRouter && } diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index c55a7bddc80c..22885b6ceac5 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -128,6 +128,9 @@ type HeaderWithBackButtonProps = Partial & { /** Whether we should overlay the 3 dots menu */ shouldOverlayDots?: boolean; + /** Whether we should display the button that opens new SearchRouter */ + shouldDisplaySearchRouter?: boolean; + /** 0 - 100 number indicating current progress of the progress bar */ progressBarPercentage?: number; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 4fc92d619e68..d5570cb18872 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -41,7 +41,7 @@ import SettlementButton from './SettlementButton'; type MoneyReportHeaderProps = { /** The report currently being looked at */ - report: OnyxTypes.Report; + report: OnyxEntry; /** The policy tied to the expense report */ policy: OnyxEntry; @@ -61,8 +61,8 @@ type MoneyReportHeaderProps = { }; function MoneyReportHeader({policy, report: moneyRequestReport, transactionThreadReportID, reportActions, shouldUseNarrowLayout = false, onBackButtonPress}: MoneyReportHeaderProps) { - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport.chatReportID}`); - const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport.reportID}`); + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID ?? '-1'}`); + const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID ?? '-1'}`); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); const [session] = useOnyx(ONYXKEYS.SESSION); const requestParentReportAction = useMemo(() => { @@ -109,10 +109,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t)); const transactionIDs = allTransactions.map((t) => t.transactionID); const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']); - const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID); + const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport?.reportID ?? ''); const isPayAtEndExpense = TransactionUtils.isPayAtEndExpense(transaction); const isArchivedReport = ReportUtils.isArchivedRoomWithID(moneyRequestReport?.reportID); - const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport.reportID}`, {selector: ReportUtils.getArchiveReason}); + const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID ?? '-1'}`, {selector: ReportUtils.getArchiveReason}); const shouldShowPayButton = useMemo( () => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined), @@ -123,7 +123,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); - const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !hasAllPendingRTERViolations; + const shouldShowSubmitButton = !!moneyRequestReport && isDraft && reimbursableSpend !== 0 && !hasAllPendingRTERViolations; const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(moneyRequestReport); @@ -137,9 +137,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || hasAllPendingRTERViolations || shouldShowExportIntegrationButton; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); - const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); + const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy); - const isAnyTransactionOnHold = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID); + const isAnyTransactionOnHold = ReportUtils.hasHeldExpenses(moneyRequestReport?.reportID); const displayedAmount = isAnyTransactionOnHold && canAllowSettlement ? nonHeldAmount : formattedAmount; const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout); const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails(); @@ -285,6 +285,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea report={moneyRequestReport} policy={policy} shouldShowBackButton={shouldUseNarrowLayout} + shouldDisplaySearchRouter onBackButtonPress={onBackButtonPress} // Shows border if no buttons or banners are showing below the header shouldShowBorderBottom={!isMoreContentShown} @@ -292,9 +293,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea {shouldShowSettlementButton && !shouldUseNarrowLayout && ( {shouldShowSettlementButton && shouldUseNarrowLayout && ( ({ + anchor: null, + report: undefined, + reportNameValuePairs: undefined, + action: undefined, + checkIfContextMenuActive: () => {}, + isDisabled: true, + }), + [], + ); + const mentionReportContextValue = useMemo(() => ({currentReportID: reportID}), [reportID]); // An intermediate structure that helps us classify the fields as "primary" and "supplementary". @@ -300,29 +313,31 @@ function MoneyRequestConfirmationListFooter({ }, { item: ( - - + { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID), - ); - }} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - numberOfLinesTitle={2} - /> - + value={mentionReportContextValue} + > + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams(), reportActionID), + ); + }} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!isReadOnly} + numberOfLinesTitle={2} + /> + + ), shouldShow: true, isSupplementary: false, diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 0e0633042a7d..ab7004ce4d17 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -27,7 +27,7 @@ import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; type MoneyRequestHeaderProps = { /** The report currently being looked at */ - report: Report; + report: OnyxEntry; /** The policy which the report is tied to */ policy: OnyxEntry; @@ -43,7 +43,7 @@ type MoneyRequestHeaderProps = { }; function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrowLayout = false, onBackButtonPress}: MoneyRequestHeaderProps) { - const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? '-1'}`); const [transaction] = useOnyx( `${ONYXKEYS.COLLECTION.TRANSACTION}${ ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? -1 : -1 @@ -58,12 +58,13 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); const isOnHold = TransactionUtils.isOnHold(transaction); const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID ?? ''); + const reportID = report?.reportID; const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']); const markAsCash = useCallback(() => { - TransactionActions.markAsCash(transaction?.transactionID ?? '-1', report.reportID); - }, [report.reportID, transaction?.transactionID]); + TransactionActions.markAsCash(transaction?.transactionID ?? '-1', reportID ?? ''); + }, [reportID, transaction?.transactionID]); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); @@ -129,10 +130,12 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow shouldShowPinButton={false} report={{ ...report, + reportID: reportID ?? '', ownerAccountID: parentReport?.ownerAccountID, }} policy={policy} shouldShowBackButton={shouldUseNarrowLayout} + shouldDisplaySearchRouter onBackButtonPress={onBackButtonPress} > {hasAllPendingRTERViolations && !shouldUseNarrowLayout && ( @@ -149,7 +152,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow text={translate('iou.reviewDuplicates')} style={[styles.p0, styles.ml2]} onPress={() => { - Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(report.reportID)); + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID ?? '')); }} /> )} @@ -171,7 +174,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, shouldUseNarrow text={translate('iou.reviewDuplicates')} style={[styles.w100, styles.pr0]} onPress={() => { - Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(report.reportID)); + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID ?? '')); }} /> diff --git a/src/components/PercentageForm.tsx b/src/components/PercentageForm.tsx index 8d9ca950f49c..76e3b19891c4 100644 --- a/src/components/PercentageForm.tsx +++ b/src/components/PercentageForm.tsx @@ -1,6 +1,5 @@ import type {ForwardedRef} from 'react'; -import React, {forwardRef, useCallback, useMemo, useRef, useState} from 'react'; -import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import React, {forwardRef, useCallback, useMemo, useRef} from 'react'; import useLocalize from '@hooks/useLocalize'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import CONST from '@src/CONST'; @@ -21,14 +20,6 @@ type PercentageFormProps = { label?: string; }; -/** - * Returns the new selection object based on the updated amount's length - */ -const getNewSelection = (oldSelection: {start: number; end: number}, prevLength: number, newLength: number) => { - const cursorPosition = oldSelection.end + (newLength - prevLength); - return {start: cursorPosition, end: cursorPosition}; -}; - function PercentageForm({value: amount, errorText, onInputChange, label, ...rest}: PercentageFormProps, forwardedRef: ForwardedRef) { const {toLocaleDigit, numberFormat} = useLocalize(); @@ -36,13 +27,6 @@ function PercentageForm({value: amount, errorText, onInputChange, label, ...rest const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]); - const [selection, setSelection] = useState({ - start: currentAmount.length, - end: currentAmount.length, - }); - - const forwardDeletePressedRef = useRef(false); - /** * Sets the selection and the amount accordingly to the value passed to the input * @param newAmount - Changed amount from user input @@ -55,16 +39,13 @@ function PercentageForm({value: amount, errorText, onInputChange, label, ...rest // Use a shallow copy of selection to trigger setSelection // More info: https://github.com/Expensify/App/issues/16385 if (!MoneyRequestUtils.validatePercentage(newAmountWithoutSpaces)) { - setSelection((prevSelection) => ({...prevSelection})); return; } const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces); - const isForwardDelete = currentAmount.length > strippedAmount.length && forwardDeletePressedRef.current; - setSelection(getNewSelection(selection, isForwardDelete ? strippedAmount.length : currentAmount.length, strippedAmount.length)); onInputChange?.(strippedAmount); }, - [currentAmount, onInputChange, selection], + [onInputChange], ); const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit); @@ -84,10 +65,6 @@ function PercentageForm({value: amount, errorText, onInputChange, label, ...rest } textInput.current = ref; }} - selection={selection} - onSelectionChange={(e: NativeSyntheticEvent) => { - setSelection(e.nativeEvent.selection); - }} suffixCharacter="%" keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 4a63714b6157..8cbbd1199b33 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -25,7 +25,7 @@ type ProcessMoneyReportHoldMenuProps = { isVisible: boolean; /** The report currently being looked at */ - moneyRequestReport: OnyxTypes.Report; + moneyRequestReport: OnyxEntry; /** Not held amount of expense report */ nonHeldAmount?: string; @@ -62,8 +62,8 @@ function ProcessMoneyReportHoldMenu({ const onSubmit = (full: boolean) => { if (isApprove) { IOU.approveMoneyRequest(moneyRequestReport, full); - if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '-1', moneyRequestReport.reportID)) { - Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(moneyRequestReport.reportID)); + if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '-1', moneyRequestReport?.reportID ?? '')) { + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(moneyRequestReport?.reportID ?? '')); } } else if (chatReport && paymentType) { IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport, full); diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 6ab2db05c49f..8546aa8165c9 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -1,7 +1,7 @@ import {Str} from 'expensify-common'; import React, {useMemo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; -import {View} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import Icon from '@components/Icon'; @@ -12,6 +12,7 @@ import SpacerView from '@components/SpacerView'; import Text from '@components/Text'; import UnreadActionIndicator from '@components/UnreadActionIndicator'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -47,6 +48,7 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + const {isOffline} = useNetwork(); const isSettled = ReportUtils.isSettled(report.reportID); const isTotalUpdated = ReportUtils.hasUpdatedTotal(report, policy); @@ -160,12 +162,20 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo /> )} - - {formattedTotalAmount} - + {!isTotalUpdated && !isOffline ? ( + + ) : ( + + {formattedTotalAmount} + + )} )} diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 7a6e4942b178..053ad0c2c63e 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -1,12 +1,13 @@ import {Str} from 'expensify-common'; import React from 'react'; import {View} from 'react-native'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import Avatar from '@components/Avatar'; import Checkbox from '@components/Checkbox'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; +import {usePersonalDetails} from '@components/OnyxProvider'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import RenderHTML from '@components/RenderHTML'; import {showContextMenuForReport} from '@components/ShowContextMenuContext'; @@ -27,45 +28,37 @@ import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Report, ReportAction} from '@src/types/onyx'; +import type {ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -type TaskPreviewOnyxProps = { - /* Onyx Props */ +type TaskPreviewProps = WithCurrentUserPersonalDetailsProps & { + /** The ID of the associated policy */ + // eslint-disable-next-line react/no-unused-prop-types + policyID: string; + /** The ID of the associated taskReport */ + taskReportID: string; - /* current report of TaskPreview */ - taskReport: OnyxEntry; -}; - -type TaskPreviewProps = WithCurrentUserPersonalDetailsProps & - TaskPreviewOnyxProps & { - /** The ID of the associated policy */ - // eslint-disable-next-line react/no-unused-prop-types - policyID: string; - /** The ID of the associated taskReport */ - taskReportID: string; - - /** Whether the task preview is hovered so we can modify its style */ - isHovered: boolean; + /** Whether the task preview is hovered so we can modify its style */ + isHovered: boolean; - /** The linked reportAction */ - action: OnyxEntry; + /** The linked reportAction */ + action: OnyxEntry; - /** The chat report associated with taskReport */ - chatReportID: string; + /** The chat report associated with taskReport */ + chatReportID: string; - /** Popover context menu anchor, used for showing context menu */ - contextMenuAnchor: ContextMenuAnchor; + /** Popover context menu anchor, used for showing context menu */ + contextMenuAnchor: ContextMenuAnchor; - /** Callback for updating context menu active state, used for showing context menu */ - checkIfContextMenuActive: () => void; - }; + /** Callback for updating context menu active state, used for showing context menu */ + checkIfContextMenuActive: () => void; +}; -function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatReportID, checkIfContextMenuActive, currentUserPersonalDetails, isHovered = false}: TaskPreviewProps) { +function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, checkIfContextMenuActive, currentUserPersonalDetails, isHovered = false}: TaskPreviewProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - + const [taskReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`); // The reportAction might not contain details regarding the taskReport // Only the direct parent reportAction will contain details about the taskReport // Other linked reportActions will only contain the taskReportID and we will grab the details from there @@ -74,7 +67,8 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitle(taskReportID, action?.childReportName ?? '')); const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? -1; - const [avatar] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: (personalDetails) => personalDetails?.[taskAssigneeAccountID]?.avatar}); + const personalDetails = usePersonalDetails(); + const avatar = personalDetails?.[taskAssigneeAccountID]?.avatar ?? Expensicons.FallbackAvatar; const htmlForTaskPreview = `${taskTitle}`; const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action); @@ -134,10 +128,4 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR TaskPreview.displayName = 'TaskPreview'; -export default withCurrentUserPersonalDetails( - withOnyx({ - taskReport: { - key: ({taskReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`, - }, - })(TaskPreview), -); +export default withCurrentUserPersonalDetails(TaskPreview); diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 73829989409c..704f72055410 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -33,6 +33,7 @@ import type {SearchDataTypes, SearchReport} from '@src/types/onyx/SearchResults' import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type IconAsset from '@src/types/utils/IconAsset'; import {useSearchContext} from './SearchContext'; +import SearchButton from './SearchRouter/SearchButton'; import type {SearchQueryJSON} from './types'; type HeaderWrapperProps = Pick & { @@ -295,11 +296,13 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa } const onPress = () => { - const values = SearchUtils.getFiltersFormValues(queryJSON); + const values = SearchUtils.buildFilterFormValuesFromQuery(queryJSON); SearchActions.updateAdvancedFilters(values); Navigation.navigate(ROUTES.SEARCH_ADVANCED_FILTERS); }; + const displaySearchRouter = SearchUtils.isCannedSearchQuery(queryJSON); + return ( - {headerButtonsOptions.length > 0 ? ( - null} - shouldAlwaysShowDropdownMenu - pressOnEnter - buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} - customText={translate('workspace.common.selected', {selectedNumber: selectedTransactionsKeys.length})} - options={headerButtonsOptions} - isSplitButton={false} - shouldUseStyleUtilityForAnchorPosition - /> - ) : ( -