Add trial value replay upgrade trigger
This commit is contained in:
@@ -63,6 +63,10 @@ import {
|
||||
import { createAccountCopy } from "./account-copy";
|
||||
import { resetWalletConnectProvider } from "./wallet";
|
||||
import { useAccountPayment } from "./useAccountPayment";
|
||||
import {
|
||||
buildTrialValueReplaySummary,
|
||||
readTrialValueReplay,
|
||||
} from "@/lib/trial-value-replay";
|
||||
|
||||
// --- Main Component ---
|
||||
|
||||
@@ -78,6 +82,7 @@ export function AccountCenter() {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showSecondarySections, setShowSecondarySections] = useState(false);
|
||||
const [trialValueReplay, setTrialValueReplay] = useState(() => readTrialValueReplay());
|
||||
|
||||
// ── Shared state (declared in component, written by hook via setters) ─
|
||||
const [showOverlay, setShowOverlay] = useState(false);
|
||||
@@ -445,6 +450,13 @@ export function AccountCenter() {
|
||||
const overlayPeriodLabel = isEn
|
||||
? `/ ${selectedPlanDurationDays} days`
|
||||
: `/ ${selectedPlanDurationDays} 天`;
|
||||
const trialValueReplaySummary = useMemo(
|
||||
() => buildTrialValueReplaySummary(trialValueReplay, {
|
||||
isEn,
|
||||
trialExpiresAt: displayExpiryRaw,
|
||||
}),
|
||||
[displayExpiryRaw, isEn, trialValueReplay],
|
||||
);
|
||||
const referral = backend?.referral;
|
||||
const referralCode = String(referral?.code || "").trim();
|
||||
const appliedReferralCode = String(referral?.applied_code || "").trim();
|
||||
@@ -481,6 +493,24 @@ export function AccountCenter() {
|
||||
}
|
||||
}, [canOpenCheckoutOverlay, searchParams, showOverlay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTrialSubscription) return;
|
||||
setTrialValueReplay(readTrialValueReplay());
|
||||
}, [authUserId, isTrialSubscription, showOverlay]);
|
||||
|
||||
const openTrialValueReplayCheckout = useCallback(() => {
|
||||
trackAppEvent("paywall_feature_clicked", {
|
||||
entry: "account_center",
|
||||
feature: "trial_value_replay_upgrade",
|
||||
user_id: authUserId || null,
|
||||
replay_cities: trialValueReplay.citiesViewed.length,
|
||||
replay_terminal_visits: trialValueReplay.terminalVisits,
|
||||
replay_rows_available: trialValueReplay.rowsAvailableMax,
|
||||
subscription_plan_code: planCode || null,
|
||||
});
|
||||
setShowOverlay(true);
|
||||
}, [authUserId, planCode, trialValueReplay]);
|
||||
|
||||
// ── Referral points display ────────────────────────────
|
||||
const referralRewardPointsRaw = Number(referral?.reward_points ?? 3500);
|
||||
const referralRewardPoints = Number.isFinite(referralRewardPointsRaw)
|
||||
@@ -690,6 +720,45 @@ export function AccountCenter() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isTrialSubscription && canOpenCheckoutOverlay && (
|
||||
<section className="lg:col-span-12 overflow-hidden rounded-2xl border border-amber-200 bg-white shadow-sm">
|
||||
<div className="grid gap-0 md:grid-cols-[minmax(0,1fr)_auto]">
|
||||
<div className="min-w-0 p-6">
|
||||
<div className="mb-3 flex flex-wrap items-center gap-2 text-xs font-black uppercase text-amber-700">
|
||||
<Sparkles size={15} />
|
||||
<span>{isEn ? "Trial value replay" : "试用价值回放"}</span>
|
||||
{trialValueReplaySummary.hasUsageEvidence ? (
|
||||
<span className="rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-[10px] text-amber-800">
|
||||
{isEn ? "Based on your trial" : "基于你的试用"}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="max-w-3xl text-base font-bold leading-7 text-slate-950">
|
||||
{trialValueReplaySummary.headline}
|
||||
</p>
|
||||
<div className="mt-4 grid gap-2 text-sm text-slate-600 md:grid-cols-3">
|
||||
{trialValueReplaySummary.bullets.map((bullet) => (
|
||||
<div key={bullet} className="flex min-w-0 items-start gap-2">
|
||||
<CheckCircle2 size={15} className="mt-0.5 shrink-0 text-emerald-500" />
|
||||
<span className="min-w-0 leading-5">{bullet}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center border-t border-amber-100 bg-amber-50 p-6 md:border-l md:border-t-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openTrialValueReplayCheckout}
|
||||
className="inline-flex min-h-11 w-full items-center justify-center gap-2 rounded-xl bg-amber-600 px-5 py-3 text-sm font-black text-white shadow-sm transition hover:bg-amber-700 md:w-auto"
|
||||
>
|
||||
<Crown size={16} />
|
||||
{trialValueReplaySummary.primaryCta}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* User Card */}
|
||||
<div className="flex flex-col items-center gap-8 rounded-2xl border border-slate-200 bg-white p-8 shadow-sm lg:col-span-8 md:flex-row">
|
||||
<div className="relative">
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { buildTrialValueReplaySummary } from "@/lib/trial-value-replay";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
export function runTests() {
|
||||
const projectRoot = process.cwd();
|
||||
const replayPath = path.join(projectRoot, "lib", "trial-value-replay.ts");
|
||||
const accountCenter = fs.readFileSync(
|
||||
path.join(projectRoot, "components", "account", "AccountCenter.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const dashboardSource = fs.readFileSync(
|
||||
path.join(projectRoot, "components", "dashboard", "ScanTerminalDashboard.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert(fs.existsSync(replayPath), "trial value replay helper must exist");
|
||||
const replaySource = fs.readFileSync(replayPath, "utf8");
|
||||
const accountReplaySurface = `${accountCenter}\n${replaySource}`;
|
||||
|
||||
assert(
|
||||
replaySource.includes("TRIAL_VALUE_REPLAY_STORAGE_KEY") &&
|
||||
replaySource.includes("recordTrialValueReplay") &&
|
||||
replaySource.includes("readTrialValueReplay") &&
|
||||
replaySource.includes("buildTrialValueReplaySummary"),
|
||||
"trial value replay must persist and summarize local trial usage evidence",
|
||||
);
|
||||
|
||||
assert(
|
||||
dashboardSource.includes("recordTrialValueReplay") &&
|
||||
dashboardSource.includes("isTrialTerminalAccess") &&
|
||||
dashboardSource.includes("selectedRow"),
|
||||
"terminal trial users must record real terminal usage for later value replay",
|
||||
);
|
||||
|
||||
assert(
|
||||
accountCenter.includes("buildTrialValueReplaySummary") &&
|
||||
accountCenter.includes("trialValueReplay") &&
|
||||
accountReplaySurface.includes("你本次试用已经") &&
|
||||
accountReplaySurface.includes("保持访问") &&
|
||||
accountReplaySurface.includes("Keep access"),
|
||||
"account trial upgrade trigger must replay the user's own trial value before opening checkout",
|
||||
);
|
||||
|
||||
assert(
|
||||
accountCenter.includes('trackAppEvent("paywall_feature_clicked"') &&
|
||||
accountCenter.includes('feature: "trial_value_replay_upgrade"'),
|
||||
"trial value replay upgrade CTA must be tracked before checkout opens",
|
||||
);
|
||||
|
||||
const zhSummary = buildTrialValueReplaySummary(
|
||||
{
|
||||
firstActiveAt: "2026-06-09T00:00:00.000Z",
|
||||
lastActiveAt: "2026-06-09T00:20:00.000Z",
|
||||
lastCityName: "Taipei",
|
||||
lastSignalLabel: "Hot",
|
||||
lastUserId: "user-1",
|
||||
terminalVisits: 2,
|
||||
rowsAvailableMax: 50,
|
||||
signalsViewed: 2,
|
||||
citiesViewed: ["Taipei", "New York"],
|
||||
},
|
||||
{ isEn: false },
|
||||
);
|
||||
|
||||
assert(
|
||||
zhSummary.headline === "你本次试用已经查看 2 个天气信号。" &&
|
||||
zhSummary.bullets.includes("已记录 2 次终端使用。") &&
|
||||
zhSummary.bullets.includes("Pro 终端本次提供了 50 个城市机会。") &&
|
||||
zhSummary.primaryCta === "保持访问",
|
||||
"trial value replay summary must turn usage evidence into concrete upgrade copy",
|
||||
);
|
||||
|
||||
const expirySummary = buildTrialValueReplaySummary(
|
||||
{
|
||||
firstActiveAt: null,
|
||||
lastActiveAt: null,
|
||||
lastCityName: null,
|
||||
lastSignalLabel: null,
|
||||
lastUserId: null,
|
||||
terminalVisits: 0,
|
||||
rowsAvailableMax: 0,
|
||||
signalsViewed: 0,
|
||||
citiesViewed: [],
|
||||
},
|
||||
{ isEn: false, trialExpiresAt: "2099-01-01T00:00:00.000Z" },
|
||||
);
|
||||
assert(
|
||||
expirySummary.bullets.some((bullet) => bullet.includes("试用剩余")),
|
||||
"trial value replay summary must include trial time remaining when expiry is known",
|
||||
);
|
||||
}
|
||||
@@ -80,6 +80,7 @@ import {
|
||||
} from "@/components/dashboard/scan-terminal/city-fallback-rows";
|
||||
import { markAnalyticsOnce, trackAppEvent } from "@/lib/app-analytics";
|
||||
import { STATIC_CITY_LIST } from "@/lib/static-cities";
|
||||
import { recordTrialValueReplay } from "@/lib/trial-value-replay";
|
||||
|
||||
const TrainingDashboard = dynamic(
|
||||
() =>
|
||||
@@ -615,6 +616,7 @@ function PolyWeatherTerminal({
|
||||
toggleLocale,
|
||||
isTrialTerminalAccess,
|
||||
trialSubscriptionExpiresAt,
|
||||
trialUserId,
|
||||
userLocalTime,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
@@ -637,6 +639,7 @@ function PolyWeatherTerminal({
|
||||
toggleLocale: () => void;
|
||||
isTrialTerminalAccess: boolean;
|
||||
trialSubscriptionExpiresAt: string | null;
|
||||
trialUserId: string | null;
|
||||
userLocalTime: string;
|
||||
searchQuery: string;
|
||||
setSearchQuery: (val: string) => void;
|
||||
@@ -882,6 +885,23 @@ function PolyWeatherTerminal({
|
||||
const selectedSignal = selectedRow ? getSignalState(selectedRow) : "data" as const;
|
||||
const selectedLabel = selectedRow ? getSignalLabel(selectedSignal, isEn) : "";
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTrialTerminalAccess || !selectedRow) return;
|
||||
recordTrialValueReplay({
|
||||
userId: trialUserId,
|
||||
cityName: rowName(selectedRow),
|
||||
signalLabel: selectedLabel,
|
||||
rowsAvailable: filteredRegionRows.length || rows.length,
|
||||
});
|
||||
}, [
|
||||
filteredRegionRows.length,
|
||||
isTrialTerminalAccess,
|
||||
rows.length,
|
||||
selectedLabel,
|
||||
selectedRow,
|
||||
trialUserId,
|
||||
]);
|
||||
|
||||
const continentGroups = useMemo(
|
||||
() => buildContinentGroups(filteredRegionRows, isEn),
|
||||
[filteredRegionRows, isEn]
|
||||
@@ -1778,6 +1798,7 @@ function ScanTerminalScreen() {
|
||||
trialSubscriptionExpiresAt={
|
||||
proAccess.subscriptionTotalExpiresAt || proAccess.subscriptionExpiresAt
|
||||
}
|
||||
trialUserId={proAccess.userId}
|
||||
userLocalTime={userLocalTime}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
|
||||
Reference in New Issue
Block a user