diff --git a/src/App.tsx b/src/App.tsx index f9403e258af1..3513cb23953b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,8 +43,9 @@ import type {Route} from './ROUTES'; import './setup/backgroundTask'; import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; +/** Values passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */ type AppProps = { - /** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */ + /** URL containing all necessary data to run React Native app (e.g. login data) */ url?: Route; }; diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index b22b4eac3fc6..638ef0737ed5 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -19,6 +19,7 @@ import addViewportResizeListener from '@libs/VisualViewport'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; +import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen'; import type FocusTrapForScreenProps from './FocusTrap/FocusTrapForScreen/FocusTrapProps'; import HeaderGap from './HeaderGap'; @@ -152,6 +153,7 @@ function ScreenWrapper( const {windowHeight} = useWindowDimensions(shouldUseCachedViewportHeight); // since Modals are drawn in separate native view hierarchy we should always add paddings const ignoreInsetsConsumption = !useContext(ModalContext).default; + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for a case where we want to show the offline indicator only on small screens // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -171,6 +173,7 @@ function ScreenWrapper( UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => { NativeModules.HybridAppModule?.closeReactNativeApp(false, false); + setRootStatusBarEnabled(false); }); const panResponder = useRef( diff --git a/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts b/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts index d31c3693d495..bdea8c157425 100644 --- a/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts +++ b/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts @@ -1,5 +1,5 @@ import {findFocusedRoute, StackActions} from '@react-navigation/native'; -import {BackHandler, NativeModules} from 'react-native'; +import {BackHandler} from 'react-native'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import getTopmostCentralPaneRoute from '@navigation/getTopmostCentralPaneRoute'; import navigationRef from '@navigation/navigationRef'; @@ -22,12 +22,6 @@ function setupCustomAndroidBackHandler() { return false; } - const isLastScreenOnStack = bottomTabRoutes.length === 1 && rootState?.routes?.length === 1; - - if (NativeModules.HybridAppModule && isLastScreenOnStack) { - NativeModules.HybridAppModule.exitApp(); - } - // Handle back press on the search page. // We need to pop two screens, from the central pane and from the bottom tab. if (bottomTabRoutes[bottomTabRoutes.length - 1].name === SCREENS.SEARCH.BOTTOM_TAB && focusedRoute?.name === SCREENS.SEARCH.CENTRAL_PANE) { diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts index 237a615b570a..59f1384c0ce9 100644 --- a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts +++ b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts @@ -116,37 +116,35 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : []; const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID); - Navigation.isNavigationReady() - .then(Navigation.waitForProtectedRoutes) - .then(() => { - // The attachment modal remains open when navigating to the report so we need to close it - Modal.close(() => { - try { - // Get rid of the transition screen, if it is on the top of the stack - if (NativeModules.HybridAppModule && Navigation.getActiveRoute().includes(ROUTES.TRANSITION_BETWEEN_APPS)) { - Navigation.goBack(); - } - // If a chat is visible other than the one we are trying to navigate to, then we need to navigate back - if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) { - Navigation.goBack(); - } - - Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID}); - if (!reportBelongsToWorkspace) { - Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); - } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); - updateLastVisitedPath(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); - } catch (error) { - let errorMessage = String(error); - if (error instanceof Error) { - errorMessage = error.message; - } - - Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: errorMessage}); + Navigation.waitForProtectedRoutes().then(() => { + // The attachment modal remains open when navigating to the report so we need to close it + Modal.close(() => { + try { + // Get rid of the transition screen, if it is on the top of the stack + if (NativeModules.HybridAppModule && Navigation.getActiveRoute().includes(ROUTES.TRANSITION_BETWEEN_APPS)) { + Navigation.goBack(); } - }); + // If a chat is visible other than the one we are trying to navigate to, then we need to navigate back + if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) { + Navigation.goBack(); + } + + Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID}); + if (!reportBelongsToWorkspace) { + Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); + } + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); + updateLastVisitedPath(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); + } catch (error) { + let errorMessage = String(error); + if (error instanceof Error) { + errorMessage = error.message; + } + + Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: errorMessage}); + } }); + }); return Promise.resolve(); } diff --git a/src/libs/actions/Travel.ts b/src/libs/actions/Travel.ts index 1886885587c4..2aeb04b60f1b 100644 --- a/src/libs/actions/Travel.ts +++ b/src/libs/actions/Travel.ts @@ -114,7 +114,12 @@ function provisionDomain(domain: string) { Navigation.navigate(ROUTES.TRAVEL_TCS.getRoute(domain)); } -function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessage: Dispatch>, ctaErrorMessage = ''): void { +function bookATrip( + translate: LocaleContextProps['translate'], + setCtaErrorMessage: Dispatch>, + setRootStatusBarEnabled: (isEnabled: boolean) => void, + ctaErrorMessage = '', +): void { if (!activePolicyID) { return; } @@ -138,6 +143,7 @@ function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessag Log.info('[HybridApp] Returning to OldDot after opening TravelDot'); NativeModules.HybridAppModule.closeReactNativeApp(false, false); + setRootStatusBarEnabled(false); }) ?.catch(() => { setCtaErrorMessage(translate('travel.errorMessage')); diff --git a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx index 12722e87f05a..2d951474f459 100644 --- a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx +++ b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx @@ -1,7 +1,8 @@ -import React, {useMemo, useState} from 'react'; +import React, {useContext, useMemo, useState} from 'react'; import {NativeModules} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -12,11 +13,11 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import {completeOnboarding} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as Policy from '@userActions/Policy/Policy'; -import * as Report from '@userActions/Report'; -import * as Welcome from '@userActions/Welcome'; +import {isPaidGroupPolicy} from '@libs/PolicyUtils'; +import {createWorkspace, generatePolicyID} from '@userActions/Policy/Policy'; +import {setOnboardingAdminsChatReportID, setOnboardingCompanySize, setOnboardingPolicyID} from '@userActions/Welcome'; import CONST from '@src/CONST'; import type {OnboardingCompanySize} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -34,8 +35,9 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID); const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); - const paidGroupPolicy = Object.values(allPolicies ?? {}).find(PolicyUtils.isPaidGroupPolicy); + const paidGroupPolicy = Object.values(allPolicies ?? {}).find(isPaidGroupPolicy); const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout(); const [selectedCompanySize, setSelectedCompanySize] = useState(onboardingCompanySize); @@ -69,19 +71,19 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE setError(translate('onboarding.errorSelection')); return; } - Welcome.setOnboardingCompanySize(selectedCompanySize); + setOnboardingCompanySize(selectedCompanySize); const shouldCreateWorkspace = !onboardingPolicyID && !paidGroupPolicy; // We need `adminsChatReportID` for `Report.completeOnboarding`, but at the same time, we don't want to call `Policy.createWorkspace` more than once. // If we have already created a workspace, we want to reuse the `onboardingAdminsChatReportID` and `onboardingPolicyID`. const {adminsChatReportID, policyID} = shouldCreateWorkspace - ? Policy.createWorkspace(undefined, true, '', Policy.generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM) + ? createWorkspace(undefined, true, '', generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM) : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; if (shouldCreateWorkspace) { - Welcome.setOnboardingAdminsChatReportID(adminsChatReportID); - Welcome.setOnboardingPolicyID(policyID); + setOnboardingAdminsChatReportID(adminsChatReportID); + setOnboardingPolicyID(policyID); } // For MICRO companies (1-10 employees), we want to remain on NewDot. @@ -93,7 +95,7 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE // For other company sizes we want to complete onboarding here. // At this point `onboardingPurposeSelected` should always exist as we set it in `BaseOnboardingPurpose`. if (onboardingPurposeSelected) { - Report.completeOnboarding( + completeOnboarding( onboardingPurposeSelected, CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected], undefined, @@ -106,6 +108,7 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE } NativeModules.HybridAppModule.closeReactNativeApp(false, true); + setRootStatusBarEnabled(false); }} pressOnEnter /> diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index c27edc2e70e2..7ab8268494f7 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -1,8 +1,9 @@ -import React, {useMemo, useState} from 'react'; +import React, {useContext, useMemo, useState} from 'react'; import {Linking, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import EmptyStateComponent from '@components/EmptyStateComponent'; import type {FeatureListItem} from '@components/FeatureList'; @@ -60,6 +61,7 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { const shouldRedirectToExpensifyClassic = useMemo(() => { return areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection) ?? {}); }, [allPolicies]); + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const [ctaErrorMessage, setCtaErrorMessage] = useState(''); @@ -133,7 +135,7 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { buttons: [ { buttonText: translate('search.searchResults.emptyTripResults.buttonText'), - buttonAction: () => bookATrip(translate, setCtaErrorMessage, ctaErrorMessage), + buttonAction: () => bookATrip(translate, setCtaErrorMessage, setRootStatusBarEnabled, ctaErrorMessage), success: true, }, ], @@ -238,6 +240,7 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { styles.emptyStateFolderWebStyles, subtitleComponent, hasSeenTour, + setRootStatusBarEnabled, ctaErrorMessage, navatticURL, shouldRedirectToExpensifyClassic, diff --git a/src/pages/Travel/ManageTrips.tsx b/src/pages/Travel/ManageTrips.tsx index ac427d1d56c9..9a9b59b002c1 100644 --- a/src/pages/Travel/ManageTrips.tsx +++ b/src/pages/Travel/ManageTrips.tsx @@ -1,6 +1,7 @@ -import React, {useState} from 'react'; +import React, {useContext, useState} from 'react'; import {Linking, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import type {FeatureListItem} from '@components/FeatureList'; import FeatureList from '@components/FeatureList'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -34,6 +35,7 @@ function ManageTrips() { const {translate} = useLocalize(); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const policy = usePolicy(activePolicyID); + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const [ctaErrorMessage, setCtaErrorMessage] = useState(''); @@ -55,7 +57,7 @@ function ManageTrips() { ctaText={translate('travel.bookTravel')} ctaAccessibilityLabel={translate('travel.bookTravel')} onCtaPress={() => { - bookATrip(translate, setCtaErrorMessage, ctaErrorMessage); + bookATrip(translate, setCtaErrorMessage, setRootStatusBarEnabled, ctaErrorMessage); }} ctaErrorMessage={ctaErrorMessage} illustration={LottieAnimations.TripsEmptyState} diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 13d0d1d74802..e5b269a7be6e 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -8,6 +8,7 @@ import type {ValueOf} from 'type-fest'; import AccountSwitcher from '@components/AccountSwitcher'; import AccountSwitcherSkeletonView from '@components/AccountSwitcherSkeletonView'; import ConfirmModal from '@components/ConfirmModal'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -100,6 +101,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; const [allConnectionSyncProgresses] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}`); const {setInitialURL} = useContext(InitialURLContext); + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); const subscriptionPlan = useSubscriptionPlan(); @@ -243,6 +245,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr action: () => { NativeModules.HybridAppModule.closeReactNativeApp(false, true); setInitialURL(undefined); + setRootStatusBarEnabled(false); }, } : { @@ -285,7 +288,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr }, ], }; - }, [styles.pt4, signOut, setInitialURL, shouldOpenBookACall, isActingAsDelegate]); + }, [styles.pt4, setInitialURL, setRootStatusBarEnabled, isActingAsDelegate, shouldOpenBookACall, signOut]); /** * Retuns JSX.Element with menu items diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index cbe3f10cbae8..ae63bf77b2a0 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -9,7 +9,6 @@ type HybridAppModule = { closeReactNativeApp: (shouldSignOut: boolean, shouldSetNVP: boolean) => void; completeOnboarding: (status: boolean) => void; switchAccount: (newDotCurrentAccountEmail: string, authToken: string, policyID: string, accountID: string) => void; - exitApp: () => void; }; type RNTextInputResetModule = {