Improve trial upgrade conversion

This commit is contained in:
2569718930@qq.com
2026-06-09 18:17:19 +08:00
parent c98ff34c1e
commit 7b965ac60d
11 changed files with 221 additions and 8 deletions
+11 -2
View File
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import type { User } from "@supabase/supabase-js";
import {
User as UserIcon,
@@ -68,6 +68,7 @@ import { useAccountPayment } from "./useAccountPayment";
export function AccountCenter() {
const router = useRouter();
const searchParams = useSearchParams();
const { locale } = useI18n();
const isEn = locale === "en-US";
const copy = useMemo(() => createAccountCopy(isEn), [isEn]);
@@ -401,10 +402,11 @@ export function AccountCenter() {
!isSubscribed && expiryInfo && expiryInfo.expired,
);
const paymentFeatureReady = paymentReadyForRecovery;
const canTrialUpgrade = Boolean(isSubscribed && isTrialSubscription);
const canOpenCheckoutOverlay = Boolean(
paymentFeatureReady &&
!isSubscriptionUnknown &&
(!isSubscribed || showExpiringSoon || showExpiredReminder),
(canTrialUpgrade || !isSubscribed || showExpiringSoon || showExpiredReminder),
);
const subscriptionStatusTitle = showExpiredReminder
? copy.proExpiredTitle
@@ -472,6 +474,13 @@ export function AccountCenter() {
showOverlay,
]);
useEffect(() => {
if (showOverlay || !canOpenCheckoutOverlay) return;
if (searchParams.get("checkout") === "1") {
setShowOverlay(true);
}
}, [canOpenCheckoutOverlay, searchParams, showOverlay]);
// ── Referral points display ────────────────────────────
const referralRewardPointsRaw = Number(referral?.reward_points ?? 3500);
const referralRewardPoints = Number.isFinite(referralRewardPointsRaw)
@@ -0,0 +1,46 @@
import fs from "node:fs";
import path from "node:path";
function assert(condition: unknown, message: string) {
if (!condition) throw new Error(message);
}
export function runTests() {
const projectRoot = process.cwd();
const accountDir = path.join(projectRoot, "components", "account");
const accountCenter = fs.readFileSync(path.join(accountDir, "AccountCenter.tsx"), "utf8");
const paymentFlow = fs.readFileSync(path.join(accountDir, "usePaymentFlow.ts"), "utf8");
const accountPayment = fs.readFileSync(path.join(accountDir, "useAccountPayment.ts"), "utf8");
const productAccess = fs.readFileSync(
path.join(projectRoot, "components", "dashboard", "scan-terminal", "ProductAccessRequired.tsx"),
"utf8",
);
assert(
accountCenter.includes("canTrialUpgrade") &&
accountCenter.includes("isTrialSubscription") &&
accountCenter.includes("canTrialUpgrade || !isSubscribed"),
"trial subscribers must be allowed to open the Pro checkout before expiry",
);
assert(
accountCenter.includes("useSearchParams") &&
accountCenter.includes('searchParams.get("checkout") === "1"') &&
accountCenter.includes("setShowOverlay(true)"),
"account page must support /account?checkout=1 for direct upgrade entry",
);
assert(
productAccess.includes('href="/account?checkout=1"') &&
productAccess.includes("Subscribe & Activate") &&
productAccess.includes("立即订阅并激活"),
"expired terminal gate must send users directly to the checkout entry, not a generic account page",
);
assert(
accountPayment.includes("authUserId,") &&
paymentFlow.includes("authUserId: string") &&
paymentFlow.includes("user_id: authUserId || null"),
"payment_start and payment_success analytics must carry the authenticated user id for trial-to-paid attribution",
);
}
@@ -487,6 +487,7 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
isEn,
copy,
backend,
authUserId,
paymentConfig,
setPaymentConfig,
selectedPlanCode,
@@ -40,6 +40,7 @@ export interface UsePaymentFlowParams {
isEn: boolean;
copy: Record<string, string>;
backend: AuthMeResponse | null;
authUserId: string;
// Shared payment config state
paymentConfig: PaymentConfig | null;
@@ -122,6 +123,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
isEn,
copy,
backend,
authUserId,
paymentConfig,
setPaymentConfig,
selectedPlanCode,
@@ -528,6 +530,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
if (!intentId || !txPayload?.to || !txPayload?.data) throw new Error(copy.intentPayloadInvalid);
trackPaymentStart({
entry: "account_center",
user_id: authUserId || null,
plan_code: selectedPlan?.plan_code || "pro_monthly",
intent_id: intentId,
use_points: billing.canRedeem && usePoints,
@@ -634,6 +637,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
setPaymentInfo(copy.paymentConfirmed.replace("{txHash}", shortAddress(txHashNorm)));
trackPaymentSuccess({
entry: "account_center",
user_id: authUserId || null,
plan_code: selectedPlan?.plan_code || "pro_monthly",
intent_id: intentId,
tx_hash: txHashNorm,
@@ -732,6 +736,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
);
trackPaymentStart({
entry: "account_center_manual_transfer",
user_id: authUserId || null,
plan_code: selectedPlan?.plan_code || "pro_monthly",
intent_id: intentId,
payment_mode: "direct",
@@ -803,6 +808,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
setTxValidation({ loading: false, checked: false });
trackPaymentSuccess({
entry: "account_center_manual_transfer",
user_id: authUserId || null,
plan_code: selectedPlan?.plan_code || "pro_monthly",
intent_id: intentIdVal,
tx_hash: txHashNorm,