* feat(invitations): always return the claim link so admins can share it directly The claim URL was only surfaced when email sending failed. Admins who want to hand the link over another channel (chat, SMS) had no way to get it — and the raw token exists only in the send/resend response, since the server stores just its hash. The create and resend flows now always include claim_url (additive on /api/v1), and the admin UI keeps the dialog open after either action with the link and a labeled Copy button. Truncation and stacked buttons keep the unbreakable URL from forcing horizontal scroll on phone widths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): offer to open invite claims in the Android app The Android app already registers silo://invite?server=...&token=... with a full native claim flow, but nothing ever emitted that link — an https invite always ended in the browser. On Android user agents the claim page now leads with a prominent 'Open in the Silo app' button carrying that deep link, with the web form kept below as the fallback ('or set up in the browser'). The button is a plain anchor: a user-tapped custom-scheme link is the one reliable path, and we never fire it automatically since there is no installed-check and a miss surfaces an OS error. The password field's autofocus is suppressed alongside it so the keyboard doesn't push the button off screen. iOS is excluded until the Apple app registers the scheme. The server origin travels in the server param verbatim, so non-443 ports and plain-http LAN servers need no extra convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
44 lines
1.8 KiB
TypeScript
44 lines
1.8 KiB
TypeScript
/**
|
|
* Deep links into the native Silo apps via the silo:// custom scheme.
|
|
*
|
|
* Silo is self-hosted, so the store apps cannot pre-verify every server's
|
|
* domain for App Links / Universal Links; a custom scheme is the only
|
|
* universal way in. The Android app already registers
|
|
* `silo://invite?server=<url>&token=<token>` (see silo-android
|
|
* InviteClaimRouteParser.kt and its navDeepLink) — this module emits that
|
|
* exact contract, with `server` carrying the full origin so non-443 ports
|
|
* and plain-http LAN servers need no extra convention.
|
|
*
|
|
* Custom-scheme URLs don't linkify in email or SMS and error when the app
|
|
* is missing, so they are never sent anywhere: they only back an explicit
|
|
* in-page button, rendered on platforms with a native app.
|
|
*/
|
|
|
|
export type MobilePlatform = "android" | "ios";
|
|
|
|
/** Detects a platform with a native Silo app from the user agent. */
|
|
export function detectMobilePlatform(ua: string): MobilePlatform | null {
|
|
// iPadOS 13+ Safari masquerades as macOS; maxTouchPoints tells it apart,
|
|
// but that's a live-DOM concern — callers pass a UA and we keep this pure.
|
|
if (/android/i.test(ua)) return "android";
|
|
if (/iphone|ipad|ipod/i.test(ua)) return "ios";
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Builds the silo:// deep link that opens the native invite claim flow.
|
|
* Returns null for origins the apps can't talk to (non-http(s), userinfo).
|
|
*/
|
|
export function buildInviteDeepLink(pageOrigin: string, token: string): string | null {
|
|
let origin: URL;
|
|
try {
|
|
origin = new URL(pageOrigin);
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (origin.username || origin.password) return null;
|
|
if (origin.protocol !== "https:" && origin.protocol !== "http:") return null;
|
|
const server = encodeURIComponent(origin.origin);
|
|
return `silo://invite?server=${server}&token=${encodeURIComponent(token)}`;
|
|
}
|