Refine pricing display and Gaussian context
This commit is contained in:
@@ -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;
|
||||
|
||||
+7
-3
@@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
+22
-2
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user