From f889f9eadcdca35ee2e736ffe900cc081db70004 Mon Sep 17 00:00:00 2001
From: Stephanie Freitag <stephanie.freitag@holi.team>
Date: Fri, 28 Mar 2025 18:19:17 +0100
Subject: [PATCH] HOLI-11059: synchronize pathname with deeplinks on mobile

---
 apps/mobile/navigation/NavigationProvider.tsx | 18 +++++++++++---
 .../__tests__/NavigationProvider.test.ts      | 24 ++++++++++++++++++-
 2 files changed, 38 insertions(+), 4 deletions(-)

diff --git a/apps/mobile/navigation/NavigationProvider.tsx b/apps/mobile/navigation/NavigationProvider.tsx
index f61c6eeb13..b1d0371a9a 100644
--- a/apps/mobile/navigation/NavigationProvider.tsx
+++ b/apps/mobile/navigation/NavigationProvider.tsx
@@ -1,18 +1,19 @@
 import * as Linking from 'expo-linking'
 import * as Notifications from 'expo-notifications'
 import { StatusBar } from 'expo-status-bar'
-import React from 'react'
+import type React from 'react'
 
 import type { LinkingOptions, Theme } from '@react-navigation/native'
 import { DefaultTheme, NavigationContainer, createNavigationContainerRef } from '@react-navigation/native'
 
 import { getLogger } from '@holi/core/helpers/logging'
-import { RootStackScreens } from '@holi/mobile/navigation/RootNavigator'
+import type { RootStackScreens } from '@holi/mobile/navigation/RootNavigator'
 import { appendLocaleSubPaths } from '@holi/mobile/navigation/helpers/appendLocaleSubPaths'
 import linkingConfig from '@holi/mobile/navigation/linkingConfig'
 import { screenUrlFromNotificationResponse } from '@holi/mobile/notifications'
 import { navigationIntegration } from '@holi/mobile/tracing'
-import { HoliTheme, useTheme } from '@holi/ui/styles/theme'
+import { type HoliTheme, useTheme } from '@holi/ui/styles/theme'
+import { setPathname } from '@holi/core/navigation/hooks/usePathname'
 
 const logger = getLogger('NavigationProvider')
 
@@ -28,6 +29,13 @@ const getNavigationTheme = (currentTheme: HoliTheme): Theme => ({
   dark: currentTheme.dark,
 })
 
+const syncPathname = (url: string) => {
+  const parsedUrl = Linking.parse(url)
+  if (parsedUrl?.path) {
+    setPathname('/' + parsedUrl.path)
+  }
+}
+
 // linking is done to configure deep linking (e.g. to get https://staging.dev.holi.social/profiles/dieter
 // or holi://profiles/dieter to open the corresponding screen on the app). See
 // https://reactnavigation.org/docs/deep-linking and https://reactnavigation.org/docs/configuring-links for details
