Invite people after review, then finish the Rule, with login before inviting and Invite your members on Profile.

This commit is contained in:
adilallo committed 2026-11-06 08:56:13 -07:00
1 parent 88bfccd1e7
commit 70ceb73104
31 files changed
+1388 -401

No files matched your search

+161 -118
View File
@@ -17,21 +17,27 @@ import { createFlowStepPath } from "../utils/createFlowPaths";
type AppRouterLike = { push: (_href: string) => void };
type OpenLogin = (args: {
variant: "default" | "saveProgress" | "keepRule";
variant: "default" | "saveProgress" | "keepRule" | "invite";
nextPath: string;
backdropVariant: "blurredYellow";
}) => void;
type SessionUser = { id: string; email: string };
export type FinalizeOptions = {
/** When true, finish without sending the current email list. */
skipInvites?: boolean;
};
export type UseCreateFlowFinalizeResult = {
publishBannerTitle: string | null;
publishBannerMessage: string | null;
setPublishBannerMessage: (_message: string | null) => void;
isPublishing: boolean;
finalize: () => Promise<void>;
finalize: (_opts?: FinalizeOptions) => Promise<void>;
};
/** Final Review → publish: banner + `isPublishing`, consumed by `CreateFlowLayoutClient`. */
/** Invite / Finish → publish: banner + `isPublishing`, consumed by `CreateFlowLayoutClient`. */
export function useCreateFlowFinalize({
state,
router,
@@ -55,65 +61,67 @@ export function useCreateFlowFinalize({
/** Guest publish succeeded; `skippedInvites` when stakeholder emails were not sent. */
onGuestPublished?: (_info: { skippedInvites: boolean }) => void;
}): UseCreateFlowFinalizeResult {
const [publishBannerTitle, setPublishBannerTitle] = useState<string | null>(
null,
);
const [publishBannerMessage, setPublishBannerMessage] = useState<
string | null
>(null);
const [isPublishing, setIsPublishing] = useState(false);
const finalize = useCallback(async () => {
if (sessionUser === undefined) return;
setPublishBannerMessage(null);
setIsPublishing(true);
const prepared = await stateWithPendingCommunityAvatar(state);
if (prepared.ok === false) {
setIsPublishing(false);
setPublishBannerMessage(messages.create.upload.errors.generic);
return;
}
if (prepared.state.communityAvatarUrl !== state.communityAvatarUrl) {
updateState({ communityAvatarUrl: prepared.state.communityAvatarUrl });
}
const payloadResult = buildPublishPayload(prepared.state);
if (payloadResult.ok === false) {
setIsPublishing(false);
setPublishBannerMessage(
payloadResult.error === "missingCommunityName"
? messages.create.reviewAndComplete.publish.missingCommunityName
: payloadResult.error,
const showFailedBanner = useCallback(
(message: string, sendingInvites: boolean) => {
const inviteCopy = messages.create.reviewAndComplete.confirmStakeholders;
if (sendingInvites) {
setPublishBannerTitle(inviteCopy.sendFailedTitle);
setPublishBannerMessage(inviteCopy.sendFailedDescription);
return;
}
setPublishBannerTitle(
messages.create.reviewAndComplete.publish.publishBannerTitle,
);
return;
}
const { title, summary, document: ruleDocument } = payloadResult;
setPublishBannerMessage(message);
},
[],
);
const editingId =
typeof state.editingPublishedRuleId === "string"
? state.editingPublishedRuleId.trim()
: "";
const finalize = useCallback(
async (opts?: FinalizeOptions) => {
if (sessionUser === undefined) return;
const skipInvites = opts?.skipInvites === true;
if (editingId.length > 0 && sessionUser === null) {
setIsPublishing(false);
writeLastPublishedRule({
id: editingId,
title,
summary: summary ?? null,
document: ruleDocument,
});
updateState({ editingPublishedRuleId: undefined });
writePendingKeepRuleLogin();
onGuestPublished?.({ skippedInvites: false });
router.push(createFlowStepPath("completed"));
return;
}
setPublishBannerTitle(null);
setPublishBannerMessage(null);
setIsPublishing(true);
const prepared = await stateWithPendingCommunityAvatar(state);
if (prepared.ok === false) {
setIsPublishing(false);
showFailedBanner(messages.create.upload.errors.generic, false);
return;
}
if (prepared.state.communityAvatarUrl !== state.communityAvatarUrl) {
updateState({ communityAvatarUrl: prepared.state.communityAvatarUrl });
}
const payloadResult = buildPublishPayload(prepared.state);
if (payloadResult.ok === false) {
setIsPublishing(false);
showFailedBanner(
payloadResult.error === "missingCommunityName"
? messages.create.reviewAndComplete.publish.missingCommunityName
: payloadResult.error,
false,
);
return;
}
const { title, summary, document: ruleDocument } = payloadResult;
if (editingId.length > 0) {
const updateResult = await updatePublishedRule(editingId, {
title,
summary: summary ?? null,
document: ruleDocument,
});
setIsPublishing(false);
if (updateResult.ok === true) {
const editingId =
typeof state.editingPublishedRuleId === "string"
? state.editingPublishedRuleId.trim()
: "";
if (editingId.length > 0 && sessionUser === null) {
setIsPublishing(false);
writeLastPublishedRule({
id: editingId,
title,
@@ -121,10 +129,87 @@ export function useCreateFlowFinalize({
document: ruleDocument,
});
updateState({ editingPublishedRuleId: undefined });
writePendingKeepRuleLogin();
onGuestPublished?.({ skippedInvites: false });
router.push(createFlowStepPath("completed"));
return;
}
if (updateResult.status === 401) {
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: sessionUser === null ? "keepRule" : "default",
nextPath:
sessionUser === null
? createFlowStepPath("completed")
: loginReturnPath,
backdropVariant: "blurredYellow",
});
return;
}
showFailedBanner(
updateResult.error.trim() !== ""
? updateResult.error
: messages.create.reviewAndComplete.publish.genericPublishFailed,
false,
);
return;
}
const isGuest = sessionUser === null;
const rawStakeholderEmails = skipInvites
? []
: (state.stakeholderEmails ?? []).filter(
(e) => typeof e === "string" && e.trim() !== "",
);
const stakeholderEmails = isGuest ? [] : rawStakeholderEmails;
const skippedGuestInvites = isGuest && rawStakeholderEmails.length > 0;
const sendingInvites = stakeholderEmails.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) {
writePendingKeepRuleLogin();
onGuestPublished?.({ skippedInvites: skippedGuestInvites });
}
router.push(
createFlowStepPath("completed", {
[CREATE_FLOW_COMPLETED_CELEBRATE_QUERY]:
CREATE_FLOW_COMPLETED_CELEBRATE_VALUE,
}),
);
return;
}
if (publishResult.status === 401) {
openLogin({
variant: sessionUser === null ? "keepRule" : "default",
nextPath:
@@ -135,76 +220,34 @@ export function useCreateFlowFinalize({
});
return;
}
setPublishBannerMessage(
updateResult.error.trim() !== ""
? updateResult.error
showFailedBanner(
publishResult.error.trim() !== ""
? publishResult.error
: messages.create.reviewAndComplete.publish.genericPublishFailed,
sendingInvites,
);
return;
}
},
[
state,
router,
openLogin,
updateState,
loginReturnPath,
sessionUser,
onGuestPublished,
showFailedBanner,
],
);
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) {
writePendingKeepRuleLogin();
onGuestPublished?.({ skippedInvites: skippedGuestInvites });
}
router.push(
createFlowStepPath("completed", {
[CREATE_FLOW_COMPLETED_CELEBRATE_QUERY]:
CREATE_FLOW_COMPLETED_CELEBRATE_VALUE,
}),
);
return;
}
if (publishResult.status === 401) {
openLogin({
variant: sessionUser === null ? "keepRule" : "default",
nextPath:
sessionUser === null
? createFlowStepPath("completed")
: loginReturnPath,
backdropVariant: "blurredYellow",
});
return;
}
setPublishBannerMessage(
publishResult.error.trim() !== ""
? publishResult.error
: messages.create.reviewAndComplete.publish.genericPublishFailed,
);
}, [
state,
router,
openLogin,
updateState,
loginReturnPath,
sessionUser,
onGuestPublished,
]);
const clearPublishBanner = useCallback((message: string | null) => {
if (message === null) setPublishBannerTitle(null);
setPublishBannerMessage(message);
}, []);
return {
publishBannerTitle,
publishBannerMessage,
setPublishBannerMessage,
setPublishBannerMessage: clearPublishBanner,
isPublishing,
finalize,
};