mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
co-authored by
npub1k766u8wq088nkr9vy94uqlh9kmw5956g593a7xk7cks7frhgctmsr00p27
Will Pfleger
parent
b97777350d
commit
2e529aab75
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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'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 human–agent 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? “Open in Buzz” will launch it
|
||||
and accept the invite. Otherwise download the app first, then come back
|
||||
and use this link again.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user