From 5dbe38d9aa6b215a7b1631e87bd01be9397b8ed1 Mon Sep 17 00:00:00 2001 From: naizo10 <76167545+naizo01@users.noreply.github.com> Date: Sat, 18 Jan 2025 16:33:31 +0900 Subject: [PATCH 1/4] Implement network switcher in header component --- pkgs/frontend/app/components/Header.tsx | 113 +++++++++++++++--- .../frontend/app/components/SwitchNetwork.tsx | 57 +++++++-- pkgs/frontend/app/root.tsx | 2 +- pkgs/frontend/hooks/useViem.ts | 40 +++++-- 4 files changed, 174 insertions(+), 38 deletions(-) diff --git a/pkgs/frontend/app/components/Header.tsx b/pkgs/frontend/app/components/Header.tsx index 98a00907..3f82569d 100644 --- a/pkgs/frontend/app/components/Header.tsx +++ b/pkgs/frontend/app/components/Header.tsx @@ -4,11 +4,14 @@ import { Link, useLocation, useNavigate, useParams } from "@remix-run/react"; import axios from "axios"; import { useActiveWalletIdentity } from "hooks/useENS"; import { useTreeInfo } from "hooks/useHats"; +import { currentChain } from "hooks/useViem"; import { useActiveWallet } from "hooks/useWallet"; import { useEffect, useMemo, useState } from "react"; import type { HatsDetailSchama } from "types/hats"; import { ipfs2https } from "utils/ipfs"; import { abbreviateAddress } from "utils/wallet"; +import type { Chain } from "viem"; +import { useSwitchNetwork } from "./SwitchNetwork"; import CommonButton from "./common/CommonButton"; import { UserIcon } from "./icon/UserIcon"; import { WorkspaceIcon } from "./icon/WorkspaceIcon"; @@ -28,6 +31,52 @@ enum HeaderType { WorkspaceAndUserIcons = "WorkspaceAndUserIcons", } +// ネットワーク切り替えコンポーネントを作成 +const NetworkSwitcher = ({ + switchToChain, + connectedChainId, +}: { + switchToChain: (chain: Chain) => Promise; + connectedChainId?: number; +}) => { + const isWrongNetwork = + connectedChainId && connectedChainId !== currentChain.id; + + return ( + + + Network + + {isWrongNetwork ? ( + <> + + ⚠️ 異なるネットワークに接続されています + + switchToChain(currentChain)} + w="100%" + mb={1} + colorScheme="red" + > + {currentChain.name}に切り替える + + + ) : ( + switchToChain(currentChain)} + w="100%" + mb={1} + variant="solid" + > + {currentChain.name} + + )} + + ); +}; + export const Header = () => { const [headerType, setHeaderType] = useState( HeaderType.NonHeader, @@ -76,18 +125,30 @@ export const Header = () => { return avatar ? ipfs2https(avatar) : undefined; }, [identity]); - const handleLogout = () => { - if (isSmartWallet) { - logout(); - } else { - if (wallets.find((w) => w.connectorType === "injected")) { - alert("ウォレット拡張機能から切断してください。"); + // ログアウトハンドラー + const handleLogout = async () => { + try { + if (isSmartWallet) { + await logout(); } else { - Promise.all(wallets.map((wallet) => wallet.disconnect())); + if (wallets.find((w) => w.connectorType === "injected")) { + alert("ウォレット拡張機能から切断してください。"); + return; + } + await Promise.all(wallets.map((wallet) => wallet.disconnect())); + navigate("/login"); } + } catch (error) { + console.error("ログアウトに失敗しました:", error); } }; + const { switchToChain } = useSwitchNetwork(); + + const connectedChainId = wallets[0] + ? Number(wallets[0].chainId.split(":")[1]) + : undefined; + return headerType !== HeaderType.NonHeader ? ( @@ -128,20 +189,42 @@ export const Header = () => { {abbreviateAddress(identity.address)} + Logout ) : ( - { - navigate("/login"); - }} - w="auto" - > - Login - + + + + {connectedChainId && connectedChainId !== currentChain.id + ? "⚠️ ネットワークを切り替えてください" + : currentChain.name} + + + + + + Logout + + + )} ) : ( diff --git a/pkgs/frontend/app/components/SwitchNetwork.tsx b/pkgs/frontend/app/components/SwitchNetwork.tsx index 20db900c..427891e7 100644 --- a/pkgs/frontend/app/components/SwitchNetwork.tsx +++ b/pkgs/frontend/app/components/SwitchNetwork.tsx @@ -1,22 +1,57 @@ -import { currentChain } from "hooks/useViem"; -import { useActiveWallet } from "hooks/useWallet"; +import { useWallets } from "@privy-io/react-auth"; +import { currentChain, getChainById, supportedChains } from "hooks/useViem"; import { type FC, useEffect } from "react"; +import type { Chain } from "viem"; + +export const useSwitchNetwork = () => { + const { wallets } = useWallets(); + + const switchToChain = async (targetChain: Chain) => { + const connectedWallet = wallets[0]; + if (!connectedWallet) { + throw new Error("ウォレットが接続されていません"); + } + + // チェーンがサポートされているか確認 + if (!supportedChains.some((chain) => chain.id === targetChain.id)) { + throw new Error("サポートされていないチェーンです"); + } + + const currentChainId = Number(connectedWallet.chainId.split(":")[1]); + + if (currentChainId !== targetChain.id) { + try { + await connectedWallet.switchChain(targetChain.id); + return true; + } catch (error) { + console.error("チェーンの切り替えに失敗しました:", error); + throw error; + } + } + return false; + }; + + return { switchToChain }; +}; export const SwitchNetwork: FC = () => { - const { connectedWallet } = useActiveWallet(); + const { wallets } = useWallets(); + const { switchToChain } = useSwitchNetwork(); useEffect(() => { - const switchChain = async () => { - if ( - connectedWallet && - Number(connectedWallet.chainId) !== currentChain.id - ) { - await connectedWallet.switchChain(currentChain.id); + const initializeChain = async () => { + // ウォレットが接続されているか確認 + if (!wallets.length) return; + + try { + await switchToChain(currentChain); + } catch (error) { + console.error("初期チェーンの設定に失敗しました:", error); } }; - switchChain(); - }, [connectedWallet]); + initializeChain(); + }, [switchToChain, wallets]); // walletsを依存配列に追加 return <>; }; diff --git a/pkgs/frontend/app/root.tsx b/pkgs/frontend/app/root.tsx index c10965c2..618da14e 100644 --- a/pkgs/frontend/app/root.tsx +++ b/pkgs/frontend/app/root.tsx @@ -9,7 +9,7 @@ import { Scripts, ScrollRestoration, } from "@remix-run/react"; -import { currentChain } from "hooks/useViem"; +import { currentChain, supportedChains } from "hooks/useViem"; import { goldskyClient } from "utils/apollo"; import { Header } from "./components/Header"; import { SwitchNetwork } from "./components/SwitchNetwork"; diff --git a/pkgs/frontend/hooks/useViem.ts b/pkgs/frontend/hooks/useViem.ts index 09c68901..5309404d 100644 --- a/pkgs/frontend/hooks/useViem.ts +++ b/pkgs/frontend/hooks/useViem.ts @@ -1,18 +1,36 @@ -import { http, createPublicClient } from "viem"; +import { http, type Chain, createPublicClient } from "viem"; import { base, mainnet, optimism, sepolia } from "viem/chains"; export const chainId = Number(import.meta.env.VITE_CHAIN_ID) || 1; -export const currentChain = - chainId === 1 - ? mainnet - : chainId === 11155111 - ? sepolia - : chainId === 10 - ? optimism - : chainId === 8453 - ? base - : sepolia; +// サポートするチェーンの定義 +export const SUPPORTED_CHAINS = { + MAINNET: mainnet, + SEPOLIA: sepolia, + OPTIMISM: optimism, + BASE: base, +} as const; + +// チェーンIDからチェーンを取得する関数 +export const getChainById = (id: number): Chain => { + switch (id) { + case 1: + return SUPPORTED_CHAINS.MAINNET; + case 11155111: + return SUPPORTED_CHAINS.SEPOLIA; + case 10: + return SUPPORTED_CHAINS.OPTIMISM; + case 8453: + return SUPPORTED_CHAINS.BASE; + default: + return SUPPORTED_CHAINS.SEPOLIA; // デフォルトチェーン + } +}; + +export const currentChain = getChainById(chainId); + +// サポートされているすべてのチェーンのリスト +export const supportedChains = Object.values(SUPPORTED_CHAINS); /** * Public client for fetching data from the blockchain From dd05c7645b01fafdf1b3d8f9c1fa6e6bdbac4a02 Mon Sep 17 00:00:00 2001 From: naizo10 <76167545+naizo01@users.noreply.github.com> Date: Sat, 18 Jan 2025 16:39:41 +0900 Subject: [PATCH 2/4] Refactor Header component and useHats hook --- pkgs/frontend/app/components/Header.tsx | 72 ++++++++----------------- pkgs/frontend/hooks/useHats.ts | 63 +++++++++++++++------- 2 files changed, 66 insertions(+), 69 deletions(-) diff --git a/pkgs/frontend/app/components/Header.tsx b/pkgs/frontend/app/components/Header.tsx index 3f82569d..b71c9bd2 100644 --- a/pkgs/frontend/app/components/Header.tsx +++ b/pkgs/frontend/app/components/Header.tsx @@ -34,45 +34,37 @@ enum HeaderType { // ネットワーク切り替えコンポーネントを作成 const NetworkSwitcher = ({ switchToChain, - connectedChainId, }: { switchToChain: (chain: Chain) => Promise; - connectedChainId?: number; }) => { - const isWrongNetwork = - connectedChainId && connectedChainId !== currentChain.id; - return ( Network - {isWrongNetwork ? ( - <> - - ⚠️ 異なるネットワークに接続されています - - switchToChain(currentChain)} - w="100%" - mb={1} - colorScheme="red" - > - {currentChain.name}に切り替える - - - ) : ( + {/* チェーン切り替えボタンを表示 */} + {/* {supportedChains.map((chain) => ( switchToChain(currentChain)} + key={chain.id} + onClick={() => switchToChain(chain)} w="100%" mb={1} - variant="solid" + variant={chain.id === currentChain.id ? "solid" : "outline"} > - {currentChain.name} + {chain.name} - )} + ))} */} + + {/* 現在のチェーンを表示 */} + switchToChain(currentChain)} + w="100%" + mb={1} + variant={"solid"} + > + {currentChain.name} + ); }; @@ -145,10 +137,6 @@ export const Header = () => { const { switchToChain } = useSwitchNetwork(); - const connectedChainId = wallets[0] - ? Number(wallets[0].chainId.split(":")[1]) - : undefined; - return headerType !== HeaderType.NonHeader ? ( @@ -189,10 +177,7 @@ export const Header = () => { {abbreviateAddress(identity.address)} - + Logout @@ -201,25 +186,12 @@ export const Header = () => { ) : ( - - {connectedChainId && connectedChainId !== currentChain.id - ? "⚠️ ネットワークを切り替えてください" - : currentChain.name} + + {currentChain.name} - + Logout diff --git a/pkgs/frontend/hooks/useHats.ts b/pkgs/frontend/hooks/useHats.ts index 29f36777..c6af40fd 100644 --- a/pkgs/frontend/hooks/useHats.ts +++ b/pkgs/frontend/hooks/useHats.ts @@ -6,6 +6,7 @@ import { } from "@hatsprotocol/sdk-v1-subgraph"; import { HATS_ABI } from "abi/hats"; import { useCallback, useEffect, useState } from "react"; +import useSWR from "swr"; import { ipfs2https, ipfs2httpsJson } from "utils/ipfs"; import { type Address, parseEventLogs } from "viem"; import { base, optimism, sepolia } from "viem/chains"; @@ -35,28 +36,52 @@ export const hatsSubgraphClient = new HatsSubgraphClient({ }, }); -export const useTreeInfo = (treeId: number) => { - const [treeInfo, setTreeInfo] = useState(); - - const { getTreeInfo } = useHats(); - - useEffect(() => { - const fetch = async () => { - setTreeInfo(undefined); - if (!treeId) return; - - const tree = await getTreeInfo({ - treeId: treeId, - }); - if (!tree) return; +export const useTreeInfo = (treeId?: number) => { + const { data, error, isLoading } = useSWR( + treeId ? ["treeInfo", treeId] : null, + async ([_, treeId]) => { + try { + const response = await getTreeInfo(treeId); + return response; + } catch (error) { + console.error("Error fetching tree info:", error); + return null; + } + }, + ); - setTreeInfo(tree); - }; + return { + treeInfo: data, + isLoading, + error, + }; +}; - fetch(); - }, [treeId, getTreeInfo]); +export const useWearerInfo = (address?: string) => { + const { data, error, isLoading } = useSWR( + address ? ["wearerInfo", address] : null, + async ([_, address]) => { + try { + const response = await getWearerInfo(address); + return response; + } catch (error) { + if (error instanceof SubgraphWearerNotExistError) { + // ユーザーがまだHatsを持っていない場合は空の配列を返す + return { + wearerStatic: [], + wearerDynamic: [], + }; + } + throw error; + } + }, + ); - return treeInfo; + return { + wearerInfo: data, + isLoading, + error, + }; }; /** From 4d82e6d4d0e61ada8582b002dfeeb492156cba36 Mon Sep 17 00:00:00 2001 From: naizo10 <76167545+naizo01@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:31:30 +0900 Subject: [PATCH 3/4] Revert "Refactor Header component and useHats hook" This reverts commit dd05c7645b01fafdf1b3d8f9c1fa6e6bdbac4a02. --- pkgs/frontend/hooks/useHats.ts | 63 ++++++++++------------------------ 1 file changed, 19 insertions(+), 44 deletions(-) diff --git a/pkgs/frontend/hooks/useHats.ts b/pkgs/frontend/hooks/useHats.ts index c6af40fd..29f36777 100644 --- a/pkgs/frontend/hooks/useHats.ts +++ b/pkgs/frontend/hooks/useHats.ts @@ -6,7 +6,6 @@ import { } from "@hatsprotocol/sdk-v1-subgraph"; import { HATS_ABI } from "abi/hats"; import { useCallback, useEffect, useState } from "react"; -import useSWR from "swr"; import { ipfs2https, ipfs2httpsJson } from "utils/ipfs"; import { type Address, parseEventLogs } from "viem"; import { base, optimism, sepolia } from "viem/chains"; @@ -36,52 +35,28 @@ export const hatsSubgraphClient = new HatsSubgraphClient({ }, }); -export const useTreeInfo = (treeId?: number) => { - const { data, error, isLoading } = useSWR( - treeId ? ["treeInfo", treeId] : null, - async ([_, treeId]) => { - try { - const response = await getTreeInfo(treeId); - return response; - } catch (error) { - console.error("Error fetching tree info:", error); - return null; - } - }, - ); +export const useTreeInfo = (treeId: number) => { + const [treeInfo, setTreeInfo] = useState(); - return { - treeInfo: data, - isLoading, - error, - }; -}; + const { getTreeInfo } = useHats(); -export const useWearerInfo = (address?: string) => { - const { data, error, isLoading } = useSWR( - address ? ["wearerInfo", address] : null, - async ([_, address]) => { - try { - const response = await getWearerInfo(address); - return response; - } catch (error) { - if (error instanceof SubgraphWearerNotExistError) { - // ユーザーがまだHatsを持っていない場合は空の配列を返す - return { - wearerStatic: [], - wearerDynamic: [], - }; - } - throw error; - } - }, - ); + useEffect(() => { + const fetch = async () => { + setTreeInfo(undefined); + if (!treeId) return; - return { - wearerInfo: data, - isLoading, - error, - }; + const tree = await getTreeInfo({ + treeId: treeId, + }); + if (!tree) return; + + setTreeInfo(tree); + }; + + fetch(); + }, [treeId, getTreeInfo]); + + return treeInfo; }; /** From dd793b6866efdd71fa60d43e2b36b2b75a6f047c Mon Sep 17 00:00:00 2001 From: naizo10 <76167545+naizo01@users.noreply.github.com> Date: Mon, 20 Jan 2025 20:52:16 +0900 Subject: [PATCH 4/4] Refactor Header component and fix navigation flow --- pkgs/frontend/app/components/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/frontend/app/components/Header.tsx b/pkgs/frontend/app/components/Header.tsx index b71c9bd2..8ae3b235 100644 --- a/pkgs/frontend/app/components/Header.tsx +++ b/pkgs/frontend/app/components/Header.tsx @@ -128,8 +128,8 @@ export const Header = () => { return; } await Promise.all(wallets.map((wallet) => wallet.disconnect())); - navigate("/login"); } + navigate("/login"); } catch (error) { console.error("ログアウトに失敗しました:", error); }