feat: add wallet login modal
This commit is contained in:
@@ -4,6 +4,7 @@ import { MotionProvider } from "./motion";
|
|||||||
import { ToastProvider } from "./components/Toast";
|
import { ToastProvider } from "./components/Toast";
|
||||||
import { SaveToAlbumGuideProvider } from "./components/SaveToAlbumGuide";
|
import { SaveToAlbumGuideProvider } from "./components/SaveToAlbumGuide";
|
||||||
import { RainbowWalletProvider } from "./wallet/RainbowWalletProvider";
|
import { RainbowWalletProvider } from "./wallet/RainbowWalletProvider";
|
||||||
|
import { WalletLoginModal } from "./wallet/WalletLoginModal";
|
||||||
import { WalletProvider } from "./wallet/WalletProvider";
|
import { WalletProvider } from "./wallet/WalletProvider";
|
||||||
import { PublicLayout } from "./layouts/PublicLayout";
|
import { PublicLayout } from "./layouts/PublicLayout";
|
||||||
import { LocalizedHomePage } from "./pages/LocalizedHome";
|
import { LocalizedHomePage } from "./pages/LocalizedHome";
|
||||||
@@ -37,6 +38,7 @@ export default function App() {
|
|||||||
<ImageLightboxProvider>
|
<ImageLightboxProvider>
|
||||||
<VideoPlayerProvider>
|
<VideoPlayerProvider>
|
||||||
<PageTitleProvider>
|
<PageTitleProvider>
|
||||||
|
<WalletLoginModal />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
stripLangPrefix,
|
stripLangPrefix,
|
||||||
} from "../languageRoutes";
|
} from "../languageRoutes";
|
||||||
import { useLocalizedPath } from "../useLocalizedPath";
|
import { useLocalizedPath } from "../useLocalizedPath";
|
||||||
|
import { WalletButton } from "../wallet/WalletButton";
|
||||||
|
|
||||||
type PublicNavWhich =
|
type PublicNavWhich =
|
||||||
| "home"
|
| "home"
|
||||||
@@ -657,6 +658,9 @@ export function PublicLayout() {
|
|||||||
ariaLabel={t("langLabel")}
|
ariaLabel={t("langLabel")}
|
||||||
className="hidden h-10 w-36 md:block lg:w-40"
|
className="hidden h-10 w-36 md:block lg:w-40"
|
||||||
/>
|
/>
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<WalletButton />
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
ref={desktopMenuButtonRef}
|
ref={desktopMenuButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -718,6 +722,9 @@ export function PublicLayout() {
|
|||||||
>
|
>
|
||||||
{t("popular")}
|
{t("popular")}
|
||||||
</Link>
|
</Link>
|
||||||
|
<div className="mt-2 w-full max-w-xs">
|
||||||
|
<WalletButton compact />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -143,6 +143,33 @@ export const enDict: Dict = {
|
|||||||
favoritesComingSoon: "Coming Soon",
|
favoritesComingSoon: "Coming Soon",
|
||||||
favoritesComingSoonDesc:
|
favoritesComingSoonDesc:
|
||||||
"Sign-in and favorites are in development. Stay tuned.",
|
"Sign-in and favorites are in development. Stay tuned.",
|
||||||
|
close: "Close",
|
||||||
|
walletConnect: "Connect Wallet",
|
||||||
|
walletConnectedAs: "Connected wallet",
|
||||||
|
walletDisconnect: "Disconnect",
|
||||||
|
walletLoginTitle: "Connect wallet",
|
||||||
|
walletLoginDesc:
|
||||||
|
"Sign a message to verify your wallet address. No transaction or gas fee.",
|
||||||
|
walletInjected: "Browser wallet / DApp browser",
|
||||||
|
walletInjectedDesc: "Use the wallet already available in this browser.",
|
||||||
|
walletTokenPocketQr: "TokenPocket QR login",
|
||||||
|
walletTokenPocketQrDesc:
|
||||||
|
"Recommended for China users. Scan with TokenPocket and sign to return login to this browser.",
|
||||||
|
walletGenerateQr: "Generate QR",
|
||||||
|
walletQrUseAnotherDevice: "Scan with TokenPocket on another device.",
|
||||||
|
walletOpenTokenPocket: "Open TokenPocket",
|
||||||
|
walletOpenMetaMask: "Open MetaMask",
|
||||||
|
walletOpenImToken: "Open imToken",
|
||||||
|
walletRainbowFallback: "MetaMask / imToken QR fallback",
|
||||||
|
walletRainbowFallbackDesc:
|
||||||
|
"Use RainbowKit/Reown scan login if you need QR for MetaMask or imToken.",
|
||||||
|
walletOpenRainbow: "Open QR login",
|
||||||
|
walletNetworkWarning:
|
||||||
|
"This fallback uses WalletConnect/Reown and may be unstable on some China networks. If it fails, open this site inside your wallet app.",
|
||||||
|
walletSigning: "Signing…",
|
||||||
|
walletTpExpired: "TokenPocket QR expired. Please generate a new one.",
|
||||||
|
walletTpQrFailed: "Could not generate TokenPocket QR.",
|
||||||
|
walletRainbowUnavailable: "QR login is not available yet.",
|
||||||
walletLoginSuccess: "Wallet connected",
|
walletLoginSuccess: "Wallet connected",
|
||||||
walletLoginFailed: "Wallet login failed",
|
walletLoginFailed: "Wallet login failed",
|
||||||
walletDisconnected: "Wallet disconnected",
|
walletDisconnected: "Wallet disconnected",
|
||||||
|
|||||||
@@ -143,6 +143,33 @@ export const idDict: Dict = {
|
|||||||
favoritesComingSoon: "Segera Hadir",
|
favoritesComingSoon: "Segera Hadir",
|
||||||
favoritesComingSoonDesc:
|
favoritesComingSoonDesc:
|
||||||
"Fitur masuk dan favorit sedang dikembangkan. Nantikan.",
|
"Fitur masuk dan favorit sedang dikembangkan. Nantikan.",
|
||||||
|
close: "Tutup",
|
||||||
|
walletConnect: "Hubungkan Dompet",
|
||||||
|
walletConnectedAs: "Dompet terhubung",
|
||||||
|
walletDisconnect: "Putuskan",
|
||||||
|
walletLoginTitle: "Hubungkan dompet",
|
||||||
|
walletLoginDesc:
|
||||||
|
"Tanda tangani pesan untuk memverifikasi alamat dompet. Tidak ada transaksi atau gas.",
|
||||||
|
walletInjected: "Dompet browser / browser DApp",
|
||||||
|
walletInjectedDesc: "Gunakan dompet yang tersedia di browser ini.",
|
||||||
|
walletTokenPocketQr: "Login QR TokenPocket",
|
||||||
|
walletTokenPocketQrDesc:
|
||||||
|
"Direkomendasikan untuk pengguna Tiongkok. Pindai dengan TokenPocket dan tanda tangani untuk login di browser ini.",
|
||||||
|
walletGenerateQr: "Buat QR",
|
||||||
|
walletQrUseAnotherDevice: "Pindai dengan TokenPocket di perangkat lain.",
|
||||||
|
walletOpenTokenPocket: "Buka TokenPocket",
|
||||||
|
walletOpenMetaMask: "Buka MetaMask",
|
||||||
|
walletOpenImToken: "Buka imToken",
|
||||||
|
walletRainbowFallback: "Fallback QR MetaMask / imToken",
|
||||||
|
walletRainbowFallbackDesc:
|
||||||
|
"Gunakan RainbowKit/Reown jika butuh QR untuk MetaMask atau imToken.",
|
||||||
|
walletOpenRainbow: "Buka login QR",
|
||||||
|
walletNetworkWarning:
|
||||||
|
"Fallback ini memakai WalletConnect/Reown dan mungkin tidak stabil di beberapa jaringan Tiongkok. Jika gagal, buka situs ini di browser DApp dompet.",
|
||||||
|
walletSigning: "Menandatangani…",
|
||||||
|
walletTpExpired: "QR TokenPocket kedaluwarsa. Buat yang baru.",
|
||||||
|
walletTpQrFailed: "Tidak dapat membuat QR TokenPocket.",
|
||||||
|
walletRainbowUnavailable: "Login QR belum tersedia.",
|
||||||
walletLoginSuccess: "Dompet terhubung",
|
walletLoginSuccess: "Dompet terhubung",
|
||||||
walletLoginFailed: "Login dompet gagal",
|
walletLoginFailed: "Login dompet gagal",
|
||||||
walletDisconnected: "Dompet terputus",
|
walletDisconnected: "Dompet terputus",
|
||||||
|
|||||||
@@ -143,6 +143,34 @@ export const jaDict: Dict = {
|
|||||||
favorites: "お気に入り",
|
favorites: "お気に入り",
|
||||||
favoritesComingSoon: "近日公開",
|
favoritesComingSoon: "近日公開",
|
||||||
favoritesComingSoonDesc: "ログインとお気に入り機能は開発中です。お楽しみに。",
|
favoritesComingSoonDesc: "ログインとお気に入り機能は開発中です。お楽しみに。",
|
||||||
|
close: "閉じる",
|
||||||
|
walletConnect: "ウォレット接続",
|
||||||
|
walletConnectedAs: "接続中のウォレット",
|
||||||
|
walletDisconnect: "切断",
|
||||||
|
walletLoginTitle: "ウォレットを接続",
|
||||||
|
walletLoginDesc:
|
||||||
|
"メッセージ署名でウォレットアドレスを確認します。取引やガス代は発生しません。",
|
||||||
|
walletInjected: "ブラウザウォレット / DApp ブラウザ",
|
||||||
|
walletInjectedDesc: "このブラウザで利用可能なウォレットを使います。",
|
||||||
|
walletTokenPocketQr: "TokenPocket QR ログイン",
|
||||||
|
walletTokenPocketQrDesc:
|
||||||
|
"中国ユーザーに推奨。TokenPocket でスキャンして署名すると、このブラウザでログインが完了します。",
|
||||||
|
walletGenerateQr: "QR を生成",
|
||||||
|
walletQrUseAnotherDevice: "別の端末の TokenPocket でスキャンしてください。",
|
||||||
|
walletOpenTokenPocket: "TokenPocket を開く",
|
||||||
|
walletOpenMetaMask: "MetaMask を開く",
|
||||||
|
walletOpenImToken: "imToken を開く",
|
||||||
|
walletRainbowFallback: "MetaMask / imToken QR 予備",
|
||||||
|
walletRainbowFallbackDesc:
|
||||||
|
"MetaMask または imToken の QR が必要な場合は RainbowKit/Reown 接続を使います。",
|
||||||
|
walletOpenRainbow: "QR ログインを開く",
|
||||||
|
walletNetworkWarning:
|
||||||
|
"この予備方式は WalletConnect/Reown に依存するため、中国の一部ネットワークでは不安定な場合があります。失敗した場合はウォレット内蔵ブラウザで開いてください。",
|
||||||
|
walletSigning: "署名中…",
|
||||||
|
walletTpExpired:
|
||||||
|
"TokenPocket QR の有効期限が切れました。再生成してください。",
|
||||||
|
walletTpQrFailed: "TokenPocket QR を生成できませんでした。",
|
||||||
|
walletRainbowUnavailable: "QR ログインは現在利用できません。",
|
||||||
walletLoginSuccess: "ウォレットを接続しました",
|
walletLoginSuccess: "ウォレットを接続しました",
|
||||||
walletLoginFailed: "ウォレットログインに失敗しました",
|
walletLoginFailed: "ウォレットログインに失敗しました",
|
||||||
walletDisconnected: "ウォレットを切断しました",
|
walletDisconnected: "ウォレットを切断しました",
|
||||||
|
|||||||
@@ -143,6 +143,33 @@ export const koDict: Dict = {
|
|||||||
favoritesComingSoon: "출시 예정",
|
favoritesComingSoon: "출시 예정",
|
||||||
favoritesComingSoonDesc:
|
favoritesComingSoonDesc:
|
||||||
"로그인과 즐겨찾기 기능을 개발 중입니다. 많은 기대 부탁드립니다.",
|
"로그인과 즐겨찾기 기능을 개발 중입니다. 많은 기대 부탁드립니다.",
|
||||||
|
close: "닫기",
|
||||||
|
walletConnect: "지갑 연결",
|
||||||
|
walletConnectedAs: "연결된 지갑",
|
||||||
|
walletDisconnect: "연결 해제",
|
||||||
|
walletLoginTitle: "지갑 연결",
|
||||||
|
walletLoginDesc:
|
||||||
|
"메시지 서명으로 지갑 주소를 확인합니다. 트랜잭션이나 가스 수수료는 없습니다.",
|
||||||
|
walletInjected: "브라우저 지갑 / DApp 브라우저",
|
||||||
|
walletInjectedDesc: "현재 브라우저에서 사용 가능한 지갑을 사용합니다.",
|
||||||
|
walletTokenPocketQr: "TokenPocket QR 로그인",
|
||||||
|
walletTokenPocketQrDesc:
|
||||||
|
"중국 사용자에게 권장됩니다. TokenPocket으로 스캔하고 서명하면 이 브라우저에서 로그인이 완료됩니다.",
|
||||||
|
walletGenerateQr: "QR 생성",
|
||||||
|
walletQrUseAnotherDevice: "다른 기기의 TokenPocket으로 스캔하세요.",
|
||||||
|
walletOpenTokenPocket: "TokenPocket 열기",
|
||||||
|
walletOpenMetaMask: "MetaMask 열기",
|
||||||
|
walletOpenImToken: "imToken 열기",
|
||||||
|
walletRainbowFallback: "MetaMask / imToken QR 대체",
|
||||||
|
walletRainbowFallbackDesc:
|
||||||
|
"MetaMask 또는 imToken QR이 필요하면 RainbowKit/Reown 연결을 사용하세요.",
|
||||||
|
walletOpenRainbow: "QR 로그인 열기",
|
||||||
|
walletNetworkWarning:
|
||||||
|
"이 대체 방식은 WalletConnect/Reown을 사용하므로 일부 중국 네트워크에서 불안정할 수 있습니다. 실패하면 지갑 DApp 브라우저에서 사이트를 여세요.",
|
||||||
|
walletSigning: "서명 중…",
|
||||||
|
walletTpExpired: "TokenPocket QR이 만료되었습니다. 새로 생성하세요.",
|
||||||
|
walletTpQrFailed: "TokenPocket QR을 생성할 수 없습니다.",
|
||||||
|
walletRainbowUnavailable: "QR 로그인을 사용할 수 없습니다.",
|
||||||
walletLoginSuccess: "지갑이 연결되었습니다",
|
walletLoginSuccess: "지갑이 연결되었습니다",
|
||||||
walletLoginFailed: "지갑 로그인에 실패했습니다",
|
walletLoginFailed: "지갑 로그인에 실패했습니다",
|
||||||
walletDisconnected: "지갑 연결이 해제되었습니다",
|
walletDisconnected: "지갑 연결이 해제되었습니다",
|
||||||
|
|||||||
@@ -143,6 +143,33 @@ export const msDict: Dict = {
|
|||||||
favoritesComingSoon: "Akan Hadir",
|
favoritesComingSoon: "Akan Hadir",
|
||||||
favoritesComingSoonDesc:
|
favoritesComingSoonDesc:
|
||||||
"Ciri log masuk dan kegemaran sedang dibangunkan. Nantikan.",
|
"Ciri log masuk dan kegemaran sedang dibangunkan. Nantikan.",
|
||||||
|
close: "Tutup",
|
||||||
|
walletConnect: "Sambung Dompet",
|
||||||
|
walletConnectedAs: "Dompet disambungkan",
|
||||||
|
walletDisconnect: "Putuskan",
|
||||||
|
walletLoginTitle: "Sambung dompet",
|
||||||
|
walletLoginDesc:
|
||||||
|
"Tandatangani mesej untuk mengesahkan alamat dompet. Tiada transaksi atau gas.",
|
||||||
|
walletInjected: "Dompet pelayar / pelayar DApp",
|
||||||
|
walletInjectedDesc: "Gunakan dompet yang tersedia dalam pelayar ini.",
|
||||||
|
walletTokenPocketQr: "Log masuk QR TokenPocket",
|
||||||
|
walletTokenPocketQrDesc:
|
||||||
|
"Disyorkan untuk pengguna China. Imbas dengan TokenPocket dan tandatangani untuk log masuk pada pelayar ini.",
|
||||||
|
walletGenerateQr: "Jana QR",
|
||||||
|
walletQrUseAnotherDevice: "Imbas dengan TokenPocket pada peranti lain.",
|
||||||
|
walletOpenTokenPocket: "Buka TokenPocket",
|
||||||
|
walletOpenMetaMask: "Buka MetaMask",
|
||||||
|
walletOpenImToken: "Buka imToken",
|
||||||
|
walletRainbowFallback: "Sandaran QR MetaMask / imToken",
|
||||||
|
walletRainbowFallbackDesc:
|
||||||
|
"Gunakan RainbowKit/Reown jika perlu QR untuk MetaMask atau imToken.",
|
||||||
|
walletOpenRainbow: "Buka log masuk QR",
|
||||||
|
walletNetworkWarning:
|
||||||
|
"Kaedah sandaran ini menggunakan WalletConnect/Reown dan mungkin tidak stabil pada sesetengah rangkaian China. Jika gagal, buka laman ini dalam pelayar DApp dompet.",
|
||||||
|
walletSigning: "Menandatangani…",
|
||||||
|
walletTpExpired: "QR TokenPocket tamat tempoh. Sila jana semula.",
|
||||||
|
walletTpQrFailed: "Tidak dapat menjana QR TokenPocket.",
|
||||||
|
walletRainbowUnavailable: "Log masuk QR belum tersedia.",
|
||||||
walletLoginSuccess: "Dompet disambungkan",
|
walletLoginSuccess: "Dompet disambungkan",
|
||||||
walletLoginFailed: "Log masuk dompet gagal",
|
walletLoginFailed: "Log masuk dompet gagal",
|
||||||
walletDisconnected: "Dompet diputuskan",
|
walletDisconnected: "Dompet diputuskan",
|
||||||
|
|||||||
@@ -143,6 +143,33 @@ export const viDict: Dict = {
|
|||||||
favoritesComingSoon: "Sắp ra mắt",
|
favoritesComingSoon: "Sắp ra mắt",
|
||||||
favoritesComingSoonDesc:
|
favoritesComingSoonDesc:
|
||||||
"Tính năng đăng nhập và yêu thích đang phát triển. Hãy chờ đón.",
|
"Tính năng đăng nhập và yêu thích đang phát triển. Hãy chờ đón.",
|
||||||
|
close: "Đóng",
|
||||||
|
walletConnect: "Kết nối ví",
|
||||||
|
walletConnectedAs: "Ví đã kết nối",
|
||||||
|
walletDisconnect: "Ngắt kết nối",
|
||||||
|
walletLoginTitle: "Kết nối ví",
|
||||||
|
walletLoginDesc:
|
||||||
|
"Ký tin nhắn để xác minh địa chỉ ví. Không có giao dịch hay phí gas.",
|
||||||
|
walletInjected: "Ví trình duyệt / trình duyệt DApp",
|
||||||
|
walletInjectedDesc: "Dùng ví đã có trong trình duyệt hiện tại.",
|
||||||
|
walletTokenPocketQr: "Đăng nhập QR TokenPocket",
|
||||||
|
walletTokenPocketQrDesc:
|
||||||
|
"Khuyến nghị cho người dùng Trung Quốc. Quét bằng TokenPocket và ký để đăng nhập trên trình duyệt này.",
|
||||||
|
walletGenerateQr: "Tạo QR",
|
||||||
|
walletQrUseAnotherDevice: "Quét bằng TokenPocket trên thiết bị khác.",
|
||||||
|
walletOpenTokenPocket: "Mở TokenPocket",
|
||||||
|
walletOpenMetaMask: "Mở MetaMask",
|
||||||
|
walletOpenImToken: "Mở imToken",
|
||||||
|
walletRainbowFallback: "QR dự phòng MetaMask / imToken",
|
||||||
|
walletRainbowFallbackDesc:
|
||||||
|
"Dùng RainbowKit/Reown nếu cần QR cho MetaMask hoặc imToken.",
|
||||||
|
walletOpenRainbow: "Mở đăng nhập QR",
|
||||||
|
walletNetworkWarning:
|
||||||
|
"Cách dự phòng này dùng WalletConnect/Reown và có thể không ổn định trên một số mạng ở Trung Quốc. Nếu lỗi, hãy mở trang trong trình duyệt DApp của ví.",
|
||||||
|
walletSigning: "Đang ký…",
|
||||||
|
walletTpExpired: "QR TokenPocket đã hết hạn. Vui lòng tạo lại.",
|
||||||
|
walletTpQrFailed: "Không thể tạo QR TokenPocket.",
|
||||||
|
walletRainbowUnavailable: "Đăng nhập QR chưa khả dụng.",
|
||||||
walletLoginSuccess: "Đã kết nối ví",
|
walletLoginSuccess: "Đã kết nối ví",
|
||||||
walletLoginFailed: "Đăng nhập ví thất bại",
|
walletLoginFailed: "Đăng nhập ví thất bại",
|
||||||
walletDisconnected: "Đã ngắt kết nối ví",
|
walletDisconnected: "Đã ngắt kết nối ví",
|
||||||
|
|||||||
@@ -140,6 +140,32 @@ export const zhDict: Dict = {
|
|||||||
favorites: "我的收藏",
|
favorites: "我的收藏",
|
||||||
favoritesComingSoon: "功能即将推出",
|
favoritesComingSoon: "功能即将推出",
|
||||||
favoritesComingSoonDesc: "登入与收藏功能开发中,敬请期待。",
|
favoritesComingSoonDesc: "登入与收藏功能开发中,敬请期待。",
|
||||||
|
close: "关闭",
|
||||||
|
walletConnect: "连接钱包",
|
||||||
|
walletConnectedAs: "已连接钱包",
|
||||||
|
walletDisconnect: "断开连接",
|
||||||
|
walletLoginTitle: "连接钱包",
|
||||||
|
walletLoginDesc: "签名验证钱包地址,不会发起交易,也不需要 Gas。",
|
||||||
|
walletInjected: "浏览器钱包 / 钱包内置浏览器",
|
||||||
|
walletInjectedDesc: "使用当前浏览器里已经注入的钱包。",
|
||||||
|
walletTokenPocketQr: "TokenPocket 扫码登录",
|
||||||
|
walletTokenPocketQrDesc:
|
||||||
|
"推荐中国用户使用。用 TokenPocket 扫码签名后,会回到当前浏览器完成登录。",
|
||||||
|
walletGenerateQr: "生成二维码",
|
||||||
|
walletQrUseAnotherDevice: "请用另一台设备上的 TokenPocket 扫码。",
|
||||||
|
walletOpenTokenPocket: "打开 TokenPocket",
|
||||||
|
walletOpenMetaMask: "打开 MetaMask",
|
||||||
|
walletOpenImToken: "打开 imToken",
|
||||||
|
walletRainbowFallback: "MetaMask / imToken 扫码备用",
|
||||||
|
walletRainbowFallbackDesc:
|
||||||
|
"如果需要 MetaMask 或 imToken 扫码,可使用 RainbowKit/Reown 连接。",
|
||||||
|
walletOpenRainbow: "打开扫码登录",
|
||||||
|
walletNetworkWarning:
|
||||||
|
"此备用方式依赖 WalletConnect/Reown,在部分中国网络可能不稳定。失败时请用钱包内置浏览器打开本站。",
|
||||||
|
walletSigning: "签名中…",
|
||||||
|
walletTpExpired: "TokenPocket 二维码已过期,请重新生成。",
|
||||||
|
walletTpQrFailed: "无法生成 TokenPocket 二维码。",
|
||||||
|
walletRainbowUnavailable: "扫码登录暂不可用。",
|
||||||
walletLoginSuccess: "钱包已连接",
|
walletLoginSuccess: "钱包已连接",
|
||||||
walletLoginFailed: "钱包登录失败",
|
walletLoginFailed: "钱包登录失败",
|
||||||
walletDisconnected: "钱包已断开",
|
walletDisconnected: "钱包已断开",
|
||||||
|
|||||||
78
src/wallet/WalletButton.tsx
Normal file
78
src/wallet/WalletButton.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useI18n } from "../i18n";
|
||||||
|
import { shortenAddress, useWallet } from "./WalletProvider";
|
||||||
|
|
||||||
|
export function WalletButton({ compact = false }: { compact?: boolean }) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const wallet = useWallet();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const closeOnOutside = (event: MouseEvent | TouchEvent) => {
|
||||||
|
if (!rootRef.current?.contains(event.target as Node)) setOpen(false);
|
||||||
|
};
|
||||||
|
const closeOnEscape = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") setOpen(false);
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", closeOnOutside);
|
||||||
|
document.addEventListener("touchstart", closeOnOutside);
|
||||||
|
window.addEventListener("keydown", closeOnEscape);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", closeOnOutside);
|
||||||
|
document.removeEventListener("touchstart", closeOnOutside);
|
||||||
|
window.removeEventListener("keydown", closeOnEscape);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
if (wallet.status === "loggedIn" && wallet.address) {
|
||||||
|
return (
|
||||||
|
<div ref={rootRef} className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((value) => !value)}
|
||||||
|
className={[
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-full border border-ark-gold/45 bg-ark-gold/10 px-3 text-sm font-semibold text-ark-gold2 outline-none transition hover:bg-ark-gold/15 focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg",
|
||||||
|
compact ? "w-full" : "",
|
||||||
|
].join(" ")}
|
||||||
|
aria-label={t("walletConnectedAs")}
|
||||||
|
aria-expanded={open}
|
||||||
|
>
|
||||||
|
<span className="mr-2 h-2 w-2 rounded-full bg-emerald-400" />
|
||||||
|
{shortenAddress(wallet.address)}
|
||||||
|
</button>
|
||||||
|
{open ? (
|
||||||
|
<div className="absolute right-0 top-[calc(100%+0.5rem)] z-50 w-52 rounded-2xl border border-white/10 bg-[#1c1c21]/95 p-2 shadow-2xl shadow-black/70 ring-1 ring-ark-line/80 backdrop-blur-xl">
|
||||||
|
<div className="truncate px-3 py-2 text-xs text-neutral-400">
|
||||||
|
{wallet.address}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
wallet.logout();
|
||||||
|
}}
|
||||||
|
className="w-full rounded-xl px-3 py-2 text-left text-sm font-medium text-red-200 transition hover:bg-red-500/10"
|
||||||
|
>
|
||||||
|
{t("walletDisconnect")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={wallet.openLoginModal}
|
||||||
|
className={[
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-full border border-ark-gold/50 bg-ark-gold px-4 text-sm font-bold text-black outline-none transition hover:bg-ark-gold2 focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg",
|
||||||
|
compact ? "w-full" : "",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{wallet.status === "loading" ? t("loading") : t("walletConnect")}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
279
src/wallet/WalletLoginModal.tsx
Normal file
279
src/wallet/WalletLoginModal.tsx
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import { useConnectModal } from "@rainbow-me/rainbowkit";
|
||||||
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useAccount, useSignMessage } from "wagmi";
|
||||||
|
import { useToast } from "../components/Toast";
|
||||||
|
import { useI18n } from "../i18n";
|
||||||
|
import {
|
||||||
|
createTokenPocketLoginRequest,
|
||||||
|
fetchTokenPocketLoginResult,
|
||||||
|
requestWalletNonce,
|
||||||
|
verifyWalletSignature,
|
||||||
|
type TokenPocketLoginRequest,
|
||||||
|
} from "./api";
|
||||||
|
import { openWalletDeepLink } from "./deepLinks";
|
||||||
|
import { useWallet } from "./WalletProvider";
|
||||||
|
|
||||||
|
const pollIntervalMs = 1800;
|
||||||
|
|
||||||
|
type ModalState = "idle" | "tpLoading" | "tpPolling" | "rainbowSigning";
|
||||||
|
|
||||||
|
export function WalletLoginModal() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const wallet = useWallet();
|
||||||
|
const { openConnectModal } = useConnectModal();
|
||||||
|
const { address, isConnected } = useAccount();
|
||||||
|
const { signMessageAsync } = useSignMessage();
|
||||||
|
const [state, setState] = useState<ModalState>("idle");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [tpRequest, setTpRequest] = useState<TokenPocketLoginRequest | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [rainbowPending, setRainbowPending] = useState(false);
|
||||||
|
const rainbowSigningRef = useRef(false);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (state === "tpLoading" || state === "rainbowSigning") return;
|
||||||
|
wallet.closeLoginModal();
|
||||||
|
setError("");
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wallet.loginModalOpen || !tpRequest) return;
|
||||||
|
if (state !== "tpPolling") return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
const result = await fetchTokenPocketLoginResult(tpRequest.actionId);
|
||||||
|
if (cancelled) return;
|
||||||
|
if (result.status === "completed") {
|
||||||
|
const verified = await verifyWalletSignature({
|
||||||
|
address: result.address,
|
||||||
|
message: result.message,
|
||||||
|
signature: result.signature,
|
||||||
|
});
|
||||||
|
if (cancelled) return;
|
||||||
|
wallet.completeLogin(verified.token, verified.wallet);
|
||||||
|
showToast(t("walletLoginSuccess"));
|
||||||
|
setState("idle");
|
||||||
|
setTpRequest(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.status === "expired" || result.status === "failed") {
|
||||||
|
setState("idle");
|
||||||
|
setError(result.error || t("walletTpExpired"));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setError(t("walletLoginFailed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void poll();
|
||||||
|
const timer = window.setInterval(() => void poll(), pollIntervalMs);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, [state, tpRequest, t, showToast, wallet]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!rainbowPending || !isConnected || !address) return;
|
||||||
|
if (rainbowSigningRef.current) return;
|
||||||
|
rainbowSigningRef.current = true;
|
||||||
|
setState("rainbowSigning");
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const nonce = await requestWalletNonce(address);
|
||||||
|
const signature = await signMessageAsync({ message: nonce.message });
|
||||||
|
const verified = await verifyWalletSignature({
|
||||||
|
address,
|
||||||
|
message: nonce.message,
|
||||||
|
signature,
|
||||||
|
});
|
||||||
|
wallet.completeLogin(verified.token, verified.wallet);
|
||||||
|
showToast(t("walletLoginSuccess"));
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : t("walletLoginFailed");
|
||||||
|
setError(message || t("walletLoginFailed"));
|
||||||
|
showToast(t("walletLoginFailed"), "error");
|
||||||
|
} finally {
|
||||||
|
setState("idle");
|
||||||
|
setRainbowPending(false);
|
||||||
|
rainbowSigningRef.current = false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [
|
||||||
|
address,
|
||||||
|
isConnected,
|
||||||
|
rainbowPending,
|
||||||
|
showToast,
|
||||||
|
signMessageAsync,
|
||||||
|
t,
|
||||||
|
wallet,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!wallet.loginModalOpen) return null;
|
||||||
|
|
||||||
|
const startInjected = async () => {
|
||||||
|
setError("");
|
||||||
|
setState("idle");
|
||||||
|
await wallet.signInInjected().catch(() => undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTokenPocketQr = async () => {
|
||||||
|
setError("");
|
||||||
|
setState("tpLoading");
|
||||||
|
try {
|
||||||
|
const req = await createTokenPocketLoginRequest();
|
||||||
|
setTpRequest(req);
|
||||||
|
setState("tpPolling");
|
||||||
|
} catch {
|
||||||
|
setState("idle");
|
||||||
|
setError(t("walletTpQrFailed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRainbowFallback = () => {
|
||||||
|
setError("");
|
||||||
|
setRainbowPending(true);
|
||||||
|
openConnectModal?.();
|
||||||
|
if (!openConnectModal) setError(t("walletRainbowUnavailable"));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[120] flex items-end justify-center bg-black/70 px-3 pb-3 pt-10 backdrop-blur-sm md:items-center md:p-6"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="wallet-login-title"
|
||||||
|
>
|
||||||
|
<div className="max-h-[92dvh] w-full max-w-[520px] overflow-y-auto rounded-3xl border border-white/10 bg-[#17171d] p-5 shadow-2xl shadow-black/70 md:p-6">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
id="wallet-login-title"
|
||||||
|
className="text-xl font-semibold text-white"
|
||||||
|
>
|
||||||
|
{t("walletLoginTitle")}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-neutral-400">
|
||||||
|
{t("walletLoginDesc")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
className="rounded-full border border-white/10 px-3 py-1.5 text-sm text-neutral-300 transition hover:border-ark-gold/50 hover:text-ark-gold"
|
||||||
|
>
|
||||||
|
{t("close")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 grid gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void startInjected()}
|
||||||
|
className="rounded-2xl border border-ark-gold/40 bg-ark-gold/10 p-4 text-left transition hover:bg-ark-gold/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80"
|
||||||
|
>
|
||||||
|
<span className="block font-semibold text-ark-gold2">
|
||||||
|
{t("walletInjected")}
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 block text-sm text-neutral-400">
|
||||||
|
{t("walletInjectedDesc")}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||||
|
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-white">
|
||||||
|
{t("walletTokenPocketQr")}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm leading-6 text-neutral-400">
|
||||||
|
{t("walletTokenPocketQrDesc")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void startTokenPocketQr()}
|
||||||
|
disabled={state === "tpLoading"}
|
||||||
|
className="shrink-0 rounded-full bg-ark-gold px-4 py-2 text-sm font-semibold text-black transition hover:bg-ark-gold2 disabled:cursor-wait disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{state === "tpLoading" ? t("loading") : t("walletGenerateQr")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{tpRequest ? (
|
||||||
|
<div className="mt-4 grid place-items-center gap-3 rounded-2xl bg-white p-4 text-center">
|
||||||
|
<QRCodeSVG value={tpRequest.qrUrl} size={196} level="M" />
|
||||||
|
<p className="text-xs font-medium text-neutral-700">
|
||||||
|
{t("walletQrUseAnotherDevice")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2 sm:grid-cols-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openWalletDeepLink("tokenPocket")}
|
||||||
|
className="rounded-2xl border border-white/10 bg-[#20202a] px-4 py-3 text-sm font-semibold text-neutral-100 transition hover:border-ark-gold/40"
|
||||||
|
>
|
||||||
|
{t("walletOpenTokenPocket")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openWalletDeepLink("metaMask")}
|
||||||
|
className="rounded-2xl border border-white/10 bg-[#20202a] px-4 py-3 text-sm font-semibold text-neutral-100 transition hover:border-ark-gold/40"
|
||||||
|
>
|
||||||
|
{t("walletOpenMetaMask")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openWalletDeepLink("imToken")}
|
||||||
|
className="rounded-2xl border border-white/10 bg-[#20202a] px-4 py-3 text-sm font-semibold text-neutral-100 transition hover:border-ark-gold/40"
|
||||||
|
>
|
||||||
|
{t("walletOpenImToken")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-2xl border border-dashed border-white/15 p-4">
|
||||||
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-white">
|
||||||
|
{t("walletRainbowFallback")}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm leading-6 text-neutral-400">
|
||||||
|
{t("walletRainbowFallbackDesc")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={startRainbowFallback}
|
||||||
|
disabled={state === "rainbowSigning"}
|
||||||
|
className="shrink-0 rounded-full border border-ark-gold/50 px-4 py-2 text-sm font-semibold text-ark-gold transition hover:bg-ark-gold/10 disabled:cursor-wait disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{state === "rainbowSigning"
|
||||||
|
? t("walletSigning")
|
||||||
|
: t("walletOpenRainbow")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-xs leading-5 text-yellow-200/80">
|
||||||
|
{t("walletNetworkWarning")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<p className="mt-4 rounded-2xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-200">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/wallet/deepLinks.ts
Normal file
29
src/wallet/deepLinks.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
type WalletKind = "tokenPocket" | "metaMask" | "imToken";
|
||||||
|
|
||||||
|
function currentDappUrl(): string {
|
||||||
|
if (typeof window === "undefined") return "https://ark-library.com";
|
||||||
|
return window.location.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function walletDeepLink(
|
||||||
|
kind: WalletKind,
|
||||||
|
dappUrl = currentDappUrl(),
|
||||||
|
): string {
|
||||||
|
switch (kind) {
|
||||||
|
case "tokenPocket":
|
||||||
|
return `tpdapp://open?params=${encodeURIComponent(
|
||||||
|
JSON.stringify({ url: dappUrl, chain: "ETH" }),
|
||||||
|
)}`;
|
||||||
|
case "metaMask":
|
||||||
|
return `https://metamask.app.link/dapp/${dappUrl.replace(/^https?:\/\//, "")}`;
|
||||||
|
case "imToken":
|
||||||
|
return `imtokenv2://navigate/DappView?url=${encodeURIComponent(dappUrl)}`;
|
||||||
|
default:
|
||||||
|
return dappUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openWalletDeepLink(kind: WalletKind): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
window.location.href = walletDeepLink(kind);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user