Refine pricing display and Gaussian context

This commit is contained in:
2569718930@qq.com
2026-06-07 01:08:13 +08:00
parent fb4a144fcf
commit f66127da44
10 changed files with 128 additions and 60 deletions
+12 -20
View File
@@ -63,10 +63,6 @@ import {
import { createAccountCopy } from "./account-copy"; import { createAccountCopy } from "./account-copy";
import { resetWalletConnectProvider } from "./wallet"; import { resetWalletConnectProvider } from "./wallet";
import { useAccountPayment } from "./useAccountPayment"; import { useAccountPayment } from "./useAccountPayment";
import {
isTelegramPrivateGroupPriceEligible,
telegramPrivateGroupAmountUsdc,
} from "./telegram-pricing";
// --- Main Component --- // --- Main Component ---
@@ -437,12 +433,14 @@ export function AccountCenter() {
{ plan_code: "pro_monthly", plan_id: 101, amount_usdc: "29.9", duration_days: 30 }, { plan_code: "pro_monthly", plan_id: 101, amount_usdc: "29.9", duration_days: 30 },
{ plan_code: "pro_quarterly", plan_id: 102, amount_usdc: "79.9", duration_days: 90 }, { plan_code: "pro_quarterly", plan_id: 102, amount_usdc: "79.9", duration_days: 90 },
]; ];
const privateTelegramGroupPriceActive = isTelegramPrivateGroupPriceEligible( const selectedPlanDurationDays = Number(selectedPlan?.duration_days || 30);
backend?.telegram_pricing, const overlayPlanLabel =
); selectedPlanCode === "pro_quarterly"
const privateTelegramGroupAmount = telegramPrivateGroupAmountUsdc( ? copy.quarterlyPlan
backend?.telegram_pricing, : copy.monthlyPlan;
); const overlayPeriodLabel = isEn
? `/ ${selectedPlanDurationDays} days`
: `/ ${selectedPlanDurationDays}`;
const referral = backend?.referral; const referral = backend?.referral;
const referralCode = String(referral?.code || "").trim(); const referralCode = String(referral?.code || "").trim();
const appliedReferralCode = String(referral?.applied_code || "").trim(); const appliedReferralCode = String(referral?.applied_code || "").trim();
@@ -915,6 +913,8 @@ export function AccountCenter() {
<UnlockProOverlay <UnlockProOverlay
points={totalPoints} points={totalPoints}
planPriceUsd={billing.planAmount} planPriceUsd={billing.planAmount}
planLabel={overlayPlanLabel}
periodLabel={overlayPeriodLabel}
usePoints={usePoints} usePoints={usePoints}
onToggleUsePoints={() => setUsePoints((prev) => !prev)} onToggleUsePoints={() => setUsePoints((prev) => !prev)}
billing={{ billing={{
@@ -932,6 +932,7 @@ export function AccountCenter() {
payBusy={paymentBusy} payBusy={paymentBusy}
payLabel={hasPayingWallet ? copy.payNow : copy.connectAndPay} payLabel={hasPayingWallet ? copy.payNow : copy.connectAndPay}
manualPayLabel="手动转账" manualPayLabel="手动转账"
locale={locale}
errorText={paymentError || undefined} errorText={paymentError || undefined}
infoText={paymentInfo || undefined} infoText={paymentInfo || undefined}
txHash={lastTxHash || undefined} txHash={lastTxHash || undefined}
@@ -1112,13 +1113,6 @@ export function AccountCenter() {
const code = String(plan.plan_code || ""); const code = String(plan.plan_code || "");
const active = code === selectedPlanCode; const active = code === selectedPlanCode;
const isQuarterly = code === "pro_quarterly"; const isQuarterly = code === "pro_quarterly";
const isPrivateGroupMonthly = Boolean(
code === "pro_monthly" &&
privateTelegramGroupPriceActive &&
privateTelegramGroupAmount &&
Number(plan.amount_usdc) ===
Number(privateTelegramGroupAmount),
);
return ( return (
<button <button
type="button" type="button"
@@ -1135,9 +1129,7 @@ export function AccountCenter() {
<span> <span>
{isQuarterly {isQuarterly
? copy.quarterlyPlan ? copy.quarterlyPlan
: isPrivateGroupMonthly : copy.monthlyPlan}
? copy.privateGroupMonthlyPlan
: copy.monthlyPlan}
</span> </span>
<span>{plan.amount_usdc} USDC</span> <span>{plan.amount_usdc} USDC</span>
</div> </div>
@@ -27,6 +27,10 @@ export function runTests() {
path.join(projectRoot, "components", "account", "useBilling.ts"), path.join(projectRoot, "components", "account", "useBilling.ts"),
"utf8", "utf8",
); );
const unlockProOverlay = fs.readFileSync(
path.join(projectRoot, "components", "subscription", "UnlockProOverlay.tsx"),
"utf8",
);
const telegramPricing = fs.readFileSync( const telegramPricing = fs.readFileSync(
path.join(projectRoot, "components", "account", "telegram-pricing.ts"), path.join(projectRoot, "components", "account", "telegram-pricing.ts"),
"utf8", "utf8",
@@ -76,20 +80,30 @@ export function runTests() {
!telegramPricing.includes("is_group_member") && !telegramPricing.includes("is_group_member") &&
useAccountPayment.includes('=== "pro_monthly"') && useAccountPayment.includes('=== "pro_monthly"') &&
useAccountPayment.includes("amount_usdc: telegramAmountUsdc"), useAccountPayment.includes("amount_usdc: telegramAmountUsdc"),
"account payment plan cards must only display the 5 USDC private Telegram group monthly price after private /bind verification", "account payment plan cards must only display the 5 USDC discounted monthly price after verified /bind eligibility",
); );
assert( assert(
useBilling.includes("telegramGroupPriceApplies") && useBilling.includes("telegramGroupPriceApplies") &&
useBilling.includes("isTelegramPrivateGroupPriceEligible") && useBilling.includes("isTelegramPrivateGroupPriceEligible") &&
useBilling.includes("backend?.telegram_pricing") && useBilling.includes("backend?.telegram_pricing") &&
useBilling.includes("!telegramGroupPriceApplies"), useBilling.includes("!telegramGroupPriceApplies"),
"billing must not let referral first-month pricing override the lower private Telegram group monthly price", "billing must not let referral first-month pricing override the lower verified monthly price",
); );
assert( assert(
accountCenter.includes("privateGroupMonthlyPlan") && !accountCenter.includes(["private", "Group", "Monthly", "Plan"].join("")) &&
accountCopy.includes("Private group monthly") && !accountCopy.includes(["Private", "group", "monthly"].join(" ")) &&
accountCopy.includes("私密群月付"), !accountCopy.includes(["私", "密", "群", "月", "付"].join("")),
"account plan card must label the 5 USDC price as a private Telegram group monthly price", "account plan card and checkout overlay should not expose a separate discounted monthly label",
);
assert(
accountCenter.includes("overlayPlanLabel") &&
accountCenter.includes("overlayPeriodLabel") &&
!accountCenter.includes(`copy.${["private", "Group", "Monthly", "Plan"].join("")}`) &&
unlockProOverlay.includes("planLabel") &&
unlockProOverlay.includes("USDC") &&
!unlockProOverlay.includes("<span className={s.price}>${planPriceUsd.toFixed(2)}</span>") &&
!unlockProOverlay.includes('<span className={s.summaryUnit}>USD</span>'),
"checkout overlay must display payment amounts as USDC without exposing the discounted-price source label",
); );
assert( assert(
types.includes("ReferralSummary") && types.includes("ReferralSummary") &&
@@ -63,7 +63,6 @@ export function createAccountCopy(isEn: boolean): Record<string, string> {
paymentMgmt: isEn ? "Payment Management" : "支付管理", paymentMgmt: isEn ? "Payment Management" : "支付管理",
proPlan: isEn ? "Pro Plan" : "Pro 套餐", proPlan: isEn ? "Pro Plan" : "Pro 套餐",
monthlyPlan: isEn ? "Monthly" : "月付", monthlyPlan: isEn ? "Monthly" : "月付",
privateGroupMonthlyPlan: isEn ? "Private group monthly" : "私密群月付",
quarterlyPlan: isEn ? "Quarterly" : "季度", quarterlyPlan: isEn ? "Quarterly" : "季度",
trialBadge: isEn ? "3-day trial" : "3天试用", trialBadge: isEn ? "3-day trial" : "3天试用",
trialPaidGroupLocked: isEn trialPaidGroupLocked: isEn
@@ -331,7 +331,7 @@ function TemperatureChartCanvasComponent({
payload={props.payload as ReadonlyArray<{ payload?: Record<string, any> }> | undefined} payload={props.payload as ReadonlyArray<{ payload?: Record<string, any> }> | undefined}
data={zoomedData} data={zoomedData}
series={activeSeries} series={activeSeries}
probabilityOverlay={null} probabilityOverlay={probabilityOverlay}
tempSymbol={tempSymbol} tempSymbol={tempSymbol}
isEn={isEn} isEn={isEn}
/> />
@@ -123,10 +123,31 @@ function buildTooltipProbabilityRows(
tempSymbol: string, tempSymbol: string,
isEn: boolean, isEn: boolean,
): TooltipProbabilityRow[] { ): TooltipProbabilityRow[] {
void probabilityOverlay; if (!probabilityOverlay) return [];
void tempSymbol; const rows: TooltipProbabilityRow[] = [];
void isEn; const mu = validNumber(probabilityOverlay.muLine?.value);
return []; if (mu !== null) {
rows.push({
key: "legacy_probability_mu",
label: isEn ? "Gaussian μ" : "高斯 μ",
value: `${mu.toFixed(1)}${tempSymbol}`,
color: "#8b5cf6",
});
}
const topBand = [...probabilityOverlay.bands]
.filter((band) => validNumber(band.probability) !== null)
.sort((a, b) => b.probability - a.probability)[0];
if (topBand) {
const probabilityPct = Math.round(topBand.probability * 100);
rows.push({
key: topBand.key,
label: topBand.label,
value: `${probabilityPct}%`,
color: "#a78bfa",
});
}
return rows;
} }
export const __buildTemperatureTooltipRowsForTest = buildTooltipRows; export const __buildTemperatureTooltipRowsForTest = buildTooltipRows;
@@ -1660,7 +1660,7 @@ export function runTests() {
const firstBand = bandPoints[0].runway_band; const firstBand = bandPoints[0].runway_band;
assert(Array.isArray(firstBand) && firstBand[0] === 24.0 && firstBand[1] === 26.0, "runway_band tuple values should match input limits"); assert(Array.isArray(firstBand) && firstBand[0] === 24.0 && firstBand[1] === 26.0, "runway_band tuple values should match input limits");
// ── Legacy Gaussian probability data should not render as a visible chart overlay ── // ── Legacy Gaussian probability data should be available as compact tooltip context only ──
const gaussianOverlayChart = __buildTemperatureChartDataForTest( const gaussianOverlayChart = __buildTemperatureChartDataForTest(
{ {
city: "toronto", city: "toronto",
@@ -1688,9 +1688,13 @@ export function runTests() {
) as any; ) as any;
const gaussianOverlay = gaussianOverlayChart.probabilityOverlay; const gaussianOverlay = gaussianOverlayChart.probabilityOverlay;
assert(gaussianOverlay === null, "legacy Gaussian probabilities should not create visible chart overlays"); assert(
gaussianOverlay?.muLine?.label === "Gaussian μ 27.4°C" &&
gaussianOverlay.bands.length === 3,
"legacy Gaussian probabilities should remain available for compact tooltip context",
);
assert( assert(
!gaussianOverlayChart.series.some((series: any) => String(series.key || "").includes("probability")), !gaussianOverlayChart.series.some((series: any) => String(series.key || "").includes("probability")),
"legacy Gaussian probability distribution should not be rendered as a time-series line", "legacy Gaussian probability distribution should not be rendered as a time-series line on the main chart",
); );
} }
@@ -1,3 +1,7 @@
import {
readFileSync,
} from "node:fs";
import path from "node:path";
import { import {
__buildTemperatureTooltipProbabilityRowsForTest, __buildTemperatureTooltipProbabilityRowsForTest,
__buildTemperatureTooltipRowsForTest, __buildTemperatureTooltipRowsForTest,
@@ -73,7 +77,23 @@ export function runTests() {
); );
assert( assert(
probabilityRows.length === 0, probabilityRows.length === 2,
"temperature tooltip should not show Gaussian μ or probability-band rows", "temperature tooltip should show Gaussian μ and the leading probability bucket as compact context",
);
assert(
probabilityRows.some((row) => row.key === "legacy_probability_mu" && row.value === "27.4°C") &&
probabilityRows.some((row) => row.key === "legacy_probability_27_0" && row.value === "42%"),
"temperature tooltip should format Gaussian μ and probability values without drawing a full probability band on the main chart",
);
const projectRoot = process.cwd();
const chartCanvasSource = readFileSync(
path.join(projectRoot, "components", "dashboard", "scan-terminal", "TemperatureChartCanvas.tsx"),
"utf8",
);
assert(
chartCanvasSource.includes("probabilityOverlay={probabilityOverlay}") &&
!chartCanvasSource.includes("probabilityOverlay={null}"),
"temperature chart canvas must pass probability overlay data into the tooltip instead of hiding Gaussian μ",
); );
} }
@@ -2179,9 +2179,9 @@ function buildFullDayChartData(
return point; return point;
}); });
// Keep probability data in the payload for market views, but do not draw // Keep legacy Gaussian data as compact tooltip context. It is not rendered
// legacy Gaussian bands on the live temperature chart. // as a time-series line on the live temperature chart.
const probabilityOverlay = null; const probabilityOverlay = buildLegacyGaussianProbabilityOverlay(row, hourly);
return { data, series, probabilityOverlay }; return { data, series, probabilityOverlay };
} }
@@ -645,8 +645,8 @@ function InstitutionalLandingScreen({ locale }: { locale: LandingLocale }) {
</h3> </h3>
<p className="mt-3 flex-1 text-sm leading-7 text-slate-600"> <p className="mt-3 flex-1 text-sm leading-7 text-slate-600">
{isEn {isEn
? "For private groups that need shared access, Telegram workflow support, and manual onboarding." ? "For teams that need shared access, Telegram workflow support, and manual onboarding."
: "面向需要共享权限、Telegram 工作流支持和人工开通的私密团队。"} : "面向需要共享权限、Telegram 工作流支持和人工开通的团队。"}
</p> </p>
<div className="mt-7 rounded-md border border-slate-200 bg-white px-4 py-3 text-xs font-semibold text-slate-600"> <div className="mt-7 rounded-md border border-slate-200 bg-white px-4 py-3 text-xs font-semibold text-slate-600">
{isEn {isEn
@@ -37,6 +37,8 @@ export type UnlockProBilling = {
type UnlockProOverlayProps = { type UnlockProOverlayProps = {
points: number; points: number;
planPriceUsd: number; planPriceUsd: number;
planLabel?: string;
periodLabel?: string;
usePoints: boolean; usePoints: boolean;
billing: UnlockProBilling; billing: UnlockProBilling;
onToggleUsePoints: () => void; onToggleUsePoints: () => void;
@@ -56,6 +58,11 @@ type UnlockProOverlayProps = {
paymentTokenLabel?: string; paymentTokenLabel?: string;
}; };
function formatUsdcAmount(value: number) {
if (!Number.isFinite(value)) return "0";
return value.toFixed(2).replace(/\.?0+$/, "");
}
const FEATURES = { const FEATURES = {
"zh-CN": [ "zh-CN": [
"市场扫描台 + V4-Pro 深度复核", "市场扫描台 + V4-Pro 深度复核",
@@ -74,6 +81,8 @@ const FEATURES = {
export function UnlockProOverlay({ export function UnlockProOverlay({
points, points,
planPriceUsd, planPriceUsd,
planLabel,
periodLabel,
usePoints, usePoints,
billing, billing,
onToggleUsePoints, onToggleUsePoints,
@@ -95,6 +104,13 @@ export function UnlockProOverlay({
const isEn = locale === "en-US"; const isEn = locale === "en-US";
const canUsePoints = billing.pointsEnabled && billing.isEligible; const canUsePoints = billing.pointsEnabled && billing.isEligible;
const featureList = FEATURES[locale] ?? FEATURES["zh-CN"]; const featureList = FEATURES[locale] ?? FEATURES["zh-CN"];
const currencyLabel = "USDC";
const formattedPlanPrice = formatUsdcAmount(planPriceUsd);
const formattedFinalPrice = formatUsdcAmount(billing.finalPrice);
const formattedDiscount = formatUsdcAmount(billing.discountAmount);
const formattedMaxDiscount = formatUsdcAmount(billing.maxDiscountUsd);
const resolvedPlanLabel = planLabel || "Standard Pro";
const resolvedPeriodLabel = periodLabel || (isEn ? "/ 30 days" : "/ 30 天");
const finalPayLabel = const finalPayLabel =
payLabel || (isEn ? "Subscribe & Activate" : "立即订阅并激活服务"); payLabel || (isEn ? "Subscribe & Activate" : "立即订阅并激活服务");
@@ -106,6 +122,8 @@ export function UnlockProOverlay({
const redeemableUsdNow = billing.pointsEnabled const redeemableUsdNow = billing.pointsEnabled
? Math.min(maxDiscountUsdInt, Math.floor(points / Math.max(1, billing.pointsPerUsd))) ? Math.min(maxDiscountUsdInt, Math.floor(points / Math.max(1, billing.pointsPerUsd)))
: 0; : 0;
const formattedRedeemableNow = formatUsdcAmount(redeemableUsdNow);
const formattedMaxDiscountInt = formatUsdcAmount(maxDiscountUsdInt);
const progressPct = billing.pointsEnabled const progressPct = billing.pointsEnabled
? Math.min(100, Math.round((points / maxPointsForFullDiscount) * 100)) ? Math.min(100, Math.round((points / maxPointsForFullDiscount) * 100))
: 0; : 0;
@@ -155,7 +173,7 @@ export function UnlockProOverlay({
<div className={s.planCard}> <div className={s.planCard}>
<span className={s.planChip}> <span className={s.planChip}>
<Zap size={10} /> <Zap size={10} />
Standard Pro {resolvedPlanLabel}
</span> </span>
<div <div
@@ -166,8 +184,8 @@ export function UnlockProOverlay({
marginTop: 12, marginTop: 12,
}} }}
> >
<span className={s.price}>${planPriceUsd.toFixed(2)}</span> <span className={s.price}>{formattedPlanPrice}</span>
<span className={s.priceSuffix}>/ {isEn ? "mo" : "月"}</span> <span className={s.priceSuffix}>{currencyLabel} {resolvedPeriodLabel}</span>
</div> </div>
<ul className={s.featureList}> <ul className={s.featureList}>
@@ -217,24 +235,24 @@ export function UnlockProOverlay({
<span <span
className={`${s.discount} ${usePoints ? s.discountActive : ""}`} className={`${s.discount} ${usePoints ? s.discountActive : ""}`}
> >
-${billing.discountAmount.toFixed(2)} -{formattedDiscount}
</span> </span>
<span className={s.discountSuffix}>{isEn ? "off" : "减免"}</span> <span className={s.discountSuffix}>{currencyLabel}</span>
</div> </div>
<p className={s.pointsNote}> <p className={s.pointsNote}>
{usePoints {usePoints
? isEn ? isEn
? `Using ${billing.pointsUsed} pts · saves $${billing.discountAmount.toFixed(2)}` ? `Using ${billing.pointsUsed} pts · saves ${formattedDiscount} ${currencyLabel}`
: `已消耗 ${billing.pointsUsed} 积分 · 省 $${billing.discountAmount.toFixed(2)}` : `已消耗 ${billing.pointsUsed} 积分 · 省 ${formattedDiscount} ${currencyLabel}`
: isEn : isEn
? `Toggle to save up to $${billing.maxDiscountUsd.toFixed(2)}` ? `Toggle to save up to ${formattedMaxDiscount} ${currencyLabel}`
: `开启可最多抵扣 $${billing.maxDiscountUsd.toFixed(2)}`} : `开启可最多抵扣 ${formattedMaxDiscount} ${currencyLabel}`}
</p> </p>
<p className={s.pointsNote}> <p className={s.pointsNote}>
{isEn {isEn
? `Now redeemable: $${redeemableUsdNow.toFixed(0)} / $${maxDiscountUsdInt.toFixed(0)}` ? `Now redeemable: ${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`
: `当前可抵:${redeemableUsdNow.toFixed(0)}U / ${maxDiscountUsdInt.toFixed(0)}U`} : `当前可抵:${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`}
</p> </p>
<div className={s.pointsBalance}> <div className={s.pointsBalance}>
@@ -276,8 +294,8 @@ export function UnlockProOverlay({
? "Points redemption is unavailable for this plan." ? "Points redemption is unavailable for this plan."
: "当前套餐暂不支持积分抵扣。" : "当前套餐暂不支持积分抵扣。"
: isEn : isEn
? `Starts at ${billing.pointsPerUsd} pts, ${billing.pointsPerUsd} pts per $1, up to $${maxDiscountUsdInt}. You have: ${points}` ? `Starts at ${billing.pointsPerUsd} pts, ${billing.pointsPerUsd} pts per 1 ${currencyLabel}, up to ${formattedMaxDiscountInt} ${currencyLabel}. You have: ${points}`
: `${billing.pointsPerUsd} 分起兑,每 ${billing.pointsPerUsd} 分抵 1U,最多抵 ${maxDiscountUsdInt}U。当前 ${points}`} : `${billing.pointsPerUsd} 分起兑,每 ${billing.pointsPerUsd} 分抵 1 ${currencyLabel},最多抵 ${formattedMaxDiscountInt} ${currencyLabel}。当前 ${points}`}
</p> </p>
{billing.pointsEnabled && ( {billing.pointsEnabled && (
@@ -288,8 +306,8 @@ export function UnlockProOverlay({
</span> </span>
<span> <span>
{isEn {isEn
? `$${redeemableUsdNow.toFixed(0)} / $${maxDiscountUsdInt.toFixed(0)}` ? `${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`
: `${redeemableUsdNow.toFixed(0)}U / ${maxDiscountUsdInt.toFixed(0)}U`} : `${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`}
</span> </span>
</div> </div>
<div className={s.progressTrack}> <div className={s.progressTrack}>
@@ -341,15 +359,15 @@ export function UnlockProOverlay({
)} )}
{billing.discountAmount > 0 && usePoints && ( {billing.discountAmount > 0 && usePoints && (
<p className={s.summaryOriginal}> <p className={s.summaryOriginal}>
${planPriceUsd.toFixed(2)} USD {formattedPlanPrice} {currencyLabel}
</p> </p>
)} )}
</div> </div>
<div className={s.summaryAmount}> <div className={s.summaryAmount}>
<span className={s.summaryPrice}> <span className={s.summaryPrice}>
${billing.finalPrice.toFixed(2)} {formattedFinalPrice}
</span> </span>
<span className={s.summaryUnit}>USD</span> <span className={s.summaryUnit}>{currencyLabel}</span>
</div> </div>
</div> </div>