feat: deeplink & vercel

This commit is contained in:
2025-11-04 17:08:59 +01:00
parent 673199ff87
commit ffaccd71a7
8 changed files with 246 additions and 16 deletions
+2 -1
View File
@@ -4,6 +4,7 @@
"slug": "musicland",
"name": "MusicLand",
"scheme": "musicland",
"deeplinks": ["musicland://"],
"version": "0.0.1",
"icon": "./assets/icon.png",
"backgroundColor": "#0F0C14",
@@ -25,7 +26,7 @@
"usesAppleSignIn": true,
"requireFullScreen": true,
"userInterfaceStyle": "dark",
"associatedDomains": ["applinks:minuit.starter"],
"associatedDomains": ["applinks:musicland-one.vercel.app"],
"bundleIdentifier": "com.omedis.musicland.ai",
"infoPlist": {
"NSMicrophoneUsageDescription": "MusicLand uses the microphone to record your voice and audio so you can create and collaborate on music projects.",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 86 KiB

+7 -5
View File
@@ -39,7 +39,9 @@ const ShareModal = ({ payload }) => {
const url = payload?.url ?? musiclandShareUrl;
const shareTitle = payload?.shareTitle ?? heading;
const shareMessage = payload?.shareMessage ?? musiclandShareMessage;
const linkLabel = payload?.linkLabel ?? url;
const copyUrl =
payload?.copyUrl ?? payload?.qrUrl ?? payload?.url ?? musiclandShareUrl;
const linkLabel = payload?.linkLabel ?? copyUrl;
const sectionTitle = payload?.sectionTitle ?? "Partager sur...";
const qrTitle = payload?.qrTitle ?? "Scannez le QR-code";
const qrUrl = payload?.qrUrl ?? musiclandShareUrl;
@@ -140,13 +142,13 @@ const ShareModal = ({ payload }) => {
}
if (isWeb && typeof navigator !== "undefined" && navigator.clipboard) {
await navigator.clipboard.writeText(url);
await navigator.clipboard.writeText(copyUrl);
} else {
const Clipboard = await import("expo-clipboard");
if (typeof Clipboard?.setStringAsync === "function") {
await Clipboard.setStringAsync(url);
await Clipboard.setStringAsync(copyUrl);
} else if (typeof Clipboard?.default?.setStringAsync === "function") {
await Clipboard.default.setStringAsync(url);
await Clipboard.default.setStringAsync(copyUrl);
}
}
@@ -157,7 +159,7 @@ const ShareModal = ({ payload }) => {
} catch (error) {
console.error("share.copy.failed", error);
}
}, [url]);
}, [copyUrl]);
const handleSystemShare = useCallback(async () => {
if (Platform.OS === "web" && typeof navigator !== "undefined") {
+31 -7
View File
@@ -102,19 +102,42 @@ const UniversalLinkProvider = ({ children }) => {
}, [isFullyLoaded, pendingMusicId]);
const handleParams = (path, _queryParams = {}) => {
if (typeof path === "string" && path.length > 0) {
const normalized = path.replace(/^\/+/, "");
const [first, second] = normalized.split("/");
if (first?.toLowerCase() === "music" && second) {
if ((typeof path === "string" && path.length > 0) || _queryParams?.music) {
const normalized = typeof path === "string" ? path.replace(/^\/+/, "") : "";
const segments = normalized
.split("/")
.map((segment) => segment.trim())
.filter(Boolean);
const musicIndex = segments.findIndex(
(segment) => segment?.toLowerCase() === "music"
);
let musicIdentifier = null;
if (musicIndex !== -1 && segments[musicIndex + 1]) {
musicIdentifier = segments[musicIndex + 1];
} else if (segments.length === 1 && segments[0]) {
musicIdentifier = segments[0];
} else if (typeof _queryParams?.music === "string") {
musicIdentifier = _queryParams.music;
} else if (typeof _queryParams?.projectId === "string") {
musicIdentifier = _queryParams.projectId;
}
if (musicIdentifier) {
try {
setPendingMusicId(decodeURIComponent(second));
setPendingMusicId(decodeURIComponent(musicIdentifier));
} catch (_) {
setPendingMusicId(second);
setPendingMusicId(musicIdentifier);
}
}
}
if (isWeb) {
setTimeout(cleanURL, 1500);
} else {
cleanURL();
}
};
const cleanURL = () => {
@@ -124,7 +147,8 @@ const UniversalLinkProvider = ({ children }) => {
// console.log(url);
url.search = "";
window.history.replaceState({}, document.title, url.origin.toString());
const next = `${url.origin}${url.pathname}${url.hash || ""}`;
window.history.replaceState({}, document.title, next);
}
};
+49 -1
View File
@@ -1,22 +1,65 @@
import { SheetManager } from "react-native-actions-sheet";
import appJson from "../../app.json";
import {
musiclandShareHeading,
musiclandShareMessage,
musiclandShareUrl,
} from "../data";
const APP_SCHEME = appJson?.expo?.scheme || "musicland";
const APP_SCHEME_BASE = `${APP_SCHEME}://app`;
const defaultPayload = {
heading: musiclandShareHeading,
url: musiclandShareUrl,
shareTitle: "MusicLand",
shareMessage: musiclandShareMessage,
qrUrl: musiclandShareUrl,
copyUrl: musiclandShareUrl,
linkLabel: musiclandShareUrl,
appLink: APP_SCHEME_BASE,
};
const trimTrailingSlash = (value) =>
typeof value === "string" ? value.replace(/\/+$/, "") : "";
const trimLeadingSlash = (value) =>
typeof value === "string" ? value.replace(/^\/+/, "") : "";
const buildQueryString = (params = {}) => {
const entries = Object.entries(params).filter(
([, value]) => value !== undefined && value !== null && value !== ""
);
if (!entries.length) {
return "";
}
const searchParams = new URLSearchParams();
entries.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((item) => {
searchParams.append(key, `${item}`);
});
} else {
searchParams.append(key, `${value}`);
}
});
const query = searchParams.toString();
return query ? `?${query}` : "";
};
const createSchemeDeepLink = (path = "", params = {}) => {
const sanitizedPath = trimLeadingSlash(path);
const basePath = sanitizedPath
? `${APP_SCHEME_BASE}/${sanitizedPath}`
: APP_SCHEME_BASE;
return `${basePath}${buildQueryString(params)}`;
};
const resolveBaseShareUrl = () => {
if (typeof window !== "undefined" && window.location?.origin) {
const origin = window.location.origin;
@@ -32,7 +75,10 @@ export const createMusicSharePayload = ({ projectId, title, artist } = {}) => {
const baseUrl = trimTrailingSlash(resolveBaseShareUrl());
if (!baseUrl) return null;
const shareUrl = `${baseUrl}/music/${projectId}`;
const encodedProjectId = encodeURIComponent(projectId);
const sharePath = `music/${encodedProjectId}`;
const shareUrl = `${baseUrl}/${sharePath}`;
const schemeDeepLink = createSchemeDeepLink(sharePath);
let shareMessage = musiclandShareMessage;
if (title) {
@@ -49,7 +95,9 @@ export const createMusicSharePayload = ({ projectId, title, artist } = {}) => {
shareMessage,
url: shareUrl,
qrUrl: shareUrl,
copyUrl: shareUrl,
linkLabel: shareUrl,
appLink: schemeDeepLink,
};
};
+16 -1
View File
@@ -1,3 +1,18 @@
{
"rewrites": [{ "source": "/tasks/:taskId", "destination": "/index.html" }]
"rewrites": [
{ "source": "/music/:musicId", "destination": "/index.html" },
{ "source": "/tasks/:taskId", "destination": "/index.html" },
{ "source": "/playback/:playbackId", "destination": "/index.html" }
],
"headers": [
{
"source": "/.well-known/apple-app-site-association",
"headers": [
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
}
]
}
]
}
@@ -0,0 +1,18 @@
{
"applinks": {
"apps": [],
"details": [
{
"appID": "W2QZ9CTMYJ.com.omedis.musicland.ai",
"paths": [
"/music/*"
]
}
]
},
"webcredentials": {
"apps": [
"W2QZ9CTMYJ.com.omedis.musicland.ai"
]
}
}
+122
View File
@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, user-scalable=no, viewport-fit=cover"
/>
<meta name="description" content="MusicLand" />
<meta
name="keywords"
content="musicland, musique, IA, création musicale, minuit.agency"
/>
<meta name="author" content="minuit.agency" />
<title>MusicLand</title>
<style>
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&display=swap");
::-webkit-scrollbar {
display: none;
}
html,
body,
#root {
width: 100%;
min-height: 100vh;
min-height: 100dvh;
margin: 0;
padding: 0;
-webkit-overflow-scrolling: touch;
overflow: -moz-scrollbars-none;
scrollbar-width: none;
}
#root {
display: flex;
flex: 1;
flex-basis: auto;
flex-grow: 1;
flex-shrink: 0;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
}
html {
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
height: calc(100% + env(safe-area-inset-top));
}
body {
display: flex;
overflow-y: auto;
overscroll-behavior-y: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-ms-overflow-style: scrollbar;
}
</style>
</head>
<body>
<noscript>
<form
action=""
style="
background-color: #fff;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
"
>
<div
style="
font-size: 18px;
font-family: Helvetica, sans-serif;
line-height: 24px;
margin: 10%;
width: 80%;
"
>
<p>
Oh non ! Il semble que JavaScript ne soit pas activé sur votre
navigateur.
</p>
<p style="margin: 20px 0">
<button
type="submit"
style="
background-color: #4630eb;
border-radius: 100px;
border: none;
box-shadow: none;
color: #fff;
cursor: pointer;
font-weight: bold;
line-height: 20px;
padding: 6px 16px;
"
>
Recharger
</button>
</p>
</div>
</form>
</noscript>
<div id="root"></div>
</body>
</html>