feat: relay invite links (mint + claim + landing page + deep link) (#1668)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: npub1k766u8wq088nkr9vy94uqlh9kmw5956g593a7xk7cks7frhgctmsr00p27 <b7b5ae1dc079cf3b0cac216bc07ee5b6dd42d348a163df1adec5a1e48ee8c2f7@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1k766u8wq088nkr9vy94uqlh9kmw5956g593a7xk7cks7frhgctmsr00p27 <b7b5ae1dc079cf3b0cac216bc07ee5b6dd42d348a163df1adec5a1e48ee8c2f7@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Bradley Axen
2026-07-10 12:06:35 -07:00
committed by GitHub
co-authored by npub1k766u8wq088nkr9vy94uqlh9kmw5956g593a7xk7cks7frhgctmsr00p27 Will Pfleger
parent b97777350d
commit 2e529aab75
18 changed files with 1586 additions and 9 deletions
+37 -3
View File
@@ -8,6 +8,7 @@ import { Route as rootRouteImport } from "./routes/root";
import { Route as reposRouteImport } from "./routes/repos";
import { Route as indexRouteImport } from "./routes/index";
import { Route as reposDotrepoIdRouteImport } from "./routes/repos.$repoId";
import { Route as inviteDotcodeRouteImport } from "./routes/invite.$code";
import { Route as reposDotrepoIdDotblobDotsplatRouteImport } from "./routes/repos.$repoId.blob.$";
const reposRoute = reposRouteImport.update({
@@ -25,6 +26,11 @@ const reposDotrepoIdRoute = reposDotrepoIdRouteImport.update({
path: "/repos/$repoId",
getParentRoute: () => rootRouteImport,
} as any);
const inviteDotcodeRoute = inviteDotcodeRouteImport.update({
id: "/invite/$code",
path: "/invite/$code",
getParentRoute: () => rootRouteImport,
} as any);
const reposDotrepoIdDotblobDotsplatRoute =
reposDotrepoIdDotblobDotsplatRouteImport.update({
id: "/repos/$repoId/blob/$",
@@ -35,12 +41,14 @@ const reposDotrepoIdDotblobDotsplatRoute =
export interface FileRoutesByFullPath {
"/": typeof indexRoute;
"/repos": typeof reposRoute;
"/invite/$code": typeof inviteDotcodeRoute;
"/repos/$repoId": typeof reposDotrepoIdRoute;
"/repos/$repoId/blob/$": typeof reposDotrepoIdDotblobDotsplatRoute;
}
export interface FileRoutesByTo {
"/": typeof indexRoute;
"/repos": typeof reposRoute;
"/invite/$code": typeof inviteDotcodeRoute;
"/repos/$repoId": typeof reposDotrepoIdRoute;
"/repos/$repoId/blob/$": typeof reposDotrepoIdDotblobDotsplatRoute;
}
@@ -48,20 +56,38 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport;
"/": typeof indexRoute;
"/repos": typeof reposRoute;
"/invite/$code": typeof inviteDotcodeRoute;
"/repos/$repoId": typeof reposDotrepoIdRoute;
"/repos/$repoId/blob/$": typeof reposDotrepoIdDotblobDotsplatRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: "/" | "/repos" | "/repos/$repoId" | "/repos/$repoId/blob/$";
fullPaths:
| "/"
| "/repos"
| "/invite/$code"
| "/repos/$repoId"
| "/repos/$repoId/blob/$";
fileRoutesByTo: FileRoutesByTo;
to: "/" | "/repos" | "/repos/$repoId" | "/repos/$repoId/blob/$";
id: "__root__" | "/" | "/repos" | "/repos/$repoId" | "/repos/$repoId/blob/$";
to:
| "/"
| "/repos"
| "/invite/$code"
| "/repos/$repoId"
| "/repos/$repoId/blob/$";
id:
| "__root__"
| "/"
| "/repos"
| "/invite/$code"
| "/repos/$repoId"
| "/repos/$repoId/blob/$";
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
indexRoute: typeof indexRoute;
reposRoute: typeof reposRoute;
inviteDotcodeRoute: typeof inviteDotcodeRoute;
reposDotrepoIdRoute: typeof reposDotrepoIdRoute;
reposDotrepoIdDotblobDotsplatRoute: typeof reposDotrepoIdDotblobDotsplatRoute;
}
@@ -89,6 +115,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof reposDotrepoIdRouteImport;
parentRoute: typeof rootRouteImport;
};
"/invite/$code": {
id: "/invite/$code";
path: "/invite/$code";
fullPath: "/invite/$code";
preLoaderRoute: typeof inviteDotcodeRouteImport;
parentRoute: typeof rootRouteImport;
};
"/repos/$repoId/blob/$": {
id: "/repos/$repoId/blob/$";
path: "/repos/$repoId/blob/$";
@@ -102,6 +135,7 @@ declare module "@tanstack/react-router" {
const rootRouteChildren: RootRouteChildren = {
indexRoute: indexRoute,
reposRoute: reposRoute,
inviteDotcodeRoute: inviteDotcodeRoute,
reposDotrepoIdRoute: reposDotrepoIdRoute,
reposDotrepoIdDotblobDotsplatRoute: reposDotrepoIdDotblobDotsplatRoute,
};
+1
View File
@@ -2,6 +2,7 @@ import { index, route, rootRoute } from "@tanstack/virtual-file-routes";
export const routes = rootRoute("root.tsx", [
index("index.tsx"),
route("/invite/$code", "invite.$code.tsx"),
route("/repos", "repos.tsx"),
route("/repos/$repoId", "repos.$repoId.tsx"),
route("/repos/$repoId/blob/$", "repos.$repoId.blob.$.tsx"),
+11
View File
@@ -0,0 +1,11 @@
import { createFileRoute } from "@tanstack/react-router";
import { InvitePage } from "@/features/invite/ui/InvitePage";
export const Route = createFileRoute("/invite/$code")({
component: InvitePageRoute,
});
function InvitePageRoute() {
const { code } = Route.useParams();
return <InvitePage code={code} />;
}
+57
View File
@@ -0,0 +1,57 @@
import { Download, ExternalLink, Hexagon } from "lucide-react";
import { relayWsUrl } from "@/shared/lib/relay-url";
import { Button } from "@/shared/ui/button";
const DOWNLOAD_URL = "https://github.com/block/buzz/releases/latest";
/**
* Landing page for a relay invite link (`/invite/<code>`).
*
* The code is not validated here — validation happens in the desktop app when
* the invite is claimed against `POST /api/invites/claim`, signed by the
* joining key. This page only hands the code off via the `buzz://join` deep
* link (or tells the visitor where to get the app first).
*/
export function InvitePage({ code }: { code: string }) {
const relay = relayWsUrl();
const host = relay.replace(/^wss?:\/\//, "");
const deepLink = `buzz://join?relay=${encodeURIComponent(relay)}&code=${encodeURIComponent(code)}`;
return (
<div className="flex flex-1 flex-col items-center justify-center px-4 py-16 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
<Hexagon className="h-8 w-8 text-primary" />
</div>
<h1 className="mt-6 text-2xl font-semibold tracking-tight">
You&apos;re invited to join
</h1>
<p className="mt-1 font-mono text-lg text-foreground">{host}</p>
<p className="mt-3 max-w-md text-sm leading-relaxed text-muted-foreground">
Buzz is a messaging platform for humanagent collaboration. Accept this
invite in the desktop app to join the workspace on this relay.
</p>
<div className="mt-8 flex flex-col items-center gap-3 sm:flex-row">
<Button asChild size="lg">
<a href={deepLink}>
<ExternalLink className="h-4 w-4" />
Open in Buzz
</a>
</Button>
<Button asChild variant="outline" size="lg">
<a href={DOWNLOAD_URL} target="_blank" rel="noreferrer">
<Download className="h-4 w-4" />
Download Buzz
</a>
</Button>
</div>
<p className="mt-6 max-w-sm text-xs leading-relaxed text-muted-foreground">
Already have Buzz installed? &ldquo;Open in Buzz&rdquo; will launch it
and accept the invite. Otherwise download the app first, then come back
and use this link again.
</p>
</div>
);
}