2026-03-13 02:23:01 +08:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
2026-05-18 17:10:44 +08:00
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
2026-03-13 02:23:01 +08:00
|
|
|
|
import Link from "next/link";
|
|
|
|
|
|
import { useRouter } from "next/navigation";
|
|
|
|
|
|
import type { User } from "@supabase/supabase-js";
|
|
|
|
|
|
import {
|
2026-03-13 08:15:27 +08:00
|
|
|
|
User as UserIcon,
|
|
|
|
|
|
Shield,
|
|
|
|
|
|
Fingerprint,
|
2026-03-13 02:23:01 +08:00
|
|
|
|
Bot,
|
2026-03-13 08:15:27 +08:00
|
|
|
|
RefreshCw,
|
|
|
|
|
|
LogOut,
|
2026-03-13 03:47:56 +08:00
|
|
|
|
ChevronLeft,
|
2026-03-13 02:23:01 +08:00
|
|
|
|
Copy,
|
2026-03-13 08:15:27 +08:00
|
|
|
|
CheckCircle2,
|
|
|
|
|
|
UserCheck,
|
|
|
|
|
|
Mail,
|
2026-03-13 03:47:56 +08:00
|
|
|
|
Hash,
|
2026-03-13 03:27:56 +08:00
|
|
|
|
LogIn,
|
2026-03-13 08:15:27 +08:00
|
|
|
|
Clock,
|
|
|
|
|
|
Crown,
|
|
|
|
|
|
ExternalLink,
|
2026-03-13 07:31:47 +08:00
|
|
|
|
Trophy,
|
2026-03-13 08:15:27 +08:00
|
|
|
|
Coins,
|
|
|
|
|
|
TrendingUp,
|
|
|
|
|
|
Info,
|
2026-03-13 07:31:47 +08:00
|
|
|
|
Wallet,
|
2026-03-13 08:15:27 +08:00
|
|
|
|
Zap,
|
|
|
|
|
|
Minus,
|
|
|
|
|
|
ShieldCheck,
|
|
|
|
|
|
BarChart3,
|
|
|
|
|
|
Sparkles,
|
|
|
|
|
|
ChevronRight,
|
|
|
|
|
|
Loader2,
|
|
|
|
|
|
CreditCard,
|
2026-03-13 02:23:01 +08:00
|
|
|
|
} from "lucide-react";
|
2026-03-13 08:15:27 +08:00
|
|
|
|
import {
|
|
|
|
|
|
getSupabaseBrowserClient,
|
|
|
|
|
|
hasSupabasePublicEnv,
|
|
|
|
|
|
} from "@/lib/supabase/client";
|
2026-05-20 20:13:18 +08:00
|
|
|
|
import { markAnalyticsOnce, trackAppEvent } from "@/lib/app-analytics";
|
2026-03-16 20:30:46 +08:00
|
|
|
|
import { useI18n } from "@/hooks/useI18n";
|
2026-05-15 00:58:40 +08:00
|
|
|
|
import { UnlockProOverlay } from "@/components/subscription/UnlockProOverlay";
|
2026-03-13 08:15:27 +08:00
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
import type { AuthMeResponse } from "./types";
|
2026-05-23 23:30:48 +08:00
|
|
|
|
import {
|
|
|
|
|
|
SUBSCRIPTION_HELP_HREF,
|
|
|
|
|
|
TELEGRAM_BOT_URL,
|
|
|
|
|
|
TELEGRAM_GROUP_URL,
|
|
|
|
|
|
TELEGRAM_TOPICS_GROUP_URL,
|
|
|
|
|
|
WALLETCONNECT_PROJECT_ID,
|
|
|
|
|
|
} from "./constants";
|
|
|
|
|
|
import { InfoRow, PlusIcon } from "./AccountInfoRow";
|
|
|
|
|
|
import {
|
|
|
|
|
|
chainIdToDisplayName,
|
|
|
|
|
|
clearStoredPaymentRecovery,
|
|
|
|
|
|
formatTime,
|
|
|
|
|
|
parseSubscriptionExpiry,
|
|
|
|
|
|
shortAddress,
|
|
|
|
|
|
} from "./formatters";
|
|
|
|
|
|
import { createAccountCopy } from "./account-copy";
|
2026-05-25 02:38:20 +08:00
|
|
|
|
import { resetWalletConnectProvider } from "./wallet";
|
|
|
|
|
|
import { useAccountPayment } from "./useAccountPayment";
|
2026-03-13 13:01:57 +08:00
|
|
|
|
|
2026-03-13 08:15:27 +08:00
|
|
|
|
// --- Main Component ---
|
2026-03-13 03:47:56 +08:00
|
|
|
|
|
2026-03-13 02:23:01 +08:00
|
|
|
|
export function AccountCenter() {
|
|
|
|
|
|
const router = useRouter();
|
2026-03-16 20:30:46 +08:00
|
|
|
|
const { locale } = useI18n();
|
|
|
|
|
|
const isEn = locale === "en-US";
|
2026-05-23 23:30:48 +08:00
|
|
|
|
const copy = useMemo(() => createAccountCopy(isEn), [isEn]);
|
2026-03-13 02:23:01 +08:00
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// ── UI-only state ──────────────────────────────────────
|
2026-03-13 02:23:01 +08:00
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
|
|
const [copied, setCopied] = useState(false);
|
2026-05-25 02:38:20 +08:00
|
|
|
|
const [showSecondarySections, setShowSecondarySections] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// ── Shared state (declared in component, written by hook via setters) ─
|
2026-03-13 07:31:47 +08:00
|
|
|
|
const [showOverlay, setShowOverlay] = useState(false);
|
|
|
|
|
|
const [usePoints, setUsePoints] = useState(true);
|
2026-05-25 02:38:20 +08:00
|
|
|
|
const [errorText, setErrorText] = useState("");
|
2026-03-13 02:23:01 +08:00
|
|
|
|
const [updatedAt, setUpdatedAt] = useState<string>("");
|
|
|
|
|
|
const [user, setUser] = useState<User | null>(null);
|
|
|
|
|
|
const [backend, setBackend] = useState<AuthMeResponse | null>(null);
|
2026-05-29 19:24:46 +08:00
|
|
|
|
const [referralCodeInput, setReferralCodeInput] = useState("");
|
|
|
|
|
|
const [referralApplying, setReferralApplying] = useState(false);
|
2026-05-25 02:38:20 +08:00
|
|
|
|
|
|
|
|
|
|
const supabaseReady = hasSupabasePublicEnv();
|
|
|
|
|
|
const walletConnectEnabled = Boolean(WALLETCONNECT_PROJECT_ID);
|
|
|
|
|
|
|
|
|
|
|
|
// ── Hook ────────────────────────────────────────────────
|
2026-05-23 23:30:48 +08:00
|
|
|
|
const {
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// State from usePaymentState
|
2026-05-23 23:30:48 +08:00
|
|
|
|
paymentBusy,
|
|
|
|
|
|
paymentInfo,
|
|
|
|
|
|
paymentError,
|
|
|
|
|
|
lastIntentId,
|
|
|
|
|
|
lastTxHash,
|
2026-05-25 02:38:20 +08:00
|
|
|
|
lastPaymentStartedAt,
|
2026-05-23 23:30:48 +08:00
|
|
|
|
telegramBindOpening,
|
2026-05-25 02:38:20 +08:00
|
|
|
|
telegramBindUrl,
|
2026-05-23 23:30:48 +08:00
|
|
|
|
manualPayment,
|
|
|
|
|
|
manualTxHash,
|
|
|
|
|
|
txValidation,
|
|
|
|
|
|
paymentMethodTab,
|
|
|
|
|
|
clearPaymentMessages,
|
|
|
|
|
|
clearPaymentState,
|
2026-03-13 02:23:01 +08:00
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// Setters
|
|
|
|
|
|
setPaymentBusy,
|
|
|
|
|
|
setPaymentInfo,
|
|
|
|
|
|
setPaymentError,
|
|
|
|
|
|
setLastIntentId,
|
|
|
|
|
|
setLastTxHash,
|
|
|
|
|
|
setLastPaymentStartedAt,
|
|
|
|
|
|
setTelegramBindOpening,
|
|
|
|
|
|
setPaymentMethodTab,
|
|
|
|
|
|
setManualPayment,
|
|
|
|
|
|
setManualTxHash,
|
|
|
|
|
|
setTxValidation,
|
2026-03-13 02:23:01 +08:00
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// Additional state
|
|
|
|
|
|
paymentConfig,
|
|
|
|
|
|
boundWallets,
|
|
|
|
|
|
walletAddress,
|
|
|
|
|
|
selectedPlanCode,
|
2026-05-29 01:57:42 +08:00
|
|
|
|
selectedPaymentChainId,
|
2026-05-25 02:38:20 +08:00
|
|
|
|
selectedTokenAddress,
|
|
|
|
|
|
selectedWallet,
|
|
|
|
|
|
providerMode,
|
|
|
|
|
|
injectedProviderOptions,
|
|
|
|
|
|
selectedInjectedProviderKey,
|
|
|
|
|
|
reconcileBusy,
|
|
|
|
|
|
|
|
|
|
|
|
// Shared setters
|
|
|
|
|
|
setSelectedTokenAddress,
|
2026-05-29 01:57:42 +08:00
|
|
|
|
setSelectedPaymentChainId,
|
2026-05-29 19:24:46 +08:00
|
|
|
|
setSelectedPlanCode,
|
2026-05-25 02:38:20 +08:00
|
|
|
|
setSelectedWallet,
|
|
|
|
|
|
setSelectedInjectedProviderKey,
|
|
|
|
|
|
setProviderMode,
|
|
|
|
|
|
|
|
|
|
|
|
// Derived values
|
|
|
|
|
|
authUserId,
|
|
|
|
|
|
authIsAuthenticated,
|
|
|
|
|
|
paymentReadyForRecovery,
|
|
|
|
|
|
hasRecentPaymentRecovery,
|
|
|
|
|
|
allowedPaymentHosts,
|
|
|
|
|
|
currentPaymentHost,
|
|
|
|
|
|
paymentHostAllowed,
|
|
|
|
|
|
selectedPlan,
|
|
|
|
|
|
selectedPaymentToken,
|
|
|
|
|
|
selectedTokenLabel,
|
|
|
|
|
|
availableTokenList,
|
2026-05-29 01:57:42 +08:00
|
|
|
|
availableChainList,
|
|
|
|
|
|
selectedPaymentChain,
|
2026-05-25 02:38:20 +08:00
|
|
|
|
effectivePlanList,
|
|
|
|
|
|
resolvedSelectedTokenAddress,
|
|
|
|
|
|
paymentReceiverAddress,
|
|
|
|
|
|
paymentWalletLabel,
|
|
|
|
|
|
hasPayingWallet,
|
|
|
|
|
|
totalPoints,
|
|
|
|
|
|
billing,
|
|
|
|
|
|
|
|
|
|
|
|
// Callbacks
|
|
|
|
|
|
loadSnapshot,
|
|
|
|
|
|
loadPaymentSnapshot,
|
|
|
|
|
|
connectAndBindWallet,
|
|
|
|
|
|
handleUnbindWallet,
|
|
|
|
|
|
createIntentAndPay,
|
|
|
|
|
|
createManualPaymentIntent,
|
|
|
|
|
|
submitManualPaymentTx,
|
|
|
|
|
|
validateTxHash,
|
|
|
|
|
|
handleOverlayCheckout,
|
|
|
|
|
|
openTelegramBotBindLink,
|
|
|
|
|
|
} = useAccountPayment({
|
|
|
|
|
|
isEn,
|
|
|
|
|
|
supabaseReady,
|
|
|
|
|
|
walletConnectEnabled,
|
|
|
|
|
|
copy,
|
|
|
|
|
|
backend,
|
|
|
|
|
|
user,
|
|
|
|
|
|
setUser,
|
|
|
|
|
|
setBackend,
|
|
|
|
|
|
setErrorText,
|
|
|
|
|
|
setUpdatedAt,
|
|
|
|
|
|
showOverlay,
|
|
|
|
|
|
setShowOverlay,
|
|
|
|
|
|
usePoints,
|
|
|
|
|
|
setUsePoints,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── Auth analytics effect ──────────────────────────────
|
2026-05-20 20:13:18 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!authIsAuthenticated || !authUserId) return;
|
|
|
|
|
|
const actorKey = authUserId.toLowerCase();
|
2026-05-30 19:50:50 +08:00
|
|
|
|
const subscriptionPlanCode = String(backend?.subscription_plan_code || "").trim();
|
|
|
|
|
|
const subscriptionSource = String(backend?.subscription_source || "").trim();
|
|
|
|
|
|
const trialSubscription = Boolean(
|
|
|
|
|
|
backend?.subscription_is_trial === true ||
|
|
|
|
|
|
subscriptionPlanCode.toLowerCase().includes("trial") ||
|
|
|
|
|
|
subscriptionSource.toLowerCase().includes("trial"),
|
|
|
|
|
|
);
|
2026-05-20 20:13:18 +08:00
|
|
|
|
if (markAnalyticsOnce(`signup_completed:${actorKey}`, "local")) {
|
|
|
|
|
|
trackAppEvent("signup_completed", {
|
|
|
|
|
|
entry: "account_center",
|
|
|
|
|
|
user_id: authUserId,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-30 19:50:50 +08:00
|
|
|
|
if (markAnalyticsOnce(`signup_success:${actorKey}`, "local")) {
|
|
|
|
|
|
trackAppEvent("signup_success", {
|
|
|
|
|
|
entry: "account_center",
|
|
|
|
|
|
user_id: authUserId,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-20 20:13:18 +08:00
|
|
|
|
if (markAnalyticsOnce(`dashboard_active:${actorKey}`, "session")) {
|
|
|
|
|
|
trackAppEvent("dashboard_active", {
|
|
|
|
|
|
entry: "account_center",
|
|
|
|
|
|
user_id: authUserId,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-30 19:50:50 +08:00
|
|
|
|
if (trialSubscription && markAnalyticsOnce(`trial_created:${actorKey}`, "local")) {
|
|
|
|
|
|
trackAppEvent("trial_created", {
|
|
|
|
|
|
entry: "account_center",
|
|
|
|
|
|
user_id: authUserId,
|
|
|
|
|
|
subscription_plan_code: subscriptionPlanCode || null,
|
|
|
|
|
|
subscription_source: subscriptionSource || null,
|
|
|
|
|
|
subscription_expires_at: backend?.subscription_expires_at || null,
|
|
|
|
|
|
subscription_is_trial: backend?.subscription_is_trial === true,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [
|
|
|
|
|
|
authIsAuthenticated,
|
|
|
|
|
|
authUserId,
|
|
|
|
|
|
backend?.subscription_expires_at,
|
|
|
|
|
|
backend?.subscription_is_trial,
|
|
|
|
|
|
backend?.subscription_plan_code,
|
|
|
|
|
|
backend?.subscription_source,
|
|
|
|
|
|
]);
|
2026-05-20 20:13:18 +08:00
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// ── Idle callback effect ──────────────────────────────
|
2026-03-18 20:38:43 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let canceled = false;
|
|
|
|
|
|
let timeoutId: number | null = null;
|
|
|
|
|
|
let idleId: number | null = null;
|
|
|
|
|
|
const win = typeof window !== "undefined" ? (window as any) : null;
|
|
|
|
|
|
|
|
|
|
|
|
const reveal = () => {
|
|
|
|
|
|
if (!canceled) {
|
|
|
|
|
|
setShowSecondarySections(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (win && typeof win.requestIdleCallback === "function") {
|
|
|
|
|
|
idleId = win.requestIdleCallback(reveal, { timeout: 320 });
|
|
|
|
|
|
} else if (typeof window !== "undefined") {
|
|
|
|
|
|
timeoutId = window.setTimeout(reveal, 140);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setShowSecondarySections(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
canceled = true;
|
2026-03-21 12:34:36 +08:00
|
|
|
|
if (
|
|
|
|
|
|
win &&
|
|
|
|
|
|
idleId != null &&
|
|
|
|
|
|
typeof win.cancelIdleCallback === "function"
|
|
|
|
|
|
) {
|
2026-03-18 20:38:43 +08:00
|
|
|
|
win.cancelIdleCallback(idleId);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (timeoutId != null && typeof window !== "undefined") {
|
|
|
|
|
|
window.clearTimeout(timeoutId);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// ── Initial load effect ────────────────────────────────
|
2026-03-13 02:23:01 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
const run = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
await loadSnapshot();
|
|
|
|
|
|
if (!cancelled) setLoading(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
void run();
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [loadSnapshot]);
|
|
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// ── Refresh ────────────────────────────────────────────
|
2026-03-13 02:23:01 +08:00
|
|
|
|
const onRefresh = async () => {
|
|
|
|
|
|
setRefreshing(true);
|
|
|
|
|
|
await loadSnapshot();
|
2026-03-13 05:13:48 +08:00
|
|
|
|
await loadPaymentSnapshot();
|
2026-03-13 02:23:01 +08:00
|
|
|
|
setRefreshing(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// ── Sign out ────────────────────────────────────────────
|
2026-03-13 02:23:01 +08:00
|
|
|
|
const onSignOut = async () => {
|
2026-05-23 23:30:48 +08:00
|
|
|
|
clearPaymentState();
|
2026-04-06 20:40:26 +08:00
|
|
|
|
clearStoredPaymentRecovery();
|
2026-05-23 23:30:48 +08:00
|
|
|
|
await resetWalletConnectProvider();
|
2026-03-13 02:23:01 +08:00
|
|
|
|
if (supabaseReady) {
|
|
|
|
|
|
try {
|
2026-03-13 03:47:56 +08:00
|
|
|
|
await getSupabaseBrowserClient().auth.signOut();
|
2026-03-13 07:31:47 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
2026-03-13 02:23:01 +08:00
|
|
|
|
}
|
2026-03-13 03:47:56 +08:00
|
|
|
|
router.replace("/");
|
2026-03-13 02:23:01 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// ── Derived display values ──────────────────────────────
|
2026-03-13 02:23:01 +08:00
|
|
|
|
const userId = backend?.user_id || user?.id || "";
|
2026-03-13 03:27:56 +08:00
|
|
|
|
const isAuthenticated = Boolean(userId);
|
2026-03-13 02:23:01 +08:00
|
|
|
|
const email = backend?.email || user?.email || "";
|
2026-03-13 08:15:27 +08:00
|
|
|
|
const displayName =
|
|
|
|
|
|
String(user?.user_metadata?.full_name || "").trim() ||
|
|
|
|
|
|
(email ? String(email).split("@")[0] : "") ||
|
2026-03-16 20:30:46 +08:00
|
|
|
|
copy.guestUser;
|
2026-03-13 03:47:56 +08:00
|
|
|
|
const initials = (displayName.slice(0, 2) || "PW").toUpperCase();
|
2026-03-16 20:30:46 +08:00
|
|
|
|
const joinedAt = formatTime(user?.created_at, locale);
|
2026-05-27 12:14:07 +08:00
|
|
|
|
const backendAuthenticated = backend?.authenticated === true;
|
|
|
|
|
|
const localAuthenticated = Boolean(user?.id);
|
|
|
|
|
|
const isSubscriptionUnknown = Boolean(
|
|
|
|
|
|
localAuthenticated &&
|
|
|
|
|
|
(!backend ||
|
|
|
|
|
|
backend.subscription_active == null ||
|
|
|
|
|
|
backend.authenticated === false),
|
|
|
|
|
|
);
|
|
|
|
|
|
const isSubscribed = backend?.subscription_active === true;
|
2026-05-29 19:24:46 +08:00
|
|
|
|
const subscriptionSource = String(backend?.subscription_source || "").trim();
|
|
|
|
|
|
const isTrialSubscription = Boolean(
|
|
|
|
|
|
backend?.subscription_is_trial === true ||
|
|
|
|
|
|
String(backend?.subscription_plan_code || "").toLowerCase().includes("trial") ||
|
|
|
|
|
|
subscriptionSource.toLowerCase().includes("trial"),
|
|
|
|
|
|
);
|
2026-05-27 12:14:07 +08:00
|
|
|
|
const subscriptionStatusLabel = isSubscribed
|
2026-05-29 19:24:46 +08:00
|
|
|
|
? isTrialSubscription
|
|
|
|
|
|
? copy.trialBadge
|
|
|
|
|
|
: copy.proMember
|
2026-05-27 12:14:07 +08:00
|
|
|
|
: isSubscriptionUnknown
|
|
|
|
|
|
? copy.subscriptionChecking
|
|
|
|
|
|
: isEn
|
|
|
|
|
|
? "UNSUBSCRIBED"
|
|
|
|
|
|
: "未订阅";
|
2026-03-30 00:58:43 +08:00
|
|
|
|
const planCode = String(backend?.subscription_plan_code || "").trim();
|
2026-04-13 16:35:22 +08:00
|
|
|
|
const currentExpiryRaw = String(
|
2026-05-25 02:38:20 +08:00
|
|
|
|
backend?.subscription_expires_at ||
|
|
|
|
|
|
user?.user_metadata?.pro_expiry ||
|
|
|
|
|
|
"",
|
2026-03-13 15:39:25 +08:00
|
|
|
|
).trim();
|
2026-04-13 16:35:22 +08:00
|
|
|
|
const totalExpiryRaw = String(
|
|
|
|
|
|
backend?.subscription_total_expires_at ||
|
|
|
|
|
|
backend?.subscription_expires_at ||
|
|
|
|
|
|
user?.user_metadata?.pro_expiry ||
|
|
|
|
|
|
"",
|
|
|
|
|
|
).trim();
|
|
|
|
|
|
const queuedExtensionDays = Math.max(
|
|
|
|
|
|
0,
|
|
|
|
|
|
Number(backend?.subscription_queued_days || 0),
|
|
|
|
|
|
);
|
2026-05-25 02:38:20 +08:00
|
|
|
|
const hasQueuedExtension = Boolean(
|
|
|
|
|
|
isSubscribed && queuedExtensionDays > 0,
|
2026-05-19 18:26:45 +08:00
|
|
|
|
);
|
2026-05-29 19:24:46 +08:00
|
|
|
|
const canAccessPaidTelegramGroup = Boolean(isSubscribed && !isTrialSubscription);
|
2026-05-29 21:23:12 +08:00
|
|
|
|
const hasTelegramPanel = Boolean(isTrialSubscription || canAccessPaidTelegramGroup);
|
2026-05-25 02:38:20 +08:00
|
|
|
|
const telegramBound =
|
|
|
|
|
|
Number(backend?.telegram_pricing?.telegram_id || 0) > 0;
|
|
|
|
|
|
const displayExpiryRaw = isSubscribed
|
|
|
|
|
|
? totalExpiryRaw
|
|
|
|
|
|
: currentExpiryRaw;
|
2026-05-19 17:47:51 +08:00
|
|
|
|
const reminderExpiryRaw = isSubscribed
|
|
|
|
|
|
? totalExpiryRaw
|
|
|
|
|
|
: currentExpiryRaw || totalExpiryRaw;
|
2026-04-13 16:35:22 +08:00
|
|
|
|
const expiryInfo = parseSubscriptionExpiry(reminderExpiryRaw);
|
|
|
|
|
|
const expiryFormatted = formatTime(displayExpiryRaw, locale);
|
|
|
|
|
|
const currentExpiryFormatted = formatTime(currentExpiryRaw, locale);
|
|
|
|
|
|
const totalExpiryFormatted = formatTime(totalExpiryRaw, locale);
|
2026-03-13 15:39:25 +08:00
|
|
|
|
const proExpiry = isSubscribed
|
|
|
|
|
|
? expiryFormatted !== "--"
|
|
|
|
|
|
? expiryFormatted
|
2026-04-13 16:35:22 +08:00
|
|
|
|
: displayExpiryRaw || copy.proPendingSync
|
2026-05-27 12:14:07 +08:00
|
|
|
|
: isSubscriptionUnknown
|
|
|
|
|
|
? copy.subscriptionUnknown
|
2026-03-16 20:30:46 +08:00
|
|
|
|
: copy.noProSubscription;
|
2026-04-19 20:14:07 +08:00
|
|
|
|
const showExpiringSoon = Boolean(
|
|
|
|
|
|
isSubscribed &&
|
2026-05-25 02:38:20 +08:00
|
|
|
|
!hasQueuedExtension &&
|
|
|
|
|
|
expiryInfo &&
|
|
|
|
|
|
!expiryInfo.expired &&
|
|
|
|
|
|
expiryInfo.daysLeft <= 3,
|
2026-05-19 17:47:51 +08:00
|
|
|
|
);
|
|
|
|
|
|
const showExpiredReminder = Boolean(
|
|
|
|
|
|
!isSubscribed && expiryInfo && expiryInfo.expired,
|
2026-04-19 20:14:07 +08:00
|
|
|
|
);
|
2026-04-10 09:00:37 +08:00
|
|
|
|
const paymentFeatureReady = paymentReadyForRecovery;
|
|
|
|
|
|
const canOpenCheckoutOverlay = Boolean(
|
|
|
|
|
|
paymentFeatureReady &&
|
2026-05-27 12:14:07 +08:00
|
|
|
|
!isSubscriptionUnknown &&
|
2026-05-25 02:38:20 +08:00
|
|
|
|
(!isSubscribed || showExpiringSoon || showExpiredReminder),
|
2026-04-10 09:00:37 +08:00
|
|
|
|
);
|
2026-03-30 00:58:43 +08:00
|
|
|
|
const subscriptionStatusTitle = showExpiredReminder
|
2026-05-20 11:12:29 +08:00
|
|
|
|
? copy.proExpiredTitle
|
2026-03-30 00:58:43 +08:00
|
|
|
|
: showExpiringSoon
|
2026-05-20 11:12:29 +08:00
|
|
|
|
? copy.proEndsSoonTitle
|
2026-03-30 00:58:43 +08:00
|
|
|
|
: "";
|
|
|
|
|
|
const subscriptionStatusBody = showExpiredReminder
|
2026-05-20 11:12:29 +08:00
|
|
|
|
? copy.proExpiredBody
|
2026-03-30 00:58:43 +08:00
|
|
|
|
: showExpiringSoon
|
2026-05-20 11:12:29 +08:00
|
|
|
|
? copy.proEndsSoonBody
|
2026-03-30 00:58:43 +08:00
|
|
|
|
: "";
|
|
|
|
|
|
const subscriptionStatusMeta =
|
|
|
|
|
|
expiryInfo && (showExpiringSoon || showExpiredReminder)
|
|
|
|
|
|
? `${formatTime(expiryInfo.raw, locale)} · ${copy.daysLeft.replace("{days}", String(Math.max(expiryInfo.daysLeft, 0)))}`
|
|
|
|
|
|
: "";
|
2026-04-13 16:35:22 +08:00
|
|
|
|
const queuedExtensionSummary = hasQueuedExtension
|
|
|
|
|
|
? copy.queuedExtensionSummary
|
|
|
|
|
|
.replace("{current}", currentExpiryFormatted)
|
|
|
|
|
|
.replace("{days}", String(queuedExtensionDays))
|
|
|
|
|
|
.replace("{total}", totalExpiryFormatted)
|
|
|
|
|
|
: "";
|
2026-05-25 02:38:20 +08:00
|
|
|
|
const expiryLabel = hasQueuedExtension
|
|
|
|
|
|
? copy.accessUntil
|
|
|
|
|
|
: copy.renewalDate;
|
2026-05-29 19:24:46 +08:00
|
|
|
|
const displayPlanList = effectivePlanList.length
|
|
|
|
|
|
? effectivePlanList
|
|
|
|
|
|
: [
|
|
|
|
|
|
{ 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 referral = backend?.referral;
|
|
|
|
|
|
const referralCode = String(referral?.code || "").trim();
|
|
|
|
|
|
const appliedReferralCode = String(referral?.applied_code || "").trim();
|
|
|
|
|
|
const canApplyReferralCode = Boolean(
|
|
|
|
|
|
isAuthenticated &&
|
|
|
|
|
|
!isSubscribed &&
|
|
|
|
|
|
!appliedReferralCode &&
|
|
|
|
|
|
referralCodeInput.trim(),
|
|
|
|
|
|
);
|
2026-03-13 02:23:01 +08:00
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// ── Payment overlay tracking effect ──────────────────────
|
2026-03-31 07:15:54 +08:00
|
|
|
|
useEffect(() => {
|
2026-04-10 09:00:37 +08:00
|
|
|
|
if (!showOverlay || !canOpenCheckoutOverlay) return;
|
2026-03-31 07:15:54 +08:00
|
|
|
|
trackAppEvent("paywall_viewed", {
|
|
|
|
|
|
entry: "account_center",
|
|
|
|
|
|
user_state: isAuthenticated ? "logged_in" : "guest",
|
|
|
|
|
|
expired: showExpiredReminder,
|
|
|
|
|
|
expiring_soon: showExpiringSoon,
|
|
|
|
|
|
subscription_plan_code: planCode || null,
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [
|
|
|
|
|
|
isAuthenticated,
|
2026-05-19 17:47:51 +08:00
|
|
|
|
canOpenCheckoutOverlay,
|
|
|
|
|
|
planCode,
|
|
|
|
|
|
showExpiredReminder,
|
|
|
|
|
|
showExpiringSoon,
|
|
|
|
|
|
showOverlay,
|
|
|
|
|
|
]);
|
2026-03-31 07:15:54 +08:00
|
|
|
|
|
2026-05-30 18:04:36 +08:00
|
|
|
|
// ── Referral points display ────────────────────────────
|
|
|
|
|
|
const referralRewardPointsRaw = Number(referral?.reward_points ?? 3500);
|
|
|
|
|
|
const referralRewardPoints = Number.isFinite(referralRewardPointsRaw)
|
|
|
|
|
|
? Math.max(0, referralRewardPointsRaw)
|
|
|
|
|
|
: 3500;
|
|
|
|
|
|
const monthlyReferralCountRaw = Number(referral?.monthly_reward_count ?? 0);
|
|
|
|
|
|
const monthlyReferralCount = Number.isFinite(monthlyReferralCountRaw)
|
|
|
|
|
|
? Math.max(0, monthlyReferralCountRaw)
|
|
|
|
|
|
: 0;
|
|
|
|
|
|
const monthlyReferralLimitRaw = Number(referral?.monthly_reward_limit ?? 10);
|
|
|
|
|
|
const monthlyReferralLimit = Number.isFinite(monthlyReferralLimitRaw)
|
|
|
|
|
|
? Math.max(0, monthlyReferralLimitRaw)
|
|
|
|
|
|
: 10;
|
|
|
|
|
|
const monthlyReferralPointsRaw = Number(
|
|
|
|
|
|
referral?.monthly_reward_points ?? monthlyReferralCount * referralRewardPoints,
|
2026-03-13 10:19:59 +08:00
|
|
|
|
);
|
2026-05-30 18:04:36 +08:00
|
|
|
|
const monthlyReferralPoints = Number.isFinite(monthlyReferralPointsRaw)
|
|
|
|
|
|
? Math.max(0, monthlyReferralPointsRaw)
|
2026-03-13 08:15:27 +08:00
|
|
|
|
: 0;
|
2026-05-30 18:04:36 +08:00
|
|
|
|
const monthlyReferralPointsLimitRaw = Number(
|
|
|
|
|
|
referral?.monthly_reward_points_limit ?? monthlyReferralLimit * referralRewardPoints,
|
|
|
|
|
|
);
|
|
|
|
|
|
const monthlyReferralPointsLimit = Number.isFinite(monthlyReferralPointsLimitRaw)
|
|
|
|
|
|
? Math.max(0, monthlyReferralPointsLimitRaw)
|
|
|
|
|
|
: monthlyReferralLimit * referralRewardPoints;
|
2026-03-13 07:31:47 +08:00
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// ── Telegram bind command ──────────────────────────────
|
2026-03-13 08:15:27 +08:00
|
|
|
|
const bindCommand = userId
|
|
|
|
|
|
? `/bind ${userId}${email ? ` ${email}` : ""}`
|
|
|
|
|
|
: "/bind <supabase_user_id> <email>";
|
|
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// ── Copy handler ──────────────────────────────────────
|
2026-03-13 08:15:27 +08:00
|
|
|
|
const handleCopy = (text: string) => {
|
|
|
|
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
|
|
|
|
setCopied(true);
|
|
|
|
|
|
window.setTimeout(() => setCopied(false), 2000);
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-03-13 07:31:47 +08:00
|
|
|
|
|
2026-05-29 19:24:46 +08:00
|
|
|
|
const applyReferralCode = useCallback(async () => {
|
|
|
|
|
|
const code = referralCodeInput.trim();
|
|
|
|
|
|
if (!code || referralApplying) return;
|
|
|
|
|
|
setReferralApplying(true);
|
|
|
|
|
|
setPaymentError("");
|
|
|
|
|
|
setPaymentInfo("");
|
|
|
|
|
|
try {
|
|
|
|
|
|
const headers: Record<string, string> = {
|
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
};
|
|
|
|
|
|
if (supabaseReady) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
data: { session },
|
|
|
|
|
|
} = await getSupabaseBrowserClient().auth.getSession();
|
|
|
|
|
|
const token = String(session?.access_token || "").trim();
|
|
|
|
|
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await fetch("/api/auth/referral/apply", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers,
|
|
|
|
|
|
body: JSON.stringify({ code }),
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
const raw = (await res.text()).slice(0, 240);
|
|
|
|
|
|
throw new Error(raw || copy.referralApplyFailed);
|
|
|
|
|
|
}
|
|
|
|
|
|
setPaymentInfo(copy.referralApplied);
|
|
|
|
|
|
setReferralCodeInput("");
|
|
|
|
|
|
await loadSnapshot();
|
|
|
|
|
|
await loadPaymentSnapshot();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setPaymentError(
|
|
|
|
|
|
error instanceof Error ? error.message : copy.referralApplyFailed,
|
|
|
|
|
|
);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setReferralApplying(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [
|
|
|
|
|
|
copy.referralApplied,
|
|
|
|
|
|
copy.referralApplyFailed,
|
|
|
|
|
|
loadPaymentSnapshot,
|
|
|
|
|
|
loadSnapshot,
|
|
|
|
|
|
referralApplying,
|
|
|
|
|
|
referralCodeInput,
|
|
|
|
|
|
setPaymentError,
|
|
|
|
|
|
setPaymentInfo,
|
|
|
|
|
|
supabaseReady,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
// ── Render ────────────────────────────────────────────
|
2026-03-13 08:15:27 +08:00
|
|
|
|
|
|
|
|
|
|
if (loading && !refreshing) {
|
|
|
|
|
|
return (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="flex h-screen w-full items-center justify-center bg-[#f4f7fb]">
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<div className="flex flex-col items-center gap-4">
|
|
|
|
|
|
<Loader2 className="h-12 w-12 animate-spin text-blue-500" />
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="font-medium text-slate-500">{copy.loadingAccount}</p>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 02:23:01 +08:00
|
|
|
|
return (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="relative flex min-h-screen w-full flex-col items-center overflow-hidden bg-[#f4f7fb] p-4 font-sans text-slate-900 md:p-8">
|
|
|
|
|
|
<div className="absolute inset-x-0 top-0 h-20 border-b border-slate-200 bg-white"></div>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<header className="w-full max-w-6xl flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 z-20">
|
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href="/"
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="group rounded-lg border border-slate-200 bg-white p-2 text-slate-500 shadow-sm transition-all hover:border-slate-300 hover:text-slate-950 active:scale-95"
|
2026-03-16 20:30:46 +08:00
|
|
|
|
title={copy.backHome}
|
|
|
|
|
|
aria-label={copy.backHome}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
>
|
|
|
|
|
|
<ChevronLeft
|
|
|
|
|
|
size={20}
|
|
|
|
|
|
className="group-hover:-translate-x-0.5 transition-transform"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
<div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<h1 className="flex items-center gap-2 text-2xl font-bold text-slate-950">
|
2026-03-16 20:30:46 +08:00
|
|
|
|
{copy.accountCenter}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</h1>
|
2026-03-13 06:41:33 +08:00
|
|
|
|
</div>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
2026-05-19 17:47:51 +08:00
|
|
|
|
{!showOverlay && canOpenCheckoutOverlay && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setShowOverlay(true)}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="flex items-center gap-2 rounded-lg border border-amber-300 bg-amber-50 px-4 py-2 text-sm font-semibold text-amber-700 transition-all hover:bg-amber-100"
|
2026-05-19 17:47:51 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Crown size={16} />{" "}
|
|
|
|
|
|
{showExpiringSoon || showExpiredReminder
|
|
|
|
|
|
? copy.renewNow
|
|
|
|
|
|
: copy.upgradePro}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => void onRefresh()}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 shadow-sm transition-all hover:border-slate-300 hover:text-slate-950 disabled:opacity-50"
|
2026-03-13 08:15:27 +08:00
|
|
|
|
disabled={refreshing}
|
|
|
|
|
|
>
|
|
|
|
|
|
{refreshing ? (
|
|
|
|
|
|
<RefreshCw size={16} className="animate-spin" />
|
2026-03-13 03:27:56 +08:00
|
|
|
|
) : (
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<RefreshCw size={16} />
|
|
|
|
|
|
)}{" "}
|
2026-03-16 20:30:46 +08:00
|
|
|
|
{copy.refresh}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
{isAuthenticated ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => void onSignOut()}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-semibold text-red-700 transition-all hover:bg-red-100"
|
2026-03-13 08:15:27 +08:00
|
|
|
|
>
|
2026-03-16 20:30:46 +08:00
|
|
|
|
<LogOut size={16} /> {copy.signOut}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href="/auth/login?next=%2Faccount"
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-4 py-2 text-sm font-semibold text-blue-700 transition-all hover:bg-blue-100"
|
2026-03-13 08:15:27 +08:00
|
|
|
|
>
|
2026-03-16 20:30:46 +08:00
|
|
|
|
<LogIn size={16} /> {copy.signIn}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</Link>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<main className="w-full max-w-6xl grid grid-cols-1 lg:grid-cols-12 gap-6 z-10 relative">
|
2026-03-30 00:58:43 +08:00
|
|
|
|
{(showExpiringSoon || showExpiredReminder) && (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="lg:col-span-12 rounded-2xl border border-amber-300 bg-amber-50 px-6 py-5 shadow-sm">
|
2026-03-30 00:58:43 +08:00
|
|
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
|
|
|
|
<div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="flex items-center gap-2 text-sm font-bold text-amber-700">
|
2026-03-30 00:58:43 +08:00
|
|
|
|
<Crown size={16} />
|
|
|
|
|
|
<span>{subscriptionStatusTitle}</span>
|
|
|
|
|
|
</div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="mt-1 text-sm text-amber-900">
|
2026-03-30 00:58:43 +08:00
|
|
|
|
{subscriptionStatusBody}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{subscriptionStatusMeta ? (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="mt-1 text-xs text-amber-700">
|
2026-03-30 00:58:43 +08:00
|
|
|
|
{subscriptionStatusMeta}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
2026-04-10 09:00:37 +08:00
|
|
|
|
{billing.canRedeem ? (
|
2026-05-25 02:38:20 +08:00
|
|
|
|
<p className="mt-2 text-xs text-emerald-700">
|
2026-05-19 17:47:51 +08:00
|
|
|
|
当前可用 {billing.pointsUsed} 积分抵扣 $
|
|
|
|
|
|
{billing.discountAmount.toFixed(2)}, 续费时会自动生效。
|
2026-04-10 09:00:37 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
2026-03-30 00:58:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowOverlay(true)}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="inline-flex items-center justify-center gap-2 rounded-lg border border-amber-300 bg-white px-4 py-2 text-sm font-bold text-amber-800 transition-all hover:bg-amber-100"
|
2026-03-30 00:58:43 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Crown size={16} />
|
|
|
|
|
|
{showExpiredReminder ? copy.renewNow : copy.upgradePro}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-13 08:15:27 +08:00
|
|
|
|
{/* User Card */}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="flex flex-col items-center gap-8 rounded-2xl border border-slate-200 bg-white p-8 shadow-sm lg:col-span-8 md:flex-row">
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<div className="relative">
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="flex h-24 w-24 items-center justify-center rounded-xl border border-blue-200 bg-blue-600 text-3xl font-bold text-white shadow-sm">
|
2026-03-13 08:15:27 +08:00
|
|
|
|
{initials}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
2026-05-27 12:14:07 +08:00
|
|
|
|
className={`absolute -bottom-2 -right-2 rounded-lg border-4 border-white p-1.5 ${
|
|
|
|
|
|
isSubscribed
|
|
|
|
|
|
? "bg-amber-400 text-slate-950"
|
|
|
|
|
|
: isSubscriptionUnknown
|
|
|
|
|
|
? "bg-blue-100 text-blue-500"
|
|
|
|
|
|
: "bg-slate-100 text-slate-400"
|
|
|
|
|
|
}`}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Crown size={16} fill="currentColor" />
|
|
|
|
|
|
</div>
|
2026-03-13 02:23:01 +08:00
|
|
|
|
</div>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<div className="flex-grow text-center md:text-left">
|
|
|
|
|
|
<div className="flex items-center justify-center md:justify-start gap-3 mb-1">
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<h2 className="text-3xl font-bold text-slate-950">{displayName}</h2>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<span
|
2026-05-27 12:14:07 +08:00
|
|
|
|
className={`rounded-full border px-2 py-0.5 text-[10px] font-black uppercase ${
|
|
|
|
|
|
isSubscribed
|
|
|
|
|
|
? "border-blue-200 bg-blue-50 text-blue-700"
|
|
|
|
|
|
: isSubscriptionUnknown
|
|
|
|
|
|
? "border-blue-200 bg-blue-50 text-blue-600"
|
|
|
|
|
|
: "border-slate-200 bg-slate-50 text-slate-500"
|
|
|
|
|
|
}`}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
>
|
2026-05-27 12:14:07 +08:00
|
|
|
|
{subscriptionStatusLabel}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="mb-4 font-mono text-sm text-slate-500">
|
2026-03-16 20:30:46 +08:00
|
|
|
|
{email || copy.guestUser}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
<div className="flex flex-wrap justify-center md:justify-start gap-4">
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="flex items-center gap-1.5 text-xs text-slate-500">
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<Hash size={14} />{" "}
|
|
|
|
|
|
<span className="font-mono">
|
|
|
|
|
|
{userId ? `${userId.substring(0, 12)}...` : "--"}
|
|
|
|
|
|
</span>
|
2026-03-13 02:23:01 +08:00
|
|
|
|
</div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="flex items-center gap-1.5 text-xs text-slate-500">
|
2026-03-16 20:30:46 +08:00
|
|
|
|
<Clock size={14} />{" "}
|
|
|
|
|
|
<span>
|
|
|
|
|
|
{copy.joinedAt}: {joinedAt}
|
|
|
|
|
|
</span>
|
2026-03-13 07:31:47 +08:00
|
|
|
|
</div>
|
2026-03-13 03:47:56 +08:00
|
|
|
|
</div>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-col gap-3">
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="min-w-[140px] rounded-xl border border-slate-200 bg-slate-50 px-6 py-4 text-center">
|
|
|
|
|
|
<p className="mb-1 text-[10px] uppercase text-slate-500">
|
2026-03-16 20:30:46 +08:00
|
|
|
|
{copy.totalPoints}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</p>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="flex items-center justify-center gap-2 text-xl font-bold text-slate-950">
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<Coins size={16} className="text-yellow-500" />{" "}
|
|
|
|
|
|
{totalPoints.toLocaleString()}
|
|
|
|
|
|
</p>
|
2026-03-13 07:31:47 +08:00
|
|
|
|
</div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="min-w-[140px] rounded-xl border border-emerald-200 bg-emerald-50 px-6 py-4 text-center">
|
|
|
|
|
|
<p className="mb-1 text-[10px] font-bold uppercase text-emerald-700">
|
2026-03-23 21:32:18 +08:00
|
|
|
|
{copy.weeklyPoints}
|
|
|
|
|
|
</p>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="flex items-center justify-center gap-2 text-xl font-bold text-slate-950">
|
2026-03-23 21:32:18 +08:00
|
|
|
|
<TrendingUp size={16} className="text-emerald-400" />{" "}
|
2026-05-30 18:04:36 +08:00
|
|
|
|
{monthlyReferralCount.toLocaleString()}
|
2026-03-23 21:32:18 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="min-w-[140px] rounded-xl border border-blue-200 bg-blue-50 px-6 py-4 text-center">
|
|
|
|
|
|
<p className="mb-1 text-[10px] font-bold uppercase text-blue-700">
|
2026-03-16 20:30:46 +08:00
|
|
|
|
{copy.weeklyRank}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</p>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="flex items-center justify-center gap-2 text-xl font-bold text-slate-950">
|
2026-03-23 21:32:18 +08:00
|
|
|
|
<Trophy size={16} className="text-amber-400" />{" "}
|
2026-05-30 18:04:36 +08:00
|
|
|
|
{monthlyReferralCount}/{monthlyReferralLimit}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-13 07:31:47 +08:00
|
|
|
|
|
2026-05-30 18:04:36 +08:00
|
|
|
|
{/* Referral rewards */}
|
2026-03-18 20:38:43 +08:00
|
|
|
|
{showSecondarySections ? (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="flex flex-col justify-between rounded-2xl border border-slate-200 bg-white p-6 shadow-sm lg:col-span-4">
|
2026-03-18 20:38:43 +08:00
|
|
|
|
<div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<h3 className="mb-6 flex items-center gap-2 text-lg font-bold text-slate-950">
|
|
|
|
|
|
<Sparkles size={20} className="text-amber-500" />{" "}
|
2026-03-18 20:38:43 +08:00
|
|
|
|
{copy.weeklyRewards}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<div className="space-y-3">
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="flex items-center justify-between rounded-xl border border-slate-200 bg-slate-50 p-3">
|
2026-03-18 20:38:43 +08:00
|
|
|
|
<span className="text-sm flex items-center gap-2">
|
2026-05-30 18:04:36 +08:00
|
|
|
|
<Coins size={16} className="text-yellow-500" />{" "}
|
|
|
|
|
|
{copy.referralRewardHint}
|
2026-03-18 20:38:43 +08:00
|
|
|
|
</span>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<span className="text-xs font-bold text-amber-600">
|
2026-05-30 18:04:36 +08:00
|
|
|
|
+{referralRewardPoints.toLocaleString()}
|
2026-03-18 20:38:43 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="flex items-center justify-between rounded-xl border border-slate-200 bg-slate-50 p-3">
|
2026-03-18 20:38:43 +08:00
|
|
|
|
<span className="text-sm flex items-center gap-2">
|
2026-05-30 18:04:36 +08:00
|
|
|
|
<TrendingUp size={16} className="text-emerald-500" />{" "}
|
|
|
|
|
|
{copy.weeklyPoints}
|
2026-03-18 20:38:43 +08:00
|
|
|
|
</span>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<span className="text-xs font-bold text-slate-600">
|
2026-05-30 18:04:36 +08:00
|
|
|
|
{monthlyReferralCount}/{monthlyReferralLimit}
|
2026-03-18 20:38:43 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="flex items-center justify-between rounded-xl border border-slate-200 bg-slate-50 p-3">
|
2026-03-18 20:38:43 +08:00
|
|
|
|
<span className="text-sm flex items-center gap-2">
|
2026-05-30 18:04:36 +08:00
|
|
|
|
<Trophy size={16} className="text-blue-500" />{" "}
|
|
|
|
|
|
{copy.totalPoints}
|
2026-03-18 20:38:43 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-xs font-bold text-orange-400">
|
2026-05-30 18:04:36 +08:00
|
|
|
|
{monthlyReferralPoints.toLocaleString()}/{monthlyReferralPointsLimit.toLocaleString()}
|
2026-03-18 20:38:43 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="mt-6 flex items-start gap-2 rounded-xl border border-slate-200 bg-slate-50 p-3">
|
2026-03-18 20:38:43 +08:00
|
|
|
|
<Info size={14} className="text-slate-500 mt-0.5 shrink-0" />
|
|
|
|
|
|
<p className="text-[10px] text-slate-500 leading-normal italic">
|
2026-05-30 18:04:36 +08:00
|
|
|
|
{copy.pointsRule}
|
2026-03-18 20:38:43 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</div>
|
2026-03-18 20:38:43 +08:00
|
|
|
|
) : (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="rounded-2xl border border-slate-200 bg-white p-6 lg:col-span-4">
|
|
|
|
|
|
<div className="h-6 w-40 animate-pulse rounded bg-slate-200" />
|
2026-03-18 20:38:43 +08:00
|
|
|
|
<div className="mt-4 space-y-2">
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="h-12 animate-pulse rounded-xl bg-slate-100" />
|
|
|
|
|
|
<div className="h-12 animate-pulse rounded-xl bg-slate-100" />
|
|
|
|
|
|
<div className="h-12 animate-pulse rounded-xl bg-slate-100" />
|
2026-03-18 20:38:43 +08:00
|
|
|
|
</div>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</div>
|
2026-03-18 20:38:43 +08:00
|
|
|
|
)}
|
2026-03-13 07:31:47 +08:00
|
|
|
|
|
2026-03-13 08:15:27 +08:00
|
|
|
|
{/* Subscription Info & Paywall */}
|
|
|
|
|
|
<div className="lg:col-span-12 relative">
|
2026-05-19 17:47:51 +08:00
|
|
|
|
<div
|
2026-04-10 09:00:37 +08:00
|
|
|
|
className={`grid grid-cols-1 md:grid-cols-2 gap-6 transition-all duration-700 ${canOpenCheckoutOverlay && showOverlay ? "blur-md grayscale-[0.3] opacity-30 select-none pointer-events-none" : ""}`}
|
2026-05-19 17:47:51 +08:00
|
|
|
|
>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<section className="space-y-3 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
|
|
|
|
|
<h3 className="mb-4 text-sm font-bold uppercase text-blue-700">
|
2026-03-16 20:30:46 +08:00
|
|
|
|
{copy.membershipDetails}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</h3>
|
2026-03-21 12:34:36 +08:00
|
|
|
|
<InfoRow
|
|
|
|
|
|
icon={ShieldCheck}
|
|
|
|
|
|
label={copy.authMode}
|
|
|
|
|
|
value="Supabase"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<InfoRow
|
|
|
|
|
|
icon={BarChart3}
|
|
|
|
|
|
label={copy.weatherEngine}
|
|
|
|
|
|
value="DEB + 多模型"
|
|
|
|
|
|
/>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<InfoRow
|
|
|
|
|
|
icon={Zap}
|
2026-03-16 20:30:46 +08:00
|
|
|
|
label={copy.intradayAnalysis}
|
2026-05-27 12:14:07 +08:00
|
|
|
|
value={
|
|
|
|
|
|
isSubscriptionUnknown
|
|
|
|
|
|
? copy.subscriptionChecking
|
|
|
|
|
|
: isSubscribed
|
|
|
|
|
|
? copy.deepMode
|
|
|
|
|
|
: copy.compactVisible
|
|
|
|
|
|
}
|
2026-03-13 15:39:25 +08:00
|
|
|
|
isPrimary={isSubscribed}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<InfoRow
|
|
|
|
|
|
icon={Clock}
|
2026-03-16 20:30:46 +08:00
|
|
|
|
label={copy.historyFuture}
|
2026-05-27 12:14:07 +08:00
|
|
|
|
value={
|
|
|
|
|
|
isSubscriptionUnknown
|
|
|
|
|
|
? copy.subscriptionChecking
|
|
|
|
|
|
: isSubscribed
|
|
|
|
|
|
? copy.enabled
|
|
|
|
|
|
: copy.locked
|
|
|
|
|
|
}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
isPrimary={isSubscribed}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<InfoRow
|
2026-03-13 15:39:25 +08:00
|
|
|
|
icon={Bot}
|
2026-03-16 20:30:46 +08:00
|
|
|
|
label={copy.smartPush}
|
2026-05-27 12:14:07 +08:00
|
|
|
|
value={
|
|
|
|
|
|
isSubscriptionUnknown
|
|
|
|
|
|
? copy.subscriptionChecking
|
|
|
|
|
|
: isSubscribed
|
|
|
|
|
|
? copy.enabled
|
|
|
|
|
|
: copy.locked
|
|
|
|
|
|
}
|
2026-03-13 15:39:25 +08:00
|
|
|
|
isPrimary={isSubscribed}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</section>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<section className="space-y-3 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
|
|
|
|
|
|
<h3 className="mb-4 text-sm font-bold uppercase text-indigo-700">
|
2026-03-16 20:30:46 +08:00
|
|
|
|
{copy.identityStatus}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</h3>
|
2026-03-21 12:34:36 +08:00
|
|
|
|
<InfoRow
|
|
|
|
|
|
icon={Mail}
|
|
|
|
|
|
label={copy.boundEmail}
|
|
|
|
|
|
value={email || "--"}
|
|
|
|
|
|
/>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<InfoRow
|
|
|
|
|
|
icon={LogIn}
|
2026-03-16 20:30:46 +08:00
|
|
|
|
label={copy.loginMethod}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
value={user?.app_metadata?.provider?.toUpperCase() || "GOOGLE"}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<InfoRow
|
|
|
|
|
|
icon={Clock}
|
2026-04-13 16:35:22 +08:00
|
|
|
|
label={expiryLabel}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
value={proExpiry}
|
|
|
|
|
|
isPrimary
|
|
|
|
|
|
/>
|
|
|
|
|
|
<InfoRow
|
|
|
|
|
|
icon={UserCheck}
|
2026-03-16 20:30:46 +08:00
|
|
|
|
label={copy.authResult}
|
2026-05-27 12:14:07 +08:00
|
|
|
|
value={
|
|
|
|
|
|
backendAuthenticated
|
|
|
|
|
|
? copy.passed
|
|
|
|
|
|
: isSubscriptionUnknown
|
|
|
|
|
|
? copy.subscriptionChecking
|
|
|
|
|
|
: copy.restricted
|
|
|
|
|
|
}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
/>
|
2026-04-13 16:35:22 +08:00
|
|
|
|
{queuedExtensionSummary ? (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="rounded-xl border border-cyan-200 bg-cyan-50 px-4 py-3 text-xs text-cyan-800">
|
2026-04-13 16:35:22 +08:00
|
|
|
|
{queuedExtensionSummary}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
2026-03-13 03:47:56 +08:00
|
|
|
|
|
2026-03-13 08:15:27 +08:00
|
|
|
|
{/* Paywall Mask */}
|
2026-04-10 09:00:37 +08:00
|
|
|
|
{canOpenCheckoutOverlay && showOverlay && (
|
2026-03-13 08:15:27 +08:00
|
|
|
|
<div className="absolute inset-0 z-30 flex items-center justify-center p-4">
|
2026-03-13 08:51:06 +08:00
|
|
|
|
<UnlockProOverlay
|
|
|
|
|
|
points={totalPoints}
|
|
|
|
|
|
planPriceUsd={billing.planAmount}
|
|
|
|
|
|
usePoints={usePoints}
|
|
|
|
|
|
onToggleUsePoints={() => setUsePoints((prev) => !prev)}
|
|
|
|
|
|
billing={{
|
|
|
|
|
|
pointsEnabled: billing.pointsEnabled,
|
|
|
|
|
|
isEligible: billing.canRedeem,
|
|
|
|
|
|
pointsUsed: billing.pointsUsed,
|
|
|
|
|
|
discountAmount: billing.discountAmount,
|
|
|
|
|
|
finalPrice: billing.payAmount,
|
|
|
|
|
|
maxDiscountUsd: billing.maxDiscountUsdc,
|
|
|
|
|
|
pointsPerUsd: billing.pointsPerUsdc,
|
|
|
|
|
|
}}
|
|
|
|
|
|
onPay={() => void handleOverlayCheckout()}
|
2026-05-18 16:18:26 +08:00
|
|
|
|
onManualPay={() => void createManualPaymentIntent()}
|
2026-03-13 08:51:06 +08:00
|
|
|
|
onClose={() => setShowOverlay(false)}
|
|
|
|
|
|
payBusy={paymentBusy}
|
2026-03-21 12:34:36 +08:00
|
|
|
|
payLabel={hasPayingWallet ? copy.payNow : copy.connectAndPay}
|
2026-05-18 16:18:26 +08:00
|
|
|
|
manualPayLabel="手动转账"
|
2026-03-13 08:51:06 +08:00
|
|
|
|
errorText={paymentError || undefined}
|
|
|
|
|
|
infoText={paymentInfo || undefined}
|
2026-03-13 13:13:51 +08:00
|
|
|
|
txHash={lastTxHash || undefined}
|
2026-05-29 01:57:42 +08:00
|
|
|
|
chainId={selectedPaymentChainId || paymentConfig?.chain_id || 137}
|
2026-03-13 13:58:41 +08:00
|
|
|
|
paymentTokenLabel={selectedTokenLabel}
|
2026-03-13 20:38:10 +08:00
|
|
|
|
faqHref={SUBSCRIPTION_HELP_HREF}
|
2026-05-19 18:26:45 +08:00
|
|
|
|
telegramGroupUrl=""
|
2026-03-13 08:51:06 +08:00
|
|
|
|
/>
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-03-13 07:31:47 +08:00
|
|
|
|
|
2026-05-20 09:59:27 +08:00
|
|
|
|
{/* Telegram Bot Section & Payment Details */}
|
|
|
|
|
|
{showSecondarySections ? (
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<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)]" : ""
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
2026-05-29 19:24:46 +08:00
|
|
|
|
{isTrialSubscription && (
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<section className="group relative min-w-0 overflow-hidden rounded-2xl border border-amber-200 bg-amber-50 p-8 shadow-sm">
|
2026-05-29 19:24:46 +08:00
|
|
|
|
<Bot
|
|
|
|
|
|
size={140}
|
|
|
|
|
|
className="absolute -right-8 -bottom-8 -rotate-12 text-amber-100"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="relative z-10">
|
|
|
|
|
|
<h3 className="mb-2 flex items-center gap-2 text-lg font-bold text-amber-800">
|
|
|
|
|
|
<Bot size={22} /> {copy.telegramBind}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p className="text-sm leading-6 text-amber-900">
|
|
|
|
|
|
{copy.trialPaidGroupLocked}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowOverlay(true)}
|
|
|
|
|
|
disabled={!canOpenCheckoutOverlay}
|
|
|
|
|
|
className="mt-5 inline-flex items-center gap-2 rounded-xl border border-amber-700 bg-amber-600 px-4 py-3 text-xs font-bold text-white hover:bg-amber-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Crown size={14} />
|
|
|
|
|
|
{copy.upgradePro}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
)}
|
2026-05-20 09:59:27 +08:00
|
|
|
|
{canAccessPaidTelegramGroup && (
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<section className="group relative min-w-0 overflow-hidden rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
|
2026-05-25 02:38:20 +08:00
|
|
|
|
<Bot
|
|
|
|
|
|
size={140}
|
|
|
|
|
|
className="absolute -right-8 -bottom-8 -rotate-12 text-slate-100 transition-transform duration-1000 group-hover:rotate-0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="relative z-10">
|
|
|
|
|
|
<h3 className="mb-2 flex items-center gap-2 text-lg font-bold text-blue-700">
|
|
|
|
|
|
<Bot size={22} /> {copy.telegramBind}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p className="mb-6 text-sm text-slate-500">
|
|
|
|
|
|
{copy.telegramHint}
|
|
|
|
|
|
</p>
|
2026-05-17 17:05:47 +08:00
|
|
|
|
|
2026-05-25 02:38:20 +08:00
|
|
|
|
<div className="mb-4 flex flex-wrap gap-2">
|
|
|
|
|
|
{TELEGRAM_TOPICS_GROUP_URL &&
|
|
|
|
|
|
TELEGRAM_TOPICS_GROUP_URL !== TELEGRAM_GROUP_URL &&
|
|
|
|
|
|
telegramBound ? (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={TELEGRAM_TOPICS_GROUP_URL}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
className="inline-flex min-h-9 items-center gap-1 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-semibold text-emerald-700 hover:bg-emerald-100"
|
|
|
|
|
|
>
|
|
|
|
|
|
{copy.telegramTopicsGroupLink}
|
|
|
|
|
|
<ExternalLink size={12} />
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
{TELEGRAM_GROUP_URL && telegramBound ? (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={TELEGRAM_GROUP_URL}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
|
className="inline-flex min-h-9 items-center gap-1 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs font-semibold text-blue-700 hover:bg-blue-100"
|
|
|
|
|
|
>
|
|
|
|
|
|
{copy.telegramGroupLink}
|
|
|
|
|
|
<ExternalLink size={12} />
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<code className="flex-grow overflow-hidden text-ellipsis whitespace-nowrap rounded-xl border border-slate-200 bg-slate-50 p-4 font-mono text-xs text-blue-700">
|
|
|
|
|
|
{bindCommand}
|
|
|
|
|
|
</code>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => void openTelegramBotBindLink()}
|
|
|
|
|
|
disabled={telegramBindOpening || !isAuthenticated}
|
|
|
|
|
|
className="rounded-xl border border-cyan-700 bg-cyan-600 px-4 py-3 text-xs font-bold text-white shadow-sm transition-all hover:bg-cyan-700 disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
|
title={copy.telegramBotBindLink}
|
|
|
|
|
|
aria-label={copy.telegramBotBindLink}
|
2026-05-19 17:15:16 +08:00
|
|
|
|
>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
{telegramBindOpening
|
|
|
|
|
|
? "..."
|
|
|
|
|
|
: copy.telegramBotBindLink}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => handleCopy(bindCommand)}
|
|
|
|
|
|
className="rounded-xl border border-blue-700 bg-blue-600 p-4 text-white shadow-sm transition-all hover:bg-blue-700"
|
|
|
|
|
|
title={copy.copyCommand}
|
|
|
|
|
|
aria-label={copy.copyCommand}
|
2026-03-21 12:34:36 +08:00
|
|
|
|
>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
{copied ? (
|
|
|
|
|
|
<CheckCircle2 size={20} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Copy size={20} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="mt-2 text-[11px] leading-5 text-slate-400">
|
|
|
|
|
|
{copy.telegramFallbackHint}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="mt-5 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs leading-6 text-amber-800">
|
|
|
|
|
|
{copy.paymentManualSupport}
|
|
|
|
|
|
</div>
|
2026-03-22 20:24:48 +08:00
|
|
|
|
</div>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
</section>
|
2026-05-20 09:59:27 +08:00
|
|
|
|
)}
|
2026-03-21 12:34:36 +08:00
|
|
|
|
|
|
|
|
|
|
{/* Payment Details / Wallet Management */}
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<section className="w-full rounded-2xl border border-slate-200 bg-white p-6 shadow-sm lg:p-7">
|
2026-03-21 12:34:36 +08:00
|
|
|
|
<div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<h3 className="mb-6 flex items-center gap-2 text-sm font-bold uppercase text-blue-700">
|
2026-03-21 12:34:36 +08:00
|
|
|
|
<Wallet size={18} /> {copy.paymentMgmt}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
{paymentError ? (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="mb-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-[11px] text-red-700">
|
2026-03-21 12:34:36 +08:00
|
|
|
|
{paymentError}
|
|
|
|
|
|
</div>
|
2026-03-17 19:18:37 +08:00
|
|
|
|
) : null}
|
2026-03-21 12:34:36 +08:00
|
|
|
|
{!paymentError && paymentInfo ? (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="mb-4 rounded-xl border border-cyan-200 bg-cyan-50 px-3 py-2 text-[11px] text-cyan-800">
|
2026-03-21 12:34:36 +08:00
|
|
|
|
{paymentInfo}
|
2026-05-24 18:55:52 +08:00
|
|
|
|
{telegramBindUrl ? (
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={telegramBindUrl}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
rel="noopener noreferrer"
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="mt-1 block break-all text-cyan-700 underline hover:text-cyan-900"
|
2026-05-24 18:55:52 +08:00
|
|
|
|
>
|
|
|
|
|
|
{telegramBindUrl}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
) : null}
|
2026-03-21 12:34:36 +08:00
|
|
|
|
</div>
|
2026-03-17 19:18:37 +08:00
|
|
|
|
) : null}
|
2026-03-22 13:42:48 +08:00
|
|
|
|
{!paymentHostAllowed ? (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="mb-4 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
|
2026-03-22 13:42:48 +08:00
|
|
|
|
{copy.paymentHostBlocked.replace(
|
|
|
|
|
|
"{host}",
|
2026-05-25 02:38:20 +08:00
|
|
|
|
allowedPaymentHosts[0] ||
|
2026-05-26 00:33:02 +08:00
|
|
|
|
"polyweather.top",
|
2026-03-22 13:42:48 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<div
|
|
|
|
|
|
data-testid="payment-management-grid"
|
2026-05-29 21:34:36 +08:00
|
|
|
|
className={`grid gap-6 lg:items-start ${
|
|
|
|
|
|
hasTelegramPanel ? "" : "lg:grid-cols-[minmax(0,1fr)_minmax(320px,380px)]"
|
|
|
|
|
|
}`}
|
2026-05-29 21:23:12 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div className="space-y-5">
|
2026-05-29 19:24:46 +08:00
|
|
|
|
<div>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<p className="mb-2 text-[11px] uppercase text-slate-500">
|
|
|
|
|
|
{copy.proPlan}
|
2026-05-29 19:24:46 +08:00
|
|
|
|
</p>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-05-29 19:24:46 +08:00
|
|
|
|
</div>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
{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}
|
2026-05-29 19:24:46 +08:00
|
|
|
|
</div>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<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>
|
2026-05-29 19:24:46 +08:00
|
|
|
|
</div>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<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}
|
2026-05-29 01:57:42 +08:00
|
|
|
|
</div>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<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>
|
2026-05-29 01:57:42 +08:00
|
|
|
|
</div>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
)}
|
|
|
|
|
|
<p className="mt-2 text-[11px] leading-5 text-slate-500">
|
|
|
|
|
|
{copy.referralDiscountHint}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-29 01:57:42 +08:00
|
|
|
|
</div>
|
2026-05-29 21:34:36 +08:00
|
|
|
|
<div
|
|
|
|
|
|
data-testid="payment-guard-grid"
|
|
|
|
|
|
className={`grid gap-3 ${hasTelegramPanel ? "" : "sm:grid-cols-2"}`}
|
|
|
|
|
|
>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<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>
|
2026-03-21 12:34:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
|
2026-05-29 21:23:12 +08:00
|
|
|
|
{/* 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}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</p>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
<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>
|
2026-05-20 20:00:32 +08:00
|
|
|
|
</div>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
|
2026-05-29 21:23:12 +08:00
|
|
|
|
{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>
|
|
|
|
|
|
|
2026-05-20 08:33:59 +08:00
|
|
|
|
{boundWallets.length ? (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{boundWallets.map((w) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={w.address}
|
|
|
|
|
|
className={`p-3 rounded-xl border transition-all ${
|
|
|
|
|
|
selectedWallet === w.address
|
2026-05-24 23:07:37 +08:00
|
|
|
|
? "border-blue-300 bg-blue-50 text-blue-900"
|
|
|
|
|
|
: "border-slate-200 bg-white text-slate-600"
|
2026-05-20 08:33:59 +08:00
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center justify-between mb-1">
|
|
|
|
|
|
<span className="text-[10px] font-mono">
|
|
|
|
|
|
{shortAddress(w.address)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{w.is_primary && (
|
|
|
|
|
|
<span className="text-[8px] bg-blue-500 px-1 rounded text-white font-semibold">
|
|
|
|
|
|
{copy.primary}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
<div className="text-[10px]">
|
2026-05-29 01:57:42 +08:00
|
|
|
|
{chainIdToDisplayName(w.chain_id)}
|
2026-05-25 02:38:20 +08:00
|
|
|
|
</div>
|
2026-05-20 08:33:59 +08:00
|
|
|
|
<div className="mt-2 flex justify-end">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-05-25 02:38:20 +08:00
|
|
|
|
onClick={() =>
|
|
|
|
|
|
void handleUnbindWallet(
|
|
|
|
|
|
w.address,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
disabled={paymentBusy}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="inline-flex items-center gap-1 rounded-md border border-red-200 bg-red-50 px-2 py-1 text-[10px] font-semibold text-red-700 transition-all hover:bg-red-100 disabled:opacity-50"
|
2026-05-20 08:33:59 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Minus size={12} />
|
|
|
|
|
|
{copy.unbind}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
2026-05-18 16:18:26 +08:00
|
|
|
|
</div>
|
2026-05-20 08:33:59 +08:00
|
|
|
|
) : (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4 text-center">
|
2026-05-20 08:33:59 +08:00
|
|
|
|
<p className="text-xs text-slate-400 italic">
|
|
|
|
|
|
{copy.noWallet}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 gap-2 pt-2">
|
|
|
|
|
|
{injectedProviderOptions.length > 1 && (
|
|
|
|
|
|
<label className="mb-2 block">
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<span className="mb-2 block text-[11px] uppercase text-slate-500">
|
2026-05-20 08:33:59 +08:00
|
|
|
|
{copy.walletExtensionDetected}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={selectedInjectedProviderKey}
|
|
|
|
|
|
onChange={(event) =>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
setSelectedInjectedProviderKey(
|
|
|
|
|
|
event.target.value,
|
|
|
|
|
|
)
|
2026-05-20 08:33:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
disabled={paymentBusy}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="w-full rounded-xl border border-slate-300 bg-white px-3 py-3 text-xs text-slate-900 outline-none transition-all hover:bg-slate-50 disabled:opacity-60"
|
2026-05-20 08:33:59 +08:00
|
|
|
|
>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
{injectedProviderOptions.map(
|
|
|
|
|
|
(option) => (
|
|
|
|
|
|
<option
|
|
|
|
|
|
key={option.key}
|
|
|
|
|
|
value={option.key}
|
|
|
|
|
|
className="bg-white text-slate-900"
|
|
|
|
|
|
>
|
|
|
|
|
|
{option.label}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
),
|
|
|
|
|
|
)}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setProviderMode("auto");
|
|
|
|
|
|
void connectAndBindWallet("auto");
|
|
|
|
|
|
}}
|
2026-05-25 02:38:20 +08:00
|
|
|
|
disabled={
|
|
|
|
|
|
paymentBusy || !isAuthenticated
|
|
|
|
|
|
}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="flex w-full items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white py-3 text-xs font-bold text-slate-700 transition-all hover:bg-slate-50 disabled:opacity-60"
|
2026-05-20 08:33:59 +08:00
|
|
|
|
>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
<PlusIcon className="w-4 h-4" />{" "}
|
|
|
|
|
|
{copy.bindExt}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setProviderMode("walletconnect");
|
2026-05-25 02:38:20 +08:00
|
|
|
|
void connectAndBindWallet(
|
|
|
|
|
|
"walletconnect",
|
|
|
|
|
|
);
|
2026-05-20 08:33:59 +08:00
|
|
|
|
}}
|
|
|
|
|
|
disabled={
|
2026-05-25 02:38:20 +08:00
|
|
|
|
paymentBusy ||
|
|
|
|
|
|
!isAuthenticated ||
|
|
|
|
|
|
!walletConnectEnabled
|
2026-05-19 17:47:51 +08:00
|
|
|
|
}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="flex w-full items-center justify-center gap-2 rounded-xl border border-cyan-200 bg-cyan-50 py-3 text-xs font-bold text-cyan-700 transition-all hover:bg-cyan-100 disabled:opacity-60"
|
2026-05-20 08:33:59 +08:00
|
|
|
|
>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
<CreditCard className="w-4 h-4" />{" "}
|
|
|
|
|
|
{copy.bindQr}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
{!walletConnectEnabled && (
|
|
|
|
|
|
<p className="text-[11px] text-slate-500 text-center mt-1">
|
|
|
|
|
|
{copy.walletConnectMissing}
|
|
|
|
|
|
<code className="mx-1">
|
|
|
|
|
|
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID
|
|
|
|
|
|
</code>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
2026-05-18 16:18:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-20 08:33:59 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<p className="text-[11px] leading-relaxed text-slate-400">
|
2026-05-21 12:25:57 +08:00
|
|
|
|
{copy.paymentManualDesc}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</p>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
|
2026-05-20 08:33:59 +08:00
|
|
|
|
<div className="flex flex-col gap-3">
|
|
|
|
|
|
<div>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="text-xs font-bold text-emerald-800">
|
2026-05-21 12:25:57 +08:00
|
|
|
|
{copy.paymentManualTitle}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</p>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="mt-1 text-[11px] leading-relaxed text-emerald-700">
|
2026-05-21 12:25:57 +08:00
|
|
|
|
{copy.paymentManualHint}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-03-21 12:34:36 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-05-25 02:38:20 +08:00
|
|
|
|
onClick={() =>
|
|
|
|
|
|
void createManualPaymentIntent()
|
|
|
|
|
|
}
|
|
|
|
|
|
disabled={
|
|
|
|
|
|
paymentBusy || !isAuthenticated
|
|
|
|
|
|
}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="w-full rounded-xl border border-emerald-700 bg-emerald-600 py-2.5 text-xs font-bold text-white transition-all hover:bg-emerald-700 disabled:opacity-50"
|
2026-03-21 12:34:36 +08:00
|
|
|
|
>
|
2026-05-21 12:25:57 +08:00
|
|
|
|
{copy.paymentManualCreate}
|
2026-03-21 12:34:36 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-05-20 08:33:59 +08:00
|
|
|
|
{manualPayment ? (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="mt-4 space-y-3 rounded-xl border border-emerald-200 bg-white p-3">
|
2026-05-20 08:33:59 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-[10px] uppercase tracking-widest text-slate-500">
|
2026-05-21 12:25:57 +08:00
|
|
|
|
{copy.paymentAmount}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</p>
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="font-mono text-sm font-bold text-slate-950">
|
2026-05-20 08:33:59 +08:00
|
|
|
|
{manualPayment.amount_usdc}{" "}
|
2026-05-25 02:38:20 +08:00
|
|
|
|
{manualPayment.token_symbol ||
|
|
|
|
|
|
selectedTokenLabel}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-[10px] uppercase tracking-widest text-slate-500">
|
2026-05-21 12:25:57 +08:00
|
|
|
|
{copy.paymentReceiverLabel}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
<div className="mt-1 flex gap-2">
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<code className="min-w-0 flex-1 break-all whitespace-normal rounded-lg border border-blue-200 bg-blue-50 px-2 py-2 font-mono text-[11px] text-blue-800">
|
2026-05-25 02:38:20 +08:00
|
|
|
|
{
|
|
|
|
|
|
manualPayment.receiver_address
|
|
|
|
|
|
}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</code>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() =>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
handleCopy(
|
|
|
|
|
|
manualPayment.receiver_address,
|
|
|
|
|
|
)
|
2026-05-20 08:33:59 +08:00
|
|
|
|
}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="rounded-lg border border-blue-700 bg-blue-600 px-2 text-xs font-bold text-white transition-colors hover:bg-blue-700"
|
2026-05-20 08:33:59 +08:00
|
|
|
|
>
|
2026-05-21 12:25:57 +08:00
|
|
|
|
{copy.paymentCopyAddress}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-[10px] uppercase tracking-widest text-slate-500">
|
|
|
|
|
|
Tx Hash
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<input
|
|
|
|
|
|
value={manualTxHash}
|
2026-05-22 04:44:14 +08:00
|
|
|
|
onChange={(event) => {
|
2026-05-25 02:38:20 +08:00
|
|
|
|
setManualTxHash(
|
|
|
|
|
|
event.target.value,
|
|
|
|
|
|
);
|
2026-05-22 04:44:14 +08:00
|
|
|
|
void validateTxHash(
|
|
|
|
|
|
manualPayment.intent_id ||
|
|
|
|
|
|
lastIntentId ||
|
|
|
|
|
|
"",
|
|
|
|
|
|
event.target.value,
|
|
|
|
|
|
);
|
|
|
|
|
|
}}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
placeholder="0x..."
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-950 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20"
|
2026-05-20 08:33:59 +08:00
|
|
|
|
/>
|
2026-05-22 04:44:14 +08:00
|
|
|
|
{txValidation.loading ? (
|
|
|
|
|
|
<p className="mt-1 text-[10px] text-slate-500">
|
2026-05-25 02:38:20 +08:00
|
|
|
|
{copy.verifying}
|
2026-05-22 04:44:14 +08:00
|
|
|
|
</p>
|
2026-05-25 02:38:20 +08:00
|
|
|
|
) : txValidation.checked &&
|
|
|
|
|
|
txValidation.valid ? (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="mt-1 text-[10px] text-emerald-700">
|
2026-05-25 02:38:20 +08:00
|
|
|
|
{copy.verifyAddressMatch}
|
2026-05-22 04:44:14 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
) : txValidation.checked &&
|
2026-05-25 02:38:20 +08:00
|
|
|
|
txValidation.valid ===
|
|
|
|
|
|
false ? (
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<p className="mt-1 text-[10px] text-red-700">
|
2026-05-22 04:44:14 +08:00
|
|
|
|
{txValidation.reason ===
|
|
|
|
|
|
"tx_not_mined"
|
2026-05-25 01:59:31 +08:00
|
|
|
|
? copy.verifyTxNotMined
|
2026-05-25 02:38:20 +08:00
|
|
|
|
: txValidation.reason ===
|
|
|
|
|
|
"receiver_mismatch"
|
|
|
|
|
|
? copy
|
|
|
|
|
|
.verifyAddressMismatch
|
2026-05-22 04:44:14 +08:00
|
|
|
|
: txValidation.reason ===
|
|
|
|
|
|
"amount_insufficient"
|
2026-05-25 02:38:20 +08:00
|
|
|
|
? copy
|
|
|
|
|
|
.verifyAmountLow
|
|
|
|
|
|
: txValidation.reason ===
|
|
|
|
|
|
"tx_reverted"
|
|
|
|
|
|
? copy
|
|
|
|
|
|
.verifyTxReverted
|
2026-05-22 04:44:14 +08:00
|
|
|
|
: txValidation.detail ||
|
2026-05-25 01:59:31 +08:00
|
|
|
|
copy.verifyFailed +
|
2026-05-22 04:44:14 +08:00
|
|
|
|
(txValidation.reason ||
|
2026-05-25 02:38:20 +08:00
|
|
|
|
copy
|
|
|
|
|
|
.verifyUnknown)}
|
2026-05-22 04:44:14 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
) : null}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-05-25 02:38:20 +08:00
|
|
|
|
onClick={() =>
|
|
|
|
|
|
void submitManualPaymentTx()
|
|
|
|
|
|
}
|
2026-05-22 04:44:14 +08:00
|
|
|
|
disabled={
|
|
|
|
|
|
paymentBusy ||
|
2026-05-29 12:11:19 +08:00
|
|
|
|
!(txValidation.checked &&
|
|
|
|
|
|
txValidation.valid === true)
|
2026-05-22 04:44:14 +08:00
|
|
|
|
}
|
2026-05-24 23:07:37 +08:00
|
|
|
|
className="w-full rounded-xl border border-emerald-700 bg-emerald-600 px-3 py-2 text-xs font-bold text-white transition-all hover:bg-emerald-700 disabled:opacity-50"
|
2026-05-20 08:33:59 +08:00
|
|
|
|
>
|
2026-05-21 12:25:57 +08:00
|
|
|
|
{copy.paymentManualSubmit}
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
2026-03-21 12:34:36 +08:00
|
|
|
|
</div>
|
2026-05-20 08:33:59 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-03-21 12:34:36 +08:00
|
|
|
|
</div>
|
2026-05-29 21:23:12 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-21 12:34:36 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
2026-03-18 20:38:43 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<div className="lg:col-span-12 grid grid-cols-1 gap-6 md:grid-cols-3">
|
2026-05-24 23:07:37 +08:00
|
|
|
|
<div className="h-48 animate-pulse rounded-2xl border border-slate-200 bg-white md:col-span-2" />
|
|
|
|
|
|
<div className="h-48 animate-pulse rounded-2xl border border-slate-200 bg-white" />
|
2026-03-18 20:38:43 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<footer className="mt-16 text-center text-slate-600 text-[10px] uppercase tracking-[0.3em] font-mono z-10 pb-8">
|
2026-05-25 01:59:31 +08:00
|
|
|
|
copy.footerEngine
|
2026-03-13 08:15:27 +08:00
|
|
|
|
</footer>
|
2026-03-13 03:47:56 +08:00
|
|
|
|
</div>
|
2026-03-13 02:23:01 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|