diff --git a/frontend/components/auth/LoginClient.tsx b/frontend/components/auth/LoginClient.tsx index f7e8920d..9b47cb76 100644 --- a/frontend/components/auth/LoginClient.tsx +++ b/frontend/components/auth/LoginClient.tsx @@ -239,8 +239,14 @@ export function LoginClient({ nextPath, initialMode }: LoginClientProps) { return (
{/* Left Dark Column */} -
-
+
+ {/* Ambient Glows */} +
+
+ {/* Grid overlay */} +
+ +
PolyWeather @@ -269,31 +275,48 @@ export function LoginClient({ nextPath, initialMode }: LoginClientProps) {
-
-
-

- {copy.trusted} -

-
- {[ - "Atmos Capital", - "Celerity Risk", - "Metis Trading", - "Aviation Hedge", - ].map((name) => ( -
- {name} -
- ))} + {/* Sleek Terminal Preview Widget */} +
+
+
+ + + {isEn ? "Runway 02L Settlement" : "跑道 02L 官方结算"} + +
+ LIVE +
+ +
+
+ {isEn ? "Current Temp:" : "当前气温:"} + 28.8°C +
+
+ {isEn ? "UMA Threshold:" : "UMA 结算阈值:"} + 30.0°C +
+
+ {isEn ? "Model Probability:" : "模型预测概率:"} + 88.5% +
+
+ {isEn ? "Market Price:" : "市场买卖价差:"} + $10.00
-
- ⭐⭐⭐⭐⭐ - 4.9 out of 5 stars (G2 rating) + {/* Sparkline visualization */} +
+
+ {[30, 45, 38, 52, 68, 85, 78, 92, 88].map((h, i) => ( +
+ ))} +
diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 4e25a356..19e52035 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -936,47 +936,6 @@ function ScanTerminalScreen() { ); const generatedText = useRelativeTime(terminalData?.generated_at ?? null); - const refreshAuth = useCallback(() => { - fetch("/api/auth/me", { - cache: "no-store", - headers: { Accept: "application/json" }, - }) - .then(async (response) => { - if (!response.ok) throw new Error(`HTTP ${response.status}`); - return response.json() as Promise<{ - authenticated?: boolean; - user_id?: string | null; - subscription_active?: boolean | null; - subscription_plan_code?: string | null; - subscription_expires_at?: string | null; - subscription_total_expires_at?: string | null; - subscription_queued_days?: number | null; - points?: number | null; - }>; - }) - .then((payload) => { - setProAccess({ - loading: false, - authenticated: Boolean(payload.authenticated), - userId: payload.user_id ?? null, - subscriptionActive: payload.subscription_active === true, - subscriptionPlanCode: payload.subscription_plan_code ?? null, - subscriptionExpiresAt: payload.subscription_expires_at ?? null, - subscriptionTotalExpiresAt: - payload.subscription_total_expires_at ?? - payload.subscription_expires_at ?? - null, - subscriptionQueuedDays: Math.max( - 0, - Number(payload.subscription_queued_days ?? 0), - ), - points: Number(payload.points ?? 0), - error: null, - }); - }) - .catch(() => {}); // keep current state on error - }, []); - if (!hydrated || (proAccess.loading && !canUseLocalFullAccess)) { return ( ); diff --git a/frontend/components/dashboard/scan-terminal/ProductAccessRequired.tsx b/frontend/components/dashboard/scan-terminal/ProductAccessRequired.tsx index fa2546a5..454be7f3 100644 --- a/frontend/components/dashboard/scan-terminal/ProductAccessRequired.tsx +++ b/frontend/components/dashboard/scan-terminal/ProductAccessRequired.tsx @@ -1,10 +1,7 @@ "use client"; -import { useState } from "react"; import Link from "next/link"; import { LockKeyhole, CreditCard, LogIn } from "lucide-react"; -import { UnlockProOverlay } from "@/components/subscription/UnlockProOverlay"; -import { useTerminalPay } from "@/components/subscription/useTerminalPay"; const ACCESS_TERM = { signInToContinue: { en: "Sign in to continue", zh: "请先登录" }, @@ -19,42 +16,7 @@ function t(key: keyof typeof ACCESS_TERM, isEn: boolean) { } // ─── Layer 2: Authenticated but no active subscription ─────────────────────── -function SubscriptionGate({ - isEn, - points, - onPaid, -}: { - isEn: boolean; - points: number; - onPaid: () => void; -}) { - const [showOverlay, setShowOverlay] = useState(false); - const { - billing, - usePoints, - setUsePoints, - payBusy, - errorText, - infoText, - txHash, - chainId, - tokenSymbol, - walletAddress, - handlePay, - } = useTerminalPay({ - points, - planPriceUsd: 10, - onPaid: () => { - setShowOverlay(false); - onPaid(); - }, - isEn, - }); - - const handleSubscribeClick = () => { - setShowOverlay(true); - }; - +function SubscriptionGate({ isEn }: { isEn: boolean }) { const features = isEn ? [ "Real-time METAR observations across 500+ stations", @@ -72,7 +34,6 @@ function SubscriptionGate({ return (
- {/* Header badge */}
@@ -80,9 +41,7 @@ function SubscriptionGate({
- {/* Main card */}
- {/* Card top band */}

{isEn @@ -97,7 +56,6 @@ function SubscriptionGate({

- {/* Price */}
$10 @@ -105,7 +63,6 @@ function SubscriptionGate({
- {/* Feature list */}
    {features.map((f) => (
  • @@ -119,15 +76,13 @@ function SubscriptionGate({ ))}
- {/* CTA */} - +

{isEn @@ -137,7 +92,6 @@ function SubscriptionGate({

- {/* Back link */}
- - {/* Payment overlay */} - {showOverlay && ( -
- setUsePoints((prev) => !prev)} - onPay={() => void handlePay()} - onClose={() => setShowOverlay(false)} - payBusy={payBusy} - payLabel={ - walletAddress - ? isEn - ? "Subscribe & Activate" - : "立即订阅并激活服务" - : isEn - ? "Connect Wallet & Subscribe" - : "连接钱包并订阅" - } - errorText={errorText || undefined} - infoText={infoText || undefined} - txHash={txHash || undefined} - chainId={chainId} - paymentTokenLabel={tokenSymbol} - locale={isEn ? "en-US" : "zh-CN"} - faqHref="/subscription-help" - /> -
- )}
); } @@ -241,18 +163,13 @@ function UnauthenticatedGate({ ); } -/** Unified access gate — routes to the correct layer based on auth state */ export function ProductAccessRequired({ isAuthenticated, isEn, - points, - onPaid, userLocalTime, }: { isAuthenticated: boolean; isEn: boolean; - points: number; - onPaid: () => void; userLocalTime: string; }) { if (!isAuthenticated) { @@ -269,7 +186,7 @@ export function ProductAccessRequired({
{userLocalTime}
- +
); diff --git a/frontend/components/subscription/useTerminalPay.ts b/frontend/components/subscription/useTerminalPay.ts deleted file mode 100644 index f0452cce..00000000 --- a/frontend/components/subscription/useTerminalPay.ts +++ /dev/null @@ -1,418 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useMemo, useState } from "react"; -import type { UnlockProBilling } from "@/components/subscription/UnlockProOverlay"; -import { computeBilling } from "@/lib/billing-utils"; -import { - WALLETCONNECT_POLYGON_RPC_URL, - WALLET_TRANSACTION_REQUEST_TIMEOUT_MS, -} from "@/components/account/constants"; -import { - buildAllowanceCalldata, - buildApproveCalldata, - buildBalanceOfCalldata, - requestWalletWithTimeout, -} from "@/components/account/payment-utils"; -import { isPaymentHostAllowed, getCurrentPaymentHost } from "@/lib/payment-host"; -import { - getEvmProvider, - getEvmWalletLabel, - getWalletConnectProvider, -} from "@/components/account/wallet"; -import { getSupabaseBrowserClient } from "@/lib/supabase/client"; -import type { EvmProvider, CreatedIntent } from "@/components/account/types"; - -const DEFAULT_CHAIN_ID = 137; -const INTENT_POLL_INTERVAL_MS = 5000; -const INTENT_POLL_TIMEOUT_MS = 180_000; - -function normalizePaymentError(err: unknown, fallback: string): string { - if (!err) return fallback; - const e = err as Record; - return String( - e.shortMessage || e.message || e.reason || e.error || fallback, - ); -} - -export function useTerminalPay(params: { - points: number; - planPriceUsd: number; - onPaid: () => void; - isEn: boolean; -}) { - const { points, planPriceUsd, onPaid, isEn } = params; - - const [paymentConfig, setPaymentConfig] = useState | null>(null); - const [walletAddress, setWalletAddress] = useState(null); - const [usePoints, setUsePoints] = useState(true); - const [payBusy, setPayBusy] = useState(false); - const [errorText, setErrorText] = useState(""); - const [infoText, setInfoText] = useState(""); - const [txHash, setTxHash] = useState(""); - - useEffect(() => { - let cancelled = false; - fetch("/api/payments/config", { - cache: "no-store", - headers: { Accept: "application/json" }, - }) - .then(async (r) => { - if (!r.ok) return null; - return r.json(); - }) - .then((cfg) => { - if (cancelled) return; - setPaymentConfig(cfg || null); - }) - .catch(() => {}); - return () => { cancelled = true; }; - }, []); - - const billing: UnlockProBilling = useMemo(() => { - const raw = computeBilling({ - planPriceUsd, - totalPoints: points, - usePoints, - redemptionCfg: (paymentConfig?.points_redemption as Record) ?? null, - }); - return { - pointsEnabled: raw.pointsEnabled, - isEligible: raw.canRedeem, - pointsUsed: raw.pointsUsed, - discountAmount: raw.discountAmount, - finalPrice: raw.payAmount, - maxDiscountUsd: raw.maxDiscountUsdc, - pointsPerUsd: raw.pointsPerUsdc, - }; - }, [planPriceUsd, points, usePoints, paymentConfig]); - - const chainId = useMemo( - () => Number(paymentConfig?.chain_id || DEFAULT_CHAIN_ID), - [paymentConfig], - ); - - const tokenAddress = useMemo(() => { - const tokens = (paymentConfig?.tokens as Array>) || []; - return String(tokens[0]?.address || ""); - }, [paymentConfig]); - - const tokenSymbol = useMemo(() => { - const tokens = (paymentConfig?.tokens as Array>) || []; - return String(tokens[0]?.symbol || "USDC"); - }, [paymentConfig]); - - const tokenDecimals = useMemo(() => { - const tokens = (paymentConfig?.tokens as Array>) || []; - return Number(tokens[0]?.decimals || 6); - }, [paymentConfig]); - - const receiverAddress = useMemo( - () => String(paymentConfig?.receiver_address || ""), - [paymentConfig], - ); - - const connectWallet = useCallback(async (): Promise => { - try { - let provider: EvmProvider | null = getEvmProvider(); - let label = getEvmWalletLabel(provider); - - if (!provider) { - const wcId = String( - process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "", - ).trim(); - if (!wcId) { - setErrorText( - isEn - ? "No wallet detected. Install MetaMask or configure WalletConnect." - : "未检测到钱包插件,请安装 MetaMask 扩展。", - ); - return null; - } - provider = await getWalletConnectProvider( - chainId, - WALLETCONNECT_POLYGON_RPC_URL, - ); - label = "WalletConnect"; - const existing = (await provider - .request({ method: "eth_accounts" }) - .catch(() => [])) as string[]; - if (!Array.isArray(existing) || existing.length === 0) { - if (typeof provider.connect === "function") { - await provider.connect({ chains: [chainId] }); - } - } - } - - const accounts = (await provider.request({ - method: "eth_requestAccounts", - })) as string[]; - const address = String(accounts[0] || "").toLowerCase(); - if (!address) throw new Error("No account returned"); - - setWalletAddress(address); - return address; - } catch (err) { - setErrorText(normalizePaymentError(err, isEn ? "Wallet connection failed" : "钱包连接失败")); - return null; - } - }, [chainId, isEn]); - - const handlePay = useCallback(async () => { - if (payBusy) return; - setPayBusy(true); - setErrorText(""); - setInfoText(""); - - try { - const host = getCurrentPaymentHost(); - const allowed = await isPaymentHostAllowed(host); - if (!allowed) throw new Error(isEn ? "Payment host not allowed" : "当前域名不在支付白名单中"); - - // Get session token - const supabase = getSupabaseBrowserClient(); - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData?.session?.access_token; - if (!token) throw new Error(isEn ? "Session expired, please refresh" : "会话过期,请刷新页面"); - const authHeaders = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; - - // Connect wallet - let payingWallet = walletAddress; - if (!payingWallet) { - payingWallet = await connectWallet(); - if (!payingWallet) { - setPayBusy(false); - return; - } - } - - // Refresh payment config - const cfgRes = await fetch("/api/payments/config", { - headers: { Accept: "application/json" }, - }); - const latestCfg = cfgRes.ok ? await cfgRes.json() : paymentConfig; - setPaymentConfig(latestCfg || paymentConfig); - - const targetChainId = Number(latestCfg?.chain_id || DEFAULT_CHAIN_ID); - - // Resolve provider for payment - let eth = getEvmProvider(); - if (!eth) { - const wcId = String(process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "").trim(); - if (!wcId) throw new Error("No wallet available"); - eth = await getWalletConnectProvider(targetChainId, WALLETCONNECT_POLYGON_RPC_URL); - const existing = (await eth.request({ method: "eth_accounts" }).catch(() => [])) as string[]; - if (!Array.isArray(existing) || existing.length === 0) { - if (typeof eth.connect === "function") { - await eth.connect({ chains: [targetChainId] }); - } - } - } - - // Ensure target chain - const currentChainHex = String( - (await requestWalletWithTimeout(eth, { method: "eth_chainId" }, "chainId", WALLET_TRANSACTION_REQUEST_TIMEOUT_MS)) || "", - ); - const targetHex = `0x${targetChainId.toString(16)}`; - if (currentChainHex.toLowerCase() !== targetHex.toLowerCase()) { - try { - await requestWalletWithTimeout(eth, { - method: "wallet_switchEthereumChain", - params: [{ chainId: targetHex }], - }, "switchChain"); - } catch (switchErr: unknown) { - const code = Number((switchErr as Record)?.code); - if (code === 4902 || targetChainId === 137) { - await requestWalletWithTimeout(eth, { - method: "wallet_addEthereumChain", - params: [{ - chainId: "0x89", - chainName: "Polygon Mainnet", - nativeCurrency: { name: "POL", symbol: "POL", decimals: 18 }, - rpcUrls: ["https://polygon-rpc.com"], - blockExplorerUrls: ["https://polygonscan.com"], - }], - }, "addChain"); - } else { - throw switchErr; - } - } - } - - // Create payment intent - const resolvedToken = String(latestCfg?.tokens?.[0]?.address || tokenAddress); - const intentRes = await fetch("/api/payments/intents", { - method: "POST", - headers: authHeaders, - body: JSON.stringify({ - plan_code: "pro_monthly", - payment_mode: "strict", - allowed_wallet: payingWallet, - token_address: resolvedToken || undefined, - use_points: billing.isEligible && usePoints, - points_to_consume: billing.isEligible && usePoints ? billing.pointsUsed : 0, - metadata: { - source: "terminal", - frontend_host: host || null, - }, - }), - }); - if (!intentRes.ok) { - const errBody = await intentRes.json().catch(() => ({})); - throw new Error(String(errBody?.detail || errBody?.message || `HTTP ${intentRes.status}`)); - } - const created = (await intentRes.json()) as CreatedIntent; - const intentId = String(created.intent?.intent_id || ""); - const txPayload = created.tx_payload; - if (!intentId || !txPayload?.to || !txPayload?.data) { - throw new Error(isEn ? "Invalid payment intent response" : "支付意图创建失败"); - } - - // Verify receiver - const expectedReceiver = String(latestCfg?.receiver_address || receiverAddress); - if (expectedReceiver && txPayload.to.toLowerCase() !== expectedReceiver.toLowerCase()) { - throw new Error(isEn ? "Payment receiver mismatch" : "收款地址不匹配"); - } - - const resolvedTokenDecimals = Number( - latestCfg?.tokens?.[0]?.decimals || tokenDecimals, - ); - const amountUnits = - typeof txPayload.amount_units === "string" - ? BigInt(txPayload.amount_units) - : BigInt(String(txPayload.value || 0)); - - // Check balance - const balanceHex = await requestWalletWithTimeout( - eth, - { - method: "eth_call", - params: [ - { to: txPayload.token_address || resolvedToken, data: buildBalanceOfCalldata(payingWallet) }, - "latest", - ], - }, - "balanceOf", - ); - const balance = BigInt(balanceHex || "0x0"); - if (balance < amountUnits) { - throw new Error(isEn ? "Insufficient USDC balance" : "USDC 余额不足"); - } - - // Check allowance and approve if needed - const allowanceHex = await requestWalletWithTimeout( - eth, - { - method: "eth_call", - params: [ - { to: txPayload.token_address || resolvedToken, data: buildAllowanceCalldata(payingWallet, txPayload.to) }, - "latest", - ], - }, - "allowance", - ); - const allowance = BigInt(allowanceHex || "0x0"); - if (allowance < amountUnits) { - setInfoText(isEn ? "Approving USDC..." : "正在授权 USDC..."); - const approveHash = await requestWalletWithTimeout( - eth, - { - method: "eth_sendTransaction", - params: [{ - from: payingWallet, - to: txPayload.token_address || resolvedToken, - data: buildApproveCalldata(txPayload.to, amountUnits), - }], - }, - "approve", - ); - // Wait for approval receipt via simple polling - const approveStart = Date.now(); - while (Date.now() - approveStart < 120_000) { - const receipt = await requestWalletWithTimeout | null>( - eth, - { method: "eth_getTransactionReceipt", params: [approveHash] }, - "approveReceipt", - ).catch(() => null); - if (receipt?.status === "0x1" || receipt?.status === 1) break; - if (receipt?.status === "0x0" || receipt?.status === 0) { - throw new Error(isEn ? "USDC approval failed" : "USDC 授权失败"); - } - await new Promise((r) => setTimeout(r, 3000)); - } - } - - // Send payment transaction - setInfoText(isEn ? "Confirm in wallet..." : "请在钱包中确认交易..."); - const payHash = await requestWalletWithTimeout( - eth, - { - method: "eth_sendTransaction", - params: [{ from: payingWallet, to: txPayload.to, data: txPayload.data }], - }, - "pay", - ); - setTxHash(payHash); - - // Submit - const submitRes = await fetch(`/api/payments/intents/${intentId}/submit`, { - method: "POST", - headers: authHeaders, - body: JSON.stringify({ tx_hash: payHash, from_address: payingWallet }), - }); - if (!submitRes.ok && submitRes.status !== 409) { - const errBody = await submitRes.json().catch(() => ({})); - throw new Error(String(errBody?.detail || `HTTP ${submitRes.status}`)); - } - - // Confirm - const confirmStart = Date.now(); - let confirmed = false; - while (Date.now() - confirmStart < INTENT_POLL_TIMEOUT_MS) { - const confirmRes = await fetch(`/api/payments/intents/${intentId}/confirm`, { - method: "POST", - headers: authHeaders, - body: JSON.stringify({ tx_hash: payHash }), - }); - if (confirmRes.ok) { - setInfoText(isEn ? "Payment confirmed! Activating..." : "支付确认!正在激活..."); - confirmed = true; - break; - } - if (confirmRes.status === 409 || confirmRes.status === 404 || confirmRes.status === 408) { - await new Promise((r) => setTimeout(r, INTENT_POLL_INTERVAL_MS)); - continue; - } - const errBody = await confirmRes.json().catch(() => ({})); - throw new Error(String(errBody?.detail || `HTTP ${confirmRes.status}`)); - } - if (!confirmed) { - throw new Error(isEn ? "Payment confirmation timeout" : "支付确认超时,请稍后检查订单状态"); - } - - setInfoText(isEn ? "Subscription activated!" : "订阅已激活!"); - onPaid(); - } catch (err) { - setErrorText(normalizePaymentError(err, isEn ? "Payment failed" : "支付失败")); - } finally { - setPayBusy(false); - } - }, [ - payBusy, walletAddress, paymentConfig, billing, usePoints, chainId, - tokenAddress, tokenDecimals, receiverAddress, isEn, connectWallet, onPaid, - ]); - - return { - billing, - usePoints, - setUsePoints, - payBusy, - errorText, - infoText, - txHash, - chainId, - tokenSymbol, - walletAddress, - connectWallet, - handlePay, - }; -} diff --git a/frontend/lib/billing-utils.ts b/frontend/lib/billing-utils.ts deleted file mode 100644 index afc1e3a3..00000000 --- a/frontend/lib/billing-utils.ts +++ /dev/null @@ -1,69 +0,0 @@ -export interface TerminalBilling { - planAmount: number; - pointsEnabled: boolean; - pointsPerUsdc: number; - maxDiscountUsdc: number; - pointsUsed: number; - discountAmount: number; - payAmount: number; - canRedeem: boolean; -} - -export interface PointsRedemptionConfig { - enabled?: boolean; - points_per_usdc?: number; - max_discount_usdc?: number; -} - -export function computeBilling(params: { - planPriceUsd: number; - totalPoints: number; - usePoints: boolean; - redemptionCfg?: PointsRedemptionConfig | null; -}): TerminalBilling { - const planAmount = - Number.isFinite(params.planPriceUsd) && params.planPriceUsd > 0 - ? params.planPriceUsd - : 10; - - const cfg = params.redemptionCfg || {}; - const pointsEnabled = cfg.enabled !== false; - const pointsPerUsdcRaw = Number(cfg.points_per_usdc ?? 500); - const pointsPerUsdc = - Number.isFinite(pointsPerUsdcRaw) && pointsPerUsdcRaw > 0 - ? Math.floor(pointsPerUsdcRaw) - : 500; - - const maxDiscountRaw = Number(cfg.max_discount_usdc ?? 3); - const maxDiscountUsdc = Math.max( - 0, - Math.min( - Math.floor(Number.isFinite(maxDiscountRaw) ? maxDiscountRaw : 3), - Math.floor(planAmount), - ), - ); - - const maxRedeemablePoints = pointsPerUsdc * maxDiscountUsdc; - const actualRedeem = pointsEnabled - ? Math.min(params.totalPoints, maxRedeemablePoints) - : 0; - const discountUnits = Math.floor(actualRedeem / pointsPerUsdc); - const pointsUsed = discountUnits * pointsPerUsdc; - const canRedeem = - pointsEnabled && - maxDiscountUsdc > 0 && - params.totalPoints >= pointsPerUsdc; - const applyDiscount = - params.usePoints && canRedeem && pointsUsed > 0; - - return { - planAmount, - pointsEnabled, - pointsPerUsdc, - maxDiscountUsdc, - pointsUsed, - discountAmount: discountUnits, - payAmount: planAmount - (applyDiscount ? discountUnits : 0), - canRedeem, - }; -}