@@ -49,6 +57,7 @@ export const rootLinkingConfig: LinkingOptions<RootStackScreens> = {
     const url = await Linking.getInitialURL()
     if (url != null) {
       logger.debug('LinkingOptions.getInitialURL', 'opening direct deep link', url)
+      syncPathname(url)
       return url
     }
 
@@ -62,6 +71,7 @@ export const rootLinkingConfig: LinkingOptions<RootStackScreens> = {
       const url = screenUrlFromNotificationResponse(response)
       if (url) {
         logger.debug('LinkingOptions.getInitialURL', 'navigate to deep link from push interaction', url)
+        syncPathname(url)
         return url
       }
     }
@@ -70,6 +80,7 @@ export const rootLinkingConfig: LinkingOptions<RootStackScreens> = {
     // listen to incoming links from deep linking, while app is active or in background
     const eventListenerSubscription = Linking.addEventListener('url', ({ url }: { url: string }) => {
       logger.debug('LinkingOptions.subscribe', 'received url from deeplinking', url)
+      syncPathname(url)
       listener(url)
     })
 
@@ -79,6 +90,7 @@ export const rootLinkingConfig: LinkingOptions<RootStackScreens> = {
       const url = screenUrlFromNotificationResponse(response)
       if (url) {
         logger.debug('LinkingOptions.subscribe', 'navigate to deep link from push interaction', url)
+        syncPathname(url)
         listener(url)
       }
     })
diff --git a/apps/mobile/navigation/__tests__/NavigationProvider.test.ts b/apps/mobile/navigation/__tests__/NavigationProvider.test.ts
index bdc20e1bad..93ac639e97 100644
--- a/apps/mobile/navigation/__tests__/NavigationProvider.test.ts
+++ b/apps/mobile/navigation/__tests__/NavigationProvider.test.ts
@@ -1,6 +1,7 @@
-import { NotificationResponse } from 'expo-notifications'
+import type { NotificationResponse } from 'expo-notifications'
 
 import { rootLinkingConfig } from '@holi/mobile/navigation/NavigationProvider'
+import { setPathname } from '@holi/core/navigation/hooks/usePathname'
 
 jest.mock('@holi/mobile/tracing', () => ({}))
 
@@ -14,6 +15,9 @@ jest.mock('expo-linking', () => ({
   createURL: (path: string) => path,
   getInitialURL: () => mockGetInitialURL?.(),
   addEventListener: (type: string, handler: unknown) => mockAddEventListener?.(type, handler),
+  parse: (url: string) => ({
+    path: url.replace('holi://', '').replace('https://app.holi.social/', ''),
+  }),
 }))
 
 let mockGetLastNotificationResponseAsync: jest.Mock | undefined
@@ -26,6 +30,11 @@ jest.mock('expo-notifications', () => ({
     mockAddNotificationResponseReceivedListener?.(listener),
 }))
 
+const mockSetPathname = jest.mocked(setPathname)
+jest.mock('@holi/core/navigation/hooks/usePathname', () => ({
+  setPathname: jest.fn(),
+}))
+
 const notificationResponseForRemoteTriggerAndroidMock: NotificationResponse = {
   notification: {
     request: {
@@ -201,6 +210,7 @@ describe('rootLinkingConfig', () => {
 
       expect(initialUrl).toEqual('https://app.holi.social/some/link')
       expect(mockGetInitialURL).toHaveBeenCalledTimes(1)
+      expect(mockSetPathname).toHaveBeenCalledWith('/some/link')
     })
 
     describe('Android', () => {
@@ -213,6 +223,7 @@ describe('rootLinkingConfig', () => {
 
         expect(initialUrl).toEqual('holi://chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
         expect(mockGetLastNotificationResponseAsync).toHaveBeenCalledTimes(1)
+        expect(mockSetPathname).toHaveBeenCalledWith('/chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
       })
 
       it('should call getLastNotificationResponseAsync and respond correctly on local trigger', async () => {
@@ -222,6 +233,7 @@ describe('rootLinkingConfig', () => {
 
         expect(initialUrl).toEqual('holi://chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
         expect(mockGetLastNotificationResponseAsync).toHaveBeenCalledTimes(1)
+        expect(mockSetPathname).toHaveBeenCalledWith('/chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
       })
     })
 
@@ -233,6 +245,7 @@ describe('rootLinkingConfig', () => {
 
         expect(initialUrl).toEqual('holi://chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
         expect(mockGetLastNotificationResponseAsync).toHaveBeenCalledTimes(1)
+        expect(mockSetPathname).toHaveBeenCalledWith('/chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
       })
 
       it('should call getLastNotificationResponseAsync and respond correctly on iOS local trigger', async () => {
@@ -242,6 +255,7 @@ describe('rootLinkingConfig', () => {
 
         expect(initialUrl).toEqual('holi://chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
         expect(mockGetLastNotificationResponseAsync).toHaveBeenCalledTimes(1)
+        expect(mockSetPathname).toHaveBeenCalledWith('/chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
       })
     })
   })
@@ -262,6 +276,7 @@ describe('rootLinkingConfig', () => {
       registeredListener({ url: 'https://app.holi.social/some/link' })
       expect(urlResultCatcher).toHaveBeenCalledTimes(1)
       expect(urlResultCatcher).toHaveBeenCalledWith('https://app.holi.social/some/link')
+      expect(mockSetPathname).toHaveBeenCalledWith('/some/link')
     })
 
     describe('Android', () => {
@@ -279,14 +294,17 @@ describe('rootLinkingConfig', () => {
         expect(urlResultCatcher).toHaveBeenCalledWith(
           'holi://chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social'
         )
+        expect(mockSetPathname).toHaveBeenCalledWith('/chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
 
         urlResultCatcher.mockClear()
+        mockSetPathname.mockClear()
 
         registeredListener(notificationResponseForLocalTriggerAndroidMock)
         expect(urlResultCatcher).toHaveBeenCalledTimes(1)
         expect(urlResultCatcher).toHaveBeenCalledWith(
           'holi://chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social'
         )
+        expect(mockSetPathname).toHaveBeenCalledWith('/chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
       })
     })
 
@@ -305,14 +323,17 @@ describe('rootLinkingConfig', () => {
         expect(urlResultCatcher).toHaveBeenCalledWith(
           'holi://chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social'
         )
+        expect(mockSetPathname).toHaveBeenCalledWith('/chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
 
         urlResultCatcher.mockClear()
+        mockSetPathname.mockClear()
 
         registeredListener(notificationResponseForLocalTriggerIosMock)
         expect(urlResultCatcher).toHaveBeenCalledTimes(1)
         expect(urlResultCatcher).toHaveBeenCalledWith(
           'holi://chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social'
         )
+        expect(mockSetPathname).toHaveBeenCalledWith('/chat/rooms/!fefGYdkrNAqHkNfbVh:development-chat.holi.social')
       })
     })
 
@@ -330,6 +351,7 @@ describe('rootLinkingConfig', () => {
 
       expect(removeMockForAddEventListener).toHaveBeenCalledTimes(1)
       expect(removeMockForAddNotificationResponseReceivedListener).toHaveBeenCalledTimes(1)
+      expect(mockSetPathname).not.toHaveBeenCalled()
     })
   })
 })
-- 
GitLab