From 272088208630c094b0cee1086a664c533b8d342e Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Wed, 29 Jan 2025 23:00:34 -0600 Subject: [PATCH] fix: deeplink can be flakey (#1831) --- .../java/app/vger/voyager/MainActivity.java | 8 ++- .../ActionRequestHandler.swift | 4 ++ src/core/listeners/AppUrlListener.tsx | 54 ++++++++++++++----- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/android/app/src/main/java/app/vger/voyager/MainActivity.java b/android/app/src/main/java/app/vger/voyager/MainActivity.java index 72966b9762..bc1ad25792 100644 --- a/android/app/src/main/java/app/vger/voyager/MainActivity.java +++ b/android/app/src/main/java/app/vger/voyager/MainActivity.java @@ -59,7 +59,13 @@ protected void onNewIntent(Intent intent) { return; } - newIntent.setData(Uri.parse(potentialUrl)); + Uri uri = Uri.parse(potentialUrl); + + // Add a timestamp to the URL to let app know it is not stale + Uri.Builder uriBuilder = uri.buildUpon(); + long currentTime = System.currentTimeMillis() / 1000L; + uriBuilder.appendQueryParameter("t", String.valueOf(currentTime)); + newIntent.setData(uriBuilder.build()); bridge.onNewIntent(newIntent); } diff --git a/ios/App/VoyagerActionExtension/ActionRequestHandler.swift b/ios/App/VoyagerActionExtension/ActionRequestHandler.swift index 1b42aa0b39..c6a79253a2 100644 --- a/ios/App/VoyagerActionExtension/ActionRequestHandler.swift +++ b/ios/App/VoyagerActionExtension/ActionRequestHandler.swift @@ -62,6 +62,10 @@ extension URL { var iceCubesAppDeepLink: URL { var components = URLComponents(url: self, resolvingAgainstBaseURL: false)! components.scheme = "vger" + + // Add a timestamp so the app can check if the deep link is stale or not + components.queryItems = (components.queryItems ?? []) + [URLQueryItem(name: "t", value: String(Int(Date().timeIntervalSince1970)))] + return components.url! } } diff --git a/src/core/listeners/AppUrlListener.tsx b/src/core/listeners/AppUrlListener.tsx index 468132d5b0..0f4e61259f 100644 --- a/src/core/listeners/AppUrlListener.tsx +++ b/src/core/listeners/AppUrlListener.tsx @@ -11,7 +11,17 @@ import { deepLinkFailed } from "#/helpers/toastMessages"; import useAppToast from "#/helpers/useAppToast"; import { useAppSelector } from "#/store"; +const PREVIOUS_APP_URL_STORAGE_KEY = "previous-app-url"; + +(async () => { + const response = await App.getLaunchUrl(); + + // If normal startup, remove previous app url + if (!response?.url) localStorage.removeItem(PREVIOUS_APP_URL_STORAGE_KEY); +})(); + export default function AppUrlListener() { + const presentToast = useAppToast(); const { redirectToLemmyObjectIfNeeded } = useLemmyUrlHandler(); const knownInstances = useAppSelector( (state) => state.instances.knownInstances, @@ -21,8 +31,7 @@ export default function AppUrlListener() { ); const deepLinkReady = useAppSelector((state) => state.deepLinkReady.ready); - const appUrlToOpen = useRef(); - const presentToast = useAppToast(); + const appUrlFromEventRef = useRef(); const notReady = !knownInstances || @@ -30,24 +39,43 @@ export default function AppUrlListener() { !connectedInstance || !deepLinkReady; - const onAppUrl = async (url: string) => { - if (notReady) { - appUrlToOpen.current = url; + const onAppUrlIfNeeded = async () => { + // wait for router to get into a good state before pushing + // (needed for pushing user profiles from app startup) + if (notReady) return; + + // If appUrl received from explicit event, redirect to it + if (appUrlFromEventRef.current) { + redirectTo(appUrlFromEventRef.current); + appUrlFromEventRef.current = undefined; return; } - // wait for router to get into a good state before pushing - // (needed for pushing user profiles from app startup) - const result = await redirectToLemmyObjectIfNeeded(url); + const url = (await App.getLaunchUrl())?.url; - if (result === "not-found") presentToast(deepLinkFailed); + if (!url) return; + + // If appUrl received from launch, redirect to it if it's not stale + // (the appUrl could be stale if web process was killed, but native wrapper wasn't) + if (url === localStorage.getItem(PREVIOUS_APP_URL_STORAGE_KEY)) return; + + redirectTo(url); }; - const onAppUrlEvent = useEffectEvent(onAppUrl); + async function redirectTo(url: string) { + const result = await redirectToLemmyObjectIfNeeded(normalizeObjectUrl(url)); + + if (result === "not-found") presentToast(deepLinkFailed); + + localStorage.setItem(PREVIOUS_APP_URL_STORAGE_KEY, url); + } + + const onAppUrlIfNeededEvent = useEffectEvent(onAppUrlIfNeeded); useEffect(() => { const listener = App.addListener("appUrlOpen", (event) => { - onAppUrlEvent(normalizeObjectUrl(event.url)); + appUrlFromEventRef.current = event.url; + onAppUrlIfNeededEvent(); }); return () => { @@ -57,10 +85,8 @@ export default function AppUrlListener() { useEffect(() => { if (notReady) return; - if (!appUrlToOpen.current) return; - onAppUrlEvent(appUrlToOpen.current); - appUrlToOpen.current = undefined; + onAppUrlIfNeededEvent(); }, [notReady]); return null;