fix(chat): sync paid voice audio unlock flow

This commit is contained in:
2026-07-14 15:20:56 +08:00
parent 759481b621
commit ca55723e48
12 changed files with 78 additions and 88 deletions
@@ -14,7 +14,6 @@ function makeResponse(
): ChatSendResponse {
return ChatSendResponse.from({
reply: "AI reply",
audioUrl: "",
messageId: "msg-1",
isGuest: false,
timestamp: 1782356425363,
@@ -62,7 +61,6 @@ describe("sendResponseToUiMessage", () => {
it("maps locked voice messages without treating them as private text", () => {
const message = sendResponseToUiMessage(
makeResponse({
audioUrl: "",
lockDetail: {
locked: true,
reason: "voice_message",
@@ -78,6 +76,34 @@ describe("sendResponseToUiMessage", () => {
expect(message.privateMessageHint).toBeUndefined();
});
it("does not expose an audio URL from a locked send response", () => {
const message = sendResponseToUiMessage(
makeResponse({
audioUrl: "https://example.com/locked-voice.mp3",
lockDetail: {
locked: true,
reason: "voice_message",
},
}),
);
expect(message.audioUrl).toBeUndefined();
expect(message.locked).toBe(true);
});
it("maps an audio URL from an unlocked send response", () => {
const message = sendResponseToUiMessage(
makeResponse({
audioUrl: "https://example.com/unlocked-voice.mp3",
}),
);
expect(message.audioUrl).toBe(
"https://example.com/unlocked-voice.mp3",
);
expect(message.locked).toBe(false);
});
it("maps locked private text messages as private messages", () => {
const message = sendResponseToUiMessage(
makeResponse({
@@ -227,7 +253,7 @@ describe("localMessagesToUi", () => {
type: "voice",
content: "hidden voice transcript",
createdAt: "2026-06-25T12:00:00.000Z",
audioUrl: null,
audioUrl: "https://example.com/locked-history-voice.mp3",
image: { type: null, url: null },
lockDetail: {
locked: true,
@@ -57,10 +57,6 @@ function makeUnlockPrivateResponse(
creditsCharged: 10,
requiredCredits: 10,
shortfallCredits: 0,
lockDetail: {
locked: false,
reason: null,
},
...overrides,
});
}
@@ -822,10 +818,6 @@ describe("chatMachine transitions", () => {
creditsCharged: 0,
requiredCredits: 10,
shortfallCredits: 7,
lockDetail: {
locked: true,
reason: "voice_message",
},
}),
},
}),
@@ -57,10 +57,6 @@ describe("chat promotion", () => {
type: "promotion",
url: "https://example.com/unlocked.jpg",
},
lockDetail: {
locked: false,
reason: null,
},
}),
});
+6 -2
View File
@@ -27,7 +27,9 @@ export function localMessagesToUi(
}),
isFromAI: m.role === "assistant",
date: messageDateFromCreatedAt(m.createdAt),
...(m.audioUrl ? { audioUrl: m.audioUrl } : {}),
...(m.audioUrl && m.lockDetail?.locked !== true
? { audioUrl: m.audioUrl }
: {}),
...deriveUiLockFields(m.lockDetail, m.image?.url),
}));
}
@@ -46,7 +48,9 @@ export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage {
}),
isFromAI: true,
date: todayString(new Date(response.timestamp)),
...(response.audioUrl ? { audioUrl: response.audioUrl } : {}),
...(response.audioUrl && !response.lockDetail.locked
? { audioUrl: response.audioUrl }
: {}),
...deriveUiLockFields(response.lockDetail, response.image.url),
};
}
+1 -1
View File
@@ -197,7 +197,7 @@ function hasRenderableSendResponse(response: ChatSendResponse): boolean {
return (
response.reply.trim().length > 0 ||
response.audioUrl.trim().length > 0 ||
(!response.lockDetail.locked && response.audioUrl.trim().length > 0) ||
Boolean(response.image.url) ||
isLockedPaidMessage
);
+1 -1
View File
@@ -80,7 +80,7 @@ export const unlockMessageActor = fromPromise<
...(unlockResult.data.image.url
? { image: unlockResult.data.image }
: {}),
lockDetail: unlockResult.data.lockDetail,
lockDetail: { locked: false, reason: null },
},
cacheIdentityResult.data,
);
+3 -15
View File
@@ -48,27 +48,15 @@ export function applySingleUnlockOutput(
content: resolvedContent,
audioUrl: getUnlockedAudioUrl(message, output.response),
imageUrl: resolvedImageUrl,
locked: output.response.lockDetail.locked,
lockReason: output.response.lockDetail.reason,
imagePaywalled: resolvedImageUrl
? isLockedImage(output.response.lockDetail)
: undefined,
locked: false,
lockReason: null,
imagePaywalled: resolvedImageUrl ? false : undefined,
lockedPrivate: false,
privateMessageHint: null,
};
});
}
function isLockedImage(lockDetail: {
locked: boolean;
reason: string | null;
}): boolean {
return (
lockDetail.locked &&
(lockDetail.reason === "image_paywall" || lockDetail.reason === "image")
);
}
function getUnlockedAudioUrl(
message: UiMessage,
response: UnlockPrivateResponse,