// pricing.jsx — Pricing page: Subscriptions + Coin packages (dynamic from API) const PLAN_COLORS = { free: '#94a3b8', month: '#22d3ee', year: '#a78bfa', week: '#fbbf24', day: '#4ade80' }; // ─── Plan card (整张卡可点击) ─────────────────────────────────────────────── const PlanCard = ({ plan, isActive, onSelect }) => { const { t } = useI18n(); const color = PLAN_COLORS[plan.type] || '#22d3ee'; const isFree = plan.base_price === 0; const hasPromo = plan.promotion && plan.current_price < plan.base_price; const price = plan.current_price || plan.base_price; const clickable = !isActive && !isFree; const handleClick = () => { if (clickable) onSelect(plan); }; const handleKey = (e) => { if (clickable && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); onSelect(plan); } }; return (
{isActive && (
{t('pricing2.currentPlan')}
)}
{plan.name}
{plan.type === 'month' ? t('pricing2.monthly') : plan.type === 'year' ? t('pricing2.yearly') : plan.type === 'week' ? t('pricing2.weekly') : plan.type === 'day' ? t('pricing2.daily') : ''} {plan.duration_days > 0 ? ` · ${plan.duration_days} ${t('pricing2.days')}` : ''}
{isFree ? (
{t('pricing2.free')}
) : (
${price.toFixed(2)} {hasPromo && ${plan.base_price.toFixed(2)}}
)} {plan.first_purchase_price > 0 && plan.first_purchase_price < plan.base_price && !isActive && (
{t('pricing2.firstPurchase')} ${plan.first_purchase_price.toFixed(2)}
)} {plan.bonus_coins > 0 && (
{t('pricing2.bonus')} {plan.bonus_coins.toLocaleString()} {t('common.credits')}
)} {hasPromo && plan.promotion.name && (
{plan.promotion.name}
)}
); }; // ─── Mobile single-card carousel for plans ────────────────────────────────── const MobilePlanCarousel = ({ plans, activeSubId, onSelect }) => { const { t } = useI18n(); const [idx, setIdx] = React.useState(0); React.useEffect(() => { // 默认选中 popular(month),否则中间一张 const popularIdx = plans.findIndex(p => p.type === 'month'); setIdx(popularIdx >= 0 ? popularIdx : Math.floor(plans.length / 2)); }, [plans.length]); if (!plans.length) return null; const plan = plans[idx]; if (!plan) return null; const isActive = activeSubId === plan.id; const color = PLAN_COLORS[plan.type] || '#22d3ee'; const isPopular = plan.type === 'month'; const price = plan.current_price || plan.base_price; const hasFirstPurchase = plan.first_purchase_price > 0 && plan.first_purchase_price < plan.base_price && !isActive; const periodSuffix = plan.type === 'month' ? '/mo' : plan.type === 'year' ? '/yr' : plan.type === 'week' ? '/wk' : ''; // i18n features payload from backend (gateway_product.features) // Shape: { zh: {description, billed_label, sections: [{title, items:[]}]}, en: {...} } const lang = (typeof window !== 'undefined' && window.i18n && window.i18n.getLang && window.i18n.getLang()) || 'zh'; const fI18n = plan.features || {}; const f = fI18n[lang] || fI18n.en || fI18n.zh || Object.values(fI18n)[0] || {}; const billedText = f.billed_label || (plan.type === 'month' ? 'Billed monthly' : plan.type === 'year' ? 'Billed yearly' : plan.type === 'week' ? 'Billed weekly' : ''); const description = f.description || ''; const sections = Array.isArray(f.sections) ? f.sections : []; // 与月卡比较省多少(仅年卡显示) const monthlyPlan = plans.find(p => p.type === 'month'); let savePct = 0; if (plan.type === 'year' && monthlyPlan && monthlyPlan.base_price > 0) { const yearAsMonthly = plan.base_price / 12; savePct = Math.round((1 - yearAsMonthly / monthlyPlan.base_price) * 100); } // touch swipe const touchStart = React.useRef(null); const onTouchStart = e => { touchStart.current = e.touches[0].clientX; }; const onTouchEnd = e => { if (touchStart.current == null) return; const dx = e.changedTouches[0].clientX - touchStart.current; if (Math.abs(dx) > 50) { if (dx < 0 && idx < plans.length - 1) setIdx(idx + 1); if (dx > 0 && idx > 0) setIdx(idx - 1); } touchStart.current = null; }; const ArrowBtn = ({ dir, disabled }) => ( ); return (
{/* Carousel control */}
{plans.map((_, i) => (
{/* Single plan card */}
{/* Header */}
{plan.name} {isPopular && ( POPULAR )} {isActive && ( {t('pricing2.currentPlan')} )} {savePct > 0 && ( 省 {savePct}% )}
{description && (
{description}
)} {/* Price */}
${price.toFixed(2)} USD{periodSuffix}
{hasFirstPurchase && (
{t('pricing2.firstPurchase')} ${plan.first_purchase_price.toFixed(2)}
)} {billedText && (
{billedText}
)} {/* CTA */} {/* i18n sections from product.features[lang].sections */} {sections.map((sec, si) => (
{sec.title}
{(sec.items || []).map((item, i) => )} ))}
); }; // ─── Feature list item with check icon ────────────────────────────────────── const FeatureItem = ({ text, highlight }) => (
{text}
); // ─── Mobile single-card carousel for coin packages ────────────────────────── const MobileCoinCarousel = ({ packages, onSelect }) => { const { t } = useI18n(); const [idx, setIdx] = React.useState(0); React.useEffect(() => { const popularIdx = packages.findIndex(p => p.tag === 'popular'); setIdx(popularIdx >= 0 ? popularIdx : Math.floor(packages.length / 2)); }, [packages.length]); if (!packages.length) return null; const pkg = packages[idx]; if (!pkg) return null; const hasBonus = pkg.bonus_coins > 0; const totalCoins = pkg.coins + (hasBonus ? pkg.bonus_coins : 0); const perCreditCents = totalCoins > 0 ? (pkg.amount / totalCoins * 100) : 0; const isPopular = pkg.tag === 'popular'; const isRecommended = pkg.tag === 'recommended'; // 与上一档比较省多少 const prevPkg = packages[idx - 1]; let savePct = 0; if (prevPkg && prevPkg.coins > 0 && pkg.coins > 0) { const prevPerCent = prevPkg.amount / (prevPkg.coins + (prevPkg.bonus_coins || 0)); const curPerCent = pkg.amount / totalCoins; if (curPerCent < prevPerCent) { savePct = Math.round((1 - curPerCent / prevPerCent) * 100); } } // touch swipe const touchStart = React.useRef(null); const onTouchStart = e => { touchStart.current = e.touches[0].clientX; }; const onTouchEnd = e => { if (touchStart.current == null) return; const dx = e.changedTouches[0].clientX - touchStart.current; if (Math.abs(dx) > 50) { if (dx < 0 && idx < packages.length - 1) setIdx(idx + 1); if (dx > 0 && idx > 0) setIdx(idx - 1); } touchStart.current = null; }; const ArrowBtn = ({ dir, disabled }) => ( ); return (
{/* Carousel control */}
{packages.map((_, i) => (
{/* Single card */}
{/* Header */}
{totalCoins.toLocaleString()} {t('common.credits')} {isPopular && ( POPULAR )} {isRecommended && ( RECOMMENDED )} {savePct > 0 && ( 更划算 {savePct}% )}
{pkg.name || '一次性充值'}
{/* Price */}
${pkg.amount.toFixed(2)} {pkg.currency || 'USD'}
{totalCoins > 0 ? (
≈ ${perCreditCents.toFixed(1)}¢ / 额度 · 一次性付款
) : (
⚠ 未配置额度数
)} {/* CTA */} {/* CREDITS section */}
CREDITS
{hasBonus && } {/* INCLUDES section */}
INCLUDES
); }; // ─── Coin package card (整张卡可点击) ───────────────────────────────────── const CoinCard = ({ pkg, onSelect }) => { const { t } = useI18n(); const hasBonus = pkg.bonus_coins > 0; const totalCoins = pkg.coins + (hasBonus ? pkg.bonus_coins : 0); const perCreditCents = totalCoins > 0 ? (pkg.amount / totalCoins * 100) : 0; const disabled = totalCoins === 0; const handleClick = () => { if (!disabled) onSelect(pkg); }; const handleKey = (e) => { if (!disabled && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); onSelect(pkg); } }; return (
{pkg.tag && (
{pkg.tag === 'popular' ? t('shared.badge.hot') : pkg.tag === 'recommended' ? t('pricing2.recommended') : pkg.tag.toUpperCase()}
)}
{totalCoins.toLocaleString()} {t('common.credits')}
{hasBonus && (
{t('pricing2.includesBonus')} {pkg.bonus_coins.toLocaleString()}
)}
${pkg.amount.toFixed(2)}
{totalCoins > 0 ? (
≈ ${perCreditCents.toFixed(1)}¢ {t('pricing2.perCredit')}
) : (
⚠ 未配置额度数
)}
); }; // ─── Main screen ────────────────────────────────────────────────────────────── const PricingScreen = () => { const { t } = useI18n(); const { isMobile } = useBreakpoint(); const [tab, setTab] = React.useState('plans'); // 'plans' | 'coins' const [plans, setPlans] = React.useState([]); const [coins, setCoins] = React.useState([]); const [activeSub, setActiveSub] = React.useState(null); const [loading, setLoading] = React.useState(true); React.useEffect(() => { Promise.all([ window.YiteAPI?.subscription?.plans?.() || fetch('/api/v1/subscriptions/plans').then(r => r.json()), window.YiteAPI?.subscription?.coinPackages?.() || fetch('/api/v1/subscriptions/coin-packages').then(r => r.json()), window.YiteAPI?.isLoggedIn?.() ? (window.YiteAPI.subscription.active().catch(() => ({ subscription: null }))) : Promise.resolve({ subscription: null }), ]).then(([planRes, coinRes, subRes]) => { setPlans(planRes.items || []); setCoins(coinRes.items || []); setActiveSub(subRes.subscription || null); setLoading(false); }).catch(() => setLoading(false)); }, []); const handleSelectPlan = plan => { if (!window.YiteAPI?.isLoggedIn?.()) { showLoginModal(); return; } if (plan.base_price === 0) return; window.location.href = '/checkout?plan_id=' + plan.id + '&combo=' + plan.type; }; const handleSelectCoin = pkg => { if (!window.YiteAPI?.isLoggedIn?.()) { showLoginModal(); return; } window.location.href = '/checkout?coin_package_id=' + pkg.id; }; const sortedPlans = [...plans].sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); const sortedCoins = [...coins].sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0)); return (
{/* Header */}

