refactor(chat): centralize unlock coordination

This commit is contained in:
2026-07-15 19:11:59 +08:00
parent 590dee417b
commit 47876484fc
5 changed files with 546 additions and 387 deletions
@@ -0,0 +1,188 @@
import { describe, expect, it } from "vitest";
import type {
PendingChatPromotion,
PendingChatUnlock,
} from "@/lib/navigation/chat_unlock_session";
import { createChatPromotionState } from "@/stores/chat/helper/promotion";
import {
resolveChatUnlockReturnUrl,
resolveMessageUnlockRequest,
shouldResumePendingChatUnlock,
} from "../hooks/use-chat-unlock-coordinator";
const defaultScope = {
defaultReturnUrl: "/chat",
imageMessageId: null,
imageReturnUrl: "/chat",
promotion: null,
};
const promotionSession: PendingChatPromotion = {
promotionType: "image",
lockType: "image_paywall",
clientLockId: "promotion-1",
createdAt: 1,
};
describe("chat unlock coordinator", () => {
it("creates a regular message unlock request with its remote id", () => {
expect(
resolveMessageUnlockRequest(
{ messageId: "message-1", kind: "private" },
defaultScope,
),
).toEqual({
displayMessageId: "message-1",
messageId: "message-1",
kind: "private",
returnUrl: "/chat",
});
});
it("returns to the active image overlay for its image unlock", () => {
const imageScope = {
...defaultScope,
imageMessageId: "image-1",
imageReturnUrl: "/chat?image=image-1",
};
expect(
resolveMessageUnlockRequest(
{ messageId: "image-1", kind: "image" },
imageScope,
).returnUrl,
).toBe("/chat?image=image-1");
});
it("uses the promotion lock identity without sending a temporary remote id", () => {
const promotion = createChatPromotionState(promotionSession);
expect(
resolveMessageUnlockRequest(
{ messageId: "promotion:promotion-1", kind: "image" },
{ ...defaultScope, promotion },
),
).toEqual({
displayMessageId: "promotion:promotion-1",
kind: "image",
lockType: "image_paywall",
clientLockId: "promotion-1",
promotion: promotionSession,
returnUrl: "/chat",
});
});
it("keeps the backend id after a promotion receives one", () => {
const promotion = createChatPromotionState(
promotionSession,
"promotion-message-1",
);
expect(
resolveMessageUnlockRequest(
{ messageId: "promotion-message-1", kind: "image" },
{ ...defaultScope, promotion },
).messageId,
).toBe("promotion-message-1");
});
it("resumes pending unlocks for the default chat return", () => {
expect(
shouldResumePendingChatUnlock(
createPendingUnlock({
displayMessageId: "message-1",
messageId: "message-1",
kind: "private",
returnUrl: "/chat",
}),
defaultScope,
),
).toBe(true);
});
it("only resumes an image return when its message matches the overlay", () => {
const imageScope = {
defaultReturnUrl: "/chat",
imageMessageId: "image-1",
imageReturnUrl: "/chat?image=image-1",
};
const pending = createPendingUnlock({
displayMessageId: "image-1",
messageId: "image-1",
kind: "image",
returnUrl: "/chat?image=image-1",
});
expect(shouldResumePendingChatUnlock(pending, imageScope)).toBe(true);
expect(
shouldResumePendingChatUnlock(
{ ...pending, displayMessageId: "image-2", messageId: "image-2" },
imageScope,
),
).toBe(false);
});
it("never routes a promotion unlock through the image overlay", () => {
const promotion = createChatPromotionState(promotionSession);
const imageMessageId = "promotion:promotion-1";
const imageScope = {
defaultReturnUrl: "/chat",
imageMessageId,
imageReturnUrl: `/chat?image=${imageMessageId}`,
};
expect(
resolveMessageUnlockRequest(
{ messageId: imageMessageId, kind: "image" },
{
...imageScope,
promotion,
},
).returnUrl,
).toBe("/chat");
expect(
shouldResumePendingChatUnlock(
{
...createPendingUnlock({
displayMessageId: imageMessageId,
kind: "image",
returnUrl: imageScope.imageReturnUrl,
}),
lockType: promotionSession.lockType,
clientLockId: promotionSession.clientLockId,
promotion: promotionSession,
},
imageScope,
),
).toBe(false);
});
it("keeps a regular image paywall in the chat flow when no overlay is open", () => {
expect(
resolveChatUnlockReturnUrl(
{
displayMessageId: "image-1",
messageId: "image-1",
kind: "image",
},
defaultScope,
),
).toBe("/chat");
});
});
function createPendingUnlock(
input: Pick<
PendingChatUnlock,
"displayMessageId" | "messageId" | "kind" | "returnUrl"
>,
): PendingChatUnlock {
return {
reason: "single_message_unlock",
...input,
stage: "auth",
createdAt: 1,
};
}