feat(payment): add chat support discounts and gratitude
Docker Image / Build and Push Docker Image (push) Successful in 2m14s

(cherry picked from commit ef9b79bc83)
This commit is contained in:
Codex
2026-07-28 17:42:53 +08:00
parent 59e4eac736
commit 74b7eae18b
35 changed files with 768 additions and 56 deletions
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { appendCommercialMessage } from "../helper/commercial-message";
describe("chat commercial message", () => {
const incoming = {
messageId: "payment-thanks-1",
message: "Thank you for supporting me.",
characterId: "maya-tan",
createdAt: "2026-07-28T08:30:00.000Z",
};
it("appends one persisted payment thank-you to the matching character chat", () => {
const messages = appendCommercialMessage([], "maya-tan", incoming);
expect(messages).toHaveLength(1);
expect(messages[0]).toMatchObject({
remoteId: "payment-thanks-1",
content: "Thank you for supporting me.",
isFromAI: true,
});
});
it("ignores duplicate delivery and a message for another character", () => {
const messages = appendCommercialMessage([], "maya-tan", incoming);
expect(appendCommercialMessage(messages, "maya-tan", incoming)).toEqual(
messages,
);
expect(appendCommercialMessage(messages, "elio", incoming)).toEqual(
messages,
);
});
});
+9
View File
@@ -28,6 +28,15 @@ export type ChatEvent =
}
| { type: "ChatOlderHistoryLoadFailed"; error: unknown }
| { type: "ChatHistoryRefreshRequested" }
| {
type: "ChatCommercialMessageReceived";
message: {
messageId: string;
message: string;
characterId: string;
createdAt: string;
};
}
// Chat domain events.
| { type: "ChatSendMessage"; content: string }
| { type: "ChatSendImage"; imageBase64: string }
@@ -0,0 +1,30 @@
import { createRemoteUiMessageIdentity, type UiMessage } from "../ui-message";
import { todayString } from "@/utils/date";
export interface IncomingCommercialMessage {
messageId: string;
message: string;
characterId: string;
createdAt: string;
}
export function appendCommercialMessage(
messages: readonly UiMessage[],
activeCharacterId: string,
incoming: IncomingCommercialMessage,
): UiMessage[] {
if (incoming.characterId !== activeCharacterId) return [...messages];
if (messages.some((message) => message.remoteId === incoming.messageId)) {
return [...messages];
}
const createdAt = new Date(incoming.createdAt);
return [
...messages,
{
...createRemoteUiMessageIdentity(incoming.messageId, true),
content: incoming.message,
isFromAI: true,
date: todayString(Number.isNaN(createdAt.getTime()) ? new Date() : createdAt),
},
];
}
+16
View File
@@ -1,4 +1,5 @@
import { createChatPromotionState } from "../helper/promotion";
import { appendCommercialMessage } from "../helper/commercial-message";
import { shouldPromptUnlockHistory } from "../helper/unlock";
import { createInitialChatState } from "../chat-state";
import {
@@ -53,6 +54,19 @@ const injectPromotionAction = unlockMachineSetup.assign(({ event }) => {
const clearPromotionAction = unlockMachineSetup.assign({ promotion: null });
const appendCommercialMessageAction = unlockMachineSetup.assign(
({ context, event }) => {
if (event.type !== "ChatCommercialMessageReceived") return {};
return {
messages: appendCommercialMessage(
context.messages,
context.characterId,
event.message,
),
};
},
);
export const chatMachineSetup = unlockMachineSetup.extend({
actions: {
startGuestSession: startGuestSessionAction,
@@ -60,6 +74,7 @@ export const chatMachineSetup = unlockMachineSetup.extend({
clearChatSession: clearChatSessionAction,
injectPromotion: injectPromotionAction,
clearPromotion: clearPromotionAction,
appendCommercialMessage: appendCommercialMessageAction,
},
});
@@ -258,6 +273,7 @@ export const chatRootStateConfig = chatMachineSetup.createStateConfig({
on: {
ChatPromotionInjected: { actions: "injectPromotion" },
ChatPromotionCleared: { actions: "clearPromotion" },
ChatCommercialMessageReceived: { actions: "appendCommercialMessage" },
},
states: {
idle: idleState,