feat(chat): add promotional external entry flow

This commit is contained in:
2026-07-13 12:43:18 +08:00
parent e5ee9940ca
commit 3752b3b729
44 changed files with 1623 additions and 190 deletions
@@ -8,6 +8,10 @@ import {
} from "@/data/dto/chat";
import { chatMachine } from "@/stores/chat/chat-machine";
import type { ChatEvent } from "@/stores/chat/chat-events";
import type {
UnlockMessageOutput as MachineUnlockMessageOutput,
UnlockMessageRequest,
} from "@/stores/chat/chat-machine.helpers";
interface LoadMoreHistoryOutput {
messages: UiMessage[];
@@ -29,7 +33,7 @@ interface UnlockHistoryOutput {
newOffset: number;
}
interface UnlockMessageOutput {
interface TestUnlockMessageOutput {
messageId: string;
response: UnlockPrivateResponse;
}
@@ -81,7 +85,7 @@ function createTestChatMachine(
options: {
historyMessages?: UiMessage[];
unlockHistoryOutput?: UnlockHistoryOutput;
unlockMessageOutput?: UnlockMessageOutput;
unlockMessageOutput?: TestUnlockMessageOutput;
} = {},
) {
return chatMachine.provide({
@@ -124,13 +128,20 @@ function createTestChatMachine(
newOffset: 0,
...options.unlockHistoryOutput,
})),
unlockMessage: fromPromise<UnlockMessageOutput, { messageId: string }>(
async ({ input }) =>
options.unlockMessageOutput ?? {
messageId: input.messageId,
response: makeUnlockPrivateResponse(),
},
),
unlockMessage: fromPromise<
MachineUnlockMessageOutput,
UnlockMessageRequest
>(async ({ input }) => {
const output = options.unlockMessageOutput ?? {
messageId: input.messageId ?? input.displayMessageId,
response: makeUnlockPrivateResponse(),
};
return {
displayMessageId: input.displayMessageId,
request: input,
response: output.response,
};
}),
},
});
}
@@ -217,6 +228,45 @@ describe("chatMachine transitions", () => {
actor.stop();
});
it("keeps a promotion separate from normal history", async () => {
const actor = createActor(
createTestChatMachine({
historyMessages: [
{
id: "history-1",
content: "Existing history",
isFromAI: true,
date: "2026-07-13",
},
],
}),
).start();
actor.send({ type: "ChatUserLogin", token: "token" });
await waitFor(actor, (snapshot) =>
snapshot.matches({ userSession: "ready" }),
);
actor.send({
type: "ChatPromotionInjected",
promotion: {
promotionType: "voice",
lockType: "voice_message",
clientLockId: "promotion-1",
createdAt: 1,
},
});
const context = actor.getSnapshot().context;
expect(context.messages).toHaveLength(1);
expect(context.promotion?.message).toMatchObject({
id: "promotion:promotion-1",
locked: true,
lockReason: "voice_message",
});
actor.stop();
});
it("renders local history before network history sync finishes", async () => {
let resolveNetwork!: () => void;
const networkReleased = new Promise<void>((resolve) => {
@@ -586,7 +636,7 @@ describe("chatMachine transitions", () => {
unlockMessageOutput: {
messageId: "msg-private-locked",
response: makeUnlockPrivateResponse({
content: "This response content must be ignored.",
content: "Unlocked private message content.",
}),
},
}),
@@ -612,7 +662,7 @@ describe("chatMachine transitions", () => {
expect(actor.getSnapshot().context.messages).toMatchObject([
{
id: "msg-private-locked",
content: "Original private message content.",
content: "Unlocked private message content.",
locked: false,
lockReason: null,
lockedPrivate: false,
@@ -647,7 +697,7 @@ describe("chatMachine transitions", () => {
unlockMessageOutput: {
messageId: "msg-shared-id",
response: makeUnlockPrivateResponse({
content: "This response content must be ignored.",
content: "Unlocked private AI message.",
}),
},
}),
@@ -676,7 +726,7 @@ describe("chatMachine transitions", () => {
},
{
id: "msg-shared-id",
content: "Original AI private message",
content: "Unlocked private AI message.",
isFromAI: true,
locked: false,
lockReason: null,
@@ -848,6 +898,7 @@ describe("chatMachine transitions", () => {
);
expect(actor.getSnapshot().context.unlockPaywallRequest).toEqual({
displayMessageId: "msg-voice-locked",
messageId: "msg-voice-locked",
kind: "voice",
reason: "insufficient_balance",