feat(analytics): add behavior and payment funnel tracking
This commit is contained in:
@@ -33,6 +33,27 @@ describe("navigation resolver", () => {
|
||||
).toBe("/subscription?type=topup");
|
||||
});
|
||||
|
||||
it("preserves payment analytics context through auth redirects", () => {
|
||||
const subscriptionUrl = ROUTE_BUILDERS.subscription("topup", {
|
||||
payChannel: "stripe",
|
||||
returnTo: "chat",
|
||||
analytics: {
|
||||
entryPoint: "chat_unlock",
|
||||
triggerReason: "ad_landing",
|
||||
},
|
||||
});
|
||||
|
||||
expect(subscriptionUrl).toBe(
|
||||
"/subscription?type=topup&payChannel=stripe&returnTo=chat&analytics_entry=chat_unlock&analytics_reason=ad_landing",
|
||||
);
|
||||
expect(
|
||||
resolveAuthenticatedNavigation({
|
||||
loginStatus: "guest",
|
||||
targetUrl: subscriptionUrl,
|
||||
}),
|
||||
).toBe(ROUTE_BUILDERS.authWithRedirect(subscriptionUrl));
|
||||
});
|
||||
|
||||
it("allows not logged in users to enter chat for guest bootstrap", () => {
|
||||
expect(
|
||||
resolveRouteGuardRedirect({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PayChannel } from "@/data/dto/payment";
|
||||
import type { PaymentAnalyticsContext } from "@/lib/analytics/payment_analytics_context";
|
||||
import type {
|
||||
PendingChatUnlockKind,
|
||||
PendingChatPromotion,
|
||||
@@ -13,6 +14,7 @@ export interface OpenSubscriptionInput {
|
||||
payChannel?: PayChannel;
|
||||
returnTo?: AppSubscriptionReturnTo;
|
||||
replace?: boolean;
|
||||
analytics?: PaymentAnalyticsContext;
|
||||
}
|
||||
|
||||
export interface StartMessageUnlockInput {
|
||||
@@ -37,4 +39,5 @@ export interface OpenSubscriptionForPendingUnlockInput {
|
||||
returnUrl: string;
|
||||
type: AppSubscriptionType;
|
||||
payChannel?: PayChannel;
|
||||
analytics?: PaymentAnalyticsContext;
|
||||
}
|
||||
|
||||
+20
-1
@@ -8,6 +8,11 @@
|
||||
*/
|
||||
|
||||
import type { PayChannel } from "@/data/dto/payment";
|
||||
import {
|
||||
PAYMENT_ANALYTICS_ENTRY_PARAM,
|
||||
PAYMENT_ANALYTICS_REASON_PARAM,
|
||||
type PaymentAnalyticsContext,
|
||||
} from "@/lib/analytics/payment_analytics_context";
|
||||
|
||||
/** 静态路由字面量 */
|
||||
export const ROUTES = {
|
||||
@@ -29,11 +34,25 @@ export type StaticRoute = (typeof ROUTES)[keyof typeof ROUTES];
|
||||
export const ROUTE_BUILDERS = {
|
||||
subscription: (
|
||||
type: "vip" | "topup",
|
||||
options: { payChannel?: PayChannel; returnTo?: "chat" } = {},
|
||||
options: {
|
||||
payChannel?: PayChannel;
|
||||
returnTo?: "chat";
|
||||
analytics?: PaymentAnalyticsContext;
|
||||
} = {},
|
||||
): `/subscription?${string}` => {
|
||||
const params = new URLSearchParams({ type });
|
||||
if (options.payChannel) params.set("payChannel", options.payChannel);
|
||||
if (options.returnTo) params.set("returnTo", options.returnTo);
|
||||
if (options.analytics) {
|
||||
params.set(
|
||||
PAYMENT_ANALYTICS_ENTRY_PARAM,
|
||||
options.analytics.entryPoint,
|
||||
);
|
||||
params.set(
|
||||
PAYMENT_ANALYTICS_REASON_PARAM,
|
||||
options.analytics.triggerReason,
|
||||
);
|
||||
}
|
||||
return `${ROUTES.subscription}?${params.toString()}` as const;
|
||||
},
|
||||
authWithRedirect: (redirectTo: string): `/auth?redirect=${string}` =>
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
peekSubscriptionExplicitExitUrl,
|
||||
} from "@/lib/navigation/subscription_exit";
|
||||
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||
import {
|
||||
behaviorAnalytics,
|
||||
getDefaultPaymentAnalyticsContext,
|
||||
} from "@/lib/analytics";
|
||||
import { useAuthSelector } from "@/stores/auth/auth-context";
|
||||
import { useUserSelector } from "@/stores/user/user-context";
|
||||
|
||||
@@ -88,12 +92,16 @@ export function useAppNavigator(): AppNavigator {
|
||||
payChannel = getDefaultPayChannel(),
|
||||
returnTo = null,
|
||||
replace: shouldReplace = false,
|
||||
analytics = getDefaultPaymentAnalyticsContext(type),
|
||||
}: OpenSubscriptionInput): void => {
|
||||
const target = ROUTE_BUILDERS.subscription(type, {
|
||||
payChannel,
|
||||
returnTo: returnTo ?? undefined,
|
||||
analytics,
|
||||
});
|
||||
|
||||
behaviorAnalytics.rechargeModalOpen(analytics);
|
||||
|
||||
const nextUrl = resolveAuthenticatedNavigation({
|
||||
loginStatus,
|
||||
targetUrl: target,
|
||||
@@ -153,6 +161,7 @@ export function useAppNavigator(): AppNavigator {
|
||||
returnUrl,
|
||||
type,
|
||||
payChannel = getDefaultPayChannel(),
|
||||
analytics,
|
||||
}: OpenSubscriptionForPendingUnlockInput): void => {
|
||||
void (async () => {
|
||||
await NavigationStorage.savePendingChatUnlock({
|
||||
@@ -169,6 +178,7 @@ export function useAppNavigator(): AppNavigator {
|
||||
type,
|
||||
payChannel,
|
||||
returnTo: "chat",
|
||||
analytics,
|
||||
});
|
||||
})();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user