From 0332039ede4dc68078cf71abd0f76d804243e98b Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 1 Feb 2024 16:01:24 -0500 Subject: [PATCH 1/9] FIxing workspace color and navigating back from the workspace details * attempting backTo param * Update ROUTES.ts --- ...MoneyTemporaryForRefactorRequestConfirmationList.js | 8 ++++---- src/pages/ReportDetailsPage.js | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 2aff0444a59e..4eb4b279b1a9 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -439,7 +439,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ } else { const formattedSelectedParticipants = _.map(selectedParticipants, (participant) => ({ ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), + isDisabled: !participant.isPolicyExpenseChat && ReportUtils.isOptimisticPersonalDetail(participant.accountID), })); sections.push({ title: translate('common.to'), @@ -496,12 +496,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ * @param {Object} option */ const navigateToReportOrUserDetail = (option) => { - if (option.accountID) { - const activeRoute = Navigation.getActiveRouteWithoutParams(); + const activeRoute = Navigation.getActiveRouteWithoutParams(); + if (option.accountID) { Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); } else if (option.reportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID)); + Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(option.reportID, activeRoute)); } }; diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 513ccbbe307c..1427549a93b7 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -46,6 +46,9 @@ const propTypes = { params: PropTypes.shape({ /** Report ID passed via route r/:reportID/details */ reportID: PropTypes.string, + + /** Back To passed via route r/:reportID/details?backTo= */ + backTo: PropTypes.string, }), }).isRequired, @@ -182,11 +185,8 @@ function ReportDetailsPage(props) { { - Navigation.goBack(); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.report.reportID)); - }} + shouldNavigateToTopMostReport={!props.route.params.backTo} + onBackButtonPress={() => {Navigation.goBack(props.route.params.backTo, !props.route.params.backTo)}} /> From 479526903c491553ff416e7e19138f1386955b2c Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 2 Feb 2024 02:58:12 +0100 Subject: [PATCH 2/9] fmt: optimize conditions and lint --- src/pages/ReportDetailsPage.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 1427549a93b7..1b63c19fd7be 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -179,14 +179,17 @@ function ReportDetailsPage(props) { shouldUseFullTitle /> ) : null; + const backTo = props.route.params.backTo; return ( {Navigation.goBack(props.route.params.backTo, !props.route.params.backTo)}} + shouldNavigateToTopMostReport={!backTo} + onBackButtonPress={() => { + Navigation.goBack(backTo, !!backTo); + }} /> From bd862250b47b918a299b09f1ae152174c2a7ec58 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 2 Feb 2024 21:24:42 +0100 Subject: [PATCH 3/9] fixing onBackButton callback function and added backTo parameter in the REPORT_WITH_ID_DETAILS route --- src/ROUTES.ts | 2 +- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- src/pages/ReportDetailsPage.js | 9 +++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a84dc9c8f9ae..638739cb1843 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -196,7 +196,7 @@ const ROUTES = { }, REPORT_WITH_ID_DETAILS: { route: 'r/:reportID/details', - getRoute: (reportID: string) => `r/${reportID}/details` as const, + getRoute: (reportID: string, backTo: string) => getUrlWithBackToParam(`r/${reportID}/details`, backTo), }, REPORT_SETTINGS: { route: 'r/:reportID/settings', diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 4eb4b279b1a9..e09c502377cf 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -501,7 +501,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ if (option.accountID) { Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); } else if (option.reportID) { - Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(option.reportID, activeRoute)); + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID, activeRoute)); } }; diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 1b63c19fd7be..0066c407e52f 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -47,7 +47,7 @@ const propTypes = { /** Report ID passed via route r/:reportID/details */ reportID: PropTypes.string, - /** Back To passed via route r/:reportID/details?backTo= */ + /** BackTo passed via route r/:reportID/details?backTo= */ backTo: PropTypes.string, }), }).isRequired, @@ -179,17 +179,14 @@ function ReportDetailsPage(props) { shouldUseFullTitle /> ) : null; - const backTo = props.route.params.backTo; return ( { - Navigation.goBack(backTo, !!backTo); - }} + shouldNavigateToTopMostReport={!props.route.params.backTo} + onBackButtonPress={() => Navigation.goBack(props.route.params.backTo)} /> From a60cb2e16375bc30c99a4806b34747651097615e Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sun, 4 Feb 2024 19:47:58 +0100 Subject: [PATCH 4/9] lint --- src/ROUTES.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 638739cb1843..aa6d9f458983 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -196,7 +196,7 @@ const ROUTES = { }, REPORT_WITH_ID_DETAILS: { route: 'r/:reportID/details', - getRoute: (reportID: string, backTo: string) => getUrlWithBackToParam(`r/${reportID}/details`, backTo), + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details`, backTo), }, REPORT_SETTINGS: { route: 'r/:reportID/settings', From a64ab1901c7b49fdbc96fc3ba9de7eebc3fa4fea Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 9 Feb 2024 19:58:01 +0100 Subject: [PATCH 5/9] merge upstream main --- .github/scripts/createHelpRedirects.sh | 2 +- .github/workflows/e2ePerformanceTests.yml | 48 +- .github/workflows/failureNotifier.yml | 96 + .github/workflows/preDeploy.yml | 1 + .storybook/webpack.config.js | 2 +- __mocks__/@react-native-community/netinfo.js | 19 - __mocks__/@react-native-community/netinfo.ts | 31 + .../push-notification-ios.js | 5 - .../@react-native-firebase/crashlytics.js | 7 - .../@react-native-firebase/crashlytics.ts | 15 + __mocks__/@react-native-firebase/perf.ts | 5 + .../native/{index.js => index.ts} | 2 +- __mocks__/push-notification-ios.js | 5 - __mocks__/react-freeze.js | 6 - __mocks__/react-freeze.ts | 8 + __mocks__/react-native-blob-util.js | 1 - __mocks__/react-native-blob-util.ts | 5 + __mocks__/react-native-dev-menu.js | 3 - __mocks__/react-native-dev-menu.ts | 11 + __mocks__/react-native-device-info.js | 3 - __mocks__/react-native-device-info.ts | 6 + __mocks__/react-native-localize.js | 3 - __mocks__/react-native-localize.ts | 3 + __mocks__/react-native-pdf.js | 4 - __mocks__/react-native-reanimated.js | 7 - android/app/build.gradle | 4 +- .../CustomAirshipExtender.java | 3 - .../CustomNotificationListener.java | 53 - .../CustomNotificationProvider.java | 135 +- .../NotificationCache.java | 193 - android/app/src/main/res/values/colors.xml | 1 + assets/images/google-meet.svg | 1 - assets/images/olddot-wireframe.svg | 3422 +++++++++++++++++ .../simple-illustration__gears.svg | 101 + .../simple-illustration__lockclosed.svg | 17 + .../simple-illustration__palmtree.svg | 15 + .../simple-illustration__profile.svg | 6 + .../simple-illustration__qr-code.svg | 4 + assets/images/zoom-icon.svg | 1 - babel.config.js | 5 +- contributingGuides/NAVIGATION.md | 2 +- docs/_data/_routes.yml | 4 +- .../getting-started/Individual-Users.md | 43 - .../Join-your-company's-workspace.md | 3 +- .../getting-started/Using-The-App.md | 65 - .../Account-Details.md | 0 .../Close-Account.md | 0 .../{account-settings => settings}/Copilot.md | 0 .../Merge-Accounts.md | 0 .../Notification-Troubleshooting.md | 0 .../Preferences.md | 0 .../{account-settings => settings}/index.html | 0 docs/redirects.csv | 9 +- ios/NewApp_AdHoc.mobileprovision.gpg | Bin 11362 -> 11432 bytes ...c_Notification_Service.mobileprovision.gpg | Bin 10923 -> 11024 bytes ios/NewExpensify/Info.plist | 4 +- ios/NewExpensify/RCTBootSplash.h | 1 + ios/NewExpensify/RCTBootSplash.m | 9 + ios/NewExpensifyTests/Info.plist | 4 +- ios/NotificationServiceExtension/Info.plist | 4 +- ios/Podfile.lock | 6 +- jest.config.js | 4 +- jest/{setup.js => setup.ts} | 5 +- jest/{setupAfterEnv.js => setupAfterEnv.ts} | 0 ...{setupMockImages.js => setupMockImages.ts} | 8 +- package-lock.json | 207 +- package.json | 29 +- ...react-native-image-manipulator+1.0.5.patch | 4 +- ...lized-lists+0.73.4+001+onStartReched.patch | 32 + ...native-camera-roll+camera-roll+5.4.0.patch | 15 + ...ommunity+cli-platform-android+12.3.0.patch | 52 + ...ve-community+cli-platform-ios+12.3.0.patch | 52 + ...act-native-firebase+analytics+12.9.3.patch | 18 + .../@react-native-firebase+app+12.9.3.patch | 25 + ...t-native-firebase+crashlytics+12.9.3.patch | 17 + .../@react-native-firebase+perf+12.9.3.patch | 17 + patches/expo+50.0.4.patch | 10 + patches/expo-modules-autolinking+1.10.2.patch | 40 + patches/expo-modules-core+1.11.8.patch | 16 + patches/react-native-pdf+6.7.3.patch | 23 + patches/react-native-reanimated+3.6.1.patch | 17 + ...era+2.16.5+001+fix-boost-dependency.patch} | 13 +- ...> react-native-vision-camera+2.16.5.patch} | 0 src/{App.js => App.tsx} | 74 +- src/CONST.ts | 30 +- src/ONYXKEYS.ts | 22 +- src/ROUTES.ts | 58 +- src/SCREENS.ts | 26 +- src/components/AddressForm.js | 4 +- src/components/AddressSearch/types.ts | 3 +- src/components/AmountTextInput.tsx | 1 + src/components/AttachmentModal.tsx | 4 +- src/components/AvatarWithImagePicker.js | 2 +- .../BlockingViews/FullPageNotFoundView.tsx | 3 +- src/components/Breadcrumbs.tsx | 2 +- src/components/ButtonWithDropdownMenu.tsx | 16 +- src/components/CheckboxWithLabel.tsx | 3 +- src/components/CommunicationsLink.js | 51 - src/components/CommunicationsLink.tsx | 42 + src/components/CountrySelector.tsx | 5 +- src/components/CurrencySymbolButton.tsx | 12 +- .../CustomStatusBarAndBackground/index.tsx | 2 +- .../DisplayNames/DisplayNamesWithTooltip.tsx | 6 +- .../DisplayNamesWithoutTooltip.tsx | 6 +- src/components/DisplayNames/index.native.tsx | 3 +- src/components/DisplayNames/index.tsx | 17 +- src/components/DisplayNames/types.ts | 5 +- src/components/DistanceEReceipt.js | 8 +- src/components/DistanceRequest/index.js | 12 +- src/components/DotIndicatorMessage.tsx | 6 +- src/components/EReceipt.tsx | 5 +- src/components/EReceiptThumbnail.tsx | 33 +- src/components/EmojiPicker/EmojiPicker.js | 6 +- .../EmojiPicker/EmojiPickerButtonDropdown.js | 19 +- .../EmojiPicker/EmojiPickerMenu/index.js | 11 +- .../EmojiPickerMenu/index.native.js | 7 +- src/components/FeatureList.js | 8 +- src/components/Form/FormProvider.tsx | 13 +- src/components/Form/FormWrapper.tsx | 11 +- src/components/Form/InputWrapper.tsx | 59 +- src/components/Form/types.ts | 17 +- src/components/FormAlertWithSubmitButton.tsx | 3 +- src/components/FormAlertWrapper.tsx | 5 +- src/components/FormSubmit/index.native.tsx | 18 - src/components/FormSubmit/index.tsx | 86 - src/components/FormSubmit/types.ts | 13 - .../BaseHTMLEngineProvider.tsx | 1 - .../{AnchorRenderer.js => AnchorRenderer.tsx} | 29 +- .../{CodeRenderer.js => CodeRenderer.tsx} | 30 +- .../{EditedRenderer.js => EditedRenderer.tsx} | 21 +- .../{ImageRenderer.js => ImageRenderer.tsx} | 39 +- ...ereRenderer.js => MentionHereRenderer.tsx} | 16 +- .../HTMLRenderers/MentionUserRenderer.js | 120 - .../HTMLRenderers/MentionUserRenderer.tsx | 98 + .../HTMLRenderers/NextStepEmailRenderer.tsx | 18 +- .../{PreRenderer.js => PreRenderer.tsx} | 49 +- .../HTMLRenderers/htmlRendererPropTypes.js | 8 - .../HTMLRenderers/{index.js => index.ts} | 7 +- src/components/HeaderPageLayout.tsx | 6 +- src/components/HeaderWithBackButton/index.tsx | 27 +- src/components/HeaderWithBackButton/types.ts | 7 + src/components/Icon/Expensicons.ts | 2 + src/components/Icon/Illustrations.ts | 10 + .../IllustratedHeaderPageLayout.tsx | 6 +- .../InlineCodeBlock/index.native.tsx | 6 +- src/components/InlineCodeBlock/index.tsx | 6 +- src/components/InlineCodeBlock/types.ts | 7 +- src/components/KYCWall/BaseKYCWall.tsx | 10 +- src/components/KYCWall/types.ts | 22 +- src/components/MagicCodeInput.tsx | 4 +- src/components/MenuItem.tsx | 23 +- src/components/MenuItemGroup.tsx | 35 + src/components/Modal/BaseModal.tsx | 2 + src/components/MoneyReportHeader.tsx | 42 +- .../MoneyRequestConfirmationList.js | 53 +- ...equestHeader.js => MoneyRequestHeader.tsx} | 136 +- ...oraryForRefactorRequestConfirmationList.js | 44 +- src/components/OfflineWithFeedback.tsx | 14 +- .../OptionsList/BaseOptionsList.tsx | 22 +- .../OptionsSelector/BaseOptionsSelector.js | 20 +- src/components/PDFView/PDFPasswordForm.js | 6 +- src/components/Picker/types.ts | 3 +- src/components/Popover/types.ts | 13 +- src/components/PopoverMenu.tsx | 2 +- src/components/PopoverProvider/index.tsx | 6 - src/components/PopoverProvider/types.ts | 2 - .../PopoverWithoutOverlay/index.tsx | 2 + .../ProcessMoneyRequestHoldMenu.tsx | 2 +- src/components/RadioButtonWithLabel.tsx | 3 +- src/components/ReceiptEmptyState.tsx | 2 +- src/components/ReferralProgramCTA.tsx | 32 +- .../ReportActionItem/MoneyReportView.tsx | 5 +- .../ReportActionItem/MoneyRequestAction.tsx | 2 +- .../ReportActionItem/MoneyRequestPreview.tsx | 27 +- .../ReportActionItem/MoneyRequestView.tsx | 32 +- .../ReportActionItemImage.tsx | 23 +- .../ReportActionItemImages.tsx | 4 + .../ReportActionItem/ReportPreview.tsx | 53 +- .../ReportActionItem/TaskPreview.tsx | 10 +- src/components/ReportActionItem/TaskView.tsx | 30 +- src/components/ReportHeaderSkeletonView.tsx | 6 +- src/components/ReportWelcomeText.tsx | 44 +- src/components/RoomNameInput/index.js | 4 +- src/components/RoomNameInput/index.native.js | 4 +- .../RoomNameInput/roomNameInputPropTypes.js | 11 +- src/components/ScreenWrapper.tsx | 13 +- src/components/Section/index.tsx | 12 +- .../SelectionList/BaseSelectionList.tsx | 13 +- .../SelectionList/RadioListItem.tsx | 31 +- src/components/SelectionList/UserListItem.tsx | 31 +- src/components/SelectionList/types.ts | 8 +- ...ttlementButton.js => SettlementButton.tsx} | 180 +- src/components/StatePicker/index.tsx | 3 +- src/components/SubscriptAvatar.tsx | 16 +- .../BaseTextInput/baseTextInputPropTypes.js | 9 +- .../TextInput/BaseTextInput/index.native.tsx | 6 +- .../TextInput/BaseTextInput/index.tsx | 6 +- .../TextInput/BaseTextInput/types.ts | 5 +- .../TextWithTooltip/index.native.tsx | 18 + src/components/TextWithTooltip/index.tsx | 41 + src/components/TextWithTooltip/types.ts | 9 + src/components/ThemeProvider.tsx | 8 - src/components/ThreeDotsMenu/index.tsx | 2 +- src/components/TimePicker/TimePicker.js | 2 +- .../ValuePicker/ValueSelectorModal.js | 10 +- src/components/ValuePicker/index.js | 3 +- .../BaseVideoChatButtonAndMenu.tsx | 139 - .../VideoChatButtonAndMenu/index.android.tsx | 21 - .../VideoChatButtonAndMenu/index.tsx | 18 - .../VideoChatButtonAndMenu/types.ts | 9 - src/components/WorkspaceSwitcherButton.tsx | 52 +- src/components/transactionPropTypes.js | 3 +- src/hooks/usePrivatePersonalDetails.ts | 2 +- src/hooks/useTackInputFocus/index.native.ts | 6 + src/hooks/useTackInputFocus/index.ts | 49 + src/hooks/useWindowDimensions/index.ts | 64 +- src/languages/en.ts | 51 +- src/languages/es.ts | 49 +- src/languages/types.ts | 3 - .../API/parameters/AddWorkspaceRoomParams.ts | 2 +- .../parameters/ApproveMoneyRequestParams.ts | 6 + .../API/parameters/CompleteSplitBillParams.ts | 13 + .../parameters/CreateDistanceRequestParams.ts | 17 + .../parameters/DeleteMoneyRequestParams.ts | 6 + .../API/parameters/DetachReceiptParams.ts | 5 + .../API/parameters/EditMoneyRequestParams.ts | 14 + .../API/parameters/PayMoneyRequestParams.ts | 10 + .../API/parameters/ReplaceReceiptParams.ts | 6 + src/libs/API/parameters/RequestMoneyParams.ts | 30 + src/libs/API/parameters/SendMoneyParams.ts | 14 + src/libs/API/parameters/SplitBillParams.ts | 17 + .../API/parameters/StartSplitBillParams.ts | 17 + src/libs/API/parameters/SubmitReportParams.ts | 7 + .../parameters/UpdateMoneyRequestParams.ts | 9 + .../parameters/UpdatePolicyRoomNameParams.ts | 1 + .../parameters/UpdateRoomDescriptionParams.ts | 6 + .../parameters/UpdateWelcomeMessageParams.ts | 6 - src/libs/API/parameters/index.ts | 16 +- src/libs/API/types.ts | 58 +- src/libs/CardUtils.ts | 9 +- src/libs/CurrencyUtils.ts | 8 +- src/libs/DistanceRequestUtils.ts | 9 +- src/libs/EmojiUtils.ts | 6 + src/libs/ErrorUtils.ts | 51 +- src/libs/GetPhysicalCardUtils.ts | 88 +- src/libs/IOUUtils.ts | 4 +- src/libs/InitialUrlContext/index.ts | 7 + src/libs/Localize/index.ts | 13 +- .../AppNavigator/ModalStackNavigators.tsx | 28 +- .../Navigators/RightModalNavigator.tsx | 8 +- .../BottomTabBar.tsx | 39 +- .../CustomFullScreenRouter.tsx | 31 +- .../CustomRouter.ts | 6 +- src/libs/Navigation/AppNavigator/index.tsx | 17 +- src/libs/Navigation/Navigation.ts | 18 +- src/libs/Navigation/linkTo.ts | 24 +- .../FULL_SCREEN_TO_RHP_MAPPING.ts | 9 +- src/libs/Navigation/linkingConfig/config.ts | 34 +- .../linkingConfig/getAdaptedStateFromPath.ts | 10 +- src/libs/Navigation/switchPolicyID.ts | 4 +- src/libs/Navigation/types.ts | 72 +- src/libs/NextStepUtils.ts | 296 +- ...bscribeToReportCommentPushNotifications.ts | 2 +- .../clearReportNotifications/index.android.ts | 9 - .../{index.ios.ts => index.native.ts} | 0 src/libs/NumberUtils.ts | 14 +- src/libs/OptionsListUtils.ts | 4 +- src/libs/PersonalDetailsUtils.ts | 59 + src/libs/PolicyUtils.ts | 28 +- src/libs/ReceiptUtils.ts | 5 +- src/libs/ReportActionsUtils.ts | 8 +- src/libs/ReportUtils.ts | 304 +- src/libs/SelectionScraper/index.ts | 3 + src/libs/SidebarUtils.ts | 3 +- src/libs/TaskUtils.ts | 2 +- src/libs/TransactionUtils.ts | 18 +- src/libs/UserUtils.ts | 8 +- src/libs/ValidationUtils.ts | 7 +- src/libs/Violations/ViolationsUtils.ts | 13 +- src/libs/actions/App.ts | 6 +- src/libs/actions/Card.ts | 7 +- src/libs/actions/EmojiPickerAction.ts | 4 +- src/libs/actions/FormActions.ts | 2 +- src/libs/actions/{IOU.js => IOU.ts} | 2714 +++++++------ src/libs/actions/PaymentMethods.ts | 4 +- src/libs/actions/PersonalDetails.ts | 87 +- src/libs/actions/Policy.ts | 12 +- src/libs/actions/Report.ts | 63 +- src/libs/actions/Session/index.ts | 2 +- src/libs/actions/TeachersUnite.ts | 1 - src/libs/actions/Transaction.ts | 21 +- src/libs/actions/User.ts | 4 +- src/libs/actions/Wallet.ts | 7 +- src/libs/checkForUpdates.ts | 9 +- src/libs/getIsNarrowLayout/index.native.ts | 3 + src/libs/getIsNarrowLayout/index.ts | 5 + .../index.native.ts | 8 + src/libs/shouldShowSubscriptionsMenu/index.ts | 8 + .../shouldShowSubscriptionsMenu/types.tsx | 3 + src/libs/updateMultilineInputRange/types.ts | 3 +- src/pages/AddPersonalBankAccountPage.tsx | 2 +- src/pages/ConciergePage.tsx | 3 +- src/pages/EditRequestDescriptionPage.js | 91 - src/pages/EditRequestPage.js | 22 - src/pages/EditSplitBillPage.js | 15 - .../EnablePayments/AdditionalDetailsStep.js | 6 +- src/pages/EnablePayments/OnfidoPrivacy.js | 6 +- src/pages/EnablePayments/OnfidoStep.js | 3 +- src/pages/FlagCommentPage.js | 209 - src/pages/FlagCommentPage.tsx | 190 + .../LogInWithShortLivedAuthTokenPage.tsx | 5 +- src/pages/LogOutPreviousUserPage.js | 32 +- src/pages/NewChatPage.js | 8 +- .../ExpensifyClassicPage.tsx | 74 + .../ManageTeamsExpensesPage.tsx | 120 + .../PurposeForUsingExpensifyPage.tsx} | 100 +- src/pages/ProfilePage.js | 43 +- src/pages/ReferralDetailsPage.tsx | 1 + src/pages/ReimbursementAccount/AddressForm.js | 12 +- src/pages/ReimbursementAccount/CompanyStep.js | 2 +- .../ReimbursementAccount/IdentityForm.js | 8 +- .../ReimbursementAccount/ValidationStep.js | 23 +- src/pages/ReportDescriptionPage.tsx | 35 + src/pages/ReportDetailsPage.js | 282 -- src/pages/ReportWelcomeMessagePage.tsx | 128 - src/pages/RoomDescriptionPage.tsx | 113 + src/pages/SearchPage.js | 226 -- src/pages/SearchPage/SearchPageFooter.tsx | 37 +- src/pages/SearchPage/index.js | 23 +- src/pages/ShareCodePage.js | 144 - src/pages/ShareCodePage.tsx | 132 + .../IntroSchoolPrincipalPage.tsx | 3 +- src/pages/TeachersUnite/SaveTheWorldPage.tsx | 1 + src/pages/ValidateLoginPage/index.tsx | 28 +- src/pages/ValidateLoginPage/index.website.tsx | 8 +- src/pages/home/HeaderView.js | 60 +- src/pages/home/ReportScreen.js | 14 +- .../report/AnimatedEmptyStateBackground.tsx | 12 +- .../report/ContextMenu/ContextMenuActions.tsx | 37 +- .../PopoverReportActionContextMenu.tsx | 2 +- .../ComposerWithSuggestions.js | 14 +- src/pages/home/report/ReportActionItem.js | 8 +- .../home/report/ReportActionItemCreated.tsx | 1 + ...ontext.js => ReportAttachmentsContext.tsx} | 26 +- .../home/report/ReportDetailsShareCodePage.js | 31 - .../report/ReportDetailsShareCodePage.tsx | 12 + .../withReportAndReportActionOrNotFound.tsx | 40 +- .../home/report/withReportOrNotFound.tsx | 6 + src/pages/home/sidebar/AllSettingsScreen.tsx | 30 +- .../sidebar/PressableAvatarWithIndicator.js | 2 +- src/pages/home/sidebar/SidebarLinks.js | 2 +- src/pages/iou/IOUCurrencySelection.js | 2 +- src/pages/iou/MoneyRequestDescriptionPage.js | 165 - src/pages/iou/MoneyRequestTagPage.js | 127 - ...yForRefactorRequestParticipantsSelector.js | 20 +- .../iou/request/step/IOURequestStepAmount.js | 30 +- .../request/step/IOURequestStepCategory.js | 3 +- .../step/IOURequestStepConfirmation.js | 137 +- .../request/step/IOURequestStepCurrency.js | 16 +- .../iou/request/step/IOURequestStepDate.js | 3 +- .../request/step/IOURequestStepDescription.js | 60 +- .../request/step/IOURequestStepDistance.js | 14 +- .../request/step/IOURequestStepMerchant.js | 3 +- .../step/IOURequestStepParticipants.js | 2 +- .../request/step/IOURequestStepScan/index.js | 2 +- .../iou/request/step/IOURequestStepTag.js | 43 +- .../step/IOURequestStepTaxAmountPage.js | 32 +- .../request/step/IOURequestStepTaxRatePage.js | 5 +- .../request/step/IOURequestStepWaypoint.js | 4 +- src/pages/iou/steps/MoneyRequestAmountForm.js | 6 +- .../MoneyRequestParticipantsPage.js | 1 - .../MoneyRequestParticipantsSelector.js | 22 +- src/pages/settings/AboutPage/AboutPage.tsx | 3 + src/pages/settings/InitialSettingsPage.js | 47 +- .../settings/Preferences/PreferencesPage.js | 3 + .../Contacts/ContactMethodDetailsPage.js | 3 +- .../Profile/Contacts/ContactMethodsPage.js | 3 +- .../Profile/Contacts/NewContactMethodPage.js | 3 +- .../ValidateCodeForm/BaseValidateCodeForm.js | 5 +- .../CustomStatus/StatusClearAfterPage.js | 12 +- .../Profile/CustomStatus/StatusPage.js | 2 +- .../settings/Profile/LoungeAccessPage.js | 1 + .../Profile/PersonalDetails/AddressPage.js | 3 +- .../PersonalDetails/DateOfBirthPage.js | 3 +- .../Profile/PersonalDetails/LegalNamePage.js | 3 +- .../PersonalDetailsInitialPage.js | 118 - src/pages/settings/Profile/ProfilePage.js | 162 +- .../Report/NotificationPreferencePage.js | 59 - .../Report/NotificationPreferencePage.tsx | 54 + ...SettingsPage.js => ReportSettingsPage.tsx} | 105 +- .../{RoomNamePage.js => RoomNamePage.tsx} | 68 +- .../settings/Report/WriteCapabilityPage.js | 75 - .../settings/Report/WriteCapabilityPage.tsx | 71 + .../Security/SecuritySettingsPage.tsx | 5 +- .../Security/TwoFactorAuth/Steps/CodesStep.js | 2 +- .../BaseTwoFactorAuthForm.js | 2 +- .../Wallet/ActivatePhysicalCardPage.js | 7 +- src/pages/settings/Wallet/AddDebitCardPage.js | 2 +- ...hysicalCard.js => BaseGetPhysicalCard.tsx} | 143 +- ...dAddress.js => GetPhysicalCardAddress.tsx} | 57 +- ...dConfirm.js => GetPhysicalCardConfirm.tsx} | 65 +- ...calCardName.js => GetPhysicalCardName.tsx} | 66 +- ...lCardPhone.js => GetPhysicalCardPhone.tsx} | 58 +- .../settings/Wallet/ExpensifyCardPage.js | 5 +- .../settings/Wallet/ReportCardLostPage.js | 4 +- .../settings/Wallet/TransferBalancePage.js | 4 +- src/pages/settings/Wallet/WalletEmptyState.js | 1 + .../settings/Wallet/WalletPage/WalletPage.js | 3 +- src/pages/signin/LoginForm/BaseLoginForm.js | 39 +- src/pages/signin/SignInPageLayout/index.js | 18 +- src/pages/signin/UnlinkLoginForm.js | 6 +- .../ValidateCodeForm/BaseValidateCodeForm.js | 6 +- src/pages/tasks/NewTaskDescriptionPage.js | 3 +- src/pages/tasks/NewTaskDetailsPage.js | 3 +- src/pages/tasks/NewTaskPage.js | 6 +- src/pages/tasks/TaskDescriptionPage.js | 3 +- .../TaskShareDestinationSelectorModal.js | 2 +- ...nitialPage.js => WorkspaceInitialPage.tsx} | 135 +- .../workspace/WorkspaceInviteMessagePage.js | 2 +- src/pages/workspace/WorkspaceInvitePage.js | 2 +- src/pages/workspace/WorkspaceMembersPage.js | 2 +- src/pages/workspace/WorkspaceNewRoomPage.js | 29 +- src/pages/workspace/WorkspaceOverviewPage.js | 2 +- .../reimburse/WorkspaceReimburseView.js | 2 +- src/pages/workspace/withPolicy.tsx | 3 +- src/setup/{index.js => index.ts} | 0 .../{index.desktop.js => index.desktop.ts} | 0 .../{index.native.js => index.native.ts} | 0 .../setup/platformSetup/index.ts | 0 .../{index.website.js => index.website.ts} | 9 +- src/setup/platformSetup/types.ts | 6 + src/stories/EReceiptThumbail.stories.js | 25 +- src/stories/Form.stories.js | 2 +- src/styles/index.ts | 37 +- src/styles/theme/themes/dark.ts | 10 +- src/styles/theme/themes/light.ts | 10 +- src/styles/utils/FontUtils/index.ts | 1 + src/styles/utils/addOutlineWidth/types.ts | 4 +- .../utils/getSignInBgStyles/index.ios.ts | 7 + src/styles/utils/getSignInBgStyles/index.ts | 5 + src/styles/utils/getSignInBgStyles/types.ts | 6 + src/styles/utils/index.ts | 16 +- src/styles/variables.ts | 10 +- src/types/global.d.ts | 4 +- src/types/modules/react-native-clipboard.d.ts | 16 + .../modules/react-native-device-info.d.ts | 7 + src/types/modules/react-native-onyx.d.ts | 11 + src/types/onyx/Card.ts | 4 +- src/types/onyx/Form.ts | 37 +- src/types/onyx/IOU.ts | 44 +- src/types/onyx/LastPaymentMethod.ts | 3 + src/types/onyx/OnyxCommon.ts | 3 +- src/types/onyx/OriginalMessage.ts | 3 +- src/types/onyx/Policy.ts | 9 +- src/types/onyx/PolicyCategory.ts | 5 + src/types/onyx/PrivatePersonalDetails.ts | 1 + src/types/onyx/Report.ts | 4 +- src/types/onyx/ReportAction.ts | 5 +- src/types/onyx/Session.ts | 3 + src/types/onyx/Transaction.ts | 123 +- src/types/onyx/index.ts | 8 + src/types/utils/AnchorAlignment.ts | 12 + tests/README.md | 4 +- tests/actions/IOUTest.js | 6 +- tests/e2e/testRunner.js | 18 +- tests/perf-test/ReportScreen.perf-test.js | 109 +- tests/perf-test/SearchPage.perf-test.js | 8 +- tests/ui/UnreadIndicatorsTest.js | 13 +- tests/unit/NextStepUtilsTest.ts | 549 +++ tests/unit/SidebarFilterTest.js | 12 +- tests/unit/SidebarOrderTest.js | 124 +- tests/unit/SidebarTest.js | 4 +- tests/utils/LHNTestUtils.js | 1 - tests/utils/ReportTestUtils.js | 9 +- tests/utils/TestHelper.js | 2 + tests/utils/collections/policies.ts | 1 - tests/utils/collections/reportActions.ts | 18 +- web/index.html | 10 +- .../assertions/failureNotifierAssertions.js | 20 + workflow_tests/failureNotifier.test.js | 59 + workflow_tests/mocks/failureNotifierMocks.js | 11 + 481 files changed, 11688 insertions(+), 6784 deletions(-) create mode 100644 .github/workflows/failureNotifier.yml delete mode 100644 __mocks__/@react-native-community/netinfo.js create mode 100644 __mocks__/@react-native-community/netinfo.ts delete mode 100644 __mocks__/@react-native-community/push-notification-ios.js delete mode 100644 __mocks__/@react-native-firebase/crashlytics.js create mode 100644 __mocks__/@react-native-firebase/crashlytics.ts create mode 100644 __mocks__/@react-native-firebase/perf.ts rename __mocks__/@react-navigation/native/{index.js => index.ts} (67%) delete mode 100644 __mocks__/push-notification-ios.js delete mode 100644 __mocks__/react-freeze.js create mode 100644 __mocks__/react-freeze.ts delete mode 100644 __mocks__/react-native-blob-util.js create mode 100644 __mocks__/react-native-blob-util.ts delete mode 100644 __mocks__/react-native-dev-menu.js create mode 100644 __mocks__/react-native-dev-menu.ts delete mode 100644 __mocks__/react-native-device-info.js create mode 100644 __mocks__/react-native-device-info.ts delete mode 100644 __mocks__/react-native-localize.js create mode 100644 __mocks__/react-native-localize.ts delete mode 100644 __mocks__/react-native-pdf.js delete mode 100644 __mocks__/react-native-reanimated.js delete mode 100644 android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java delete mode 100644 android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java delete mode 100644 assets/images/google-meet.svg create mode 100644 assets/images/olddot-wireframe.svg create mode 100644 assets/images/simple-illustrations/simple-illustration__gears.svg create mode 100644 assets/images/simple-illustrations/simple-illustration__lockclosed.svg create mode 100644 assets/images/simple-illustrations/simple-illustration__palmtree.svg create mode 100644 assets/images/simple-illustrations/simple-illustration__profile.svg create mode 100644 assets/images/simple-illustrations/simple-illustration__qr-code.svg delete mode 100644 assets/images/zoom-icon.svg delete mode 100644 docs/articles/expensify-classic/getting-started/Individual-Users.md delete mode 100644 docs/articles/expensify-classic/getting-started/Using-The-App.md rename docs/articles/expensify-classic/{account-settings => settings}/Account-Details.md (100%) rename docs/articles/expensify-classic/{account-settings => settings}/Close-Account.md (100%) rename docs/articles/expensify-classic/{account-settings => settings}/Copilot.md (100%) rename docs/articles/expensify-classic/{account-settings => settings}/Merge-Accounts.md (100%) rename docs/articles/expensify-classic/{account-settings => settings}/Notification-Troubleshooting.md (100%) rename docs/articles/expensify-classic/{account-settings => settings}/Preferences.md (100%) rename docs/expensify-classic/hubs/{account-settings => settings}/index.html (100%) rename jest/{setup.js => setup.ts} (90%) rename jest/{setupAfterEnv.js => setupAfterEnv.ts} (100%) rename jest/{setupMockImages.js => setupMockImages.ts} (87%) create mode 100644 patches/@react-native+virtualized-lists+0.73.4+001+onStartReched.patch create mode 100644 patches/@react-native-camera-roll+camera-roll+5.4.0.patch create mode 100644 patches/@react-native-community+cli-platform-android+12.3.0.patch create mode 100644 patches/@react-native-community+cli-platform-ios+12.3.0.patch create mode 100644 patches/@react-native-firebase+analytics+12.9.3.patch create mode 100644 patches/@react-native-firebase+app+12.9.3.patch create mode 100644 patches/@react-native-firebase+crashlytics+12.9.3.patch create mode 100644 patches/@react-native-firebase+perf+12.9.3.patch create mode 100644 patches/expo+50.0.4.patch create mode 100644 patches/expo-modules-autolinking+1.10.2.patch create mode 100644 patches/expo-modules-core+1.11.8.patch create mode 100644 patches/react-native-pdf+6.7.3.patch create mode 100644 patches/react-native-reanimated+3.6.1.patch rename patches/{react-native-vision-camera+2.16.2+001+fix-boost-dependency.patch => react-native-vision-camera+2.16.5+001+fix-boost-dependency.patch} (56%) rename patches/{react-native-vision-camera+2.16.2.patch => react-native-vision-camera+2.16.5.patch} (100%) rename src/{App.js => App.tsx} (60%) delete mode 100644 src/components/CommunicationsLink.js create mode 100644 src/components/CommunicationsLink.tsx delete mode 100644 src/components/FormSubmit/index.native.tsx delete mode 100644 src/components/FormSubmit/index.tsx delete mode 100644 src/components/FormSubmit/types.ts rename src/components/HTMLEngineProvider/HTMLRenderers/{AnchorRenderer.js => AnchorRenderer.tsx} (77%) rename src/components/HTMLEngineProvider/HTMLRenderers/{CodeRenderer.js => CodeRenderer.tsx} (65%) rename src/components/HTMLEngineProvider/HTMLRenderers/{EditedRenderer.js => EditedRenderer.tsx} (61%) rename src/components/HTMLEngineProvider/HTMLRenderers/{ImageRenderer.js => ImageRenderer.tsx} (77%) rename src/components/HTMLEngineProvider/HTMLRenderers/{MentionHereRenderer.js => MentionHereRenderer.tsx} (53%) delete mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx rename src/components/HTMLEngineProvider/HTMLRenderers/{PreRenderer.js => PreRenderer.tsx} (53%) delete mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/htmlRendererPropTypes.js rename src/components/HTMLEngineProvider/HTMLRenderers/{index.js => index.ts} (74%) create mode 100644 src/components/MenuItemGroup.tsx rename src/components/{MoneyRequestHeader.js => MoneyRequestHeader.tsx} (62%) rename src/components/{SettlementButton.js => SettlementButton.tsx} (65%) create mode 100644 src/components/TextWithTooltip/index.native.tsx create mode 100644 src/components/TextWithTooltip/index.tsx create mode 100644 src/components/TextWithTooltip/types.ts delete mode 100755 src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx delete mode 100644 src/components/VideoChatButtonAndMenu/index.android.tsx delete mode 100644 src/components/VideoChatButtonAndMenu/index.tsx delete mode 100644 src/components/VideoChatButtonAndMenu/types.ts create mode 100644 src/hooks/useTackInputFocus/index.native.ts create mode 100644 src/hooks/useTackInputFocus/index.ts create mode 100644 src/libs/API/parameters/ApproveMoneyRequestParams.ts create mode 100644 src/libs/API/parameters/CompleteSplitBillParams.ts create mode 100644 src/libs/API/parameters/CreateDistanceRequestParams.ts create mode 100644 src/libs/API/parameters/DeleteMoneyRequestParams.ts create mode 100644 src/libs/API/parameters/DetachReceiptParams.ts create mode 100644 src/libs/API/parameters/EditMoneyRequestParams.ts create mode 100644 src/libs/API/parameters/PayMoneyRequestParams.ts create mode 100644 src/libs/API/parameters/ReplaceReceiptParams.ts create mode 100644 src/libs/API/parameters/RequestMoneyParams.ts create mode 100644 src/libs/API/parameters/SendMoneyParams.ts create mode 100644 src/libs/API/parameters/SplitBillParams.ts create mode 100644 src/libs/API/parameters/StartSplitBillParams.ts create mode 100644 src/libs/API/parameters/SubmitReportParams.ts create mode 100644 src/libs/API/parameters/UpdateMoneyRequestParams.ts create mode 100644 src/libs/API/parameters/UpdateRoomDescriptionParams.ts delete mode 100644 src/libs/API/parameters/UpdateWelcomeMessageParams.ts create mode 100644 src/libs/InitialUrlContext/index.ts delete mode 100644 src/libs/Notification/clearReportNotifications/index.android.ts rename src/libs/Notification/clearReportNotifications/{index.ios.ts => index.native.ts} (100%) rename src/libs/actions/{IOU.js => IOU.ts} (61%) create mode 100644 src/libs/getIsNarrowLayout/index.native.ts create mode 100644 src/libs/getIsNarrowLayout/index.ts create mode 100644 src/libs/shouldShowSubscriptionsMenu/index.native.ts create mode 100644 src/libs/shouldShowSubscriptionsMenu/index.ts create mode 100644 src/libs/shouldShowSubscriptionsMenu/types.tsx delete mode 100644 src/pages/EditRequestDescriptionPage.js delete mode 100644 src/pages/FlagCommentPage.js create mode 100644 src/pages/FlagCommentPage.tsx create mode 100644 src/pages/OnboardEngagement/ExpensifyClassicPage.tsx create mode 100644 src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx rename src/{components/PurposeForUsingExpensifyModal.tsx => pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx} (63%) create mode 100644 src/pages/ReportDescriptionPage.tsx delete mode 100644 src/pages/ReportDetailsPage.js delete mode 100644 src/pages/ReportWelcomeMessagePage.tsx create mode 100644 src/pages/RoomDescriptionPage.tsx delete mode 100755 src/pages/SearchPage.js delete mode 100644 src/pages/ShareCodePage.js create mode 100644 src/pages/ShareCodePage.tsx rename src/pages/home/report/{ReportAttachmentsContext.js => ReportAttachmentsContext.tsx} (52%) delete mode 100644 src/pages/home/report/ReportDetailsShareCodePage.js create mode 100644 src/pages/home/report/ReportDetailsShareCodePage.tsx delete mode 100644 src/pages/iou/MoneyRequestDescriptionPage.js delete mode 100644 src/pages/iou/MoneyRequestTagPage.js delete mode 100644 src/pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage.js delete mode 100644 src/pages/settings/Report/NotificationPreferencePage.js create mode 100644 src/pages/settings/Report/NotificationPreferencePage.tsx rename src/pages/settings/Report/{ReportSettingsPage.js => ReportSettingsPage.tsx} (68%) rename src/pages/settings/Report/{RoomNamePage.js => RoomNamePage.tsx} (71%) delete mode 100644 src/pages/settings/Report/WriteCapabilityPage.js create mode 100644 src/pages/settings/Report/WriteCapabilityPage.tsx rename src/pages/settings/Wallet/Card/{BaseGetPhysicalCard.js => BaseGetPhysicalCard.tsx} (60%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardAddress.js => GetPhysicalCardAddress.tsx} (59%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardConfirm.js => GetPhysicalCardConfirm.tsx} (64%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardName.js => GetPhysicalCardName.tsx} (64%) rename src/pages/settings/Wallet/Card/{GetPhysicalCardPhone.js => GetPhysicalCardPhone.tsx} (58%) rename src/pages/workspace/{WorkspaceInitialPage.js => WorkspaceInitialPage.tsx} (63%) rename src/setup/{index.js => index.ts} (100%) rename src/setup/platformSetup/{index.desktop.js => index.desktop.ts} (100%) rename src/setup/platformSetup/{index.native.js => index.native.ts} (100%) rename __mocks__/@react-native-firebase/perf.js => src/setup/platformSetup/index.ts (100%) rename src/setup/platformSetup/{index.website.js => index.website.ts} (91%) create mode 100644 src/setup/platformSetup/types.ts create mode 100644 src/styles/utils/getSignInBgStyles/index.ios.ts create mode 100644 src/styles/utils/getSignInBgStyles/index.ts create mode 100644 src/styles/utils/getSignInBgStyles/types.ts create mode 100644 src/types/modules/react-native-clipboard.d.ts create mode 100644 src/types/modules/react-native-device-info.d.ts create mode 100644 src/types/onyx/LastPaymentMethod.ts create mode 100644 src/types/utils/AnchorAlignment.ts create mode 100644 tests/unit/NextStepUtilsTest.ts create mode 100644 workflow_tests/assertions/failureNotifierAssertions.js create mode 100644 workflow_tests/failureNotifier.test.js create mode 100644 workflow_tests/mocks/failureNotifierMocks.js diff --git a/.github/scripts/createHelpRedirects.sh b/.github/scripts/createHelpRedirects.sh index 1ae2220253c4..14ed9de953fc 100755 --- a/.github/scripts/createHelpRedirects.sh +++ b/.github/scripts/createHelpRedirects.sh @@ -19,7 +19,7 @@ function checkCloudflareResult { if ! [[ "$RESULT_MESSAGE" == "true" ]]; then ERROR_MESSAGE=$(echo "$RESULTS" | jq .errors) - error "Error calling Cloudfalre API: $ERROR_MESSAGE" + error "Error calling Cloudflare API: $ERROR_MESSAGE" exit 1 fi } diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 31bfdf963525..04fdb7e80d6a 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -200,26 +200,66 @@ jobs: if: failure() run: | echo ${{ steps.schedule-awsdf-main.outputs.data }} - cat "./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/Test spec output.txt" unzip "Customer Artifacts.zip" -d mainResults - cat ./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/debug.log + cat "./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/logcat.txt" || true + cat ./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/debug.log || true + cat "./mainResults/Host_Machine_Files/\$WORKING_DIRECTORY/Test spec output.txt" || true + + - name: Announce failed workflow in Slack + if: failure() + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#expensify-margelo', + attachments: [{ + color: 'danger', + text: `💥 ${process.env.AS_REPO} E2E Test run failed failed on workflow 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - name: Unzip AWS Device Farm results - if: ${{ always() }} + if: always() run: unzip "Customer Artifacts.zip" - name: Print AWS Device Farm run results - if: ${{ always() }} + if: always() run: cat "./Host_Machine_Files/\$WORKING_DIRECTORY/output.md" - name: Check if test failed, if so post the results and add the DeployBlocker label + id: checkIfRegressionDetected run: | if grep -q '🔴' ./output.md; then + # Create an output to the GH action that the test failed: + echo "performanceRegressionDetected=true" >> "$GITHUB_OUTPUT" + gh pr edit ${{ inputs.PR_NUMBER }} --add-label DeployBlockerCash gh pr comment ${{ inputs.PR_NUMBER }} -F ./output.md gh pr comment ${{ inputs.PR_NUMBER }} -b "@Expensify/mobile-deployers 📣 Please look into this performance regression as it's a deploy blocker." else + echo "performanceRegressionDetected=false" >> "$GITHUB_OUTPUT" echo '✅ no performance regression detected' fi env: GITHUB_TOKEN: ${{ github.token }} + + - name: 'Announce regression in Slack' + if: ${{ steps.checkIfRegressionDetected.outputs.performanceRegressionDetected == 'true' }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#newdot-performance', + attachments: [{ + color: 'danger', + text: `🔴 Performance regression detected in PR ${{ inputs.PR_NUMBER }}\nDetected in workflow.`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml new file mode 100644 index 000000000000..d2e0ec4f38e5 --- /dev/null +++ b/.github/workflows/failureNotifier.yml @@ -0,0 +1,96 @@ +name: Notify on Workflow Failure + +on: + workflow_run: + workflows: ["Process new code merged to main"] + types: + - completed + +permissions: + issues: write + +jobs: + notifyFailure: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + steps: + - name: Fetch Workflow Run Jobs + id: fetch-workflow-jobs + uses: actions/github-script@v7 + with: + script: | + const runId = "${{ github.event.workflow_run.id }}"; + const jobsData = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }); + return jobsData.data; + + - name: Process Each Failed Job + uses: actions/github-script@v7 + with: + script: | + const jobs = ${{ steps.fetch-workflow-jobs.outputs.result }}; + + const headCommit = "${{ github.event.workflow_run.head_commit.id }}"; + const prData = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: headCommit, + }); + + const pr = prData.data[0]; + const prLink = pr.html_url; + const prAuthor = pr.user.login; + const prMerger = "${{ github.event.workflow_run.actor.login }}"; + + const failureLabel = 'Workflow Failure'; + for (let i = 0; i < jobs.total_count; i++) { + if (jobs.jobs[i].conclusion == 'failure') { + const jobName = jobs.jobs[i].name; + const jobLink = jobs.jobs[i].html_url; + const issues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: failureLabel, + state: 'open' + }); + const existingIssue = issues.data.find(issue => issue.title.includes(jobName)); + if (!existingIssue) { + const annotations = await github.rest.checks.listAnnotations({ + owner: context.repo.owner, + repo: context.repo.repo, + check_run_id: jobs.jobs[i].id, + }); + let errorMessage = ""; + for(let j = 0; j < annotations.data.length; j++) { + errorMessage += annotations.data[j].annotation_level + ": "; + errorMessage += annotations.data[j].message + "\n"; + } + const issueTitle = `Investigate workflow job failing on main: ${ jobName }`; + const issueBody = `🚨 **Failure Summary** 🚨:\n\n` + + `- **📋 Job Name**: [${ jobName }](${ jobLink })\n` + + `- **🔧 Failure in Workflow**: Process new code merged to main\n` + + `- **🔗 Triggered by PR**: [PR Link](${ prLink })\n` + + `- **👤 PR Author**: @${ prAuthor }\n` + + `- **🤝 Merged by**: @${ prMerger }\n` + + `- **🐛 Error Message**: \n ${errorMessage}\n\n` + + `⚠️ **Action Required** ⚠️:\n\n` + + `🛠️ A recent merge appears to have caused a failure in the job named [${ jobName }](${ jobLink }).\n` + + `This issue has been automatically created and labeled with \`${ failureLabel }\` for investigation. \n\n` + + `👀 **Please look into the following**:\n` + + `1. **Why the PR caused the job to fail?**\n` + + `2. **Address any underlying issues.**\n\n` + + `🐛 We appreciate your help in squashing this bug!`; + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: issueTitle, + body: issueBody, + labels: [failureLabel, 'Daily'], + assignees: [prMerger] + }); + } + } + } diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index 8f9512062e9d..f09865de0194 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -1,3 +1,4 @@ +# Reminder: If this workflow's name changes, update the name in the dependent workflow at .github/workflows/failureNotifier.yml. name: Process new code merged to main on: diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index 6717c1736f65..204f70344b18 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -25,7 +25,7 @@ module.exports = ({config}) => { config.resolve.alias = { 'react-native-config': 'react-web-config', 'react-native$': 'react-native-web', - '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.js'), + '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'), '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'), // Module alias support for storybook files, coping from `webpack.common.js` diff --git a/__mocks__/@react-native-community/netinfo.js b/__mocks__/@react-native-community/netinfo.js deleted file mode 100644 index 53a9282ea8db..000000000000 --- a/__mocks__/@react-native-community/netinfo.js +++ /dev/null @@ -1,19 +0,0 @@ -const defaultState = { - type: 'cellular', - isConnected: true, - isInternetReachable: true, - details: { - isConnectionExpensive: true, - cellularGeneration: '3g', - }, -}; - -const RNCNetInfoMock = { - configure: () => {}, - fetch: () => Promise.resolve(defaultState), - refresh: () => Promise.resolve(defaultState), - addEventListener: () => () => {}, - useNetInfo: () => {}, -}; - -export default RNCNetInfoMock; diff --git a/__mocks__/@react-native-community/netinfo.ts b/__mocks__/@react-native-community/netinfo.ts new file mode 100644 index 000000000000..0b7bdc9010a3 --- /dev/null +++ b/__mocks__/@react-native-community/netinfo.ts @@ -0,0 +1,31 @@ +import {NetInfoCellularGeneration, NetInfoStateType} from '@react-native-community/netinfo'; +import type {addEventListener, configure, fetch, NetInfoState, refresh, useNetInfo} from '@react-native-community/netinfo'; + +const defaultState: NetInfoState = { + type: NetInfoStateType.cellular, + isConnected: true, + isInternetReachable: true, + details: { + isConnectionExpensive: true, + cellularGeneration: NetInfoCellularGeneration['3g'], + carrier: 'T-Mobile', + }, +}; + +type NetInfoMock = { + configure: typeof configure; + fetch: typeof fetch; + refresh: typeof refresh; + addEventListener: typeof addEventListener; + useNetInfo: typeof useNetInfo; +}; + +const netInfoMock: NetInfoMock = { + configure: () => {}, + fetch: () => Promise.resolve(defaultState), + refresh: () => Promise.resolve(defaultState), + addEventListener: () => () => {}, + useNetInfo: () => defaultState, +}; + +export default netInfoMock; diff --git a/__mocks__/@react-native-community/push-notification-ios.js b/__mocks__/@react-native-community/push-notification-ios.js deleted file mode 100644 index 0fe8354b9e08..000000000000 --- a/__mocks__/@react-native-community/push-notification-ios.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - addEventListener: jest.fn(), - requestPermissions: jest.fn(() => Promise.resolve()), - getInitialNotification: jest.fn(() => Promise.resolve()), -}; diff --git a/__mocks__/@react-native-firebase/crashlytics.js b/__mocks__/@react-native-firebase/crashlytics.js deleted file mode 100644 index cc7ff3f55e4a..000000000000 --- a/__mocks__/@react-native-firebase/crashlytics.js +++ /dev/null @@ -1,7 +0,0 @@ -// uses and we need to mock the imported crashlytics module -// due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 -export default () => ({ - log: jest.fn(), - recordError: jest.fn(), - setCrashlyticsCollectionEnabled: jest.fn(), -}); diff --git a/__mocks__/@react-native-firebase/crashlytics.ts b/__mocks__/@react-native-firebase/crashlytics.ts new file mode 100644 index 000000000000..2df845ba0c69 --- /dev/null +++ b/__mocks__/@react-native-firebase/crashlytics.ts @@ -0,0 +1,15 @@ +import type {FirebaseCrashlyticsTypes} from '@react-native-firebase/crashlytics'; + +type CrashlyticsModule = Pick; + +type CrashlyticsMock = () => CrashlyticsModule; + +// uses and we need to mock the imported crashlytics module +// due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 +const crashlyticsMock: CrashlyticsMock = () => ({ + log: jest.fn(), + recordError: jest.fn(), + setCrashlyticsCollectionEnabled: jest.fn(), +}); + +export default crashlyticsMock; diff --git a/__mocks__/@react-native-firebase/perf.ts b/__mocks__/@react-native-firebase/perf.ts new file mode 100644 index 000000000000..e304b1a1f007 --- /dev/null +++ b/__mocks__/@react-native-firebase/perf.ts @@ -0,0 +1,5 @@ +type PerfMock = () => void; + +const perfMock: PerfMock = () => {}; + +export default perfMock; diff --git a/__mocks__/@react-navigation/native/index.js b/__mocks__/@react-navigation/native/index.ts similarity index 67% rename from __mocks__/@react-navigation/native/index.js rename to __mocks__/@react-navigation/native/index.ts index 09abd0d02bf9..aa8067a1c862 100644 --- a/__mocks__/@react-navigation/native/index.js +++ b/__mocks__/@react-navigation/native/index.ts @@ -1,7 +1,7 @@ import {useIsFocused as realUseIsFocused} from '@react-navigation/native'; // We only want this mocked for storybook, not jest -const useIsFocused = process.env.NODE_ENV === 'test' ? realUseIsFocused : () => true; +const useIsFocused: typeof realUseIsFocused = process.env.NODE_ENV === 'test' ? realUseIsFocused : () => true; export * from '@react-navigation/core'; export * from '@react-navigation/native'; diff --git a/__mocks__/push-notification-ios.js b/__mocks__/push-notification-ios.js deleted file mode 100644 index 0fe8354b9e08..000000000000 --- a/__mocks__/push-notification-ios.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - addEventListener: jest.fn(), - requestPermissions: jest.fn(() => Promise.resolve()), - getInitialNotification: jest.fn(() => Promise.resolve()), -}; diff --git a/__mocks__/react-freeze.js b/__mocks__/react-freeze.js deleted file mode 100644 index 51294f40f9ca..000000000000 --- a/__mocks__/react-freeze.js +++ /dev/null @@ -1,6 +0,0 @@ -const Freeze = (props) => props.children; - -export { - // eslint-disable-next-line import/prefer-default-export - Freeze, -}; diff --git a/__mocks__/react-freeze.ts b/__mocks__/react-freeze.ts new file mode 100644 index 000000000000..d87abe01acfb --- /dev/null +++ b/__mocks__/react-freeze.ts @@ -0,0 +1,8 @@ +import type {Freeze as FreezeComponent} from 'react-freeze'; + +const Freeze: typeof FreezeComponent = (props) => props.children as JSX.Element; + +export { + // eslint-disable-next-line import/prefer-default-export + Freeze, +}; diff --git a/__mocks__/react-native-blob-util.js b/__mocks__/react-native-blob-util.js deleted file mode 100644 index ff8b4c56321a..000000000000 --- a/__mocks__/react-native-blob-util.js +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/__mocks__/react-native-blob-util.ts b/__mocks__/react-native-blob-util.ts new file mode 100644 index 000000000000..bdaa0ae3ded5 --- /dev/null +++ b/__mocks__/react-native-blob-util.ts @@ -0,0 +1,5 @@ +import type RNFetchBlob from 'react-native-blob-util'; + +const ReactNativeBlobUtilMock: Partial = {}; + +export default ReactNativeBlobUtilMock; diff --git a/__mocks__/react-native-dev-menu.js b/__mocks__/react-native-dev-menu.js deleted file mode 100644 index 49cb4c61a209..000000000000 --- a/__mocks__/react-native-dev-menu.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - addItem: jest.fn(), -}; diff --git a/__mocks__/react-native-dev-menu.ts b/__mocks__/react-native-dev-menu.ts new file mode 100644 index 000000000000..0d35d5c32723 --- /dev/null +++ b/__mocks__/react-native-dev-menu.ts @@ -0,0 +1,11 @@ +import type {addItem} from 'react-native-dev-menu'; + +type ReactNativeDevMenuMock = { + addItem: typeof addItem; +}; + +const reactNativeDevMenuMock: ReactNativeDevMenuMock = { + addItem: jest.fn(), +}; + +export default reactNativeDevMenuMock; diff --git a/__mocks__/react-native-device-info.js b/__mocks__/react-native-device-info.js deleted file mode 100644 index 2ba5b6ef85b3..000000000000 --- a/__mocks__/react-native-device-info.js +++ /dev/null @@ -1,3 +0,0 @@ -import MockDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock'; - -export default MockDeviceInfo; diff --git a/__mocks__/react-native-device-info.ts b/__mocks__/react-native-device-info.ts new file mode 100644 index 000000000000..bb04f1a176b2 --- /dev/null +++ b/__mocks__/react-native-device-info.ts @@ -0,0 +1,6 @@ +import type DeviceInfoModule from 'react-native-device-info'; +import MockDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock'; + +const DeviceInfo: typeof DeviceInfoModule = MockDeviceInfo; + +export default DeviceInfo; diff --git a/__mocks__/react-native-localize.js b/__mocks__/react-native-localize.js deleted file mode 100644 index 8d302b7d598b..000000000000 --- a/__mocks__/react-native-localize.js +++ /dev/null @@ -1,3 +0,0 @@ -const mockRNLocalize = require('react-native-localize/mock'); - -module.exports = mockRNLocalize; diff --git a/__mocks__/react-native-localize.ts b/__mocks__/react-native-localize.ts new file mode 100644 index 000000000000..aa0322d6714c --- /dev/null +++ b/__mocks__/react-native-localize.ts @@ -0,0 +1,3 @@ +import mockRNLocalize from 'react-native-localize/mock'; + +module.exports = mockRNLocalize; diff --git a/__mocks__/react-native-pdf.js b/__mocks__/react-native-pdf.js deleted file mode 100644 index 4d179e730903..000000000000 --- a/__mocks__/react-native-pdf.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - DocumentDir: jest.fn(), - ImageCache: jest.fn(), -}; diff --git a/__mocks__/react-native-reanimated.js b/__mocks__/react-native-reanimated.js deleted file mode 100644 index dfd96838caa7..000000000000 --- a/__mocks__/react-native-reanimated.js +++ /dev/null @@ -1,7 +0,0 @@ -// This mock is required as per setup instructions for react-navigation testing -// https://reactnavigation.org/docs/testing/#mocking-native-modules - -const Reanimated = require('react-native-reanimated/mock'); - -Reanimated.default.call = () => {}; -module.exports = Reanimated; diff --git a/android/app/build.gradle b/android/app/build.gradle index a53c6dfb62af..7a469d302e45 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 1001043600 - versionName "1.4.36-0" + versionCode 1001043901 + versionName "1.4.39-1" } flavorDimensions "default" diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java index 0cae0bd2de6d..86529e19401e 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java @@ -14,8 +14,5 @@ public void onAirshipReady(@NonNull Context context, @NonNull UAirship airship) CustomNotificationProvider notificationProvider = new CustomNotificationProvider(context, airship.getAirshipConfigOptions()); pushManager.setNotificationProvider(notificationProvider); - - NotificationListener notificationListener = airship.getPushManager().getNotificationListener(); - pushManager.setNotificationListener(new CustomNotificationListener(notificationListener, notificationProvider)); } } diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java deleted file mode 100644 index 8149ca118d58..000000000000 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationListener.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.expensify.chat.customairshipextender; - -import androidx.annotation.NonNull; -import com.urbanairship.push.NotificationActionButtonInfo; -import com.urbanairship.push.NotificationInfo; -import com.urbanairship.push.NotificationListener; -import com.urbanairship.push.PushMessage; -import org.jetbrains.annotations.NotNull; - -/** - * Allows us to clear the notification cache when the user dismisses a notification. - */ -public class CustomNotificationListener implements NotificationListener { - private final NotificationListener parent; - private final CustomNotificationProvider provider; - - CustomNotificationListener(NotificationListener parent, CustomNotificationProvider provider) { - this.parent = parent; - this.provider = provider; - } - - @Override - public void onNotificationPosted(@NonNull @NotNull NotificationInfo notificationInfo) { - parent.onNotificationPosted(notificationInfo); - } - - @Override - public boolean onNotificationOpened(@NonNull @NotNull NotificationInfo notificationInfo) { - // The notification is also dismissed when it's tapped so handle that as well - PushMessage message = notificationInfo.getMessage(); - provider.onDismissNotification(message); - - return parent.onNotificationOpened(notificationInfo); - } - - @Override - public boolean onNotificationForegroundAction(@NonNull @NotNull NotificationInfo notificationInfo, @NonNull @NotNull NotificationActionButtonInfo actionButtonInfo) { - return parent.onNotificationForegroundAction(notificationInfo, actionButtonInfo); - } - - @Override - public void onNotificationBackgroundAction(@NonNull @NotNull NotificationInfo notificationInfo, @NonNull @NotNull NotificationActionButtonInfo actionButtonInfo) { - parent.onNotificationBackgroundAction(notificationInfo, actionButtonInfo); - } - - @Override - public void onNotificationDismissed(@NonNull @NotNull NotificationInfo notificationInfo) { - parent.onNotificationDismissed(notificationInfo); - - PushMessage message = notificationInfo.getMessage(); - provider.onDismissNotification(message); - } -} diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java index c60476ad3f0a..c57819d19d03 100644 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java +++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java @@ -3,6 +3,7 @@ import static androidx.core.app.NotificationCompat.CATEGORY_MESSAGE; import static androidx.core.app.NotificationCompat.PRIORITY_MAX; +import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; @@ -15,6 +16,8 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.os.Build; +import android.os.Bundle; +import android.service.notification.StatusBarNotification; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; @@ -26,6 +29,7 @@ import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; import androidx.core.graphics.drawable.IconCompat; +import androidx.versionedparcelable.ParcelUtils; import com.urbanairship.AirshipConfigOptions; import com.urbanairship.json.JsonMap; @@ -40,18 +44,14 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; -import java.util.HashMap; +import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import com.expensify.chat.customairshipextender.NotificationCache.NotificationData; -import com.expensify.chat.customairshipextender.NotificationCache.NotificationMessage; - public class CustomNotificationProvider extends ReactNotificationProvider { // Resize icons to 100 dp x 100 dp private static final int MAX_ICON_SIZE_DPS = 100; @@ -73,6 +73,13 @@ public class CustomNotificationProvider extends ReactNotificationProvider { private static final String PAYLOAD_KEY = "payload"; private static final String ONYX_DATA_KEY = "onyxData"; + // Notification extras keys + public static final String EXTRAS_REPORT_ID_KEY = "reportID"; + public static final String EXTRAS_AVATAR_KEY = "avatar"; + public static final String EXTRAS_NAME_KEY = "name"; + public static final String EXTRAS_ACCOUNT_ID_KEY = "accountID"; + + private final ExecutorService executorService = Executors.newCachedThreadPool(); public CustomNotificationProvider(@NonNull Context context, @NonNull AirshipConfigOptions configOptions) { @@ -158,7 +165,7 @@ public Bitmap getCroppedBitmap(Bitmap bitmap) { /** * Applies the message style to the notification builder. It also takes advantage of the - * notification cache to build conversations style notifications. + * android notification API to build conversations style notifications. * * @param builder Notification builder that will receive the message style * @param payload Notification payload, which contains all the data we need to build the notifications. @@ -170,10 +177,9 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil return; } - // Retrieve and check for cached notifications - NotificationData notificationData = NotificationCache.getNotificationData(reportID); - boolean hasExistingNotification = notificationData.messages.size() >= 1; - + // Retrieve and check for existing notifications + StatusBarNotification existingReportNotification = getActiveNotificationByReportId(context, reportID); + boolean hasExistingNotification = existingReportNotification != null; try { JsonMap reportMap = payload.get(ONYX_DATA_KEY).getList().get(1).getMap().get("value").getMap(); String reportId = reportMap.keySet().iterator().next(); @@ -187,31 +193,15 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil String message = alert != null ? alert : messageData.get("message").getList().get(0).getMap().get("text").getString(); String conversationName = payload.get("roomName") == null ? "" : payload.get("roomName").getString(""); - // Retrieve or create the Person object who sent the latest report comment - Person person = notificationData.getPerson(accountID); - Bitmap personIcon = notificationData.getIcon(accountID); - - if (personIcon == null) { - personIcon = fetchIcon(context, avatar); - } + // create the Person object who sent the latest report comment + Bitmap personIcon = fetchIcon(context, avatar); builder.setLargeIcon(personIcon); - // Persist the person and icon to the notification cache - if (person == null) { - IconCompat iconCompat = IconCompat.createWithBitmap(personIcon); - person = new Person.Builder() - .setIcon(iconCompat) - .setKey(accountID) - .setName(name) - .build(); - - notificationData.putPerson(accountID, name, personIcon); - } + Person person = createMessagePersonObject(IconCompat.createWithBitmap(personIcon), accountID, name); - // Despite not using conversation style for the initial notification from each chat, we need to cache it to enable conversation style for future notifications + // Create latest received message object long createdTimeInMillis = getMessageTimeInMillis(messageData.get("created").getString("")); - notificationData.messages.add(new NotificationMessage(accountID, message, createdTimeInMillis)); - + NotificationCompat.MessagingStyle.Message newMessage = new NotificationCompat.MessagingStyle.Message(message, createdTimeInMillis, person); // Conversational styling should be applied to groups chats, rooms, and any 1:1 chats with more than one notification (ensuring the large profile image is always shown) if (!conversationName.isEmpty() || hasExistingNotification) { @@ -220,30 +210,77 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil .setGroupConversation(true) .setConversationTitle(conversationName); + // Add all conversation messages to the notification, including the last one we just received. - for (NotificationMessage cachedMessage : notificationData.messages) { - messagingStyle.addMessage(cachedMessage.text, cachedMessage.time, notificationData.getPerson(cachedMessage.accountID)); + List messages; + if (hasExistingNotification) { + NotificationCompat.MessagingStyle previousStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(existingReportNotification.getNotification()); + messages = previousStyle != null ? previousStyle.getMessages() : new ArrayList<>(List.of(recreatePreviousMessage(existingReportNotification))); + } else { + messages = new ArrayList<>(); + } + + // add the last one message we just received. + messages.add(newMessage); + + for (NotificationCompat.MessagingStyle.Message activeMessage : messages) { + messagingStyle.addMessage(activeMessage); } + builder.setStyle(messagingStyle); } + // save reportID and person info for future merging + builder.addExtras(createMessageExtrasBundle(reportID, person)); + // Clear the previous notification associated to this conversation so it looks like we are // replacing them with this new one we just built. - if (notificationData.prevNotificationID != -1) { - NotificationManagerCompat.from(context).cancel(notificationData.prevNotificationID); + if (hasExistingNotification) { + int previousNotificationID = existingReportNotification.getId(); + NotificationManagerCompat.from(context).cancel(previousNotificationID); } } catch (Exception e) { e.printStackTrace(); } + } + + private Person createMessagePersonObject (IconCompat icon, String key, String name) { + return new Person.Builder().setIcon(icon).setKey(key).setName(name).build(); + } + + private NotificationCompat.MessagingStyle.Message recreatePreviousMessage (StatusBarNotification statusBarNotification) { + // Get previous message + Notification previousNotification = statusBarNotification.getNotification(); + String previousMessage = previousNotification.extras.getString("android.text"); + long time = statusBarNotification.getNotification().when; + // Recreate Person object + IconCompat avatarBitmap = ParcelUtils.getVersionedParcelable(previousNotification.extras, EXTRAS_AVATAR_KEY); + String previousName = previousNotification.extras.getString(EXTRAS_NAME_KEY); + String previousAccountID = previousNotification.extras.getString(EXTRAS_ACCOUNT_ID_KEY); + Person previousPerson = createMessagePersonObject(avatarBitmap, previousAccountID, previousName); + + return new NotificationCompat.MessagingStyle.Message(previousMessage, time, previousPerson); + } - // Store the new notification ID so we can replace the notification if this conversation - // receives more messages - notificationData.prevNotificationID = notificationID; + private Bundle createMessageExtrasBundle(long reportID, Person person) { + Bundle extrasBundle = new Bundle(); + extrasBundle.putLong(EXTRAS_REPORT_ID_KEY, reportID); + ParcelUtils.putVersionedParcelable(extrasBundle, EXTRAS_AVATAR_KEY, person.getIcon()); + extrasBundle.putString(EXTRAS_ACCOUNT_ID_KEY, person.getKey()); + extrasBundle.putString(EXTRAS_NAME_KEY, person.getName().toString()); - NotificationCache.setNotificationData(reportID, notificationData); + return extrasBundle; } + private StatusBarNotification getActiveNotificationByReportId(@NonNull Context context, long reportId) { + List notifications = NotificationManagerCompat.from(context).getActiveNotifications(); + for (StatusBarNotification currentNotification : notifications) { + long associatedReportId = currentNotification.getNotification().extras.getLong("reportID", -1); + if (associatedReportId == reportId) return currentNotification; + } + return null; + } /** * Safely retrieve the message time in milliseconds */ @@ -260,26 +297,6 @@ private long getMessageTimeInMillis(String createdTime) { return Calendar.getInstance().getTimeInMillis(); } - /** - * Remove the notification data from the cache when the user dismisses the notification. - * - * @param message Push notification's message - */ - public void onDismissNotification(PushMessage message) { - try { - JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap(); - long reportID = payload.get("reportID").getLong(-1); - - if (reportID == -1) { - return; - } - - NotificationCache.setNotificationData(reportID, null); - } catch (Exception e) { - Log.e(TAG, "Failed to delete conversation cache. SendID=" + message.getSendId(), e); - } - } - private Bitmap fetchIcon(@NonNull Context context, String urlString) { URL parsedUrl = null; try { diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java deleted file mode 100644 index 7ddc17d37b4d..000000000000 --- a/android/app/src/main/java/com/expensify/chat/customairshipextender/NotificationCache.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.expensify.chat.customairshipextender; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.util.Base64; - -import androidx.core.app.Person; -import androidx.core.graphics.drawable.IconCompat; - -import com.expensify.chat.MainApplication; -import com.urbanairship.UAirship; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; - -public class NotificationCache { - - private static final String CACHE_FILE_NAME = "notification-cache"; - private static HashMap cache = null; - - /* - * Get NotificationData for an existing notification or create a new instance - * if it doesn't exist - */ - public static NotificationData getNotificationData(long reportID) { - if (cache == null) { - cache = readFromInternalStorage(); - } - - NotificationData notificationData = cache.get(Long.toString(reportID)); - - if (notificationData == null) { - notificationData = new NotificationData(); - setNotificationData(reportID, notificationData); - } - - return notificationData; - } - - /* - * Set and persist NotificationData in the cache - */ - public static void setNotificationData(long reportID, NotificationData data) { - if (cache == null) { - cache = readFromInternalStorage(); - } - - cache.put(Long.toString(reportID), data); - writeToInternalStorage(); - } - - private static void writeToInternalStorage() { - Context context = UAirship.getApplicationContext(); - - FileOutputStream fos = null; - ObjectOutputStream oos = null; - try { - File outputFile = new File(context.getFilesDir(), CACHE_FILE_NAME); - fos = new FileOutputStream(outputFile); - oos = new ObjectOutputStream(fos); - oos.writeObject(cache); - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (oos != null) { - oos.close(); - } - if (fos != null) { - fos.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - private static HashMap readFromInternalStorage() { - HashMap result; - Context context = UAirship.getApplicationContext(); - - FileInputStream fis = null; - ObjectInputStream ois = null; - try { - File fileCache = new File(context.getFilesDir(), CACHE_FILE_NAME); - fis = new FileInputStream(fileCache); - ois = new ObjectInputStream(fis); - result = (HashMap) ois.readObject(); - } catch (IOException | ClassNotFoundException e) { - e.printStackTrace(); - result = new HashMap<>(); - } finally { - try { - if (ois != null) { - ois.close(); - } - if (fis != null) { - fis.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - return result; - } - - /** - * A class for caching data for notifications. We use this to track active notifications so we - * can thread related notifications together - */ - public static class NotificationData implements Serializable { - private final HashMap names = new HashMap<>(); - - // A map of accountID => base64 encoded Bitmap - // In order to make Bitmaps serializable, we encode them as base64 strings - private final HashMap icons = new HashMap<>(); - public ArrayList messages = new ArrayList<>(); - - public int prevNotificationID = -1; - - public NotificationData() {} - - public Bitmap getIcon(String accountID) { - return decodeToBitmap(icons.get(accountID)); - } - - public void putIcon(String accountID, Bitmap bitmap) { - icons.put(accountID, encodeToBase64(bitmap)); - } - - public Person getPerson(String accountID) { - if (!names.containsKey(accountID) || !icons.containsKey(accountID)) { - return null; - } - - String name = names.get(accountID); - Bitmap icon = getIcon(accountID); - - return new Person.Builder() - .setIcon(IconCompat.createWithBitmap(icon)) - .setKey(accountID) - .setName(name) - .build(); - } - - public void putPerson(String accountID, String name, Bitmap icon) { - names.put(accountID, name); - putIcon(accountID, icon); - } - - public static String encodeToBase64(Bitmap bitmap) { - if (bitmap == null) { - return ""; - } - - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); - byte[] byteArray = byteArrayOutputStream.toByteArray(); - return Base64.encodeToString(byteArray, Base64.DEFAULT); - } - - public static Bitmap decodeToBitmap(String base64String) { - if (base64String == null) { - return null; - } - - byte[] decodedBytes = Base64.decode(base64String, Base64.DEFAULT); - return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length); - } - } - - public static class NotificationMessage implements Serializable { - public String accountID; - public String text; - public long time; - - NotificationMessage(String accountID, String text, long time) { - this.accountID = accountID; - this.text = text; - this.time = time; - } - } -} diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index b4d8c2181b0b..94065c5b9d19 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,4 +1,5 @@ + #03D47C #FFFFFF #03D47C diff --git a/assets/images/google-meet.svg b/assets/images/google-meet.svg deleted file mode 100644 index 8def88aa6edc..000000000000 --- a/assets/images/google-meet.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/olddot-wireframe.svg b/assets/images/olddot-wireframe.svg new file mode 100644 index 000000000000..ee9aa93be255 --- /dev/null +++ b/assets/images/olddot-wireframe.svg @@ -0,0 +1,3422 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__gears.svg b/assets/images/simple-illustrations/simple-illustration__gears.svg new file mode 100644 index 000000000000..3b4cbc001e3b --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__gears.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__lockclosed.svg b/assets/images/simple-illustrations/simple-illustration__lockclosed.svg new file mode 100644 index 000000000000..3779b92b0b0f --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__lockclosed.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__palmtree.svg b/assets/images/simple-illustrations/simple-illustration__palmtree.svg new file mode 100644 index 000000000000..2aef4956cde9 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__palmtree.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__profile.svg b/assets/images/simple-illustrations/simple-illustration__profile.svg new file mode 100644 index 000000000000..85312f26e186 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__profile.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__qr-code.svg b/assets/images/simple-illustrations/simple-illustration__qr-code.svg new file mode 100644 index 000000000000..10268d747588 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__qr-code.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/zoom-icon.svg b/assets/images/zoom-icon.svg deleted file mode 100644 index 81f025aedf79..000000000000 --- a/assets/images/zoom-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index 0a17f2b0f01c..d3bcecdae8cb 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,5 +1,7 @@ require('dotenv').config(); +const IS_E2E_TESTING = process.env.E2E_TESTING === 'true'; + const defaultPresets = ['@babel/preset-react', '@babel/preset-env', '@babel/preset-flow', '@babel/preset-typescript']; const defaultPlugins = [ // Adding the commonjs: true option to react-native-web plugin can cause styling conflicts @@ -72,7 +74,8 @@ const metro = { ], env: { production: { - plugins: [['transform-remove-console', {exclude: ['error', 'warn']}]], + // Keep console logs for e2e tests + plugins: IS_E2E_TESTING ? [] : [['transform-remove-console', {exclude: ['error', 'warn']}]], }, }, }; diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index 543b133fe62b..5bb6dfb85851 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -30,7 +30,7 @@ When creating RHP flows, you have to remember a couple things: - Since you can deeplink to different pages inside the RHP navigator, it is important to provide the possibility for the user to properly navigate back from any page with UP press (`HeaderWithBackButton` component). -- An example can be deeplinking to `/settings/profile/personal-details`. From there, when pressing the UP button, you should navigate to `/settings/profile`, so in order for it to work, you should provide the correct route in `onBackButtonPress` prop of `HeaderWithBackButton` (`Navigation.goBack(ROUTES.SETTINGS_PROFILE)` in this example). +- An example can be deeplinking to `/settings/profile/timezone/select`. From there, when pressing the UP button, you should navigate to `/settings/profile/timezone`, so in order for it to work, you should provide the correct route in `onBackButtonPress` prop of `HeaderWithBackButton` (`Navigation.goBack(ROUTES.SETTINGS_PROFILE)` in this example). - We use a custom `goBack` function to handle the browser and the `react-navigation` history stack. Under the hood, it resolves to either replacing the current screen with the one we navigate to (deeplinking scenario) or just going back if we reached the current page by navigating in App (pops the screen). It ensures the requested behaviors on web, which is navigating back to the place from where you deeplinked when going into the RHP flow by it. diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml index 33ef4ebcf0a8..d355e53d8a52 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -19,8 +19,8 @@ platforms: icon: /assets/images/accounting.svg description: From setting up your account to ensuring you get the most out of Expensify’s suite of features, click here to get started on streamlining your expense management journey. - - href: account-settings - title: Account Settings + - href: settings + title: Settings icon: /assets/images/gears.svg description: Discover how to personalize your profile, add secondary logins, and grant delegated access to employees with our comprehensive guide on Account Settings. diff --git a/docs/articles/expensify-classic/getting-started/Individual-Users.md b/docs/articles/expensify-classic/getting-started/Individual-Users.md deleted file mode 100644 index 12029f80388b..000000000000 --- a/docs/articles/expensify-classic/getting-started/Individual-Users.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Individual Users -description: Learn how Expensify can help you track and submit your personal or self-employed business expenses. ---- -# Overview -If you're an individual using Expensify, the Track and Submit plans are designed to assist self-employed users in effectively managing both their personal and business finances. - -# How to use the Track plan - -The Track plan is tailored for solo Expensify users who don't require expense submission to others. Individuals or sole proprietors can choose the Track plan to customize their Individual Workspace to align with their personal expense tracking requirements. - -You can select the Track plan from the Workspace settings. Navigate to **Settings > Workspace > Individual > *[Workspace Name]* > Plan** to select Track. -You can also do this from the Pricing page at https://www.expensify.com/pricing. - -The Track plan includes a predefined set of categories designed to align with IRS Schedule C expense categories. However, you have the flexibility to add extra categories as needed. For a more detailed breakdown, you can also set up tags to create another layer of coding. - -The Track plan offers 25 free SmartScans per month. If you require more than 25 SmartScans, you can upgrade to a Monthly Individual subscription at a cost of $4.99 USD per month. - -# How to use the Submit plan -The Submit plan is designed for individuals who need to keep track of their expenses and share them with someone else, such as their boss, accountant, or even a housemate. It's specifically tailored for single users who want to both track and submit their expenses efficiently. - -You can select the Track plan from the Workspace settings. Navigate to **Settings > Workspaces > Individual > *[Workspace Name]* > Plan** to select "Submit" or from the Pricing page at https://www.expensify.com/pricing. - -You will select who your expenses get sent to under **Settings > Workspace > Individual > *[Workspace Name]* > Reports**. If the recipient already has an Expensify account, they'll be able to see the report directly in the Expensify app. Otherwise, non-Expensify users will receive a PDF copy of the report attached to the email so it can be processed. - -The Submit plan includes a predefined set of categories designed to align with IRS Schedule C expense categories. However, you have the flexibility to add extra categories as needed. For a more detailed breakdown, you can also set up tags to create another layer of coding. - -The Submit plan offers 25 free SmartScans per month.If you require more than 25 SmartScans, you can upgrade to a Monthly Individual subscription at a cost of $4.99 USD per month. - -# FAQ - -## Who should use the Track plan? -An individual who wants to store receipts, look to track spending by category to help with budgeting and a self-employed user who needs to track receipts and mileage for tax purposes. - -## Who should use the Submit plan? -An individual who seeks to utilize the features of the track plan to monitor their expenses while also requiring the ability to submit those expenses to someone else. - -## How can I keep track of personal and business expenses in the same account? -You have the capability to create distinct "business" and "personal" tags and assign them to your expenses for proper categorization. By doing so, you can effectively code your expenses based on their nature. Additionally, you can utilize filters to ensure that you only view the expenses that are relevant to your specific needs, whether they are business-related or personal. - -## How can I export expenses for tax purposes? -From the expense page, you have the option to select all of your expenses and export them to a CSV (Comma-Separated Values) file. This CSV file can be conveniently imported directly into your tax software for easier tax preparation. - diff --git a/docs/articles/expensify-classic/getting-started/Join-your-company's-workspace.md b/docs/articles/expensify-classic/getting-started/Join-your-company's-workspace.md index 099f381e6010..a31b972e683c 100644 --- a/docs/articles/expensify-classic/getting-started/Join-your-company's-workspace.md +++ b/docs/articles/expensify-classic/getting-started/Join-your-company's-workspace.md @@ -212,7 +212,6 @@ Once you’ve created your expenses, they may be automatically added to an expen
  • Attach PDF: Select this checkbox to attach a copy of your report to the email.
  • Tap Submit.
  • - {% include end-option.html %} @@ -255,4 +254,4 @@ Add an extra layer of security to help keep your financial data safe and secure When you log in to Expensify in the future, you’ll open your authenticator app to get the code and enter it into Expensify. A new code regenerates every few seconds, so the code is always different. If the code time runs out, you can generate a new code as needed. - \ No newline at end of file + diff --git a/docs/articles/expensify-classic/getting-started/Using-The-App.md b/docs/articles/expensify-classic/getting-started/Using-The-App.md deleted file mode 100644 index f1bc31793ba8..000000000000 --- a/docs/articles/expensify-classic/getting-started/Using-The-App.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Using the app -description: Streamline expense management effortlessly with the Expensify mobile app. Learn how to install, enable push notifications, and use SmartScan to capture, categorize, and track expenses. Versatile for personal and business use, Expensify is a secure and automated solution for managing your finances on the go. ---- - -
    -# Overview -The Expensify mobile app is the ultimate expense management solution that makes it effortless to track and submit your receipts and expenses. Use the app to snap a picture of your receipts, categorize and submit expenses, and even review and approve expense reports. -# How to install the Expensify app -To get started with Expensify on your mobile device, you need to download the app: -1. Visit the App Store (iOS) or Google Play Store (Android). -2. Search for "Expensify" and select the official Expensify app. -3. Tap "Download" or "Install." - -Once the app is installed, open it and log in with your Expensify credentials. If you don't have an Expensify account, you can create one during the sign-up process. -# How to enable on push notifications -Push notifications keep you informed about expense approvals, reimbursements, and more. To enable push notifications: -1. Open the Expensify app. -2. Go to "Settings" or "Preferences." -3. Find the "Receive realtime alerts" toggle -4. Toggle realtime alerts on to begin receiving notifications - -# Deep dive -## Using SmartScan on the App -### Capture receipts -1. Open the Expensify mobile app. -2. Tap the green camera button to take a photo of a receipt. -3. The receipt will be SmartScanned automatically. - -If you have multiple receipts tap the Rapid Fire Mode button in the bottom right hand corner to snap multiple pictures. You can also upload an existing photo from your gallery. -### SmartScan analysis -After capturing or uploading a receipt, Expensify's SmartScan technology goes to work. It analyzes the receipt to extract key details such as the merchant's name, transaction date, transaction currency, and total amount spent. SmartScan inputs all the data for you, so you don’t have to type a thing. -### Review and edit -Once SmartScan is finished, you can further categorize and code your expense based on your company’s policy. Review this data to ensure accuracy. If necessary, you can edit or add additional details, such as expense categories, tags, attendees, tax rates, or descriptions. -### Multi-Currency support -For businesses dealing with international expenses, SmartScan can handle multiple currencies and provide accurate exchange rate conversion based on your policies reporting currency. It's essential to set up and configure currency preferences for these scenarios. -### Custom expense categories -SmartScan can automatically categorize expenses based on vendor or merchant. Users can customize these categories to suit their specific accounting needs. This can be particularly useful for tracking expenses across different departments or projects. -### SmartScan outcomes -SmartScan's performance can vary depending on factors such as receipt quality, language, and handwriting. It's important to keep the following variables in mind: -**Receipt quality**: The clarity and condition of a receipt can impact SmartScan's accuracy. For best results, ensure your environment is well-lit and the receipt is straight and free of obstructions. -**Language support**: While SmartScan supports multiple languages, its accuracy may differ from one language to another. Users dealing with non-English receipts should be aware of potential variations in data extraction. -**Handwriting recognition**: Handwritten receipts might pose challenges for SmartScan. In such cases, manual verification may be necessary to ensure accurate data entry. - -{% include faq-begin.md %} - -## Can I use the mobile app for both personal and business expenses? -Yes, you can use Expensify for personal and business expenses. It's versatile and suitable for both individual and corporate use. Check out our personal and business plans [here](https://www.expensify.com/pricing) to see what might be right for you. -## Is it possible to categorize and tag expenses on the mobile app? -Yes, you can categorize and tag expenses on the mobile app. The app allows you to customize categories and tags to help organize and track your spending. -## What should I do if I encounter issues with the mobile app, such as login problems or crashes? -If you experience issues, first make sure you’re using the most recent version of the app. You can also try to restarting the app. If the issue persists, you can start a chat with Concierge in the app or write to [concierge@expensify.com](mailto:concierge@expensify.com). -## Is the mobile app secure for managing sensitive financial information? -Expensify takes security seriously and employs encryption and other security measures to protect your data. It's important to use strong, unique passwords and enable device security features like biometric authentication. -## Can I use the mobile app offline, and will my data sync when I'm back online? -Yes, you can use the mobile app offline to capture receipts and create expenses. The app will sync your data once you have an internet connection. - -{% include faq-end.md %} -
    -
    - -# Coming soon - - -
    \ No newline at end of file diff --git a/docs/articles/expensify-classic/account-settings/Account-Details.md b/docs/articles/expensify-classic/settings/Account-Details.md similarity index 100% rename from docs/articles/expensify-classic/account-settings/Account-Details.md rename to docs/articles/expensify-classic/settings/Account-Details.md diff --git a/docs/articles/expensify-classic/account-settings/Close-Account.md b/docs/articles/expensify-classic/settings/Close-Account.md similarity index 100% rename from docs/articles/expensify-classic/account-settings/Close-Account.md rename to docs/articles/expensify-classic/settings/Close-Account.md diff --git a/docs/articles/expensify-classic/account-settings/Copilot.md b/docs/articles/expensify-classic/settings/Copilot.md similarity index 100% rename from docs/articles/expensify-classic/account-settings/Copilot.md rename to docs/articles/expensify-classic/settings/Copilot.md diff --git a/docs/articles/expensify-classic/account-settings/Merge-Accounts.md b/docs/articles/expensify-classic/settings/Merge-Accounts.md similarity index 100% rename from docs/articles/expensify-classic/account-settings/Merge-Accounts.md rename to docs/articles/expensify-classic/settings/Merge-Accounts.md diff --git a/docs/articles/expensify-classic/account-settings/Notification-Troubleshooting.md b/docs/articles/expensify-classic/settings/Notification-Troubleshooting.md similarity index 100% rename from docs/articles/expensify-classic/account-settings/Notification-Troubleshooting.md rename to docs/articles/expensify-classic/settings/Notification-Troubleshooting.md diff --git a/docs/articles/expensify-classic/account-settings/Preferences.md b/docs/articles/expensify-classic/settings/Preferences.md similarity index 100% rename from docs/articles/expensify-classic/account-settings/Preferences.md rename to docs/articles/expensify-classic/settings/Preferences.md diff --git a/docs/expensify-classic/hubs/account-settings/index.html b/docs/expensify-classic/hubs/settings/index.html similarity index 100% rename from docs/expensify-classic/hubs/account-settings/index.html rename to docs/expensify-classic/hubs/settings/index.html diff --git a/docs/redirects.csv b/docs/redirects.csv index df615431533f..674e5c32ab04 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -34,8 +34,8 @@ https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-C 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#gsc.tab=0 -https://community.expensify.com/discussion/4783/how-to-add-or-remove-a-copilot#latest,https://help.expensify.com/articles/expensify-classic/account-settings/Copilot#gsc.tab=0 +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/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 https://community.expensify.com/discussion/2673/personalize-your-commercial-card-feed-name,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds @@ -48,4 +48,7 @@ https://community.expensify.com/discussion/4463/how-to-remove-or-manage-settings https://community.expensify.com/discussion/5793/how-to-connect-your-personal-card-to-import-expenses,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards https://community.expensify.com/discussion/4826/how-to-set-your-annual-subscription-size,https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription https://community.expensify.com/discussion/5667/deep-dive-how-does-the-annual-subscription-billing-work,https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription -https://help.expensify.com/expensify-classic/hubs/getting-started/plan-types,https://www.expensify.com/pricing +https://help.expensify.com/articles/expensify-classic/getting-started/approved-accountants/Your-Expensify-Partner-Manager,https://help.expensify.com/articles/expensify-classic/expensify-partner-program/Your-Expensify-Partner-Manager +https://help.expensify.com/expensify-classic/hubs/getting-started/plan-types,https://use.expensify.com/ +https://help.expensify.com/articles/expensify-classic/getting-started/Employees,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace +https://help.expensify.com/articles/expensify-classic/getting-started/Using-The-App,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg index 454857981834c86eb6bf9f6841c49de4253f0fcf..643c81bd0b9cf8a4b18f31a173581d6bf6df785b 100644 GIT binary patch literal 11432 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 diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg index ba92588406385474af829bbdfc8623954388da04..8a5170cfe69758c440e0e5b0cf37f8e62b64dbbc 100644 GIT binary patch literal 11024 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 KJt|k$8xI{92zNRF literal 10923 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 APPL CFBundleShortVersionString - 1.4.36 + 1.4.39 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.36.0 + 1.4.39.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensify/RCTBootSplash.h b/ios/NewExpensify/RCTBootSplash.h index df38a5eb35bf..c25c676feb4a 100644 --- a/ios/NewExpensify/RCTBootSplash.h +++ b/ios/NewExpensify/RCTBootSplash.h @@ -10,6 +10,7 @@ @interface RCTBootSplash : NSObject ++ (void)invalidateBootSplash; + (void)initWithStoryboard:(NSString * _Nonnull)storyboardName rootView:(RCTRootView * _Nullable)rootView; diff --git a/ios/NewExpensify/RCTBootSplash.m b/ios/NewExpensify/RCTBootSplash.m index bceac70efdcf..6c2baaed4ee0 100644 --- a/ios/NewExpensify/RCTBootSplash.m +++ b/ios/NewExpensify/RCTBootSplash.m @@ -26,6 +26,12 @@ - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } ++ (void)invalidateBootSplash { + _resolverQueue = nil; + _rootView = nil; + _nativeHidden = false; +} + + (void)initWithStoryboard:(NSString * _Nonnull)storyboardName rootView:(RCTRootView * _Nullable)rootView { if (rootView == nil || _rootView != nil || RCTRunningInAppExtension()) @@ -102,6 +108,9 @@ + (void)onContentDidAppear { block:^(NSTimer * _Nonnull timer) { [timer invalidate]; + if (_rootView == nil) + return; + if (_resolverQueue == nil) _resolverQueue = [[NSMutableArray alloc] init]; diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 763896fd45f1..cc3f0b8489b7 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.36 + 1.4.39 CFBundleSignature ???? CFBundleVersion - 1.4.36.0 + 1.4.39.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index a4bef0847c95..1f1e1e1ddfce 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.36 + 1.4.39 CFBundleVersion - 1.4.36.0 + 1.4.39.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 584dd5636773..1664c982ce50 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1453,7 +1453,7 @@ PODS: - SDWebImage/Core (~> 5.17) - SocketRocket (0.6.1) - Turf (2.7.0) - - VisionCamera (2.16.2): + - VisionCamera (2.16.5): - React - React-callinvoker - React-Core @@ -1922,7 +1922,7 @@ SPEC CHECKSUMS: react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d - react-native-live-markdown: 1ca4275d4dba8eebb736a005148f24a8224f54d9 + react-native-live-markdown: 81ac491b913cb8541a06586adcdc159ab1b86fb5 react-native-netinfo: 8a7fd3f7130ef4ad2fb4276d5c9f8d3f28d2df3d react-native-pager-view: 02a5c4962530f7efc10dd51ee9cdabeff5e6c631 react-native-pdf: b4ca3d37a9a86d9165287741c8b2ef4d8940c00e @@ -1980,7 +1980,7 @@ SPEC CHECKSUMS: SDWebImageWebPCoder: af09429398d99d524cae2fe00f6f0f6e491ed102 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 - VisionCamera: 7d13aae043ffb38b224a0f725d1e23ca9c190fe7 + VisionCamera: fda554d8751e395effcc87749f8b7c198c1031be Yoga: 13c8ef87792450193e117976337b8527b49e8c03 PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2 diff --git a/jest.config.js b/jest.config.js index de7ed4b1f974..b347db593d83 100644 --- a/jest.config.js +++ b/jest.config.js @@ -22,8 +22,8 @@ module.exports = { doNotFake: ['nextTick'], }, testEnvironment: 'jsdom', - setupFiles: ['/jest/setup.js', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'], - setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect', '/jest/setupAfterEnv.js', '/tests/perf-test/setupAfterEnv.js'], + setupFiles: ['/jest/setup.ts', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'], + setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect', '/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.js'], cacheDirectory: '/.jest-cache', moduleNameMapper: { '\\.(lottie)$': '/__mocks__/fileMock.js', diff --git a/jest/setup.js b/jest/setup.ts similarity index 90% rename from jest/setup.js rename to jest/setup.ts index e82bf678941d..68d904fac5be 100644 --- a/jest/setup.js +++ b/jest/setup.ts @@ -1,6 +1,7 @@ import mockClipboard from '@react-native-clipboard/clipboard/jest/clipboard-mock'; import '@shopify/flash-list/jestSetup'; import 'react-native-gesture-handler/jestSetup'; +import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; import * as reanimatedJestUtils from 'react-native-reanimated/src/reanimated2/jestUtils'; import 'setimmediate'; import setupMockImages from './setupMockImages'; @@ -19,7 +20,7 @@ jest.mock('@react-native-clipboard/clipboard', () => mockClipboard); // Mock react-native-onyx storage layer because the SQLite storage layer doesn't work in jest. // Mocking this file in __mocks__ does not work because jest doesn't support mocking files that are not directly used in the testing project, // and we only want to mock the storage layer, not the whole Onyx module. -jest.mock('react-native-onyx/dist/storage', () => require('react-native-onyx/dist/storage/__mocks__')); +jest.mock('react-native-onyx/dist/storage', () => mockStorage); // Turn off the console logs for timing events. They are not relevant for unit tests and create a lot of noise jest.spyOn(console, 'debug').mockImplementation((...params) => { @@ -34,6 +35,6 @@ jest.spyOn(console, 'debug').mockImplementation((...params) => { // This mock is required for mocking file systems when running tests jest.mock('react-native-fs', () => ({ - unlink: jest.fn(() => new Promise((res) => res())), + unlink: jest.fn(() => new Promise((res) => res())), CachesDirectoryPath: jest.fn(), })); diff --git a/jest/setupAfterEnv.js b/jest/setupAfterEnv.ts similarity index 100% rename from jest/setupAfterEnv.js rename to jest/setupAfterEnv.ts diff --git a/jest/setupMockImages.js b/jest/setupMockImages.ts similarity index 87% rename from jest/setupMockImages.js rename to jest/setupMockImages.ts index 10925aca8736..c48797b3c07b 100644 --- a/jest/setupMockImages.js +++ b/jest/setupMockImages.ts @@ -1,14 +1,10 @@ import fs from 'fs'; import path from 'path'; -import _ from 'underscore'; -/** - * @param {String} imagePath - */ -function mockImages(imagePath) { +function mockImages(imagePath: string) { const imageFilenames = fs.readdirSync(path.resolve(__dirname, `../assets/${imagePath}/`)); // eslint-disable-next-line rulesdir/prefer-early-return - _.each(imageFilenames, (fileName) => { + imageFilenames.forEach((fileName) => { if (/\.svg/.test(fileName)) { jest.mock(`../assets/${imagePath}/${fileName}`, () => () => ''); } diff --git a/package-lock.json b/package-lock.json index 674325508da5..e6096fb82317 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "1.4.36-0", + "version": "1.4.39-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.36-0", + "version": "1.4.39-1", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#c316611781f19815caebfed5540e0faf2a274785", + "@expensify/react-native-live-markdown": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#2ed4240336e50abb4a7fa9ff6a3c180f8bc9ce5b", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", @@ -21,11 +21,11 @@ "@formatjs/intl-pluralrules": "^5.2.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", - "@kie/act-js": "^2.0.1", + "@kie/act-js": "^2.6.0", "@kie/mock-github": "^1.0.0", "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@onfido/react-native-sdk": "8.3.0", - "@react-native-async-storage/async-storage": "^1.19.5", + "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "5.4.0", "@react-native-clipboard/clipboard": "^1.12.1", "@react-native-community/geolocation": "^3.0.6", @@ -96,12 +96,12 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.1", + "react-native-onyx": "2.0.2", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -113,7 +113,7 @@ "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", - "react-native-vision-camera": "^2.16.2", + "react-native-vision-camera": "2.16.8", "react-native-web": "^0.19.9", "react-native-web-linear-gradient": "^1.1.2", "react-native-webview": "13.6.3", @@ -199,7 +199,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^25.9.4", + "electron": "^26.6.8", "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -232,6 +232,7 @@ "shellcheck": "^1.1.0", "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", + "ts-node": "^10.9.2", "type-fest": "^3.12.0", "typescript": "^5.3.2", "wait-port": "^0.2.9", @@ -2887,6 +2888,28 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -3352,8 +3375,8 @@ }, "node_modules/@expensify/react-native-live-markdown": { "version": "0.1.0", - "resolved": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#c316611781f19815caebfed5540e0faf2a274785", - "integrity": "sha512-yF3oaBhqWQonl12LPELYLsgfmqCsGg2bu15g/h8XzVX3f/nzfPtrWE/ax2lWEIpIjk4/+aEu/VGNKLnlehjTxQ==", + "resolved": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#2ed4240336e50abb4a7fa9ff6a3c180f8bc9ce5b", + "integrity": "sha512-i+HIsCFL9cdma+saH/KN2llTGqEb2DQttEJKozdm4fvcie9Ce2/q7XNDZo6nIYTbIVXPDLKPDmWLXqXTgLBKDQ==", "license": "MIT", "workspaces": [ "example" @@ -7186,9 +7209,9 @@ "dev": true }, "node_modules/@kie/act-js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@kie/act-js/-/act-js-2.3.0.tgz", - "integrity": "sha512-Q9k0b05uA46jXKWmVfoGDW+0xsCcE7QPiHi8IH7h41P36DujHKBj4k28RCeIEx3IwUCxYHKwubN8DH4Vzc9XcA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@kie/act-js/-/act-js-2.6.0.tgz", + "integrity": "sha512-AkBTAsvRWLMvJ0cBiZ0+XUC9QVNwJzcZFxZJV2i+oaXPirafCyzzmT7+cJ4t0YNFMkgM+EEN7XMWr2MgoSyOvg==", "hasInstallScript": true, "dependencies": { "@kie/mock-github": "^2.0.0", @@ -20161,6 +20184,30 @@ "node": ">=10.13.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "devOptional": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true + }, "node_modules/@turf/along": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@turf/along/-/along-6.5.0.tgz", @@ -22780,7 +22827,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/argparse": { @@ -27256,6 +27303,12 @@ "sha.js": "^2.4.8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true + }, "node_modules/cross-fetch": { "version": "3.1.5", "license": "MIT", @@ -28151,6 +28204,15 @@ "detect-port": "bin/detect-port.js" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", @@ -28529,9 +28591,9 @@ } }, "node_modules/electron": { - "version": "25.9.4", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.9.4.tgz", - "integrity": "sha512-5pDU8a7o7ZIPTZHAqjflGMq764Favdsc271KXrAT3oWvFTHs5Ve9+IOt5EUVPrwvC2qRWKpCIEM47WzwkTlENQ==", + "version": "26.6.8", + "resolved": "https://registry.npmjs.org/electron/-/electron-26.6.8.tgz", + "integrity": "sha512-nuzJ5nVButL1jErc97IVb+A6jbContMg5Uuz5fhmZ4NLcygLkSW8FZpnOT7A4k8Saa95xDJOvqGZyQdI/OPNFw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -32351,9 +32413,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -39902,6 +39964,12 @@ "semver": "bin/semver" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true + }, "node_modules/make-event-props": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.1.tgz", @@ -45044,9 +45112,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.1.tgz", - "integrity": "sha512-o6QNvq91qg8hFXIhmHjBqlNXD/YZxBZSRN8Vkq7xD2NYskzxK2mLqhBdhB8yMMwe6Cd8sVUK4vlZax/JU79xYw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.2.tgz", + "integrity": "sha512-24kcG3ChBXp+uSSCXudFvZTdCnKLRHQRgvTcnh2eA7COtKvbL8ggEJNkglSYmcf5WoDzLgYyWiKvcjcXQnmBvw==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -45138,8 +45206,8 @@ }, "node_modules/react-native-picker-select": { "version": "8.1.0", - "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", - "integrity": "sha512-NpXXyK+UuANYOysjUb9pCoq9SookRYPfpOcM4shxOD4+2Fkh7TYt2LBUpAdBicMHmtaR43RWXVQk9pMimOhg2w==", + "resolved": "git+ssh://git@github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", + "integrity": "sha512-e8TAWVR4AEw2PFGFxlevCBFr1RwvwTqq1M2w9Yi6xNz+d4SbG6tDIcJDNIqt0gyBqvxlL7BuK0G5BjbiZDLKsg==", "license": "MIT", "dependencies": { "lodash.isequal": "^4.5.0" @@ -45329,9 +45397,9 @@ } }, "node_modules/react-native-vision-camera": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/react-native-vision-camera/-/react-native-vision-camera-2.16.2.tgz", - "integrity": "sha512-QIpG33l3QB0AkTfX/ccRknwNRu1APNUkokVKF1lpRO2+tBnkXnGL0UapgXg5u9KIONZtrpupeDeO+J5B2TeQVw==", + "version": "2.16.8", + "resolved": "https://registry.npmjs.org/react-native-vision-camera/-/react-native-vision-camera-2.16.8.tgz", + "integrity": "sha512-DCYkhxHw0p8dftfYxkfyeECraPOCliNWriVUTe+qit16ejs9fXoW1zXJBq48UbysUTuIOk8QwTn9OSy4jhGvTg==", "peerDependencies": { "react": "*", "react-native": "*" @@ -50519,6 +50587,70 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "devOptional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "devOptional": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ts-object-utils": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz", @@ -50760,7 +50892,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -51510,6 +51642,12 @@ "dev": true, "license": "MIT" }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true + }, "node_modules/v8-to-istanbul": { "version": "9.0.1", "license": "ISC", @@ -53360,6 +53498,15 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 0c1105f978c6..84b8a826acfc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.36-0", + "version": "1.4.39-1", "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.", @@ -19,15 +19,15 @@ "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", "start": "npx react-native start", "web": "scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server", - "web-proxy": "node web/proxy.js", + "web-proxy": "ts-node web/proxy.js", "web-server": "webpack-dev-server --open --config config/webpack/webpack.dev.js", "build": "webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "build-staging": "webpack --config config/webpack/webpack.common.js --env envFile=.env.staging", "build-adhoc": "webpack --config config/webpack/webpack.common.js --env envFile=.env.adhoc", - "desktop": "scripts/set-pusher-suffix.sh && node desktop/start.js", + "desktop": "scripts/set-pusher-suffix.sh && ts-node desktop/start.js", "desktop-build": "scripts/build-desktop.sh production", "desktop-build-staging": "scripts/build-desktop.sh staging", - "createDocsRoutes": "node .github/scripts/createDocsRoutes.js", + "createDocsRoutes": "ts-node .github/scripts/createDocsRoutes.js", "desktop-build-adhoc": "scripts/build-desktop.sh adhoc", "ios-build": "fastlane ios build", "android-build": "fastlane android build", @@ -50,16 +50,16 @@ "analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map", "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", - "test:e2e": "node tests/e2e/testRunner.js --development --skipCheckout --skipInstallDeps --buildMode none", - "test:e2e:dev": "node tests/e2e/testRunner.js --development --skipCheckout --config ./config.dev.js --buildMode skip --skipInstallDeps", + "test:e2e": "ts-node tests/e2e/testRunner.js --development --skipCheckout --skipInstallDeps --buildMode none", + "test:e2e:dev": "ts-node tests/e2e/testRunner.js --development --skipCheckout --config ./config.dev.js --buildMode skip --skipInstallDeps", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", - "workflow-test:generate": "node workflow_tests/utils/preGenerateTest.js", + "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.js", "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1" }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#c316611781f19815caebfed5540e0faf2a274785", + "@expensify/react-native-live-markdown": "git+ssh://git@github.com/Expensify/react-native-live-markdown.git#2ed4240336e50abb4a7fa9ff6a3c180f8bc9ce5b", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", @@ -69,11 +69,11 @@ "@formatjs/intl-pluralrules": "^5.2.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", - "@kie/act-js": "^2.0.1", + "@kie/act-js": "^2.6.0", "@kie/mock-github": "^1.0.0", "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", "@onfido/react-native-sdk": "8.3.0", - "@react-native-async-storage/async-storage": "^1.19.5", + "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "5.4.0", "@react-native-clipboard/clipboard": "^1.12.1", "@react-native-community/geolocation": "^3.0.6", @@ -144,12 +144,12 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.1", + "react-native-onyx": "2.0.2", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.9.3", - "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7a407cd4174d9838a944c1c2e1cb4a9737ac69c5", + "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#42b334d0c4e71d225601f72828d3dedd0bc22212", "react-native-plaid-link-sdk": "10.8.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "^8.0.0-beta.2", @@ -161,7 +161,7 @@ "react-native-tab-view": "^3.5.2", "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "3.8.0", - "react-native-vision-camera": "^2.16.2", + "react-native-vision-camera": "2.16.8", "react-native-web": "^0.19.9", "react-native-web-linear-gradient": "^1.1.2", "react-native-webview": "13.6.3", @@ -247,7 +247,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^25.9.4", + "electron": "^26.6.8", "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -280,6 +280,7 @@ "shellcheck": "^1.1.0", "style-loader": "^2.0.0", "time-analytics-webpack-plugin": "^0.1.17", + "ts-node": "^10.9.2", "type-fest": "^3.12.0", "typescript": "^5.3.2", "wait-port": "^0.2.9", diff --git a/patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch b/patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch index c613a47a3072..d5a390daf201 100644 --- a/patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch +++ b/patches/@oguzhnatly+react-native-image-manipulator+1.0.5.patch @@ -7,13 +7,13 @@ index 3a1a548..fe030bb 100644 android { - compileSdkVersion 28 -+ compileSdkVersion 30 ++ compileSdkVersion 34 buildToolsVersion "28.0.3" defaultConfig { minSdkVersion 16 - targetSdkVersion 28 -+ targetSdkVersion 30 ++ targetSdkVersion 34 versionCode 1 versionName "1.0" } diff --git a/patches/@react-native+virtualized-lists+0.73.4+001+onStartReched.patch b/patches/@react-native+virtualized-lists+0.73.4+001+onStartReched.patch new file mode 100644 index 000000000000..b183124964f6 --- /dev/null +++ b/patches/@react-native+virtualized-lists+0.73.4+001+onStartReched.patch @@ -0,0 +1,32 @@ +diff --git a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +index 0516679..e338d90 100644 +--- a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js ++++ b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +@@ -1546,7 +1546,7 @@ class VirtualizedList extends StateSafePureComponent { + // Next check if the user just scrolled within the start threshold + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed +- else if ( ++ if ( + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && +@@ -1558,13 +1558,11 @@ class VirtualizedList extends StateSafePureComponent { + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again +- else { +- this._sentStartForContentLength = isWithinStartThreshold +- ? this._sentStartForContentLength +- : 0; +- this._sentEndForContentLength = isWithinEndThreshold +- ? this._sentEndForContentLength +- : 0; ++ if (!isWithinStartThreshold) { ++ this._sentStartForContentLength = 0; ++ } ++ if (!isWithinEndThreshold) { ++ this._sentEndForContentLength = 0; + } + } + diff --git a/patches/@react-native-camera-roll+camera-roll+5.4.0.patch b/patches/@react-native-camera-roll+camera-roll+5.4.0.patch new file mode 100644 index 000000000000..f0429bc10125 --- /dev/null +++ b/patches/@react-native-camera-roll+camera-roll+5.4.0.patch @@ -0,0 +1,15 @@ +diff --git a/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle b/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle +index 3f76132..63dc946 100644 +--- a/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle ++++ b/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle +@@ -81,7 +81,9 @@ def findNodeModulePath(baseDir, packageName) { + } + + def resolveReactNativeDirectory() { +- def reactNative = file("${findNodeModulePath(rootProject.projectDir, "react-native")}") ++ def projectDir = this.hasProperty('reactNativeProject') ? this.reactNativeProject : rootProject.projectDir ++ def modulePath = file(projectDir); ++ def reactNative = file("${findNodeModulePath(modulePath, 'react-native')}") + if (reactNative.exists()) { + return reactNative + } diff --git a/patches/@react-native-community+cli-platform-android+12.3.0.patch b/patches/@react-native-community+cli-platform-android+12.3.0.patch new file mode 100644 index 000000000000..d94baf0f9c6e --- /dev/null +++ b/patches/@react-native-community+cli-platform-android+12.3.0.patch @@ -0,0 +1,52 @@ +diff --git a/node_modules/@react-native-community/cli-platform-android/native_modules.gradle b/node_modules/@react-native-community/cli-platform-android/native_modules.gradle +index bbfa7f7..ed53872 100644 +--- a/node_modules/@react-native-community/cli-platform-android/native_modules.gradle ++++ b/node_modules/@react-native-community/cli-platform-android/native_modules.gradle +@@ -140,6 +140,7 @@ class ReactNativeModules { + private Logger logger + private String packageName + private File root ++ private File rnRoot + private ArrayList> reactNativeModules + private ArrayList unstable_reactLegacyComponentNames + private HashMap reactNativeModulesBuildVariants +@@ -147,9 +148,10 @@ class ReactNativeModules { + + private static String LOG_PREFIX = ":ReactNative:" + +- ReactNativeModules(Logger logger, File root) { ++ ReactNativeModules(Logger logger, File root, File rnRoot) { + this.logger = logger + this.root = root ++ this.rnRoot = rnRoot + + def (nativeModules, reactNativeModulesBuildVariants, androidProject, reactNativeVersion) = this.getReactNativeConfig() + this.reactNativeModules = nativeModules +@@ -416,10 +418,10 @@ class ReactNativeModules { + */ + def cliResolveScript = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}" + String[] nodeCommand = ["node", "-e", cliResolveScript] +- def cliPath = this.getCommandOutput(nodeCommand, this.root) ++ def cliPath = this.getCommandOutput(nodeCommand, this.rnRoot) + + String[] reactNativeConfigCommand = ["node", cliPath, "config"] +- def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root) ++ def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.rnRoot) + + def json + try { +@@ -486,7 +488,13 @@ class ReactNativeModules { + */ + def projectRoot = rootProject.projectDir + +-def autoModules = new ReactNativeModules(logger, projectRoot) ++def autoModules ++ ++if(this.hasProperty('reactNativeProject')){ ++ autoModules = new ReactNativeModules(logger, projectRoot, new File(projectRoot, reactNativeProject)) ++} else { ++ autoModules = new ReactNativeModules(logger, projectRoot, projectRoot) ++} + + def reactNativeVersionRequireNewArchEnabled(autoModules) { + def rnVersion = autoModules.reactNativeVersion diff --git a/patches/@react-native-community+cli-platform-ios+12.3.0.patch b/patches/@react-native-community+cli-platform-ios+12.3.0.patch new file mode 100644 index 000000000000..cfae504e44fa --- /dev/null +++ b/patches/@react-native-community+cli-platform-ios+12.3.0.patch @@ -0,0 +1,52 @@ +diff --git a/node_modules/@react-native-community/cli-platform-ios/native_modules.rb b/node_modules/@react-native-community/cli-platform-ios/native_modules.rb +index 82f537c..f5e2cda 100644 +--- a/node_modules/@react-native-community/cli-platform-ios/native_modules.rb ++++ b/node_modules/@react-native-community/cli-platform-ios/native_modules.rb +@@ -12,7 +12,7 @@ + require 'pathname' + require 'cocoapods' + +-def use_native_modules!(config = nil) ++def updateConfig(config = nil) + if (config.is_a? String) + Pod::UI.warn("Passing custom root to use_native_modules! is deprecated.", + [ +@@ -24,7 +24,6 @@ def use_native_modules!(config = nil) + # Resolving the path the RN CLI. The `@react-native-community/cli` module may not be there for certain package managers, so we fall back to resolving it through `react-native` package, that's always present in RN projects + cli_resolve_script = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}" + cli_bin = Pod::Executable.execute_command("node", ["-e", cli_resolve_script], true).strip +- + if (!config) + json = [] + +@@ -36,10 +35,30 @@ def use_native_modules!(config = nil) + + config = JSON.parse(json.join("\n")) + end ++end ++ ++def use_native_modules!(config = nil) ++ if (ENV['REACT_NATIVE_DIR']) ++ Dir.chdir(ENV['REACT_NATIVE_DIR']) do ++ config = updateConfig(config) ++ end ++ else ++ config = updateConfig(config) ++ end + + project_root = Pathname.new(config["project"]["ios"]["sourceDir"]) + ++ if(ENV["PROJECT_ROOT_DIR"]) ++ project_root = File.join(Dir.pwd, ENV["PROJECT_ROOT_DIR"]) ++ ++ end ++ + packages = config["dependencies"] ++ ++ if (ENV["NO_FLIPPER"]) ++ packages = {**packages, "react-native-flipper" => {"platforms" => {"ios" => nil}}} ++ end ++ + found_pods = [] + + packages.each do |package_name, package| diff --git a/patches/@react-native-firebase+analytics+12.9.3.patch b/patches/@react-native-firebase+analytics+12.9.3.patch new file mode 100644 index 000000000000..74d3e2a8005a --- /dev/null +++ b/patches/@react-native-firebase+analytics+12.9.3.patch @@ -0,0 +1,18 @@ +diff --git a/node_modules/@react-native-firebase/analytics/android/build.gradle b/node_modules/@react-native-firebase/analytics/android/build.gradle +index d223ebf..821b730 100644 +--- a/node_modules/@react-native-firebase/analytics/android/build.gradle ++++ b/node_modules/@react-native-firebase/analytics/android/build.gradle +@@ -45,6 +45,8 @@ if (coreVersionDetected != coreVersionRequired) { + } + } + ++apply plugin: 'com.android.library' ++ + project.ext { + set('react-native', [ + versions: [ +@@ -144,4 +146,3 @@ dependencies { + ReactNative.shared.applyPackageVersion() + ReactNative.shared.applyDefaultExcludes() + ReactNative.module.applyAndroidVersions() +-ReactNative.module.applyReactNativeDependency("api") diff --git a/patches/@react-native-firebase+app+12.9.3.patch b/patches/@react-native-firebase+app+12.9.3.patch new file mode 100644 index 000000000000..312fdacf4432 --- /dev/null +++ b/patches/@react-native-firebase+app+12.9.3.patch @@ -0,0 +1,25 @@ +diff --git a/node_modules/@react-native-firebase/app/android/build.gradle b/node_modules/@react-native-firebase/app/android/build.gradle +index 05f629a..7c36693 100644 +--- a/node_modules/@react-native-firebase/app/android/build.gradle ++++ b/node_modules/@react-native-firebase/app/android/build.gradle +@@ -18,6 +18,7 @@ buildscript { + + plugins { + id "io.invertase.gradle.build" version "1.5" ++ id 'com.android.library' + } + + def packageJson = PackageJson.getForProject(project) +@@ -91,6 +92,7 @@ repositories { + } + + dependencies { ++ api 'com.facebook.react:react-native:+' + implementation platform("com.google.firebase:firebase-bom:${ReactNative.ext.getVersion("firebase", "bom")}") + implementation "com.google.firebase:firebase-common" + implementation "com.google.android.gms:play-services-auth:${ReactNative.ext.getVersion("play", "play-services-auth")}" +@@ -99,4 +101,3 @@ dependencies { + ReactNative.shared.applyPackageVersion() + ReactNative.shared.applyDefaultExcludes() + ReactNative.module.applyAndroidVersions() +-ReactNative.module.applyReactNativeDependency("api") diff --git a/patches/@react-native-firebase+crashlytics+12.9.3.patch b/patches/@react-native-firebase+crashlytics+12.9.3.patch new file mode 100644 index 000000000000..560f462731dc --- /dev/null +++ b/patches/@react-native-firebase+crashlytics+12.9.3.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/@react-native-firebase/crashlytics/android/build.gradle b/node_modules/@react-native-firebase/crashlytics/android/build.gradle +index 6b6de57..9b89ae7 100644 +--- a/node_modules/@react-native-firebase/crashlytics/android/build.gradle ++++ b/node_modules/@react-native-firebase/crashlytics/android/build.gradle +@@ -18,6 +18,7 @@ buildscript { + + plugins { + id "io.invertase.gradle.build" version "1.5" ++ id 'com.android.library' + } + + def appProject +@@ -92,4 +93,3 @@ dependencies { + ReactNative.shared.applyPackageVersion() + ReactNative.shared.applyDefaultExcludes() + ReactNative.module.applyAndroidVersions() +-ReactNative.module.applyReactNativeDependency("api") diff --git a/patches/@react-native-firebase+perf+12.9.3.patch b/patches/@react-native-firebase+perf+12.9.3.patch new file mode 100644 index 000000000000..7d8a9f4f23b5 --- /dev/null +++ b/patches/@react-native-firebase+perf+12.9.3.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/@react-native-firebase/perf/android/build.gradle b/node_modules/@react-native-firebase/perf/android/build.gradle +index b4a9c7b..5835e3d 100644 +--- a/node_modules/@react-native-firebase/perf/android/build.gradle ++++ b/node_modules/@react-native-firebase/perf/android/build.gradle +@@ -19,6 +19,7 @@ buildscript { + + plugins { + id "io.invertase.gradle.build" version "1.5" ++ id 'com.android.library' + } + + def appProject +@@ -129,4 +130,3 @@ dependencies { + ReactNative.shared.applyPackageVersion() + ReactNative.shared.applyDefaultExcludes() + ReactNative.module.applyAndroidVersions() +-ReactNative.module.applyReactNativeDependency("api") diff --git a/patches/expo+50.0.4.patch b/patches/expo+50.0.4.patch new file mode 100644 index 000000000000..95157e1d73c6 --- /dev/null +++ b/patches/expo+50.0.4.patch @@ -0,0 +1,10 @@ +diff --git a/node_modules/expo/scripts/autolinking.gradle b/node_modules/expo/scripts/autolinking.gradle +index 60d6ef8..3ed90a4 100644 +--- a/node_modules/expo/scripts/autolinking.gradle ++++ b/node_modules/expo/scripts/autolinking.gradle +@@ -1,4 +1,4 @@ + // Resolve `expo` > `expo-modules-autolinking` dependency chain + def autolinkingPath = ["node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim() +-apply from: new File(autolinkingPath, "../scripts/android/autolinking_implementation.gradle"); + ++apply from: hasProperty("reactNativeProject") ? file('../../expo-modules-autolinking/scripts/android/autolinking_implementation.gradle') : new File(autolinkingPath, "../scripts/android/autolinking_implementation.gradle"); diff --git a/patches/expo-modules-autolinking+1.10.2.patch b/patches/expo-modules-autolinking+1.10.2.patch new file mode 100644 index 000000000000..4b68007ba125 --- /dev/null +++ b/patches/expo-modules-autolinking+1.10.2.patch @@ -0,0 +1,40 @@ +diff --git a/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle b/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle +index 92f1fd6..ada01ad 100644 +--- a/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle ++++ b/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle +@@ -149,12 +149,13 @@ class ExpoAutolinkingManager { + } + + static private String[] convertOptionsToCommandArgs(String command, Map options) { ++ def expoPath = options.searchPaths ? "../react-native/node_modules/expo" : "expo" + String[] args = [ + 'node', + '--no-warnings', + '--eval', + // Resolve the `expo` > `expo-modules-autolinking` chain from the project root +- 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo\')] }))(process.argv.slice(1))', ++ "require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'${expoPath}\')] }))(process.argv.slice(1))", + '--', + command, + '--platform', +diff --git a/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb b/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb +index 5d46f1e..3db7b89 100644 +--- a/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb ++++ b/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb +@@ -215,6 +215,7 @@ module Expo + args = autolinking_manager.base_command_args.map { |arg| "\"#{arg}\"" } + platform = autolinking_manager.platform_name.downcase + package_names = autolinking_manager.packages_to_generate.map { |package| "\"#{package.name}\"" } ++ expo_path = ENV['REACT_NATIVE_DIR'] ? "#{ENV['REACT_NATIVE_DIR']}/node_modules/expo" : "expo" + + <<~SUPPORT_SCRIPT + #!/usr/bin/env bash +@@ -262,7 +263,7 @@ module Expo + + with_node \\ + --no-warnings \\ +- --eval "require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))" \\ ++ --eval "require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'#{expo_path}/package.json\')] }))(process.argv.slice(1))" \\ + generate-modules-provider #{args.join(' ')} \\ + --target "#{modules_provider_path}" \\ + --platform "apple" \\ diff --git a/patches/expo-modules-core+1.11.8.patch b/patches/expo-modules-core+1.11.8.patch new file mode 100644 index 000000000000..fe8c5ed7b9cc --- /dev/null +++ b/patches/expo-modules-core+1.11.8.patch @@ -0,0 +1,16 @@ +diff --git a/node_modules/expo-modules-core/android/build.gradle b/node_modules/expo-modules-core/android/build.gradle +index 3603ffd..1599a69 100644 +--- a/node_modules/expo-modules-core/android/build.gradle ++++ b/node_modules/expo-modules-core/android/build.gradle +@@ -53,9 +53,10 @@ def isExpoModulesCoreTests = { + }.call() + + def REACT_NATIVE_BUILD_FROM_SOURCE = findProject(":packages:react-native:ReactAndroid") != null ++def FALLBACK_REACT_NATIVE_DIR = hasProperty("reactNativeProject") ? file('../../react-native') : new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).parent + def REACT_NATIVE_DIR = REACT_NATIVE_BUILD_FROM_SOURCE + ? findProject(":packages:react-native:ReactAndroid").getProjectDir().parent +- : new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).parent ++ : FALLBACK_REACT_NATIVE_DIR + + def reactProperties = new Properties() + file("$REACT_NATIVE_DIR/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) } diff --git a/patches/react-native-pdf+6.7.3.patch b/patches/react-native-pdf+6.7.3.patch new file mode 100644 index 000000000000..63024e7d4c1f --- /dev/null +++ b/patches/react-native-pdf+6.7.3.patch @@ -0,0 +1,23 @@ +diff --git a/node_modules/react-native-pdf/index.js b/node_modules/react-native-pdf/index.js +index c05de52..bea7af8 100644 +--- a/node_modules/react-native-pdf/index.js ++++ b/node_modules/react-native-pdf/index.js +@@ -367,11 +367,17 @@ export default class Pdf extends Component { + message[4] = message.splice(4).join('|'); + } + if (message[0] === 'loadComplete') { ++ let tableContents; ++ try { ++ tableContents = message[4]&&JSON.parse(message[4]); ++ } catch(e) { ++ tableContents = message[4]; ++ } + this.props.onLoadComplete && this.props.onLoadComplete(Number(message[1]), this.state.path, { + width: Number(message[2]), + height: Number(message[3]), + }, +- message[4]&&JSON.parse(message[4])); ++ tableContents); + } else if (message[0] === 'pageChanged') { + this.props.onPageChanged && this.props.onPageChanged(Number(message[1]), Number(message[2])); + } else if (message[0] === 'error') { diff --git a/patches/react-native-reanimated+3.6.1.patch b/patches/react-native-reanimated+3.6.1.patch new file mode 100644 index 000000000000..3b40360d5860 --- /dev/null +++ b/patches/react-native-reanimated+3.6.1.patch @@ -0,0 +1,17 @@ +diff --git a/node_modules/react-native-reanimated/scripts/reanimated_utils.rb b/node_modules/react-native-reanimated/scripts/reanimated_utils.rb +index af0935f..ccd2a9e 100644 +--- a/node_modules/react-native-reanimated/scripts/reanimated_utils.rb ++++ b/node_modules/react-native-reanimated/scripts/reanimated_utils.rb +@@ -17,7 +17,11 @@ def find_config() + :react_native_common_dir => nil, + } + +- react_native_node_modules_dir = File.join(File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native/package.json')"`), '..') ++ root_project = Pod::Config.instance.installation_root.to_s ++ if(ENV['PROJECT_ROOT_DIR']) ++ root_project = ENV['PROJECT_ROOT_DIR'] ++ end ++ react_native_node_modules_dir = File.join(File.dirname(`cd "#{root_project}" && node --print "require.resolve('react-native/package.json')"`), '..') + react_native_json = try_to_parse_react_native_package_json(react_native_node_modules_dir) + + if react_native_json == nil diff --git a/patches/react-native-vision-camera+2.16.2+001+fix-boost-dependency.patch b/patches/react-native-vision-camera+2.16.5+001+fix-boost-dependency.patch similarity index 56% rename from patches/react-native-vision-camera+2.16.2+001+fix-boost-dependency.patch rename to patches/react-native-vision-camera+2.16.5+001+fix-boost-dependency.patch index ef4fbf1d5084..3afc4573985d 100644 --- a/patches/react-native-vision-camera+2.16.2+001+fix-boost-dependency.patch +++ b/patches/react-native-vision-camera+2.16.5+001+fix-boost-dependency.patch @@ -1,8 +1,17 @@ diff --git a/node_modules/react-native-vision-camera/android/build.gradle b/node_modules/react-native-vision-camera/android/build.gradle -index d308e15..2d87d8e 100644 +index ddfa243..bafffc3 100644 --- a/node_modules/react-native-vision-camera/android/build.gradle +++ b/node_modules/react-native-vision-camera/android/build.gradle -@@ -347,7 +347,7 @@ if (ENABLE_FRAME_PROCESSORS) { +@@ -334,7 +334,7 @@ if (ENABLE_FRAME_PROCESSORS) { + def thirdPartyVersions = new Properties() + thirdPartyVersions.load(new FileInputStream(thirdPartyVersionsFile)) + +- def BOOST_VERSION = thirdPartyVersions["BOOST_VERSION"] ++ def BOOST_VERSION = thirdPartyVersions["BOOST_VERSION"] ?: "1.83.0" + def boost_file = new File(downloadsDir, "boost_${BOOST_VERSION}.tar.gz") + def DOUBLE_CONVERSION_VERSION = thirdPartyVersions["DOUBLE_CONVERSION_VERSION"] + def double_conversion_file = new File(downloadsDir, "double-conversion-${DOUBLE_CONVERSION_VERSION}.tar.gz") +@@ -352,7 +352,7 @@ if (ENABLE_FRAME_PROCESSORS) { task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) { def transformedVersion = BOOST_VERSION.replace("_", ".") diff --git a/patches/react-native-vision-camera+2.16.2.patch b/patches/react-native-vision-camera+2.16.5.patch similarity index 100% rename from patches/react-native-vision-camera+2.16.2.patch rename to patches/react-native-vision-camera+2.16.5.patch diff --git a/src/App.js b/src/App.tsx similarity index 60% rename from src/App.js rename to src/App.tsx index 8045f4eb30ad..7c1ead1d86d3 100644 --- a/src/App.js +++ b/src/App.tsx @@ -29,7 +29,14 @@ import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import * as Session from './libs/actions/Session'; import * as Environment from './libs/Environment/Environment'; +import InitialUrlContext from './libs/InitialUrlContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; +import type {Route} from './ROUTES'; + +type AppProps = { + /** If we have an authToken this is true */ + url?: Route; +}; // For easier debugging and development, when we are in web we expose Onyx to the window, so you can more easily set data into Onyx if (window && Environment.isDevelopment()) { @@ -46,41 +53,44 @@ LogBox.ignoreLogs([ const fill = {flex: 1}; -function App() { +function App({url}: AppProps) { useDefaultDragAndDrop(); OnyxUpdateManager(); return ( - - - - - - - - - - + + + + + + + {/* @ts-expect-error TODO: Remove this once Expensify (https://github.com/Expensify/App/issues/25231) is migrated to TypeScript. */} + + + + + + ); } diff --git a/src/CONST.ts b/src/CONST.ts index b27923465a1f..79895d20aa57 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -46,6 +46,8 @@ const CONST = { IN: 'in', OUT: 'out', }, + // Multiplier for gyroscope animation in order to make it a bit more subtle + ANIMATION_GYROSCOPE_VALUE: 0.4, BACKGROUND_IMAGE_TRANSITION_DURATION: 1000, ARROW_HIDE_DELAY: 3000, @@ -100,6 +102,10 @@ const CONST = { MAX_LENGTH: 40, }, + REPORT_DESCRIPTION: { + MAX_LENGTH: 1024, + }, + PULL_REQUEST_NUMBER, MERCHANT_NAME_MAX_LENGTH: 255, @@ -146,7 +152,7 @@ const CONST = { CONTAINER_MINHEIGHT: 500, VIEW_HEIGHT: 275, }, - MONEY_REPORT: { + MONEY_OR_TASK_REPORT: { SMALL_SCREEN: { IMAGE_HEIGHT: 300, CONTAINER_MINHEIGHT: 280, @@ -183,6 +189,7 @@ const CONST = { UNIX_EPOCH: '1970-01-01 00:00:00.000', MAX_DATE: '9999-12-31', MIN_DATE: '0001-01-01', + ORDINAL_DAY_OF_MONTH: 'do', }, SMS: { DOMAIN: '@expensify.sms', @@ -456,8 +463,6 @@ const CONST = { EMPTY_ARRAY, EMPTY_OBJECT, USE_EXPENSIFY_URL, - NEW_ZOOM_MEETING_URL: 'https://zoom.us/start/videomeeting', - NEW_GOOGLE_MEET_MEETING_URL: 'https://meet.google.com/new', GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', GOOGLE_DOC_IMAGE_LINK_MATCH: 'googleusercontent.com', IMAGE_BASE64_MATCH: 'base64', @@ -494,6 +499,7 @@ const CONST = { ADMIN_POLICIES_URL: 'admin_policies', ADMIN_DOMAINS_URL: 'admin_domains', INBOX: 'inbox', + DISMMISSED_REASON: '?dismissedReason=missingFeatures', }, SIGN_IN_FORM_WIDTH: 300, @@ -730,7 +736,6 @@ const CONST = { REPORT_INITIAL_RENDER: 'report_initial_render', SWITCH_REPORT: 'switch_report', SIDEBAR_LOADED: 'sidebar_loaded', - OPEN_SEARCH: 'open_search', LOAD_SEARCH_OPTIONS: 'load_search_options', COLD: 'cold', WARM: 'warm', @@ -1330,9 +1335,9 @@ const CONST = { OWNER_EMAIL_FAKE: '_FAKE_', OWNER_ACCOUNT_ID_FAKE: 0, REIMBURSEMENT_CHOICES: { - REIMBURSEMENT_YES: 'reimburseYes', - REIMBURSEMENT_NO: 'reimburseNo', - REIMBURSEMENT_MANUAL: 'reimburseManual', + REIMBURSEMENT_YES: 'reimburseYes', // Direct + REIMBURSEMENT_NO: 'reimburseNo', // None + REIMBURSEMENT_MANUAL: 'reimburseManual', // Indirect }, ID_FAKE: '_FAKE_', EMPTY: 'EMPTY', @@ -1431,6 +1436,8 @@ const CONST = { EMOJI: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu, // eslint-disable-next-line max-len, no-misleading-character-class EMOJIS: /[\p{Extended_Pictographic}](\u200D[\p{Extended_Pictographic}]|[\u{1F3FB}-\u{1F3FF}]|[\u{E0020}-\u{E007F}]|\uFE0F|\u20E3)*|[\u{1F1E6}-\u{1F1FF}]{2}|[#*0-9]\uFE0F?\u20E3/gu, + // eslint-disable-next-line max-len, no-misleading-character-class + EMOJI_SKIN_TONES: /[\u{1f3fb}-\u{1f3ff}]/gu, TAX_ID: /^\d{9}$/, NON_NUMERIC: /\D/g, @@ -1588,7 +1595,6 @@ const CONST = { INVITE: 'invite', SETTINGS: 'settings', LEAVE_ROOM: 'leaveRoom', - WELCOME_MESSAGE: 'welcomeMessage', PRIVATE_NOTES: 'privateNotes', }, EDIT_REQUEST_FIELD: { @@ -3167,6 +3173,14 @@ const CONST = { CHAT_SPLIT: 'newDotSplitChat', }, + MANAGE_TEAMS_CHOICE: { + MULTI_LEVEL: 'multiLevelApproval', + CUSTOM_EXPENSE: 'customExpenseCoding', + CARD_TRACKING: 'companyCardTracking', + ACCOUNTING: 'accountingIntegrations', + RULE: 'ruleEnforcement', + }, + MINI_CONTEXT_MENU_MAX_ITEMS: 4, WORKSPACE_SWITCHER: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 7328fb2543ad..cbf4d7714967 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -313,8 +313,8 @@ const ONYXKEYS = { DISPLAY_NAME_FORM_DRAFT: 'displayNameFormDraft', ROOM_NAME_FORM: 'roomNameForm', ROOM_NAME_FORM_DRAFT: 'roomNameFormDraft', - WELCOME_MESSAGE_FORM: 'welcomeMessageForm', - WELCOME_MESSAGE_FORM_DRAFT: 'welcomeMessageFormDraft', + REPORT_DESCRIPTION_FORM: 'reportDescriptionForm', + REPORT_DESCRIPTION_FORM_DRAFT: 'reportDescriptionFormDraft', LEGAL_NAME_FORM: 'legalNameForm', LEGAL_NAME_FORM_DRAFT: 'legalNameFormDraft', WORKSPACE_INVITE_MESSAGE_FORM: 'workspaceInviteMessageForm', @@ -408,7 +408,7 @@ type OnyxValues = { [ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string; [ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean; [ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean; - [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: Record; + [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: OnyxTypes.LastPaymentMethod; [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; [ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL]: boolean; [ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected; @@ -424,7 +424,7 @@ type OnyxValues = { [ONYXKEYS.WALLET_TERMS]: OnyxTypes.WalletTerms; [ONYXKEYS.BANK_ACCOUNT_LIST]: OnyxTypes.BankAccountList; [ONYXKEYS.FUND_LIST]: OnyxTypes.FundList; - [ONYXKEYS.CARD_LIST]: Record; + [ONYXKEYS.CARD_LIST]: OnyxTypes.CardList; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; @@ -480,9 +480,9 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; + [ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; - [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolation[]; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; @@ -499,10 +499,10 @@ type OnyxValues = { [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.DisplayNameForm; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.DisplayNameForm; - [ONYXKEYS.FORMS.ROOM_NAME_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.ROOM_NAME_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.ROOM_NAME_FORM]: OnyxTypes.RoomNameForm; + [ONYXKEYS.FORMS.ROOM_NAME_FORM_DRAFT]: OnyxTypes.RoomNameForm; + [ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.LEGAL_NAME_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.LEGAL_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: OnyxTypes.Form; @@ -547,8 +547,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM]: OnyxTypes.GetPhysicalCardForm; + [ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.GetPhysicalCardForm; [ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM]: OnyxTypes.ReportFieldEditForm; [ONYXKEYS.FORMS.REPORT_FIELD_EDIT_FORM_DRAFT]: OnyxTypes.Form; // @ts-expect-error Different values are defined under the same key: ReimbursementAccount and ReimbursementAccountForm diff --git a/src/ROUTES.ts b/src/ROUTES.ts index aa6d9f458983..678dbe433417 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -125,13 +125,12 @@ const ROUTES = { route: 'settings/wallet/card/:domain/activate', getRoute: (domain: string) => `settings/wallet/card/${domain}/activate` as const, }, - SETTINGS_PERSONAL_DETAILS: 'settings/profile/personal-details', - SETTINGS_PERSONAL_DETAILS_LEGAL_NAME: 'settings/profile/personal-details/legal-name', - SETTINGS_PERSONAL_DETAILS_DATE_OF_BIRTH: 'settings/profile/personal-details/date-of-birth', - SETTINGS_PERSONAL_DETAILS_ADDRESS: 'settings/profile/personal-details/address', - SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY: { - route: 'settings/profile/personal-details/address/country', - getRoute: (country: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/personal-details/address/country?country=${country}`, backTo), + SETTINGS_LEGAL_NAME: 'settings/profile/legal-name', + SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', + SETTINGS_ADDRESS: 'settings/profile/address', + SETTINGS_ADDRESS_COUNTRY: { + route: 'settings/profile/address/country', + getRoute: (country: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/address/country?country=${country}`, backTo), }, SETTINGS_CONTACT_METHODS: { route: 'settings/profile/contact-methods', @@ -214,10 +213,6 @@ const ROUTES = { route: 'r/:reportID/settings/who-can-post', getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post` as const, }, - REPORT_WELCOME_MESSAGE: { - route: 'r/:reportID/welcomeMessage', - getRoute: (reportID: string) => `r/${reportID}/welcomeMessage` as const, - }, SPLIT_BILL_DETAILS: { route: 'r/:reportID/split/:reportActionID', getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}` as const, @@ -235,7 +230,7 @@ const ROUTES = { route: 'r/:reportID/title', getRoute: (reportID: string) => `r/${reportID}/title` as const, }, - TASK_DESCRIPTION: { + REPORT_DESCRIPTION: { route: 'r/:reportID/description', getRoute: (reportID: string) => `r/${reportID}/description` as const, }, @@ -285,18 +280,10 @@ 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_DESCRIPTION: { - route: ':iouType/new/description/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/description/${reportID}` as const, - }, MONEY_REQUEST_CATEGORY: { route: ':iouType/new/category/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}` as const, }, - MONEY_REQUEST_TAG: { - route: ':iouType/new/tag/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}` as const, - }, MONEY_REQUEST_MERCHANT: { route: ':iouType/new/merchant/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}` as const, @@ -355,9 +342,9 @@ const ROUTES = { getUrlWithBackToParam(`create/${iouType}/date/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DESCRIPTION: { - route: 'create/:iouType/description/:transactionID/:reportID', - getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/description/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/description/:transactionID/:reportID', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/description/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DISTANCE: { route: 'create/:iouType/distance/:transactionID/:reportID', @@ -380,9 +367,9 @@ const ROUTES = { getUrlWithBackToParam(`${action}/${iouType}/scan/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_TAG: { - route: 'create/:iouType/tag/:transactionID/:reportID', - getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/tag/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/tag/:transactionID/:reportID', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/tag/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_WAYPOINT: { route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex', @@ -421,6 +408,10 @@ const ROUTES = { NEW_TASK_TITLE: 'new/task/title', NEW_TASK_DESCRIPTION: 'new/task/description', + ONBOARD: 'onboard', + ONBOARD_MANAGE_EXPENSES: 'onboard/manage-expenses', + ONBOARD_EXPENSIFY_CLASSIC: 'onboard/expensify-classic', + TEACHERS_UNITE: 'teachersunite', I_KNOW_A_TEACHER: 'teachersunite/i-know-a-teacher', I_AM_A_TEACHER: 'teachersunite/i-am-a-teacher', @@ -501,7 +492,16 @@ const ROUTES = { PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', } as const; -export {getUrlWithBackToParam}; +/** + * Proxy routes can be used to generate a correct url with dynamic values + * + * It will be used by HybridApp, that has no access to methods generating dynamic routes in NewDot + */ +const HYBRID_APP_ROUTES = { + MONEY_REQUEST_CREATE: '/request/new/scan', +} as const; + +export {getUrlWithBackToParam, HYBRID_APP_ROUTES}; export default ROUTES; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -521,4 +521,6 @@ type RouteIsPlainString = IsEqual; */ type Route = RouteIsPlainString extends true ? never : AllRoutes; -export type {Route}; +type HybridAppRoute = (typeof HYBRID_APP_ROUTES)[keyof typeof HYBRID_APP_ROUTES]; + +export type {Route, HybridAppRoute}; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 96b284dbea2f..cd80937a3864 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -49,14 +49,10 @@ const SCREENS = { PRONOUNS: 'Settings_Pronouns', TIMEZONE: 'Settings_Timezone', TIMEZONE_SELECT: 'Settings_Timezone_Select', - - PERSONAL_DETAILS: { - INITIAL: 'Settings_PersonalDetails_Initial', - LEGAL_NAME: 'Settings_PersonalDetails_LegalName', - DATE_OF_BIRTH: 'Settings_PersonalDetails_DateOfBirth', - ADDRESS: 'Settings_PersonalDetails_Address', - ADDRESS_COUNTRY: 'Settings_PersonalDetails_Address_Country', - }, + LEGAL_NAME: 'Settings_LegalName', + DATE_OF_BIRTH: 'Settings_DateOfBirth', + ADDRESS: 'Settings_Address', + ADDRESS_COUNTRY: 'Settings_Address_Country', }, PREFERENCES: { @@ -100,10 +96,11 @@ const SCREENS = { PROFILE: 'Profile', REPORT_DETAILS: 'Report_Details', REPORT_SETTINGS: 'Report_Settings', - REPORT_WELCOME_MESSAGE: 'Report_WelcomeMessage', + REPORT_DESCRIPTION: 'Report_Description', PARTICIPANTS: 'Participants', MONEY_REQUEST: 'MoneyRequest', NEW_TASK: 'NewTask', + ONBOARD_ENGAGEMENT: 'Onboard_Engagement', TEACHERS_UNITE: 'TeachersUnite', TASK_DETAILS: 'Task_Details', ENABLE_PAYMENTS: 'EnablePayments', @@ -150,9 +147,7 @@ const SCREENS = { CONFIRMATION: 'Money_Request_Confirmation', CURRENCY: 'Money_Request_Currency', DATE: 'Money_Request_Date', - DESCRIPTION: 'Money_Request_Description', CATEGORY: 'Money_Request_Category', - TAG: 'Money_Request_Tag', MERCHANT: 'Money_Request_Merchant', WAYPOINT: 'Money_Request_Waypoint', EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', @@ -184,7 +179,6 @@ const SCREENS = { TASK: { TITLE: 'Task_Title', - DESCRIPTION: 'Task_Description', ASSIGNEE: 'Task_Assignee', }, @@ -232,6 +226,12 @@ const SCREENS = { EDIT_CURRENCY: 'SplitDetails_Edit_Currency', }, + ONBOARD_ENGAGEMENT: { + ROOT: 'Onboard_Engagement_Root', + MANAGE_TEAMS_EXPENSES: 'Manage_Teams_Expenses', + EXPENSIFY_CLASSIC: 'Expenisfy_Classic', + }, + I_KNOW_A_TEACHER: 'I_Know_A_Teacher', INTRO_SCHOOL_PRINCIPAL: 'Intro_School_Principal', I_AM_A_TEACHER: 'I_Am_A_Teacher', @@ -243,7 +243,7 @@ const SCREENS = { DETAILS_ROOT: 'Details_Root', PROFILE_ROOT: 'Profile_Root', PROCESS_MONEY_REQUEST_HOLD_ROOT: 'ProcessMoneyRequestHold_Root', - REPORT_WELCOME_MESSAGE_ROOT: 'Report_WelcomeMessage_Root', + REPORT_DESCRIPTION_ROOT: 'Report_Description_Root', REPORT_PARTICIPANTS_ROOT: 'ReportParticipants_Root', ROOM_MEMBERS_ROOT: 'RoomMembers_Root', ROOM_INVITE_ROOT: 'RoomInvite_Root', diff --git a/src/components/AddressForm.js b/src/components/AddressForm.js index 68d451e5c7c8..aee1b652b22c 100644 --- a/src/components/AddressForm.js +++ b/src/components/AddressForm.js @@ -67,7 +67,7 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS const styles = useThemeStyles(); const {translate} = useLocalize(); const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [country, 'samples'], ''); - const zipFormat = translate('common.zipCodeExampleFormat', {zipSampleFormat}); + const zipFormat = ['common.zipCodeExampleFormat', {zipSampleFormat}]; const isUSAForm = country === CONST.COUNTRY.US; /** @@ -103,7 +103,7 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS if (countrySpecificZipRegex) { if (!countrySpecificZipRegex.test(values.zipPostCode.trim().toUpperCase())) { if (ValidationUtils.isRequiredFulfilled(values.zipPostCode.trim())) { - errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', {zipFormat: countryZipFormat}]; + errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', countryZipFormat]; } else { errors.zipPostCode = 'common.error.fieldRequired'; } diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 8016f1b2ea39..9b4254a9bc45 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -1,6 +1,7 @@ import type {RefObject} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, View, ViewStyle} from 'react-native'; import type {Place} from 'react-native-google-places-autocomplete'; +import type {MaybePhraseKey} from '@libs/Localize'; import type Locale from '@src/types/onyx/Locale'; type CurrentLocationButtonProps = { @@ -43,7 +44,7 @@ type AddressSearchProps = { onBlur?: () => void; /** Error text to display */ - errorText?: string; + errorText?: MaybePhraseKey; /** Hint text to display */ hint?: string; diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index 05080fcdd21c..245aa2126d08 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -43,6 +43,7 @@ function AmountTextInput( disableKeyboard autoGrow hideFocusedState + shouldInterceptSwipe inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius, style]} textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} onChangeText={onChangeAmount} diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 90954c63b751..ab39e5379230 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -283,7 +283,7 @@ function AttachmentModal({ * Detach the receipt and close the modal. */ const deleteAndCloseModal = useCallback(() => { - IOU.detachReceipt(transaction?.transactionID); + IOU.detachReceipt(transaction?.transactionID ?? ''); setIsDeleteReceiptConfirmModalVisible(false); Navigation.dismissModal(report?.reportID); }, [transaction, report]); @@ -436,7 +436,7 @@ function AttachmentModal({ }, }); } - if (!isOffline) { + if (!isOffline && allowDownload) { menuItems.push({ icon: Expensicons.Download, text: translate('common.download'), diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 010d074d1da6..f55db3dd0620 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -421,7 +421,7 @@ function AvatarWithImagePicker({ {errorData.validationError && ( )} diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index 8cabf7dce494..5039de3b20b6 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import type {TranslationPaths} from '@src/languages/types'; -import ROUTES from '@src/ROUTES'; import BlockingView from './BlockingView'; import ForceFullScreenView from './ForceFullScreenView'; @@ -50,7 +49,7 @@ function FullPageNotFoundView({ titleKey = 'notFound.notHere', subtitleKey = 'notFound.pageNotFound', linkKey = 'notFound.goBackHome', - onBackButtonPress = () => Navigation.goBack(ROUTES.HOME), + onBackButtonPress = () => Navigation.goBack(), shouldShowLink = true, shouldShowBackButton = true, onLinkPress = () => Navigation.dismissModal(), diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 6af3a4c6d477..34bc3f7e30c8 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -38,7 +38,7 @@ function Breadcrumbs({breadcrumbs, style}: BreadcrumbsProps) { const [primaryBreadcrumb, secondaryBreadcrumb] = breadcrumbs; return ( - + {primaryBreadcrumb.type === CONST.BREADCRUMB_TYPE.ROOT ? (
    ; + type DropdownOption = { - value: string; + value: PaymentType; text: string; icon: IconAsset; iconWidth?: number; @@ -30,7 +33,7 @@ type ButtonWithDropdownMenuProps = { menuHeaderText?: string; /** Callback to execute when the main button is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: string) => void; + onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: PaymentType) => void; /** Callback to execute when a dropdown option is selected */ onOptionSelected?: (option: DropdownOption) => void; @@ -59,6 +62,9 @@ type ButtonWithDropdownMenuProps = { /* ref for the button */ buttonRef: RefObject; + + /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ + enterKeyEventListenerPriority?: number; }; function ButtonWithDropdownMenu({ @@ -76,6 +82,7 @@ function ButtonWithDropdownMenu({ onPress, options, onOptionSelected, + enterKeyEventListenerPriority = 0, }: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -126,6 +133,7 @@ function ButtonWithDropdownMenu({ large={isButtonSizeLarge} medium={!isButtonSizeLarge} innerStyles={[innerStyleDropButton]} + enterKeyEventListenerPriority={enterKeyEventListenerPriority} />