fix: persist saved client delivery choice
This commit is contained in:
parent
de0cf49490
commit
5142ff30db
|
|
@ -143,6 +143,9 @@ export const ClientDeliveryPage = () => {
|
||||||
|
|
||||||
const effectiveSelectedSlot = selectedSlot || invitationSelectedSlot;
|
const effectiveSelectedSlot = selectedSlot || invitationSelectedSlot;
|
||||||
const isChoiceSaved = choiceSaved || (!isActiveState && Boolean(invitationSelectedSlot));
|
const isChoiceSaved = choiceSaved || (!isActiveState && Boolean(invitationSelectedSlot));
|
||||||
|
const savedChoiceLabel = effectiveSelectedSlot
|
||||||
|
? `${formatDeliveryDate(effectiveSelectedSlot.date)} / ${effectiveSelectedSlot.time}`
|
||||||
|
: "";
|
||||||
|
|
||||||
const handleSaveChoice = React.useCallback(
|
const handleSaveChoice = React.useCallback(
|
||||||
async () => {
|
async () => {
|
||||||
|
|
@ -236,6 +239,16 @@ export const ClientDeliveryPage = () => {
|
||||||
</p>
|
</p>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
{isChoiceSaved && savedChoiceLabel ? (
|
||||||
|
<Panel className="space-y-2 p-5 sm:p-6">
|
||||||
|
<p className="text-sm uppercase tracking-[0.24em] text-[var(--color-text-muted)]">Ваш выбор</p>
|
||||||
|
<h2 className="text-xl font-semibold leading-tight">Сохранено: {savedChoiceLabel}</h2>
|
||||||
|
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||||||
|
При повторном открытии этой ссылки будет показан тот же выбор.
|
||||||
|
</p>
|
||||||
|
</Panel>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{isActiveState && !isChoiceSaved && slots.length ? (
|
{isActiveState && !isChoiceSaved && slots.length ? (
|
||||||
<DeliverySlotsPicker
|
<DeliverySlotsPicker
|
||||||
slots={slots}
|
slots={slots}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { supabase, hasSupabaseConfig } from "../supabaseClient";
|
||||||
const SHOWCASE_TOKEN = "showcase";
|
const SHOWCASE_TOKEN = "showcase";
|
||||||
const LOCAL_CLIENT_FLOW_TOKEN_PREFIX = "client-flow-";
|
const LOCAL_CLIENT_FLOW_TOKEN_PREFIX = "client-flow-";
|
||||||
const LOCAL_CLIENT_FLOW_TOKEN = "client-flow-1001";
|
const LOCAL_CLIENT_FLOW_TOKEN = "client-flow-1001";
|
||||||
|
const LOCAL_INVITATION_STORAGE_PREFIX = "delivery-invitation:";
|
||||||
|
|
||||||
const localDeliveryInvitationCache = new Map();
|
const localDeliveryInvitationCache = new Map();
|
||||||
|
|
||||||
|
|
@ -16,6 +17,43 @@ const addDays = (date, days) => {
|
||||||
|
|
||||||
const cloneInvitation = (invitation) => JSON.parse(JSON.stringify(invitation));
|
const cloneInvitation = (invitation) => JSON.parse(JSON.stringify(invitation));
|
||||||
|
|
||||||
|
const getLocalStorage = () => {
|
||||||
|
try {
|
||||||
|
return globalThis.localStorage || null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLocalStorageKey = (token) => `${LOCAL_INVITATION_STORAGE_PREFIX}${token}`;
|
||||||
|
|
||||||
|
const readStoredInvitation = (token) => {
|
||||||
|
const storage = getLocalStorage();
|
||||||
|
if (!storage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = storage.getItem(getLocalStorageKey(token));
|
||||||
|
return raw ? JSON.parse(raw) : null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeStoredInvitation = (invitation) => {
|
||||||
|
const storage = getLocalStorage();
|
||||||
|
if (!storage?.setItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
storage.setItem(getLocalStorageKey(invitation.token), JSON.stringify(invitation));
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore storage quota and privacy mode failures.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isLocalClientInvitationToken = (token) =>
|
const isLocalClientInvitationToken = (token) =>
|
||||||
token === SHOWCASE_TOKEN || token?.startsWith(LOCAL_CLIENT_FLOW_TOKEN_PREFIX);
|
token === SHOWCASE_TOKEN || token?.startsWith(LOCAL_CLIENT_FLOW_TOKEN_PREFIX);
|
||||||
|
|
||||||
|
|
@ -52,8 +90,14 @@ const getCachedInvitation = (token) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheInvitation = (invitation) => {
|
const cacheInvitation = (invitation) => {
|
||||||
localDeliveryInvitationCache.set(invitation.token, cloneInvitation(invitation));
|
const clonedInvitation = cloneInvitation(invitation);
|
||||||
return getCachedInvitation(invitation.token);
|
localDeliveryInvitationCache.set(clonedInvitation.token, clonedInvitation);
|
||||||
|
|
||||||
|
if (isLocalClientInvitationToken(clonedInvitation.token)) {
|
||||||
|
writeStoredInvitation(clonedInvitation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getCachedInvitation(clonedInvitation.token);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildFallbackInvitation = (token) =>
|
const buildFallbackInvitation = (token) =>
|
||||||
|
|
@ -125,7 +169,10 @@ export const fetchDeliveryInvitation = async (token) => {
|
||||||
|
|
||||||
if (isLocalClientInvitationToken(token)) {
|
if (isLocalClientInvitationToken(token)) {
|
||||||
const cachedInvitation = getCachedInvitation(token);
|
const cachedInvitation = getCachedInvitation(token);
|
||||||
const invitation = cachedInvitation ?? cacheInvitation(buildFallbackInvitation(token));
|
const storedInvitation = cachedInvitation ?? readStoredInvitation(token);
|
||||||
|
const invitation = storedInvitation
|
||||||
|
? cacheInvitation(storedInvitation)
|
||||||
|
: cacheInvitation(buildFallbackInvitation(token));
|
||||||
warmLocalInvitationCache(token, "get-delivery-invitation");
|
warmLocalInvitationCache(token, "get-delivery-invitation");
|
||||||
return invitation;
|
return invitation;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,19 @@ describe("deliveryInvitationApi", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
invoke.mockReset();
|
invoke.mockReset();
|
||||||
__resetLocalDeliveryInvitationCache();
|
__resetLocalDeliveryInvitationCache();
|
||||||
|
const storage = new Map();
|
||||||
|
vi.stubGlobal("localStorage", {
|
||||||
|
getItem: (key) => storage.get(key) ?? null,
|
||||||
|
setItem: (key, value) => {
|
||||||
|
storage.set(key, String(value));
|
||||||
|
},
|
||||||
|
removeItem: (key) => {
|
||||||
|
storage.delete(key);
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
storage.clear();
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("loads a delivery invitation by token", async () => {
|
it("loads a delivery invitation by token", async () => {
|
||||||
|
|
@ -173,6 +186,27 @@ describe("deliveryInvitationApi", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("restores the saved local client invitation after cache reset", async () => {
|
||||||
|
invoke.mockRejectedValueOnce(new Error("worker boot error"));
|
||||||
|
await fetchDeliveryInvitation("client-flow-1001");
|
||||||
|
|
||||||
|
invoke.mockRejectedValueOnce(new Error("worker boot error"));
|
||||||
|
await confirmDeliveryChoice({
|
||||||
|
token: "client-flow-1001",
|
||||||
|
deliveryDate: "2026-04-16",
|
||||||
|
deliveryTime: "После обеда",
|
||||||
|
});
|
||||||
|
|
||||||
|
__resetLocalDeliveryInvitationCache();
|
||||||
|
|
||||||
|
await expect(fetchDeliveryInvitation("client-flow-1001")).resolves.toMatchObject({
|
||||||
|
token: "client-flow-1001",
|
||||||
|
deliveryDate: "2026-04-16",
|
||||||
|
deliveryTime: "После обеда",
|
||||||
|
state: "confirmed",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("creates a delivery invitation from order data", async () => {
|
it("creates a delivery invitation from order data", async () => {
|
||||||
invoke.mockResolvedValueOnce({
|
invoke.mockResolvedValueOnce({
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue