refactor(private-zoom): rename full feature surface

This commit is contained in:
Codex
2026-07-22 18:21:47 +08:00
parent e813333607
commit ed3b34ce1c
102 changed files with 695 additions and 519 deletions
@@ -85,8 +85,8 @@ describe("shared Tailwind components", () => {
<CharacterAvatar
src="/images/avatar/elio.png"
alt="Elio Silvestri"
actionLabel="Open Elio's private room"
analyticsKey="chat.open_private_room_from_avatar"
actionLabel="Open Elio's private zoom"
analyticsKey="chat.open_private_zoom_from_avatar"
onClick={() => undefined}
/>,
);
@@ -100,10 +100,10 @@ describe("shared Tailwind components", () => {
expect(characterHtml).toContain('<button type="button"');
expect(characterHtml).toContain(
'aria-label="Open Elio&#x27;s private room"',
'aria-label="Open Elio&#x27;s private zoom"',
);
expect(characterHtml).toContain(
'data-analytics-key="chat.open_private_room_from_avatar"',
'data-analytics-key="chat.open_private_zoom_from_avatar"',
);
expect(userHtml).toContain('<button type="button"');
expect(userHtml).toContain('aria-label="Open profile"');
@@ -79,18 +79,18 @@ describe("core Tailwind components", () => {
<AppBottomNav
activeItem="chat"
variant="dark"
privateRoomLabel="Maya Private room"
privateZoomLabel="Maya Private Zoom"
onChatClick={() => undefined}
onPrivateRoomClick={() => undefined}
onPrivateZoomClick={() => undefined}
/>,
);
const privateRoomHtml = renderToStaticMarkup(
const privateZoomHtml = renderToStaticMarkup(
<AppBottomNav
activeItem="privateRoom"
activeItem="privateZoom"
variant="warm"
privateRoomLabel="Maya Private room"
privateZoomLabel="Maya Private Zoom"
onChatClick={() => undefined}
onPrivateRoomClick={() => undefined}
onPrivateZoomClick={() => undefined}
/>,
);
@@ -99,13 +99,13 @@ describe("core Tailwind components", () => {
expect(chatHtml).toMatch(
/<button[^>]*aria-current="page"[^>]*>.*?<span>Chat<\/span>/,
);
expect(privateRoomHtml).toMatch(/class="[^"]*warm[^"]*"/);
expect(privateRoomHtml).toMatch(
/<button[^>]*aria-current="page"[^>]*>.*?<span>Maya Private room<\/span>/,
expect(privateZoomHtml).toMatch(/class="[^"]*warm[^"]*"/);
expect(privateZoomHtml).toMatch(
/<button[^>]*aria-current="page"[^>]*>.*?<span>Maya Private Zoom<\/span>/,
);
expect(chatHtml).toContain('data-analytics-key="navigation.chat"');
expect(privateRoomHtml).toContain(
'data-analytics-key="navigation.private_room"',
expect(privateZoomHtml).toContain(
'data-analytics-key="navigation.private_zoom"',
);
});
});
+11 -11
View File
@@ -4,23 +4,23 @@ import { Camera, MessageCircle } from "lucide-react";
import styles from "./app-bottom-nav.module.css";
export type AppBottomNavItem = "chat" | "privateRoom";
export type AppBottomNavItem = "chat" | "privateZoom";
export type AppBottomNavVariant = "warm" | "dark";
export interface AppBottomNavProps {
activeItem?: AppBottomNavItem | null;
variant?: AppBottomNavVariant;
privateRoomLabel: string;
privateZoomLabel: string;
onChatClick: () => void;
onPrivateRoomClick: () => void;
onPrivateZoomClick: () => void;
}
export function AppBottomNav({
activeItem = null,
variant = "warm",
privateRoomLabel,
privateZoomLabel,
onChatClick,
onPrivateRoomClick,
onPrivateZoomClick,
}: AppBottomNavProps) {
return (
<nav className={getRootClass(variant)} aria-label="Primary navigation">
@@ -37,14 +37,14 @@ export function AppBottomNav({
</button>
<button
type="button"
data-analytics-key="navigation.private_room"
data-analytics-label="Private room navigation"
className={getButtonClass(activeItem === "privateRoom")}
aria-current={activeItem === "privateRoom" ? "page" : undefined}
onClick={onPrivateRoomClick}
data-analytics-key="navigation.private_zoom"
data-analytics-label="Private Zoom navigation"
className={getButtonClass(activeItem === "privateZoom")}
aria-current={activeItem === "privateZoom" ? "page" : undefined}
onClick={onPrivateZoomClick}
>
<Camera size={20} aria-hidden="true" />
<span>{privateRoomLabel}</span>
<span>{privateZoomLabel}</span>
</button>
</nav>
);
@@ -1,5 +0,0 @@
import { PrivateRoomScreen } from "@/app/private-room/private-room-screen";
export default function CharacterPrivateRoomPage() {
return <PrivateRoomScreen />;
}
@@ -2,9 +2,9 @@ import type { ReactNode } from "react";
import { notFound } from "next/navigation";
import { getCharacterBySlug } from "@/data/constants/character";
import { PrivateRoomRouteProvider } from "@/providers/private-room-route-provider";
import { PrivateZoomRouteProvider } from "@/providers/private-zoom-route-provider";
export default async function CharacterPrivateRoomLayout({
export default async function CharacterPrivateZoomLayout({
children,
params,
}: {
@@ -13,11 +13,11 @@ export default async function CharacterPrivateRoomLayout({
}) {
const { characterSlug } = await params;
const character = getCharacterBySlug(characterSlug);
if (!character?.capabilities.privateRoom) notFound();
if (!character?.capabilities.privateZoom) notFound();
return (
<PrivateRoomRouteProvider characterId={character.id}>
<PrivateZoomRouteProvider characterId={character.id}>
{children}
</PrivateRoomRouteProvider>
</PrivateZoomRouteProvider>
);
}
@@ -0,0 +1,5 @@
import { PrivateZoomScreen } from "@/app/private-zoom/private-zoom-screen";
export default function CharacterPrivateZoomPage() {
return <PrivateZoomScreen />;
}
+3 -3
View File
@@ -253,8 +253,8 @@ export function ChatScreen() {
);
}
function handleOpenCharacterPrivateRoom(): void {
router.push(characterRoutes.privateRoom);
function handleOpenCharacterPrivateZoom(): void {
router.push(characterRoutes.privateZoom);
}
return (
@@ -304,7 +304,7 @@ export function ChatScreen() {
onUnlockImageMessage={handleUnlockImageMessage}
onOpenImage={handleOpenImage}
onUserAvatarClick={handleOpenUserProfile}
onCharacterAvatarClick={handleOpenCharacterPrivateRoom}
onCharacterAvatarClick={handleOpenCharacterPrivateZoom}
onLoadMoreHistory={() => {
chatDispatch({ type: "ChatLoadMoreHistoryRequested" });
}}
@@ -62,10 +62,10 @@ describe("chat Tailwind components", () => {
expect(aiHtml).toContain('<button type="button"');
expect(aiHtml).toContain(
'data-analytics-key="chat.open_private_room_from_avatar"',
'data-analytics-key="chat.open_private_zoom_from_avatar"',
);
expect(aiHtml).toContain(
'aria-label="Open Elio Silvestri&#x27;s private room"',
'aria-label="Open Elio Silvestri&#x27;s private zoom"',
);
expect(userHtml).toContain('<button type="button"');
expect(userHtml).toContain(
+2 -2
View File
@@ -29,8 +29,8 @@ export function MessageAvatar({
imageSize={43}
priority
className={AVATAR_CLASS_NAME}
actionLabel={`Open ${character.displayName}'s private room`}
analyticsKey="chat.open_private_room_from_avatar"
actionLabel={`Open ${character.displayName}'s private zoom`}
analyticsKey="chat.open_private_zoom_from_avatar"
onClick={onClick}
/>
);
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import {
buildPrivateAlbumGalleryUrl,
buildPrivateRoomWithoutGalleryUrl,
buildPrivateZoomWithoutGalleryUrl,
getPrivateAlbumGalleryState,
} from "../private-album-gallery-url";
@@ -11,7 +11,7 @@ describe("private album gallery URL", () => {
const url = buildPrivateAlbumGalleryUrl("album:91", 3);
const params = new URL(url, "https://cozsweet.test").searchParams;
expect(url).toBe("/private-room?album=album%3A91&image=3");
expect(url).toBe("/private-zoom?album=album%3A91&image=3");
expect(getPrivateAlbumGalleryState(params)).toEqual({
albumId: "album:91",
imageIndex: 3,
@@ -29,9 +29,9 @@ describe("private album gallery URL", () => {
it("removes gallery params without dropping other query values", () => {
expect(
buildPrivateRoomWithoutGalleryUrl(
buildPrivateZoomWithoutGalleryUrl(
new URLSearchParams("album=album-1&image=2&source=external"),
),
).toBe("/private-room?source=external");
).toBe("/private-zoom?source=external");
});
});
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { PrivateAlbumSchema } from "@/data/schemas/private-room";
import { PrivateAlbumSchema } from "@/data/schemas/private-zoom";
import {
findPrivateAlbumDisplayImage,
@@ -6,10 +6,10 @@ import { getCharacterBySlug } from "@/data/constants/character";
import type { LoginStatus } from "@/data/schemas/auth";
import { CharacterProvider } from "@/providers/character-provider";
import { getCharacterRoutes } from "@/router/routes";
import type { PrivateRoomEvent } from "@/stores/private-room";
import type { PrivateRoomUnlockPaywallRequest } from "@/stores/private-room/private-room-state";
import type { PrivateZoomEvent } from "@/stores/private-zoom";
import type { PrivateZoomUnlockPaywallRequest } from "@/stores/private-zoom/private-zoom-state";
import { usePrivateRoomUnlockPaywallNavigation } from "../use-private-room-flow";
import { usePrivateZoomUnlockPaywallNavigation } from "../use-private-zoom-flow";
const mocks = vi.hoisted(() => ({
openAuth: vi.fn(),
@@ -30,7 +30,7 @@ vi.mock("@/lib/analytics", () => ({
},
}));
const unlockPaywallRequest: PrivateRoomUnlockPaywallRequest = {
const unlockPaywallRequest: PrivateZoomUnlockPaywallRequest = {
albumId: "album-1",
reason: "insufficient_credits",
requiredCredits: 10,
@@ -38,7 +38,7 @@ const unlockPaywallRequest: PrivateRoomUnlockPaywallRequest = {
shortfallCredits: 7,
};
describe("Private Room paywall navigation", () => {
describe("Private Zoom paywall navigation", () => {
let container: HTMLDivElement;
let root: Root;
@@ -58,31 +58,31 @@ describe("Private Room paywall navigation", () => {
container.remove();
});
it("opens top up with a private room return target", () => {
const roomDispatch = vi.fn<Dispatch<PrivateRoomEvent>>();
it("opens top up with a private zoom return target", () => {
const roomDispatch = vi.fn<Dispatch<PrivateZoomEvent>>();
renderHarness(root, "email", roomDispatch);
expect(mocks.openSubscription).toHaveBeenCalledWith({
type: "topup",
returnTo: "private-room",
returnTo: "private-zoom",
analytics: {
entryPoint: "private_album_unlock",
triggerReason: "insufficient_credits",
},
});
expect(roomDispatch).toHaveBeenCalledWith({
type: "PrivateRoomUnlockPaywallConsumed",
type: "PrivateZoomUnlockPaywallConsumed",
});
});
it("keeps guest users on the private room auth return path", () => {
const roomDispatch = vi.fn<Dispatch<PrivateRoomEvent>>();
it("keeps guest users on the private zoom auth return path", () => {
const roomDispatch = vi.fn<Dispatch<PrivateZoomEvent>>();
renderHarness(root, "guest", roomDispatch);
expect(mocks.openAuth).toHaveBeenCalledWith(
getCharacterRoutes("maya").privateRoom,
getCharacterRoutes("maya").privateZoom,
);
expect(mocks.openSubscription).not.toHaveBeenCalled();
});
@@ -91,7 +91,7 @@ describe("Private Room paywall navigation", () => {
function renderHarness(
root: Root,
loginStatus: LoginStatus,
roomDispatch: Dispatch<PrivateRoomEvent>,
roomDispatch: Dispatch<PrivateZoomEvent>,
): void {
const character = getCharacterBySlug("maya");
if (!character) throw new Error("Missing Maya character fixture");
@@ -109,9 +109,9 @@ function Harness({
roomDispatch,
}: {
loginStatus: LoginStatus;
roomDispatch: Dispatch<PrivateRoomEvent>;
roomDispatch: Dispatch<PrivateZoomEvent>;
}) {
usePrivateRoomUnlockPaywallNavigation({
usePrivateZoomUnlockPaywallNavigation({
loginStatus,
roomDispatch,
unlockPaywallRequest,
@@ -1,19 +1,19 @@
import { describe, expect, it } from "vitest";
import { isPrivateRoomAuthRequired } from "../use-private-room-flow";
import { isPrivateZoomAuthRequired } from "../use-private-zoom-flow";
describe("isPrivateRoomAuthRequired", () => {
describe("isPrivateZoomAuthRequired", () => {
it.each(["guest", "notLoggedIn"] as const)(
"requires auth for %s users",
(loginStatus) => {
expect(isPrivateRoomAuthRequired(loginStatus)).toBe(true);
expect(isPrivateZoomAuthRequired(loginStatus)).toBe(true);
},
);
it.each(["email", "facebook", "google"] as const)(
"allows %s users to top up directly",
(loginStatus) => {
expect(isPrivateRoomAuthRequired(loginStatus)).toBe(false);
expect(isPrivateZoomAuthRequired(loginStatus)).toBe(false);
},
);
});
@@ -4,7 +4,7 @@ import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { PrivateAlbumSchema } from "@/data/schemas/private-room";
import { PrivateAlbumSchema } from "@/data/schemas/private-zoom";
import { PrivateAlbumCard } from "../private-album-card";
@@ -33,9 +33,9 @@ describe("PrivateAlbumCard interactions", () => {
imageCount: 4,
images: [
{ url: "", locked: false, index: 0 },
{ url: "/images/private-room/locked.png", locked: true, index: 1 },
{ url: "/images/private-room/photo-2.png", locked: false, index: 2 },
{ url: "/images/private-room/photo-3.png", locked: false, index: 3 },
{ url: "/images/private-zoom/locked.png", locked: true, index: 1 },
{ url: "/images/private-zoom/photo-2.png", locked: false, index: 2 },
{ url: "/images/private-zoom/photo-3.png", locked: false, index: 3 },
],
locked: false,
unlocked: true,
@@ -72,7 +72,7 @@ describe("PrivateAlbumCard interactions", () => {
title: "Locked afternoon",
imageCount: 3,
images: [
{ url: "/images/private-room/locked.png", locked: true, index: 0 },
{ url: "/images/private-zoom/locked.png", locked: true, index: 0 },
],
locked: true,
unlocked: false,
@@ -5,11 +5,11 @@ import {
PrivateAlbumSchema,
type PrivateAlbum,
type PrivateAlbumInput,
} from "@/data/schemas/private-room";
} from "@/data/schemas/private-zoom";
import { PrivateAlbumCard } from "../private-album-card";
const COVER_URL = "/images/private-room/banner/elio.png";
const COVER_URL = "/images/private-zoom/banner/elio.png";
function makeAlbum(overrides: Partial<PrivateAlbumInput> = {}): PrivateAlbum {
return PrivateAlbumSchema.parse({
@@ -157,7 +157,7 @@ describe("PrivateAlbumCard", () => {
function makeImages(count: number) {
return Array.from({ length: count }, (_, index) => ({
url: `/images/private-room/photo-${index}.png`,
url: `/images/private-zoom/photo-${index}.png`,
locked: false,
index,
}));
@@ -4,7 +4,7 @@ import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { PrivateAlbumSchema } from "@/data/schemas/private-room";
import { PrivateAlbumSchema } from "@/data/schemas/private-zoom";
import { PrivateAlbumGallery } from "../private-album-gallery";
@@ -13,11 +13,11 @@ const album = PrivateAlbumSchema.parse({
title: "Private afternoon",
imageCount: 5,
images: [
{ url: "/images/private-room/photo-0.png", locked: false, index: 0 },
{ url: "/images/private-zoom/photo-0.png", locked: false, index: 0 },
{ url: "", locked: false, index: 1 },
{ url: "/images/private-room/locked.png", locked: true, index: 2 },
{ url: "/images/private-room/photo-3.png", locked: false, index: 3 },
{ url: "/images/private-room/photo-4.png", locked: false, index: 4 },
{ url: "/images/private-zoom/locked.png", locked: true, index: 2 },
{ url: "/images/private-zoom/photo-3.png", locked: false, index: 3 },
{ url: "/images/private-zoom/photo-4.png", locked: false, index: 4 },
],
locked: false,
unlocked: true,
@@ -3,8 +3,8 @@ import Image from "next/image";
import { Copy, ImageIcon, LockKeyhole } from "lucide-react";
import { CharacterAvatar } from "@/app/_components";
import type { PrivateAlbum } from "@/data/schemas/private-room";
import { isPrivateAlbumLocked } from "@/lib/private-room/private_album";
import type { PrivateAlbum } from "@/data/schemas/private-zoom";
import { isPrivateAlbumLocked } from "@/lib/private-zoom/private_album";
import {
getPrivateAlbumDisplayImages,
@@ -12,7 +12,7 @@ import {
PRIVATE_ALBUM_GRID_LIMIT,
} from "../private-album-images";
import styles from "../private-room-screen.module.css";
import styles from "../private-zoom-screen.module.css";
export interface PrivateAlbumCardProps {
album: PrivateAlbum;
@@ -11,14 +11,14 @@ import {
import Image from "next/image";
import { ChevronLeft, ChevronRight, X } from "lucide-react";
import type { PrivateAlbum } from "@/data/schemas/private-room";
import type { PrivateAlbum } from "@/data/schemas/private-zoom";
import {
getResistedGalleryDragDistance,
resolveGallerySwipeDirection,
} from "../private-album-gallery-motion";
import { getPrivateAlbumDisplayImages } from "../private-album-images";
import styles from "../private-room-screen.module.css";
import styles from "../private-zoom-screen.module.css";
export interface PrivateAlbumGalleryProps {
album: PrivateAlbum;
@@ -1,4 +1,4 @@
import styles from "../private-room-screen.module.css";
import styles from "../private-zoom-screen.module.css";
export interface StatusCardProps {
message: string;
@@ -1,6 +1,6 @@
import type { PrivateAlbum } from "@/data/schemas/private-room";
import type { PrivateAlbum } from "@/data/schemas/private-zoom";
import styles from "../private-room-screen.module.css";
import styles from "../private-zoom-screen.module.css";
export interface UnlockConfirmDialogProps {
album: PrivateAlbum;
@@ -7,14 +7,14 @@ import {
type RouteSearchParams,
} from "@/router/routes";
export default async function PrivateRoomPage({
export default async function PrivateZoomPage({
searchParams,
}: {
searchParams: Promise<RouteSearchParams>;
}) {
redirect(
appendRouteSearchParams(
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).privateRoom,
getCharacterRoutes(DEFAULT_CHARACTER_SLUG).privateZoom,
await searchParams,
),
);
@@ -15,7 +15,7 @@ export function getPrivateAlbumGalleryState(input: {
export function buildPrivateAlbumGalleryUrl(
albumId: string,
imageIndex: number,
basePath: string = ROUTES.privateRoom,
basePath: string = ROUTES.privateZoom,
): string {
const params = new URLSearchParams({
[PRIVATE_ALBUM_QUERY_PARAM]: albumId,
@@ -24,9 +24,9 @@ export function buildPrivateAlbumGalleryUrl(
return `${basePath}?${params.toString()}`;
}
export function buildPrivateRoomWithoutGalleryUrl(
export function buildPrivateZoomWithoutGalleryUrl(
input: { toString: () => string },
basePath: string = ROUTES.privateRoom,
basePath: string = ROUTES.privateZoom,
): string {
const params = new URLSearchParams(input.toString());
params.delete(PRIVATE_ALBUM_QUERY_PARAM);
@@ -1,4 +1,4 @@
import type { PrivateAlbum } from "@/data/schemas/private-room";
import type { PrivateAlbum } from "@/data/schemas/private-zoom";
export const PRIVATE_ALBUM_GRID_LIMIT = 9;
@@ -11,12 +11,12 @@ import {
useActiveCharacter,
useActiveCharacterRoutes,
} from "@/providers/character-provider";
import { isPrivateAlbumLocked } from "@/lib/private-room/private_album";
import { isPrivateAlbumLocked } from "@/lib/private-zoom/private_album";
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import {
usePrivateRoomDispatch,
usePrivateRoomState,
} from "@/stores/private-room";
usePrivateZoomDispatch,
usePrivateZoomState,
} from "@/stores/private-zoom";
import {
PrivateAlbumCard,
@@ -24,21 +24,21 @@ import {
StatusCard,
UnlockConfirmDialog,
} from "./components";
import styles from "./private-room-screen.module.css";
import styles from "./private-zoom-screen.module.css";
import {
isPrivateRoomAuthRequired,
usePrivateRoomBootstrapFlow,
usePrivateRoomUnlockPaywallNavigation,
usePrivateRoomUnlockSuccessRefresh,
} from "./use-private-room-flow";
isPrivateZoomAuthRequired,
usePrivateZoomBootstrapFlow,
usePrivateZoomUnlockPaywallNavigation,
usePrivateZoomUnlockSuccessRefresh,
} from "./use-private-zoom-flow";
import {
buildPrivateAlbumGalleryUrl,
buildPrivateRoomWithoutGalleryUrl,
buildPrivateZoomWithoutGalleryUrl,
getPrivateAlbumGalleryState,
} from "./private-album-gallery-url";
import { findPrivateAlbumDisplayImage } from "./private-album-images";
export function PrivateRoomScreen() {
export function PrivateZoomScreen() {
const router = useRouter();
const searchParams = useSearchParams();
const navigator = useAppNavigator();
@@ -46,15 +46,15 @@ export function PrivateRoomScreen() {
const characterRoutes = useActiveCharacterRoutes();
const authState = useAuthState();
const authDispatch = useAuthDispatch();
const state = usePrivateRoomState();
const dispatch = usePrivateRoomDispatch();
const state = usePrivateZoomState();
const dispatch = usePrivateZoomDispatch();
const openedGalleryInPageRef = useRef(false);
const galleryState = useMemo(
() => getPrivateAlbumGalleryState(searchParams),
[searchParams],
);
usePrivateRoomBootstrapFlow({
usePrivateZoomBootstrapFlow({
authDispatch,
hasInitialized: authState.hasInitialized,
isAuthLoading: authState.isLoading,
@@ -62,17 +62,17 @@ export function PrivateRoomScreen() {
roomDispatch: dispatch,
roomStatus: state.status,
});
usePrivateRoomUnlockPaywallNavigation({
usePrivateZoomUnlockPaywallNavigation({
loginStatus: authState.loginStatus,
roomDispatch: dispatch,
unlockPaywallRequest: state.unlockPaywallRequest,
});
usePrivateRoomUnlockSuccessRefresh(state.unlockSuccessNonce);
usePrivateZoomUnlockSuccessRefresh(state.unlockSuccessNonce);
const displayName = character.displayName;
const avatarUrl = character.assets.avatar;
const title = character.copy.privateRoomTitle;
const subtitle = character.copy.privateRoomSubtitle;
const title = character.copy.privateZoomTitle;
const subtitle = character.copy.privateZoomSubtitle;
const pendingAlbum = useMemo(
() =>
state.items.find(
@@ -107,15 +107,15 @@ export function PrivateRoomScreen() {
!galleryImage
) {
router.replace(
buildPrivateRoomWithoutGalleryUrl(
buildPrivateZoomWithoutGalleryUrl(
searchParams,
characterRoutes.privateRoom,
characterRoutes.privateZoom,
),
{ scroll: false },
);
}
}, [
characterRoutes.privateRoom,
characterRoutes.privateZoom,
galleryAlbum,
galleryImage,
galleryState,
@@ -125,15 +125,15 @@ export function PrivateRoomScreen() {
]);
const handleTopUpClick = () => {
if (isPrivateRoomAuthRequired(authState.loginStatus)) {
navigator.openAuth(characterRoutes.privateRoom);
if (isPrivateZoomAuthRequired(authState.loginStatus)) {
navigator.openAuth(characterRoutes.privateZoom);
return;
}
navigator.openSubscription({
type: "topup",
returnTo: "private-room",
returnTo: "private-zoom",
analytics: {
entryPoint: "private_room",
entryPoint: "private_zoom",
triggerReason: "vip_cta",
},
});
@@ -145,7 +145,7 @@ export function PrivateRoomScreen() {
buildPrivateAlbumGalleryUrl(
albumId,
imageIndex,
characterRoutes.privateRoom,
characterRoutes.privateZoom,
),
{ scroll: false },
);
@@ -158,9 +158,9 @@ export function PrivateRoomScreen() {
return;
}
router.replace(
buildPrivateRoomWithoutGalleryUrl(
buildPrivateZoomWithoutGalleryUrl(
searchParams,
characterRoutes.privateRoom,
characterRoutes.privateZoom,
),
{ scroll: false },
);
@@ -172,7 +172,7 @@ export function PrivateRoomScreen() {
buildPrivateAlbumGalleryUrl(
galleryAlbum.albumId,
imageIndex,
characterRoutes.privateRoom,
characterRoutes.privateZoom,
),
{ scroll: false },
);
@@ -187,7 +187,7 @@ export function PrivateRoomScreen() {
<section className={styles.hero} aria-label={title}>
<div className={styles.heroCover}>
<Image
src={character.assets.privateRoomBanner}
src={character.assets.privateZoomBanner}
alt=""
width={2048}
height={1152}
@@ -213,8 +213,8 @@ export function PrivateRoomScreen() {
<button
type="button"
data-analytics-key="private_room.primary_cta"
data-analytics-label="Open private room top up"
data-analytics-key="private_zoom.primary_cta"
data-analytics-label="Open private zoom top up"
className={styles.primaryCta}
onClick={handleTopUpClick}
>
@@ -237,7 +237,7 @@ export function PrivateRoomScreen() {
<StatusCard
message={state.errorMessage}
actionLabel="Retry"
onAction={() => dispatch({ type: "PrivateRoomRefresh" })}
onAction={() => dispatch({ type: "PrivateZoomRefresh" })}
/>
) : null}
@@ -249,7 +249,7 @@ export function PrivateRoomScreen() {
<StatusCard
message="No private albums yet."
actionLabel="Refresh"
onAction={() => dispatch({ type: "PrivateRoomRefresh" })}
onAction={() => dispatch({ type: "PrivateZoomRefresh" })}
/>
) : null}
@@ -267,7 +267,7 @@ export function PrivateRoomScreen() {
}
onUnlock={() =>
dispatch({
type: "PrivateRoomUnlockRequested",
type: "PrivateZoomUnlockRequested",
albumId: album.albumId,
})
}
@@ -277,12 +277,12 @@ export function PrivateRoomScreen() {
</section>
<AppBottomNav
activeItem="privateRoom"
privateRoomLabel={character.copy.privateRoomTitle}
activeItem="privateZoom"
privateZoomLabel={character.copy.privateZoomTitle}
onChatClick={() =>
navigator.push(characterRoutes.splash, { scroll: false })
}
onPrivateRoomClick={() => undefined}
onPrivateZoomClick={() => undefined}
/>
{pendingAlbum ? (
@@ -290,8 +290,8 @@ export function PrivateRoomScreen() {
album={pendingAlbum}
isUnlocking={state.isUnlocking}
errorMessage={state.unlockErrorMessage}
onCancel={() => dispatch({ type: "PrivateRoomUnlockCancelled" })}
onConfirm={() => dispatch({ type: "PrivateRoomUnlockConfirmed" })}
onCancel={() => dispatch({ type: "PrivateZoomUnlockCancelled" })}
onConfirm={() => dispatch({ type: "PrivateZoomUnlockConfirmed" })}
/>
) : state.unlockErrorMessage ? (
<div className={styles.toast} role="status">
@@ -8,37 +8,37 @@ import { behaviorAnalytics } from "@/lib/analytics";
import { useActiveCharacterRoutes } from "@/providers/character-provider";
import { useAppNavigator } from "@/router/use-app-navigator";
import type { AuthEvent } from "@/stores/auth/auth-events";
import type { PrivateRoomEvent } from "@/stores/private-room";
import type { PrivateRoomUnlockPaywallRequest } from "@/stores/private-room/private-room-state";
import type { PrivateZoomEvent } from "@/stores/private-zoom";
import type { PrivateZoomUnlockPaywallRequest } from "@/stores/private-zoom/private-zoom-state";
import { useUserDispatch } from "@/stores/user/user-context";
export interface UsePrivateRoomBootstrapFlowInput {
export interface UsePrivateZoomBootstrapFlowInput {
authDispatch: Dispatch<AuthEvent>;
hasInitialized: boolean;
isAuthLoading: boolean;
loginStatus: LoginStatus;
roomDispatch: Dispatch<PrivateRoomEvent>;
roomDispatch: Dispatch<PrivateZoomEvent>;
roomStatus: string;
}
export interface UsePrivateRoomUnlockPaywallNavigationInput {
export interface UsePrivateZoomUnlockPaywallNavigationInput {
loginStatus: LoginStatus;
roomDispatch: Dispatch<PrivateRoomEvent>;
unlockPaywallRequest: PrivateRoomUnlockPaywallRequest | null;
roomDispatch: Dispatch<PrivateZoomEvent>;
unlockPaywallRequest: PrivateZoomUnlockPaywallRequest | null;
}
export function isPrivateRoomAuthRequired(loginStatus: LoginStatus): boolean {
export function isPrivateZoomAuthRequired(loginStatus: LoginStatus): boolean {
return loginStatus === "guest" || loginStatus === "notLoggedIn";
}
export function usePrivateRoomBootstrapFlow({
export function usePrivateZoomBootstrapFlow({
authDispatch,
hasInitialized,
isAuthLoading,
loginStatus,
roomDispatch,
roomStatus,
}: UsePrivateRoomBootstrapFlowInput): void {
}: UsePrivateZoomBootstrapFlowInput): void {
const previousLoginStatusRef = useRef(loginStatus);
useGuestLoginBootstrap({
@@ -56,12 +56,12 @@ export function usePrivateRoomBootstrapFlow({
previousLoginStatusRef.current = loginStatus;
if (roomStatus === "idle") {
roomDispatch({ type: "PrivateRoomInit" });
roomDispatch({ type: "PrivateZoomInit" });
return;
}
if (previousLoginStatus !== loginStatus && roomStatus !== "loading") {
roomDispatch({ type: "PrivateRoomRefresh" });
roomDispatch({ type: "PrivateZoomRefresh" });
}
}, [
hasInitialized,
@@ -72,19 +72,19 @@ export function usePrivateRoomBootstrapFlow({
]);
}
export function usePrivateRoomUnlockPaywallNavigation({
export function usePrivateZoomUnlockPaywallNavigation({
loginStatus,
roomDispatch,
unlockPaywallRequest,
}: UsePrivateRoomUnlockPaywallNavigationInput): void {
}: UsePrivateZoomUnlockPaywallNavigationInput): void {
const navigator = useAppNavigator();
const characterRoutes = useActiveCharacterRoutes();
useEffect(() => {
if (!unlockPaywallRequest) return;
if (isPrivateRoomAuthRequired(loginStatus)) {
navigator.openAuth(characterRoutes.privateRoom);
if (isPrivateZoomAuthRequired(loginStatus)) {
navigator.openAuth(characterRoutes.privateZoom);
} else {
behaviorAnalytics.paywallShown({
entryPoint: "private_album_unlock",
@@ -92,16 +92,16 @@ export function usePrivateRoomUnlockPaywallNavigation({
});
navigator.openSubscription({
type: "topup",
returnTo: "private-room",
returnTo: "private-zoom",
analytics: {
entryPoint: "private_album_unlock",
triggerReason: "insufficient_credits",
},
});
}
roomDispatch({ type: "PrivateRoomUnlockPaywallConsumed" });
roomDispatch({ type: "PrivateZoomUnlockPaywallConsumed" });
}, [
characterRoutes.privateRoom,
characterRoutes.privateZoom,
loginStatus,
navigator,
roomDispatch,
@@ -109,7 +109,7 @@ export function usePrivateRoomUnlockPaywallNavigation({
]);
}
export function usePrivateRoomUnlockSuccessRefresh(
export function usePrivateZoomUnlockSuccessRefresh(
unlockSuccessNonce: number,
): void {
const userDispatch = useUserDispatch();
+4 -4
View File
@@ -37,8 +37,8 @@ export function SplashScreen() {
navigator.openChat({ replace: true });
};
const handleOpenPrivateRoom = () => {
navigator.push(characterRoutes.privateRoom, { scroll: false });
const handleOpenPrivateZoom = () => {
navigator.push(characterRoutes.privateZoom, { scroll: false });
};
const handleOpenSplash = () => {
@@ -77,9 +77,9 @@ export function SplashScreen() {
</div>
<AppBottomNav
activeItem="chat"
privateRoomLabel={character.copy.privateRoomTitle}
privateZoomLabel={character.copy.privateZoomTitle}
onChatClick={handleOpenSplash}
onPrivateRoomClick={handleOpenPrivateRoom}
onPrivateZoomClick={handleOpenPrivateZoom}
/>
</div>
</MobileShell>