/** Profile links: what QR codes and invites contain, and how scans are read back. */ const USERNAME = /^[a-z0-9_]{3,32}$/; /** Shareable profile URL on the website, e.g. https://musicshare.app/u/alice */ export function profileUrl(webBase: string, username: string): string { return `${webBase.replace(/\/+$/, '')}/u/${encodeURIComponent(username)}`; } /** * Pull a username out of anything a QR scan might contain: a website profile * URL (any host), the app's deep link (musicshare://u/alice), an Expo Go dev * link (exp://…/--/u/alice), or a bare "@alice". Returns null if it isn't one. */ export function parseProfileLink(text: string): string | null { const raw = text.trim(); const at = raw.match(/^@([A-Za-z0-9_]+)$/); if (at) return valid(at[1]!); let url: URL; try { url = new URL(raw); } catch { return null; } // musicshare://u/alice parses with host "u" and path "/alice" const path = url.protocol === 'musicshare:' ? `/${url.host}${url.pathname}` : url.pathname.replace(/^\/--/, ''); const match = path.match(/^\/u\/([^/?#]+)\/?$/); return match ? valid(decodeURIComponent(match[1]!)) : null; } function valid(name: string): string | null { const username = name.toLowerCase(); return USERNAME.test(username) ? username : null; }