diff --git a/src/providers/UniversalLinkProvider.js b/src/providers/UniversalLinkProvider.js index 78882f6..76fb21d 100644 --- a/src/providers/UniversalLinkProvider.js +++ b/src/providers/UniversalLinkProvider.js @@ -33,6 +33,92 @@ const UniversalLinkProvider = ({ children }) => { 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); @@ -127,6 +213,11 @@ const UniversalLinkProvider = ({ children }) => { 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; diff --git a/src/screens/Playbacks/Playbacks.js b/src/screens/Playbacks/Playbacks.js index 0fb01b8..f31b273 100644 --- a/src/screens/Playbacks/Playbacks.js +++ b/src/screens/Playbacks/Playbacks.js @@ -8,7 +8,7 @@ import React, { useRef, useState, } 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 { responsiveHeight } from "react-native-responsive-dimensions"; import { icons, img } from "../../assets"; @@ -28,6 +28,10 @@ import { LIKE_TARGET, toggleProjectLike, } from "../../utils/likes"; +import { + createPlaybackSharePayload, + openShareSheet, +} from "../../utils/shareSheet"; import { SheetManager } from "react-native-actions-sheet"; 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 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) => { p.loop = false; @@ -182,7 +192,23 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => { return () => global.clearInterval(id); }, [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 ( { )} - { - 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) {} - }} - > + diff --git a/src/screens/Playbacks/components/PlaybackItem.web.js b/src/screens/Playbacks/components/PlaybackItem.web.js index 6fcd400..bc61724 100644 --- a/src/screens/Playbacks/components/PlaybackItem.web.js +++ b/src/screens/Playbacks/components/PlaybackItem.web.js @@ -9,7 +9,6 @@ import React, { import { Image, Pressable, - Share, StyleSheet, Text, View, @@ -30,6 +29,10 @@ import { LIKE_TARGET, toggleProjectLike, } from "../../../utils/likes"; +import { + createPlaybackSharePayload, + openShareSheet, +} from "../../../utils/shareSheet"; import { SheetManager } from "react-native-actions-sheet"; import { Feather } from "@expo/vector-icons"; @@ -254,6 +257,30 @@ const PlaybackItem = ({ const descriptionText = 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(() => { if (!item?.id) return; SheetManager.show("Report", { @@ -481,18 +508,7 @@ const PlaybackItem = ({ )} { - 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) {} - }} + onPress={handleShare} style={[styles.actionButton, styles.actionSpacing]} > { }; }; +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 = {}) => { SheetManager.show("Share", { payload: { diff --git a/vercel.json b/vercel.json index 8fe7994..fbdfefb 100644 --- a/vercel.json +++ b/vercel.json @@ -1,8 +1,11 @@ { "rewrites": [ + { "source": "/link", "destination": "/index.html" }, + { "source": "/link/:path*", "destination": "/index.html" }, { "source": "/music/:musicId", "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": [ {