Skip to content

Commit

Permalink
fix: avoid prefetching when following deeplinks (#11097)
Browse files Browse the repository at this point in the history
* fix: avoid prefetching when following deeplinks

* fix: avoid making home view query requests when opening deep links

Co-authored-by: Ole <[email protected]>
Co-authored-by: Anandaroop Roy <[email protected]>

* fix: don't treat / as a deep link

Prior to this change we were rendering a blank home screen in the
event of navigating directly to /

* fix: switch back to matching route to module

This will be more reliable in the case of various edges cases that
might bring the user to the home screen

* fix: code review suggestions

Co-authored-by: Ole <[email protected]>
Co-authored-by: Mounir Dhahri <[email protected]>

* fix(test): mock the deep link hook

---------

Co-authored-by: Ole <[email protected]>
Co-authored-by: Mounir Dhahri <[email protected]>
  • Loading branch information
3 people authored Nov 25, 2024
1 parent ce85c9b commit 0f62932
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 11 deletions.
24 changes: 19 additions & 5 deletions src/app/Scenes/HomeView/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -86,10 +87,15 @@ export const HomeView: React.FC = () => {
const sections = extractNodes(data?.homeView.sectionsConnection)

useEffect(() => {
prefetchUrl<SearchQuery>("search", searchQueryDefaultVariables)
prefetchUrl("my-profile")
prefetchUrl("inbox")
prefetchUrl("sell")
Linking.getInitialURL().then((url) => {
const isDeepLink = !!url
if (!isDeepLink) {
prefetchUrl<SearchQuery>("search", searchQueryDefaultVariables)
prefetchUrl("my-profile")
prefetchUrl("inbox")
prefetchUrl("sell")
}
})
}, [])

useEffect(() => {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions src/app/Scenes/HomeView/__tests__/HomeView.tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HomeViewSectionArtworksTestsQuery>({
Component: () => {
Expand Down
79 changes: 79 additions & 0 deletions src/app/utils/hooks/useIsDeepLink.tests.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
51 changes: 51 additions & 0 deletions src/app/utils/hooks/useIsDeepLink.ts
Original file line number Diff line number Diff line change
@@ -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<boolean | null>(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,
}
}
17 changes: 11 additions & 6 deletions src/app/utils/useHideSplashScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down

0 comments on commit 0f62932

Please sign in to comment.