Fix save progress bug

This commit is contained in:
adilallo
2026-05-20 19:58:32 -06:00
parent 2f2b5d0dc2
commit 7ee6282c1a
14 changed files with 193 additions and 88 deletions
+12 -2
View File
@@ -44,6 +44,7 @@ import {
} from "./utils/createFlowScreenRegistry";
import Button from "../../components/buttons/Button";
import { isValidCreateFlowSaveEmail } from "../../../lib/create/isValidCreateFlowSaveEmail";
import { buildCreateFlowDraftPayload } from "../../../lib/create/buildCreateFlowDraftPayload";
import {
fetchAuthSession,
requestMagicLink,
@@ -52,6 +53,7 @@ import { safeInternalPath } from "../../../lib/safeInternalPath";
import {
clearAnonymousCreateFlowStorage,
setTransferPendingFlag,
writeAnonymousCreateFlowState,
} from "./utils/anonymousDraftStorage";
import {
createFlowStateFromPublishedRule,
@@ -396,7 +398,15 @@ function CreateFlowLayoutContent({
const segment = stepAfterSave ?? "review";
const rawNext = `/create/${segment}?syncDraft=1`;
const nextPath = safeInternalPath(rawNext);
const result = await requestMagicLink(trimmed, nextPath);
const draftPayload = buildCreateFlowDraftPayload(state, currentStep);
writeAnonymousCreateFlowState({
...draftPayload,
communitySaveEmail: trimmed,
});
const result = await requestMagicLink(trimmed, nextPath, {
...draftPayload,
communitySaveEmail: trimmed,
});
if (result.ok === false) {
if (result.retryAfterMs != null && result.retryAfterMs > 0) {
const seconds = Math.ceil(result.retryAfterMs / 1000);
@@ -418,7 +428,7 @@ function CreateFlowLayoutContent({
} finally {
setCommunitySaveMagicLinkSubmitting(false);
}
}, [state.communitySaveEmail, tLogin, updateState]);
}, [state, currentStep, tLogin, updateState]);
const isCompletedStep = currentStep === "completed";
const isRightRailStep = currentStep === "decision-approaches";
+67 -62
View File
@@ -9,16 +9,55 @@ import {
} from "./utils/anonymousDraftStorage";
import { useCreateFlow } from "./context/CreateFlowContext";
import { parseCreateFlowScreenFromPathname } from "./utils/flowSteps";
import { saveDraftToServer } from "../../../lib/create/api";
import { fetchDraftFromServer, saveDraftToServer } from "../../../lib/create/api";
import { createFlowStateHasKeys } from "../../../lib/create/draftHydrationUtils";
import type { CreateFlowState } from "./types";
import messages from "../../../messages/en/index";
import Alert from "../../components/modals/Alert";
const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
function buildPayloadWithStep(
base: CreateFlowState,
pathname: string | null,
): CreateFlowState {
const step =
parseCreateFlowScreenFromPathname(pathname ?? null) ?? undefined;
return {
...base,
...(step ? { currentStep: step } : {}),
};
}
/**
* Prefer the on-device anonymous mirror when present; otherwise use the draft
* stored on the magic-link token at request time (written during verify).
*/
async function resolvePostLoginDraftPayload(
local: CreateFlowState,
pathname: string | null,
): Promise<CreateFlowState | null> {
const localPayload = createFlowStateHasKeys(local)
? buildPayloadWithStep(local, pathname)
: null;
const serverDraft = await fetchDraftFromServer();
const serverPayload =
serverDraft != null && createFlowStateHasKeys(serverDraft)
? buildPayloadWithStep(serverDraft, pathname)
: null;
if (localPayload && serverPayload) {
return { ...serverPayload, ...localPayload };
}
return localPayload ?? serverPayload;
}
/**
* After magic-link verify, redirects to `/create/...?syncDraft=1` with session cookie.
* With backend sync: PUT draft once then hydrates context. Without sync: hydrates from
* `create-flow-anonymous` localStorage only (no server write).
* With backend sync: PUT draft once when the device mirror is non-empty, then hydrates
* context. Without sync: hydrates from localStorage and/or the server draft saved at
* verify. Never writes an empty payload over an existing server draft.
*/
export function PostLoginDraftTransfer({
sessionUser,
@@ -39,49 +78,6 @@ export function PostLoginDraftTransfer({
if (!wantsTransfer) return;
if (attemptedRef.current) return;
if (!SYNC_ENABLED) {
attemptedRef.current = true;
let cancelled = false;
void (async () => {
const local = readAnonymousCreateFlowState();
const pending = hasTransferPendingFlag();
if (Object.keys(local).length === 0 && !pending) {
const params = new URLSearchParams(searchParams.toString());
params.delete("syncDraft");
const q = params.toString();
if (pathname) {
router.replace(q ? `${pathname}?${q}` : pathname);
}
attemptedRef.current = false;
return;
}
const step =
parseCreateFlowScreenFromPathname(pathname ?? null) ?? undefined;
const payload = {
...local,
...(step ? { currentStep: step } : {}),
};
if (cancelled) return;
clearAnonymousCreateFlowStorage();
replaceState(payload);
if (cancelled) return;
if (pathname) {
const params = new URLSearchParams(searchParams.toString());
params.delete("syncDraft");
const q = params.toString();
router.replace(q ? `${pathname}?${q}` : pathname);
}
})();
return () => {
cancelled = true;
};
}
attemptedRef.current = true;
let cancelled = false;
@@ -90,7 +86,7 @@ export function PostLoginDraftTransfer({
const local = readAnonymousCreateFlowState();
const pending = hasTransferPendingFlag();
if (Object.keys(local).length === 0 && !pending) {
if (!createFlowStateHasKeys(local) && !pending) {
const params = new URLSearchParams(searchParams.toString());
params.delete("syncDraft");
const q = params.toString();
@@ -101,27 +97,36 @@ export function PostLoginDraftTransfer({
return;
}
const step =
parseCreateFlowScreenFromPathname(pathname ?? null) ?? undefined;
const payload = {
...local,
...(step ? { currentStep: step } : {}),
};
const saveResult = await saveDraftToServer(payload);
const payload = await resolvePostLoginDraftPayload(local, pathname);
if (cancelled) return;
if (saveResult.ok === false) {
setTransferError(
messages.create.topNav.postLoginSaveFailedWithReason.replace(
"{reason}",
saveResult.message,
),
);
if (payload == null || !createFlowStateHasKeys(payload)) {
const params = new URLSearchParams(searchParams.toString());
params.delete("syncDraft");
const q = params.toString();
if (pathname) {
router.replace(q ? `${pathname}?${q}` : pathname);
}
attemptedRef.current = false;
return;
}
if (SYNC_ENABLED && createFlowStateHasKeys(local)) {
const saveResult = await saveDraftToServer(payload);
if (cancelled) return;
if (saveResult.ok === false) {
setTransferError(
messages.create.topNav.postLoginSaveFailedWithReason.replace(
"{reason}",
saveResult.message,
),
);
attemptedRef.current = false;
return;
}
}
clearAnonymousCreateFlowStorage();
replaceState(payload);
@@ -65,7 +65,6 @@ export function SignedInDraftHydration({
if (finishedUserIdRef.current === userId) return;
if (syncDraftParam === "1" || hasTransferPendingFlag()) {
finishedUserIdRef.current = userId;
return;
}
@@ -18,6 +18,7 @@ import type {
import {
clearAnonymousCreateFlowStorage,
clearLegacyCreateFlowKeysOnce,
hasTransferPendingFlag,
readAnonymousCreateFlowState,
writeAnonymousCreateFlowState,
} from "../utils/anonymousDraftStorage";
@@ -94,6 +95,13 @@ export function CreateFlowProvider({
const wasOff = !prevPersistRef.current;
prevPersistRef.current = true;
if (!wasOff) return;
if (hasTransferPendingFlag()) return;
if (
typeof window !== "undefined" &&
new URLSearchParams(window.location.search).get("syncDraft") === "1"
) {
return;
}
const from = readAnonymousCreateFlowState();
if (Object.keys(from).length === 0) return;
// eslint-disable-next-line react-hooks/set-state-in-effect -- hydrate local draft when mirroring turns on