diff --git a/src/app/Scenes/HomeView/HomeView.tsx b/src/app/Scenes/HomeView/HomeView.tsx index 1796aed9fc8..52b582b0d10 100644 --- a/src/app/Scenes/HomeView/HomeView.tsx +++ b/src/app/Scenes/HomeView/HomeView.tsx @@ -25,13 +25,14 @@ import { useBottomTabsScrollToTop } from "app/utils/bottomTabsHelper" import { useExperimentVariant } from "app/utils/experiments/hooks" import { extractNodes } from "app/utils/extractNodes" import { useDevToggle } from "app/utils/hooks/useDevToggle" +import { useIsDeepLink } from "app/utils/hooks/useIsDeepLink" import { ProvidePlaceholderContext } from "app/utils/placeholders" import { usePrefetch } from "app/utils/queryPrefetching" import { requestPushNotificationsPermission } from "app/utils/requestPushNotificationsPermission" import { useMaybePromptForReview } from "app/utils/useMaybePromptForReview" import { useSwitchStatusBarStyle } from "app/utils/useStatusBarStyle" import { RefObject, Suspense, useCallback, useEffect, useState } from "react" -import { FlatList, RefreshControl } from "react-native" +import { FlatList, Linking, RefreshControl } from "react-native" import { fetchQuery, graphql, useLazyLoadQuery, usePaginationFragment } from "react-relay" export const NUMBER_OF_SECTIONS_TO_LOAD = 10 @@ -86,10 +87,15 @@ export const HomeView: React.FC = () => { const sections = extractNodes(data?.homeView.sectionsConnection) useEffect(() => { - prefetchUrl("search", searchQueryDefaultVariables) - prefetchUrl("my-profile") - prefetchUrl("inbox") - prefetchUrl("sell") + Linking.getInitialURL().then((url) => { + const isDeepLink = !!url + if (!isDeepLink) { + prefetchUrl("search", searchQueryDefaultVariables) + prefetchUrl("my-profile") + prefetchUrl("inbox") + prefetchUrl("sell") + } + }) }, []) useEffect(() => { @@ -186,6 +192,8 @@ const HomeViewScreenComponent: React.FC = () => { const isNavigationReady = GlobalStore.useAppState((state) => state.sessionState.isNavigationReady) const showPlayground = useDevToggle("DTShowPlayground") + const { isDeepLink } = useIsDeepLink() + useSwitchStatusBarStyle("dark-content", "dark-content") useEffect(() => { @@ -200,6 +208,12 @@ const HomeViewScreenComponent: React.FC = () => { } }, [artQuizState, isNavigationReady]) + // We want to avoid rendering the home view when the user comes back from a deep link + // Because it triggers a lot of queries that affect the user's experience and can be avoided + if (isDeepLink !== false) { + return null + } + if (artQuizState === "incomplete") { return null } diff --git a/src/app/Scenes/HomeView/__tests__/HomeView.tests.tsx b/src/app/Scenes/HomeView/__tests__/HomeView.tests.tsx index 996d66647af..e93e5fda17e 100644 --- a/src/app/Scenes/HomeView/__tests__/HomeView.tests.tsx +++ b/src/app/Scenes/HomeView/__tests__/HomeView.tests.tsx @@ -15,6 +15,12 @@ const requestPushNotificationsPermissionSpy = jest.spyOn( "requestPushNotificationsPermission" ) +jest.mock("app/utils/hooks/useIsDeepLink", () => { + return { + useIsDeepLink: jest.fn().mockReturnValue({ isDeepLink: false }), + } +}) + describe("HomeView", () => { const { renderWithRelay } = setupTestWrapper({ Component: () => { diff --git a/src/app/utils/hooks/useIsDeepLink.tests.ts b/src/app/utils/hooks/useIsDeepLink.tests.ts new file mode 100644 index 00000000000..0555da21386 --- /dev/null +++ b/src/app/utils/hooks/useIsDeepLink.tests.ts @@ -0,0 +1,79 @@ +import { renderHook } from "@testing-library/react-hooks" +import { matchRoute } from "app/routes" +import { useIsDeepLink } from "app/utils/hooks/useIsDeepLink" +import { Linking } from "react-native" + +const mockUseIsFocusedMock = jest.fn() +jest.mock("@react-navigation/native", () => ({ + useIsFocused: () => mockUseIsFocusedMock(), +})) + +jest.mock("react-native", () => ({ + Linking: { + getInitialURL: jest.fn(), + }, +})) + +jest.mock("app/routes", () => ({ + matchRoute: jest.fn(), +})) + +describe("useIsDeepLink", () => { + const mockLinkingGetInitialURL = Linking.getInitialURL as jest.Mock + const mockMatchRoute = matchRoute as jest.Mock + + it("should return true if opened from a deep link", async () => { + // Setup the mock to return the specific URL + mockLinkingGetInitialURL.mockResolvedValue("artsy:///artwork/foo") + mockMatchRoute.mockReturnValue({ type: "match", module: "Artwork" }) + mockUseIsFocusedMock.mockReturnValue(true) + + // Render the hook under test + const { result, waitForNextUpdate } = renderHook(() => useIsDeepLink()) + + // Wait for async effects to resolve + await waitForNextUpdate() + + expect(result.current).toEqual({ + isDeepLink: true, + }) + expect(mockUseIsFocusedMock).toHaveBeenCalled() + expect(mockLinkingGetInitialURL).toHaveBeenCalled() + expect(mockMatchRoute).toHaveBeenCalled() + }) + + it("should return false if not opened from a deep link", async () => { + // Setup the mock to return null + mockLinkingGetInitialURL.mockResolvedValue(null) + mockUseIsFocusedMock.mockReturnValue(true) + + // Render the hook under test + const { result, waitForNextUpdate } = renderHook(() => useIsDeepLink()) + + // Wait for async effects to resolve + await waitForNextUpdate() + + expect(result.current.isDeepLink).toEqual(false) + expect(mockUseIsFocusedMock).toHaveBeenCalled() + expect(mockLinkingGetInitialURL).toHaveBeenCalled() + expect(mockMatchRoute).toHaveBeenCalled() + }) + + it("should return false if opened from a link to /", async () => { + // Setup the mock to return null + mockLinkingGetInitialURL.mockResolvedValue("artsy:///") + mockMatchRoute.mockReturnValue({ type: "match", module: "Home" }) + mockUseIsFocusedMock.mockReturnValue(true) + + // Render the hook under test + const { result, waitForNextUpdate } = renderHook(() => useIsDeepLink()) + + // Wait for async effects to resolve + await waitForNextUpdate() + + expect(result.current.isDeepLink).toEqual(false) + expect(mockUseIsFocusedMock).toHaveBeenCalled() + expect(mockLinkingGetInitialURL).toHaveBeenCalled() + expect(mockMatchRoute).toHaveBeenCalled() + }) +}) diff --git a/src/app/utils/hooks/useIsDeepLink.ts b/src/app/utils/hooks/useIsDeepLink.ts new file mode 100644 index 00000000000..65fc0bdd287 --- /dev/null +++ b/src/app/utils/hooks/useIsDeepLink.ts @@ -0,0 +1,51 @@ +import { useIsFocused } from "@react-navigation/native" +import { matchRoute } from "app/routes" +import { useEffect, useState } from "react" +import { Linking } from "react-native" + +/** + * This is a hook that returns whether or not the user came from a deep link + * (defined as a direct navigation to a route other than "/"). + * + * This can be used to avoid rendering content in previous screens in react-navigation history + * + * @returns {isDeepLink: boolean | null}` isDeepLink` is true if the user came from a deep link. Initially, it is set to `null` while retrieving the deep link URL asynchronously and will be set to `false` if retrieving the `URL` fails. + */ +export const useIsDeepLink = () => { + const [isDeepLink, setIsDeepLink] = useState(null) + + const isFocused = useIsFocused() + + useEffect(() => { + // When the user comes back from a deep link, + // we want to show content again + if (isFocused && isDeepLink === true) { + setIsDeepLink(false) + } + }, [isFocused]) + + useEffect(() => { + Linking.getInitialURL() + .then((url) => { + if (!url) { + setIsDeepLink(false) + return + } + + const result = matchRoute(url) + const isExternalUrl = result.type === "external_url" + const isHomeLink = result.type === "match" && result.module === "Home" + const shouldTreatAsDeepLink = !isHomeLink && !isExternalUrl + + setIsDeepLink(shouldTreatAsDeepLink) + }) + .catch((error) => { + console.error("Error getting initial URL", error) + setIsDeepLink(false) + }) + }, []) + + return { + isDeepLink, + } +} diff --git a/src/app/utils/useHideSplashScreen.ts b/src/app/utils/useHideSplashScreen.ts index ebf09a837b8..5c5de5090c3 100644 --- a/src/app/utils/useHideSplashScreen.ts +++ b/src/app/utils/useHideSplashScreen.ts @@ -2,6 +2,7 @@ import { homeViewScreenQueryVariables } from "app/Scenes/HomeView/HomeView" import { GlobalStore } from "app/store/GlobalStore" import { usePrefetch } from "app/utils/queryPrefetching" import { useEffect } from "react" +import { Linking } from "react-native" import RNBootSplash from "react-native-bootsplash" const HOME_VIEW_SPLASH_SCREEN_DELAY = 500 @@ -20,14 +21,18 @@ export const useHideSplashScreen = () => { if (isHydrated) { if (isLoggedIn && isNavigationReady) { - prefetchUrl("/", homeViewScreenQueryVariables(), { - force: false, + Linking.getInitialURL().then((url) => { + const isDeepLink = !!url + if (!isDeepLink) { + prefetchUrl("/", homeViewScreenQueryVariables(), { + force: false, + }) + } + setTimeout(() => { + hideSplashScreen() + }, HOME_VIEW_SPLASH_SCREEN_DELAY) }) - setTimeout(() => { - hideSplashScreen() - }, HOME_VIEW_SPLASH_SCREEN_DELAY) - return }