fix: link provider
This commit is contained in:
@@ -33,6 +33,92 @@ const UniversalLinkProvider = ({ children }) => {
|
|||||||
const [pendingNavigation, setPendingNavigation] = useState(null);
|
const [pendingNavigation, setPendingNavigation] = useState(null);
|
||||||
const lastUrlRef = useRef(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(() => {
|
useEffect(() => {
|
||||||
const handleDeepLink = async (event) => {
|
const handleDeepLink = async (event) => {
|
||||||
// console.log("event", event);
|
// console.log("event", event);
|
||||||
@@ -127,6 +213,11 @@ const UniversalLinkProvider = ({ children }) => {
|
|||||||
const normalizedPath = normalizePath(path);
|
const normalizedPath = normalizePath(path);
|
||||||
if (!normalizedPath) return;
|
if (!normalizedPath) return;
|
||||||
|
|
||||||
|
if (normalizedPath.toLowerCase() === "link") {
|
||||||
|
handleLinkRedirect(queryParams);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [resource, identifier] = normalizedPath.split("/").filter(Boolean);
|
const [resource, identifier] = normalizedPath.split("/").filter(Boolean);
|
||||||
if (!resource || !identifier) return;
|
if (!resource || !identifier) return;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Image, Platform, Pressable, Share, Text, View } from "react-native";
|
import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||||
import Carousel from "react-native-reanimated-carousel";
|
import Carousel from "react-native-reanimated-carousel";
|
||||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||||
import { icons, img } from "../../assets";
|
import { icons, img } from "../../assets";
|
||||||
@@ -28,6 +28,10 @@ import {
|
|||||||
LIKE_TARGET,
|
LIKE_TARGET,
|
||||||
toggleProjectLike,
|
toggleProjectLike,
|
||||||
} from "../../utils/likes";
|
} from "../../utils/likes";
|
||||||
|
import {
|
||||||
|
createPlaybackSharePayload,
|
||||||
|
openShareSheet,
|
||||||
|
} from "../../utils/shareSheet";
|
||||||
import { SheetManager } from "react-native-actions-sheet";
|
import { SheetManager } from "react-native-actions-sheet";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
|
||||||
@@ -109,7 +113,13 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
|||||||
|
|
||||||
const projectTitle = typeof item?.title === "string" ? item.title.trim() : "";
|
const projectTitle = typeof item?.title === "string" ? item.title.trim() : "";
|
||||||
|
|
||||||
const creatorName = item?.userName;
|
const creatorName = useMemo(() => {
|
||||||
|
if (owner?.displayName) return owner.displayName;
|
||||||
|
if (owner?.artistName) return owner.artistName;
|
||||||
|
if (owner?.userName) return owner.userName;
|
||||||
|
if (typeof item?.userName === "string") return item.userName;
|
||||||
|
return "";
|
||||||
|
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName]);
|
||||||
|
|
||||||
const videoPlayer = useVideoPlayer(videoUrl || null, (p) => {
|
const videoPlayer = useVideoPlayer(videoUrl || null, (p) => {
|
||||||
p.loop = false;
|
p.loop = false;
|
||||||
@@ -182,7 +192,23 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
|||||||
return () => global.clearInterval(id);
|
return () => global.clearInterval(id);
|
||||||
}, [isActive, videoPlayer]);
|
}, [isActive, videoPlayer]);
|
||||||
|
|
||||||
// partage: géré via l'API native Share directement dans l'UI
|
const sharePayload = useMemo(() => {
|
||||||
|
if (!item?.id) return null;
|
||||||
|
return createPlaybackSharePayload({
|
||||||
|
projectId: item.id,
|
||||||
|
title: projectTitle || undefined,
|
||||||
|
artist: creatorName || undefined,
|
||||||
|
playbackUrl: videoUrl || undefined,
|
||||||
|
});
|
||||||
|
}, [creatorName, item?.id, projectTitle, videoUrl]);
|
||||||
|
|
||||||
|
const handleShare = useCallback(() => {
|
||||||
|
if (sharePayload) {
|
||||||
|
openShareSheet(sharePayload);
|
||||||
|
}
|
||||||
|
}, [sharePayload]);
|
||||||
|
|
||||||
|
// partage: via feuille unifiée (prend en charge QR)
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -358,25 +384,7 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable onPress={handleShare}>
|
||||||
onPress={async () => {
|
|
||||||
try {
|
|
||||||
const url = item?.playbackUrl || "";
|
|
||||||
const title = item?.title || "Partager";
|
|
||||||
const base = item?.title
|
|
||||||
? `Découvre « ${item.title} » sur MusicLand`
|
|
||||||
: `Découvre ce playback sur MusicLand`;
|
|
||||||
const message = url ? `${base}\n${url}` : base;
|
|
||||||
|
|
||||||
await Share.share(
|
|
||||||
Platform.select({
|
|
||||||
ios: url ? { url, message, title } : { message, title },
|
|
||||||
default: { message, title },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image source={icons.share} style={size({ size: 26 })} />
|
<Image source={icons.share} style={size({ size: 26 })} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable onPress={handleReport} style={{ alignItems: "center" }}>
|
<Pressable onPress={handleReport} style={{ alignItems: "center" }}>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import React, {
|
|||||||
import {
|
import {
|
||||||
Image,
|
Image,
|
||||||
Pressable,
|
Pressable,
|
||||||
Share,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
@@ -30,6 +29,10 @@ import {
|
|||||||
LIKE_TARGET,
|
LIKE_TARGET,
|
||||||
toggleProjectLike,
|
toggleProjectLike,
|
||||||
} from "../../../utils/likes";
|
} from "../../../utils/likes";
|
||||||
|
import {
|
||||||
|
createPlaybackSharePayload,
|
||||||
|
openShareSheet,
|
||||||
|
} from "../../../utils/shareSheet";
|
||||||
import { SheetManager } from "react-native-actions-sheet";
|
import { SheetManager } from "react-native-actions-sheet";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
|
||||||
@@ -254,6 +257,30 @@ const PlaybackItem = ({
|
|||||||
const descriptionText =
|
const descriptionText =
|
||||||
item?.description || item?.title || "Description chanson";
|
item?.description || item?.title || "Description chanson";
|
||||||
|
|
||||||
|
const creatorName = useMemo(() => {
|
||||||
|
if (owner?.displayName) return owner.displayName;
|
||||||
|
if (owner?.artistName) return owner.artistName;
|
||||||
|
if (owner?.userName) return owner.userName;
|
||||||
|
if (typeof item?.userName === "string") return item.userName;
|
||||||
|
return "";
|
||||||
|
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName]);
|
||||||
|
|
||||||
|
const sharePayload = useMemo(() => {
|
||||||
|
if (!item?.id) return null;
|
||||||
|
return createPlaybackSharePayload({
|
||||||
|
projectId: item.id,
|
||||||
|
title: typeof item?.title === "string" ? item.title.trim() : undefined,
|
||||||
|
artist: creatorName || undefined,
|
||||||
|
playbackUrl: videoUrl || undefined,
|
||||||
|
});
|
||||||
|
}, [creatorName, item?.id, item?.title, videoUrl]);
|
||||||
|
|
||||||
|
const handleShare = useCallback(() => {
|
||||||
|
if (sharePayload) {
|
||||||
|
openShareSheet(sharePayload);
|
||||||
|
}
|
||||||
|
}, [sharePayload]);
|
||||||
|
|
||||||
const handleReport = useCallback(() => {
|
const handleReport = useCallback(() => {
|
||||||
if (!item?.id) return;
|
if (!item?.id) return;
|
||||||
SheetManager.show("Report", {
|
SheetManager.show("Report", {
|
||||||
@@ -481,18 +508,7 @@ const PlaybackItem = ({
|
|||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={async () => {
|
onPress={handleShare}
|
||||||
try {
|
|
||||||
const url = item?.playbackUrl || "";
|
|
||||||
const title = item?.title || "Partager";
|
|
||||||
const base = item?.title
|
|
||||||
? `Découvre « ${item.title} » sur MusicLand`
|
|
||||||
: `Découvre ce playback sur MusicLand`;
|
|
||||||
const message = url ? `${base}\n${url}` : base;
|
|
||||||
|
|
||||||
await Share.share({ message, title, url });
|
|
||||||
} catch (_e) {}
|
|
||||||
}}
|
|
||||||
style={[styles.actionButton, styles.actionSpacing]}
|
style={[styles.actionButton, styles.actionSpacing]}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -135,6 +135,48 @@ export const createMusicSharePayload = ({ projectId, title, artist } = {}) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createPlaybackSharePayload = ({
|
||||||
|
projectId,
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
playbackUrl,
|
||||||
|
} = {}) => {
|
||||||
|
if (!projectId) return null;
|
||||||
|
|
||||||
|
const baseUrl = trimTrailingSlash(resolveBaseShareUrl());
|
||||||
|
if (!baseUrl) return null;
|
||||||
|
const encodedProjectId = encodeURIComponent(projectId);
|
||||||
|
const sharePath = `playback/${encodedProjectId}`;
|
||||||
|
const shareUrl = `${baseUrl}/${sharePath}`;
|
||||||
|
const schemeDeepLink = createSchemeDeepLink(sharePath);
|
||||||
|
const qrUrl = createShareQrUrl({
|
||||||
|
sharePath,
|
||||||
|
fallbackUrl: playbackUrl || shareUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
let shareMessage = "Découvre ce playback sur MusicLand.";
|
||||||
|
if (title) {
|
||||||
|
shareMessage = `Découvre "${title}"`;
|
||||||
|
if (artist) {
|
||||||
|
shareMessage += ` par ${artist}`;
|
||||||
|
}
|
||||||
|
shareMessage += " sur MusicLand.";
|
||||||
|
} else if (artist) {
|
||||||
|
shareMessage = `Découvre ce playback de ${artist} sur MusicLand.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
heading: "Partager le playback",
|
||||||
|
shareTitle: title ? `${title} - MusicLand` : "MusicLand",
|
||||||
|
shareMessage,
|
||||||
|
url: shareUrl,
|
||||||
|
qrUrl,
|
||||||
|
copyUrl: shareUrl,
|
||||||
|
linkLabel: shareUrl,
|
||||||
|
appLink: schemeDeepLink,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const openShareSheet = (payload = {}) => {
|
export const openShareSheet = (payload = {}) => {
|
||||||
SheetManager.show("Share", {
|
SheetManager.show("Share", {
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
+4
-1
@@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"rewrites": [
|
"rewrites": [
|
||||||
|
{ "source": "/link", "destination": "/index.html" },
|
||||||
|
{ "source": "/link/:path*", "destination": "/index.html" },
|
||||||
{ "source": "/music/:musicId", "destination": "/index.html" },
|
{ "source": "/music/:musicId", "destination": "/index.html" },
|
||||||
{ "source": "/tasks/:taskId", "destination": "/index.html" },
|
{ "source": "/tasks/:taskId", "destination": "/index.html" },
|
||||||
{ "source": "/playback/:playbackId", "destination": "/index.html" }
|
{ "source": "/playback/:playbackId", "destination": "/index.html" },
|
||||||
|
{ "source": "/playbacks/:playbackId", "destination": "/index.html" }
|
||||||
],
|
],
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user