258 lines
7.5 KiB
JavaScript
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;
|