feat: let guests publish a CommunityRule and claim it on this browser later

Finalize no longer requires a magic link. Guest rows stay off the catalog until sign-in on the same browser attaches ownership, and the login modal kebab no longer acts as a second close.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-26 11:01:10 -06:00
co-authored by Cursor
parent bf4020005a
commit 950b3224ac
23 changed files with 689 additions and 31 deletions
@@ -28,11 +28,13 @@ vi.mock("../../../lib/create/lastPublishedRule", () => ({
}));
const emptyState = {} as CreateFlowState;
const signedIn = { id: "user-1", email: "owner@example.com" };
describe("useCreateFlowFinalize", () => {
const router = { push: vi.fn() };
const updateState = vi.fn();
const openLogin = vi.fn();
const onGuestPublished = vi.fn();
beforeEach(() => {
vi.mocked(publishRule).mockReset();
@@ -41,6 +43,7 @@ describe("useCreateFlowFinalize", () => {
router.push.mockReset();
updateState.mockReset();
openLogin.mockReset();
onGuestPublished.mockReset();
});
afterEach(() => {
@@ -61,6 +64,7 @@ describe("useCreateFlowFinalize", () => {
openLogin,
updateState,
loginReturnPath: "/create/final-review",
sessionUser: signedIn,
}),
);
@@ -80,6 +84,99 @@ describe("useCreateFlowFinalize", () => {
});
});
it("does not publish while the session is unresolved", async () => {
const { result } = renderHook(() =>
useCreateFlowFinalize({
state: emptyState,
router,
openLogin,
updateState,
loginReturnPath: "/create/final-review",
sessionUser: undefined,
}),
);
await act(async () => {
await result.current.finalize();
});
expect(publishRule).not.toHaveBeenCalled();
expect(router.push).not.toHaveBeenCalled();
});
it("omits stakeholder emails when a guest publishes with invites in state", async () => {
vi.mocked(publishRule).mockResolvedValue({
ok: true,
id: "guest-rule-id",
title: "Published title",
});
const { result } = renderHook(() =>
useCreateFlowFinalize({
state: {
...emptyState,
stakeholderEmails: ["invitee@example.com"],
},
router,
openLogin,
updateState,
loginReturnPath: "/create/final-review",
sessionUser: null,
onGuestPublished,
}),
);
await act(async () => {
await result.current.finalize();
});
expect(publishRule).toHaveBeenCalledWith({
title: "Published title",
summary: "Published summary",
document: {},
});
expect(onGuestPublished).toHaveBeenCalledWith({ skippedInvites: true });
expect(openLogin).not.toHaveBeenCalled();
expect(router.push).toHaveBeenCalledWith(
`/create/completed?${CREATE_FLOW_COMPLETED_CELEBRATE_QUERY}=${CREATE_FLOW_COMPLETED_CELEBRATE_VALUE}`,
);
});
it("hints to claim later when a guest publishes without stakeholder emails", async () => {
vi.mocked(publishRule).mockResolvedValue({
ok: true,
id: "guest-rule-id",
title: "Published title",
});
const { result } = renderHook(() =>
useCreateFlowFinalize({
state: emptyState,
router,
openLogin,
updateState,
loginReturnPath: "/create/final-review",
sessionUser: null,
onGuestPublished,
}),
);
await act(async () => {
await result.current.finalize();
});
expect(publishRule).toHaveBeenCalledWith({
title: "Published title",
summary: "Published summary",
document: {},
});
expect(onGuestPublished).toHaveBeenCalledWith({ skippedInvites: false });
expect(openLogin).not.toHaveBeenCalled();
expect(router.push).toHaveBeenCalledWith(
`/create/completed?${CREATE_FLOW_COMPLETED_CELEBRATE_QUERY}=${CREATE_FLOW_COMPLETED_CELEBRATE_VALUE}`,
);
});
it("passes stakeholderEmails to publishRule on initial publish", async () => {
vi.mocked(publishRule).mockResolvedValue({
ok: true,
@@ -97,6 +194,7 @@ describe("useCreateFlowFinalize", () => {
openLogin,
updateState,
loginReturnPath: "/create/final-review",
sessionUser: signedIn,
}),
);
@@ -124,6 +222,7 @@ describe("useCreateFlowFinalize", () => {
openLogin,
updateState,
loginReturnPath: "/create/edit-rule",
sessionUser: signedIn,
}),
);