fix(chat): suppress paywall for missing unlock

This commit is contained in:
2026-07-15 14:44:12 +08:00
parent 58ca40a728
commit 684ae4bf5d
2 changed files with 89 additions and 24 deletions
@@ -490,6 +490,68 @@ describe("chat unlock flow", () => {
actor.stop(); actor.stop();
}); });
it("does not request payment when the unlock target is not found", async () => {
const actor = createActor(
createTestChatMachine({
historyMessages: [
{
id: "msg-missing",
content: "",
isFromAI: true,
date: "2026-06-29",
locked: true,
lockReason: "private_message",
privateMessageHint: "A private message is waiting.",
},
],
unlockMessageOutput: {
messageId: "msg-missing",
response: makeUnlockPrivateResponse({
unlocked: false,
content: "",
reason: "not_found",
creditBalance: 3,
creditsCharged: 0,
requiredCredits: 0,
shortfallCredits: 0,
}),
},
}),
).start();
actor.send({ type: "ChatUserLogin", token: "token" });
await waitFor(actor, (snapshot) =>
snapshot.matches({ userSession: "ready" }),
);
actor.send({
type: "ChatUnlockMessageRequested",
messageId: "msg-missing",
kind: "private",
});
await waitFor(
actor,
(snapshot) => snapshot.context.unlockMessageError === "not_found",
);
expect(actor.getSnapshot().context).toMatchObject({
isUnlockingMessage: false,
unlockingMessageId: null,
unlockMessageError: "not_found",
unlockPaywallRequest: null,
});
expect(actor.getSnapshot().context.messages).toMatchObject([
{
id: "msg-missing",
locked: true,
lockReason: "private_message",
},
]);
actor.stop();
});
it("clears the insufficient credits prompt after payment succeeds", async () => { it("clears the insufficient credits prompt after payment succeeds", async () => {
const actor = createActor(createTestChatMachine()).start(); const actor = createActor(createTestChatMachine()).start();
+27 -24
View File
@@ -85,6 +85,7 @@ const applyUnlockMessageSucceededAction = sendMachineSetup.assign(
const requestUnlockPaymentFromOutputAction = sendMachineSetup.assign( const requestUnlockPaymentFromOutputAction = sendMachineSetup.assign(
({ context, event }) => { ({ context, event }) => {
const { output } = event as unknown as DoneActorEvent<UnlockMessageOutput>; const { output } = event as unknown as DoneActorEvent<UnlockMessageOutput>;
const shouldOpenPaywall = output.response.reason !== "not_found";
return { return {
promotion: applyPromotionUnlockOutput(context.promotion, output), promotion: applyPromotionUnlockOutput(context.promotion, output),
isUnlockingMessage: false, isUnlockingMessage: false,
@@ -94,30 +95,32 @@ const requestUnlockPaymentFromOutputAction = sendMachineSetup.assign(
unlockingLockType: null, unlockingLockType: null,
unlockingClientLockId: null, unlockingClientLockId: null,
unlockMessageError: output.response.reason, unlockMessageError: output.response.reason,
unlockPaywallRequest: { unlockPaywallRequest: shouldOpenPaywall
displayMessageId: ? {
output.response.messageId || output.displayMessageId, displayMessageId:
...(output.response.messageId || output.request.messageId output.response.messageId || output.displayMessageId,
? { ...(output.response.messageId || output.request.messageId
messageId: ? {
output.response.messageId || output.request.messageId, messageId:
} output.response.messageId || output.request.messageId,
: {}), }
kind: context.unlockingMessageKind ?? "private", : {}),
...(output.request.lockType kind: context.unlockingMessageKind ?? "private",
? { lockType: output.request.lockType } ...(output.request.lockType
: {}), ? { lockType: output.request.lockType }
...(output.request.clientLockId : {}),
? { clientLockId: output.request.clientLockId } ...(output.request.clientLockId
: {}), ? { clientLockId: output.request.clientLockId }
...(context.promotion?.message.id === output.displayMessageId : {}),
? { promotion: context.promotion.session } ...(context.promotion?.message.id === output.displayMessageId
: {}), ? { promotion: context.promotion.session }
reason: output.response.reason, : {}),
creditBalance: output.response.creditBalance, reason: output.response.reason,
requiredCredits: output.response.requiredCredits, creditBalance: output.response.creditBalance,
shortfallCredits: output.response.shortfallCredits, requiredCredits: output.response.requiredCredits,
}, shortfallCredits: output.response.shortfallCredits,
}
: null,
creditBalance: output.response.creditBalance, creditBalance: output.response.creditBalance,
requiredCredits: output.response.requiredCredits, requiredCredits: output.response.requiredCredits,
shortfallCredits: output.response.shortfallCredits, shortfallCredits: output.response.shortfallCredits,