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 { resetWalletConnectProvider } from "./wallet";
import { useAccountPayment } from "./useAccountPayment";
import {
isTelegramPrivateGroupPriceEligible,
telegramPrivateGroupAmountUsdc,
} from "./telegram-pricing";
// --- 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_quarterly", plan_id: 102, amount_usdc: "79.9", duration_days: 90 },
];
const privateTelegramGroupPriceActive = isTelegramPrivateGroupPriceEligible(
backend?.telegram_pricing,
);
const privateTelegramGroupAmount = telegramPrivateGroupAmountUsdc(
backend?.telegram_pricing,
);
const selectedPlanDurationDays = Number(selectedPlan?.duration_days || 30);
const overlayPlanLabel =
selectedPlanCode === "pro_quarterly"
? copy.quarterlyPlan
: copy.monthlyPlan;
const overlayPeriodLabel = isEn
? `/ ${selectedPlanDurationDays} days`
: `/ ${selectedPlanDurationDays}`;
const referral = backend?.referral;
const referralCode = String(referral?.code || "").trim();
const appliedReferralCode = String(referral?.applied_code || "").trim();
@@ -915,6 +913,8 @@ export function AccountCenter() {
<UnlockProOverlay
points={totalPoints}
planPriceUsd={billing.planAmount}
planLabel={overlayPlanLabel}
periodLabel={overlayPeriodLabel}
usePoints={usePoints}
onToggleUsePoints={() => setUsePoints((prev) => !prev)}
billing={{
@@ -932,6 +932,7 @@ export function AccountCenter() {
payBusy={paymentBusy}
payLabel={hasPayingWallet ? copy.payNow : copy.connectAndPay}
manualPayLabel="手动转账"
locale={locale}
errorText={paymentError || undefined}
infoText={paymentInfo || undefined}
txHash={lastTxHash || undefined}
@@ -1112,13 +1113,6 @@ export function AccountCenter() {
const code = String(plan.plan_code || "");
const active = code === selectedPlanCode;
const isQuarterly = code === "pro_quarterly";
const isPrivateGroupMonthly = Boolean(
code === "pro_monthly" &&
privateTelegramGroupPriceActive &&
privateTelegramGroupAmount &&
Number(plan.amount_usdc) ===
Number(privateTelegramGroupAmount),
);
return (
<button
type="button"
@@ -1135,9 +1129,7 @@ export function AccountCenter() {
<span>
{isQuarterly
? copy.quarterlyPlan
: isPrivateGroupMonthly
? copy.privateGroupMonthlyPlan
: copy.monthlyPlan}
: copy.monthlyPlan}
</span>
<span>{plan.amount_usdc} USDC</span>
</div>
@@ -27,6 +27,10 @@ export function runTests() {
path.join(projectRoot, "components", "account", "useBilling.ts"),
"utf8",
);
const unlockProOverlay = fs.readFileSync(
path.join(projectRoot, "components", "subscription", "UnlockProOverlay.tsx"),
"utf8",
);
const telegramPricing = fs.readFileSync(
path.join(projectRoot, "components", "account", "telegram-pricing.ts"),
"utf8",
@@ -76,20 +80,30 @@ export function runTests() {
!telegramPricing.includes("is_group_member") &&
useAccountPayment.includes('=== "pro_monthly"') &&
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(
useBilling.includes("telegramGroupPriceApplies") &&
useBilling.includes("isTelegramPrivateGroupPriceEligible") &&
useBilling.includes("backend?.telegram_pricing") &&
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(
accountCenter.includes("privateGroupMonthlyPlan") &&
accountCopy.includes("Private group monthly") &&
accountCopy.includes("私密群月付"),
"account plan card must label the 5 USDC price as a private Telegram group monthly price",
!accountCenter.includes(["private", "Group", "Monthly", "Plan"].join("")) &&
!accountCopy.includes(["Private", "group", "monthly"].join(" ")) &&
!accountCopy.includes(["私", "密", "群", "月", "付"].join("")),
"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(
types.includes("ReferralSummary") &&
@@ -63,7 +63,6 @@ export function createAccountCopy(isEn: boolean): Record<string, string> {
paymentMgmt: isEn ? "Payment Management" : "支付管理",
proPlan: isEn ? "Pro Plan" : "Pro 套餐",
monthlyPlan: isEn ? "Monthly" : "月付",
privateGroupMonthlyPlan: isEn ? "Private group monthly" : "私密群月付",
quarterlyPlan: isEn ? "Quarterly" : "季度",
trialBadge: isEn ? "3-day trial" : "3天试用",
trialPaidGroupLocked: isEn
@@ -331,7 +331,7 @@ function TemperatureChartCanvasComponent({
payload={props.payload as ReadonlyArray<{ payload?: Record<string, any> }> | undefined}
data={zoomedData}
series={activeSeries}
probabilityOverlay={null}
probabilityOverlay={probabilityOverlay}
tempSymbol={tempSymbol}
isEn={isEn}
/>
@@ -123,10 +123,31 @@ function buildTooltipProbabilityRows(
tempSymbol: string,
isEn: boolean,
): TooltipProbabilityRow[] {
void probabilityOverlay;
void tempSymbol;
void isEn;
return [];
if (!probabilityOverlay) return [];
const rows: TooltipProbabilityRow[] = [];
const mu = validNumber(probabilityOverlay.muLine?.value);
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;
@@ -1660,7 +1660,7 @@ export function runTests() {
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");
// ── 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(
{
city: "toronto",
@@ -1688,9 +1688,13 @@ export function runTests() {
) as any;
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(
!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 {
__buildTemperatureTooltipProbabilityRowsForTest,
__buildTemperatureTooltipRowsForTest,
@@ -73,7 +77,23 @@ export function runTests() {
);
assert(
probabilityRows.length === 0,
"temperature tooltip should not show Gaussian μ or probability-band rows",
probabilityRows.length === 2,
"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;
});
// Keep probability data in the payload for market views, but do not draw
// legacy Gaussian bands on the live temperature chart.
const probabilityOverlay = null;
// Keep legacy Gaussian data as compact tooltip context. It is not rendered
// as a time-series line on the live temperature chart.
const probabilityOverlay = buildLegacyGaussianProbabilityOverlay(row, hourly);
return { data, series, probabilityOverlay };
}
@@ -645,8 +645,8 @@ function InstitutionalLandingScreen({ locale }: { locale: LandingLocale }) {
</h3>
<p className="mt-3 flex-1 text-sm leading-7 text-slate-600">
{isEn
? "For private groups that need shared access, Telegram workflow support, and manual onboarding."
: "面向需要共享权限、Telegram 工作流支持和人工开通的私密团队。"}
? "For teams that need shared access, Telegram workflow support, and manual onboarding."
: "面向需要共享权限、Telegram 工作流支持和人工开通的团队。"}
</p>
<div className="mt-7 rounded-md border border-slate-200 bg-white px-4 py-3 text-xs font-semibold text-slate-600">
{isEn
@@ -37,6 +37,8 @@ export type UnlockProBilling = {
type UnlockProOverlayProps = {
points: number;
planPriceUsd: number;
planLabel?: string;
periodLabel?: string;
usePoints: boolean;
billing: UnlockProBilling;
onToggleUsePoints: () => void;
@@ -56,6 +58,11 @@ type UnlockProOverlayProps = {
paymentTokenLabel?: string;
};
function formatUsdcAmount(value: number) {
if (!Number.isFinite(value)) return "0";
return value.toFixed(2).replace(/\.?0+$/, "");
}
const FEATURES = {
"zh-CN": [
"市场扫描台 + V4-Pro 深度复核",
@@ -74,6 +81,8 @@ const FEATURES = {
export function UnlockProOverlay({
points,
planPriceUsd,
planLabel,
periodLabel,
usePoints,
billing,
onToggleUsePoints,
@@ -95,6 +104,13 @@ export function UnlockProOverlay({
const isEn = locale === "en-US";
const canUsePoints = billing.pointsEnabled && billing.isEligible;
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 =
payLabel || (isEn ? "Subscribe & Activate" : "立即订阅并激活服务");
@@ -106,6 +122,8 @@ export function UnlockProOverlay({
const redeemableUsdNow = billing.pointsEnabled
? Math.min(maxDiscountUsdInt, Math.floor(points / Math.max(1, billing.pointsPerUsd)))
: 0;
const formattedRedeemableNow = formatUsdcAmount(redeemableUsdNow);
const formattedMaxDiscountInt = formatUsdcAmount(maxDiscountUsdInt);
const progressPct = billing.pointsEnabled
? Math.min(100, Math.round((points / maxPointsForFullDiscount) * 100))
: 0;
@@ -155,7 +173,7 @@ export function UnlockProOverlay({
<div className={s.planCard}>
<span className={s.planChip}>
<Zap size={10} />
Standard Pro
{resolvedPlanLabel}
</span>
<div
@@ -166,8 +184,8 @@ export function UnlockProOverlay({
marginTop: 12,
}}
>
<span className={s.price}>${planPriceUsd.toFixed(2)}</span>
<span className={s.priceSuffix}>/ {isEn ? "mo" : "月"}</span>
<span className={s.price}>{formattedPlanPrice}</span>
<span className={s.priceSuffix}>{currencyLabel} {resolvedPeriodLabel}</span>
</div>
<ul className={s.featureList}>
@@ -217,24 +235,24 @@ export function UnlockProOverlay({
<span
className={`${s.discount} ${usePoints ? s.discountActive : ""}`}
>
-${billing.discountAmount.toFixed(2)}
-{formattedDiscount}
</span>
<span className={s.discountSuffix}>{isEn ? "off" : "减免"}</span>
<span className={s.discountSuffix}>{currencyLabel}</span>
</div>
<p className={s.pointsNote}>
{usePoints
? isEn
? `Using ${billing.pointsUsed} pts · saves $${billing.discountAmount.toFixed(2)}`
: `已消耗 ${billing.pointsUsed} 积分 · 省 $${billing.discountAmount.toFixed(2)}`
? `Using ${billing.pointsUsed} pts · saves ${formattedDiscount} ${currencyLabel}`
: `已消耗 ${billing.pointsUsed} 积分 · 省 ${formattedDiscount} ${currencyLabel}`
: isEn
? `Toggle to save up to $${billing.maxDiscountUsd.toFixed(2)}`
: `开启可最多抵扣 $${billing.maxDiscountUsd.toFixed(2)}`}
? `Toggle to save up to ${formattedMaxDiscount} ${currencyLabel}`
: `开启可最多抵扣 ${formattedMaxDiscount} ${currencyLabel}`}
</p>
<p className={s.pointsNote}>
{isEn
? `Now redeemable: $${redeemableUsdNow.toFixed(0)} / $${maxDiscountUsdInt.toFixed(0)}`
: `当前可抵:${redeemableUsdNow.toFixed(0)}U / ${maxDiscountUsdInt.toFixed(0)}U`}
? `Now redeemable: ${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`
: `当前可抵:${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`}
</p>
<div className={s.pointsBalance}>
@@ -276,8 +294,8 @@ export function UnlockProOverlay({
? "Points redemption is unavailable for this plan."
: "当前套餐暂不支持积分抵扣。"
: isEn
? `Starts at ${billing.pointsPerUsd} pts, ${billing.pointsPerUsd} pts per $1, up to $${maxDiscountUsdInt}. You have: ${points}`
: `${billing.pointsPerUsd} 分起兑,每 ${billing.pointsPerUsd} 分抵 1U,最多抵 ${maxDiscountUsdInt}U。当前 ${points}`}
? `Starts at ${billing.pointsPerUsd} pts, ${billing.pointsPerUsd} pts per 1 ${currencyLabel}, up to ${formattedMaxDiscountInt} ${currencyLabel}. You have: ${points}`
: `${billing.pointsPerUsd} 分起兑,每 ${billing.pointsPerUsd} 分抵 1 ${currencyLabel},最多抵 ${formattedMaxDiscountInt} ${currencyLabel}。当前 ${points}`}
</p>
{billing.pointsEnabled && (
@@ -288,8 +306,8 @@ export function UnlockProOverlay({
</span>
<span>
{isEn
? `$${redeemableUsdNow.toFixed(0)} / $${maxDiscountUsdInt.toFixed(0)}`
: `${redeemableUsdNow.toFixed(0)}U / ${maxDiscountUsdInt.toFixed(0)}U`}
? `${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`
: `${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`}
</span>
</div>
<div className={s.progressTrack}>
@@ -341,15 +359,15 @@ export function UnlockProOverlay({
)}
{billing.discountAmount > 0 && usePoints && (
<p className={s.summaryOriginal}>
${planPriceUsd.toFixed(2)} USD
{formattedPlanPrice} {currencyLabel}
</p>
)}
</div>
<div className={s.summaryAmount}>
<span className={s.summaryPrice}>
${billing.finalPrice.toFixed(2)}
{formattedFinalPrice}
</span>
<span className={s.summaryUnit}>USD</span>
<span className={s.summaryUnit}>{currencyLabel}</span>
</div>
</div>