fix(web): leave app invite consent to the client

Co-authored-by: Jitter <d14dfe033ef0f809866f9f984de04821b0d900d7652fd85a54776ee40ca3a68f@buzz.block.builderlab.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
Matt Toohey
2026-10-26 21:36:58 +11:00
parent d676c7b8db
commit 32767f9144
2 changed files with 60 additions and 47 deletions

View File

@@ -48,7 +48,6 @@ export function InvitePage({ code }: { code: string }) {
const [document, setDocument] = React.useState<PolicyDocument | null>(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<string | null>(
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"}
</Button>
) : null}
{policy === null ? (
<Button
asChild
className={`h-10 w-full ${
browserSigningAvailable
? "border border-black bg-white text-black hover:bg-black/5"
: "bg-black text-white hover:bg-black/90 focus-visible:ring-black"
}`}
>
<a
href={`buzz://join?relay=${encodeURIComponent(relay)}&code=${encodeURIComponent(code)}`}
>
Accept invite in Buzz
</a>
</Button>
) : (
<Button
className={`h-10 w-full disabled:cursor-not-allowed disabled:bg-black/30 disabled:text-white/70 ${
browserSigningAvailable
? "border border-black bg-white text-black hover:bg-black/5"
: "bg-black text-white hover:bg-black/90 focus-visible:ring-black"
}`}
disabled={disabled}
onClick={openInvite}
>
Accept invite in Buzz
</Button>
)}
<Button
asChild
className={`h-10 w-full ${
browserSigningAvailable
? "border border-black bg-white text-black hover:bg-black/5"
: "bg-black text-white hover:bg-black/90 focus-visible:ring-black"
}`}
>
<a href={`buzz://join?${new URLSearchParams({ relay, code })}`}>
Open invite in Buzz
</a>
</Button>
{browserJoinError ? (
<p className="text-sm text-red-700" role="alert">
{browserJoinError}

View File

@@ -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,
}) => {