{t('pricing2.chooseYourPlan')}

{t('pricing2.description')}

{/* Tab toggle */}
{[ { id: 'plans', label: t('pricing2.plans') }, { id: 'coins', label: t('pricing2.coins') }, ].map(tb => ( ))}
{loading ? (
) : tab === 'plans' ? ( /* ── Subscription plans ── */
{sortedPlans.length > 0 ? ( isMobile ? ( ) : (
{sortedPlans.map(p => (
))}
) ) : (
{t('pricing2.noPlans')}
{t('pricing2.configPlans')}
)} {/* Active subscription info */} {activeSub && (
{t('pricing2.activeSubscription')}
{t('pricing2.expiryDate')}{new Date(activeSub.expires_at).toLocaleDateString()} · {t('pricing2.paid')}${activeSub.paid_amount} {activeSub.currency}
)}
) : ( /* ── Coin packages ── */
{sortedCoins.length > 0 ? ( isMobile ? ( ) : (
{sortedCoins.map(p => (
))}
) ) : (
{t('pricing2.noCoins')}
{t('pricing2.configCoins')}
)}
)} {/* FAQ / Contact */}

{t('pricing2.customSolution')}

{t('pricing2.enterpriseDesc')}

{t('pricing2.contactUs')}
); }; Object.assign(window, { PricingScreen });