From 32767f91444c23da0836af1cf463594df32c7e8b Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 26 Oct 2026 21:36:58 +1100 Subject: [PATCH] fix(web): leave app invite consent to the client Co-authored-by: Jitter Signed-off-by: Matt Toohey --- web/src/features/invite/ui/InvitePage.tsx | 57 ++++++----------------- web/tests/e2e/smoke.spec.ts | 50 ++++++++++++++++++-- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/web/src/features/invite/ui/InvitePage.tsx b/web/src/features/invite/ui/InvitePage.tsx index 9d4034dafc..8cd15a10bb 100644 --- a/web/src/features/invite/ui/InvitePage.tsx +++ b/web/src/features/invite/ui/InvitePage.tsx @@ -48,7 +48,6 @@ export function InvitePage({ code }: { code: string }) { const [document, setDocument] = React.useState(null); const [ageConfirmed, setAgeConfirmed] = React.useState(false); const [agreementConfirmed, setAgreementConfirmed] = React.useState(false); - const [opening, setOpening] = React.useState(false); const [joiningBrowser, setJoiningBrowser] = React.useState(false); const [browserJoinError, setBrowserJoinError] = React.useState( null, @@ -104,18 +103,6 @@ export function InvitePage({ code }: { code: string }) { return ((await response.json()) as { receipt: string }).receipt; }; - const openInvite = async () => { - setOpening(true); - try { - const receipt = await acceptPolicy(); - const query = new URLSearchParams({ relay, code }); - if (receipt) query.set("policy_receipt", receipt); - window.location.href = `buzz://join?${query.toString()}`; - } finally { - setOpening(false); - } - }; - const joinInBrowser = async () => { setBrowserJoinError(null); setJoiningBrowser(true); @@ -135,7 +122,6 @@ export function InvitePage({ code }: { code: string }) { const browserSigningAvailable = hasNip07Provider(); const disabled = policy === undefined || - opening || joiningBrowser || Boolean(policy?.age_attestation_required && !ageConfirmed) || Boolean( @@ -144,7 +130,8 @@ export function InvitePage({ code }: { code: string }) { !agreementConfirmed, ); const hasPolicyRequirements = Boolean( - policy && + browserSigningAvailable && + policy && (policy.age_attestation_required || policy.terms_markdown || policy.privacy_markdown), @@ -235,34 +222,18 @@ export function InvitePage({ code }: { code: string }) { {joiningBrowser ? "Joining…" : "Join in browser"} ) : null} - {policy === null ? ( - - ) : ( - - )} + {browserJoinError ? (

{browserJoinError} diff --git a/web/tests/e2e/smoke.spec.ts b/web/tests/e2e/smoke.spec.ts index 11bec34409..ea587d1d92 100644 --- a/web/tests/e2e/smoke.spec.ts +++ b/web/tests/e2e/smoke.spec.ts @@ -18,7 +18,7 @@ test("home page shows repositories section", async ({ page }) => { await expect(page.getByText("Repositories")).toBeVisible(); }); -test("invite requires age and legal consent before opening Buzz", async ({ +test("browser joining requires consent but app handoff does not", async ({ page, }) => { await page.route("**/api/join-policy", async (route) => { @@ -71,6 +71,14 @@ test("invite requires age and legal consent before opening Buzz", async ({ ]), }); }); + await page.addInitScript(() => { + Object.assign(window, { + nostr: { + getPublicKey: async () => "ab".repeat(32), + signEvent: async (event: unknown) => event, + }, + }); + }); await page.goto("/invite/demo-code"); await expect( @@ -87,9 +95,10 @@ test("invite requires age and legal consent before opening Buzz", async ({ const agreementConfirmation = page.getByLabel( "I agree to the Buzz Terms of Service and Privacy Policy.", ); - const acceptInvite = page.getByRole("button", { - name: "Accept invite in Buzz", - }); + const acceptInvite = page.getByRole("button", { name: "Join in browser" }); + await expect( + page.getByRole("link", { name: "Open invite in Buzz" }), + ).toHaveAttribute("href", /^buzz:\/\/join\?relay=.*&code=demo-code$/); await expect(ageConfirmation).toBeVisible(); await expect(agreementConfirmation).toBeVisible(); @@ -128,6 +137,39 @@ test("invite requires age and legal consent before opening Buzz", async ({ expect(consentBox?.width).toBe(acceptButtonBox?.width); }); +for (const policyStatus of [200, 503]) { + test(`app invite handoff needs no browser consent (policy HTTP ${policyStatus})`, async ({ + page, + }) => { + let acceptanceRequests = 0; + await page.route("**/api/join-policy", (route) => + route.fulfill({ + status: policyStatus, + contentType: "application/json", + body: JSON.stringify({ + policy: { + terms_markdown: "Terms", + age_attestation_required: true, + version: "v1", + }, + }), + }), + ); + await page.route("**/api/invites/accept-policy", (route) => { + acceptanceRequests++; + return route.fulfill({ status: 500 }); + }); + const loaded = page.waitForResponse("**/api/join-policy"); + await page.goto("/invite/demo-code"); + await loaded; + await expect( + page.getByRole("link", { name: "Open invite in Buzz" }), + ).toHaveAttribute("href", /^buzz:\/\/join\?relay=.*&code=demo-code$/); + await expect(page.getByRole("checkbox")).toHaveCount(0); + expect(acceptanceRequests).toBe(0); + }); +} + test("invite can enroll a NIP-07 identity for browser access", async ({ page, }) => {