From 7c7c15f026e332a689664bfb0d86318ca35b02d5 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Wed, 15 May 2024 10:45:06 +0200 Subject: [PATCH 1/4] Fix bug with Delay in showing new currency when selecting a new currency --- src/libs/Navigation/Navigation.ts | 2 ++ .../setNabigationActionToMicrotaskQueue.ts | 13 +++++++++++++ src/libs/actions/Policy.ts | 12 +----------- .../iou/request/step/IOURequestStepCurrency.tsx | 3 ++- .../workspace/WorkspaceProfileCurrencyPage.tsx | 2 +- 5 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 src/libs/Navigation/setNabigationActionToMicrotaskQueue.ts diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 6f5e86f057d8..f60c212a23b3 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -19,6 +19,7 @@ import originalGetTopmostReportId from './getTopmostReportId'; import linkingConfig from './linkingConfig'; import linkTo from './linkTo'; import navigationRef from './navigationRef'; +import setNabigationActionToMicrotaskQueue from './setNabigationActionToMicrotaskQueue'; import switchPolicyID from './switchPolicyID'; import type {NavigationStateRoute, State, StateOrRoute, SwitchPolicyIDParams} from './types'; @@ -384,6 +385,7 @@ export default { resetToHome, isDisplayedInModal, closeRHPFlow, + setNabigationActionToMicrotaskQueue, }; export {navigationRef}; diff --git a/src/libs/Navigation/setNabigationActionToMicrotaskQueue.ts b/src/libs/Navigation/setNabigationActionToMicrotaskQueue.ts new file mode 100644 index 000000000000..c93d032eb954 --- /dev/null +++ b/src/libs/Navigation/setNabigationActionToMicrotaskQueue.ts @@ -0,0 +1,13 @@ +export default function setNabigationActionToMicrotaskQueue(nabigationAction: () => void) { + /** + * The function is used when the app needs to set a navigation action to the microtask queue, it guarantees to execute Onyx.update first, then the navigation action. + * More details - https://github.com/Expensify/App/issues/37785#issuecomment-1989056726. + */ + new Promise((resolve) => { + resolve(); + }).then(() => { + requestAnimationFrame(() => { + nabigationAction(); + }); + }); +} diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 7f346933873d..fd6d57ec4655 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -4005,17 +4005,7 @@ function navigateWhenEnableFeature(policyID: string, featureRoute: Route) { return; } - /** - * The app needs to set a navigation action to the microtask queue, it guarantees to execute Onyx.update first, then the navigation action. - * More details - https://github.com/Expensify/App/issues/37785#issuecomment-1989056726. - */ - new Promise((resolve) => { - resolve(); - }).then(() => { - requestAnimationFrame(() => { - Navigation.navigate(featureRoute); - }); - }); + Navigation.setNabigationActionToMicrotaskQueue(() => Navigation.navigate(featureRoute)); } function enablePolicyCategories(policyID: string, enabled: boolean) { diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.tsx b/src/pages/iou/request/step/IOURequestStepCurrency.tsx index 8669563f3b9f..ee1b9c3180af 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.tsx +++ b/src/pages/iou/request/step/IOURequestStepCurrency.tsx @@ -61,7 +61,8 @@ function IOURequestStepCurrency({ if (pageIndex !== 'confirm') { IOU.setMoneyRequestCurrency(transactionID, option.currencyCode, action === CONST.IOU.ACTION.EDIT); } - navigateBack(option.currencyCode); + + Navigation.setNabigationActionToMicrotaskQueue(() => navigateBack(option.currencyCode)); }; return ( diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx index 8d00975c7494..3215a1c9150c 100644 --- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx @@ -20,7 +20,7 @@ function WorkspaceProfileCurrencyPage({policy}: WorkspaceProfileCurrencyPageProp const onSelectCurrency = (item: CurrencyListItem) => { Policy.updateGeneralSettings(policy?.id ?? '', policy?.name ?? '', item.currencyCode); - Navigation.goBack(); + Navigation.setNabigationActionToMicrotaskQueue(Navigation.goBack); }; return ( From 1dbf9cd198357ec2fc41f242a5a53dc2b97d94ab Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 16 May 2024 10:14:13 +0200 Subject: [PATCH 2/4] Rename setNavigationActionToMicrotaskQueue function --- src/libs/Navigation/Navigation.ts | 4 ++-- ...crotaskQueue.ts => setNavigationActionToMicrotaskQueue.ts} | 2 +- src/libs/actions/Policy.ts | 2 +- src/pages/iou/request/step/IOURequestStepCurrency.tsx | 2 +- src/pages/workspace/WorkspaceProfileCurrencyPage.tsx | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename src/libs/Navigation/{setNabigationActionToMicrotaskQueue.ts => setNavigationActionToMicrotaskQueue.ts} (88%) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index f60c212a23b3..8a764d845fa6 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -19,7 +19,7 @@ import originalGetTopmostReportId from './getTopmostReportId'; import linkingConfig from './linkingConfig'; import linkTo from './linkTo'; import navigationRef from './navigationRef'; -import setNabigationActionToMicrotaskQueue from './setNabigationActionToMicrotaskQueue'; +import setNavigationActionToMicrotaskQueue from './setNavigationActionToMicrotaskQueue'; import switchPolicyID from './switchPolicyID'; import type {NavigationStateRoute, State, StateOrRoute, SwitchPolicyIDParams} from './types'; @@ -385,7 +385,7 @@ export default { resetToHome, isDisplayedInModal, closeRHPFlow, - setNabigationActionToMicrotaskQueue, + setNavigationActionToMicrotaskQueue, }; export {navigationRef}; diff --git a/src/libs/Navigation/setNabigationActionToMicrotaskQueue.ts b/src/libs/Navigation/setNavigationActionToMicrotaskQueue.ts similarity index 88% rename from src/libs/Navigation/setNabigationActionToMicrotaskQueue.ts rename to src/libs/Navigation/setNavigationActionToMicrotaskQueue.ts index c93d032eb954..0794b4a0bf26 100644 --- a/src/libs/Navigation/setNabigationActionToMicrotaskQueue.ts +++ b/src/libs/Navigation/setNavigationActionToMicrotaskQueue.ts @@ -1,4 +1,4 @@ -export default function setNabigationActionToMicrotaskQueue(nabigationAction: () => void) { +export default function setNavigationActionToMicrotaskQueue(nabigationAction: () => void) { /** * The function is used when the app needs to set a navigation action to the microtask queue, it guarantees to execute Onyx.update first, then the navigation action. * More details - https://github.com/Expensify/App/issues/37785#issuecomment-1989056726. diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index fd6d57ec4655..72dea5a725e9 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -4005,7 +4005,7 @@ function navigateWhenEnableFeature(policyID: string, featureRoute: Route) { return; } - Navigation.setNabigationActionToMicrotaskQueue(() => Navigation.navigate(featureRoute)); + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(featureRoute)); } function enablePolicyCategories(policyID: string, enabled: boolean) { diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.tsx b/src/pages/iou/request/step/IOURequestStepCurrency.tsx index ee1b9c3180af..9fec4019f49c 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.tsx +++ b/src/pages/iou/request/step/IOURequestStepCurrency.tsx @@ -62,7 +62,7 @@ function IOURequestStepCurrency({ IOU.setMoneyRequestCurrency(transactionID, option.currencyCode, action === CONST.IOU.ACTION.EDIT); } - Navigation.setNabigationActionToMicrotaskQueue(() => navigateBack(option.currencyCode)); + Navigation.setNavigationActionToMicrotaskQueue(() => navigateBack(option.currencyCode)); }; return ( diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx index 3215a1c9150c..9ee7840c2cf6 100644 --- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx @@ -20,7 +20,7 @@ function WorkspaceProfileCurrencyPage({policy}: WorkspaceProfileCurrencyPageProp const onSelectCurrency = (item: CurrencyListItem) => { Policy.updateGeneralSettings(policy?.id ?? '', policy?.name ?? '', item.currencyCode); - Navigation.setNabigationActionToMicrotaskQueue(Navigation.goBack); + Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack); }; return ( From 49b050f3426380dd85b89457ac98a258fb5ef038 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Fri, 17 May 2024 20:16:43 +0200 Subject: [PATCH 3/4] rename param for setNavigationActionToMicrotaskQueue --- src/libs/Navigation/setNavigationActionToMicrotaskQueue.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/setNavigationActionToMicrotaskQueue.ts b/src/libs/Navigation/setNavigationActionToMicrotaskQueue.ts index 0794b4a0bf26..f6c221185de6 100644 --- a/src/libs/Navigation/setNavigationActionToMicrotaskQueue.ts +++ b/src/libs/Navigation/setNavigationActionToMicrotaskQueue.ts @@ -1,4 +1,4 @@ -export default function setNavigationActionToMicrotaskQueue(nabigationAction: () => void) { +export default function setNavigationActionToMicrotaskQueue(navigationAction: () => void) { /** * The function is used when the app needs to set a navigation action to the microtask queue, it guarantees to execute Onyx.update first, then the navigation action. * More details - https://github.com/Expensify/App/issues/37785#issuecomment-1989056726. @@ -7,7 +7,7 @@ export default function setNavigationActionToMicrotaskQueue(nabigationAction: () resolve(); }).then(() => { requestAnimationFrame(() => { - nabigationAction(); + navigationAction(); }); }); } From e9dc2ef95eb8007aa2b41f676fe952da0359ef47 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Tue, 28 May 2024 09:59:47 +0200 Subject: [PATCH 4/4] Fix conflicts and update branch --- .eslintignore | 14 +- .eslintrc.js | 371 ++-- .github/.eslintrc.js | 6 +- .../javascript/bumpVersion/bumpVersion.ts | 11 +- .../actions/javascript/bumpVersion/index.js | 5 +- .../checkDeployBlockers.ts | 2 +- .../getGraphiteString/getGraphiteString.ts | 2 +- .../getPreviousVersion/getPreviousVersion.ts | 9 +- .../javascript/getPreviousVersion/index.js | 5 +- .../reviewerChecklist/reviewerChecklist.ts | 2 +- .github/libs/sanitizeStringForJSONParse.ts | 2 +- .github/scripts/detectRedirectCycle.ts | 2 +- .github/workflows/e2ePerformanceTests.yml | 4 + .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- .github/workflows/typecheck.yml | 4 + .storybook/preview.tsx | 2 +- __mocks__/@react-navigation/native/index.ts | 4 +- android/app/build.gradle | 15 +- android/build.gradle | 6 +- babel.config.js | 11 + config/webpack/webpack.common.ts | 2 +- contributingGuides/CONTRIBUTING.md | 9 +- contributingGuides/STYLE.md | 2 +- desktop/contextBridge.ts | 2 +- desktop/main.ts | 4 +- .../expenses/Referral-Program.md | 49 - .../Change-Expensify-Card-limit.md | 25 + .../Deactivate-or-cancel-an-Expensify-Card.md | 28 + .../expensify-card/Unlimited-Virtual-Cards.md | 14 +- .../Referral-Program.md | 6 +- .../accounting-integrations/NetSuite.md | 6 +- .../workspaces/reports/Currency.md | 31 - .../new-expensify/connections/Coming-Soon.md | 6 - .../connections/Set-up-Xero-connection.md | 102 + .../expenses/Approve-and-pay-expenses.md | 72 + .../expensify-card/Coming-Soon.md | 4 - .../new-expensify/workspaces/Coming-Soon.md | 6 - ...equire-tags-and-categories-for-expenses.md | 4 +- docs/redirects.csv | 4 + ios/NewExpensify.xcodeproj/project.pbxproj | 4 + ios/NewExpensify/Info.plist | 4 +- ios/NewExpensifyTests/Info.plist | 4 +- ios/NotificationServiceExtension/Info.plist | 4 +- ios/Podfile | 2 + ios/Podfile.lock | 49 +- jest/setup.ts | 2 + jest/setupMockFullstoryLib.ts | 24 + package-lock.json | 120 +- package.json | 19 +- src/CONST.ts | 42 +- src/Expensify.tsx | 10 +- src/ONYXKEYS.ts | 10 + src/ROUTES.ts | 4 - src/SCREENS.ts | 6 - src/components/AccountingListSkeletonView.tsx | 34 + src/components/AddressForm.tsx | 4 +- .../BaseAnchorForAttachmentsOnly.tsx | 4 +- src/components/ArrowKeyFocusManager.js | 142 -- src/components/AttachmentModal.tsx | 5 +- src/components/AttachmentOfflineIndicator.tsx | 57 + .../AttachmentPicker/index.native.tsx | 4 +- .../AttachmentCarousel/CarouselItem.tsx | 2 +- .../Attachments/AttachmentCarousel/index.tsx | 5 +- .../BaseAttachmentViewPdf.tsx | 6 +- .../AttachmentViewPdf/index.tsx | 5 +- .../AttachmentView/AttachmentViewPdf/types.ts | 11 +- .../DefaultAttachmentView/index.tsx | 67 + .../Attachments/AttachmentView/index.tsx | 60 +- .../BaseAutoCompleteSuggestions.tsx | 24 +- src/components/Avatar.tsx | 8 +- src/components/AvatarWithDisplayName.tsx | 6 +- src/components/AvatarWithImagePicker.tsx | 6 +- src/components/CategoryPicker.tsx | 7 +- src/components/Composer/index.tsx | 2 +- .../index.native.tsx | 3 + .../ConnectToQuickbooksOnlineButton/index.tsx | 3 + .../ConnectToXeroButton/index.native.tsx | 3 + src/components/ConnectToXeroButton/index.tsx | 3 + src/components/CountrySelector.tsx | 28 +- src/components/FixedFooter.tsx | 4 +- src/components/FullstoryNative.tsx | 3 + .../HTMLRenderers/MentionUserRenderer.tsx | 2 +- src/components/HeaderWithBackButton/index.tsx | 2 +- src/components/Icon/__mocks__/Expensicons.ts | 5 +- src/components/ImageView/index.tsx | 9 +- src/components/ImageWithSizeCalculation.tsx | 4 +- .../BaseInvertedFlatList/index.tsx | 2 +- .../KeyboardAvoidingView/index.ios.tsx | 2 +- src/components/KeyboardAvoidingView/index.tsx | 2 +- src/components/KeyboardAvoidingView/types.ts | 5 +- .../LHNOptionsList/OptionRowLHN.tsx | 19 +- src/components/Lightbox/index.tsx | 16 +- src/components/MentionSuggestions.tsx | 2 +- src/components/MenuItem.tsx | 9 +- src/components/MoneyReportHeader.tsx | 119 +- src/components/MoneyRequestAmountInput.tsx | 1 + .../MoneyRequestConfirmationList.tsx | 15 +- src/components/MoneyRequestHeader.tsx | 173 +- .../MoneyRequestHeaderStatusBar.tsx | 9 +- src/components/MultipleAvatars.tsx | 8 +- src/components/OfflineIndicator.tsx | 23 +- src/components/OfflineWithFeedback.tsx | 8 +- src/components/Onfido/BaseOnfidoWeb.tsx | 5 +- src/components/Onfido/index.native.tsx | 7 +- src/components/OptionListContextProvider.tsx | 15 +- src/components/PDFThumbnail/index.tsx | 1 - src/components/PDFView/index.native.tsx | 30 +- src/components/PDFView/index.tsx | 23 +- src/components/PDFView/types.ts | 9 +- .../GenericPressable/BaseGenericPressable.tsx | 2 + .../Pressable/GenericPressable/types.ts | 5 + .../BaseProfilingToolMenu.tsx | 2 +- .../MoneyRequestPreviewContent.tsx | 42 +- .../MoneyRequestPreview/types.ts | 18 +- .../ReportActionItem/MoneyRequestView.tsx | 4 +- .../ReportActionItem/ReportPreview.tsx | 62 +- src/components/RoomHeaderAvatars.tsx | 4 +- src/components/ScreenWrapper.tsx | 9 +- src/components/Search.tsx | 50 +- .../SelectionList/BaseSelectionList.tsx | 132 +- .../SelectionList/TransactionListItem.tsx | 414 ++-- src/components/SelectionList/types.ts | 74 +- .../AppleSignIn/index.android.tsx | 6 +- .../SignInButtons/AppleSignIn/index.ios.tsx | 7 +- .../Skeletons/ItemListSkeletonView.tsx | 9 +- .../Skeletons/TableListItemSkeleton.tsx | 4 +- src/components/StateSelector.tsx | 4 +- src/components/SubscriptAvatar.tsx | 4 +- src/components/TabSelector/TabSelector.tsx | 2 +- .../TextInput/BaseTextInput/index.tsx | 4 +- .../TextInputWithCurrencySymbol/types.ts | 5 + .../TextPicker/TextSelectorModal.tsx | 29 +- .../BaseUserDetailsTooltip/index.tsx | 1 + .../VideoPlayer/BaseVideoPlayer.tsx | 30 +- src/components/VideoPlayer/types.ts | 1 + .../VideoPopoverMenuContext.tsx | 11 +- src/components/VideoPlayerContexts/types.ts | 1 + src/components/VideoPlayerPreview/index.tsx | 1 + src/components/tagPropTypes.js | 1 + .../useActiveWorkspaceFromNavigationState.ts | 33 + .../config.native.ts | 5 - src/hooks/useAnimatedHighlightStyle/config.ts | 8 - src/hooks/useAnimatedHighlightStyle/index.ts | 79 +- src/hooks/useAutoFocusInput.ts | 1 - ...useGeographicalStateAndCountryFromRoute.ts | 27 + src/hooks/useGeographicalStateFromRoute.ts | 23 - src/hooks/useNetwork.ts | 9 +- .../useReportScrollManager/index.native.ts | 8 +- src/hooks/useReportScrollManager/index.ts | 9 +- src/hooks/useReportScrollManager/types.ts | 2 +- src/hooks/useTabNavigatorFocus/index.ts | 1 - src/hooks/useViolations.ts | 3 +- src/languages/en.ts | 41 +- src/languages/es.ts | 29 +- src/languages/types.ts | 23 +- src/libs/API/parameters/Search.ts | 2 + ...anyPolicyConnectionConfigurationsParams.ts | 8 + .../UpdatePolicyConnectionConfigParams.ts | 2 +- src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/ActiveClientManager/index.ts | 32 +- src/libs/EmojiUtils.ts | 12 +- .../Environment/betaChecker/index.android.ts | 2 +- src/libs/ErrorUtils.ts | 21 +- src/libs/Fullstory/index.native.ts | 52 + src/libs/Fullstory/index.ts | 94 + src/libs/Fullstory/types.ts | 5 + src/libs/Localize/index.ts | 2 +- src/libs/Middleware/Logging.ts | 7 +- src/libs/MoneyRequestUtils.ts | 6 +- .../WorkspaceSettingsModalStackNavigator.tsx | 95 - .../ModalStackNavigators/index.tsx | 24 +- .../Navigators/BottomTabNavigator.tsx | 4 +- .../Navigators/FullScreenNavigator.tsx | 35 +- .../Navigators/LeftModalNavigator.tsx | 8 +- .../Navigators/OnboardingModalNavigator.tsx | 24 +- .../Navigators/Overlay/BaseOverlay.tsx | 5 +- .../createCustomBottomTabNavigator/TopBar.tsx | 2 +- .../CustomFullScreenRouter.tsx | 22 +- .../AppNavigator/getPartialStateDiff.ts | 4 +- .../getRootNavigatorScreenOptions.ts | 3 + src/libs/Navigation/Navigation.ts | 2 +- src/libs/Navigation/NavigationRoot.tsx | 7 + .../Navigation/extrapolateStateFromParams.ts | 30 + .../Navigation/getTopmostNestedRHPRoute.ts | 25 - .../getTopmostWorkspacesCentralPaneName.ts | 27 - src/libs/Navigation/isSideModalNavigator.ts | 7 + .../linkTo/getActionForBottomTabNavigator.ts | 51 + .../Navigation/linkTo/getMinimalAction.ts | 42 + .../Navigation/{linkTo.ts => linkTo/index.ts} | 185 +- src/libs/Navigation/linkTo/types.ts | 11 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 1 - src/libs/Navigation/linkingConfig/config.ts | 97 +- .../linkingConfig/customGetPathFromState.ts | 10 +- .../extractPolicyIDsFromState.ts | 13 + .../linkingConfig/getAdaptedStateFromPath.ts | 30 +- src/libs/Navigation/linkingConfig/index.ts | 14 +- src/libs/Navigation/linkingConfig/prefixes.ts | 15 + .../linkingConfig/subscribe/index.native.ts | 40 + .../linkingConfig/subscribe/index.ts | 7 + src/libs/Navigation/switchPolicyID.ts | 91 +- src/libs/Navigation/types.ts | 42 +- src/libs/NetworkConnection.ts | 102 +- src/libs/NextStepUtils.ts | 47 +- .../LocalNotification/BrowserNotifications.ts | 2 +- .../PushNotification/index.native.ts | 2 +- src/libs/ObjectUtils.ts | 8 +- src/libs/OnboardingRefManager.ts | 17 + src/libs/OptionsListUtils.ts | 73 +- src/libs/PersonalDetailsUtils.ts | 8 +- src/libs/PolicyUtils.ts | 78 +- src/libs/Pusher/pusher.ts | 30 +- src/libs/ReportActionComposeFocusManager.ts | 3 +- src/libs/ReportActionsUtils.ts | 14 +- src/libs/ReportUtils.ts | 139 +- src/libs/SearchUtils.ts | 5 +- src/libs/SelectionScraper/index.ts | 2 +- src/libs/StringUtils.ts | 9 +- src/libs/TransactionUtils.ts | 55 +- src/libs/Url.ts | 5 +- src/libs/Violations/ViolationsUtils.ts | 2 + src/libs/actions/BankAccounts.ts | 3 +- src/libs/actions/Device/index.ts | 2 +- src/libs/actions/FormActions.ts | 2 +- src/libs/actions/IOU.ts | 60 +- src/libs/actions/Network.ts | 6 +- src/libs/actions/OnyxUpdateManager/index.ts | 30 +- .../utils/DeferredOnyxUpdates.ts | 133 ++ .../utils/deferredUpdates.ts | 8 - .../actions/OnyxUpdateManager/utils/index.ts | 31 +- src/libs/actions/OnyxUpdates.ts | 6 +- src/libs/actions/Policy.ts | 42 +- src/libs/actions/Report.ts | 83 +- src/libs/actions/ReportActions.ts | 4 +- src/libs/actions/Search.ts | 33 +- src/libs/actions/Session/index.ts | 6 + src/libs/actions/Task.ts | 3 +- src/libs/actions/Transaction.ts | 19 +- src/libs/actions/Welcome.ts | 6 +- src/libs/actions/connections/index.ts | 73 +- .../index.android.ts | 15 + src/libs/checkInternetReachability/index.ts | 5 + src/libs/checkInternetReachability/types.ts | 3 + src/libs/migrations/NVPMigration.ts | 20 +- src/libs/migrations/RenameReceiptFilename.ts | 4 +- .../TransactionBackupsToCollection.ts | 4 +- src/pages/AddressPage.tsx | 107 + src/pages/ChatFinderPage/index.tsx | 7 +- .../EnablePayments/AdditionalDetailsStep.tsx | 3 +- src/pages/EnablePayments/IdologyQuestions.tsx | 2 +- src/pages/GroupChatNameEditPage.tsx | 3 +- src/pages/InviteReportParticipantsPage.tsx | 2 +- src/pages/NewChatConfirmPage.tsx | 1 + src/pages/NewChatPage.tsx | 6 +- .../BaseOnboardingPersonalDetails.tsx | 4 +- .../BaseOnboardingPurpose.tsx | 30 +- .../OnboardingWork/BaseOnboardingWork.tsx | 17 +- src/pages/OnboardingWork/types.ts | 3 + .../PrivateNotes/PrivateNotesEditPage.tsx | 6 +- .../PrivateNotes/PrivateNotesListPage.tsx | 6 +- .../DateOfBirthUBO.tsx | 3 +- .../substeps/IncorporationDateBusiness.tsx | 3 +- .../components/BankAccountValidationForm.tsx | 3 +- .../PersonalInfo/substeps/DateOfBirth.tsx | 3 +- .../utils/getValuesForBeneficialOwner.ts | 3 +- src/pages/ReportAvatar.tsx | 3 +- src/pages/ReportDetailsPage.tsx | 5 +- src/pages/RoomInvitePage.tsx | 2 +- src/pages/RoomMembersPage.tsx | 22 +- src/pages/Search/SearchPage.tsx | 18 +- src/pages/Search/SearchPageBottomTab.tsx | 10 +- src/pages/WorkspaceSwitcherPage/index.tsx | 3 +- src/pages/home/ReportScreen.tsx | 52 +- .../report/ContextMenu/ContextMenuActions.tsx | 6 +- .../AttachmentPickerWithMenuItems.tsx | 2 +- .../ComposerWithSuggestions.tsx | 11 +- .../ReportActionCompose.tsx | 12 +- src/pages/home/report/ReportActionItem.tsx | 22 +- .../report/ReportActionItemMessageEdit.tsx | 10 +- .../home/report/ReportActionItemSingle.tsx | 4 +- src/pages/home/report/ReportActionsList.tsx | 40 +- src/pages/home/report/ReportActionsView.tsx | 57 +- src/pages/home/report/ReportFooter.tsx | 8 +- .../sidebar/ProfileAvatarWithIndicator.tsx | 3 +- src/pages/home/sidebar/SidebarLinksData.tsx | 4 +- .../SidebarScreen/BaseSidebarScreen.tsx | 4 +- .../FloatingActionButtonAndPopover.tsx | 3 +- src/pages/iou/SplitBillDetailsPage.tsx | 1 + .../MoneyRequestParticipantsSelector.tsx | 30 +- .../iou/request/step/IOURequestStepAmount.tsx | 24 +- .../request/step/IOURequestStepMerchant.tsx | 4 +- .../step/IOURequestStepParticipants.tsx | 7 + .../{index.native.tsx => Camera.tsx} | 10 +- .../{index.tsx => WebCamera.tsx} | 6 +- .../NavigationAwareCamera/types.ts | 2 +- .../step/IOURequestStepScan/index.native.tsx | 11 +- .../request/step/IOURequestStepScan/index.tsx | 7 +- .../request/step/IOURequestStepSendFrom.tsx | 1 + .../step/IOURequestStepTaxAmountPage.tsx | 2 +- .../ShareLogList/BaseShareLogList.tsx | 2 +- .../Profile/PersonalDetails/AddressPage.tsx | 153 -- .../PersonalDetails/PersonalAddressPage.tsx | 61 + .../PersonalDetails/StateSelectionPage.tsx | 3 +- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 2 +- .../TaskShareDestinationSelectorModal.tsx | 2 +- src/pages/workspace/WorkspaceInitialPage.tsx | 4 +- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- src/pages/workspace/WorkspaceMembersPage.tsx | 13 +- .../workspace/WorkspaceMoreFeaturesPage.tsx | 6 +- .../workspace/WorkspaceProfileAddressPage.tsx | 117 +- src/pages/workspace/WorkspaceProfilePage.tsx | 2 +- src/pages/workspace/WorkspacesListRow.tsx | 2 +- .../accounting/PolicyAccountingPage.tsx | 42 +- .../qbo/advanced/QuickbooksAdvancedPage.tsx | 5 + ...uickbooksCompanyCardExpenseAccountPage.tsx | 69 +- ...anyCardExpenseAccountPayableSelectPage.tsx | 73 - ...ompanyCardExpenseAccountSelectCardPage.tsx | 32 +- ...oksCompanyCardExpenseAccountSelectPage.tsx | 8 +- ...oksOutOfPocketExpenseConfigurationPage.tsx | 2 +- ...ooksOutOfPocketExpenseEntitySelectPage.tsx | 28 +- .../qbo/import/QuickbooksTaxesPage.tsx | 6 +- .../accounting/xero/XeroImportPage.tsx | 30 +- .../xero/XeroTaxesConfigurationPage.tsx | 60 +- .../XeroTrackingCategoryConfigurationPage.tsx | 50 +- .../xero/advanced/XeroAdvancedPage.tsx | 2 + .../xero/import/XeroChartOfAccountsPage.tsx | 9 +- .../import/XeroCustomerConfigurationPage.tsx | 55 +- .../workspace/bills/WorkspaceBillsPage.tsx | 4 +- .../workspace/card/WorkspaceCardPage.tsx | 4 +- .../categories/WorkspaceCategoriesPage.tsx | 45 +- .../WorkspaceCategoriesSettingsPage.tsx | 13 +- .../PolicyDistanceRateDetailsPage.tsx | 12 +- .../distanceRates/PolicyDistanceRatesPage.tsx | 17 +- .../invoices/WorkspaceInvoicesPage.tsx | 4 +- .../members/WorkspaceMemberDetailsPage.tsx | 19 +- .../reimburse/WorkspaceReimbursePage.tsx | 4 +- .../workspace/tags/WorkspaceTagsPage.tsx | 49 +- .../workspace/tags/WorkspaceViewTagsPage.tsx | 6 +- .../taxes/WorkspaceCreateTaxPage.tsx | 1 - .../workspace/taxes/WorkspaceTaxesPage.tsx | 23 +- .../workspace/travel/WorkspaceTravelPage.tsx | 4 +- src/pages/workspace/withPolicy.tsx | 8 +- src/pages/workspace/withPolicyConnections.tsx | 25 +- .../workflows/ToggleSettingsOptionRow.tsx | 18 +- .../WorkspaceAutoReportingFrequencyPage.tsx | 4 +- ...orkspaceAutoReportingMonthlyOffsetPage.tsx | 5 +- .../WorkspaceWorkflowsApproverPage.tsx | 6 +- .../workflows/WorkspaceWorkflowsPage.tsx | 9 +- .../workflows/WorkspaceWorkflowsPayerPage.tsx | 2 +- src/setup/addUtilsToWindow.ts | 6 +- src/stories/Form.stories.tsx | 27 +- src/styles/index.ts | 20 + src/styles/theme/themes/dark.ts | 2 +- src/styles/theme/themes/light.ts | 2 +- src/styles/utils/sizing.ts | 4 + src/types/modules/dom.d.ts | 45 + src/types/modules/pdf.worker.d.ts | 1 + src/types/modules/react-native-onyx.d.ts | 7 +- src/types/modules/react-native.d.ts | 15 + src/types/onyx/IOU.ts | 1 + src/types/onyx/Network.ts | 3 + src/types/onyx/Report.ts | 1 + src/types/onyx/ReportNameValuePairs.ts | 7 + src/types/onyx/SearchResults.ts | 1 + src/types/onyx/TransactionViolation.ts | 1 + src/types/onyx/UserMetadata.ts | 8 + src/types/onyx/index.ts | 4 + src/types/utils/isLoadingOnyxValue.ts | 4 +- src/utils/arraysEqual.ts | 25 + tests/.eslintrc.js | 16 + tests/actions/IOUTest.ts | 1863 ++++++++--------- tests/actions/PolicyCategoryTest.ts | 247 +++ tests/actions/PolicyMemberTest.ts | 232 ++ tests/actions/PolicyProfileTest.ts | 68 + tests/actions/PolicyTagTest.ts | 1147 +++++----- tests/actions/PolicyTaxTest.ts | 790 ++++--- tests/actions/PolicyTest.ts | 8 +- tests/actions/ReportTest.ts | 6 - tests/e2e/server/index.ts | 2 +- tests/perf-test/ChatFinderPage.perf-test.tsx | 81 +- tests/perf-test/ReportScreen.perf-test.tsx | 9 +- tests/perf-test/SidebarLinks.perf-test.tsx | 1 + tests/ui/UnreadIndicatorsTest.tsx | 9 +- tests/unit/APITest.ts | 121 +- tests/unit/EmojiTest.ts | 1 - tests/unit/MiddlewareTest.ts | 9 +- tests/unit/MigrationTest.ts | 14 +- tests/unit/NetworkTest.ts | 11 +- tests/unit/NextStepUtilsTest.ts | 33 +- tests/unit/ReportActionsUtilsTest.ts | 4 +- tests/unit/RequestTest.ts | 1 - tests/unit/SidebarOrderTest.ts | 1 + tests/unit/SidebarTest.ts | 1 + tests/unit/awaitStagingDeploysTest.ts | 6 +- tests/unit/markPullRequestsAsDeployedTest.ts | 6 +- tests/unit/sanitizeStringForJSONParseTest.ts | 1 - tests/utils/TestHelper.ts | 5 +- tests/utils/collections/policyCategory.ts | 21 + tests/utils/collections/reportActions.ts | 17 +- tsconfig.json | 30 +- web/gtm.js | 11 - web/index.html | 6 +- web/thirdPartyScripts.js | 155 ++ workflow_tests/.eslintrc.js | 7 + workflow_tests/utils/ExtendedAct.ts | 4 +- workflow_tests/utils/preGenerateTest.ts | 8 +- 407 files changed, 7451 insertions(+), 5279 deletions(-) delete mode 100644 docs/articles/expensify-classic/expenses/Referral-Program.md create mode 100644 docs/articles/expensify-classic/expensify-card/Change-Expensify-Card-limit.md create mode 100644 docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md delete mode 100644 docs/articles/expensify-classic/workspaces/reports/Currency.md delete mode 100644 docs/articles/new-expensify/connections/Coming-Soon.md create mode 100644 docs/articles/new-expensify/connections/Set-up-Xero-connection.md create mode 100644 docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md delete mode 100644 docs/articles/new-expensify/expensify-card/Coming-Soon.md delete mode 100644 docs/articles/new-expensify/workspaces/Coming-Soon.md create mode 100644 jest/setupMockFullstoryLib.ts create mode 100644 src/components/AccountingListSkeletonView.tsx delete mode 100644 src/components/ArrowKeyFocusManager.js create mode 100644 src/components/AttachmentOfflineIndicator.tsx create mode 100644 src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx create mode 100644 src/components/FullstoryNative.tsx create mode 100644 src/hooks/useActiveWorkspaceFromNavigationState.ts delete mode 100644 src/hooks/useAnimatedHighlightStyle/config.native.ts delete mode 100644 src/hooks/useAnimatedHighlightStyle/config.ts create mode 100644 src/hooks/useGeographicalStateAndCountryFromRoute.ts delete mode 100644 src/hooks/useGeographicalStateFromRoute.ts create mode 100644 src/libs/API/parameters/UpdateManyPolicyConnectionConfigurationsParams.ts create mode 100644 src/libs/Fullstory/index.native.ts create mode 100644 src/libs/Fullstory/index.ts create mode 100644 src/libs/Fullstory/types.ts delete mode 100644 src/libs/Navigation/AppNavigator/ModalStackNavigators/WorkspaceSettingsModalStackNavigator.tsx create mode 100644 src/libs/Navigation/extrapolateStateFromParams.ts delete mode 100644 src/libs/Navigation/getTopmostNestedRHPRoute.ts delete mode 100644 src/libs/Navigation/getTopmostWorkspacesCentralPaneName.ts create mode 100644 src/libs/Navigation/isSideModalNavigator.ts create mode 100644 src/libs/Navigation/linkTo/getActionForBottomTabNavigator.ts create mode 100644 src/libs/Navigation/linkTo/getMinimalAction.ts rename src/libs/Navigation/{linkTo.ts => linkTo/index.ts} (56%) create mode 100644 src/libs/Navigation/linkTo/types.ts create mode 100644 src/libs/Navigation/linkingConfig/extractPolicyIDsFromState.ts create mode 100644 src/libs/Navigation/linkingConfig/prefixes.ts create mode 100644 src/libs/Navigation/linkingConfig/subscribe/index.native.ts create mode 100644 src/libs/Navigation/linkingConfig/subscribe/index.ts create mode 100644 src/libs/OnboardingRefManager.ts create mode 100644 src/libs/actions/OnyxUpdateManager/utils/DeferredOnyxUpdates.ts delete mode 100644 src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts create mode 100644 src/libs/checkInternetReachability/index.android.ts create mode 100644 src/libs/checkInternetReachability/index.ts create mode 100644 src/libs/checkInternetReachability/types.ts create mode 100644 src/pages/AddressPage.tsx rename src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/{index.native.tsx => Camera.tsx} (66%) rename src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/{index.tsx => WebCamera.tsx} (75%) delete mode 100644 src/pages/settings/Profile/PersonalDetails/AddressPage.tsx create mode 100644 src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx delete mode 100644 src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPayableSelectPage.tsx create mode 100644 src/types/modules/pdf.worker.d.ts create mode 100644 src/types/onyx/ReportNameValuePairs.ts create mode 100644 src/types/onyx/UserMetadata.ts create mode 100644 src/utils/arraysEqual.ts create mode 100644 tests/actions/PolicyCategoryTest.ts create mode 100644 tests/actions/PolicyMemberTest.ts create mode 100644 tests/actions/PolicyProfileTest.ts create mode 100644 tests/utils/collections/policyCategory.ts delete mode 100644 web/gtm.js create mode 100644 web/thirdPartyScripts.js create mode 100644 workflow_tests/.eslintrc.js diff --git a/.eslintignore b/.eslintignore index d3358a02fe4b..5338df11c520 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,11 @@ -**/node_modules/* -**/dist/* -android/**/build/* -.github/actions/**/index.js" +!.storybook +!.github +.github/actions/**/index.js +*.config.js +**/.eslintrc.js +**/node_modules/** +**/dist/** +android/**/build/** docs/vendor/** +docs/assets/** +web/gtm.js diff --git a/.eslintrc.js b/.eslintrc.js index c8a842fa4650..135252825dcf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,9 +4,9 @@ const restrictedImportPaths = [ importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text', 'ScrollView'], message: [ '', - "For 'useWindowDimensions', please use 'src/hooks/useWindowDimensions' instead.", - "For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.", - "For 'StatusBar', please use 'src/libs/StatusBar' instead.", + "For 'useWindowDimensions', please use '@src/hooks/useWindowDimensions' instead.", + "For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from '@components/Pressable' instead.", + "For 'StatusBar', please use '@src/libs/StatusBar' instead.", "For 'Text', please use '@components/Text' instead.", "For 'ScrollView', please use '@components/ScrollView' instead.", ].join('\n'), @@ -14,7 +14,7 @@ const restrictedImportPaths = [ { name: 'react-native-gesture-handler', importNames: ['TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight'], - message: "Please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.", + message: "Please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from '@components/Pressable' instead.", }, { name: 'awesome-phonenumber', @@ -24,7 +24,7 @@ const restrictedImportPaths = [ { name: 'react-native-safe-area-context', importNames: ['useSafeAreaInsets', 'SafeAreaConsumer', 'SafeAreaInsetsContext'], - message: "Please use 'useSafeAreaInsets' from 'src/hooks/useSafeAreaInset' and/or 'SafeAreaConsumer' from 'src/components/SafeAreaConsumer' instead.", + message: "Please use 'useSafeAreaInsets' from '@src/hooks/useSafeAreaInset' and/or 'SafeAreaConsumer' from '@components/SafeAreaConsumer' instead.", }, { name: 'react', @@ -34,18 +34,18 @@ const restrictedImportPaths = [ { name: '@styles/index', importNames: ['default', 'defaultStyles'], - message: 'Do not import styles directly. Please use the `useThemeStyles` hook or `withThemeStyles` HOC instead.', + message: 'Do not import styles directly. Please use the `useThemeStyles` hook instead.', }, { name: '@styles/utils', importNames: ['default', 'DefaultStyleUtils'], - message: 'Do not import StyleUtils directly. Please use the `useStyleUtils` hook or `withStyleUtils` HOC instead.', + message: 'Do not import StyleUtils directly. Please use the `useStyleUtils` hook instead.', }, { name: '@styles/theme', importNames: ['default', 'defaultTheme'], - message: 'Do not import themes directly. Please use the `useTheme` hook or `withTheme` HOC instead.', + message: 'Do not import themes directly. Please use the `useTheme` hook instead.', }, { name: '@styles/theme/illustrations', @@ -60,15 +60,15 @@ const restrictedImportPaths = [ const restrictedImportPatterns = [ { group: ['**/assets/animations/**/*.json'], - message: "Do not import animations directly. Please use the 'src/components/LottieAnimations' import instead.", + message: "Do not import animations directly. Please use the '@components/LottieAnimations' import instead.", }, { group: ['@styles/theme/themes/**'], - message: 'Do not import themes directly. Please use the `useTheme` hook or `withTheme` HOC instead.', + message: 'Do not import themes directly. Please use the `useTheme` hook instead.', }, { group: ['@styles/utils/**', '!@styles/utils/FontUtils', '!@styles/utils/types'], - message: 'Do not import style util functions directly. Please use the `useStyleUtils` hook or `withStyleUtils` HOC instead.', + message: 'Do not import style util functions directly. Please use the `useStyleUtils` hook instead.', }, { group: ['@styles/theme/illustrations/themes/**'], @@ -77,218 +77,175 @@ const restrictedImportPatterns = [ ]; module.exports = { - extends: ['expensify', 'plugin:storybook/recommended', 'plugin:react-native-a11y/basic', 'plugin:@dword-design/import-alias/recommended', 'prettier'], - plugins: ['react-native-a11y', 'testing-library'], - parser: 'babel-eslint', - ignorePatterns: ['!.*', 'src/vendor', '.github/actions/**/index.js', 'desktop/dist/*.js', 'dist/*.js', 'node_modules/.bin/**', 'node_modules/.cache/**', '.git/**'], + extends: [ + 'expensify', + 'airbnb-typescript', + 'plugin:storybook/recommended', + 'plugin:react-native-a11y/basic', + 'plugin:@dword-design/import-alias/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', + 'plugin:@typescript-eslint/stylistic-type-checked', + 'plugin:you-dont-need-lodash-underscore/all', + 'prettier', + ], + plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + }, env: { jest: true, }, globals: { __DEV__: 'readonly', }, - overrides: [ - { - files: ['*.js', '*.jsx', '*.ts', '*.tsx'], - plugins: ['react'], - rules: { - 'prefer-regex-literals': 'off', - 'rulesdir/no-multiple-onyx-in-file': 'off', - 'react-native-a11y/has-accessibility-hint': ['off'], - 'react/jsx-no-constructed-context-values': 'error', - 'react-native-a11y/has-valid-accessibility-descriptors': [ - 'error', - { - touchables: ['PressableWithoutFeedback', 'PressableWithFeedback'], - }, - ], - '@dword-design/import-alias/prefer-alias': [ - 'warn', - { - alias: { - '@assets': './assets', - '@components': './src/components', - '@hooks': './src/hooks', - // This is needed up here, if not @libs/actions would take the priority - '@userActions': './src/libs/actions', - '@libs': './src/libs', - '@navigation': './src/libs/Navigation', - '@pages': './src/pages', - '@styles': './src/styles', - // This path is provide alias for files like `ONYXKEYS` and `CONST`. - '@src': './src', - '@desktop': './desktop', - '@github': './.github', - }, - }, - ], - 'rulesdir/avoid-anonymous-functions': 'off', + rules: { + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + + // TypeScript specific rules + '@typescript-eslint/prefer-enum-initializers': 'error', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/switch-exhaustiveness-check': 'error', + '@typescript-eslint/consistent-type-definitions': ['error', 'type'], + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/array-type': ['error', {default: 'array-simple'}], + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: ['variable', 'property'], + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], }, - }, - // This helps disable the `prefer-alias` rule to be enabled for specific directories - { - files: ['tests/**/*.js', 'tests/**/*.ts', 'tests/**/*.jsx', 'assets/**/*.js', '.storybook/**/*.js'], - rules: {'@dword-design/import-alias/prefer-alias': ['off']}, - }, - { - files: ['tests/**/*.js', 'tests/**/*.ts', 'tests/**/*.jsx', 'tests/**/*.tsx'], - extends: ['plugin:testing-library/react'], - rules: { - 'testing-library/await-async-queries': 'error', - 'testing-library/await-async-utils': 'error', - 'testing-library/no-debugging-utils': 'error', - 'testing-library/no-manual-cleanup': 'error', - 'testing-library/no-unnecessary-act': 'error', - 'testing-library/prefer-find-by': 'error', - 'testing-library/prefer-presence-queries': 'error', - 'testing-library/prefer-screen-queries': 'error', + { + selector: 'function', + format: ['camelCase', 'PascalCase'], }, - }, - { - files: ['*.js', '*.jsx'], - settings: { - 'import/resolver': { - node: { - extensions: ['.js', '.website.js', '.desktop.js', '.native.js', '.ios.js', '.android.js', '.config.js', '.ts', '.tsx'], - }, + { + selector: ['typeLike', 'enumMember'], + format: ['PascalCase'], + }, + { + selector: ['parameter', 'method'], + format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allow', + }, + ], + '@typescript-eslint/ban-types': [ + 'error', + { + types: { + object: "Use 'Record' instead.", }, + extendDefaults: true, }, - rules: { - 'import/extensions': [ - 'error', - 'ignorePackages', - { - js: 'never', - jsx: 'never', - ts: 'never', - tsx: 'never', - }, - ], - 'no-restricted-imports': [ - 'error', - { - paths: restrictedImportPaths, - patterns: restrictedImportPatterns, - }, - ], - curly: 'error', - 'react/display-name': 'error', + ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + prefer: 'type-imports', + fixStyle: 'separate-type-imports', }, - }, - { - files: ['*.ts', '*.tsx'], - extends: [ - 'airbnb-typescript', - 'plugin:@typescript-eslint/recommended-type-checked', - 'plugin:@typescript-eslint/stylistic-type-checked', - 'plugin:you-dont-need-lodash-underscore/all', - 'prettier', - ], - plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore'], - parser: '@typescript-eslint/parser', - parserOptions: { - project: './tsconfig.json', + ], + '@typescript-eslint/consistent-type-exports': [ + 'error', + { + fixMixedExportsWithInlineTypeSpecifier: false, }, - rules: { - // TODO: Remove the following rules after TypeScript migration is complete. - '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-assignment': 'off', + ], + '@typescript-eslint/no-use-before-define': ['error', {functions: false}], + + // ESLint core rules + 'es/no-nullish-coalescing-operators': 'off', + 'es/no-optional-chaining': 'off', + + // Import specific rules + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + 'import/no-extraneous-dependencies': 'off', + + // Rulesdir specific rules + 'rulesdir/no-default-props': 'error', + 'rulesdir/no-multiple-onyx-in-file': 'off', + 'rulesdir/prefer-underscore-method': 'off', + 'rulesdir/prefer-import-module-contents': 'off', - '@typescript-eslint/naming-convention': [ - 'error', - { - selector: ['variable', 'property'], - format: ['camelCase', 'UPPER_CASE', 'PascalCase'], - }, - { - selector: 'function', - format: ['camelCase', 'PascalCase'], - }, - { - selector: ['typeLike', 'enumMember'], - format: ['PascalCase'], - }, - { - selector: ['parameter', 'method'], - format: ['camelCase', 'PascalCase'], - leadingUnderscore: 'allow', - }, - ], - '@typescript-eslint/ban-types': [ - 'error', - { - types: { - object: "Use 'Record' instead.", - }, - extendDefaults: true, - }, - ], - '@typescript-eslint/array-type': ['error', {default: 'array-simple'}], - '@typescript-eslint/prefer-enum-initializers': 'error', - '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/no-non-null-assertion': 'error', - '@typescript-eslint/switch-exhaustiveness-check': 'error', - '@typescript-eslint/consistent-type-definitions': ['error', 'type'], - '@typescript-eslint/no-floating-promises': 'off', - '@typescript-eslint/consistent-type-imports': [ - 'error', - { - prefer: 'type-imports', - fixStyle: 'separate-type-imports', - }, - ], - '@typescript-eslint/no-import-type-side-effects': 'error', - '@typescript-eslint/consistent-type-exports': [ - 'error', - { - fixMixedExportsWithInlineTypeSpecifier: false, - }, - ], - 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - 'es/no-nullish-coalescing-operators': 'off', - 'es/no-optional-chaining': 'off', - 'valid-jsdoc': 'off', - 'jsdoc/no-types': 'error', - 'rulesdir/no-default-props': 'error', - 'import/no-extraneous-dependencies': 'off', - 'rulesdir/prefer-underscore-method': 'off', - 'rulesdir/prefer-import-module-contents': 'off', - 'react/require-default-props': 'off', - 'react/prop-types': 'off', - 'no-restricted-syntax': [ - 'error', - { - selector: 'TSEnumDeclaration', - message: "Please don't declare enums, use union types instead.", - }, - ], - 'no-restricted-properties': [ - 'error', - { - object: 'Image', - property: 'getSize', - message: 'Usage of Image.getImage is restricted. Please use the `react-native-image-size`.', - }, - ], - 'no-restricted-imports': [ - 'error', - { - paths: restrictedImportPaths, - patterns: restrictedImportPatterns, - }, - ], - curly: 'error', - 'you-dont-need-lodash-underscore/throttle': 'off', + // React and React Native specific rules + 'react-native-a11y/has-accessibility-hint': ['off'], + 'react/require-default-props': 'off', + 'react/prop-types': 'off', + 'react/jsx-no-constructed-context-values': 'error', + 'react-native-a11y/has-valid-accessibility-descriptors': [ + 'error', + { + touchables: ['PressableWithoutFeedback', 'PressableWithFeedback'], }, - }, + ], + + // Disallow usage of certain functions and imports + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSEnumDeclaration', + message: "Please don't declare enums, use union types instead.", + }, + ], + 'no-restricted-properties': [ + 'error', + { + object: 'Image', + property: 'getSize', + message: 'Usage of Image.getImage is restricted. Please use the `react-native-image-size`.', + }, + ], + 'no-restricted-imports': [ + 'error', + { + paths: restrictedImportPaths, + patterns: restrictedImportPatterns, + }, + ], + + // Other rules + curly: 'error', + 'you-dont-need-lodash-underscore/throttle': 'off', + 'prefer-regex-literals': 'off', + 'valid-jsdoc': 'off', + 'jsdoc/no-types': 'error', + '@dword-design/import-alias/prefer-alias': [ + 'warn', + { + alias: { + '@assets': './assets', + '@components': './src/components', + '@hooks': './src/hooks', + // This is needed up here, if not @libs/actions would take the priority + '@userActions': './src/libs/actions', + '@libs': './src/libs', + '@navigation': './src/libs/Navigation', + '@pages': './src/pages', + '@styles': './src/styles', + // This path is provide alias for files like `ONYXKEYS` and `CONST`. + '@src': './src', + '@desktop': './desktop', + '@github': './.github', + }, + }, + ], + }, + + // Remove once no JS files are left + overrides: [ { - files: ['workflow_tests/**/*.{js,jsx,ts,tsx}', 'tests/**/*.{js,jsx,ts,tsx}', '.github/**/*.{js,jsx,ts,tsx}'], + files: ['*.js', '*.jsx'], rules: { - '@lwc/lwc/no-async-await': 'off', - 'no-await-in-loop': 'off', - 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/unbound-method': 'off', + 'jsdoc/no-types': 'off', + 'react/jsx-filename-extension': 'off', + 'rulesdir/no-default-props': 'off', }, }, { diff --git a/.github/.eslintrc.js b/.github/.eslintrc.js index e769944cd1a9..d6d39822b737 100644 --- a/.github/.eslintrc.js +++ b/.github/.eslintrc.js @@ -1,6 +1,10 @@ -// For all these Node.js scripts, we do not want to disable `console` statements module.exports = { rules: { + // For all these Node.js scripts, we do not want to disable `console` statements 'no-console': 'off', + + '@lwc/lwc/no-async-await': 'off', + 'no-await-in-loop': 'off', + 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], }, }; diff --git a/.github/actions/javascript/bumpVersion/bumpVersion.ts b/.github/actions/javascript/bumpVersion/bumpVersion.ts index ed4828367cf2..eba79c7c9edb 100644 --- a/.github/actions/javascript/bumpVersion/bumpVersion.ts +++ b/.github/actions/javascript/bumpVersion/bumpVersion.ts @@ -1,6 +1,7 @@ import * as core from '@actions/core'; import {exec as originalExec} from 'child_process'; import fs from 'fs'; +import type {PackageJson} from 'type-fest'; import {promisify} from 'util'; import {generateAndroidVersionCode, updateAndroidVersion, updateiOSVersion} from '@github/libs/nativeVersionUpdater'; import * as versionUpdater from '@github/libs/versionUpdater'; @@ -19,7 +20,7 @@ function updateNativeVersions(version: string) { .then(() => { console.log('Successfully updated Android!'); }) - .catch((err) => { + .catch((err: string | Error) => { console.error('Error updating Android'); core.setFailed(err); }); @@ -47,8 +48,12 @@ if (!semanticVersionLevel || !Object.keys(versionUpdater.SEMANTIC_VERSION_LEVELS console.log(`Invalid input for 'SEMVER_LEVEL': ${semanticVersionLevel}`, `Defaulting to: ${semanticVersionLevel}`); } -const {version: previousVersion} = JSON.parse(fs.readFileSync('./package.json').toString()); -const newVersion = versionUpdater.incrementVersion(previousVersion, semanticVersionLevel); +const {version: previousVersion}: PackageJson = JSON.parse(fs.readFileSync('./package.json').toString()); +if (!previousVersion) { + core.setFailed('Error: Could not read package.json'); +} + +const newVersion = versionUpdater.incrementVersion(previousVersion ?? '', semanticVersionLevel); console.log(`Previous version: ${previousVersion}`, `New version: ${newVersion}`); updateNativeVersions(newVersion); diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index d4a085fc9ddf..e1a5cf13a8d9 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -3478,7 +3478,10 @@ if (!semanticVersionLevel || !Object.keys(versionUpdater.SEMANTIC_VERSION_LEVELS console.log(`Invalid input for 'SEMVER_LEVEL': ${semanticVersionLevel}`, `Defaulting to: ${semanticVersionLevel}`); } const { version: previousVersion } = JSON.parse(fs_1.default.readFileSync('./package.json').toString()); -const newVersion = versionUpdater.incrementVersion(previousVersion, semanticVersionLevel); +if (!previousVersion) { + core.setFailed('Error: Could not read package.json'); +} +const newVersion = versionUpdater.incrementVersion(previousVersion ?? '', semanticVersionLevel); console.log(`Previous version: ${previousVersion}`, `New version: ${newVersion}`); updateNativeVersions(newVersion); console.log(`Setting npm version to ${newVersion}`); diff --git a/.github/actions/javascript/checkDeployBlockers/checkDeployBlockers.ts b/.github/actions/javascript/checkDeployBlockers/checkDeployBlockers.ts index bf94b136ce43..d51d68796070 100644 --- a/.github/actions/javascript/checkDeployBlockers/checkDeployBlockers.ts +++ b/.github/actions/javascript/checkDeployBlockers/checkDeployBlockers.ts @@ -60,7 +60,7 @@ const run = function (): Promise { core.setOutput('HAS_DEPLOY_BLOCKERS', false); } }) - .catch((error) => { + .catch((error: string | Error) => { console.error('A problem occurred while trying to communicate with the GitHub API', error); core.setFailed(error); }); diff --git a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts index 57a941105f90..c486fdbd39f3 100644 --- a/.github/actions/javascript/getGraphiteString/getGraphiteString.ts +++ b/.github/actions/javascript/getGraphiteString/getGraphiteString.ts @@ -28,7 +28,7 @@ const run = () => { // Extract timestamp, Graphite accepts timestamp in seconds if (current.metadata?.creationDate) { - timestamp = Math.floor(new Date(current.metadata.creationDate).getTime() / 1000); + timestamp = Math.floor(new Date(current.metadata.creationDate as string).getTime() / 1000); } if (current.name && current.meanDuration && current.meanCount && timestamp) { diff --git a/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts b/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts index dc1e99d1e3b8..262b603124fa 100644 --- a/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts +++ b/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts @@ -1,5 +1,6 @@ import * as core from '@actions/core'; import {readFileSync} from 'fs'; +import type {PackageJson} from 'type-fest'; import * as versionUpdater from '@github/libs/versionUpdater'; const semverLevel = core.getInput('SEMVER_LEVEL', {required: true}); @@ -7,6 +8,10 @@ if (!semverLevel || !Object.values(versionUpdater.SEMANTIC_VERSION_LEVEL core.setFailed(`'Error: Invalid input for 'SEMVER_LEVEL': ${semverLevel}`); } -const {version: currentVersion} = JSON.parse(readFileSync('./package.json', 'utf8')); -const previousVersion = versionUpdater.getPreviousVersion(currentVersion, semverLevel); +const {version: currentVersion}: PackageJson = JSON.parse(readFileSync('./package.json', 'utf8')); +if (!currentVersion) { + core.setFailed('Error: Could not read package.json'); +} + +const previousVersion = versionUpdater.getPreviousVersion(currentVersion ?? '', semverLevel); core.setOutput('PREVIOUS_VERSION', previousVersion); diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js index f372f0fdaf99..8eac2f62f03e 100644 --- a/.github/actions/javascript/getPreviousVersion/index.js +++ b/.github/actions/javascript/getPreviousVersion/index.js @@ -2728,7 +2728,10 @@ if (!semverLevel || !Object.values(versionUpdater.SEMANTIC_VERSION_LEVELS).inclu core.setFailed(`'Error: Invalid input for 'SEMVER_LEVEL': ${semverLevel}`); } const { version: currentVersion } = JSON.parse((0, fs_1.readFileSync)('./package.json', 'utf8')); -const previousVersion = versionUpdater.getPreviousVersion(currentVersion, semverLevel); +if (!currentVersion) { + core.setFailed('Error: Could not read package.json'); +} +const previousVersion = versionUpdater.getPreviousVersion(currentVersion ?? '', semverLevel); core.setOutput('PREVIOUS_VERSION', previousVersion); diff --git a/.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts b/.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts index aabc6b33086a..f57ef6c36a04 100644 --- a/.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts +++ b/.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts @@ -90,7 +90,7 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems: number) { getNumberOfItemsFromReviewerChecklist() .then(checkIssueForCompletedChecklist) - .catch((err) => { + .catch((err: string | Error) => { console.error(err); core.setFailed(err); }); diff --git a/.github/libs/sanitizeStringForJSONParse.ts b/.github/libs/sanitizeStringForJSONParse.ts index 3fbdaa8661f8..ddb7549b0186 100644 --- a/.github/libs/sanitizeStringForJSONParse.ts +++ b/.github/libs/sanitizeStringForJSONParse.ts @@ -16,7 +16,7 @@ const replacer = (str: string): string => * Solution partly taken from SO user Gabriel Rodríguez Flores 🙇 * https://stackoverflow.com/questions/52789718/how-to-remove-special-characters-before-json-parse-while-file-reading */ -const sanitizeStringForJSONParse = (inputString: string): string => { +const sanitizeStringForJSONParse = (inputString: string | number | boolean | null | undefined): string => { if (typeof inputString !== 'string') { throw new TypeError('Input must me of type String'); } diff --git a/.github/scripts/detectRedirectCycle.ts b/.github/scripts/detectRedirectCycle.ts index 5aa0d1daf342..6da0ecba158c 100644 --- a/.github/scripts/detectRedirectCycle.ts +++ b/.github/scripts/detectRedirectCycle.ts @@ -52,7 +52,7 @@ function detectCycle(): boolean { fs.createReadStream(`${process.cwd()}/docs/redirects.csv`) .pipe(parser) - .on('data', (row) => { + .on('data', (row: [string, string]) => { // Create a directed graph of sourceURL -> targetURL addEdge(row[0], row[1]); }) diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 10723d5efa04..7e7d55ac5d2e 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -15,6 +15,10 @@ on: type: string required: true +concurrency: + group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-e2e + cancel-in-progress: true + jobs: buildBaseline: runs-on: ubuntu-latest-xl diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 50e886942c98..da7757fcbfa8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,7 +8,7 @@ on: paths: ['**.js', '**.ts', '**.tsx', '**.json', '**.mjs', '**.cjs', 'config/.editorconfig', '.watchmanconfig', '.imgbotconfig'] concurrency: - group: "${{ github.ref }}-lint" + group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-lint cancel-in-progress: true jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c76425a40fbf..d6b346cb3995 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: paths: ['**.js', '**.ts', '**.tsx', '**.sh', 'package.json', 'package-lock.json'] concurrency: - group: "${{ github.ref }}-jest" + group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-jest cancel-in-progress: true jobs: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 88d4d24a5723..476b01f87b07 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -7,6 +7,10 @@ on: branches-ignore: [staging, production] paths: ['**.js', '**.ts', '**.tsx', 'package.json', 'package-lock.json', 'tsconfig.json'] +concurrency: + group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-typecheck + cancel-in-progress: true + jobs: typecheck: if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }} diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index ec8e17dda4cf..7527857eeda7 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -16,7 +16,7 @@ import './fonts.css'; Onyx.init({ keys: ONYXKEYS, initialKeyStates: { - [ONYXKEYS.NETWORK]: {isOffline: false}, + [ONYXKEYS.NETWORK]: {isOffline: false, isBackendReachable: true}, }, }); diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index 9a6680ba0b6e..0b7dda4621ad 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -3,9 +3,7 @@ import {useIsFocused as realUseIsFocused, useTheme as realUseTheme} from '@react // We only want these mocked for storybook, not jest const useIsFocused: typeof realUseIsFocused = process.env.NODE_ENV === 'test' ? realUseIsFocused : () => true; -// @ts-expect-error as we're mocking this function -const useTheme: typeof realUseTheme = process.env.NODE_ENV === 'test' ? realUseTheme : () => ({}); +const useTheme = process.env.NODE_ENV === 'test' ? realUseTheme : () => ({}); export * from '@react-navigation/core'; -export * from '@react-navigation/native'; export {useIsFocused, useTheme}; diff --git a/android/app/build.gradle b/android/app/build.gradle index ceba6269cc7e..aa1dbaa59ff3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -2,12 +2,21 @@ apply plugin: "com.android.application" apply plugin: "org.jetbrains.kotlin.android" apply plugin: "com.facebook.react" apply plugin: "com.google.firebase.firebase-perf" +apply plugin: "fullstory" apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" /** * This is the configuration block to customize your React Native Android app. * By default you don't need to apply any configuration, just uncomment the lines you need. */ + +/* Fullstory settings */ +fullstory { + org 'o-1WN56P-na1' + enabledVariants 'all' + logcatLevel 'debug' +} + react { /* Folders */ // The root of your project, i.e. where "package.json" lives. Default is '..' @@ -98,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001047304 - versionName "1.4.73-4" + versionCode 1001047603 + versionName "1.4.76-3" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" @@ -162,7 +171,7 @@ android { signingConfig null // buildTypes take precedence over productFlavors when it comes to the signing configuration, // thus we need to manually set the signing config, so that the e2e uses the debug config again. - // In other words, the signingConfig setting above will be ignored when we build the flavor in release mode. + // In other words, the signingConfig setting above will be ignored when we build the flavor in release mode. productFlavors.all { flavor -> // All release builds should be signed with the release config ... flavor.signingConfig signingConfigs.release diff --git a/android/build.gradle b/android/build.gradle index 10600480d8bb..52c998998ba0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -20,6 +20,7 @@ buildscript { repositories { google() mavenCentral() + maven {url "https://maven.fullstory.com"} } dependencies { classpath("com.android.tools.build:gradle") @@ -27,6 +28,9 @@ buildscript { classpath("com.google.gms:google-services:4.3.4") classpath("com.google.firebase:firebase-crashlytics-gradle:2.7.1") classpath("com.google.firebase:perf-plugin:1.4.1") + // Fullstory integration + classpath ("com.fullstory:gradle-plugin-local:1.47.0") + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") @@ -70,7 +74,7 @@ allprojects { // 'mapbox' is the fixed username for Mapbox's Maven repository. username = 'mapbox' - // The value for password is read from the 'MAPBOX_DOWNLOADS_TOKEN' gradle property. + // The value for password is read from the 'MAPBOX_DOWNLOADS_TOKEN' gradle property. // Run "npm run setup-mapbox-sdk" to set this property in «USER_HOME»/.gradle/gradle.properties // Example gradle.properties entry: diff --git a/babel.config.js b/babel.config.js index 9f8b7a711d78..3023d37df7e0 100644 --- a/babel.config.js +++ b/babel.config.js @@ -35,6 +35,17 @@ const metro = { ['@babel/plugin-proposal-private-property-in-object', {loose: true}], // The reanimated babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation 'react-native-reanimated/plugin', + + /* Fullstory */ + '@fullstory/react-native', + [ + '@fullstory/babel-plugin-annotate-react', + { + native: true, + setFSTagName: true, + }, + ], + // Import alias for native devices [ 'module-resolver', diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 7cafafca9973..9d397b9557a3 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -98,7 +98,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): {from: 'web/apple-touch-icon.png'}, {from: 'assets/images/expensify-app-icon.svg'}, {from: 'web/manifest.json'}, - {from: 'web/gtm.js'}, + {from: 'web/thirdPartyScripts.js'}, {from: 'assets/css', to: 'css'}, {from: 'assets/fonts/web', to: 'fonts'}, {from: 'assets/sounds', to: 'sounds'}, diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 13f7592b65e1..aec527edabe0 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -95,9 +95,10 @@ Additionally, if you want to discuss an idea with the open source community with #### Propose a solution for the job 4. You can propose solutions on any issue at any time, but if you propose solutions to jobs before the `Help Wanted` label is applied, you do so at your own risk. Proposals will not be reviewed until the label is added and there is always a chance that we might not add the label or hire an external contributor for the job. -5. After you reproduce the issue, complete the [proposal template here](./PROPOSAL_TEMPLATE.md) and post it as a comment in the corresponding GitHub issue (linked in the Upwork job). +5. Contributors should **not** submit proposals on issues when they have assigned issues or PRs that are awaiting an action from them. If so, they will be in violation of Rule #1 (Get Shit Done) in our [Code of Conduct](https://github.com/Expensify/App/blob/main/CODE_OF_CONDUCT.md) and will receive a warning. Multiple warnings can lead to removal from the program. +6. After you reproduce the issue, complete the [proposal template here](./PROPOSAL_TEMPLATE.md) and post it as a comment in the corresponding GitHub issue (linked in the Upwork job). - Note: Before submitting a proposal on an issue, be sure to read any other existing proposals. ALL NEW PROPOSALS MUST BE DIFFERENT FROM EXISTING PROPOSALS. The *difference* should be important, meaningful or considerable. -6. Refrain from leaving additional comments until someone from the Contributor-Plus team and / or someone from Expensify provides feedback on your proposal (do not create a pull request yet). +7. Refrain from leaving additional comments until someone from the Contributor-Plus team and / or someone from Expensify provides feedback on your proposal (do not create a pull request yet). - Do not leave more than one proposal. - Do not make extensive changes to your current proposal until after it has been reviewed. - If you want to make an entirely new proposal or update an existing proposal, please go back and edit your original proposal, then post a new comment to the issue in this format to alert everyone that it has been updated: @@ -105,8 +106,8 @@ Additionally, if you want to discuss an idea with the open source community with ## Proposal [Updated](link to proposal) ``` -7. If your proposal is accepted by the Expensify engineer assigned to the issue, Expensify will hire you on Upwork and assign the GitHub issue to you. -8. Once hired, post a comment in the Github issue stating when you expect to have your PR ready for review. +8. If your proposal is accepted by the Expensify engineer assigned to the issue, Expensify will hire you on Upwork and assign the GitHub issue to you. +9. Once hired, post a comment in the Github issue stating when you expect to have your PR ready for review. #### Begin coding your solution in a pull request 9. When you are ready to start, fork the repository and create a new branch. diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 22b1dea61bae..f3e928da8cb0 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -242,7 +242,7 @@ const foo: object = [1, 2, 3]; // TypeScript does not error If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. -> Even though `string` is specified as a key, `Record` type can still accepts objects whose keys are numbers. This is because numbers are converted to strings when used as an object index. Note that you cannot use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) for `Record`. +> Even though `string` is specified as a key, `Record` type can still accept objects whose keys are numbers. This is because numbers are converted to strings when used as an object index. Note that you cannot use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) for `Record`. ```ts function logObject(object: Record) { diff --git a/desktop/contextBridge.ts b/desktop/contextBridge.ts index 487e528a7485..61ede178da2d 100644 --- a/desktop/contextBridge.ts +++ b/desktop/contextBridge.ts @@ -67,7 +67,7 @@ contextBridge.exposeInMainWorld('electron', { } // Deliberately strip event as it includes `sender` - ipcRenderer.on(channel, (event, ...args) => func(...args)); + ipcRenderer.on(channel, (event, ...args: unknown[]) => func(...args)); }, /** Remove listeners for a single channel from the main process and sent to the renderer process. */ diff --git a/desktop/main.ts b/desktop/main.ts index b40557464ec1..64587f42bf56 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -581,7 +581,7 @@ const mainWindow = (): Promise => { app.hide(); } - ipcMain.on(ELECTRON_EVENTS.LOCALE_UPDATED, (event, updatedLocale) => { + ipcMain.on(ELECTRON_EVENTS.LOCALE_UPDATED, (event, updatedLocale: Locale) => { Menu.setApplicationMenu(Menu.buildFromTemplate(localizeMenuItems(initialMenuTemplate, updatedLocale))); disposeContextMenu(); disposeContextMenu = createContextMenu(updatedLocale); @@ -601,7 +601,7 @@ const mainWindow = (): Promise => { // Listen to badge updater event emitted by the render process // and update the app badge count (MacOS only) - ipcMain.on(ELECTRON_EVENTS.REQUEST_UPDATE_BADGE_COUNT, (event, totalCount) => { + ipcMain.on(ELECTRON_EVENTS.REQUEST_UPDATE_BADGE_COUNT, (event, totalCount?: number) => { if (totalCount === -1) { // The electron docs say you should be able to update this and pass no parameters to set the badge // to a single red dot, but in practice it resulted in an error "TypeError: Insufficient number of diff --git a/docs/articles/expensify-classic/expenses/Referral-Program.md b/docs/articles/expensify-classic/expenses/Referral-Program.md deleted file mode 100644 index 24605dd17d3f..000000000000 --- a/docs/articles/expensify-classic/expenses/Referral-Program.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Earn money with Expensify referrals -description: Get paid with the Expensify referral program! Share your link, earn $250 per successful sign-up, and enjoy unlimited income potential. It’s that easy. -redirect_from: articles/other/Referral-Program/ ---- - - -# Earn money with Expensify referrals - -Picture this: You've found Expensify and it's transformed your approach to expense management and financial organization. You love it so much that you can't help but recommend it to friends, family, and colleagues. Wouldn’t it be nice if you could get rewarded just for spreading the word? - -With Expensify referrals, you can. Every time someone you invite to the platform signs up for a paid annual plan on Expensify, you’ll earn $250. Think of it as a thank-you gift from us to you! - -## How to get paid for Expensify referrals - -Here are a few easy ways to get paid for Expensify friend referrals: - -- Submit an expense report to your boss (even just one receipt!) -- Send an invoice to a client or customer -- Share your referral link with a friend - - To find your referral link, open your Expensify mobile app and go to **Settings > Refer a friend, earn cash! > Share invite link**. - -**If the person you referred commits to an annual subscription with two or more active users and makes two monthly payments, you’ll get $250. Cha-ching!** - -## Who can you refer? - -You can refer anyone who might benefit from Expensify. Seriously. Anybody. - -Know a small business owner? Refer them! An [accountant](https://use.expensify.com/accountants-program)? Refer them! A best friend from childhood who keeps losing paper receipts? Refer them! - -Plus, you can [refer an unlimited amount of new users](https://use.expensify.com/blog/earn-50000-by-referring-your-friends-to-expensify/) with the Expensify referral program, so your earning potential is truly sky-high. - -## Common questions about Expensify benefits - -Still have questions about the Expensify referral program? We’ve got answers. Check out our FAQ below. - -### How will I know if I am the first person to refer someone to Expensify? - -You’ll know if you’re the first person to refer someone to Expensify if we reach out to let you know that they’ve successfully adopted Expensify and have paid for two months of an annual subscription. - -Simply put, we check for the earliest recorded referrer of a member on the workspace, and if that’s you, then we’ll let you know. - -### My referral wasn’t counted! How can I appeal? - -If you think your Expensify friend referral wasn’t counted, please send a message to concierge@expensify.com with the email of the person you referred. Our team will review the referral and get back to you. - -## Share the Expensify love — and get paid in the process - -Who needs a side hustle when you have Expensify? With Expensify benefits, it’s not just about managing your expenses — it's about expanding your income too. Share your Expensify referral link now or send over an invoice to unlock unlimited earning potential. diff --git a/docs/articles/expensify-classic/expensify-card/Change-Expensify-Card-limit.md b/docs/articles/expensify-classic/expensify-card/Change-Expensify-Card-limit.md new file mode 100644 index 000000000000..81ce761f84f4 --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/Change-Expensify-Card-limit.md @@ -0,0 +1,25 @@ +--- +title: Change Expensify Card limit +description: Increase or decrease the limit for an Expensify Card or for a group +--- +
+ +You can set Expensify Card limits for each group in your organization, or you can set the limit per card. + +# Set a limit per card + +1. Hover over Settings, then click **Domains**. +2. Click the name of the domain. +3. Next to the card, click **Edit Limit**. +4. Ensure the Custom Smart Limit toggle is enabled to be able to set a specific card limit. Otherwise, the card limit will be determined by the limit set for the group that the employee is in. +5. In the Limit Amount field, enter the desired limit. If set to $0, the card will be disabled for use until the limit is increased. +6. Click **Save**. + +# Set a limit per group + +1. Hover over Settings, then click **Domains**. +2. Click the name of the domain. +3. Click the **Groups** tab on the left. +4. Click the Expensify Card Smart Limit field for the card and enter the desired limit. + +
diff --git a/docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md b/docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md new file mode 100644 index 000000000000..d7fa33221834 --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/Deactivate-or-cancel-an-Expensify-Card.md @@ -0,0 +1,28 @@ +--- +title: Deactivate or cancel an Expensify Card +description: Close an Expensify Card +--- +
+ +A cardholder or a Domain Admin can cancel an Expensify Card. You may want to cancel a card: +- To cancel an old Expensify Card after upgrading to the new Expensify Visa® Commercial Card +- After a fraudulent or suspicious charge +- After an employee leaves the company + +# Domain Admins + +To cancel an employee's Expensify Card as a Domain Admin, + +1. Hover over Settings, then click **Domains**. +2. Click the name of the domain. +3. Next to the card, click **Terminate**. + +# Cardholders + +To cancel an Expensify Card assigned to you, + +1. Hover over Settings, then click **Account**. +2. Click the **Credit Card Import** tab. +3. Click **Cancel** next to the card. + +
diff --git a/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md index c5578249289a..239da6518be7 100644 --- a/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md +++ b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md @@ -26,13 +26,13 @@ After adopting the new Expensify Card, domain admins can issue virtual cards to **To assign a virtual card:** -1. Head to **Settings** > **Domains** > [**Company Cards**](https://www.expensify.com/domain_companycards). -2. Click the **Issue Virtual Cards** button. -3. Enter a card name (i.e., "Google Ads"). -4. Select a domain member to assign the card to. -5. Enter a card limit. -6. Select a **Limit Type** of _Fixed_ or _Monthly_. -7. Click **Issue Card**. +Head to **Settings** > **Domains** > [**Company Cards**](https://www.expensify.com/domain_companycards) and click the **Issue Virtual Cards** button. From there: + +1. Enter a card name (i.e., "Google Ads"). +2. Select a domain member to assign the card to. +3. Enter a card limit. +4. Select a **Limit Type** of _Fixed_ or _Monthly_. +5. Click **Issue Card**. ![The Issue Virtual Cards modal is open in the middle of the screen. There are four options to set; Card Name, Assignee, Card Limit, and Limit type. A cancel (left) and save (right) button are at the bottom right of the modal.]({{site.url}}/assets/images/AdminissuedVirtualCards.png){:width="100%"} diff --git a/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md b/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md index a1b1043dff47..424c8dc9d107 100644 --- a/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md +++ b/docs/articles/expensify-classic/expensify-partner-program/Referral-Program.md @@ -16,9 +16,9 @@ The sky's the limit for this referral program! Your referral can be anyone - a f 1. There are a bunch of different ways to refer someone to New Expensify: - Start a chat - - Request money - - Send money - - Split a bill + - Submit an expense to them + - Split an expense with them + - Pay someone (them) - Assign them a task - @ mention them - Invite them to a room diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md b/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md index 3ce0d07cb65d..ee116f65a398 100644 --- a/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md +++ b/docs/articles/expensify-classic/integrations/accounting-integrations/NetSuite.md @@ -276,9 +276,11 @@ To add a Custom Segment to your workspace, you’ll need to locate three fields **To find the Script/Field ID:** -If configuring Custom Segments as Report Fields, use the Field ID on the Transactions tab (under _Custom Segments > Transactions_). +Note that as of 2019.1, any new custom segments that you create automatically use the unified ID, and the Use as Field ID box is not visible. If you are editing a custom segment definition that was created before 2019.1, the Use as Field ID box is available. +To use a unified ID for the entire custom segment definition, check the Use as Field ID box. When the box is checked, no field ID fields or columns are shown on the Application & Sourcing subtabs because one ID is used for all fields. -If configuring Custom Segments as Tags, use the Field ID on the Transaction Columns tab (under _Custom Segments > Transaction Columns_). +- If configuring Custom Segments as Report Fields, use the Field ID on the Transactions tab (under _Custom Segments > Transactions_), or if no Field ID is shown, use the unified ID (just called "ID" right below the "Label"). +- If configuring Custom Segments as Tags, use the Field ID on the Transaction Columns tab (under _Custom Segments > Transaction Columns_), or if no Field ID is shown, use the unified ID (just called "ID" right below the "Label"). Lastly, head over to Expensify and do the following: 1. Navigate to **Settings > Workspace > Group > _[Workspace Name]_ > Connections > Configure > Coding tab** diff --git a/docs/articles/expensify-classic/workspaces/reports/Currency.md b/docs/articles/expensify-classic/workspaces/reports/Currency.md deleted file mode 100644 index 19fd4e8f3723..000000000000 --- a/docs/articles/expensify-classic/workspaces/reports/Currency.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Currency -description: How currency works in Expensify and how to change the default currency in your Expensify workspace ---- -# Overview - -In this article, we’ll outline how to change the default currency in your account and how currency works in Expensify. -Expensify supports expenses in almost every currency in the world. Group workspace admins and individual workspace users can specify the desired output currency for employee reports. Expensify handles the currency conversion process. - -# How to change your default currency - -The default currency for all expenses added to your account is set by the primary company workspace. Just head to **Settings > Workspaces > Group > *[Workspace Name]* > Reports > Report Basics** and select your desired Report Currency. - -If you are not using a group workspace, you can change your default currency under **Settings > Workspaces > Individual > *[Workspace Name]* > Reports** and then choose your desired Report Currency. Please note that the currency selected here will be overridden should you begin reporting on a group workspace. - -# How currency works in Expensify - -When totaling expenses across multiple currencies, we convert them to a single currency, which is the "report currency" of the report's expense workspace, or your personal output currency if no workspace is in use. - -**Important notes:** - -- Currency settings on a workspace are all-or-nothing. To reflect a different output currency in reports, create a new workspace for those employees and adjust the currency settings accordingly. -- Currency settings in the workspace take precedence over a user's individual account settings. - -# How the conversion rate is determined -When converting expenses between currencies, we rely on [Open Exchange Rates](https://openexchangerates.org/) to determine the average bid and ask rate on the expense date. This rate becomes available after the market closes for that day, resulting in varying conversion rates depending on when the expense occurred and how the currencies were trading. - -If the markets were closed on the expense date (e.g., weekends), we use the daily average rate from the last open market day prior to the purchase. For future-dated expenses, we use the most recent available data. Consequently, the report's value may fluctuate until the expense date, potentially leading to unexpected results. You cannot submit reports until the markets have closed for all the expense dates on the report. - -To bypass exchange rate calculations, manually enter expenses in your default currency. These entries will only be converted when included in a report with a different default currency. - diff --git a/docs/articles/new-expensify/connections/Coming-Soon.md b/docs/articles/new-expensify/connections/Coming-Soon.md deleted file mode 100644 index 4d32487a14b5..000000000000 --- a/docs/articles/new-expensify/connections/Coming-Soon.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Coming soon -description: Coming soon ---- - -# Coming soon \ No newline at end of file diff --git a/docs/articles/new-expensify/connections/Set-up-Xero-connection.md b/docs/articles/new-expensify/connections/Set-up-Xero-connection.md new file mode 100644 index 000000000000..73bff6ad5862 --- /dev/null +++ b/docs/articles/new-expensify/connections/Set-up-Xero-connection.md @@ -0,0 +1,102 @@ +--- +title: Set up Xero connection +description: Integrate Xero with Expensify +--- +
+ +{% include info.html %} +To use the Xero connection, you must have a Xero account and an Expensify Collect plan. +{% include end-info.html %} + +To set up your Xero connection, complete the 4 steps below. + +# Step 1: Connect Expensify to Xero + +
    +
  1. Click your profile image or icon in the bottom left menu.
  2. +
  3. Scroll down and click Workspaces in the left menu.
  4. +
  5. Select the workspace you want to connect to Xero.
  6. +
  7. Click More features in the left menu.
  8. +
  9. Scroll down to the Integrate section and enable the Accounting toggle.
  10. +
  11. Click Accounting in the left menu.
  12. +
  13. Click Set up to the right of Xero.
  14. +
  15. Enter your Xero login details to import your settings from Xero to Expensify.
  16. +
+ +# Step 2: Configure import settings + +The following steps help you determine how data will be imported from Xero to Expensify. + +
    +
  1. Under the Accounting settings for your workspace, click Import under the Xero connection.
  2. +
  3. Select an option for each of the following settings to determine what information will be imported from Xero into Expensify:
  4. +
      +
    • Xero organization: Select which Xero organization your Expensify workspace is connected to. Each organization can only be connected to one workspace at a time.
    • +
    • Chart of Accounts: Your Xero chart of accounts and any accounts marked as “Show In Expense Claims” will be automatically imported into Expensify as Categories. This cannot be amended.
    • +
    • Tracking Categories: Choose whether to import your Xero categories for cost centers and regions as tags in Expensify.
    • +
    • Re-bill Customers: When enabled, Xero customer contacts are imported into Expensify as tags for expense tracking. After exporting to Xero, tagged billable expenses can be included on a sales invoice to your customer.
    • +
    • Taxes: Choose whether to import tax rates and tax defaults from Xero.
    • +
    +
+ +# Step 3: Configure export settings +The following steps help you determine how data will be exported from Expensify to Xero. + +
    +
  1. Under the Accounting settings for your workspace, click Export under the Xero connection.
  2. +
  3. Review each of the following export settings:
  4. +
      +
    • Preferred Exporter: Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
    • +
    +
+{% include info.html %} +- Other Workspace Admins will still be able to export to Xero. +- If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin. +{% include end-info.html %} + +
    +
      +
    • Export Out-of-Pocket Expenses as: All out-of-pocket expenses will be exported as purchase bills. This cannot be amended.
    • +
    • Purchase Bill Date: Choose whether to use the date of last expense, export date, or submitted date.
    • +
    • Export invoices as: All invoices exported to Xero will be as a sales invoice. This cannot be amended.
    • +
    • Export company card expenses as: All company card expenses export to Xero as bank transactions. This cannot be amended.
    • +
    • Xero Bank Account: Select which bank account will be used to post bank transactions when non-reimbursable expenses are exported.
    • +
    +
+ +# Step 4: Configure advanced settings + +The following steps help you determine the advanced settings for your connection, like auto-sync. + +
    +
  1. Under the Accounting settings for your workspace, click Advanced under the Xero connection.
  2. +
  3. Select an option for each of the following settings:
  4. +
      +
    • Auto-sync: Choose whether to enable Xero to automatically communicate changes with Expensify to ensure that the data shared between the two systems is up-to-date. New report approvals/reimbursements will be synced during the next auto-sync period. Once you’ve added a business bank account for ACH reimbursement, any reimbursable expenses will be sent to Xero automatically when the report is reimbursed. For non-reimbursable reports, Expensify automatically queues the report to export to Xero after it has completed the approval workflow in Expensify.
    • +
    • Set Purchase Bill Status: Choose the status of your purchase bills:
    • +
        +
      • Draft
      • +
      • Awaiting Approval
      • +
      • Awaiting Payment
      • +
      +
    • Sync Reimbursed Reports: Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in Xero will also show in Expensify as Paid. If enabled, you must also select the Xero account that reimbursements are coming out of, and Expensify will automatically create the payment in Xero.
    • +
    • Xero Bill Payment Account: If you enable Sync Reimbursed Reports, you must select the Xero Bill Payment account your reimbursements will come from.
    • +
    • Xero Invoice Collections Account: If you are exporting invoices from Expensify, select the invoice collection account that you want invoices to appear under once they are marked as paid.
    • +
    +
+ +{% include faq-begin.md %} + +**How do I disconnect Xero from Expensify?** + +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace you want to disconnect from Xero. +4. Click **Accounting** in the left menu. +5. Click the three dot menu icon to the right of Xero and select **Disconnect**. +6. Click **Disconnect** to confirm. + +You will no longer see the imported options from Xero. +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md b/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md new file mode 100644 index 000000000000..0cf642c76e4c --- /dev/null +++ b/docs/articles/new-expensify/expenses/Approve-and-pay-expenses.md @@ -0,0 +1,72 @@ +--- +title: Approve and Pay Expenses +description: Approve, hold, or pay expenses submitted to you +--- +
+ +When expenses are sent to you for approval, you have the option to: +- Approve and pay the expenses. +- Hold the expenses if payment needs to be delayed or if the expenses require additional information before they can be approved. + +{% include info.html %} +If your workspace does not require expense approvals, or if the expense is sent to you by a friend, you will not need to approve the expense and instead can immediately pay the expense when you are ready. +{% include end-info.html %} + +# Approve expenses + +When someone sends an expense or a group of expenses to you for approval, you’ll receive the expense in Expensify Chat for the related workspace. Chats with new updates appear with a green dot to the right of the chat message. Concierge also sends you an email notification for the new expense. + +To approve an expense, + +1. Open the Expensify Chat thread for the expense. +2. Click the expense or group of expenses. +3. Review the expense details to ensure they are correct. Look at each receipt, the amount, the description, and any additional details. +4. Determine the next steps. + - **Approve**: When you’re satisfied with the expense, click **Approve**. + - **Handle holds**: If any of the expenses are on hold, you can choose to either approve only the expenses that are not on hold or approve the full amount, including any held expenses. + - **Request changes**: You can add a comment to the expense’s chat thread in your Expensify Chat inbox to request changes to the expense details. + +{% include info.html %} +Admins can modify an expense, if needed. +{% include end-info.html %} + +You’re now ready to pay the expense. + +# Hold an expense + +If you need to delay a payment or if you need more information on the expense before it can be approved, you can hold the expense. + +To hold an expense, + +1. Open the Expensify Chat thread for the expense. +2. Click the expense or group of expenses. +3. Click the three dot menu at the top right of the expense and select **Hold**. +4. Enter a reason for the delay. +5. Review the Hold Overview page and click **Got It**. + +When you’re ready, you can choose to: +- **Remove the hold**: Complete the steps above and select **Unhold**. +- **Approve the expense**: Complete the steps above for “Approve expenses.” +Once the expense has been approved, you can now pay the expense. + +{% include info.html %} +Held expenses will not be available for payment until they have been approved. +{% include end-info.html %} + +# Pay expenses + +Once you’ve approved an expense—or if the expense does not require approval—you’ll be able to pay it. + +{% include info.html %} +To pay expenses within Expensify, you’ll need to set up your Expensify Wallet. +{% include end-info.html %} + +To pay an expense, + +1. Open the Expensify Chat thread for the expense. +2. Click the expense or group of expenses. +3. Select a payment option. + - Click **Pay** to pay the full expense within Expensify. If the expenses contain one that has been held, the pay amount will only include the expenses that have not been held. Then you’ll select your payment method. + - Click **Pay Elsewhere** to indicate that a payment has been sent using a method outside of Expensify, such as cash or a check. This will label the expense as Paid. + +
diff --git a/docs/articles/new-expensify/expensify-card/Coming-Soon.md b/docs/articles/new-expensify/expensify-card/Coming-Soon.md deleted file mode 100644 index 6b85bb0364b5..000000000000 --- a/docs/articles/new-expensify/expensify-card/Coming-Soon.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- diff --git a/docs/articles/new-expensify/workspaces/Coming-Soon.md b/docs/articles/new-expensify/workspaces/Coming-Soon.md deleted file mode 100644 index 266784414761..000000000000 --- a/docs/articles/new-expensify/workspaces/Coming-Soon.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Coming Soon! -description: More info coming soon! ---- - - diff --git a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md index 294dcfc57a23..8f2cf0897ad0 100644 --- a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md +++ b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md @@ -14,7 +14,7 @@ To require workspace members to add tags and/or categories to their expenses, 3. Select a workspace. 4. Click **Tags** or **Categories** in the left menu. 5. Click **Settings** at the top right of the page. -6. Enable the “Members must tag/categorize all spend” toggle. +6. Enable the “Members must tag/categorize all expenses" toggle. 7. If desired, repeat steps 4-6 for tags or categories (whichever you haven’t done yet). {% include end-option.html %} @@ -24,7 +24,7 @@ To require workspace members to add tags and/or categories to their expenses, 3. Select a workspace. 4. Tap **Tags** or **Categories**. 5. Tap **Settings** at the top right of the page. -6. Enable the “Members must tag/categorize all spend” toggle. +6. Enable the “Members must tag/categorize all expenses" toggle. 7. If desired, repeat steps 4-6 for tags or categories (whichever you haven’t done yet). {% include end-option.html %} diff --git a/docs/redirects.csv b/docs/redirects.csv index c79c07ee3cf4..4867ee4f8f4b 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -172,3 +172,7 @@ https://help.expensify.com/articles/new-expensify/getting-started/Free-plan-upgr https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account,https://help.expensify.com/new-expensify/hubs/expenses/Connect-a-Bank-Account https://help.expensify.com/articles/new-expensify/settings/Profile,https://help.expensify.com/new-expensify/hubs/settings/ https://help.expensify.com/articles/new-expensify/expenses/Referral-Program.html,https://help.expensify.com/articles/expensify-classic/expensify-partner-program/Referral-Program +https://help.expensify.com/articles/new-expensify/workspaces/The-Free-Plan,https://help.expensify.com/new-expensify/hubs/workspaces/ +https://help.expensify.com/new-expensify/hubs/expenses/Connect-a-Bank-Account,https://help.expensify.com/articles/new-expensify/expenses/Connect-a-Business-Bank-Account +https://help.expensify.com/articles/new-expensify/settings/Security,https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security +https://help.expensify.com/articles/expensify-classic/workspaces/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency \ No newline at end of file diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 94118eb3bfa4..9e4fcaef55dd 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -648,6 +648,7 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify/Pods-NewExpensify-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/FullStory/FullStory.framework/FullStory", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", @@ -659,6 +660,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FullStory.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", @@ -680,6 +682,7 @@ "${PODS_ROOT}/Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests-frameworks.sh", "${BUILT_PRODUCTS_DIR}/MapboxMaps/MapboxMaps.framework", "${BUILT_PRODUCTS_DIR}/Turf/Turf.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/FullStory/FullStory.framework/FullStory", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCommon/MapboxCommon.framework/MapboxCommon", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxMobileEvents/MapboxMobileEvents.framework/MapboxMobileEvents", @@ -691,6 +694,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FullStory.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxMobileEvents.framework", diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index d792d4d9a7bd..b19fd95ade26 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.73 + 1.4.76 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.73.4 + 1.4.76.3 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 2e78620d456d..d169f0f85629 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.73 + 1.4.76 CFBundleSignature ???? CFBundleVersion - 1.4.73.4 + 1.4.76.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 73f8aced2c26..87d0273d1334 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.73 + 1.4.76 CFBundleVersion - 1.4.73.4 + 1.4.76.3 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile b/ios/Podfile index 4f00eb2adfdd..d72086d4c07b 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -117,3 +117,5 @@ end target 'NotificationServiceExtension' do pod 'AirshipServiceExtension' end + +pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz' \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d0155051fc3b..1ea6b65a58b7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -138,6 +138,27 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - fmt (6.2.1) + - FullStory (1.48.0) + - fullstory_react-native (1.4.2): + - FullStory (~> 1.14) + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - glog (0.3.5) - GoogleAppMeasurement (8.8.0): - GoogleAppMeasurement/AdIdSupport (= 8.8.0) @@ -1731,7 +1752,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNCPicker (2.6.1): + - RNCPicker (2.7.6): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1831,7 +1852,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.69): + - RNLiveMarkdown (0.1.70): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1849,9 +1870,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.69) + - RNLiveMarkdown/common (= 0.1.70) - Yoga - - RNLiveMarkdown/common (0.1.69): + - RNLiveMarkdown/common (0.1.70): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2077,6 +2098,8 @@ DEPENDENCIES: - ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz\"}`)" + - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)" - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libevent (~> 2.1.12) @@ -2239,6 +2262,10 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-modules-core" FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" + FullStory: + :http: https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz + fullstory_react-native: + :path: "../node_modules/@fullstory/react-native" glog: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: @@ -2429,6 +2456,10 @@ EXTERNAL SOURCES: Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" +CHECKOUT OPTIONS: + FullStory: + :http: https://ios-releases.fullstory.com/fullstory-1.48.0-xcframework.tar.gz + SPEC CHECKSUMS: Airship: 5a6d3f8a982398940b0d48423bb9b8736717c123 AirshipFrameworkProxy: 7255f4ed9836dc2920f2f1ea5657ced4cee8a35c @@ -2454,6 +2485,8 @@ SPEC CHECKSUMS: FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 + FullStory: 097347c823c21c655ca25fd8d5e6355a9326ec54 + fullstory_react-native: 6cba8a2c054374a24a44dc4310407d9435459cae glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a @@ -2545,7 +2578,7 @@ SPEC CHECKSUMS: ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522 RNAppleAuthentication: 0571c08da8c327ae2afc0261b48b4a515b0286a6 RNCClipboard: 081418ae3b391b1012c3f41d045e5e39f1beed71 - RNCPicker: a37026a67de0cf1a33ffe8722783527e3b18ea9f + RNCPicker: 106d11a1c159ce937009b2bd52db2bdb1577454f RNDeviceInfo: 449272e9faf2afe94a3fe2896d169e92277fffa8 RNDevMenu: 72807568fe4188bd4c40ce32675d82434b43c45d RNFBAnalytics: f76bfa164ac235b00505deb9fc1776634056898c @@ -2556,7 +2589,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: bfabd5938e5af5afc1e60e4e34286b17f8308184 + RNLiveMarkdown: 23250f3d64c9d5f82ff36c4733c03544af0222d2 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 @@ -2573,8 +2606,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 - Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 + Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 -PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d +PODFILE CHECKSUM: 66a5c97ae1059e4da1993a4ad95abe5d819f555b COCOAPODS: 1.13.0 diff --git a/jest/setup.ts b/jest/setup.ts index 488e3e36a1d3..174e59a7e493 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -2,9 +2,11 @@ import '@shopify/flash-list/jestSetup'; import 'react-native-gesture-handler/jestSetup'; import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; import 'setimmediate'; +import mockFSLibrary from './setupMockFullstoryLib'; import setupMockImages from './setupMockImages'; setupMockImages(); +mockFSLibrary(); // This mock is required as per setup instructions for react-navigation testing // https://reactnavigation.org/docs/testing/#mocking-native-modules diff --git a/jest/setupMockFullstoryLib.ts b/jest/setupMockFullstoryLib.ts new file mode 100644 index 000000000000..9edfccab9441 --- /dev/null +++ b/jest/setupMockFullstoryLib.ts @@ -0,0 +1,24 @@ +type FSPageInterface = { + start: jest.Mock; +}; + +export default function mockFSLibrary() { + jest.mock('@fullstory/react-native', () => { + class Fullstory { + consent = jest.fn(); + + anonymize = jest.fn(); + + identify = jest.fn(); + } + + return { + FSPage(): FSPageInterface { + return { + start: jest.fn(), + }; + }, + default: Fullstory, + }; + }); +} diff --git a/package-lock.json b/package-lock.json index 3cf4cd0737d2..71b6a72c0f92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,28 @@ { "name": "new.expensify", - "version": "1.4.73-4", + "version": "1.4.76-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.73-4", + "version": "1.4.76-3", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.70", + "@expensify/react-native-live-markdown": "0.1.76", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", "@formatjs/intl-pluralrules": "^5.2.2", + "@fullstory/babel-plugin-react-native": "^1.2.1", + "@fullstory/browser": "^2.0.3", + "@fullstory/react-native": "^1.4.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", @@ -34,7 +37,7 @@ "@react-native-firebase/crashlytics": "^12.3.0", "@react-native-firebase/perf": "^12.3.0", "@react-native-google-signin/google-signin": "^10.0.1", - "@react-native-picker/picker": "2.6.1", + "@react-native-picker/picker": "2.7.6", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "6.1.12", "@react-navigation/stack": "6.3.29", @@ -56,7 +59,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -100,7 +103,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.32", + "react-native-onyx": "2.0.41", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -109,7 +112,7 @@ "react-native-plaid-link-sdk": "11.5.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", - "react-native-reanimated": "^3.7.2", + "react-native-reanimated": "^3.8.0", "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", @@ -125,7 +128,7 @@ "react-native-web-linear-gradient": "^1.1.2", "react-native-web-sound": "^0.1.3", "react-native-webview": "13.6.4", - "react-pdf": "^7.7.0", + "react-pdf": "^7.7.3", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", @@ -238,7 +241,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "type-fest": "^4.10.2", - "typescript": "^5.3.2", + "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", @@ -3569,9 +3572,14 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.70", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.70.tgz", - "integrity": "sha512-HyqBtZyvuJFB4gIUECKIMxWCnTPlPj+GPWmw80VzMBRFV9QiFRKUKRWefNEJ1cXV5hl8a6oOWDQla+dCnjCzOQ==", + "version": "0.1.76", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.76.tgz", + "integrity": "sha512-JUXiLg0Y2FJiVOfZKRgoOP1no8ThPaJ6MBc122UsW6SG53OvS12MTHfgfKHjXRH1nIGro/p9ekYb8GAzpp+kdw==", + "workspaces": [ + "parser", + "example", + "WebExample" + ], "engines": { "node": ">= 18.0.0" }, @@ -5589,6 +5597,52 @@ "tslib": "^2.4.0" } }, + "node_modules/@fullstory/babel-plugin-annotate-react": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@fullstory/babel-plugin-annotate-react/-/babel-plugin-annotate-react-2.3.0.tgz", + "integrity": "sha512-gYLUL6Tu0exbvTIhK9nSCaztmqBlQAm07Fvtl/nKTc+lxwFkcX9vR8RrdTbyjJZKbPaA5EMlExQ6GeLCXkfm5g==" + }, + "node_modules/@fullstory/babel-plugin-react-native": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fullstory/babel-plugin-react-native/-/babel-plugin-react-native-1.2.1.tgz", + "integrity": "sha512-EMAgoPOo+31eppHxQf05oAGhKKTem7rw8GHDdbNJF0c5dQWzBGNVgF72TPjcxES91UI6hbss2eqoVOhUttLEoQ==", + "dependencies": { + "@babel/parser": "^7.0.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@fullstory/browser": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-2.0.3.tgz", + "integrity": "sha512-usjH8FB1O2LiSWoblsuKhFhlYDGpIPuyQVOx4JXtxm9QmQARdKZdNq1vPijxuDvOGjhwtVZa4JmhvByRRuDPnQ==", + "dependencies": { + "@fullstory/snippet": "2.0.3" + } + }, + "node_modules/@fullstory/react-native": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@fullstory/react-native/-/react-native-1.4.2.tgz", + "integrity": "sha512-Ig85ghn5UN+Tc1JWL/y4hY9vleeaVHL3f6qH9W4odDNP4XAv29+G82nIYQhBOQGoVnIQ4oQFQftir/dqAbidSw==", + "dependencies": { + "@fullstory/babel-plugin-annotate-react": "^2.2.0", + "@fullstory/babel-plugin-react-native": "^1.1.0" + }, + "peerDependencies": { + "expo": ">=47.0.0", + "react": "*", + "react-native": ">=0.61.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@fullstory/snippet": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@fullstory/snippet/-/snippet-2.0.3.tgz", + "integrity": "sha512-EaCuTQSLv5FvnjHLbTxErn3sS1+nLqf1p6sA/c4PV49stBtkUakA0eLhJJdaw0WLdXyEzZXf86lRNsjEzrgGPw==" + }, "node_modules/@gar/promisify": { "version": "1.1.3", "license": "MIT" @@ -8854,12 +8908,12 @@ } }, "node_modules/@react-native-picker/picker": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.6.1.tgz", - "integrity": "sha512-oJftvmLOj6Y6/bF4kPcK6L83yNBALGmqNYugf94BzP0FQGpHBwimVN2ygqkQ2Sn2ZU3pGUZMs0jV6+Gku2GyYg==", + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.7.6.tgz", + "integrity": "sha512-Cs3PxRmE2vu6TofM9vt9TV8ZYFOtEPSupNxwoorH9lpkKM9HGG8QwK2i29KOEoODpUbtudKHUTtqhMZSuX9pgA==", "peerDependencies": { - "react": ">=16", - "react-native": ">=0.57" + "react": "*", + "react-native": "*" } }, "node_modules/@react-native/assets-registry": { @@ -20301,8 +20355,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", - "integrity": "sha512-9BHjM3kZs7/dil0oykEQFkEhXjVD5liTttmO7ZYtPZkl4j6g97mubY2p9lYpWwpkWckUfvU7nGuZQjahw9xSFA==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", + "integrity": "sha512-uy1+axUTTuPKwAR06xNG/tGIJ+uaavmSQgKiNU7pQVR94ibNzDD2WESn2E7OEP9/QrHa61lfFlluTjFvvz5I8Q==", "license": "MIT", "dependencies": { "classnames": "2.5.0", @@ -31431,9 +31485,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.32", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.32.tgz", - "integrity": "sha512-tB9wqMJGTLOYfrfplRP+9aq5JdD8w/hV/OZsMAVH+ewbE1zLY8OymUsAsIFdF1v+cB8HhehP569JVLZmhm6bsg==", + "version": "2.0.41", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.41.tgz", + "integrity": "sha512-33r0sVBq7MV/GZwRneRt81uxgW8x3YG75VNJvThycB/dkCnGCfbxoVkZADVH3ET3jzfFXy9wnS06sZnZp78zMQ==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -31554,10 +31608,15 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.7.2", - "license": "MIT", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.8.1.tgz", + "integrity": "sha512-EdM0vr3JEaNtqvstqESaPfOBy0gjYBkr1iEolWJ82Ax7io8y9OVUIphgsLKTB36CtR1XtmBw0RZVj7KArc7ZVA==", "dependencies": { - "@babel/plugin-transform-object-assign": "^7.16.7", + "@babel/plugin-transform-arrow-functions": "^7.0.0-0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", + "@babel/plugin-transform-optional-chaining": "^7.0.0-0", + "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", + "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4" @@ -31959,9 +32018,9 @@ } }, "node_modules/react-pdf": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.1.tgz", - "integrity": "sha512-cbbf/PuRtGcPPw+HLhMI1f6NSka8OJgg+j/yPWTe95Owf0fK6gmVY7OXpTxMeh92O3T3K3EzfE0ML0eXPGwR5g==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.7.3.tgz", + "integrity": "sha512-a2VfDl8hiGjugpqezBTUzJHYLNB7IS7a2t7GD52xMI9xHg8LdVaTMsnM9ZlNmKadnStT/tvX5IfV0yLn+JvYmw==", "dependencies": { "clsx": "^2.0.0", "dequal": "^2.0.3", @@ -36029,9 +36088,10 @@ } }, "node_modules/typescript": { - "version": "5.3.3", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 0a1c37747ae9..0666111e25c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.73-4", + "version": "1.4.76-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -65,13 +65,16 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.70", + "@expensify/react-native-live-markdown": "0.1.76", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", "@formatjs/intl-locale": "^3.3.0", "@formatjs/intl-numberformat": "^8.5.0", "@formatjs/intl-pluralrules": "^5.2.2", + "@fullstory/babel-plugin-react-native": "^1.2.1", + "@fullstory/browser": "^2.0.3", + "@fullstory/react-native": "^1.4.2", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.2.2", "@kie/act-js": "^2.6.0", @@ -86,7 +89,7 @@ "@react-native-firebase/crashlytics": "^12.3.0", "@react-native-firebase/perf": "^12.3.0", "@react-native-google-signin/google-signin": "^10.0.1", - "@react-native-picker/picker": "2.6.1", + "@react-native-picker/picker": "2.7.6", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "6.1.12", "@react-navigation/stack": "6.3.29", @@ -108,7 +111,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#1713f28214f0e7176c4fd13433fb0ea15491ebf9", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -152,7 +155,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.32", + "react-native-onyx": "2.0.41", "react-native-pager-view": "6.2.3", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -161,7 +164,7 @@ "react-native-plaid-link-sdk": "11.5.0", "react-native-qrcode-svg": "^6.2.0", "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", - "react-native-reanimated": "^3.7.2", + "react-native-reanimated": "^3.8.0", "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.8.2", @@ -177,7 +180,7 @@ "react-native-web-linear-gradient": "^1.1.2", "react-native-web-sound": "^0.1.3", "react-native-webview": "13.6.4", - "react-pdf": "^7.7.0", + "react-pdf": "^7.7.3", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", @@ -290,7 +293,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "type-fest": "^4.10.2", - "typescript": "^5.3.2", + "typescript": "^5.4.5", "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", diff --git a/src/CONST.ts b/src/CONST.ts index bef41baf159e..d2be946cdb2b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -73,14 +73,18 @@ const onboardingChoices = { type OnboardingPurposeType = ValueOf; const CONST = { - MERGED_ACCOUNT_PREFIX: 'MERGED_', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], ANDROID_PACKAGE_NAME, - ANIMATED_HIGHLIGHT_DELAY: 500, - ANIMATED_HIGHLIGHT_DURATION: 500, + WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100, + ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50, + ANIMATED_HIGHLIGHT_ENTRY_DURATION: 300, + ANIMATED_HIGHLIGHT_START_DELAY: 10, + ANIMATED_HIGHLIGHT_START_DURATION: 300, + ANIMATED_HIGHLIGHT_END_DELAY: 800, + ANIMATED_HIGHLIGHT_END_DURATION: 2000, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, ANIMATION_IN_TIMING: 100, @@ -91,6 +95,7 @@ const CONST = { // Multiplier for gyroscope animation in order to make it a bit more subtle ANIMATION_GYROSCOPE_VALUE: 0.4, BACKGROUND_IMAGE_TRANSITION_DURATION: 1000, + SCREEN_TRANSITION_END_TIMEOUT: 1000, ARROW_HIDE_DELAY: 3000, API_ATTACHMENT_VALIDATIONS: { @@ -166,6 +171,9 @@ const CONST = { PULL_REQUEST_NUMBER, + // Regex to get link in href prop inside of component + REGEX_LINK_IN_ANCHOR: /]*?\s+)?href="([^"]*)"/gi, + MERCHANT_NAME_MAX_LENGTH: 255, REQUEST_PREVIEW: { @@ -555,8 +563,10 @@ const CONST = { EMPTY_ARRAY, EMPTY_OBJECT, USE_EXPENSIFY_URL, + STATUS_EXPENSIFY_URL: 'https://status.expensify.com', GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', GOOGLE_DOC_IMAGE_LINK_MATCH: 'googleusercontent.com', + GOOGLE_CLOUD_URL: 'https://clients3.google.com/generate_204', IMAGE_BASE64_MATCH: 'base64', DEEPLINK_BASE_URL: 'new-expensify://', PDF_VIEWER_URL: '/pdf/web/viewer.html', @@ -1047,6 +1057,7 @@ const CONST = { MAX_RETRY_WAIT_TIME_MS: 10 * 1000, PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, + BACKEND_CHECK_INTERVAL_MS: 60 * 1000, MAX_REQUEST_RETRIES: 10, NETWORK_STATUS: { ONLINE: 'online', @@ -1058,7 +1069,7 @@ const CONST = { DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, DEFAULT_CLOSE_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, - DEFAULT_NETWORK_DATA: {isOffline: false}, + DEFAULT_NETWORK_DATA: {isOffline: false, isBackendReachable: true}, FORMS: { LOGIN_FORM: 'LoginForm', VALIDATE_CODE_FORM: 'ValidateCodeForm', @@ -1267,6 +1278,8 @@ const CONST = { EXPENSIFY_EMAIL_DOMAIN: '@expensify.com', }, + CONCIERGE_DISPLAY_NAME: 'Concierge', + INTEGRATION_ENTITY_MAP_TYPES: { DEFAULT: 'DEFAULT', NONE: 'NONE', @@ -1922,7 +1935,7 @@ const CONST = { EMOJI_NAME: /:[\w+-]+:/g, EMOJI_SUGGESTIONS: /:[a-zA-Z0-9_+-]{1,40}$/, AFTER_FIRST_LINE_BREAK: /\n.*/g, - LINE_BREAK: /\r|\n/g, + LINE_BREAK: /\r\n|\r|\n/g, CODE_2FA: /^\d{6}$/, ATTACHMENT_ID: /chat-attachments\/(\d+)/, HAS_COLON_ONLY_AT_THE_BEGINNING: /^:[^:]+$/, @@ -3488,7 +3501,6 @@ const CONST = { NAVIGATION: { TYPE: { - FORCED_UP: 'FORCED_UP', UP: 'UP', }, ACTION_TYPE: { @@ -3565,7 +3577,7 @@ const CONST = { TRACK_EXPENSE: 'track-expenses', }, 'track-expenses': { - VIDEO_URL: `${CLOUDFRONT_URL}/videos/guided-setup-track-business.mp4`, + VIDEO_URL: `${CLOUDFRONT_URL}/videos/guided-setup-track-business-v2.mp4`, LEARN_MORE_LINK: `${USE_EXPENSIFY_URL}/track-expenses`, }, }, @@ -3581,10 +3593,10 @@ const CONST = { BACK_BUTTON_NATIVE_ID: 'backButton', /** - * The maximum count of items per page for OptionsSelector. + * The maximum count of items per page for SelectionList. * When paginate, it multiplies by page number. */ - MAX_OPTIONS_SELECTOR_PAGE_LENGTH: 500, + MAX_SELECTION_LIST_PAGE_LENGTH: 500, /** * Bank account names @@ -3670,6 +3682,7 @@ const CONST = { TAX_OUT_OF_POLICY: 'taxOutOfPolicy', TAX_RATE_CHANGED: 'taxRateChanged', TAX_REQUIRED: 'taxRequired', + HOLD: 'hold', }, /** Context menu types */ @@ -3732,6 +3745,7 @@ const CONST = { WELCOME_VIDEO_URL: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + ONBOARDING_INTRODUCTION: 'Let’s get you set up 🔧', ONBOARDING_CHOICES: {...onboardingChoices}, ONBOARDING_CONCIERGE: { @@ -3778,7 +3792,7 @@ const CONST = { [onboardingChoices.EMPLOYER]: { message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-get-paid-back.mp4`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-get-paid-back-v2.mp4`, thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-get-paid-back.jpg`, duration: 55, width: 1280, @@ -3821,7 +3835,7 @@ const CONST = { [onboardingChoices.MANAGE_TEAM]: { message: 'Here are some important tasks to help get your team’s expenses under control.', video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-manage-team.mp4`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-manage-team-v2.mp4`, thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-manage-team.jpg`, duration: 55, width: 1280, @@ -3907,7 +3921,7 @@ const CONST = { [onboardingChoices.PERSONAL_SPEND]: { message: 'Here’s how to track your spend in a few clicks.', video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-track-personal.mp4`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-track-personal-v2.mp4`, thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-track-personal.jpg`, duration: 55, width: 1280, @@ -3935,7 +3949,7 @@ const CONST = { [onboardingChoices.CHAT_SPLIT]: { message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', video: { - url: `${CLOUDFRONT_URL}/videos/guided-setup-chat-split-bills.mp4`, + url: `${CLOUDFRONT_URL}/videos/guided-setup-chat-split-bills-v2.mp4`, thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-chat-split-bills.jpg`, duration: 55, width: 1280, @@ -4764,6 +4778,8 @@ const CONST = { DISTANCE: 'distance', }, + SEARCH_RESULTS_PAGE_SIZE: 50, + SEARCH_DATA_TYPES: { TRANSACTION: 'transaction', }, diff --git a/src/Expensify.tsx b/src/Expensify.tsx index c67f1400fc4b..6205afb9c03c 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -76,7 +76,9 @@ type ExpensifyOnyxProps = { type ExpensifyProps = ExpensifyOnyxProps; -const SplashScreenHiddenContext = React.createContext({}); +type SplashScreenHiddenContextType = {isSplashHidden?: boolean}; + +const SplashScreenHiddenContext = React.createContext({}); function Expensify({ isCheckingPublicRoom = true, @@ -140,8 +142,10 @@ function Expensify({ // Initialize this client as being an active client ActiveClientManager.init(); - // Used for the offline indicator appearing when someone is offline - NetworkConnection.subscribeToNetInfo(); + // Used for the offline indicator appearing when someone is offline or backend is unreachable + const unsubscribeNetworkStatus = NetworkConnection.subscribeToNetworkStatus(); + + return () => unsubscribeNetworkStatus(); }, []); useEffect(() => { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 804c8dadd553..e669d4740f98 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -188,6 +188,9 @@ const ONYXKEYS = { /** User's Expensify Wallet */ USER_WALLET: 'userWallet', + /** User's metadata that will be used to segmentation */ + USER_METADATA: 'userMetadata', + /** Object containing Onfido SDK Token + applicantID */ WALLET_ONFIDO: 'walletOnfido', @@ -282,6 +285,9 @@ const ONYXKEYS = { /** Onboarding Purpose selected by the user during Onboarding flow */ ONBOARDING_PURPOSE_SELECTED: 'onboardingPurposeSelected', + /** Onboarding policyID selected by the user during Onboarding flow */ + ONBOARDING_POLICY_ID: 'onboardingPolicyID', + /** Onboarding Purpose selected by the user during Onboarding flow */ ONBOARDING_ADMINS_CHAT_REPORT_ID: 'onboardingAdminsChatReportID', @@ -336,6 +342,7 @@ const ONYXKEYS = { WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', REPORT: 'report_', + REPORT_NAME_VALUE_PAIRS: 'reportNameValuePairs_', REPORT_DRAFT: 'reportDraft_', // REPORT_METADATA is a perf optimization used to hold loading states (isLoadingInitialReportActions, isLoadingOlderReportActions, isLoadingNewerReportActions). // A lot of components are connected to the Report entity and do not care about the actions. Setting the loading state @@ -548,6 +555,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; + [ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS]: OnyxTypes.ReportNameValuePairs; [ONYXKEYS.COLLECTION.REPORT_DRAFT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; @@ -605,6 +613,7 @@ type OnyxValuesMapping = { [ONYXKEYS.USER_LOCATION]: OnyxTypes.UserLocation; [ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList; [ONYXKEYS.SESSION]: OnyxTypes.Session; + [ONYXKEYS.USER_METADATA]: OnyxTypes.UserMetadata; [ONYXKEYS.STASHED_SESSION]: OnyxTypes.Session; [ONYXKEYS.BETAS]: OnyxTypes.Beta[]; [ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf; @@ -662,6 +671,7 @@ type OnyxValuesMapping = { [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; [ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string; + [ONYXKEYS.ONBOARDING_POLICY_ID]: string; [ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string; [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean; [ONYXKEYS.LAST_VISITED_PATH]: string | undefined; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0dff2992fe91..2bc04c4a99ea 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -515,10 +515,6 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/invoice-account-select', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/invoice-account-select` as const, }, - POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT: { - route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/company-card-expense-account/account-payable-select', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/company-card-expense-account/account-payable-select` as const, - }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_PREFERRED_EXPORTER: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export/preferred-exporter', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/export/preferred-exporter` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 45a7bf1f255d..f74002312623 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -27,7 +27,6 @@ const SCREENS = { MY_TRIPS: 'Travel_MyTrips', TCS: 'Travel_TCS', }, - WORKSPACES_CENTRAL_PANE: 'WorkspacesCentralPane', SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', REPORT_RHP: 'Search_Report_RHP', @@ -112,9 +111,6 @@ const SCREENS = { CHAT_FINDER: 'ChatFinder', WORKSPACE_SWITCHER: 'WorkspaceSwitcher', }, - WORKSPACE_SWITCHER: { - ROOT: 'WorkspaceSwitcher_Root', - }, RIGHT_MODAL: { SETTINGS: 'Settings', NEW_CHAT: 'NewChat', @@ -233,7 +229,6 @@ const SCREENS = { QUICKBOOKS_ONLINE_EXPORT_INVOICE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Invoice_Account_Select', QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense', QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Account_Select', - QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Account_Payable_Select', QUICKBOOKS_ONLINE_NON_REIMBURSABLE_DEFAULT_VENDOR_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Non_Reimbursable_Default_Vendor_Select', QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT: 'Workspace_Accounting_Quickbooks_Online_Export_Company_Card_Expense_Select', QUICKBOOKS_ONLINE_EXPORT_PREFERRED_EXPORTER: 'Workspace_Accounting_Quickbooks_Online_Export_Preferred_Exporter', @@ -364,7 +359,6 @@ const SCREENS = { }, ROOM_MEMBERS_ROOT: 'RoomMembers_Root', ROOM_INVITE_ROOT: 'RoomInvite_Root', - CHAT_FINDER_ROOT: 'ChatFinder_Root', FLAG_COMMENT_ROOT: 'FlagComment_Root', REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount', GET_ASSISTANCE: 'GetAssistance', diff --git a/src/components/AccountingListSkeletonView.tsx b/src/components/AccountingListSkeletonView.tsx new file mode 100644 index 000000000000..b977903d3adc --- /dev/null +++ b/src/components/AccountingListSkeletonView.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import {Circle, Rect} from 'react-native-svg'; +import ItemListSkeletonView from './Skeletons/ItemListSkeletonView'; + +type AccountingListSkeletonViewProps = { + shouldAnimate?: boolean; +}; + +function AccountingListSkeletonView({shouldAnimate = true}: AccountingListSkeletonViewProps) { + return ( + ( + <> + + + + )} + /> + ); +} + +AccountingListSkeletonView.displayName = 'AccountingListSkeletonView'; + +export default AccountingListSkeletonView; diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 9ad4643e834a..f716548f55d6 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -4,7 +4,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import type {MaybePhraseKey} from '@libs/Localize'; -import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; @@ -149,8 +148,6 @@ function AddressForm({ label={translate('common.addressLine', {lineNumber: 1})} onValueChange={(data: unknown, key: unknown) => { onAddressChanged(data, key); - // This enforces the country selector to use the country from address instead of the country from URL - Navigation.setParams({country: undefined}); }} defaultValue={street1} renamedInputKeys={{ @@ -183,6 +180,7 @@ function AddressForm({ InputComponent={CountrySelector} inputID={INPUT_IDS.COUNTRY} value={country} + onValueChange={onAddressChanged} shouldSaveDraft={shouldSaveDraft} /> diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index da8e3694a7d2..0c8af3dfc826 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -52,8 +52,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow }} onPressIn={onPressIn} onPressOut={onPressOut} - // @ts-expect-error TODO: Remove this once ShowContextMenuContext (https://github.com/Expensify/App/issues/24980) is migrated to TypeScript. - onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} + onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} shouldUseHapticsOnLongPress accessibilityLabel={displayName} role={CONST.ROLE.BUTTON} @@ -63,6 +62,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', dow file={{name: displayName}} shouldShowDownloadIcon={!isOffline} shouldShowLoadingSpinnerIcon={isDownloading} + isUsedAsChatAttachment /> )} diff --git a/src/components/ArrowKeyFocusManager.js b/src/components/ArrowKeyFocusManager.js deleted file mode 100644 index 2532e52156df..000000000000 --- a/src/components/ArrowKeyFocusManager.js +++ /dev/null @@ -1,142 +0,0 @@ -import {useIsFocused} from '@react-navigation/native'; -import PropTypes from 'prop-types'; -import React, {Component} from 'react'; -import KeyboardShortcut from '@libs/KeyboardShortcut'; -import CONST from '@src/CONST'; - -const propTypes = { - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /** Array of disabled indexes. */ - disabledIndexes: PropTypes.arrayOf(PropTypes.number), - - /** The current focused index. */ - focusedIndex: PropTypes.number.isRequired, - - /** The maximum index – provided so that the focus can be sent back to the beginning of the list when the end is reached. */ - maxIndex: PropTypes.number.isRequired, - - /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, - - /** A callback executed when the focused input changes. */ - onFocusedIndexChanged: PropTypes.func.isRequired, - - /** If this value is true, then we exclude TextArea Node. */ - shouldExcludeTextAreaNodes: PropTypes.bool, - - /** If this value is true, then the arrow down callback would be triggered when the max index is exceeded */ - shouldResetIndexOnEndReached: PropTypes.bool, -}; - -const defaultProps = { - disabledIndexes: [], - shouldExcludeTextAreaNodes: true, - shouldResetIndexOnEndReached: true, -}; - -class BaseArrowKeyFocusManager extends Component { - componentDidMount() { - const arrowUpConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_UP; - const arrowDownConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_DOWN; - - this.onArrowUpKey = this.onArrowUpKey.bind(this); - this.onArrowDownKey = this.onArrowDownKey.bind(this); - - this.unsubscribeArrowUpKey = KeyboardShortcut.subscribe(arrowUpConfig.shortcutKey, this.onArrowUpKey, arrowUpConfig.descriptionKey, arrowUpConfig.modifiers, true, false, 0, true, [ - this.props.shouldExcludeTextAreaNodes && 'TEXTAREA', - ]); - - this.unsubscribeArrowDownKey = KeyboardShortcut.subscribe( - arrowDownConfig.shortcutKey, - this.onArrowDownKey, - arrowDownConfig.descriptionKey, - arrowDownConfig.modifiers, - true, - false, - 0, - true, - [this.props.shouldExcludeTextAreaNodes && 'TEXTAREA'], - ); - } - - componentDidUpdate(prevProps) { - if (prevProps.maxIndex === this.props.maxIndex) { - return; - } - if (this.props.focusedIndex > this.props.maxIndex && this.props.shouldResetIndexOnEndReached) { - this.onArrowDownKey(); - } - } - - componentWillUnmount() { - if (this.unsubscribeArrowUpKey) { - this.unsubscribeArrowUpKey(); - } - - if (this.unsubscribeArrowDownKey) { - this.unsubscribeArrowDownKey(); - } - } - - onArrowUpKey() { - if (this.props.maxIndex < 0 || !this.props.isFocused) { - return; - } - - const currentFocusedIndex = this.props.focusedIndex > 0 ? this.props.focusedIndex - 1 : this.props.maxIndex; - let newFocusedIndex = currentFocusedIndex; - - while (this.props.disabledIndexes.includes(newFocusedIndex)) { - newFocusedIndex = newFocusedIndex > 0 ? newFocusedIndex - 1 : this.props.maxIndex; - if (newFocusedIndex === currentFocusedIndex) { - // all indexes are disabled - return; // no-op - } - } - - this.props.onFocusedIndexChanged(newFocusedIndex); - } - - onArrowDownKey() { - if (this.props.maxIndex < 0 || !this.props.isFocused) { - return; - } - - const currentFocusedIndex = this.props.focusedIndex < this.props.maxIndex ? this.props.focusedIndex + 1 : 0; - let newFocusedIndex = currentFocusedIndex; - - while (this.props.disabledIndexes.includes(newFocusedIndex)) { - newFocusedIndex = newFocusedIndex < this.props.maxIndex ? newFocusedIndex + 1 : 0; - if (newFocusedIndex === currentFocusedIndex) { - // all indexes are disabled - return; // no-op - } - } - - this.props.onFocusedIndexChanged(newFocusedIndex); - } - - render() { - return this.props.children; - } -} - -function ArrowKeyFocusManager(props) { - const isFocused = useIsFocused(); - - return ( - - ); -} - -BaseArrowKeyFocusManager.propTypes = propTypes; -BaseArrowKeyFocusManager.defaultProps = defaultProps; -ArrowKeyFocusManager.displayName = 'ArrowKeyFocusManager'; - -export default ArrowKeyFocusManager; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index f697142c556e..de98ba79d23c 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -456,7 +456,7 @@ function AttachmentModal({ } const context = useMemo( () => ({ - pagerItems: [], + pagerItems: [{source: sourceForAttachmentView, index: 0, isActive: true}], activePage: 0, pagerRef: undefined, isPagerScrolling: nope, @@ -465,7 +465,7 @@ function AttachmentModal({ onScaleChanged: () => {}, onSwipeDown: closeModal, }), - [closeModal, nope], + [closeModal, nope, sourceForAttachmentView], ); return ( @@ -607,7 +607,6 @@ export default withOnyx({ const transactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction?.originalMessage.IOUTransactionID ?? '0' : '0'; return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; }, - initWithStoredValues: false, }, })(memo(AttachmentModal)); diff --git a/src/components/AttachmentOfflineIndicator.tsx b/src/components/AttachmentOfflineIndicator.tsx new file mode 100644 index 000000000000..d425e6f18e0e --- /dev/null +++ b/src/components/AttachmentOfflineIndicator.tsx @@ -0,0 +1,57 @@ +import React, {useEffect, useState} from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import Text from './Text'; + +type AttachmentOfflineIndicatorProps = { + /** Whether the offline indicator is displayed for the attachment preview. */ + isPreview?: boolean; +}; + +function AttachmentOfflineIndicator({isPreview = false}: AttachmentOfflineIndicatorProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + + // We don't want to show the offline indicator when the attachment is a cached one, so + // we delay the display by 200 ms to ensure it is not a cached one. + const [onCacheDelay, setOnCacheDelay] = useState(true); + + useEffect(() => { + const timeout = setTimeout(() => setOnCacheDelay(false), 200); + + return () => clearTimeout(timeout); + }, []); + + if (!isOffline || onCacheDelay) { + return null; + } + + return ( + + + {!isPreview && ( + + {translate('common.youAppearToBeOffline')} + {translate('common.attachementWillBeAvailableOnceBackOnline')} + + )} + + ); +} + +AttachmentOfflineIndicator.displayName = 'AttachmentOfflineIndicator'; + +export default AttachmentOfflineIndicator; diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index ad4cf023c096..218a255d34d3 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -171,7 +171,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s */ const showDocumentPicker = useCallback( (): Promise => - RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error) => { + RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error: Error) => { if (RNDocumentPicker.isCancel(error)) { return; } @@ -247,7 +247,7 @@ function AttachmentPicker({type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, s .then((result) => { completeAttachmentSelection.current(result); }) - .catch((error) => { + .catch((error: Error) => { showGeneralAlert(error.message); throw error; }); diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index ec2687d634bb..2ec1883fd7de 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -71,7 +71,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr return ( - + ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index e16a81702f66..e8f6568f98c0 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx @@ -2,7 +2,7 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; import type AttachmentViewPdfProps from './types'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}: AttachmentViewPdfProps) { +function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onToggleKeyboard, onLoadComplete, style, isUsedAsChatAttachment, onLoadError}: AttachmentViewPdfProps) { return ( ); } diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts index 605be3b25ec4..c20593449d58 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts @@ -1,4 +1,4 @@ -import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import type {AttachmentViewProps} from '..'; type AttachmentViewPdfProps = Pick & { @@ -8,11 +8,14 @@ type AttachmentViewPdfProps = Pick; - /** Styles for the error label */ - errorLabelStyles?: StyleProp; - /** Triggered when the PDF's onScaleChanged event is triggered */ onScaleChanged?: (scale: number) => void; + + /** Triggered when the PDF fails to load */ + onLoadError?: () => void; + + /** Whether the PDF is used as a chat attachment */ + isUsedAsChatAttachment?: boolean; }; export default AttachmentViewPdfProps; diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx new file mode 100644 index 000000000000..e06ea3064150 --- /dev/null +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type DefaultAttachmentViewProps = { + /** The name of the file */ + fileName?: string; + + /** Should show the download icon */ + shouldShowDownloadIcon?: boolean; + + /** Should show the loading spinner icon */ + shouldShowLoadingSpinnerIcon?: boolean; + + /** Additional styles for the container */ + containerStyles?: StyleProp; +}; + +function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles}: DefaultAttachmentViewProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + + + + {fileName} + {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( + + + + + + )} + {shouldShowLoadingSpinnerIcon && ( + + + + + + )} + + ); +} + +DefaultAttachmentView.displayName = 'DefaultAttachmentView'; + +export default DefaultAttachmentView; diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 2685a5cef407..cee2264894a7 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -1,17 +1,14 @@ import Str from 'expensify-common/lib/str'; import React, {memo, useEffect, useState} from 'react'; import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; -import {ActivityIndicator, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; import ScrollView from '@components/ScrollView'; -import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -28,6 +25,7 @@ import type {Transaction} from '@src/types/onyx'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; import AttachmentViewVideo from './AttachmentViewVideo'; +import DefaultAttachmentView from './DefaultAttachmentView'; type AttachmentViewOnyxProps = { transaction: OnyxEntry; @@ -64,9 +62,14 @@ type AttachmentViewProps = AttachmentViewOnyxProps & /** Denotes whether it is an icon (ex: SVG) */ maybeIcon?: boolean; + /** Fallback source to use in case of error */ fallbackSource?: AttachmentSource; + /* Whether it is hovered or not */ isHovered?: boolean; + + /** Whether the attachment is used as a chat attachment */ + isUsedAsChatAttachment?: boolean; }; function AttachmentView({ @@ -88,6 +91,7 @@ function AttachmentView({ reportActionID, isHovered, duration, + isUsedAsChatAttachment, }: AttachmentViewProps) { const {translate} = useLocalize(); const {updateCurrentlyPlayingURL} = usePlaybackContext(); @@ -95,6 +99,7 @@ function AttachmentView({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [loadComplete, setLoadComplete] = useState(false); + const [hasPDFFailedToLoad, setHasPDFFailedToLoad] = useState(false); const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file?.name && Str.isVideo(file.name)); useEffect(() => { @@ -145,7 +150,9 @@ function AttachmentView({ // Check both source and file.name since PDFs dragged into the text field // will appear with a source that is a blob - if ((typeof source === 'string' && Str.isPDF(source)) || (file && Str.isPDF(file.name ?? translate('attachmentView.unknownFilename')))) { + const isSourcePDF = typeof source === 'string' && Str.isPDF(source); + const isFilePDF = file && Str.isPDF(file.name ?? translate('attachmentView.unknownFilename')); + if (!hasPDFFailedToLoad && (isSourcePDF || isFilePDF)) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source as string) : (source as string); const onPDFLoadComplete = (path: string) => { @@ -158,6 +165,10 @@ function AttachmentView({ } }; + const onPDFLoadError = () => { + setHasPDFFailedToLoad(true); + }; + // We need the following View component on android native // So that the event will propagate properly and // the Password protected preview will be shown for pdf attachement we are about to send. @@ -170,8 +181,9 @@ function AttachmentView({ onPress={onPress} onToggleKeyboard={onToggleKeyboard} onLoadComplete={onPDFLoadComplete} - errorLabelStyles={isUsedInAttachmentModal ? [styles.textLabel, styles.textLarge] : [styles.cursorAuto]} style={isUsedInAttachmentModal ? styles.imageModalPDF : styles.flex1} + isUsedAsChatAttachment={isUsedAsChatAttachment} + onLoadError={onPDFLoadError} /> ); @@ -228,36 +240,12 @@ function AttachmentView({ } return ( - - - - - - {file?.name} - {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( - - - - - - )} - {shouldShowLoadingSpinnerIcon && ( - - - - - - )} - + ); } diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 32ac018acb04..4c11f1f0e35c 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -1,16 +1,12 @@ -import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -// We take ScrollView from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another -import {ScrollView} from 'react-native-gesture-handler'; +import React, {useCallback, useEffect, useRef} from 'react'; +import {FlatList} from 'react-native-gesture-handler'; import Animated, {Easing, FadeOutDown, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps} from './types'; @@ -45,11 +41,10 @@ function BaseAutoCompleteSuggestions({ isSuggestionPickerLarge, keyExtractor, }: AutoCompleteSuggestionsProps) { - const {windowWidth, isLargeScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); - const scrollRef = useRef>(null); + const scrollRef = useRef>(null); /** * Render a suggestion menu item component. */ @@ -72,13 +67,7 @@ function BaseAutoCompleteSuggestions({ const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); - const estimatedListSize = useMemo( - () => ({ - height: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length, - width: (isLargeScreenWidth ? windowWidth - variables.sideBarWidth : windowWidth) - CONST.CHAT_FOOTER_HORIZONTAL_PADDING, - }), - [isLargeScreenWidth, suggestions.length, windowWidth], - ); + useEffect(() => { rowHeight.value = withTiming(measureHeightOfSuggestionRows(suggestions.length, isSuggestionPickerLarge), { duration: 100, @@ -105,14 +94,11 @@ function BaseAutoCompleteSuggestions({ }} > - rowHeight.value} diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 4acf197ba178..f633869ccd30 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -52,7 +52,7 @@ type AvatarProps = { name?: string; /** Optional account id if it's user avatar or policy id if it's workspace avatar */ - accountID?: number | string; + avatarID?: number | string; }; function Avatar({ @@ -66,7 +66,7 @@ function Avatar({ fallbackIconTestID = '', type = CONST.ICON_TYPE_AVATAR, name = '', - accountID, + avatarID, }: AvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -86,7 +86,7 @@ function Avatar({ const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; // We pass the color styles down to the SVG for the workspace and fallback avatar. - const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, Number(accountID)); + const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, Number(avatarID)); const useFallBackAvatar = imageError || !source || source === Expensicons.FallbackAvatar; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; @@ -94,7 +94,7 @@ function Avatar({ let iconColors; if (isWorkspace) { - iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(accountID?.toString() ?? ''); + iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(avatarID?.toString() ?? ''); } else if (useFallBackAvatar) { iconColors = StyleUtils.getBackgroundColorAndFill(theme.buttonHoveredBG, theme.icon); } else { diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 8942bf97a7dd..156c27abbc9d 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -12,7 +12,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, Policy, Report, ReportActions} from '@src/types/onyx'; import DisplayNames from './DisplayNames'; import MultipleAvatars from './MultipleAvatars'; import ParentNavigationSubtitle from './ParentNavigationSubtitle'; @@ -25,7 +25,7 @@ type AvatarWithDisplayNamePropsWithOnyx = { parentReportActions: OnyxEntry; /** Personal details of all users */ - personalDetails: OnyxCollection; + personalDetails: OnyxEntry; }; type AvatarWithDisplayNameProps = AvatarWithDisplayNamePropsWithOnyx & { diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 5fa17e726c6f..a3bd98bd8440 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -123,6 +123,9 @@ type AvatarWithImagePickerProps = { /** Optionally override the default "Edit" icon */ editIcon?: IconAsset; + + /** Determines if a style utility function should be used for calculating the PopoverMenu anchor position. */ + shouldUseStyleUtilityForAnchorPosition?: boolean; }; function AvatarWithImagePicker({ @@ -152,6 +155,7 @@ function AvatarWithImagePicker({ enablePreview = false, shouldDisableViewPhoto = false, editIcon = Expensicons.Pencil, + shouldUseStyleUtilityForAnchorPosition = false, }: AvatarWithImagePickerProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -393,7 +397,7 @@ function AvatarWithImagePicker({ } }} menuItems={menuItems} - anchorPosition={popoverPosition} + anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverMenuOffset(windowWidth) : popoverPosition} anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}} withoutOverlay anchorRef={anchorRef} diff --git a/src/components/CategoryPicker.tsx b/src/components/CategoryPicker.tsx index 40ebebdd0488..e5c85a8f5f6d 100644 --- a/src/components/CategoryPicker.tsx +++ b/src/components/CategoryPicker.tsx @@ -45,6 +45,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC }, [selectedCategory]); const [sections, headerMessage, shouldShowTextInput] = useMemo(() => { + const categories = policyCategories ?? policyCategoriesDraft ?? {}; const validPolicyRecentlyUsedCategories = policyRecentlyUsedCategories?.filter?.((p) => !isEmptyObject(p)); const {categoryOptions} = OptionsListUtils.getFilteredOptions( [], @@ -56,15 +57,15 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC false, false, true, - policyCategories ?? policyCategoriesDraft ?? {}, + categories, validPolicyRecentlyUsedCategories, false, ); const categoryData = categoryOptions?.[0]?.data ?? []; const header = OptionsListUtils.getHeaderMessageForNonUserList(categoryData.length > 0, debouncedSearchValue); - const policiesCount = OptionsListUtils.getEnabledCategoriesCount(policyCategories ?? {}); - const isCategoriesCountBelowThreshold = policiesCount < CONST.CATEGORY_LIST_THRESHOLD; + const categoriesCount = OptionsListUtils.getEnabledCategoriesCount(categories); + const isCategoriesCountBelowThreshold = categoriesCount < CONST.CATEGORY_LIST_THRESHOLD; const showInput = !isCategoriesCountBelowThreshold; return [categoryOptions, header, showInput]; diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 4bc54d13b056..f8bff00f3689 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -122,7 +122,7 @@ function Composer( if (shouldCalculateCaretPosition && isRendered) { // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { - setValueBeforeCaret(webEvent.target.value.slice(0, webEvent.nativeEvent.selection.start)); + setValueBeforeCaret((webEvent.target as HTMLInputElement).value.slice(0, webEvent.nativeEvent.selection.start)); setCaretContent(getNextChars(value ?? '', webEvent.nativeEvent.selection.start)); }); const selectionValue = { diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx index 212674e2a125..7e1d81cc4071 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx @@ -9,6 +9,7 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import {removePolicyConnection} from '@libs/actions/connections'; import {getQuickBooksOnlineSetupLink} from '@libs/actions/connections/QuickBooksOnline'; @@ -34,6 +35,7 @@ function ConnectToQuickbooksOnlineButton({ const {translate} = useLocalize(); const webViewRef = useRef(null); const [isWebViewOpen, setWebViewOpen] = useState(false); + const {isOffline} = useNetwork(); const authToken = session?.authToken ?? null; @@ -52,6 +54,7 @@ function ConnectToQuickbooksOnlineButton({ text={translate('workspace.accounting.setup')} style={styles.justifyContentCenter} small + isDisabled={isOffline} /> {shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect && isDisconnectModalOpen && ( ; const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false); @@ -47,6 +49,7 @@ function ConnectToXeroButton({policyID, session, shouldDisconnectIntegrationBefo text={translate('workspace.accounting.setup')} style={styles.justifyContentCenter} small + isDisabled={isOffline} /> {shouldDisconnectIntegrationBeforeConnecting && isDisconnectModalOpen && integrationToDisconnect && ( {shouldDisconnectIntegrationBeforeConnecting && isDisconnectModalOpen && integrationToDisconnect && ( {}, onBlur}: CountrySelectorProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const {country: countryFromUrl} = useGeographicalStateAndCountryFromRoute(); const title = countryCode ? translate(`allCountries.${countryCode}`) : ''; const countryTitleDescStyle = title.length === 0 ? styles.textNormal : null; @@ -39,12 +41,30 @@ function CountrySelector({errorText = '', value: countryCode, onInputChange = () const didOpenContrySelector = useRef(false); const isFocused = useIsFocused(); useEffect(() => { - if (!isFocused || !didOpenContrySelector.current) { + // Check if the country selector was opened and no value was selected, triggering onBlur to display an error + if (isFocused && didOpenContrySelector.current) { + didOpenContrySelector.current = false; + if (!countryFromUrl) { + onBlur?.(); + } + } + + // If no country is selected from the URL, exit the effect early to avoid further processing. + if (!countryFromUrl) { return; } - didOpenContrySelector.current = false; - onBlur?.(); - }, [isFocused, onBlur]); + + // If a country is selected, invoke `onInputChange` to update the form and clear any validation errors related to the country selection. + if (onInputChange) { + onInputChange(countryFromUrl); + } + + // Clears the `country` parameter from the URL to ensure the component country is driven by the parent component rather than URL parameters. + // This helps prevent issues where the component might not update correctly if the country is controlled by both the parent and the URL. + Navigation.setParams({country: undefined}); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [countryFromUrl, isFocused, onBlur]); useEffect(() => { // This will cause the form to revalidate and remove any error related to country name diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx index 2f09b27f3067..c9d60f3ced46 100644 --- a/src/components/FixedFooter.tsx +++ b/src/components/FixedFooter.tsx @@ -2,7 +2,6 @@ import type {ReactNode} from 'react'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import useKeyboardState from '@hooks/useKeyboardState'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,7 +14,6 @@ type FixedFooterProps = { }; function FixedFooter({style, children}: FixedFooterProps) { - const {isKeyboardShown} = useKeyboardState(); const insets = useSafeAreaInsets(); const styles = useThemeStyles(); @@ -23,7 +21,7 @@ function FixedFooter({style, children}: FixedFooterProps) { return null; } - const shouldAddBottomPadding = isKeyboardShown || !insets.bottom; + const shouldAddBottomPadding = !insets.bottom; return {children}; } diff --git a/src/components/FullstoryNative.tsx b/src/components/FullstoryNative.tsx new file mode 100644 index 000000000000..63e6217a464e --- /dev/null +++ b/src/components/FullstoryNative.tsx @@ -0,0 +1,3 @@ +import FS from '@fullstory/react-native'; + +export default FS; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index 0c7c82534bbb..504ddecb492b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -105,7 +105,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona {...defaultRendererProps} style={[styles.link, styleWithoutColor, StyleUtils.getMentionStyle(isOurMention), {color: StyleUtils.getMentionTextColor(isOurMention)}]} role={CONST.ROLE.LINK} - testID="span" + testID="mention-user" href={`/${navigationRoute}`} > {htmlAttribAccountID ? `@${mentionDisplayText}` : } diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 70f1b97c04b1..78cd92dbc223 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -182,7 +182,7 @@ function HeaderWithBackButton({ containerStyles={[StyleUtils.getWidthAndHeightStyle(StyleUtils.getAvatarSize(CONST.AVATAR_SIZE.DEFAULT)), styles.mr3]} source={policyAvatar?.source} name={policyAvatar?.name} - accountID={policyAvatar?.id} + avatarID={policyAvatar?.id} type={policyAvatar?.type} /> )} diff --git a/src/components/Icon/__mocks__/Expensicons.ts b/src/components/Icon/__mocks__/Expensicons.ts index 78ad86210bf0..e268b109f0a5 100644 --- a/src/components/Icon/__mocks__/Expensicons.ts +++ b/src/components/Icon/__mocks__/Expensicons.ts @@ -1,4 +1,7 @@ -const Expensicons = jest.requireActual('../Expensicons'); +// eslint-disable-next-line import/no-import-module-exports +import type {SvgProps} from 'react-native-svg'; + +const Expensicons = jest.requireActual>>('../Expensicons'); module.exports = Object.keys(Expensicons).reduce((prev, curr) => { // We set the name of the anonymous mock function here so we can dynamically build the list of mocks and access the diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 5d09e7abf41d..f08941ef7d77 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -2,11 +2,13 @@ import type {SyntheticEvent} from 'react'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; import {View} from 'react-native'; +import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; import type {ImageOnLoadEvent} from '@components/Image/types'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -33,6 +35,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV const [imgHeight, setImgHeight] = useState(0); const [zoomScale, setZoomScale] = useState(0); const [zoomDelta, setZoomDelta] = useState(); + const {isOffline} = useNetwork(); const scrollableRef = useRef(null); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); @@ -210,7 +213,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV onLoad={imageLoad} onError={onError} /> - {(isLoading || zoomScale === 0) && } + {((isLoading && !isOffline) || (!isLoading && zoomScale === 0)) && } + {isLoading && } ); } @@ -243,7 +247,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV /> - {isLoading && } + {isLoading && !isOffline && } + {isLoading && } ); } diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index eac5c676370b..3d940103715d 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -6,6 +6,7 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; import CONST from '@src/CONST'; +import AttachmentOfflineIndicator from './AttachmentOfflineIndicator'; import FullscreenLoadingIndicator from './FullscreenLoadingIndicator'; import Image from './Image'; import RESIZE_MODES from './Image/resizeModes'; @@ -108,7 +109,8 @@ function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthT onLoad={imageLoadedSuccessfully} objectPosition={objectPosition} /> - {isLoading && !isImageCached && } + {isLoading && !isImageCached && !isOffline && } + {isLoading && !isImageCached && } ); } diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx index 54f6748986f4..5c48652f8cc5 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx @@ -7,7 +7,7 @@ type BaseInvertedFlatListProps = FlatListProps & { shouldEnableAutoScrollToTopThreshold?: boolean; }; -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; +const AUTOSCROLL_TO_TOP_THRESHOLD = 250; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { const {shouldEnableAutoScrollToTopThreshold, ...rest} = props; diff --git a/src/components/KeyboardAvoidingView/index.ios.tsx b/src/components/KeyboardAvoidingView/index.ios.tsx index a7cd767377ef..171210eab7ac 100644 --- a/src/components/KeyboardAvoidingView/index.ios.tsx +++ b/src/components/KeyboardAvoidingView/index.ios.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native'; -import type KeyboardAvoidingViewProps from './types'; +import type {KeyboardAvoidingViewProps} from './types'; function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/KeyboardAvoidingView/index.tsx b/src/components/KeyboardAvoidingView/index.tsx index 09ec21e5b219..c0882ae1e9cc 100644 --- a/src/components/KeyboardAvoidingView/index.tsx +++ b/src/components/KeyboardAvoidingView/index.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; import {View} from 'react-native'; -import type KeyboardAvoidingViewProps from './types'; +import type {KeyboardAvoidingViewProps} from './types'; function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { const {behavior, contentContainerStyle, enabled, keyboardVerticalOffset, ...rest} = props; diff --git a/src/components/KeyboardAvoidingView/types.ts b/src/components/KeyboardAvoidingView/types.ts index 48d354e8b53f..2c1ef64ced8f 100644 --- a/src/components/KeyboardAvoidingView/types.ts +++ b/src/components/KeyboardAvoidingView/types.ts @@ -1,3 +1,4 @@ -import {KeyboardAvoidingViewProps} from 'react-native'; +import type {KeyboardAvoidingViewProps} from 'react-native'; -export default KeyboardAvoidingViewProps; +// eslint-disable-next-line import/prefer-default-export +export type {KeyboardAvoidingViewProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index b665c305f5cf..b0418c9fdb28 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -58,20 +58,31 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], ); - if (!optionItem) { + if (!optionItem && !isFocused) { // rendering null as a render item causes the FlashList to render all - // its children and consume signficant memory. We can avoid this by - // rendering a placeholder view instead. + // its children and consume signficant memory on the first render. We can avoid this by + // rendering a placeholder view instead. This behaviour is only observed when we + // first sign in to the App. + // We can fix this by checking if the optionItem is null and the component is not focused. + // Which means that the currentReportID is not the same as the reportID. The currentReportID + // in this case is empty and hence the component is not focused. return ; } + if (!optionItem) { + // This is the case when the component is focused and the optionItem is null. + // For example, when you submit an expense in offline mode and click on the + // generated expense report, we would only see the Report Details but no item in LHN. + return null; + } + const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.requiresAttentionFromCurrentUser(optionItem, optionItem.parentReportAction); const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = optionItem?.isUnread && optionItem.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, style]; const alternateTextStyle = isInFocusMode - ? [textStyle, styles.optionAlternateText, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style] + ? [textStyle, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style] : [textStyle, styles.optionAlternateText, styles.textLabelSupporting, style]; const contentContainerStyles = isInFocusMode ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 86a52c2baf6c..0be0171eaa9a 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -2,12 +2,14 @@ import React, {useCallback, useContext, useEffect, useMemo, useState} from 'reac import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import {useSharedValue} from 'react-native-reanimated'; +import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import Image from '@components/Image'; import type {ImageOnLoadEvent} from '@components/Image/types'; import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCanvas'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from '@components/MultiGestureCanvas/types'; import {getCanvasFitScale} from '@components/MultiGestureCanvas/utils'; +import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import NUMBER_OF_CONCURRENT_LIGHTBOXES from './numberOfConcurrentLightboxes'; @@ -47,6 +49,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan * we need to create a shared value that can be used in the render function. */ const isPagerScrollingFallback = useSharedValue(false); + const {isOffline} = useNetwork(); const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const { @@ -219,8 +222,8 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan style={[contentSize ?? styles.invisibleImage]} isAuthTokenRequired={isAuthTokenRequired} onError={onError} - onLoad={updateContentSize} - onLoadEnd={() => { + onLoad={(e) => { + updateContentSize(e); setLightboxImageLoaded(true); }} /> @@ -236,19 +239,22 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan resizeMode="contain" style={[fallbackSize ?? styles.invisibleImage]} isAuthTokenRequired={isAuthTokenRequired} - onLoad={updateContentSize} - onLoadEnd={() => setFallbackImageLoaded(true)} + onLoad={(e) => { + updateContentSize(e); + setFallbackImageLoaded(true); + }} /> )} {/* Show activity indicator while the lightbox is still loading the image. */} - {isLoading && ( + {isLoading && !isOffline && ( )} + {isLoading && } )} diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index b11ae4f5ecd8..a787ac5ded56 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -83,7 +83,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe source={item.icons[0].source} size={isIcon ? CONST.AVATAR_SIZE.MENTION_ICON : CONST.AVATAR_SIZE.SMALLER} name={item.icons[0].name} - accountID={item.icons[0].id} + avatarID={item.icons[0].id} type={item.icons[0].type} fill={isIcon ? theme.success : undefined} fallbackIcon={item.icons[0].fallbackIcon} diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index eb525ef3d1ff..ad06f9fcb78c 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -265,6 +265,9 @@ type MenuItemBaseProps = { /** Handles what to do when the item is focused */ onFocus?: () => void; + + /** Optional account id if it's user avatar or policy id if it's workspace avatar */ + avatarID?: number | string; }; type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps; @@ -342,6 +345,7 @@ function MenuItem( isPaneMenu = false, shouldPutLeftPaddingWhenNoIcon = false, onFocus, + avatarID, }: MenuItemProps, ref: PressableRef, ) { @@ -472,8 +476,8 @@ function MenuItem( onFocus={onFocus} > {({pressed}) => ( - - + + {!!label && isLabelHoverable && ( @@ -526,6 +530,7 @@ function MenuItem( source={icon as AvatarSource} fallbackIcon={fallbackIcon} name={title} + avatarID={avatarID} type={CONST.ICON_TYPE_WORKSPACE} /> )} diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index e1a998f78cab..2393273ec957 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -1,15 +1,19 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as HeaderUtils from '@libs/HeaderUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; +import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; +import * as TransactionActions from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -19,26 +23,14 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Button from './Button'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; +import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; +import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import SettlementButton from './SettlementButton'; -type MoneyReportHeaderOnyxProps = { - /** The chat report this report is linked to */ - chatReport: OnyxEntry; - - /** The next step for the report */ - nextStep: OnyxEntry; - - /** Session info for the currently logged in user. */ - session: OnyxEntry; - - /** The transaction thread report associated with the current report, if any */ - transactionThreadReport: OnyxEntry; -}; - -type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & { +type MoneyReportHeaderProps = { /** The report currently being looked at */ report: OnyxTypes.Report; @@ -59,18 +51,14 @@ type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & { onBackButtonPress: () => void; }; -function MoneyReportHeader({ - session, - policy, - chatReport, - nextStep, - report: moneyRequestReport, - transactionThreadReport, - reportActions, - shouldUseNarrowLayout = false, - onBackButtonPress, -}: MoneyReportHeaderProps) { +function MoneyReportHeader({policy, report: moneyRequestReport, transactionThreadReportID, reportActions, shouldUseNarrowLayout = false, onBackButtonPress}: MoneyReportHeaderProps) { + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport.chatReportID}`); + const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport.reportID}`); + const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); + const [session] = useOnyx(ONYXKEYS.SESSION); + const styles = useThemeStyles(); + const theme = useTheme(); const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); @@ -98,6 +86,9 @@ function MoneyReportHeader({ const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); + const transactionIDs = TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID).map((transaction) => transaction.transactionID); + const allHavePendingRTERViolation = TransactionUtils.allHavePendingRTERViolation(transactionIDs); + const cancelPayment = useCallback(() => { if (!chatReport) { return; @@ -112,13 +103,14 @@ function MoneyReportHeader({ const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); - const shouldShowSettlementButton = !ReportUtils.isInvoiceReport(moneyRequestReport) && (shouldShowPayButton || shouldShowApproveButton); + const shouldShowSettlementButton = !ReportUtils.isInvoiceReport(moneyRequestReport) && (shouldShowPayButton || shouldShowApproveButton) && !allHavePendingRTERViolation; - const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; + const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !allHavePendingRTERViolation; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); + const shouldShowMarkAsCashButton = isDraft && allHavePendingRTERViolation; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; - const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length; - const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; + const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !allHavePendingRTERViolation; + const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy); @@ -160,6 +152,16 @@ function MoneyReportHeader({ setIsDeleteRequestModalVisible(false); }, [moneyRequestReport?.reportID, requestParentReportAction, setIsDeleteRequestModalVisible]); + const markAsCash = useCallback(() => { + if (!requestParentReportAction) { + return; + } + const iouTransactionID = requestParentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? requestParentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + const reportID = transactionThreadReport?.reportID ?? ''; + + TransactionActions.markAsCash(iouTransactionID, reportID); + }, [requestParentReportAction, transactionThreadReport?.reportID]); + // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( () => chatReport?.isOwnPolicyExpenseChat && !policy?.harvesting?.enabled, @@ -203,7 +205,7 @@ function MoneyReportHeader({ shouldShowBackButton={shouldUseNarrowLayout} onBackButtonPress={onBackButtonPress} // Shows border if no buttons or next steps are showing below the header - shouldShowBorderBottom={!(shouldShowAnyButton && shouldUseNarrowLayout) && !(shouldShowNextStep && !shouldUseNarrowLayout)} + shouldShowBorderBottom={!(shouldShowAnyButton && shouldUseNarrowLayout) && !(shouldShowNextStep && !shouldUseNarrowLayout) && !allHavePendingRTERViolation} shouldShowThreeDotsButton threeDotsMenuItems={threeDotsMenuItems} threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} @@ -240,7 +242,45 @@ function MoneyReportHeader({ /> )} + {shouldShowMarkAsCashButton && !shouldUseNarrowLayout && ( + +