feat(chat): unlock paid messages before payment

This commit is contained in:
2026-06-30 18:56:17 +08:00
parent 5f94105bc6
commit e7a9e7abe5
21 changed files with 774 additions and 31 deletions
+108
View File
@@ -36,6 +36,7 @@ import type { ChatEvent } from "./chat-events";
import {
applyHttpSendOutput,
applyHistoryLoadedOutput,
applySingleUnlockOutput,
beginPendingReply,
countLockedHistoryMessages,
finishPendingReply,
@@ -48,6 +49,8 @@ import {
loadMoreHistoryActor,
httpMessageQueueActor,
unlockHistoryActor,
unlockMessageActor,
type UnlockMessageOutput,
} from "./chat-machine.actors";
const log = new Logger("StoresChatChatMachine");
@@ -71,6 +74,7 @@ export const chatMachine = setup({
loadMoreHistory: loadMoreHistoryActor,
httpMessageQueue: httpMessageQueueActor,
unlockHistory: unlockHistoryActor,
unlockMessage: unlockMessageActor,
},
actions: {
enqueueMessage: sendTo("messageQueue", ({ event }) => event),
@@ -242,6 +246,78 @@ export const chatMachine = setup({
unlockHistoryPromptVisible: true,
unlockHistoryError: "Failed to unlock messages. Please try again.",
})),
markUnlockMessageStarted: assign(({ event }) => {
if (event.type !== "ChatUnlockMessageRequested") return {};
return {
isUnlockingMessage: true,
unlockingMessageId: event.messageId,
unlockingMessageKind: event.kind,
unlockMessageError: null,
unlockPaywallRequest: null,
};
}),
applyUnlockMessageSucceeded: assign(({ context, event }) => {
const output = (event as { output?: UnlockMessageOutput }).output;
if (!output) return {};
return {
messages: applySingleUnlockOutput(context.messages, output),
isUnlockingMessage: false,
unlockingMessageId: null,
unlockingMessageKind: null,
unlockMessageError: null,
unlockPaywallRequest: null,
creditBalance: output.response.creditBalance,
creditsCharged: output.response.creditsCharged,
requiredCredits: 0,
shortfallCredits: 0,
};
}),
requestUnlockPaymentFromOutput: assign(({ context, event }) => {
const output = (event as { output?: UnlockMessageOutput }).output;
if (!output) return {};
return {
isUnlockingMessage: false,
unlockingMessageId: null,
unlockingMessageKind: null,
unlockMessageError: output.response.reason,
unlockPaywallRequest: {
messageId: output.messageId,
kind: context.unlockingMessageKind ?? "private",
reason: output.response.reason,
creditBalance: output.response.creditBalance,
requiredCredits: output.response.requiredCredits,
shortfallCredits: output.response.shortfallCredits,
},
creditBalance: output.response.creditBalance,
requiredCredits: output.response.requiredCredits,
shortfallCredits: output.response.shortfallCredits,
};
}),
requestUnlockPaymentFromError: assign(({ context }) => ({
isUnlockingMessage: false,
unlockMessageError: "unlock_failed",
unlockPaywallRequest:
context.unlockingMessageId && context.unlockingMessageKind
? {
messageId: context.unlockingMessageId,
kind: context.unlockingMessageKind,
reason: "unlock_failed",
creditBalance: context.creditBalance,
requiredCredits: context.requiredCredits,
shortfallCredits: context.shortfallCredits,
}
: null,
unlockingMessageId: null,
unlockingMessageKind: null,
})),
clearUnlockPaywallRequest: assign(() => ({
unlockPaywallRequest: null,
})),
},
}).createMachine({
id: "chat",
@@ -389,6 +465,9 @@ export const chatMachine = setup({
ChatUnlockHistoryDismissed: {
actions: "dismissUnlockHistoryPrompt",
},
ChatUnlockPaywallNavigationConsumed: {
actions: "clearUnlockPaywallRequest",
},
},
initial: "initializing",
states: {
@@ -456,6 +535,11 @@ export const chatMachine = setup({
ChatLoadMoreHistory: {
target: "loadingMore",
},
ChatUnlockMessageRequested: {
guard: ({ event }) => event.messageId.trim().length > 0,
target: "unlockingMessage",
actions: "markUnlockMessageStarted",
},
},
},
sendingViaHttp: {
@@ -529,6 +613,30 @@ export const chatMachine = setup({
},
},
},
unlockingMessage: {
invoke: {
id: "unlockMessage",
src: "unlockMessage",
input: ({ context }) => ({
messageId: context.unlockingMessageId ?? "",
}),
onDone: [
{
guard: ({ event }) => event.output.response.unlocked,
target: "ready",
actions: "applyUnlockMessageSucceeded",
},
{
target: "ready",
actions: "requestUnlockPaymentFromOutput",
},
],
onError: {
target: "ready",
actions: "requestUnlockPaymentFromError",
},
},
},
},
},
},