feat(analytics): add behavior and payment funnel tracking
This commit is contained in:
@@ -66,6 +66,9 @@ describe("subscription Tailwind components", () => {
|
||||
/<button[^>]*aria-pressed="true"[^>]*>/,
|
||||
)?.[0];
|
||||
expect(selectedButton).toBeDefined();
|
||||
expect(selectedButton).toContain(
|
||||
'data-analytics-key="subscription.payment_method"',
|
||||
);
|
||||
expect(selectedButton).toContain("border-[#f657a0]");
|
||||
expect(selectedButton).not.toContain("border-[rgba(246,87,160,0.18)]");
|
||||
expect(selectedButton).toContain("bg-[#fff4f9]");
|
||||
|
||||
@@ -67,6 +67,8 @@ export function SubscriptionCheckoutButton({
|
||||
<>
|
||||
<SubscriptionCtaButton
|
||||
type="button"
|
||||
data-analytics-key="subscription.checkout"
|
||||
data-analytics-label="Start subscription checkout"
|
||||
disabled={disabled}
|
||||
isLoading={isLoading}
|
||||
onClick={handleClick}
|
||||
@@ -147,6 +149,8 @@ function EzpayRedirectConfirmDialog({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-analytics-key="subscription.external_checkout"
|
||||
data-analytics-label="Continue to external checkout"
|
||||
className={`${dialogStyles.button} ${dialogStyles.primary}`}
|
||||
disabled={isConfirming}
|
||||
onClick={onConfirm}
|
||||
|
||||
@@ -40,6 +40,8 @@ export function SubscriptionCoinsOfferSection({
|
||||
<button
|
||||
key={plan.id}
|
||||
type="button"
|
||||
data-analytics-key="subscription.plan_select"
|
||||
data-analytics-label="Select coin plan"
|
||||
className={`${styles.row} ${selected ? styles.selected : ""}`}
|
||||
aria-pressed={selected}
|
||||
aria-label={`${plan.coins} Coins, ${plan.currency}${plan.price}`}
|
||||
|
||||
@@ -75,6 +75,8 @@ export function SubscriptionPaymentMethod({
|
||||
<button
|
||||
key={method.channel}
|
||||
type="button"
|
||||
data-analytics-key="subscription.payment_method"
|
||||
data-analytics-label="Select payment method"
|
||||
className={classes}
|
||||
disabled={disabled}
|
||||
aria-pressed={selected}
|
||||
|
||||
@@ -46,6 +46,8 @@ export function SubscriptionVipOfferSection({
|
||||
<button
|
||||
key={plan.id}
|
||||
type="button"
|
||||
data-analytics-key="subscription.plan_select"
|
||||
data-analytics-label="Select VIP plan"
|
||||
className={`${styles.planCard} ${selected ? styles.selected : ""}`}
|
||||
aria-pressed={selected}
|
||||
aria-label={`${plan.title}, ${plan.price} ${plan.currency}`}
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import type { PayChannel } from "@/data/dto/payment";
|
||||
import {
|
||||
PAYMENT_ANALYTICS_ENTRY_PARAM,
|
||||
PAYMENT_ANALYTICS_REASON_PARAM,
|
||||
parsePaymentAnalyticsContext,
|
||||
} from "@/lib/analytics";
|
||||
|
||||
import {
|
||||
SubscriptionScreen,
|
||||
@@ -28,6 +33,11 @@ export function SubscriptionPageClient() {
|
||||
const shouldResumePendingOrder = searchParams.get("paymentReturn") === "1";
|
||||
const returnTo = toReturnTo(searchParams.get("returnTo"));
|
||||
const initialPayChannel = toPayChannel(searchParams.get("payChannel"));
|
||||
const analyticsContext = parsePaymentAnalyticsContext({
|
||||
entryPoint: searchParams.get(PAYMENT_ANALYTICS_ENTRY_PARAM),
|
||||
triggerReason: searchParams.get(PAYMENT_ANALYTICS_REASON_PARAM),
|
||||
subscriptionType,
|
||||
});
|
||||
|
||||
return (
|
||||
<SubscriptionScreen
|
||||
@@ -35,6 +45,7 @@ export function SubscriptionPageClient() {
|
||||
shouldResumePendingOrder={shouldResumePendingOrder}
|
||||
returnTo={returnTo}
|
||||
initialPayChannel={initialPayChannel}
|
||||
analyticsContext={analyticsContext}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,14 @@ import { useEffect, useMemo, useRef } from "react";
|
||||
|
||||
import { BackButton } from "@/app/_components";
|
||||
import { Checkbox, MobileShell } from "@/app/_components/core";
|
||||
import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics";
|
||||
import { AppConstants } from "@/core/app_constants";
|
||||
import type { PayChannel } from "@/data/dto/payment";
|
||||
import {
|
||||
behaviorAnalytics,
|
||||
getDefaultPaymentAnalyticsContext,
|
||||
type PaymentAnalyticsContext,
|
||||
} from "@/lib/analytics";
|
||||
import { useUserState } from "@/stores/user/user-context";
|
||||
|
||||
import {
|
||||
@@ -34,6 +40,7 @@ export interface SubscriptionScreenProps {
|
||||
shouldResumePendingOrder?: boolean;
|
||||
returnTo?: "chat" | null;
|
||||
initialPayChannel?: PayChannel | null;
|
||||
analyticsContext?: PaymentAnalyticsContext;
|
||||
}
|
||||
|
||||
export function SubscriptionScreen({
|
||||
@@ -41,6 +48,7 @@ export function SubscriptionScreen({
|
||||
shouldResumePendingOrder = false,
|
||||
returnTo = null,
|
||||
initialPayChannel = null,
|
||||
analyticsContext: providedAnalyticsContext,
|
||||
}: SubscriptionScreenProps) {
|
||||
const userState = useUserState();
|
||||
const countryCode = userState.currentUser?.countryCode;
|
||||
@@ -64,6 +72,9 @@ export function SubscriptionScreen({
|
||||
initialPayChannel: resolvedInitialPayChannel,
|
||||
});
|
||||
const canSubscribeVip = subscriptionType === "vip";
|
||||
const analyticsContext =
|
||||
providedAnalyticsContext ??
|
||||
getDefaultPaymentAnalyticsContext(subscriptionType);
|
||||
|
||||
const vipOfferPlans = useMemo(
|
||||
() => toVipOfferPlanViews(payment.plans),
|
||||
@@ -73,6 +84,17 @@ export function SubscriptionScreen({
|
||||
() => toCoinsOfferPlanViews(payment.plans),
|
||||
[payment.plans],
|
||||
);
|
||||
const displayedPlans = useMemo(() => {
|
||||
const displayedPlanIds = [
|
||||
...(canSubscribeVip ? vipOfferPlans : []),
|
||||
...directCoinsPlans,
|
||||
].map((plan) => plan.id);
|
||||
return displayedPlanIds.flatMap((planId) => {
|
||||
const plan = payment.plans.find((item) => item.planId === planId);
|
||||
return plan ? [plan] : [];
|
||||
});
|
||||
}, [canSubscribeVip, directCoinsPlans, payment.plans, vipOfferPlans]);
|
||||
usePaymentPlanAnalytics(displayedPlans, analyticsContext);
|
||||
const firstRechargeOffer = useMemo(
|
||||
() =>
|
||||
getFirstRechargeOfferView({
|
||||
@@ -95,6 +117,12 @@ export function SubscriptionScreen({
|
||||
const canActivate =
|
||||
selectedPlan !== null && payment.agreed && !isPaymentBusy;
|
||||
|
||||
const handleSelectPlan = (planId: string) => {
|
||||
const plan = payment.plans.find((item) => item.planId === planId);
|
||||
if (plan) behaviorAnalytics.planClick(plan, analyticsContext);
|
||||
paymentDispatch({ type: "PaymentPlanSelected", planId });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isPaymentBusy) return;
|
||||
|
||||
@@ -160,6 +188,7 @@ export function SubscriptionScreen({
|
||||
className={styles.backSlot}
|
||||
onClick={handleBackClick}
|
||||
variant="soft"
|
||||
analyticsKey="subscription.back"
|
||||
/>
|
||||
</header>
|
||||
|
||||
@@ -187,23 +216,13 @@ export function SubscriptionScreen({
|
||||
<SubscriptionVipOfferSection
|
||||
plans={vipOfferPlans}
|
||||
selectedPlanId={payment.selectedPlanId || selectedPlan?.id || null}
|
||||
onSelectPlan={(planId) =>
|
||||
paymentDispatch({
|
||||
type: "PaymentPlanSelected",
|
||||
planId,
|
||||
})
|
||||
}
|
||||
onSelectPlan={handleSelectPlan}
|
||||
/>
|
||||
) : null}
|
||||
<SubscriptionCoinsOfferSection
|
||||
plans={directCoinsPlans}
|
||||
selectedPlanId={payment.selectedPlanId || selectedPlan?.id || null}
|
||||
onSelectPlan={(planId) =>
|
||||
paymentDispatch({
|
||||
type: "PaymentPlanSelected",
|
||||
planId,
|
||||
})
|
||||
}
|
||||
onSelectPlan={handleSelectPlan}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user