Files
musicland/src/providers/UniversalLinkProvider.js
T
2025-11-05 10:15:50 +01:00

258 lines
7.5 KiB
JavaScript

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;