Let guests keep a published rule via Save & Exit and a post-finalize sign-in prompt.

Email is optional so they can continue without saving; skip from Save & Exit leaves the flow instead of returning to completed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-02 16:18:41 -06:00
co-authored by Cursor
parent 84ef943df4
commit db34a1f0df
17 changed files with 521 additions and 60 deletions
+18
View File
@@ -8,6 +8,7 @@ import {
getStepIndex,
parseReviewReturnSearchParam,
resolveCreateFlowBackTarget,
shouldOfferCreateFlowSaveAndExit,
TEMPLATES_FACET_RECOMMEND_QUERY,
TEMPLATES_FACET_RECOMMEND_VALUE,
TEMPLATE_REVIEW_FROM_CREATE_FLOW_QUERY,
@@ -132,4 +133,21 @@ describe("flowSteps", () => {
).toBeNull();
expect(parseReviewReturnSearchParam(null)).toBeNull();
});
it("offers Save & Exit from community-structure through final-review and on edit-rule", () => {
expect(shouldOfferCreateFlowSaveAndExit("informational")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("community-name")).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("community-structure")).toBe(true);
expect(shouldOfferCreateFlowSaveAndExit("final-review")).toBe(true);
expect(shouldOfferCreateFlowSaveAndExit("edit-rule")).toBe(true);
});
it("offers Save & Exit on completed only for guests", () => {
expect(shouldOfferCreateFlowSaveAndExit("completed")).toBe(false);
expect(
shouldOfferCreateFlowSaveAndExit("completed", { id: "user-1" }),
).toBe(false);
expect(shouldOfferCreateFlowSaveAndExit("completed", null)).toBe(true);
expect(shouldOfferCreateFlowSaveAndExit(null)).toBe(false);
});
});
@@ -4,6 +4,7 @@ import type { CreateFlowState } from "../../../app/(app)/create/types";
import { useCreateFlowFinalize } from "../../../app/(app)/create/hooks/useCreateFlowFinalize";
import { publishRule, updatePublishedRule } from "../../../lib/create/api";
import { writeLastPublishedRule } from "../../../lib/create/lastPublishedRule";
import { CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY } from "../../../lib/create/pendingKeepRuleLogin";
import {
CREATE_FLOW_COMPLETED_CELEBRATE_QUERY,
CREATE_FLOW_COMPLETED_CELEBRATE_VALUE,
@@ -44,6 +45,7 @@ describe("useCreateFlowFinalize", () => {
updateState.mockReset();
openLogin.mockReset();
onGuestPublished.mockReset();
sessionStorage.clear();
});
afterEach(() => {
@@ -82,6 +84,9 @@ describe("useCreateFlowFinalize", () => {
summary: "Published summary",
document: {},
});
expect(
sessionStorage.getItem(CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY),
).toBeNull();
});
it("does not publish while the session is unresolved", async () => {
@@ -137,6 +142,7 @@ describe("useCreateFlowFinalize", () => {
});
expect(onGuestPublished).toHaveBeenCalledWith({ skippedInvites: true });
expect(openLogin).not.toHaveBeenCalled();
expect(sessionStorage.getItem(CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY)).toBe("1");
expect(router.push).toHaveBeenCalledWith(
`/create/completed?${CREATE_FLOW_COMPLETED_CELEBRATE_QUERY}=${CREATE_FLOW_COMPLETED_CELEBRATE_VALUE}`,
);
@@ -172,6 +178,7 @@ describe("useCreateFlowFinalize", () => {
});
expect(onGuestPublished).toHaveBeenCalledWith({ skippedInvites: false });
expect(openLogin).not.toHaveBeenCalled();
expect(sessionStorage.getItem(CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY)).toBe("1");
expect(router.push).toHaveBeenCalledWith(
`/create/completed?${CREATE_FLOW_COMPLETED_CELEBRATE_QUERY}=${CREATE_FLOW_COMPLETED_CELEBRATE_VALUE}`,
);
@@ -250,4 +257,69 @@ describe("useCreateFlowFinalize", () => {
editingPublishedRuleId: undefined,
});
});
it("does not PATCH when a guest finalizes an already-published rule", async () => {
const { result } = renderHook(() =>
useCreateFlowFinalize({
state: {
...emptyState,
editingPublishedRuleId: "guest-rule-1",
},
router,
openLogin,
updateState,
loginReturnPath: "/create/final-review?syncDraft=1",
sessionUser: null,
onGuestPublished,
}),
);
await act(async () => {
await result.current.finalize();
});
expect(updatePublishedRule).not.toHaveBeenCalled();
expect(publishRule).not.toHaveBeenCalled();
expect(openLogin).not.toHaveBeenCalled();
expect(onGuestPublished).toHaveBeenCalledWith({ skippedInvites: false });
expect(writeLastPublishedRule).toHaveBeenCalledWith({
id: "guest-rule-1",
title: "Published title",
summary: "Published summary",
document: {},
});
expect(sessionStorage.getItem(CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY)).toBe("1");
expect(router.push).toHaveBeenCalledWith("/create/completed");
});
it("opens keepRule login when a guest publish is unauthorized", async () => {
vi.mocked(publishRule).mockResolvedValue({
ok: false,
error: "Unauthorized",
status: 401,
});
const { result } = renderHook(() =>
useCreateFlowFinalize({
state: emptyState,
router,
openLogin,
updateState,
loginReturnPath: "/create/final-review?syncDraft=1",
sessionUser: null,
onGuestPublished,
}),
);
await act(async () => {
await result.current.finalize();
});
expect(openLogin).toHaveBeenCalledWith({
variant: "keepRule",
nextPath: "/create/completed",
backdropVariant: "blurredYellow",
});
expect(router.push).not.toHaveBeenCalled();
});
});
+24
View File
@@ -0,0 +1,24 @@
import { describe, it, expect, beforeEach } from "vitest";
import {
CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY,
clearPendingKeepRuleLogin,
hasPendingKeepRuleLogin,
writePendingKeepRuleLogin,
} from "../../lib/create/pendingKeepRuleLogin";
describe("pendingKeepRuleLogin", () => {
beforeEach(() => {
sessionStorage.clear();
});
it("writes, reads, and clears the session flag", () => {
expect(hasPendingKeepRuleLogin()).toBe(false);
writePendingKeepRuleLogin();
expect(sessionStorage.getItem(CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY)).toBe(
"1",
);
expect(hasPendingKeepRuleLogin()).toBe(true);
clearPendingKeepRuleLogin();
expect(hasPendingKeepRuleLogin()).toBe(false);
});
});