Add EIP-6963 wallet discovery for injected providers
This commit is contained in:
@@ -183,6 +183,18 @@ type InjectedProviderOption = ProviderSelection & {
|
|||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Eip6963ProviderInfo = {
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
rdns: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Eip6963ProviderDetail = {
|
||||||
|
info: Eip6963ProviderInfo;
|
||||||
|
provider: EvmProvider;
|
||||||
|
};
|
||||||
|
|
||||||
type ConnectBindOptions = {
|
type ConnectBindOptions = {
|
||||||
openOverlayAfterBind?: boolean;
|
openOverlayAfterBind?: boolean;
|
||||||
};
|
};
|
||||||
@@ -205,6 +217,7 @@ const SUBSCRIPTION_HELP_HREF = "/subscription-help";
|
|||||||
|
|
||||||
let walletConnectProviderCache: EvmProvider | null = null;
|
let walletConnectProviderCache: EvmProvider | null = null;
|
||||||
let walletConnectProviderChainId: number | null = null;
|
let walletConnectProviderChainId: number | null = null;
|
||||||
|
const eip6963Providers = new Map<string, Eip6963ProviderDetail>();
|
||||||
|
|
||||||
function isWalletConnectResetError(error: unknown): boolean {
|
function isWalletConnectResetError(error: unknown): boolean {
|
||||||
const source = error as any;
|
const source = error as any;
|
||||||
@@ -293,6 +306,50 @@ function getEvmProvider(): EvmProvider | null {
|
|||||||
return listInjectedProviders()[0]?.provider || null;
|
return listInjectedProviders()[0]?.provider || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEip6963Providers(): Eip6963ProviderDetail[] {
|
||||||
|
return Array.from(eip6963Providers.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectWalletLabel(
|
||||||
|
provider: EvmProvider | null,
|
||||||
|
detail?: Eip6963ProviderDetail,
|
||||||
|
): string {
|
||||||
|
if (!provider && !detail) return "EVM 钱包";
|
||||||
|
const announcedName = String(detail?.info?.name || "").trim();
|
||||||
|
const announcedRdns = String(detail?.info?.rdns || "").toLowerCase();
|
||||||
|
if (
|
||||||
|
provider?.isOkxWallet ||
|
||||||
|
announcedName.toLowerCase().includes("okx") ||
|
||||||
|
announcedRdns.includes("okx")
|
||||||
|
) {
|
||||||
|
return "OKX Wallet";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
provider?.isMetaMask ||
|
||||||
|
announcedName.toLowerCase().includes("metamask") ||
|
||||||
|
announcedRdns.includes("metamask")
|
||||||
|
) {
|
||||||
|
return "MetaMask";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
provider?.isRabby ||
|
||||||
|
announcedName.toLowerCase().includes("rabby") ||
|
||||||
|
announcedRdns.includes("rabby")
|
||||||
|
) {
|
||||||
|
return "Rabby";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
provider?.isBitKeep ||
|
||||||
|
announcedName.toLowerCase().includes("bitget") ||
|
||||||
|
announcedRdns.includes("bitkeep") ||
|
||||||
|
announcedRdns.includes("bitget")
|
||||||
|
) {
|
||||||
|
return "Bitget Wallet";
|
||||||
|
}
|
||||||
|
if (announcedName) return announcedName;
|
||||||
|
return "EVM 钱包";
|
||||||
|
}
|
||||||
|
|
||||||
function collectInjectedProviders(): EvmProvider[] {
|
function collectInjectedProviders(): EvmProvider[] {
|
||||||
if (typeof window === "undefined") return [];
|
if (typeof window === "undefined") return [];
|
||||||
const out: EvmProvider[] = [];
|
const out: EvmProvider[] = [];
|
||||||
@@ -320,21 +377,46 @@ function collectInjectedProviders(): EvmProvider[] {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInjectedProviderStableId(provider: EvmProvider, index: number): string {
|
function getInjectedProviderStableId(
|
||||||
if (provider.isOkxWallet) return `okx:${index}`;
|
provider: EvmProvider,
|
||||||
if (provider.isMetaMask) return `metamask:${index}`;
|
index: number,
|
||||||
if (provider.isRabby) return `rabby:${index}`;
|
detail?: Eip6963ProviderDetail,
|
||||||
if (provider.isBitKeep) return `bitget:${index}`;
|
): string {
|
||||||
|
const uuid = String(detail?.info?.uuid || "").trim();
|
||||||
|
const rdns = String(detail?.info?.rdns || "").toLowerCase();
|
||||||
|
if (uuid) return `eip6963:${uuid}`;
|
||||||
|
if (provider.isOkxWallet || rdns.includes("okx")) return `okx:${index}`;
|
||||||
|
if (provider.isMetaMask || rdns.includes("metamask")) return `metamask:${index}`;
|
||||||
|
if (provider.isRabby || rdns.includes("rabby")) return `rabby:${index}`;
|
||||||
|
if (
|
||||||
|
provider.isBitKeep ||
|
||||||
|
rdns.includes("bitkeep") ||
|
||||||
|
rdns.includes("bitget")
|
||||||
|
) {
|
||||||
|
return `bitget:${index}`;
|
||||||
|
}
|
||||||
return `evm:${index}`;
|
return `evm:${index}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function listInjectedProviders(): InjectedProviderOption[] {
|
function listInjectedProviders(): InjectedProviderOption[] {
|
||||||
|
const detailByProvider = new Map<EvmProvider, Eip6963ProviderDetail>();
|
||||||
|
getEip6963Providers().forEach((detail) => {
|
||||||
|
if (detail?.provider && typeof detail.provider.request === "function") {
|
||||||
|
detailByProvider.set(detail.provider, detail);
|
||||||
|
}
|
||||||
|
});
|
||||||
const candidates = collectInjectedProviders();
|
const candidates = collectInjectedProviders();
|
||||||
|
detailByProvider.forEach((_detail, provider) => {
|
||||||
|
if (!candidates.includes(provider)) {
|
||||||
|
candidates.push(provider);
|
||||||
|
}
|
||||||
|
});
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const out: InjectedProviderOption[] = [];
|
const out: InjectedProviderOption[] = [];
|
||||||
candidates.forEach((provider, index) => {
|
candidates.forEach((provider, index) => {
|
||||||
const label = getEvmWalletLabel(provider);
|
const detail = detailByProvider.get(provider);
|
||||||
const key = getInjectedProviderStableId(provider, index);
|
const label = detectWalletLabel(provider, detail);
|
||||||
|
const key = getInjectedProviderStableId(provider, index, detail);
|
||||||
if (seen.has(key)) return;
|
if (seen.has(key)) return;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
out.push({
|
out.push({
|
||||||
@@ -348,12 +430,7 @@ function listInjectedProviders(): InjectedProviderOption[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getEvmWalletLabel(provider: EvmProvider | null): string {
|
function getEvmWalletLabel(provider: EvmProvider | null): string {
|
||||||
if (!provider) return "EVM 钱包";
|
return detectWalletLabel(provider);
|
||||||
if (provider.isMetaMask) return "MetaMask";
|
|
||||||
if (provider.isRabby) return "Rabby";
|
|
||||||
if (provider.isOkxWallet) return "OKX Wallet";
|
|
||||||
if (provider.isBitKeep) return "Bitget Wallet";
|
|
||||||
return "EVM 钱包";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWalletConnectProvider(
|
async function getWalletConnectProvider(
|
||||||
@@ -476,7 +553,10 @@ function normalizePaymentError(error: unknown): NormalizedPaymentError {
|
|||||||
?.trim();
|
?.trim();
|
||||||
const lower = String(rawMessage || "").toLowerCase();
|
const lower = String(rawMessage || "").toLowerCase();
|
||||||
|
|
||||||
if (lower.includes("confirm pending") || lower.includes("payment pending timeout")) {
|
if (
|
||||||
|
lower.includes("confirm pending") ||
|
||||||
|
lower.includes("payment pending timeout")
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
message: "链上交易已提交,正在确认中,请稍后刷新查看状态。",
|
message: "链上交易已提交,正在确认中,请稍后刷新查看状态。",
|
||||||
pending: true,
|
pending: true,
|
||||||
@@ -569,12 +649,15 @@ export function AccountCenter() {
|
|||||||
authMode: isEn ? "Auth Mode" : "鉴权模式",
|
authMode: isEn ? "Auth Mode" : "鉴权模式",
|
||||||
weatherEngine: isEn ? "Weather Engine" : "气象引擎",
|
weatherEngine: isEn ? "Weather Engine" : "气象引擎",
|
||||||
intradayAnalysis: isEn ? "Intraday Analysis" : "今日内分析",
|
intradayAnalysis: isEn ? "Intraday Analysis" : "今日内分析",
|
||||||
historyFuture:
|
historyFuture: isEn
|
||||||
isEn ? "Historical + Future-date Analysis" : "历史对账 + 未来日期分析",
|
? "Historical + Future-date Analysis"
|
||||||
smartPush:
|
: "历史对账 + 未来日期分析",
|
||||||
isEn ? "Cross-platform Smart Weather Push" : "全平台智能气象推送",
|
smartPush: isEn
|
||||||
deepMode:
|
? "Cross-platform Smart Weather Push"
|
||||||
isEn ? "Deep mode (incl. high-temp window)" : "深度版(含高温时段)",
|
: "全平台智能气象查询",
|
||||||
|
deepMode: isEn
|
||||||
|
? "Deep mode (incl. high-temp window)"
|
||||||
|
: "深度版(含高温时段)",
|
||||||
compactVisible: isEn ? "Compact visible" : "简版可见",
|
compactVisible: isEn ? "Compact visible" : "简版可见",
|
||||||
enabled: isEn ? "Enabled" : "已开启",
|
enabled: isEn ? "Enabled" : "已开启",
|
||||||
locked: isEn ? "Locked" : "锁定",
|
locked: isEn ? "Locked" : "锁定",
|
||||||
@@ -587,7 +670,7 @@ export function AccountCenter() {
|
|||||||
telegramBind: isEn ? "Telegram Bot Binding" : "Telegram Bot 绑定",
|
telegramBind: isEn ? "Telegram Bot Binding" : "Telegram Bot 绑定",
|
||||||
telegramHint: isEn
|
telegramHint: isEn
|
||||||
? "Send the command below to the polyweather bot to sync notifications and access."
|
? "Send the command below to the polyweather bot to sync notifications and access."
|
||||||
: "将下方命令发送给polyweather机器人,实现全平台气象推送与权限同步。",
|
: "将下方命令发送给polyweather机器人,实现全平台气象查询与权限同步。",
|
||||||
telegramBotLink: isEn
|
telegramBotLink: isEn
|
||||||
? "Open Bot (@WeatherQuant_bot)"
|
? "Open Bot (@WeatherQuant_bot)"
|
||||||
: "打开机器人 (@WeatherQuant_bot)",
|
: "打开机器人 (@WeatherQuant_bot)",
|
||||||
@@ -598,9 +681,12 @@ export function AccountCenter() {
|
|||||||
primary: "Primary",
|
primary: "Primary",
|
||||||
polygonChain: "Polygon Chain",
|
polygonChain: "Polygon Chain",
|
||||||
noWallet: isEn ? "No payout wallet bound yet." : "未绑定任何收件钱包",
|
noWallet: isEn ? "No payout wallet bound yet." : "未绑定任何收件钱包",
|
||||||
bindExt:
|
bindExt: isEn
|
||||||
isEn ? "Bind Browser Wallet (EVM Extension)" : "绑定浏览器钱包(EVM扩展)",
|
? "Bind Browser Wallet (EVM Extension)"
|
||||||
bindQr: isEn ? "Bind via QR (WalletConnect)" : "扫码绑定(WalletConnect)",
|
: "绑定浏览器钱包(EVM扩展)",
|
||||||
|
bindQr: isEn
|
||||||
|
? "Bind via QR (WalletConnect)"
|
||||||
|
: "扫码绑定(WalletConnect)",
|
||||||
walletConnectMissing: isEn
|
walletConnectMissing: isEn
|
||||||
? "WalletConnect disabled: please configure"
|
? "WalletConnect disabled: please configure"
|
||||||
: "未启用 WalletConnect:请配置",
|
: "未启用 WalletConnect:请配置",
|
||||||
@@ -720,7 +806,11 @@ export function AccountCenter() {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
if (win && idleId != null && typeof win.cancelIdleCallback === "function") {
|
if (
|
||||||
|
win &&
|
||||||
|
idleId != null &&
|
||||||
|
typeof win.cancelIdleCallback === "function"
|
||||||
|
) {
|
||||||
win.cancelIdleCallback(idleId);
|
win.cancelIdleCallback(idleId);
|
||||||
}
|
}
|
||||||
if (timeoutId != null && typeof window !== "undefined") {
|
if (timeoutId != null && typeof window !== "undefined") {
|
||||||
@@ -740,12 +830,40 @@ export function AccountCenter() {
|
|||||||
return nextOptions[0]?.key || "";
|
return nextOptions[0]?.key || "";
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAnnounce = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<Eip6963ProviderDetail>;
|
||||||
|
const detail = customEvent.detail;
|
||||||
|
if (!detail?.provider || typeof detail.provider.request !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uuid = String(detail.info?.uuid || "").trim();
|
||||||
|
const fallbackKey = `${String(detail.info?.rdns || "wallet").toLowerCase()}:${String(
|
||||||
|
detail.info?.name || "wallet",
|
||||||
|
).toLowerCase()}`;
|
||||||
|
eip6963Providers.set(uuid || fallbackKey, detail);
|
||||||
|
syncProviders();
|
||||||
|
};
|
||||||
|
|
||||||
syncProviders();
|
syncProviders();
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
window.addEventListener("ethereum#initialized", syncProviders as EventListener, {
|
window.addEventListener(
|
||||||
once: false,
|
"eip6963:announceProvider",
|
||||||
});
|
handleAnnounce as EventListener,
|
||||||
|
);
|
||||||
|
window.dispatchEvent(new Event("eip6963:requestProvider"));
|
||||||
|
window.addEventListener(
|
||||||
|
"ethereum#initialized",
|
||||||
|
syncProviders as EventListener,
|
||||||
|
{
|
||||||
|
once: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
return () => {
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
"eip6963:announceProvider",
|
||||||
|
handleAnnounce as EventListener,
|
||||||
|
);
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
"ethereum#initialized",
|
"ethereum#initialized",
|
||||||
syncProviders as EventListener,
|
syncProviders as EventListener,
|
||||||
@@ -790,11 +908,7 @@ export function AccountCenter() {
|
|||||||
} = await client.auth.refreshSession();
|
} = await client.auth.refreshSession();
|
||||||
const refreshedToken = String(refreshed?.access_token || "").trim();
|
const refreshedToken = String(refreshed?.access_token || "").trim();
|
||||||
if (refreshedToken) return refreshedToken;
|
if (refreshedToken) return refreshedToken;
|
||||||
if (
|
if (cachedToken && Number.isFinite(expiresAtSec) && expiresAtSec > nowSec) {
|
||||||
cachedToken &&
|
|
||||||
Number.isFinite(expiresAtSec) &&
|
|
||||||
expiresAtSec > nowSec
|
|
||||||
) {
|
|
||||||
return cachedToken;
|
return cachedToken;
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -852,8 +966,8 @@ export function AccountCenter() {
|
|||||||
injectedOptions.find((row) => row.key === preferredInjectedKey)
|
injectedOptions.find((row) => row.key === preferredInjectedKey)
|
||||||
?.provider || getEvmProvider();
|
?.provider || getEvmProvider();
|
||||||
const label =
|
const label =
|
||||||
injectedOptions.find((row) => row.key === preferredInjectedKey)?.label ||
|
injectedOptions.find((row) => row.key === preferredInjectedKey)
|
||||||
getEvmWalletLabel(injected);
|
?.label || getEvmWalletLabel(injected);
|
||||||
if (injected) {
|
if (injected) {
|
||||||
return {
|
return {
|
||||||
provider: injected,
|
provider: injected,
|
||||||
@@ -971,7 +1085,8 @@ export function AccountCenter() {
|
|||||||
if (wallets.length) {
|
if (wallets.length) {
|
||||||
const currentSelected = String(selectedWallet || "").toLowerCase();
|
const currentSelected = String(selectedWallet || "").toLowerCase();
|
||||||
const hasCurrent = wallets.some(
|
const hasCurrent = wallets.some(
|
||||||
(row) => String(row.address || "").toLowerCase() === currentSelected,
|
(row) =>
|
||||||
|
String(row.address || "").toLowerCase() === currentSelected,
|
||||||
);
|
);
|
||||||
const fallback =
|
const fallback =
|
||||||
wallets.find((row) => Boolean(row.is_primary))?.address ||
|
wallets.find((row) => Boolean(row.is_primary))?.address ||
|
||||||
@@ -979,7 +1094,9 @@ export function AccountCenter() {
|
|||||||
if (!currentSelected || !hasCurrent) {
|
if (!currentSelected || !hasCurrent) {
|
||||||
setSelectedWallet(fallback);
|
setSelectedWallet(fallback);
|
||||||
}
|
}
|
||||||
const currentWalletAddress = String(walletAddress || "").toLowerCase();
|
const currentWalletAddress = String(
|
||||||
|
walletAddress || "",
|
||||||
|
).toLowerCase();
|
||||||
const hasWalletAddress = wallets.some(
|
const hasWalletAddress = wallets.some(
|
||||||
(row) =>
|
(row) =>
|
||||||
String(row.address || "").toLowerCase() === currentWalletAddress,
|
String(row.address || "").toLowerCase() === currentWalletAddress,
|
||||||
@@ -1053,7 +1170,10 @@ export function AccountCenter() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
if (!lastIntentId) return;
|
if (!lastIntentId) return;
|
||||||
window.sessionStorage.setItem("polyweather:lastPaymentIntentId", lastIntentId);
|
window.sessionStorage.setItem(
|
||||||
|
"polyweather:lastPaymentIntentId",
|
||||||
|
lastIntentId,
|
||||||
|
);
|
||||||
}, [lastIntentId]);
|
}, [lastIntentId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1405,7 +1525,11 @@ export function AccountCenter() {
|
|||||||
await loadPaymentSnapshot();
|
await loadPaymentSnapshot();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status === "failed" || status === "cancelled" || status === "expired") {
|
if (
|
||||||
|
status === "failed" ||
|
||||||
|
status === "cancelled" ||
|
||||||
|
status === "expired"
|
||||||
|
) {
|
||||||
throw new Error(`payment ${status}`);
|
throw new Error(`payment ${status}`);
|
||||||
}
|
}
|
||||||
setPaymentInfo(
|
setPaymentInfo(
|
||||||
@@ -1508,7 +1632,8 @@ export function AccountCenter() {
|
|||||||
method: "eth_requestAccounts",
|
method: "eth_requestAccounts",
|
||||||
})) as string[];
|
})) as string[];
|
||||||
const address = String(accounts?.[0] || "").toLowerCase();
|
const address = String(accounts?.[0] || "").toLowerCase();
|
||||||
if (!address) throw new Error(isEn ? "Wallet account is empty." : "钱包账户为空");
|
if (!address)
|
||||||
|
throw new Error(isEn ? "Wallet account is empty." : "钱包账户为空");
|
||||||
|
|
||||||
const existingWallet = boundWallets.find(
|
const existingWallet = boundWallets.find(
|
||||||
(w) => String(w.address || "").toLowerCase() === address,
|
(w) => String(w.address || "").toLowerCase() === address,
|
||||||
@@ -1637,7 +1762,10 @@ export function AccountCenter() {
|
|||||||
await loadPaymentSnapshot();
|
await loadPaymentSnapshot();
|
||||||
setPaymentInfo(
|
setPaymentInfo(
|
||||||
newPrimary
|
newPrimary
|
||||||
? copy.unbindDonePrimary.replace("{address}", shortAddress(newPrimary))
|
? copy.unbindDonePrimary.replace(
|
||||||
|
"{address}",
|
||||||
|
shortAddress(newPrimary),
|
||||||
|
)
|
||||||
: copy.unbindDone,
|
: copy.unbindDone,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1690,7 +1818,8 @@ export function AccountCenter() {
|
|||||||
method: "eth_requestAccounts",
|
method: "eth_requestAccounts",
|
||||||
})) as string[];
|
})) as string[];
|
||||||
const activeAddress = String(activeAccounts?.[0] || "").toLowerCase();
|
const activeAddress = String(activeAccounts?.[0] || "").toLowerCase();
|
||||||
if (!activeAddress) throw new Error(isEn ? "Wallet account is empty." : "钱包账户为空");
|
if (!activeAddress)
|
||||||
|
throw new Error(isEn ? "Wallet account is empty." : "钱包账户为空");
|
||||||
|
|
||||||
const boundAddrSet = new Set(
|
const boundAddrSet = new Set(
|
||||||
boundWallets.map((row) => String(row.address || "").toLowerCase()),
|
boundWallets.map((row) => String(row.address || "").toLowerCase()),
|
||||||
@@ -2132,8 +2261,16 @@ export function AccountCenter() {
|
|||||||
<h3 className="text-sm font-bold text-blue-400 uppercase tracking-widest mb-4">
|
<h3 className="text-sm font-bold text-blue-400 uppercase tracking-widest mb-4">
|
||||||
{copy.membershipDetails}
|
{copy.membershipDetails}
|
||||||
</h3>
|
</h3>
|
||||||
<InfoRow icon={ShieldCheck} label={copy.authMode} value="Supabase" />
|
<InfoRow
|
||||||
<InfoRow icon={BarChart3} label={copy.weatherEngine} value="DEB + 多模型" />
|
icon={ShieldCheck}
|
||||||
|
label={copy.authMode}
|
||||||
|
value="Supabase"
|
||||||
|
/>
|
||||||
|
<InfoRow
|
||||||
|
icon={BarChart3}
|
||||||
|
label={copy.weatherEngine}
|
||||||
|
value="DEB + 多模型"
|
||||||
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={Zap}
|
icon={Zap}
|
||||||
label={copy.intradayAnalysis}
|
label={copy.intradayAnalysis}
|
||||||
@@ -2157,7 +2294,11 @@ export function AccountCenter() {
|
|||||||
<h3 className="text-sm font-bold text-indigo-400 uppercase tracking-widest mb-4">
|
<h3 className="text-sm font-bold text-indigo-400 uppercase tracking-widest mb-4">
|
||||||
{copy.identityStatus}
|
{copy.identityStatus}
|
||||||
</h3>
|
</h3>
|
||||||
<InfoRow icon={Mail} label={copy.boundEmail} value={email || "--"} />
|
<InfoRow
|
||||||
|
icon={Mail}
|
||||||
|
label={copy.boundEmail}
|
||||||
|
value={email || "--"}
|
||||||
|
/>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={LogIn}
|
icon={LogIn}
|
||||||
label={copy.loginMethod}
|
label={copy.loginMethod}
|
||||||
@@ -2197,9 +2338,7 @@ export function AccountCenter() {
|
|||||||
onPay={() => void handleOverlayCheckout()}
|
onPay={() => void handleOverlayCheckout()}
|
||||||
onClose={() => setShowOverlay(false)}
|
onClose={() => setShowOverlay(false)}
|
||||||
payBusy={paymentBusy}
|
payBusy={paymentBusy}
|
||||||
payLabel={
|
payLabel={hasPayingWallet ? copy.payNow : copy.connectAndPay}
|
||||||
hasPayingWallet ? copy.payNow : copy.connectAndPay
|
|
||||||
}
|
|
||||||
errorText={paymentError || undefined}
|
errorText={paymentError || undefined}
|
||||||
infoText={paymentInfo || undefined}
|
infoText={paymentInfo || undefined}
|
||||||
txHash={lastTxHash || undefined}
|
txHash={lastTxHash || undefined}
|
||||||
@@ -2214,207 +2353,209 @@ export function AccountCenter() {
|
|||||||
|
|
||||||
{/* Telegram Bot Section */}
|
{/* Telegram Bot Section */}
|
||||||
{showSecondarySections ? (
|
{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 md:flex gap-6">
|
||||||
<section className="flex-1 bg-white/5 border border-white/10 rounded-[2rem] p-8 relative overflow-hidden group">
|
<section className="flex-1 bg-white/5 border border-white/10 rounded-[2rem] p-8 relative overflow-hidden group">
|
||||||
<Bot
|
<Bot
|
||||||
size={140}
|
size={140}
|
||||||
className="absolute -right-8 -bottom-8 text-white/5 -rotate-12 group-hover:rotate-0 transition-transform duration-1000"
|
className="absolute -right-8 -bottom-8 text-white/5 -rotate-12 group-hover:rotate-0 transition-transform duration-1000"
|
||||||
/>
|
/>
|
||||||
<div className="relative z-10">
|
<div className="relative z-10">
|
||||||
<h3 className="text-lg font-bold mb-2 flex items-center gap-2 text-blue-400">
|
<h3 className="text-lg font-bold mb-2 flex items-center gap-2 text-blue-400">
|
||||||
<Bot size={22} /> {copy.telegramBind}
|
<Bot size={22} /> {copy.telegramBind}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-slate-400 text-sm mb-6">
|
<p className="text-slate-400 text-sm mb-6">
|
||||||
{copy.telegramHint}
|
{copy.telegramHint}
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-4 flex flex-wrap gap-2">
|
<div className="mb-4 flex flex-wrap gap-2">
|
||||||
{TELEGRAM_BOT_URL ? (
|
{TELEGRAM_BOT_URL ? (
|
||||||
<Link
|
<Link
|
||||||
href={TELEGRAM_BOT_URL}
|
href={TELEGRAM_BOT_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="inline-flex items-center gap-1 rounded-lg border border-cyan-400/30 bg-cyan-500/10 px-3 py-1.5 text-xs font-semibold text-cyan-200 hover:bg-cyan-500/20"
|
className="inline-flex items-center gap-1 rounded-lg border border-cyan-400/30 bg-cyan-500/10 px-3 py-1.5 text-xs font-semibold text-cyan-200 hover:bg-cyan-500/20"
|
||||||
>
|
|
||||||
{copy.telegramBotLink}
|
|
||||||
<ExternalLink size={12} />
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
{TELEGRAM_GROUP_URL ? (
|
|
||||||
<Link
|
|
||||||
href={TELEGRAM_GROUP_URL}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="inline-flex items-center gap-1 rounded-lg border border-blue-400/30 bg-blue-500/10 px-3 py-1.5 text-xs font-semibold text-blue-200 hover:bg-blue-500/20"
|
|
||||||
>
|
|
||||||
{copy.telegramGroupLink}
|
|
||||||
<ExternalLink size={12} />
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<code className="flex-grow bg-black/40 border border-white/10 p-4 rounded-xl font-mono text-xs text-blue-300 overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
{bindCommand}
|
|
||||||
</code>
|
|
||||||
<button
|
|
||||||
onClick={() => handleCopy(bindCommand)}
|
|
||||||
className="p-4 bg-blue-600 hover:bg-blue-500 rounded-xl transition-all shadow-lg text-white"
|
|
||||||
title={copy.copyCommand}
|
|
||||||
aria-label={copy.copyCommand}
|
|
||||||
>
|
|
||||||
{copied ? <CheckCircle2 size={20} /> : <Copy size={20} />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Payment Details / Wallet Management */}
|
|
||||||
<section className="w-full md:w-96 bg-white/5 border border-white/10 rounded-[2rem] p-8 flex flex-col justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-blue-400 text-sm font-bold uppercase tracking-widest mb-6 flex items-center gap-2">
|
|
||||||
<Wallet size={18} /> {copy.paymentMgmt}
|
|
||||||
</h3>
|
|
||||||
{paymentError ? (
|
|
||||||
<div className="mb-4 rounded-xl border border-red-400/40 bg-red-500/10 px-3 py-2 text-[11px] text-red-200">
|
|
||||||
{paymentError}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{!paymentError && paymentInfo ? (
|
|
||||||
<div className="mb-4 rounded-xl border border-cyan-400/35 bg-cyan-500/10 px-3 py-2 text-[11px] text-cyan-200">
|
|
||||||
{paymentInfo}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{availableTokenList.length > 0 && (
|
|
||||||
<div className="mb-5">
|
|
||||||
<p className="text-[11px] uppercase tracking-widest text-slate-500 mb-2">
|
|
||||||
{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.address}
|
|
||||||
onClick={() => setSelectedTokenAddress(token.address)}
|
|
||||||
disabled={paymentBusy}
|
|
||||||
className={`rounded-xl border px-3 py-2 text-left transition-all ${
|
|
||||||
active
|
|
||||||
? "bg-blue-500/15 border-blue-500/40 text-white"
|
|
||||||
: "bg-white/5 border-white/10 text-slate-400 hover:bg-white/10"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="text-xs font-bold">
|
|
||||||
{token.symbol}
|
|
||||||
</div>
|
|
||||||
<div className="text-[10px] opacity-80 truncate">
|
|
||||||
{token.name}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{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 ? "bg-blue-500/10 border-blue-500/30 text-white" : "bg-white/5 border-white/5 text-slate-400"}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-1">
|
{copy.telegramBotLink}
|
||||||
<span className="text-[10px] font-mono">
|
<ExternalLink size={12} />
|
||||||
{shortAddress(w.address)}
|
</Link>
|
||||||
</span>
|
) : null}
|
||||||
{w.is_primary && (
|
{TELEGRAM_GROUP_URL ? (
|
||||||
<span className="text-[8px] bg-blue-500 px-1 rounded">
|
<Link
|
||||||
{copy.primary}
|
href={TELEGRAM_GROUP_URL}
|
||||||
</span>
|
target="_blank"
|
||||||
)}
|
rel="noreferrer"
|
||||||
</div>
|
className="inline-flex items-center gap-1 rounded-lg border border-blue-400/30 bg-blue-500/10 px-3 py-1.5 text-xs font-semibold text-blue-200 hover:bg-blue-500/20"
|
||||||
<div className="text-[10px]">{copy.polygonChain}</div>
|
>
|
||||||
<div className="mt-2 flex justify-end">
|
{copy.telegramGroupLink}
|
||||||
<button
|
<ExternalLink size={12} />
|
||||||
type="button"
|
</Link>
|
||||||
onClick={() => void handleUnbindWallet(w.address)}
|
) : null}
|
||||||
disabled={paymentBusy}
|
|
||||||
className="inline-flex items-center gap-1 rounded-md border border-red-500/30 bg-red-500/10 px-2 py-1 text-[10px] font-semibold text-red-300 transition-all hover:bg-red-500/20 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<Minus size={12} />
|
|
||||||
{copy.unbind}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<div className="flex gap-2">
|
||||||
<p className="text-xs text-slate-500 italic">
|
<code className="flex-grow bg-black/40 border border-white/10 p-4 rounded-xl font-mono text-xs text-blue-300 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
{copy.noWallet}
|
{bindCommand}
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-1 gap-2">
|
|
||||||
{injectedProviderOptions.length > 1 && (
|
|
||||||
<label className="mb-2 block">
|
|
||||||
<span className="mb-2 block text-[11px] uppercase tracking-widest text-slate-500">
|
|
||||||
{copy.walletExtensionDetected}
|
|
||||||
</span>
|
|
||||||
<select
|
|
||||||
value={selectedInjectedProviderKey}
|
|
||||||
onChange={(event) =>
|
|
||||||
setSelectedInjectedProviderKey(event.target.value)
|
|
||||||
}
|
|
||||||
disabled={paymentBusy}
|
|
||||||
className="w-full rounded-xl border border-white/10 bg-white/5 px-3 py-3 text-xs text-slate-200 outline-none transition-all hover:bg-white/10 disabled:opacity-60"
|
|
||||||
>
|
|
||||||
{injectedProviderOptions.map((option) => (
|
|
||||||
<option
|
|
||||||
key={option.key}
|
|
||||||
value={option.key}
|
|
||||||
className="bg-slate-900 text-slate-200"
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setProviderMode("auto");
|
|
||||||
void connectAndBindWallet("auto");
|
|
||||||
}}
|
|
||||||
disabled={paymentBusy || !isAuthenticated}
|
|
||||||
className="w-full py-3 border border-white/10 bg-white/5 hover:bg-white/10 rounded-xl text-xs font-bold text-slate-300 transition-all flex items-center justify-center gap-2 disabled:opacity-60"
|
|
||||||
>
|
|
||||||
<PlusIcon className="w-4 h-4" /> {copy.bindExt}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setProviderMode("walletconnect");
|
|
||||||
void connectAndBindWallet("walletconnect");
|
|
||||||
}}
|
|
||||||
disabled={
|
|
||||||
paymentBusy || !isAuthenticated || !walletConnectEnabled
|
|
||||||
}
|
|
||||||
className="w-full py-3 border border-cyan-400/30 bg-cyan-500/10 hover:bg-cyan-500/20 rounded-xl text-xs font-bold text-cyan-300 transition-all flex items-center justify-center gap-2 disabled:opacity-60"
|
|
||||||
>
|
|
||||||
<CreditCard className="w-4 h-4" /> {copy.bindQr}
|
|
||||||
</button>
|
|
||||||
{!walletConnectEnabled && (
|
|
||||||
<p className="text-[11px] text-slate-500">
|
|
||||||
{copy.walletConnectMissing}
|
|
||||||
<code className="mx-1">
|
|
||||||
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID
|
|
||||||
</code>
|
</code>
|
||||||
</p>
|
<button
|
||||||
)}
|
onClick={() => handleCopy(bindCommand)}
|
||||||
</div>
|
className="p-4 bg-blue-600 hover:bg-blue-500 rounded-xl transition-all shadow-lg text-white"
|
||||||
</section>
|
title={copy.copyCommand}
|
||||||
</div>
|
aria-label={copy.copyCommand}
|
||||||
|
>
|
||||||
|
{copied ? <CheckCircle2 size={20} /> : <Copy size={20} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Payment Details / Wallet Management */}
|
||||||
|
<section className="w-full md:w-96 bg-white/5 border border-white/10 rounded-[2rem] p-8 flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-blue-400 text-sm font-bold uppercase tracking-widest mb-6 flex items-center gap-2">
|
||||||
|
<Wallet size={18} /> {copy.paymentMgmt}
|
||||||
|
</h3>
|
||||||
|
{paymentError ? (
|
||||||
|
<div className="mb-4 rounded-xl border border-red-400/40 bg-red-500/10 px-3 py-2 text-[11px] text-red-200">
|
||||||
|
{paymentError}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!paymentError && paymentInfo ? (
|
||||||
|
<div className="mb-4 rounded-xl border border-cyan-400/35 bg-cyan-500/10 px-3 py-2 text-[11px] text-cyan-200">
|
||||||
|
{paymentInfo}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{availableTokenList.length > 0 && (
|
||||||
|
<div className="mb-5">
|
||||||
|
<p className="text-[11px] uppercase tracking-widest text-slate-500 mb-2">
|
||||||
|
{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.address}
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedTokenAddress(token.address)
|
||||||
|
}
|
||||||
|
disabled={paymentBusy}
|
||||||
|
className={`rounded-xl border px-3 py-2 text-left transition-all ${
|
||||||
|
active
|
||||||
|
? "bg-blue-500/15 border-blue-500/40 text-white"
|
||||||
|
: "bg-white/5 border-white/10 text-slate-400 hover:bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-xs font-bold">
|
||||||
|
{token.symbol}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] opacity-80 truncate">
|
||||||
|
{token.name}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{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 ? "bg-blue-500/10 border-blue-500/30 text-white" : "bg-white/5 border-white/5 text-slate-400"}`}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
{copy.primary}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px]">{copy.polygonChain}</div>
|
||||||
|
<div className="mt-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void handleUnbindWallet(w.address)}
|
||||||
|
disabled={paymentBusy}
|
||||||
|
className="inline-flex items-center gap-1 rounded-md border border-red-500/30 bg-red-500/10 px-2 py-1 text-[10px] font-semibold text-red-300 transition-all hover:bg-red-500/20 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Minus size={12} />
|
||||||
|
{copy.unbind}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-slate-500 italic">
|
||||||
|
{copy.noWallet}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-1 gap-2">
|
||||||
|
{injectedProviderOptions.length > 1 && (
|
||||||
|
<label className="mb-2 block">
|
||||||
|
<span className="mb-2 block text-[11px] uppercase tracking-widest text-slate-500">
|
||||||
|
{copy.walletExtensionDetected}
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={selectedInjectedProviderKey}
|
||||||
|
onChange={(event) =>
|
||||||
|
setSelectedInjectedProviderKey(event.target.value)
|
||||||
|
}
|
||||||
|
disabled={paymentBusy}
|
||||||
|
className="w-full rounded-xl border border-white/10 bg-white/5 px-3 py-3 text-xs text-slate-200 outline-none transition-all hover:bg-white/10 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{injectedProviderOptions.map((option) => (
|
||||||
|
<option
|
||||||
|
key={option.key}
|
||||||
|
value={option.key}
|
||||||
|
className="bg-slate-900 text-slate-200"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setProviderMode("auto");
|
||||||
|
void connectAndBindWallet("auto");
|
||||||
|
}}
|
||||||
|
disabled={paymentBusy || !isAuthenticated}
|
||||||
|
className="w-full py-3 border border-white/10 bg-white/5 hover:bg-white/10 rounded-xl text-xs font-bold text-slate-300 transition-all flex items-center justify-center gap-2 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
<PlusIcon className="w-4 h-4" /> {copy.bindExt}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setProviderMode("walletconnect");
|
||||||
|
void connectAndBindWallet("walletconnect");
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
paymentBusy || !isAuthenticated || !walletConnectEnabled
|
||||||
|
}
|
||||||
|
className="w-full py-3 border border-cyan-400/30 bg-cyan-500/10 hover:bg-cyan-500/20 rounded-xl text-xs font-bold text-cyan-300 transition-all flex items-center justify-center gap-2 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
<CreditCard className="w-4 h-4" /> {copy.bindQr}
|
||||||
|
</button>
|
||||||
|
{!walletConnectEnabled && (
|
||||||
|
<p className="text-[11px] text-slate-500">
|
||||||
|
{copy.walletConnectMissing}
|
||||||
|
<code className="mx-1">
|
||||||
|
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="lg:col-span-12 grid grid-cols-1 gap-6 md:grid-cols-3">
|
<div className="lg:col-span-12 grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||||
<div className="md:col-span-2 h-48 animate-pulse rounded-[2rem] border border-white/10 bg-white/5" />
|
<div className="md:col-span-2 h-48 animate-pulse rounded-[2rem] border border-white/10 bg-white/5" />
|
||||||
|
|||||||
Reference in New Issue
Block a user