Files
silo-server/web/src/lib/appDeepLink.ts
T
95edf19389 feat(invitations): shareable claim links and open-in-app on the claim page (#509)
* 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>
2026-07-28 15:01:23 -04:00

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)}`;
}