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>
178 lines
5.3 KiB
TypeScript
178 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useState } from "react";
|
|
import { buildPublishPayload } from "../../../../lib/create/buildPublishPayload";
|
|
import { publishRule, updatePublishedRule } from "../../../../lib/create/api";
|
|
import { writeLastPublishedRule } from "../../../../lib/create/lastPublishedRule";
|
|
import messages from "../../../../messages/en/index";
|
|
import type { CreateFlowState } from "../types";
|
|
import {
|
|
CREATE_FLOW_COMPLETED_CELEBRATE_QUERY,
|
|
CREATE_FLOW_COMPLETED_CELEBRATE_VALUE,
|
|
} from "../utils/flowSteps";
|
|
import { createFlowStepPath } from "../utils/createFlowPaths";
|
|
|
|
type AppRouterLike = { push: (_href: string) => void };
|
|
|
|
type OpenLogin = (args: {
|
|
variant: "default" | "saveProgress";
|
|
nextPath: string;
|
|
backdropVariant: "blurredYellow";
|
|
}) => void;
|
|
|
|
type SessionUser = { id: string; email: string };
|
|
|
|
export type UseCreateFlowFinalizeResult = {
|
|
publishBannerMessage: string | null;
|
|
setPublishBannerMessage: (_message: string | null) => void;
|
|
isPublishing: boolean;
|
|
finalize: () => Promise<void>;
|
|
};
|
|
|
|
/** Final Review → publish: banner + `isPublishing`, consumed by `CreateFlowLayoutClient`. */
|
|
export function useCreateFlowFinalize({
|
|
state,
|
|
router,
|
|
openLogin,
|
|
updateState,
|
|
loginReturnPath,
|
|
sessionUser,
|
|
onGuestPublished,
|
|
}: {
|
|
state: CreateFlowState;
|
|
router: AppRouterLike;
|
|
openLogin: OpenLogin;
|
|
updateState: (_patch: Partial<CreateFlowState>) => void;
|
|
/** Session gate return path (`?syncDraft=1`) — differs for `/create/edit-rule` vs `/create/final-review`. */
|
|
loginReturnPath: string;
|
|
/**
|
|
* `undefined` while `/api/auth/session` is in flight — finalize is a no-op
|
|
* until it resolves. `null` is a guest (anonymous publish).
|
|
*/
|
|
sessionUser: SessionUser | null | undefined;
|
|
/** Guest publish succeeded; `skippedInvites` when stakeholder emails were not sent. */
|
|
onGuestPublished?: (_info: { skippedInvites: boolean }) => void;
|
|
}): UseCreateFlowFinalizeResult {
|
|
const [publishBannerMessage, setPublishBannerMessage] = useState<
|
|
string | null
|
|
>(null);
|
|
const [isPublishing, setIsPublishing] = useState(false);
|
|
|
|
const finalize = useCallback(async () => {
|
|
if (sessionUser === undefined) return;
|
|
|
|
setPublishBannerMessage(null);
|
|
const payloadResult = buildPublishPayload(state);
|
|
if (payloadResult.ok === false) {
|
|
setPublishBannerMessage(
|
|
payloadResult.error === "missingCommunityName"
|
|
? messages.create.reviewAndComplete.publish.missingCommunityName
|
|
: payloadResult.error,
|
|
);
|
|
return;
|
|
}
|
|
const { title, summary, document: ruleDocument } = payloadResult;
|
|
setIsPublishing(true);
|
|
|
|
const editingId =
|
|
typeof state.editingPublishedRuleId === "string"
|
|
? state.editingPublishedRuleId.trim()
|
|
: "";
|
|
|
|
if (editingId.length > 0) {
|
|
const updateResult = await updatePublishedRule(editingId, {
|
|
title,
|
|
summary: summary ?? null,
|
|
document: ruleDocument,
|
|
});
|
|
setIsPublishing(false);
|
|
if (updateResult.ok === true) {
|
|
writeLastPublishedRule({
|
|
id: editingId,
|
|
title,
|
|
summary: summary ?? null,
|
|
document: ruleDocument,
|
|
});
|
|
updateState({ editingPublishedRuleId: undefined });
|
|
router.push(createFlowStepPath("completed"));
|
|
return;
|
|
}
|
|
if (updateResult.status === 401) {
|
|
openLogin({
|
|
variant: "default",
|
|
nextPath: loginReturnPath,
|
|
backdropVariant: "blurredYellow",
|
|
});
|
|
return;
|
|
}
|
|
setPublishBannerMessage(
|
|
updateResult.error.trim() !== ""
|
|
? updateResult.error
|
|
: messages.create.reviewAndComplete.publish.genericPublishFailed,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const isGuest = sessionUser === null;
|
|
const rawStakeholderEmails = (state.stakeholderEmails ?? []).filter(
|
|
(e) => typeof e === "string" && e.trim() !== "",
|
|
);
|
|
const stakeholderEmails = isGuest ? [] : rawStakeholderEmails;
|
|
const skippedGuestInvites = isGuest && rawStakeholderEmails.length > 0;
|
|
|
|
const publishResult = await publishRule({
|
|
title,
|
|
summary,
|
|
document: ruleDocument,
|
|
...(stakeholderEmails.length > 0 ? { stakeholderEmails } : {}),
|
|
});
|
|
setIsPublishing(false);
|
|
if (publishResult.ok === true) {
|
|
writeLastPublishedRule({
|
|
id: publishResult.id,
|
|
title,
|
|
summary: summary ?? null,
|
|
document: ruleDocument,
|
|
});
|
|
if (isGuest) {
|
|
onGuestPublished?.({ skippedInvites: skippedGuestInvites });
|
|
}
|
|
router.push(
|
|
createFlowStepPath("completed", {
|
|
[CREATE_FLOW_COMPLETED_CELEBRATE_QUERY]:
|
|
CREATE_FLOW_COMPLETED_CELEBRATE_VALUE,
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
if (publishResult.status === 401) {
|
|
openLogin({
|
|
variant: "default",
|
|
nextPath: loginReturnPath,
|
|
backdropVariant: "blurredYellow",
|
|
});
|
|
return;
|
|
}
|
|
setPublishBannerMessage(
|
|
publishResult.error.trim() !== ""
|
|
? publishResult.error
|
|
: messages.create.reviewAndComplete.publish.genericPublishFailed,
|
|
);
|
|
}, [
|
|
state,
|
|
router,
|
|
openLogin,
|
|
updateState,
|
|
loginReturnPath,
|
|
sessionUser,
|
|
onGuestPublished,
|
|
]);
|
|
|
|
return {
|
|
publishBannerMessage,
|
|
setPublishBannerMessage,
|
|
isPublishing,
|
|
finalize,
|
|
};
|
|
}
|