feat(profile)!: replace sidebar and add avatar navigation

Rename the global Sidebar route, UI, assets, analytics, and payment return context to Profile. Add accessible message-avatar navigation and preserve the source character across auth and logout flows.

BREAKING CHANGE: /sidebar has been removed; use /profile instead.
This commit is contained in:
2026-07-21 18:08:12 +08:00
parent 6cd3a0c2d2
commit 9deb320cf6
67 changed files with 565 additions and 256 deletions
@@ -30,10 +30,10 @@ describe("payment analytics context", () => {
expect(
parsePaymentAnalyticsContext({
entryPoint: "sidebar",
entryPoint: "profile",
triggerReason: null,
subscriptionType: "vip",
}),
).toEqual({ entryPoint: "sidebar", triggerReason: "vip_cta" });
).toEqual({ entryPoint: "profile", triggerReason: "vip_cta" });
});
});
@@ -5,7 +5,7 @@ export type PaymentAnalyticsTriggerReason =
| "daily_chat_limit"
| "private_topic_limit"
| "insufficient_credits"
| "sidebar_recharge"
| "profile_recharge"
| "vip_cta"
| "ad_landing"
| "manual_recharge"
@@ -16,7 +16,7 @@ export type PaymentAnalyticsEntryPoint =
| "chat_unlock"
| "private_album_unlock"
| "private_room"
| "sidebar"
| "profile"
| "chat_offer_banner"
| "subscription_direct"
| "tip_page"
@@ -33,7 +33,7 @@ const ENTRY_POINTS = new Set<PaymentAnalyticsEntryPoint>([
"chat_unlock",
"private_album_unlock",
"private_room",
"sidebar",
"profile",
"chat_offer_banner",
"subscription_direct",
"tip_page",
@@ -45,7 +45,7 @@ const TRIGGER_REASONS = new Set<PaymentAnalyticsTriggerReason>([
"daily_chat_limit",
"private_topic_limit",
"insufficient_credits",
"sidebar_recharge",
"profile_recharge",
"vip_cta",
"ad_landing",
"manual_recharge",
@@ -29,7 +29,7 @@ describe("external entry navigation", () => {
ROUTES.chat,
);
expect(resolveExternalEntryTarget({ target: "/tip" })).toBe(ROUTES.chat);
expect(resolveExternalEntryTarget({ target: "sidebar" })).toBe(ROUTES.chat);
expect(resolveExternalEntryTarget({ target: "profile" })).toBe(ROUTES.chat);
});
it("routes every tip entry to the tier selection page", () => {
@@ -60,30 +60,30 @@ describe("subscription exit helpers", () => {
);
});
it("falls back to sidebar by default", async () => {
it("falls back to profile by default", async () => {
consumePendingChatImageReturnMock.mockResolvedValue(null);
peekPendingChatUnlockMock.mockResolvedValue(null);
expect(getSubscriptionFallbackExitUrl(null)).toBe(ROUTES.sidebar);
expect(getSubscriptionFallbackExitUrl(null)).toBe(ROUTES.profile);
await expect(consumeSubscriptionExitUrl(null)).resolves.toBe(
ROUTES.sidebar,
ROUTES.profile,
);
});
it("returns to global sidebar with the source character", async () => {
it("returns to global profile with the source character", async () => {
const expectedUrl = buildGlobalPageUrl(
ROUTES.sidebar,
ROUTES.profile,
getCharacterRoutes("maya").chat,
);
expect(getSubscriptionFallbackExitUrl("sidebar", "maya")).toBe(
expect(getSubscriptionFallbackExitUrl("profile", "maya")).toBe(
expectedUrl,
);
await expect(
consumeSubscriptionExitUrl("sidebar", "maya"),
consumeSubscriptionExitUrl("profile", "maya"),
).resolves.toBe(expectedUrl);
await expect(
resolveSubscriptionSuccessExitUrl("sidebar", "maya"),
resolveSubscriptionSuccessExitUrl("profile", "maya"),
).resolves.toBe(expectedUrl);
expect(consumePendingChatImageReturnMock).not.toHaveBeenCalled();
expect(peekPendingChatUnlockMock).not.toHaveBeenCalled();
+5 -5
View File
@@ -48,10 +48,10 @@ export function getSubscriptionFallbackExitUrl(
const routes = getCharacterRoutes(characterSlug);
if (returnTo === "chat") return routes.chat;
if (returnTo === "private-room") return routes.privateRoom;
if (returnTo === "sidebar") {
return buildGlobalPageUrl(ROUTES.sidebar, routes.chat);
if (returnTo === "profile") {
return buildGlobalPageUrl(ROUTES.profile, routes.chat);
}
return ROUTES.sidebar;
return ROUTES.profile;
}
export async function consumeSubscriptionExitUrl(
@@ -61,7 +61,7 @@ export async function consumeSubscriptionExitUrl(
if (returnTo === "private-room") {
return getCharacterRoutes(characterSlug).privateRoom;
}
if (returnTo === "sidebar") {
if (returnTo === "profile") {
return getSubscriptionFallbackExitUrl(returnTo, characterSlug);
}
@@ -78,7 +78,7 @@ export async function resolveSubscriptionSuccessExitUrl(
if (returnTo === "private-room") {
return getCharacterRoutes(characterSlug).privateRoom;
}
if (returnTo === "sidebar") {
if (returnTo === "profile") {
return getSubscriptionFallbackExitUrl(returnTo, characterSlug);
}
@@ -40,7 +40,7 @@ describe("payment search params", () => {
it("accepts only supported subscription return targets", () => {
expect(parseSubscriptionReturnTo("chat")).toBe("chat");
expect(parseSubscriptionReturnTo("private-room")).toBe("private-room");
expect(parseSubscriptionReturnTo("sidebar")).toBe("sidebar");
expect(parseSubscriptionReturnTo("profile")).toBe("profile");
expect(parseSubscriptionReturnTo(["private-room", "chat"])).toBe(
"private-room",
);
@@ -54,12 +54,12 @@ describe("pending payment order helpers", () => {
await clearPendingPaymentOrder();
});
it("stores and rebuilds global sidebar returns", async () => {
it("stores and rebuilds global profile returns", async () => {
await clearPendingPaymentOrder();
const saveResult = await savePendingEzpayOrder({
orderId: "order-sidebar",
returnTo: "sidebar",
orderId: "order-profile",
returnTo: "profile",
subscriptionType: "topup",
characterSlug: "maya",
createdAt: 1,
@@ -70,20 +70,20 @@ describe("pending payment order helpers", () => {
expect(storedResult).toMatchObject({
success: true,
data: {
orderId: "order-sidebar",
returnTo: "sidebar",
orderId: "order-profile",
returnTo: "profile",
characterSlug: "maya",
},
});
expect(
buildPendingPaymentSubscriptionUrl({
payChannel: "ezpay",
returnTo: "sidebar",
returnTo: "profile",
subscriptionType: "topup",
characterSlug: "maya",
}),
).toBe(
"/subscription?type=topup&payChannel=ezpay&paymentReturn=1&returnTo=sidebar&character=maya",
"/subscription?type=topup&payChannel=ezpay&paymentReturn=1&returnTo=profile&character=maya",
);
await clearPendingPaymentOrder();
});
+1 -1
View File
@@ -29,7 +29,7 @@ export function parseSubscriptionReturnTo(
if (
returnTo === "chat" ||
returnTo === "private-room" ||
returnTo === "sidebar"
returnTo === "profile"
) {
return returnTo;
}