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
+65 -25
View File
@@ -23,8 +23,8 @@ import {
CREATE_FLOW_MANAGE_STAKEHOLDERS_VALUE,
CREATE_FLOW_REVIEW_RETURN_QUERY_KEY,
getNextStep,
getStepIndex,
parseReviewReturnSearchParam,
shouldOfferCreateFlowSaveAndExit,
createFlowStepUsesSelectSplitScroll,
TEMPLATES_FACET_RECOMMEND_QUERY,
TEMPLATES_FACET_RECOMMEND_VALUE,
@@ -64,6 +64,10 @@ import {
} from "../../../lib/create/publishedDocumentToCreateFlowState";
import { METHOD_FACET_API_SECTION_IDS } from "../../../lib/create/customRuleFacets";
import { readLastPublishedRule } from "../../../lib/create/lastPublishedRule";
import {
clearPendingKeepRuleLogin,
hasPendingKeepRuleLogin,
} from "../../../lib/create/pendingKeepRuleLogin";
import { runCompletedStepExit } from "./utils/runCompletedStepExit";
import messages from "../../../messages/en/index";
import {
@@ -91,9 +95,6 @@ import {
useCreateFlowDraftSaveBanner,
} from "./context/CreateFlowDraftSaveBannerContext";
/** First step where Save & Exit is offered (first Create Community select per Figma). */
const SAVE_EXIT_FROM_STEP_INDEX = getStepIndex("community-structure");
function CreateFlowSessionShell({ children }: { children: ReactNode }) {
const [sessionUser, setSessionUser] = useState<
{ id: string; email: string } | null | undefined
@@ -111,9 +112,9 @@ function CreateFlowSessionShell({ children }: { children: ReactNode }) {
const sessionResolved = sessionUser !== undefined;
// Mirror in-progress draft to localStorage for ALL visitors once we know who
// they are. Refresh-survival is the same UX for guest and signed-in users;
// signed-in users additionally get an explicit "Save & Exit" that PUTs to
// the server (handled in `useCreateFlowExit`).
// they are. Refresh-survival is the same UX for guest and signed-in users.
// Save & Exit: guests open the save-progress login modal; signed-in users
// PUT the server draft (`useCreateFlowExit`).
const enableLocalDraftMirroring = sessionResolved;
return (
@@ -197,6 +198,8 @@ function CreateFlowLayoutContent({
} | null>(null);
const [shareModalOpen, setShareModalOpen] = useState(false);
const [leaveConfirmOpen, setLeaveConfirmOpen] = useState(false);
const [completedHasPublishedRule, setCompletedHasPublishedRule] =
useState(false);
const leaveConfirmResolverRef = useRef<((proceed: boolean) => void) | null>(
null,
);
@@ -267,14 +270,14 @@ function CreateFlowLayoutContent({
title: completedCopy.guestInvitesSkippedTitle,
description: completedCopy.guestInvitesSkippedDescription,
});
return;
} else {
setCompletedFlowBanner({
key: "guestClaimHint",
status: "warning",
title: completedCopy.guestClaimHintTitle,
description: completedCopy.guestClaimHintDescription,
});
}
setCompletedFlowBanner({
key: "guestClaimHint",
status: "warning",
title: completedCopy.guestClaimHintTitle,
description: completedCopy.guestClaimHintDescription,
});
},
});
@@ -305,14 +308,26 @@ function CreateFlowLayoutContent({
});
const handleExit = async (opts?: { saveDraft?: boolean }) => {
const saveDraft = opts?.saveDraft ?? false;
if (!sessionResolved) return;
// Exit from `/create/completed` is post-publish: the rule is saved, so we
// skip the leave-confirm + login prompt and just wipe the in-flight draft.
// For signed-in users we also DELETE the server draft so a future visit to
// /create starts fresh instead of rehydrating yesterday's work.
// Completed is post-publish. Guests still need Save & Exit → keep-this-rule
// login (the document is live but unclaimed). Signed-in users just leave.
if (currentStep === "completed") {
if (sessionUser === null) {
openLogin({
variant: "keepRule",
nextPath: CREATE_ROUTES.completed,
backdropVariant: "blurredYellow",
onDismiss: () => {
runCompletedStepExit({
clearState,
clearAnonymousCreateFlowStorage,
router,
});
},
});
return;
}
runCompletedStepExit({
clearState,
clearAnonymousCreateFlowStorage,
@@ -322,7 +337,6 @@ function CreateFlowLayoutContent({
}
if (sessionUser === null) {
if (saveDraft) return;
const returnToTemplateReview =
templateReviewSlug != null
? `/create/review-template/${encodeURIComponent(templateReviewSlug)}?syncDraft=1`
@@ -428,9 +442,32 @@ function CreateFlowLayoutContent({
useEffect(() => {
if (currentStep !== "completed") {
setCompletedFlowBanner(null);
setCompletedHasPublishedRule(false);
return;
}
setCompletedHasPublishedRule(Boolean(readLastPublishedRule()));
}, [currentStep]);
useEffect(() => {
if (currentStep !== "completed") return;
if (!sessionResolved) return;
if (sessionUser !== null) {
clearPendingKeepRuleLogin();
return;
}
if (!hasPendingKeepRuleLogin()) return;
const timeoutId = window.setTimeout(() => {
if (!hasPendingKeepRuleLogin()) return;
clearPendingKeepRuleLogin();
openLogin({
variant: "keepRule",
nextPath: CREATE_ROUTES.completed,
backdropVariant: "blurredYellow",
});
}, 0);
return () => window.clearTimeout(timeoutId);
}, [currentStep, sessionResolved, sessionUser, openLogin]);
const handleCommunitySaveMagicLinkSubmit = useCallback(async () => {
setCommunitySaveMagicLinkError(null);
setCommunitySaveMagicLinkSuccess(false);
@@ -486,7 +523,6 @@ function CreateFlowLayoutContent({
const isSelectSplitScrollStep = createFlowStepUsesSelectSplitScroll(
currentStep,
);
const stepIdx = currentStep != null ? getStepIndex(currentStep) : -1;
/** Lockup+card / card-stack: `items-start` + shell `my-auto` so overflow scrolls from the top. */
const mainContentClass = isCompletedStep
@@ -506,9 +542,10 @@ function CreateFlowLayoutContent({
? "max-md:flex-col max-md:items-stretch"
: "max-md:flex-col max-md:items-center";
const mainResponsiveLayout = `${mainMaxMdCross} ${mainMaxMdJustify} md:flex-row md:justify-center`;
const saveDraftOnExit =
Boolean(sessionUser) &&
(stepIdx >= SAVE_EXIT_FROM_STEP_INDEX || currentStep === "edit-rule");
const saveDraftOnExit = shouldOfferCreateFlowSaveAndExit(
currentStep,
sessionUser,
);
const proportionBarProgress = getProportionBarProgressForCreateFlowStep(
currentStep,
@@ -674,7 +711,10 @@ function CreateFlowLayoutContent({
<CreateFlowTopNav
hasShare={isCompletedStep}
hasExport={isCompletedStep}
hasEdit={isCompletedStep && Boolean(sessionUser)}
hasEdit={
isCompletedStep &&
(Boolean(sessionUser) || completedHasPublishedRule)
}
hasManageStakeholders={isEditRuleStep}
saveDraftOnExit={saveDraftOnExit}
onShare={