feat(characters): use local character catalog
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { getCharacterRoutes, ROUTES } from "@/router/routes";
|
||||
|
||||
import {
|
||||
resolveExternalEntryDestination,
|
||||
@@ -33,7 +33,21 @@ describe("external entry navigation", () => {
|
||||
});
|
||||
|
||||
it("routes every tip entry to the tier selection page", () => {
|
||||
expect(resolveExternalEntryDestination({ target: "tip" })).toBe("/tip");
|
||||
expect(resolveExternalEntryDestination({ target: "tip" })).toBe(
|
||||
getCharacterRoutes("elio").tip,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a known character slug and falls back to Elio", () => {
|
||||
expect(
|
||||
resolveExternalEntryDestination({ target: "chat", character: "maya" }),
|
||||
).toBe(getCharacterRoutes("maya").chat);
|
||||
expect(
|
||||
resolveExternalEntryDestination({
|
||||
target: "private-room",
|
||||
character: "unknown",
|
||||
}),
|
||||
).toBe(getCharacterRoutes("elio").privateRoom);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { getCharacterRoutes } from "@/router/routes";
|
||||
|
||||
import { consumePendingChatImageReturn } from "../chat_image_return_session";
|
||||
import {
|
||||
@@ -53,16 +53,20 @@ describe("subscription exit helpers", () => {
|
||||
consumePendingChatImageReturnMock.mockResolvedValue(null);
|
||||
peekPendingChatUnlockMock.mockResolvedValue(null);
|
||||
|
||||
await expect(consumeSubscriptionExitUrl("chat")).resolves.toBe(ROUTES.chat);
|
||||
await expect(consumeSubscriptionExitUrl("chat")).resolves.toBe(
|
||||
getCharacterRoutes("elio").chat,
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to sidebar by default", async () => {
|
||||
consumePendingChatImageReturnMock.mockResolvedValue(null);
|
||||
peekPendingChatUnlockMock.mockResolvedValue(null);
|
||||
|
||||
expect(getSubscriptionFallbackExitUrl(null)).toBe(ROUTES.sidebar);
|
||||
expect(getSubscriptionFallbackExitUrl(null)).toBe(
|
||||
getCharacterRoutes("elio").sidebar,
|
||||
);
|
||||
await expect(consumeSubscriptionExitUrl(null)).resolves.toBe(
|
||||
ROUTES.sidebar,
|
||||
getCharacterRoutes("elio").sidebar,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -84,10 +88,10 @@ describe("subscription exit helpers", () => {
|
||||
});
|
||||
|
||||
expect(getSubscriptionFallbackExitUrl("private-room")).toBe(
|
||||
ROUTES.privateRoom,
|
||||
getCharacterRoutes("elio").privateRoom,
|
||||
);
|
||||
await expect(consumeSubscriptionExitUrl("private-room")).resolves.toBe(
|
||||
ROUTES.privateRoom,
|
||||
getCharacterRoutes("elio").privateRoom,
|
||||
);
|
||||
expect(consumePendingChatImageReturnMock).not.toHaveBeenCalled();
|
||||
expect(peekPendingChatUnlockMock).not.toHaveBeenCalled();
|
||||
@@ -106,8 +110,8 @@ describe("subscription exit helpers", () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveSubscriptionSuccessExitUrl("private-room"),
|
||||
).resolves.toBe(ROUTES.privateRoom);
|
||||
resolveSubscriptionSuccessExitUrl("private-room", "maya"),
|
||||
).resolves.toBe(getCharacterRoutes("maya").privateRoom);
|
||||
expect(peekPendingChatUnlockMock).not.toHaveBeenCalled();
|
||||
expect(clearPendingChatUnlockMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -4,7 +4,11 @@ import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||
import { UserStorage } from "@/data/storage/user/user_storage";
|
||||
import type { PendingChatPromotionType } from "@/data/storage/navigation";
|
||||
import type { LoginStatus } from "@/data/schemas/auth";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import {
|
||||
DEFAULT_CHARACTER_SLUG,
|
||||
getCharacterBySlug,
|
||||
} from "@/data/constants/character";
|
||||
import { getCharacterRoutes, ROUTES } from "@/router/routes";
|
||||
|
||||
export type ExternalEntryTarget =
|
||||
| typeof ROUTES.chat
|
||||
@@ -20,6 +24,7 @@ export interface ExternalEntryPayload {
|
||||
|
||||
export interface ExternalEntryTargetInput {
|
||||
target?: string | null;
|
||||
character?: string | null;
|
||||
}
|
||||
|
||||
export interface ExternalEntryPromotionInput {
|
||||
@@ -79,8 +84,15 @@ export function resolveExternalEntryTarget({
|
||||
|
||||
export function resolveExternalEntryDestination({
|
||||
target,
|
||||
character,
|
||||
}: ExternalEntryTargetInput): string {
|
||||
return resolveExternalEntryTarget({ target });
|
||||
const characterSlug =
|
||||
getCharacterBySlug(character)?.slug ?? DEFAULT_CHARACTER_SLUG;
|
||||
const routes = getCharacterRoutes(characterSlug);
|
||||
const resolvedTarget = resolveExternalEntryTarget({ target });
|
||||
if (resolvedTarget === ROUTES.tip) return routes.tip;
|
||||
if (resolvedTarget === ROUTES.privateRoom) return routes.privateRoom;
|
||||
return routes.chat;
|
||||
}
|
||||
|
||||
export async function persistExternalEntryPayload({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import { DEFAULT_CHARACTER_SLUG } from "@/data/constants/character";
|
||||
import { getCharacterRoutes } from "@/router/routes";
|
||||
import type { AppSubscriptionReturnTo } from "@/router/navigation-types";
|
||||
|
||||
import { consumePendingChatImageReturn } from "./chat_image_return_session";
|
||||
@@ -30,29 +31,37 @@ export async function peekSubscriptionExplicitExitUrl(): Promise<string | null>
|
||||
|
||||
export function getSubscriptionFallbackExitUrl(
|
||||
returnTo: SubscriptionReturnTo,
|
||||
characterSlug: string = DEFAULT_CHARACTER_SLUG,
|
||||
): string {
|
||||
if (returnTo === "chat") return ROUTES.chat;
|
||||
if (returnTo === "private-room") return ROUTES.privateRoom;
|
||||
return ROUTES.sidebar;
|
||||
const routes = getCharacterRoutes(characterSlug);
|
||||
if (returnTo === "chat") return routes.chat;
|
||||
if (returnTo === "private-room") return routes.privateRoom;
|
||||
return routes.sidebar;
|
||||
}
|
||||
|
||||
export async function consumeSubscriptionExitUrl(
|
||||
returnTo: SubscriptionReturnTo,
|
||||
characterSlug: string = DEFAULT_CHARACTER_SLUG,
|
||||
): Promise<string> {
|
||||
if (returnTo === "private-room") return ROUTES.privateRoom;
|
||||
if (returnTo === "private-room") {
|
||||
return getCharacterRoutes(characterSlug).privateRoom;
|
||||
}
|
||||
|
||||
return (
|
||||
(await consumeSubscriptionExplicitExitUrl()) ??
|
||||
getSubscriptionFallbackExitUrl(returnTo)
|
||||
getSubscriptionFallbackExitUrl(returnTo, characterSlug)
|
||||
);
|
||||
}
|
||||
|
||||
export async function resolveSubscriptionSuccessExitUrl(
|
||||
returnTo: SubscriptionReturnTo,
|
||||
characterSlug: string = DEFAULT_CHARACTER_SLUG,
|
||||
): Promise<string> {
|
||||
if (returnTo === "private-room") return ROUTES.privateRoom;
|
||||
if (returnTo === "private-room") {
|
||||
return getCharacterRoutes(characterSlug).privateRoom;
|
||||
}
|
||||
|
||||
const pendingExitUrl = await peekSubscriptionExplicitExitUrl();
|
||||
if (pendingExitUrl) return pendingExitUrl;
|
||||
return consumeSubscriptionExitUrl(returnTo);
|
||||
return consumeSubscriptionExitUrl(returnTo, characterSlug);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ describe("pending payment order helpers", () => {
|
||||
returnTo: "chat",
|
||||
subscriptionType: "topup",
|
||||
}),
|
||||
).toBe("/subscription?type=topup&payChannel=ezpay&paymentReturn=1&returnTo=chat");
|
||||
).toBe(
|
||||
"/subscription?type=topup&payChannel=ezpay&paymentReturn=1&returnTo=chat&character=elio",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves private room returns when rebuilding Ezpay urls", () => {
|
||||
@@ -26,7 +28,7 @@ describe("pending payment order helpers", () => {
|
||||
subscriptionType: "topup",
|
||||
}),
|
||||
).toBe(
|
||||
"/subscription?type=topup&payChannel=ezpay&paymentReturn=1&returnTo=private-room",
|
||||
"/subscription?type=topup&payChannel=ezpay&paymentReturn=1&returnTo=private-room&character=elio",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -59,7 +61,9 @@ describe("pending payment order helpers", () => {
|
||||
subscriptionType: "tip",
|
||||
tipCoffeeType: "large",
|
||||
}),
|
||||
).toBe("/tip?payChannel=ezpay&paymentReturn=1&coffee_type=large");
|
||||
).toBe(
|
||||
"/characters/elio/tip?payChannel=ezpay&paymentReturn=1&coffee_type=large",
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults legacy tip payment returns to medium coffee", () => {
|
||||
@@ -68,6 +72,21 @@ describe("pending payment order helpers", () => {
|
||||
payChannel: "ezpay",
|
||||
subscriptionType: "tip",
|
||||
}),
|
||||
).toBe("/tip?payChannel=ezpay&paymentReturn=1&coffee_type=medium");
|
||||
).toBe(
|
||||
"/characters/elio/tip?payChannel=ezpay&paymentReturn=1&coffee_type=medium",
|
||||
);
|
||||
});
|
||||
|
||||
it("restores the character that started an Ezpay tip", () => {
|
||||
expect(
|
||||
buildPendingPaymentSubscriptionUrl({
|
||||
payChannel: "ezpay",
|
||||
subscriptionType: "tip",
|
||||
tipCoffeeType: "small",
|
||||
characterSlug: "nayeli",
|
||||
}),
|
||||
).toBe(
|
||||
"/characters/nayeli/tip?payChannel=ezpay&paymentReturn=1&coffee_type=small",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,7 @@ export interface LaunchEzpayRedirectInput {
|
||||
subscriptionType: PendingPaymentSubscriptionType;
|
||||
tipCoffeeType?: PendingPaymentTipCoffeeType;
|
||||
returnTo?: PendingPaymentReturnTo;
|
||||
characterSlug?: string;
|
||||
onOpened?: () => void;
|
||||
onFailed: (errorMessage: string) => void;
|
||||
}
|
||||
@@ -75,6 +76,7 @@ export async function launchEzpayRedirect({
|
||||
subscriptionType,
|
||||
tipCoffeeType,
|
||||
returnTo,
|
||||
characterSlug,
|
||||
onOpened,
|
||||
onFailed,
|
||||
}: LaunchEzpayRedirectInput): Promise<void> {
|
||||
@@ -101,6 +103,7 @@ export async function launchEzpayRedirect({
|
||||
subscriptionType,
|
||||
...(tipCoffeeType ? { tipCoffeeType } : {}),
|
||||
...(returnTo ? { returnTo } : {}),
|
||||
...(characterSlug ? { characterSlug } : {}),
|
||||
});
|
||||
if (Result.isErr(saveResult)) {
|
||||
const errorMessage =
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
PendingPaymentOrderStorage,
|
||||
type PendingPaymentOrder,
|
||||
} from "@/data/storage/payment/pending_payment_order_storage";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import {
|
||||
DEFAULT_CHARACTER_SLUG,
|
||||
getCharacterBySlug,
|
||||
} from "@/data/constants/character";
|
||||
import { getCharacterRoutes, ROUTES } from "@/router/routes";
|
||||
import {
|
||||
DEFAULT_TIP_COFFEE_TYPE,
|
||||
TIP_COFFEE_TYPE_PARAM,
|
||||
@@ -23,6 +27,7 @@ export function savePendingEzpayOrder(input: {
|
||||
subscriptionType: PendingPaymentSubscriptionType;
|
||||
tipCoffeeType?: PendingPaymentTipCoffeeType;
|
||||
returnTo?: PendingPaymentReturnTo;
|
||||
characterSlug?: string;
|
||||
createdAt?: number;
|
||||
}): Promise<Result<void>> {
|
||||
return PendingPaymentOrderStorage.setPendingOrder({
|
||||
@@ -31,6 +36,7 @@ export function savePendingEzpayOrder(input: {
|
||||
subscriptionType: input.subscriptionType,
|
||||
...(input.tipCoffeeType ? { tipCoffeeType: input.tipCoffeeType } : {}),
|
||||
...(input.returnTo ? { returnTo: input.returnTo } : {}),
|
||||
...(input.characterSlug ? { characterSlug: input.characterSlug } : {}),
|
||||
createdAt: input.createdAt ?? Date.now(),
|
||||
});
|
||||
}
|
||||
@@ -54,9 +60,17 @@ export function clearPendingPaymentOrder(): Promise<Result<void>> {
|
||||
export function buildPendingPaymentSubscriptionUrl(
|
||||
order: Pick<
|
||||
PendingPaymentOrder,
|
||||
"payChannel" | "returnTo" | "subscriptionType" | "tipCoffeeType"
|
||||
| "payChannel"
|
||||
| "returnTo"
|
||||
| "subscriptionType"
|
||||
| "tipCoffeeType"
|
||||
| "characterSlug"
|
||||
>,
|
||||
): string {
|
||||
const characterSlug =
|
||||
getCharacterBySlug(order.characterSlug)?.slug ?? DEFAULT_CHARACTER_SLUG;
|
||||
const characterRoutes = getCharacterRoutes(characterSlug);
|
||||
|
||||
if (order.subscriptionType === "tip") {
|
||||
const params = new URLSearchParams({
|
||||
payChannel: order.payChannel,
|
||||
@@ -64,7 +78,7 @@ export function buildPendingPaymentSubscriptionUrl(
|
||||
[TIP_COFFEE_TYPE_PARAM]:
|
||||
order.tipCoffeeType ?? DEFAULT_TIP_COFFEE_TYPE,
|
||||
});
|
||||
return `${ROUTES.tip}?${params.toString()}`;
|
||||
return `${characterRoutes.tip}?${params.toString()}`;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
@@ -73,5 +87,6 @@ export function buildPendingPaymentSubscriptionUrl(
|
||||
paymentReturn: "1",
|
||||
});
|
||||
if (order.returnTo) params.set("returnTo", order.returnTo);
|
||||
params.set("character", characterSlug);
|
||||
return `${ROUTES.subscription}?${params.toString()}`;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,10 @@ export function getTipCoffeeOption(type: TipCoffeeType): TipCoffeeOption {
|
||||
return TIP_COFFEE_OPTION_BY_TYPE[type];
|
||||
}
|
||||
|
||||
export function buildTipCoffeePath(type: TipCoffeeType): string {
|
||||
export function buildTipCoffeePath(
|
||||
type: TipCoffeeType,
|
||||
basePath: string = ROUTES.tip,
|
||||
): string {
|
||||
const params = new URLSearchParams({ [TIP_COFFEE_TYPE_PARAM]: type });
|
||||
return `${ROUTES.tip}?${params.toString()}`;
|
||||
return `${basePath}?${params.toString()}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user