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
+109
View File
@@ -0,0 +1,109 @@
"use client";
const STORAGE_KEY = "cozsweet.chat.pendingUnlock";
const MAX_AGE_MS = 30 * 60 * 1000;
export type PendingChatUnlockKind = "private" | "voice" | "image";
export type PendingChatUnlockStage = "auth" | "payment";
export interface PendingChatUnlock {
reason: "single_message_unlock";
messageId: string;
kind: PendingChatUnlockKind;
returnUrl: string;
stage: PendingChatUnlockStage;
createdAt: number;
}
export function savePendingChatUnlock(input: {
messageId: string;
kind: PendingChatUnlockKind;
returnUrl: string;
stage: PendingChatUnlockStage;
}): void {
if (typeof window === "undefined") return;
const payload: PendingChatUnlock = {
reason: "single_message_unlock",
messageId: input.messageId,
kind: input.kind,
returnUrl: input.returnUrl,
stage: input.stage,
createdAt: Date.now(),
};
window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
}
export function consumePendingChatUnlock(): PendingChatUnlock | null {
if (typeof window === "undefined") return null;
const raw = window.sessionStorage.getItem(STORAGE_KEY);
window.sessionStorage.removeItem(STORAGE_KEY);
if (!raw) return null;
return parsePendingChatUnlock(raw);
}
export function peekPendingChatUnlock(): PendingChatUnlock | null {
if (typeof window === "undefined") return null;
const raw = window.sessionStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const value = parsePendingChatUnlock(raw);
if (!value) {
window.sessionStorage.removeItem(STORAGE_KEY);
}
return value;
}
export function hasPendingChatUnlock(): boolean {
return peekPendingChatUnlock() !== null;
}
export function clearPendingChatUnlock(): void {
if (typeof window === "undefined") return;
window.sessionStorage.removeItem(STORAGE_KEY);
}
function parsePendingChatUnlock(raw: string): PendingChatUnlock | null {
try {
const value = JSON.parse(raw) as Partial<PendingChatUnlock>;
if (value.reason !== "single_message_unlock") return null;
if (typeof value.messageId !== "string" || value.messageId.length === 0) {
return null;
}
if (!isPendingChatUnlockKind(value.kind)) return null;
if (!isPendingChatUnlockStage(value.stage)) return null;
if (typeof value.returnUrl !== "string" || value.returnUrl.length === 0) {
return null;
}
if (!value.returnUrl.startsWith("/") || value.returnUrl.startsWith("//")) {
return null;
}
if (typeof value.createdAt !== "number") return null;
if (Date.now() - value.createdAt > MAX_AGE_MS) return null;
return {
reason: value.reason,
messageId: value.messageId,
kind: value.kind,
returnUrl: value.returnUrl,
stage: value.stage,
createdAt: value.createdAt,
};
} catch {
return null;
}
}
function isPendingChatUnlockKind(
value: unknown,
): value is PendingChatUnlockKind {
return value === "private" || value === "voice" || value === "image";
}
function isPendingChatUnlockStage(
value: unknown,
): value is PendingChatUnlockStage {
return value === "auth" || value === "payment";
}