+
+
+ {inviteUrl ? (
+
+ {inviteUrl}
+
+ ) : null}
+
+
+
+
+
+
Expires after
@@ -94,7 +214,7 @@ export function InviteLinkSection({
aria-label="Choose invite expiry"
className="h-8 shrink-0 gap-1.5 px-2 text-sm text-muted-foreground"
data-testid="invite-link-ttl-trigger"
- disabled={copyStatus === "copying"}
+ disabled={isGenerating || copyStatus === "copying"}
size="sm"
type="button"
variant="ghost"
@@ -129,7 +249,7 @@ export function InviteLinkSection({
aria-label="Choose maximum invite uses"
className="h-8 shrink-0 gap-1.5 px-2 text-sm text-muted-foreground"
data-testid="invite-link-max-uses-trigger"
- disabled={copyStatus === "copying"}
+ disabled={isGenerating || copyStatus === "copying"}
size="sm"
type="button"
variant="ghost"
@@ -159,28 +279,6 @@ export function InviteLinkSection({
-
-
-
-
);
}
diff --git a/desktop/tests/e2e/invite-link-copy.spec.ts b/desktop/tests/e2e/invite-link-copy.spec.ts
index 20df59a00c..353793ec4a 100644
--- a/desktop/tests/e2e/invite-link-copy.spec.ts
+++ b/desktop/tests/e2e/invite-link-copy.spec.ts
@@ -27,7 +27,7 @@ test.beforeEach(async ({ page }) => {
});
});
-test("copies a freshly minted invite link without showing a URL or QR code", async ({
+test("copies a freshly minted invite link from the link field", async ({
page,
}) => {
await page.goto("/");
@@ -38,7 +38,9 @@ test("copies a freshly minted invite link without showing a URL or QR code", asy
await expect(page.getByTestId("member-pubkey-input")).toBeVisible();
await expect(page.getByTestId("member-role")).toHaveCount(0);
await expect(page.getByTestId("confirm-add-member")).toHaveCount(0);
- await expect(page.getByTestId("invite-link-url")).toHaveCount(0);
+ await expect(page.getByTestId("invite-link-url")).toHaveValue(
+ "buzz://join?relay=wss%3A%2F%2Frelay.example.com&code=qr-download-test",
+ );
await expect(page.getByTestId("invite-link-qr-code")).toHaveCount(0);
await expect(page.getByTestId("invite-link-max-uses-trigger")).toHaveText(
"No limit",
@@ -80,9 +82,82 @@ test("sets a selected invite-use limit", async ({ page }) => {
).toBeVisible();
await page.getByTestId("invite-link-max-uses-10").click();
await expect(maxUsesTrigger).toHaveText("10 uses");
+ await expect
+ .poll(() => invitePayloads.at(-1))
+ .toEqual({
+ max_uses: 10,
+ ttl_secs: 3 * 24 * 60 * 60,
+ });
await page.getByTestId("copy-invite-link").click();
await expect(page.getByTestId("copy-invite-link")).toContainText("Copied");
- expect(invitePayloads).toEqual([
- { max_uses: 10, ttl_secs: 3 * 24 * 60 * 60 },
- ]);
+});
+
+test("retries a failed invite-link generation", async ({ page }) => {
+ let attempts = 0;
+ await page.route("**/api/invites", async (route) => {
+ attempts += 1;
+ if (attempts === 1) {
+ await route.fulfill({ status: 500 });
+ return;
+ }
+
+ await route.fulfill({
+ contentType: "application/json",
+ json: {
+ code: "retry-test",
+ expires_at: Math.floor(Date.now() / 1000) + 86_400,
+ url: "buzz://join?relay=wss%3A%2F%2Frelay.example.com&code=retry-test",
+ },
+ status: 200,
+ });
+ });
+
+ await page.goto("/");
+ await openSettings(page, "community-members");
+ await page.getByTestId("community-invite-dialog-trigger").click();
+
+ const linkField = page.getByTestId("invite-link-url");
+ const copyButton = page.getByTestId("copy-invite-link");
+ await expect(linkField).toHaveAttribute(
+ "placeholder",
+ "Couldn’t create invite link",
+ );
+ await expect(copyButton).toHaveText("Retry");
+ await expect(copyButton).toBeEnabled();
+
+ await copyButton.click();
+ await expect(linkField).toHaveValue(
+ "buzz://join?relay=wss%3A%2F%2Frelay.example.com&code=retry-test",
+ );
+ await expect(copyButton).toHaveText("Copy link");
+ expect(attempts).toBe(2);
+});
+
+test("reopens with the default expiry before generating a new link", async ({
+ page,
+}) => {
+ await page.goto("/");
+ await openSettings(page, "community-members");
+ await page.getByTestId("community-invite-dialog-trigger").click();
+
+ await expect
+ .poll(() => invitePayloads)
+ .toEqual([{ ttl_secs: 3 * 24 * 60 * 60 }]);
+ await page.getByTestId("invite-link-ttl-trigger").click();
+ await page.getByTestId("invite-link-ttl-604800").click();
+ await expect
+ .poll(() => invitePayloads)
+ .toEqual([{ ttl_secs: 3 * 24 * 60 * 60 }, { ttl_secs: 7 * 24 * 60 * 60 }]);
+
+ const dialog = page.getByTestId("community-invite-dialog");
+ await dialog.getByRole("button", { name: "Close" }).click();
+ await expect(dialog).toHaveCount(0);
+ await page.getByTestId("community-invite-dialog-trigger").click();
+ await expect
+ .poll(() => invitePayloads)
+ .toEqual([
+ { ttl_secs: 3 * 24 * 60 * 60 },
+ { ttl_secs: 7 * 24 * 60 * 60 },
+ { ttl_secs: 3 * 24 * 60 * 60 },
+ ]);
});
diff --git a/desktop/tests/e2e/invites-settings-screenshots.spec.ts b/desktop/tests/e2e/invites-settings-screenshots.spec.ts
index e0856ac400..5421fa6428 100644
--- a/desktop/tests/e2e/invites-settings-screenshots.spec.ts
+++ b/desktop/tests/e2e/invites-settings-screenshots.spec.ts
@@ -104,15 +104,19 @@ test("capture: share-style community invite dialog", async ({ page }) => {
await expect(
dialog.getByRole("heading", { name: "Add someone", exact: true }),
).toHaveCount(0);
+ await expect(dialog.getByTestId("invite-options-divider")).toBeVisible();
await expect(
- dialog.getByText("Or share a link", { exact: true }),
- ).toHaveCount(0);
- await expect(
- dialog.getByText("Link settings", { exact: true }),
+ dialog.getByText("Or, copy a link", { exact: true }),
).toBeVisible();
+ await expect(dialog.getByText("Link settings", { exact: true })).toHaveCount(
+ 0,
+ );
await expect(page.getByTestId("member-pubkey-input")).toBeVisible();
await expect(page.getByTestId("member-role")).toHaveCount(0);
await expect(page.getByTestId("confirm-add-member")).toHaveCount(0);
+ await expect(page.getByTestId("invite-link-url")).toHaveValue(
+ "https://alpha.example.com/invite/community-email-test",
+ );
await expect(page.getByTestId("copy-invite-link")).toHaveText("Copy link");
await expect(page.getByTestId("invite-link-ttl-trigger")).toHaveText(
"3 days",