import * as Linking from "expo-linking"; import { useContext, useEffect, useRef, useState } from "reactn"; import { appleAppStoreUrl } from "../data"; import { isWeb } from "../hooks/useLayoutType"; import { Routes } from "../navigation"; import { navigate, navigateToTask } from "../navigation/NavigationService"; import { SplashAnimationContext } from "./SplashAnimationProvider"; import { UserDataContext } from "./UserDataProvider"; const APP_SCHEME = "musicland"; const APP_PATH_PREFIX = "app"; const APP_STORE_FALLBACK_URL = appleAppStoreUrl; const normalizePath = (rawPath = "") => { if (!rawPath) return ""; const trimmed = rawPath.replace(/^\//, ""); if (trimmed.startsWith(`${APP_PATH_PREFIX}/`)) { return trimmed.slice(APP_PATH_PREFIX.length + 1); } return trimmed; }; const buildAppSchemeUrl = (rawPath, queryParams = {}) => { const path = normalizePath(rawPath); const fullPath = path ? `${APP_PATH_PREFIX}/${path}` : APP_PATH_PREFIX; const queryString = new URLSearchParams(queryParams ?? {}).toString(); return `${APP_SCHEME}://${fullPath}${queryString ? `?${queryString}` : ""}`; }; const UniversalLinkProvider = ({ children }) => { const { currentUID } = useContext(UserDataContext); const { isFullyLoaded } = useContext(SplashAnimationContext); const [pendingNavigation, setPendingNavigation] = useState(null); const lastUrlRef = useRef(null); const handleLinkRedirect = async (queryParams = {}) => { const schemeParam = queryParams?.scheme; const fallbackParam = queryParams?.fallback; const scheme = typeof schemeParam === "string" && schemeParam.trim().length > 0 ? schemeParam.trim() : null; const fallback = typeof fallbackParam === "string" && fallbackParam.trim().length > 0 ? fallbackParam.trim() : null; if (isWeb) { if (scheme) { let fallbackTimer = null; if (fallback) { fallbackTimer = window.setTimeout(() => { try { window.location.href = fallback; } catch (error) { console.error("link.redirect.fallback.error", error); } }, 1500); } try { window.location.href = scheme; } catch (error) { console.error("link.redirect.scheme.error", error); if (fallbackTimer) window.clearTimeout(fallbackTimer); if (fallback) { try { window.location.href = fallback; } catch (fallbackError) { console.error("link.redirect.fallback.error", fallbackError); } } } return; } if (fallback) { try { window.location.href = fallback; } catch (error) { console.error("link.redirect.fallback.error", error); } } return; } if (!scheme && fallback) { try { await Linking.openURL(fallback); } catch (error) { console.error("link.redirect.native.fallback.error", error); } return; } if (!scheme) return; try { const canOpen = await Linking.canOpenURL(scheme); if (canOpen) { await Linking.openURL(scheme); } else if (fallback) { await Linking.openURL(fallback); } else { await Linking.openURL(APP_STORE_FALLBACK_URL); } } catch (error) { console.error("link.redirect.native.scheme.error", error); try { if (fallback) { await Linking.openURL(fallback); } else { await Linking.openURL(APP_STORE_FALLBACK_URL); } } catch (fallbackError) { console.error("link.redirect.native.fallback.error", fallbackError); } } }; useEffect(() => { const handleDeepLink = async (event) => { // console.log("event", event); const incomingUrl = event?.url || ""; if (!incomingUrl) return; // Prevent handling the exact same URL multiple times if (lastUrlRef.current === incomingUrl) return; lastUrlRef.current = incomingUrl; const { path = "", queryParams = {} } = Linking.parse(incomingUrl) || {}; // console.log("path", path); // console.log("queryParams", queryParams); const appSchemeURL = buildAppSchemeUrl(path, queryParams); if (isWeb) { const userAgent = navigator.userAgent || navigator.vendor || window.opera; const isIOSBrowser = /iPad|iPhone|iPod/.test(userAgent) || (/Macintosh/.test(userAgent) && navigator.maxTouchPoints && navigator.maxTouchPoints > 2); if (isIOSBrowser) { try { const canOpen = await Linking.canOpenURL(appSchemeURL); if (canOpen) { window.location.href = appSchemeURL; } else { window.location.href = APP_STORE_FALLBACK_URL; } } catch (error) { console.error("Redirection error:", error); handleParams(path, queryParams); } } else { handleParams(path, queryParams); } } else { handleParams(path, queryParams); } }; const init = async () => { try { const initialUrl = await Linking.getInitialURL(); if (initialUrl) { // Handle initial URL once handleDeepLink({ url: initialUrl }); } } catch (e) {} // Subscribe to future app-open deep links const sub = Linking.addEventListener("url", handleDeepLink); return () => { try { sub?.remove?.(); } catch (e) { // Fallback for older expo-linking versions Linking.removeEventListener?.("url", handleDeepLink); } }; }; let cleanup; init().then((c) => { cleanup = c; }); return () => cleanup?.(); }, []); useEffect(() => { if (!pendingNavigation || !isFullyLoaded) return; if (pendingNavigation.type === "task") { if (!currentUID) return; navigateToTask(pendingNavigation.params); setPendingNavigation(null); return; } if (pendingNavigation.type === "music") { navigate(Routes.MusicDetails, pendingNavigation.params); setPendingNavigation(null); return; } if (pendingNavigation.type === "playback") { navigate(Routes.Playbacks, pendingNavigation.params); setPendingNavigation(null); } }, [pendingNavigation, currentUID, isFullyLoaded]); const handleParams = (path, queryParams = {}) => { const normalizedPath = normalizePath(path); if (!normalizedPath) return; if (normalizedPath.toLowerCase() === "link") { handleLinkRedirect(queryParams); return; } const [resource, identifier] = normalizedPath.split("/").filter(Boolean); if (!resource || !identifier) return; const resourceKey = resource.toLowerCase(); if (resourceKey === "tasks" || resourceKey === "task") { setPendingNavigation({ type: "task", params: { taskID: identifier, ...queryParams }, }); return; } if (resourceKey === "music" || resourceKey === "musics") { setPendingNavigation({ type: "music", params: { projectId: identifier, ...queryParams }, }); return; } if (resourceKey === "playback" || resourceKey === "playbacks") { setPendingNavigation({ type: "playback", params: { projectId: identifier, playbackId: identifier, focusId: identifier, ...queryParams, }, }); return; } }; return children; }; export default UniversalLinkProvider;