feat: implement AccountCenter dashboard with subscription and payment management features
This commit is contained in:
@@ -337,6 +337,7 @@ export function AccountCenter() {
|
||||
isSubscribed && queuedExtensionDays > 0,
|
||||
);
|
||||
const canAccessPaidTelegramGroup = Boolean(isSubscribed && !isTrialSubscription);
|
||||
const hasTelegramPanel = Boolean(isTrialSubscription || canAccessPaidTelegramGroup);
|
||||
const telegramBound =
|
||||
Number(backend?.telegram_pricing?.telegram_id || 0) > 0;
|
||||
const displayExpiryRaw = isSubscribed
|
||||
@@ -900,9 +901,13 @@ export function AccountCenter() {
|
||||
|
||||
{/* Telegram Bot Section & Payment Details */}
|
||||
{showSecondarySections ? (
|
||||
<div className="lg:col-span-12 grid grid-cols-1 md:flex gap-6">
|
||||
<div
|
||||
className={`lg:col-span-12 grid grid-cols-1 items-start gap-6 ${
|
||||
hasTelegramPanel ? "xl:grid-cols-[minmax(0,0.9fr)_minmax(620px,1.1fr)]" : ""
|
||||
}`}
|
||||
>
|
||||
{isTrialSubscription && (
|
||||
<section className="group relative flex-1 overflow-hidden rounded-2xl border border-amber-200 bg-amber-50 p-8 shadow-sm">
|
||||
<section className="group relative min-w-0 overflow-hidden rounded-2xl border border-amber-200 bg-amber-50 p-8 shadow-sm">
|
||||
<Bot
|
||||
size={140}
|
||||
className="absolute -right-8 -bottom-8 -rotate-12 text-amber-100"
|
||||
@@ -927,7 +932,7 @@ export function AccountCenter() {
|
||||
</section>
|
||||
)}
|
||||
{canAccessPaidTelegramGroup && (
|
||||
<section className="group relative flex-1 overflow-hidden rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<section className="group relative min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<Bot
|
||||
size={140}
|
||||
className="absolute -right-8 -bottom-8 -rotate-12 text-slate-100 transition-transform duration-1000 group-hover:rotate-0"
|
||||
@@ -1005,11 +1010,7 @@ export function AccountCenter() {
|
||||
)}
|
||||
|
||||
{/* Payment Details / Wallet Management */}
|
||||
<section
|
||||
className={`flex flex-col justify-between rounded-2xl border border-slate-200 bg-white p-8 shadow-sm ${
|
||||
canAccessPaidTelegramGroup || isTrialSubscription ? "w-full md:w-96" : "w-full"
|
||||
}`}
|
||||
>
|
||||
<section className="w-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm lg:p-7">
|
||||
<div>
|
||||
<h3 className="mb-6 flex items-center gap-2 text-sm font-bold uppercase text-blue-700">
|
||||
<Wallet size={18} /> {copy.paymentMgmt}
|
||||
@@ -1043,275 +1044,282 @@ export function AccountCenter() {
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mb-5">
|
||||
<p className="mb-2 text-[11px] uppercase text-slate-500">
|
||||
{copy.proPlan}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{displayPlanList.map((plan) => {
|
||||
const code = String(plan.plan_code || "");
|
||||
const active = code === selectedPlanCode;
|
||||
const isQuarterly = code === "pro_quarterly";
|
||||
return (
|
||||
<div
|
||||
data-testid="payment-management-grid"
|
||||
className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(320px,380px)] lg:items-start"
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<p className="mb-2 text-[11px] uppercase text-slate-500">
|
||||
{copy.proPlan}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{displayPlanList.map((plan) => {
|
||||
const code = String(plan.plan_code || "");
|
||||
const active = code === selectedPlanCode;
|
||||
const isQuarterly = code === "pro_quarterly";
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={code}
|
||||
onClick={() => setSelectedPlanCode(code)}
|
||||
disabled={paymentBusy}
|
||||
className={`rounded-xl border px-3 py-3 text-left transition-all ${
|
||||
active
|
||||
? "border-blue-300 bg-blue-50 text-blue-900"
|
||||
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 text-xs font-bold">
|
||||
<span>
|
||||
{isQuarterly ? copy.quarterlyPlan : copy.monthlyPlan}
|
||||
</span>
|
||||
<span>{plan.amount_usdc} USDC</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] opacity-80">
|
||||
{code} · {plan.duration_days} 天
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{appliedReferralCode && selectedPlanCode === "pro_monthly" ? (
|
||||
<p className="mt-2 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-[11px] text-emerald-800">
|
||||
{copy.referralDiscountHint}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<p className="text-xs font-bold uppercase text-slate-700">
|
||||
{copy.referralTitle}
|
||||
</p>
|
||||
<span className="text-[10px] text-slate-500">
|
||||
{copy.referralInviteLimit}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2">
|
||||
<div>
|
||||
<p className="mb-1 text-[10px] uppercase text-slate-500">
|
||||
{copy.referralMyCode}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<code className="min-w-0 flex-1 truncate rounded-xl border border-slate-200 bg-white px-3 py-2 font-mono text-xs text-blue-700">
|
||||
{referralCode || "--"}
|
||||
</code>
|
||||
{referralCode ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopy(referralCode)}
|
||||
className="rounded-xl border border-blue-700 bg-blue-600 px-3 text-xs font-bold text-white hover:bg-blue-700"
|
||||
>
|
||||
{copied ? <CheckCircle2 size={15} /> : <Copy size={15} />}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-2 text-[11px] leading-5 text-slate-500">
|
||||
{copy.referralRewardHint}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-[10px] uppercase text-slate-500">
|
||||
{copy.referralApplyLabel}
|
||||
</p>
|
||||
{appliedReferralCode ? (
|
||||
<div className="rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-semibold text-emerald-800">
|
||||
{appliedReferralCode}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={referralCodeInput}
|
||||
onChange={(event) => setReferralCodeInput(event.target.value)}
|
||||
placeholder={copy.referralApplyPlaceholder}
|
||||
className="min-w-0 flex-1 rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs text-slate-950 outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void applyReferralCode()}
|
||||
disabled={!canApplyReferralCode || referralApplying}
|
||||
className="rounded-xl border border-slate-900 bg-slate-900 px-3 text-xs font-bold text-white hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{referralApplying ? "..." : copy.referralApplyButton}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-2 text-[11px] leading-5 text-slate-500">
|
||||
{copy.referralDiscountHint}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-testid="payment-guard-grid" className="grid gap-3 sm:grid-cols-2">
|
||||
<InfoRow
|
||||
icon={Mail}
|
||||
label={copy.paymentAccount}
|
||||
value={email || "--"}
|
||||
isPrimary
|
||||
/>
|
||||
<InfoRow
|
||||
icon={Wallet}
|
||||
label={copy.paymentWallet}
|
||||
value={shortAddress(paymentWalletLabel) || "--"}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={ShieldCheck}
|
||||
label={copy.paymentReceiver}
|
||||
value={shortAddress(paymentReceiverAddress) || "--"}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={ExternalLink}
|
||||
label={copy.paymentNetwork}
|
||||
value={
|
||||
selectedPaymentChain?.name ||
|
||||
chainIdToDisplayName(selectedPaymentChainId)
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={ExternalLink}
|
||||
label={copy.paymentHost}
|
||||
value={currentPaymentHost || "--"}
|
||||
/>
|
||||
<p className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-[11px] leading-5 text-slate-500 sm:col-span-2">
|
||||
{copy.paymentGuardHint}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-5">
|
||||
{availableChainList.length > 1 && (
|
||||
<div>
|
||||
<p className="mb-2 text-[11px] uppercase text-slate-500">
|
||||
{copy.paymentNetwork}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{availableChainList.map((chain) => {
|
||||
const active = chain.chain_id === selectedPaymentChainId;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={chain.chain_id}
|
||||
onClick={() => {
|
||||
setSelectedPaymentChainId(chain.chain_id);
|
||||
const nextToken =
|
||||
paymentConfig?.tokens?.find(
|
||||
(token) =>
|
||||
Number(token.chain_id || chain.chain_id) ===
|
||||
chain.chain_id &&
|
||||
token.is_default,
|
||||
) ||
|
||||
paymentConfig?.tokens?.find(
|
||||
(token) =>
|
||||
Number(token.chain_id || chain.chain_id) ===
|
||||
chain.chain_id,
|
||||
);
|
||||
if (nextToken?.address) {
|
||||
setSelectedTokenAddress(
|
||||
String(nextToken.address).toLowerCase(),
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={paymentBusy}
|
||||
className={`rounded-xl border px-3 py-2 text-left transition-all ${
|
||||
active
|
||||
? "border-blue-300 bg-blue-50 text-blue-900"
|
||||
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs font-bold">
|
||||
{chain.name || chainIdToDisplayName(chain.chain_id)}
|
||||
</div>
|
||||
<div className="text-[10px] opacity-80">
|
||||
{chain.native_currency_symbol || "ETH"} gas
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{availableTokenList.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-2 text-[11px] uppercase text-slate-500">
|
||||
{copy.paymentToken}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{availableTokenList.map((token) => {
|
||||
const active =
|
||||
token.address ===
|
||||
(resolvedSelectedTokenAddress ||
|
||||
token.address);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={`${token.chain_id || selectedPaymentChainId}:${token.address}`}
|
||||
onClick={() =>
|
||||
setSelectedTokenAddress(
|
||||
token.address,
|
||||
)
|
||||
}
|
||||
disabled={paymentBusy}
|
||||
className={`rounded-xl border px-3 py-2 text-left transition-all ${
|
||||
active
|
||||
? "border-blue-300 bg-blue-50 text-blue-900"
|
||||
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs font-bold">
|
||||
{token.symbol}
|
||||
</div>
|
||||
<div className="truncate text-[10px] opacity-80">
|
||||
{token.name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Method Tabs */}
|
||||
<div className="border-t border-slate-200 pt-5">
|
||||
<p className="mb-3 text-[10px] font-semibold uppercase text-slate-500">
|
||||
{copy.paymentMethodLabel}
|
||||
</p>
|
||||
<div className="mb-5 grid grid-cols-2 gap-2 rounded-xl border border-slate-200 bg-slate-100 p-1">
|
||||
<button
|
||||
type="button"
|
||||
key={code}
|
||||
onClick={() => setSelectedPlanCode(code)}
|
||||
disabled={paymentBusy}
|
||||
className={`rounded-xl border px-3 py-3 text-left transition-all ${
|
||||
active
|
||||
? "border-blue-300 bg-blue-50 text-blue-900"
|
||||
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
|
||||
onClick={() => setPaymentMethodTab("wallet")}
|
||||
className={`py-2 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-1.5 ${
|
||||
paymentMethodTab === "wallet"
|
||||
? "bg-white text-blue-700 shadow-sm"
|
||||
: "text-slate-500 hover:bg-white/70 hover:text-slate-900"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 text-xs font-bold">
|
||||
<span>
|
||||
{isQuarterly ? copy.quarterlyPlan : copy.monthlyPlan}
|
||||
</span>
|
||||
<span>{plan.amount_usdc} USDC</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] opacity-80">
|
||||
{code} · {plan.duration_days} 天
|
||||
</div>
|
||||
<Wallet size={12} />
|
||||
{copy.paymentMethodWallet}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPaymentMethodTab("manual")}
|
||||
className={`py-2 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-1.5 ${
|
||||
paymentMethodTab === "manual"
|
||||
? "bg-white text-emerald-700 shadow-sm"
|
||||
: "text-slate-500 hover:bg-white/70 hover:text-slate-900"
|
||||
}`}
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
{copy.paymentMethodManual}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{appliedReferralCode && selectedPlanCode === "pro_monthly" ? (
|
||||
<p className="mt-2 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-[11px] text-emerald-800">
|
||||
{copy.referralDiscountHint}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-5 rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<p className="text-xs font-bold uppercase text-slate-700">
|
||||
{copy.referralTitle}
|
||||
</p>
|
||||
<span className="text-[10px] text-slate-500">
|
||||
{copy.referralInviteLimit}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-3 lg:grid-cols-2">
|
||||
<div>
|
||||
<p className="mb-1 text-[10px] uppercase text-slate-500">
|
||||
{copy.referralMyCode}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<code className="min-w-0 flex-1 truncate rounded-xl border border-slate-200 bg-white px-3 py-2 font-mono text-xs text-blue-700">
|
||||
{referralCode || "--"}
|
||||
</code>
|
||||
{referralCode ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopy(referralCode)}
|
||||
className="rounded-xl border border-blue-700 bg-blue-600 px-3 text-xs font-bold text-white hover:bg-blue-700"
|
||||
>
|
||||
{copied ? <CheckCircle2 size={15} /> : <Copy size={15} />}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-2 text-[11px] leading-5 text-slate-500">
|
||||
{copy.referralRewardHint}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-[10px] uppercase text-slate-500">
|
||||
{copy.referralApplyLabel}
|
||||
</p>
|
||||
{appliedReferralCode ? (
|
||||
<div className="rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-semibold text-emerald-800">
|
||||
{appliedReferralCode}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={referralCodeInput}
|
||||
onChange={(event) => setReferralCodeInput(event.target.value)}
|
||||
placeholder={copy.referralApplyPlaceholder}
|
||||
className="min-w-0 flex-1 rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs text-slate-950 outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void applyReferralCode()}
|
||||
disabled={!canApplyReferralCode || referralApplying}
|
||||
className="rounded-xl border border-slate-900 bg-slate-900 px-3 text-xs font-bold text-white hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{referralApplying ? "..." : copy.referralApplyButton}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-2 text-[11px] leading-5 text-slate-500">
|
||||
{copy.referralDiscountHint}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5 space-y-3">
|
||||
<InfoRow
|
||||
icon={Mail}
|
||||
label={copy.paymentAccount}
|
||||
value={email || "--"}
|
||||
isPrimary
|
||||
/>
|
||||
<InfoRow
|
||||
icon={Wallet}
|
||||
label={copy.paymentWallet}
|
||||
value={shortAddress(paymentWalletLabel) || "--"}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={ShieldCheck}
|
||||
label={copy.paymentReceiver}
|
||||
value={shortAddress(paymentReceiverAddress) || "--"}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={ExternalLink}
|
||||
label={copy.paymentNetwork}
|
||||
value={
|
||||
selectedPaymentChain?.name ||
|
||||
chainIdToDisplayName(selectedPaymentChainId)
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={ExternalLink}
|
||||
label={copy.paymentHost}
|
||||
value={currentPaymentHost || "--"}
|
||||
/>
|
||||
<p className="text-[11px] text-slate-500">
|
||||
{copy.paymentGuardHint}
|
||||
</p>
|
||||
</div>
|
||||
{availableChainList.length > 1 && (
|
||||
<div className="mb-5">
|
||||
<p className="mb-2 text-[11px] uppercase text-slate-500">
|
||||
{copy.paymentNetwork}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{availableChainList.map((chain) => {
|
||||
const active = chain.chain_id === selectedPaymentChainId;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={chain.chain_id}
|
||||
onClick={() => {
|
||||
setSelectedPaymentChainId(chain.chain_id);
|
||||
const nextToken =
|
||||
paymentConfig?.tokens?.find(
|
||||
(token) =>
|
||||
Number(token.chain_id || chain.chain_id) ===
|
||||
chain.chain_id &&
|
||||
token.is_default,
|
||||
) ||
|
||||
paymentConfig?.tokens?.find(
|
||||
(token) =>
|
||||
Number(token.chain_id || chain.chain_id) ===
|
||||
chain.chain_id,
|
||||
);
|
||||
if (nextToken?.address) {
|
||||
setSelectedTokenAddress(
|
||||
String(nextToken.address).toLowerCase(),
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={paymentBusy}
|
||||
className={`rounded-xl border px-3 py-2 text-left transition-all ${
|
||||
active
|
||||
? "border-blue-300 bg-blue-50 text-blue-900"
|
||||
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs font-bold">
|
||||
{chain.name || chainIdToDisplayName(chain.chain_id)}
|
||||
</div>
|
||||
<div className="text-[10px] opacity-80">
|
||||
{chain.native_currency_symbol || "ETH"} gas
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{availableTokenList.length > 0 && (
|
||||
<div className="mb-5">
|
||||
<p className="mb-2 text-[11px] uppercase text-slate-500">
|
||||
{copy.paymentToken}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{availableTokenList.map((token) => {
|
||||
const active =
|
||||
token.address ===
|
||||
(resolvedSelectedTokenAddress ||
|
||||
token.address);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={`${token.chain_id || selectedPaymentChainId}:${token.address}`}
|
||||
onClick={() =>
|
||||
setSelectedTokenAddress(
|
||||
token.address,
|
||||
)
|
||||
}
|
||||
disabled={paymentBusy}
|
||||
className={`rounded-xl border px-3 py-2 text-left transition-all ${
|
||||
active
|
||||
? "border-blue-300 bg-blue-50 text-blue-900"
|
||||
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs font-bold">
|
||||
{token.symbol}
|
||||
</div>
|
||||
<div className="text-[10px] opacity-80 truncate">
|
||||
{token.name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Method Tabs */}
|
||||
<div className="mt-6 border-t border-slate-200 pt-6">
|
||||
<p className="mb-3 text-[10px] font-semibold uppercase text-slate-500">
|
||||
{copy.paymentMethodLabel}
|
||||
</p>
|
||||
<div className="mb-5 grid grid-cols-2 gap-2 rounded-xl border border-slate-200 bg-slate-100 p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPaymentMethodTab("wallet")}
|
||||
className={`py-2 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-1.5 ${
|
||||
paymentMethodTab === "wallet"
|
||||
? "bg-white text-blue-700 shadow-sm"
|
||||
: "text-slate-500 hover:bg-white/70 hover:text-slate-900"
|
||||
}`}
|
||||
>
|
||||
<Wallet size={12} />
|
||||
{copy.paymentMethodWallet}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPaymentMethodTab("manual")}
|
||||
className={`py-2 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-1.5 ${
|
||||
paymentMethodTab === "manual"
|
||||
? "bg-white text-emerald-700 shadow-sm"
|
||||
: "text-slate-500 hover:bg-white/70 hover:text-slate-900"
|
||||
}`}
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
{copy.paymentMethodManual}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{paymentMethodTab === "wallet" ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-[11px] leading-relaxed text-slate-400">
|
||||
{copy.paymentWalletDesc}
|
||||
</p>
|
||||
<div className="rounded-xl border border-amber-200 bg-amber-50 p-3 text-[11px] leading-relaxed text-amber-800">
|
||||
{copy.paymentGasWarning}
|
||||
</div>
|
||||
{paymentMethodTab === "wallet" ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-[11px] leading-relaxed text-slate-400">
|
||||
{copy.paymentWalletDesc}
|
||||
</p>
|
||||
<div className="rounded-xl border border-amber-200 bg-amber-50 p-3 text-[11px] leading-relaxed text-amber-800">
|
||||
{copy.paymentGasWarning}
|
||||
</div>
|
||||
|
||||
{boundWallets.length ? (
|
||||
<div className="space-y-3">
|
||||
@@ -1574,6 +1582,8 @@ export function AccountCenter() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -173,6 +173,21 @@ export function runTests() {
|
||||
accountFeatureSource.includes("只有 USDC 可能无法完成授权或支付"),
|
||||
"payment wallet tab must warn users that Polygon POL gas is required in addition to USDC",
|
||||
);
|
||||
assert(
|
||||
!accountCenterSource.includes("md:w-96"),
|
||||
"account payment management must not use a narrow fixed sidebar that creates an overlong column",
|
||||
);
|
||||
assert(
|
||||
accountCenterSource.includes("items-start") &&
|
||||
accountCenterSource.includes("xl:grid-cols-[minmax(0,0.9fr)_minmax(620px,1.1fr)]"),
|
||||
"account secondary sections must align cards to the top and give payment management a wider responsive column",
|
||||
);
|
||||
assert(
|
||||
accountCenterSource.includes("data-testid=\"payment-management-grid\"") &&
|
||||
accountCenterSource.includes("lg:grid-cols-[minmax(0,1fr)_minmax(320px,380px)]") &&
|
||||
accountCenterSource.includes("data-testid=\"payment-guard-grid\""),
|
||||
"payment management must split plans/referrals from payment controls and compact guard details into grids",
|
||||
);
|
||||
assert(
|
||||
!appAnalyticsSource.includes('NEXT_PUBLIC_POLYWEATHER_APP_ANALYTICS === "true"') &&
|
||||
!analyticsRouteSource.includes('NEXT_PUBLIC_POLYWEATHER_APP_ANALYTICS === "true"'),
|
||||
|
||||
Reference in New Issue
Block a user