diff --git a/public/mobile-app/src/app.d.ts b/public/mobile-app/src/app.d.ts index f7959382a..1e36c5c31 100644 --- a/public/mobile-app/src/app.d.ts +++ b/public/mobile-app/src/app.d.ts @@ -11,6 +11,7 @@ declare global { interface Window { _paq?: any[]; NativeBridge?: any; + NativeURLs?: string[]; } } diff --git a/public/mobile-app/src/lib/notifications.ts b/public/mobile-app/src/lib/notifications.ts index 6183fac7e..ab252a5ee 100644 --- a/public/mobile-app/src/lib/notifications.ts +++ b/public/mobile-app/src/lib/notifications.ts @@ -38,7 +38,7 @@ export const fetchAndStoreNotifications = async () => { }; export const getNotificationsFromStore = async (): Promise => { - const notificationsString: string = localStorage.getItem('notifications') || ''; + const notificationsString: string = localStorage.getItem('notifications') || '[]'; return JSON.parse(notificationsString); }; diff --git a/public/mobile-app/src/routes/+layout.svelte b/public/mobile-app/src/routes/+layout.svelte index 0248b4650..c45ff8ab5 100644 --- a/public/mobile-app/src/routes/+layout.svelte +++ b/public/mobile-app/src/routes/+layout.svelte @@ -3,11 +3,12 @@ import '@gouvfr/dsfr/dist/utility/utility.min.css'; import '../app.css'; import { onMount } from 'svelte'; - import { afterNavigate, goto } from '$app/navigation'; + import { afterNavigate, beforeNavigate, goto } from '$app/navigation'; import { env } from '$env/dynamic/public'; import Toasts from '$lib/components/Toasts.svelte'; import { initDsfr } from '$lib/dsfr'; import { initMatomo, trackPageView } from '$lib/matomo'; + import { emit } from '$lib/nativeEvents'; let { children } = $props(); @@ -21,6 +22,21 @@ initMatomo(); }); + beforeNavigate((navigation) => { + const url = navigation.to?.url; + if (!url) { + return; + } + emit('navigateTo', url); + const path = url.href.replace(url.origin, ''); // Remove the `https://xxxx.yyy:zzzz` part of the current url, keep eg `/#/settings`. + if (window.NativeURLs?.includes(path)) { + console.log( + `Cancel navigation to ${url}, found an entry in the window.NativeURLs, let the mobile app handle it` + ); + navigation.cancel(); + } + }); + afterNavigate(() => { trackPageView(document.title); }); diff --git a/public/mobile-app/src/routes/layout.svelte.test.ts b/public/mobile-app/src/routes/layout.svelte.test.ts index a02f53c2b..f792734e6 100644 --- a/public/mobile-app/src/routes/layout.svelte.test.ts +++ b/public/mobile-app/src/routes/layout.svelte.test.ts @@ -24,6 +24,7 @@ describe('+layout.svelte', () => { afterEach(() => { vi.resetAllMocks(); delete window.NativeBridge; + delete window.NativeURLs; }); describe('whitelisting', () => { @@ -76,4 +77,48 @@ describe('+layout.svelte', () => { }); }); }); + + describe('cancelling navigation to pages promoted in mobile apps', () => { + test('should cancel navigation if the URL is in window.NativeURLs', () => { + // Given + let beforeNavigateCallback: (navigation: any) => void; + vi.spyOn(navigationMethods, 'beforeNavigate').mockImplementation((cb) => { + beforeNavigateCallback = cb; + }); + window.NativeURLs = ['/#/settings']; + render(Layout, { children: (() => {}) as any }); + + const cancel = vi.fn(); + + // When + beforeNavigateCallback!({ + to: { url: new URL('https://localhost:5173/#/settings') }, + cancel, + }); + + // Then + expect(cancel).toHaveBeenCalled(); + }); + + test('should not cancel navigation if the URL is not in window.NativeURLs', () => { + // Given + let beforeNavigateCallback: (navigation: any) => void; + vi.spyOn(navigationMethods, 'beforeNavigate').mockImplementation((cb) => { + beforeNavigateCallback = cb; + }); + window.NativeURLs = ['/#/settings']; + render(Layout, { children: (() => {}) as any }); + + const cancel = vi.fn(); + + // When + beforeNavigateCallback!({ + to: { url: new URL('https://localhost:5173/#/other') }, + cancel, + }); + + // Then + expect(cancel).not.toHaveBeenCalled(); + }); + }); });