fix(chat): isolate pending flows and cancel stale requests
This commit is contained in:
@@ -39,6 +39,7 @@ describe("subscription exit helpers", () => {
|
||||
it("uses explicit chat image return urls first", async () => {
|
||||
consumePendingChatImageReturnMock.mockResolvedValue({
|
||||
reason: "image_paywall",
|
||||
characterId: "character_elio",
|
||||
messageId: "msg_1",
|
||||
returnUrl: "/chat?image=msg_1",
|
||||
createdAt: 1,
|
||||
@@ -73,12 +74,14 @@ describe("subscription exit helpers", () => {
|
||||
it("returns directly to private room without consuming chat state", async () => {
|
||||
consumePendingChatImageReturnMock.mockResolvedValue({
|
||||
reason: "image_paywall",
|
||||
characterId: "character_elio",
|
||||
messageId: "msg_1",
|
||||
returnUrl: "/chat?image=msg_1",
|
||||
createdAt: 1,
|
||||
});
|
||||
peekPendingChatUnlockMock.mockResolvedValue({
|
||||
reason: "single_message_unlock",
|
||||
characterId: "character_elio",
|
||||
displayMessageId: "msg_1",
|
||||
messageId: "msg_1",
|
||||
kind: "image",
|
||||
@@ -101,6 +104,7 @@ describe("subscription exit helpers", () => {
|
||||
it("keeps private room ahead of stale chat state after payment", async () => {
|
||||
peekPendingChatUnlockMock.mockResolvedValue({
|
||||
reason: "single_message_unlock",
|
||||
characterId: "character_elio",
|
||||
displayMessageId: "msg_1",
|
||||
messageId: "msg_1",
|
||||
kind: "image",
|
||||
@@ -119,6 +123,7 @@ describe("subscription exit helpers", () => {
|
||||
it("keeps pending chat unlocks ahead of chat fallback after payment", async () => {
|
||||
peekPendingChatUnlockMock.mockResolvedValue({
|
||||
reason: "single_message_unlock",
|
||||
characterId: "character_elio",
|
||||
displayMessageId: "msg_1",
|
||||
messageId: "msg_1",
|
||||
kind: "image",
|
||||
@@ -136,6 +141,7 @@ describe("subscription exit helpers", () => {
|
||||
it("peeks chat unlock return urls without clearing them", async () => {
|
||||
peekPendingChatUnlockMock.mockResolvedValue({
|
||||
reason: "single_message_unlock",
|
||||
characterId: "character_elio",
|
||||
displayMessageId: "msg_1",
|
||||
messageId: "msg_1",
|
||||
kind: "image",
|
||||
@@ -154,6 +160,7 @@ describe("subscription exit helpers", () => {
|
||||
consumePendingChatImageReturnMock.mockResolvedValue(null);
|
||||
peekPendingChatUnlockMock.mockResolvedValue({
|
||||
reason: "single_message_unlock",
|
||||
characterId: "character_elio",
|
||||
displayMessageId: "msg_1",
|
||||
messageId: "msg_1",
|
||||
kind: "image",
|
||||
|
||||
@@ -8,12 +8,15 @@ import {
|
||||
export type { PendingChatImageReturn };
|
||||
|
||||
export async function savePendingChatImageReturn(input: {
|
||||
characterId: string;
|
||||
messageId: string;
|
||||
returnUrl: string;
|
||||
}): Promise<void> {
|
||||
await NavigationStorage.savePendingChatImageReturn(input);
|
||||
}
|
||||
|
||||
export async function consumePendingChatImageReturn(): Promise<PendingChatImageReturn | null> {
|
||||
return NavigationStorage.consumePendingChatImageReturn();
|
||||
export async function consumePendingChatImageReturn(
|
||||
characterId: string,
|
||||
): Promise<PendingChatImageReturn | null> {
|
||||
return NavigationStorage.consumePendingChatImageReturn(characterId);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export type {
|
||||
};
|
||||
|
||||
export async function savePendingChatUnlock(input: {
|
||||
characterId: string;
|
||||
displayMessageId?: string;
|
||||
messageId?: string;
|
||||
kind: PendingChatUnlockKind;
|
||||
@@ -30,16 +31,20 @@ export async function savePendingChatUnlock(input: {
|
||||
await NavigationStorage.savePendingChatUnlock(input);
|
||||
}
|
||||
|
||||
export async function consumePendingChatUnlock(): Promise<PendingChatUnlock | null> {
|
||||
return NavigationStorage.consumePendingChatUnlock();
|
||||
export async function consumePendingChatUnlock(
|
||||
characterId: string,
|
||||
): Promise<PendingChatUnlock | null> {
|
||||
return NavigationStorage.consumePendingChatUnlock(characterId);
|
||||
}
|
||||
|
||||
export async function peekPendingChatUnlock(): Promise<PendingChatUnlock | null> {
|
||||
return NavigationStorage.peekPendingChatUnlock();
|
||||
export async function peekPendingChatUnlock(
|
||||
characterId: string,
|
||||
): Promise<PendingChatUnlock | null> {
|
||||
return NavigationStorage.peekPendingChatUnlock(characterId);
|
||||
}
|
||||
|
||||
export async function hasPendingChatUnlock(): Promise<boolean> {
|
||||
return NavigationStorage.hasPendingChatUnlock();
|
||||
export async function hasPendingChatUnlock(characterId: string): Promise<boolean> {
|
||||
return NavigationStorage.hasPendingChatUnlock(characterId);
|
||||
}
|
||||
|
||||
export async function clearPendingChatUnlock(): Promise<void> {
|
||||
@@ -48,12 +53,15 @@ export async function clearPendingChatUnlock(): Promise<void> {
|
||||
|
||||
export async function savePendingChatPromotion(
|
||||
promotionType: PendingChatPromotionType,
|
||||
characterId: string,
|
||||
): Promise<PendingChatPromotion> {
|
||||
return NavigationStorage.savePendingChatPromotion(promotionType);
|
||||
return NavigationStorage.savePendingChatPromotion(promotionType, characterId);
|
||||
}
|
||||
|
||||
export async function consumePendingChatPromotion(): Promise<PendingChatPromotion | null> {
|
||||
return NavigationStorage.consumePendingChatPromotion();
|
||||
export async function consumePendingChatPromotion(
|
||||
characterId: string,
|
||||
): Promise<PendingChatPromotion | null> {
|
||||
return NavigationStorage.consumePendingChatPromotion(characterId);
|
||||
}
|
||||
|
||||
export async function clearPendingChatPromotion(): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
import {
|
||||
DEFAULT_CHARACTER_ID,
|
||||
DEFAULT_CHARACTER_SLUG,
|
||||
getCharacterBySlug,
|
||||
} from "@/data/constants/character";
|
||||
import { getCharacterRoutes } from "@/router/routes";
|
||||
import type { AppSubscriptionReturnTo } from "@/router/navigation-types";
|
||||
|
||||
@@ -12,10 +16,13 @@ import {
|
||||
|
||||
export type SubscriptionReturnTo = AppSubscriptionReturnTo;
|
||||
|
||||
export async function consumeSubscriptionExplicitExitUrl(): Promise<string | null> {
|
||||
const pendingImageReturn = await consumePendingChatImageReturn();
|
||||
export async function consumeSubscriptionExplicitExitUrl(
|
||||
characterSlug: string = DEFAULT_CHARACTER_SLUG,
|
||||
): Promise<string | null> {
|
||||
const characterId = resolveCharacterId(characterSlug);
|
||||
const pendingImageReturn = await consumePendingChatImageReturn(characterId);
|
||||
if (pendingImageReturn) return pendingImageReturn.returnUrl;
|
||||
const pendingChatUnlock = await peekPendingChatUnlock();
|
||||
const pendingChatUnlock = await peekPendingChatUnlock(characterId);
|
||||
if (pendingChatUnlock) {
|
||||
await clearPendingChatUnlock();
|
||||
return pendingChatUnlock.returnUrl;
|
||||
@@ -23,8 +30,12 @@ export async function consumeSubscriptionExplicitExitUrl(): Promise<string | nul
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function peekSubscriptionExplicitExitUrl(): Promise<string | null> {
|
||||
const pendingChatUnlock = await peekPendingChatUnlock();
|
||||
export async function peekSubscriptionExplicitExitUrl(
|
||||
characterSlug: string = DEFAULT_CHARACTER_SLUG,
|
||||
): Promise<string | null> {
|
||||
const pendingChatUnlock = await peekPendingChatUnlock(
|
||||
resolveCharacterId(characterSlug),
|
||||
);
|
||||
if (pendingChatUnlock) return pendingChatUnlock.returnUrl;
|
||||
return null;
|
||||
}
|
||||
@@ -48,7 +59,7 @@ export async function consumeSubscriptionExitUrl(
|
||||
}
|
||||
|
||||
return (
|
||||
(await consumeSubscriptionExplicitExitUrl()) ??
|
||||
(await consumeSubscriptionExplicitExitUrl(characterSlug)) ??
|
||||
getSubscriptionFallbackExitUrl(returnTo, characterSlug)
|
||||
);
|
||||
}
|
||||
@@ -61,7 +72,11 @@ export async function resolveSubscriptionSuccessExitUrl(
|
||||
return getCharacterRoutes(characterSlug).privateRoom;
|
||||
}
|
||||
|
||||
const pendingExitUrl = await peekSubscriptionExplicitExitUrl();
|
||||
const pendingExitUrl = await peekSubscriptionExplicitExitUrl(characterSlug);
|
||||
if (pendingExitUrl) return pendingExitUrl;
|
||||
return consumeSubscriptionExitUrl(returnTo, characterSlug);
|
||||
}
|
||||
|
||||
function resolveCharacterId(characterSlug: string): string {
|
||||
return getCharacterBySlug(characterSlug)?.id ?? DEFAULT_CHARACTER_ID;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user