chore: refactor 386a82dd
Steve · 2025-09-20 00:57 1 file(s) · +83 −168
src/components/connect-wallet.js +83 −168
1 1
class ConnectWallet extends HTMLElement {
2 +
	// Constructor and lifecycle methods
2 3
	constructor() {
3 4
		super();
4 5
		this.attachShadow({ mode: "open" });
20 21
	attributeChangedCallback(name, oldValue, newValue) {
21 22
		if (name === "chain-id" && oldValue !== newValue) {
22 23
			this.chainId = newValue;
23 -
			// If already connected, check if we need to switch chains
24 24
			if (this.connected) {
25 25
				this.checkAndSwitchChain();
26 26
			}
28 28
	}
29 29
30 30
	connectedCallback() {
31 -
		// Get chain-id from attribute
32 -
		this.chainId = this.getAttribute("chain-id");
31 +
		this.chainId = this.getAttribute("chain-id") || "0x1";
33 32
		this.render();
34 33
	}
35 34
35 +
	// Wallet connection methods
36 36
	async connect() {
37 37
		if (window.ethereum) {
38 38
			try {
45 45
46 46
				this.address = accounts[0];
47 47
48 -
				// Get current chain
49 48
				this.currentChainId = await window.ethereum.request({
50 49
					method: "eth_chainId",
51 50
				});
52 51
53 -
				// Switch to desired chain if specified
54 52
				if (this.chainId && this.chainId !== this.currentChainId) {
55 53
					await this.switchChain(this.chainId);
56 54
				}
62 60
				this.loading = false;
63 61
				this.render();
64 62
65 -
				// Dispatch custom event
66 63
				this.dispatchEvent(
67 64
					new CustomEvent("wallet-connected", {
68 65
						detail: {
77 74
				this.loading = false;
78 75
				this.render();
79 76
80 -
				// Dispatch error event
81 77
				this.dispatchEvent(
82 78
					new CustomEvent("wallet-error", {
83 79
						detail: { error: error.message },
89 85
		}
90 86
	}
91 87
88 +
	disconnect() {
89 +
		this.connected = false;
90 +
		this.address = "";
91 +
		this.ensData = null;
92 +
		this.currentChainId = null;
93 +
		this.balance = "0";
94 +
		this.showPopover = false;
95 +
		this.copySuccess = false;
96 +
		this.render();
97 +
98 +
		this.dispatchEvent(new CustomEvent("wallet-disconnected"));
99 +
	}
100 +
101 +
	// Chain management methods
92 102
	async switchChain(chainId) {
93 103
		try {
94 104
			await window.ethereum.request({
97 107
			});
98 108
			this.currentChainId = chainId;
99 109
		} catch (switchError) {
100 -
			// Chain not added to wallet
101 -
			if (switchError.code === 4902) {
102 -
				try {
103 -
					await this.addChain(chainId);
104 -
				} catch (addError) {
105 -
					throw new Error(`Failed to add chain: ${addError.message}`);
106 -
				}
107 -
			} else {
108 -
				throw new Error(`Failed to switch chain: ${switchError.message}`);
109 -
			}
110 +
			throw new Error(`Failed to switch chain: ${switchError.message}`);
110 111
		}
111 -
	}
112 -
113 -
	async addChain(chainId) {
114 -
		const chainConfig = this.getChainConfig(chainId);
115 -
		if (!chainConfig) {
116 -
			throw new Error(`Unknown chain ID: ${chainId}`);
117 -
		}
118 -
119 -
		await window.ethereum.request({
120 -
			method: "wallet_addEthereumChain",
121 -
			params: [chainConfig],
122 -
		});
123 -
		this.currentChainId = chainId;
124 112
	}
125 113
126 114
	async checkAndSwitchChain() {
140 128
		}
141 129
	}
142 130
143 -
	getChainConfig(chainId) {
144 -
		const chains = {
145 -
			"0x1": {
146 -
				chainId: "0x1",
147 -
				chainName: "Ethereum Mainnet",
148 -
				rpcUrls: ["https://eth.drpc.org"],
149 -
				nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
150 -
				blockExplorerUrls: ["https://etherscan.io/"],
151 -
			},
152 -
			"0x89": {
153 -
				chainId: "0x89",
154 -
				chainName: "Polygon",
155 -
				rpcUrls: ["https://polygon.drpc.org"],
156 -
				nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
157 -
				blockExplorerUrls: ["https://polygonscan.com/"],
158 -
			},
159 -
			"0xa": {
160 -
				chainId: "0xa",
161 -
				chainName: "Optimism",
162 -
				rpcUrls: ["https://optimism.drpc.org"],
163 -
				nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
164 -
				blockExplorerUrls: ["https://optimistic.etherscan.io/"],
165 -
			},
166 -
			"0xa4b1": {
167 -
				chainId: "0xa4b1",
168 -
				chainName: "Arbitrum One",
169 -
				rpcUrls: ["https://arbitrum.drpc.org"],
170 -
				nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
171 -
				blockExplorerUrls: ["https://arbiscan.io/"],
172 -
			},
173 -
			"0x2105": {
174 -
				chainId: "0x2105",
175 -
				chainName: "Base",
176 -
				rpcUrls: ["https://base.drpc.org"],
177 -
				nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
178 -
				blockExplorerUrls: ["https://basescan.org/"],
179 -
			},
180 -
			// Add more chains as needed
181 -
		};
182 -
183 -
		return chains[chainId];
184 -
	}
185 -
186 131
	getChainName(chainId) {
187 132
		const chainNames = {
188 133
			"0x1": "Ethereum",
190 135
			"0xa": "Optimism",
191 136
			"0xa4b1": "Arbitrum",
192 137
			"0x2105": "Base",
193 -
			// Add more as needed
194 138
		};
195 139
		return chainNames[chainId] || `Chain ${chainId}`;
196 140
	}
197 141
198 -
	getChainSymbol(chainId) {
199 -
		const chainSymbols = {
200 -
			"0x1": "ETH",
201 -
			"0x89": "MATIC",
202 -
			"0xa": "ETH",
203 -
			"0xa4b1": "ETH",
204 -
			"0x2105": "ETH",
205 -
			// Add more as needed
206 -
		};
207 -
		return chainSymbols[chainId] || "ETH";
208 -
	}
209 -
	renderProfile() {
210 -
		const profileContainer = document.createElement("div");
211 -
		profileContainer.className = "profile-container";
212 -
213 -
		const profileDiv = document.createElement("div");
214 -
		profileDiv.className = "profile";
215 -
216 -
		const avatar = this.ensData?.avatar_small;
217 -
		const displayName = this.getDisplayName();
218 -
		const chainSymbol = this.getChainSymbol(this.currentChainId);
219 -
220 -
		// Create avatar element
221 -
		let avatarElement = "";
222 -
		if (avatar) {
223 -
			avatarElement = `<img src="${avatar}" alt="Avatar" class="avatar" onerror="this.style.display='none'">`;
224 -
		} else {
225 -
			avatarElement = `<div class="avatar-placeholder"></div>`;
226 -
		}
227 -
228 -
		profileDiv.innerHTML = `
229 -
			${avatarElement}
230 -
			<div class="profile-info">
231 -
				<h3>${displayName}</h3>
232 -
				<p>${this.balance} ${chainSymbol}</p>
233 -
			</div>
234 -
		`;
235 -
236 -
		profileDiv.addEventListener("click", (e) => {
237 -
			e.stopPropagation();
238 -
			this.togglePopover();
239 -
		});
240 -
241 -
		profileContainer.appendChild(profileDiv);
242 -
		this.shadowRoot.appendChild(profileContainer);
243 -
244 -
		// Show popover if it was open before re-render
245 -
		if (this.showPopover) {
246 -
			this.showPopoverElement();
247 -
		}
248 -
	}
249 -
142 +
	// Data fetching methods
250 143
	async fetchEnsData() {
251 144
		try {
252 145
			const response = await fetch(`https://api.ensdata.net/${this.address}`);
270 163
				params: [this.address, "latest"],
271 164
			});
272 165
273 -
			// Convert from wei to ether (divide by 10^18)
274 166
			const balanceEth = parseInt(balanceWei, 16) / Math.pow(10, 18);
275 167
			this.balance = balanceEth.toFixed(4);
276 168
		} catch (error) {
279 171
		}
280 172
	}
281 173
174 +
	// UI helper methods
175 +
	getDisplayName() {
176 +
		if (this.ensData?.ens_primary) {
177 +
			return this.ensData.ens_primary;
178 +
		}
179 +
		return this.truncateAddress(this.address);
180 +
	}
181 +
182 +
	truncateAddress(addr) {
183 +
		if (!addr) return "";
184 +
		return addr.slice(0, 5) + "..." + addr.slice(-5);
185 +
	}
186 +
187 +
	async copyAddress() {
188 +
		try {
189 +
			await navigator.clipboard.writeText(this.address);
190 +
			this.copySuccess = true;
191 +
			this.showPopoverElement();
192 +
193 +
			setTimeout(() => {
194 +
				this.copySuccess = false;
195 +
				this.showPopoverElement();
196 +
			}, 1000);
197 +
		} catch (error) {
198 +
			console.error("Failed to copy address", error);
199 +
		}
200 +
	}
201 +
202 +
	// Popover management methods
282 203
	togglePopover() {
283 204
		this.showPopover = !this.showPopover;
284 205
		if (this.showPopover) {
300 221
			this.shadowRoot.querySelector(".profile-container");
301 222
		if (!profileContainer) return;
302 223
303 -
		// Remove existing popover if any
304 224
		const existingPopover = profileContainer.querySelector(".popover");
305 225
		if (existingPopover) {
306 226
			existingPopover.remove();
339 259
340 260
		profileContainer.appendChild(popover);
341 261
342 -
		// Add click outside listener to close popover
343 262
		setTimeout(() => {
344 263
			document.addEventListener("click", this.hidePopover.bind(this), {
345 264
				once: true,
358 277
		}
359 278
	}
360 279
361 -
	async copyAddress() {
362 -
		try {
363 -
			await navigator.clipboard.writeText(this.address);
364 -
			this.copySuccess = true;
365 -
			this.showPopoverElement(); // Re-render popover with checkmark
366 -
367 -
			// Reset after 1 second
368 -
			setTimeout(() => {
369 -
				this.copySuccess = false;
370 -
				this.showPopoverElement(); // Re-render popover back to normal
371 -
			}, 1000);
372 -
		} catch (error) {
373 -
			console.error("Failed to copy address", error);
374 -
		}
375 -
	}
376 -
377 -
	disconnect() {
378 -
		this.connected = false;
379 -
		this.address = "";
380 -
		this.ensData = null;
381 -
		this.currentChainId = null;
382 -
		this.balance = "0";
383 -
		this.showPopover = false;
384 -
		this.copySuccess = false;
385 -
		this.render();
386 -
387 -
		// Dispatch custom event
388 -
		this.dispatchEvent(new CustomEvent("wallet-disconnected"));
389 -
	}
390 -
391 -
	// Add CSS for chain display
280 +
	// Render methods and styling
392 281
	render() {
393 282
		this.shadowRoot.innerHTML = `
394 283
			<style>
556 445
		}
557 446
	}
558 447
559 -
	getDisplayName() {
560 -
		if (this.ensData?.ens_primary) {
561 -
			return this.ensData.ens_primary;
448 +
	renderProfile() {
449 +
		const profileContainer = document.createElement("div");
450 +
		profileContainer.className = "profile-container";
451 +
452 +
		const profileDiv = document.createElement("div");
453 +
		profileDiv.className = "profile";
454 +
455 +
		const avatar = this.ensData?.avatar_small;
456 +
		const displayName = this.getDisplayName();
457 +
458 +
		let avatarElement = "";
459 +
		if (avatar) {
460 +
			avatarElement = `<img src="${avatar}" alt="Avatar" class="avatar" onerror="this.style.display='none'">`;
461 +
		} else {
462 +
			avatarElement = `<div class="avatar-placeholder"></div>`;
562 463
		}
563 -
		return this.truncateAddress(this.address);
564 -
	}
565 464
566 -
	truncateAddress(addr) {
567 -
		if (!addr) return "";
568 -
		return addr.slice(0, 5) + "..." + addr.slice(-5);
465 +
		profileDiv.innerHTML = `
466 +
			${avatarElement}
467 +
			<div class="profile-info">
468 +
				<h3>${displayName}</h3>
469 +
				<p>${this.balance} ETH</p>
470 +
			</div>
471 +
		`;
472 +
473 +
		profileDiv.addEventListener("click", (e) => {
474 +
			e.stopPropagation();
475 +
			this.togglePopover();
476 +
		});
477 +
478 +
		profileContainer.appendChild(profileDiv);
479 +
		this.shadowRoot.appendChild(profileContainer);
480 +
481 +
		if (this.showPopover) {
482 +
			this.showPopoverElement();
483 +
		}
569 484
	}
570 485
571 486
	renderLoading() {