diff --git a/bun.lockb b/bun.lockb index 2e62c17..809fc99 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/react/modal.tsx b/components/react/modal.tsx index 474960f..a403654 100644 --- a/components/react/modal.tsx +++ b/components/react/modal.tsx @@ -1,4 +1,20 @@ /* eslint-disable @next/next/no-img-element */ + +/** + * TailwindModal + * + * This component handles the wallet connection modal, displaying different views + * based on the current wallet status or user actions. + * + * It includes logic for: + * - Displaying a wallet list (with browser or social login options). + * - Handling Email / SMS flows (which need extra input / login hints). + * - Handling WalletConnect QR codes for desktop usage (to scan using a mobile device). + * - Handling various error states (NotExist, Connection Errors, Provider Errors, etc.). + * + * The code below is refactored for better readability and composability, especially around + * the onWalletClicked() function (which is the main handler for selecting / connecting to wallets). + */ import type { ChainWalletBase, WalletModalProps } from 'cosmos-kit'; import { WalletStatus } from 'cosmos-kit'; import React, { useCallback, Fragment, useState, useMemo, useEffect } from 'react'; @@ -8,16 +24,19 @@ import { Connecting, Error, NotExist, - QRCode, + QRCodeView, WalletList, Contacts, EmailInput, SMSInput, } from './views'; -import { useRouter } from 'next/router'; + import { ToastProvider } from '@/contexts/toastContext'; import { Web3AuthClient, Web3AuthWallet } from '@cosmos-kit/web3auth'; import { useDeviceDetect } from '@/hooks'; +import { State } from '@cosmos-kit/core'; +import { ExpiredError } from '@cosmos-kit/core'; + export enum ModalView { WalletList, QRCode, @@ -30,6 +49,25 @@ export enum ModalView { SMSInput, } +// Add new error types +const WALLET_ERRORS = { + NO_MATCHING_KEY: 'No matching key', + PROPOSAL_EXPIRED: 'Proposal expired', + RECORD_DELETED: 'Record was recently deleted', +} as const; + +/** + * Helper to check if an error message matches known wallet connection errors + */ +const isWalletConnectionError = (message?: string): boolean => { + if (!message) return false; + return ( + message.includes(WALLET_ERRORS.NO_MATCHING_KEY) || + message.includes(WALLET_ERRORS.PROPOSAL_EXPIRED) || + message.includes(WALLET_ERRORS.RECORD_DELETED) + ); +}; + export const TailwindModal: React.FC< WalletModalProps & { showContacts?: boolean; @@ -48,15 +86,14 @@ export const TailwindModal: React.FC< showMemberManagementModal = false, showMessageEditModal = false, }) => { - const router = useRouter(); - const [currentView, setCurrentView] = useState(ModalView.WalletList); const [qrWallet, setQRWallet] = useState(); const [selectedWallet, setSelectedWallet] = useState(); + const [qrState, setQRState] = useState(State.Init); + const [qrMessage, setQrMessage] = useState(''); const current = walletRepo?.current; const currentWalletData = current?.walletInfo; - const walletStatus = current?.walletStatus || WalletStatus.Disconnected; const currentWalletName = current?.walletName; const { isMobile } = useDeviceDetect(); @@ -71,7 +108,11 @@ export const TailwindModal: React.FC< setCurrentView(ModalView.WalletList); break; case WalletStatus.Connecting: - setCurrentView(ModalView.Connecting); + if (current?.walletInfo.mode === 'wallet-connect' && !isMobile) { + setCurrentView(ModalView.QRCode); + } else { + setCurrentView(ModalView.Connecting); + } break; case WalletStatus.Connected: setCurrentView(ModalView.Connected); @@ -88,45 +129,250 @@ export const TailwindModal: React.FC< } } } - }, [isOpen, walletStatus, currentWalletName, showContacts]); + }, [isOpen, walletStatus, currentWalletName, showContacts, current?.walletInfo.mode, isMobile]); + + /** + * Handle the lifecycle for QR Code actions. + * When the view is QRCode and qrWallet is set, we tie the qrUrl's state updates + * and error events to our local states (qrState, qrMessage). + */ + useEffect(() => { + if (currentView === ModalView.QRCode && qrWallet) { + // The .setActions() is a special method from the wallet client to listen for + // QR URL changes, errors, and messages that might occur during the handshake. + (qrWallet.client as any)?.setActions?.({ + qrUrl: { + state: (s: State) => setQRState(s), + message: (msg: string) => setQrMessage(msg), + }, + onError: (err: Error) => { + if (err.message?.includes('No matching key')) { + setQRState(State.Error); + setQrMessage(err.message); + qrWallet.setMessage?.(err.message); + } + }, + }); + } + }, [currentView, qrWallet]); + + /** + * Helper to handle Email or SMS wallet flows. + * These wallets need user input (email or phone), so we switch views accordingly. + */ + const handleEmailOrSmsIfNeeded = useCallback((wallet: ChainWalletBase | undefined): boolean => { + if (!wallet) return false; + + const { prettyName } = wallet.walletInfo; + + // If the wallet is "Email" or "SMS", we set appropriate inputs. + if (prettyName === 'Email') { + setCurrentView(ModalView.EmailInput); + return true; + } + if (prettyName === 'SMS') { + setCurrentView(ModalView.SMSInput); + return true; + } + return false; + }, []); + + /** + * Helper to handle a metamask extension that doesn't fully register as 'NotExist' + * in the standard wallet flow. We force set the view to NotExist if we detect the error message. + */ + const handleMetamaskErrorCheck = useCallback((wallet: ChainWalletBase) => { + if ( + wallet?.walletInfo.name === 'cosmos-extension-metamask' && + wallet.message?.includes("Cannot read properties of undefined (reading 'request')") + ) { + setCurrentView(ModalView.NotExist); + setSelectedWallet(wallet); + return true; + } + + if (wallet?.isWalletNotExist) { + setCurrentView(ModalView.NotExist); + setSelectedWallet(wallet); + return true; + } + + return false; + }, []); + + /** + * Connect with a wallet that has 'wallet-connect' mode. + * For desktop: show the QR code for scanning from a mobile device. + * For an actual mobile device: skip QR code and proceed connecting directly. + */ + const handleWalletConnectFlow = useCallback( + (wallet: ChainWalletBase, name: string) => { + // If user is already on a mobile device, do not display QR code. + if (isMobile) { + setCurrentView(ModalView.Connecting); + walletRepo?.connect(name).catch(error => { + console.error('Wallet connection error:', error); + // Check for specific wallet errors + if (isWalletConnectionError(error?.message)) { + setQRState(State.Error); + setQrMessage(error.message); + } + setCurrentView(ModalView.Error); + }); + return; + } + + // Show QR code for desktop, so the user can scan with their mobile wallet. + setQRWallet(wallet); + setCurrentView(ModalView.QRCode); + + walletRepo + ?.connect(name) + .then(() => { + if (wallet?.walletStatus === WalletStatus.Connected) { + setCurrentView(ModalView.Connected); + } + }) + .catch(error => { + console.error('Wallet connection error:', error); + // Always keep QRCode view but update its state for these errors + if (isWalletConnectionError(error?.message)) { + setQRState(State.Error); + setQrMessage(error.message); + } else { + // For other errors, show the Error view + setCurrentView(ModalView.Error); + } + }); + + // Remove the timeout and handle errors through the catch block + }, + [isMobile, walletRepo] + ); + + /** + * For wallets that do not use 'wallet-connect', + * we simply show "Connecting" while the connection is established, + * then on success, we switch to "Connected" or "Error" if it fails/times out. + */ + const handleStandardWalletFlow = useCallback( + (wallet: ChainWalletBase, name: string) => { + setQRWallet(undefined); + setCurrentView(ModalView.Connecting); + + const timeoutId = setTimeout(() => { + if (wallet?.walletStatus === WalletStatus.Connecting) { + wallet.disconnect(); + setCurrentView(ModalView.Error); + } + }, 30000); + + walletRepo + ?.connect(name) + .catch(error => { + console.error('Wallet connection error:', error); + setCurrentView(ModalView.Error); + }) + .finally(() => { + clearTimeout(timeoutId); + }); + }, + [walletRepo] + ); + /** + * The main handler for clicking on a wallet in the WalletList. + * 1) We fetch the wallet from walletRepo by name. + * 2) Check for Email / SMS (special flow). + * 3) Delay a bit (setTimeout) to handle a special metamask extension error check. + * 4) Depending on wallet mode, proceed with "wallet-connect" or normal flow. + */ const onWalletClicked = useCallback( (name: string) => { const wallet = walletRepo?.getWallet(name); - if (wallet?.walletInfo.prettyName === 'Email') { - setCurrentView(ModalView.EmailInput); - return; - } - if (wallet?.walletInfo.prettyName === 'SMS') { - setCurrentView(ModalView.SMSInput); + if (!wallet) return; + + // Step 1: Check for Email or SMS. If found, we set the corresponding view & exit. + if (handleEmailOrSmsIfNeeded(wallet)) { return; } - walletRepo?.connect(name); - + // Step 2: We do a small setTimeout to check for metamask extension error + // or if the wallet doesn't exist. This ensures the error message has time + // to populate in the wallet's state after calling `getWallet()`. setTimeout(() => { - if ( - wallet?.walletInfo.name === 'cosmos-extension-metamask' && - wallet.message?.includes("Cannot read properties of undefined (reading 'request')") - ) { - setCurrentView(ModalView.NotExist); - setSelectedWallet(wallet); - } else if (wallet?.isWalletNotExist) { - setCurrentView(ModalView.NotExist); - setSelectedWallet(wallet); - } else if (wallet?.walletInfo.mode === 'wallet-connect') { - setCurrentView(isMobile ? ModalView.Connecting : ModalView.QRCode); - setQRWallet(wallet); + if (handleMetamaskErrorCheck(wallet)) { + return; } }, 1); + + // Step 3: If the wallet is "wallet-connect" style, handle phone vs. desktop flows + if (wallet?.walletInfo.mode === 'wallet-connect') { + handleWalletConnectFlow(wallet, name); + return; + } + + // Step 4: Otherwise, handle standard extension or browser-based wallet + handleStandardWalletFlow(wallet, name); }, - [walletRepo] + [ + walletRepo, + handleEmailOrSmsIfNeeded, + handleMetamaskErrorCheck, + handleWalletConnectFlow, + handleStandardWalletFlow, + ] ); + /** + * Whenever the modal closes, if we had a QR wallet that was mid-connection, we disconnect it. + * We also reset the QR states, so next time we open it, it's fresh. + */ + useEffect(() => { + if (!isOpen) { + if (qrWallet?.walletStatus === WalletStatus.Connecting) { + qrWallet.disconnect(); + setQRWallet(undefined); + } + setQRState(State.Init); + setQrMessage(''); + } + }, [isOpen, qrWallet]); + + /** + * Called whenever the user closes the modal. + * If there's a wallet in "Connecting" state, we want to disconnect it before closing. + */ const onCloseModal = useCallback(() => { + if (qrWallet?.walletStatus === WalletStatus.Connecting) { + qrWallet.disconnect(); + } setOpen(false); - }, [setOpen]); + }, [setOpen, qrWallet]); + + /** + * If the user clicks "Back to Wallet List" while a QR code is displayed, + * and the wallet is mid-connection, we disconnect. Then we reset the QrWallet + * and show the wallet list. + */ + const onReturnToWalletList = useCallback(() => { + if (qrWallet?.walletStatus === WalletStatus.Connecting) { + qrWallet.disconnect(); + setQRWallet(undefined); + } + setCurrentView(ModalView.WalletList); + }, [qrWallet]); + /** + * Decide what to render based on currentView. + * We have: + * - WalletList (default) + * - EmailInput, SMSInput (social login flows) + * - Connected, Connecting + * - QRCode + * - Error, NotExist + * - Contacts (an address book / contact list view) + */ const _render = useMemo(() => { switch (currentView) { case ModalView.WalletList: @@ -137,6 +383,7 @@ export const TailwindModal: React.FC< wallets={walletRepo?.wallets || []} /> ); + case ModalView.EmailInput: return ( @@ -173,12 +420,14 @@ export const TailwindModal: React.FC< | undefined; if (smsWallet?.client instanceof Web3AuthClient) { + // Provide the user's phone number to the client before connecting smsWallet.client.setLoginHint(phone); walletRepo?.connect(smsWallet.walletInfo.name); } }} /> ); + case ModalView.Connected: return ( ); + case ModalView.Connecting: + // Decide a tailored message if it's a WalletConnect flow let subtitle: string; if (currentWalletData!?.mode === 'wallet-connect') { - subtitle = `Approve ${currentWalletData!.prettyName} connection request on your mobile.`; + subtitle = `Approve ${currentWalletData!.prettyName} connection request on your mobile device.`; } else { subtitle = `Open the ${ currentWalletData!?.prettyName } browser extension to connect your wallet.`; } - return ( ); + case ModalView.QRCode: return ( - setCurrentView(ModalView.WalletList)} - qrUri={qrWallet?.qrUrl.data} - name={qrWallet?.walletInfo.prettyName} - /> + ); + case ModalView.Error: return ( onWalletClicked(currentWalletData?.name!)} /> ); + case ModalView.NotExist: return ( ); + case ModalView.Contacts: return ( ); + + default: + // A fallback if we are syncing or re-connecting + return ( +
+

Reconnecting your wallet...

+
+
+ ); } }, [ currentView, onCloseModal, onWalletClicked, walletRepo, - walletRepo?.wallets, currentWalletData, current, - qrWallet?.qrUrl.data, - qrWallet?.walletInfo.prettyName, - router, onSelect, currentAddress, - showMemberManagementModal, showMessageEditModal, selectedWallet, + qrState, + qrMessage, + qrWallet, + onReturnToWalletList, + currentWalletName, ]); + /** + * Render the Modal with transitions. We wrap our entire view in ToastProvider + * to ensure we can display toast messages if needed. + */ return ( diff --git a/components/react/qrCode.tsx b/components/react/qrCode.tsx new file mode 100644 index 0000000..2be7187 --- /dev/null +++ b/components/react/qrCode.tsx @@ -0,0 +1,120 @@ +import Image from 'next/image'; +import QRCodeUtil from 'qrcode'; +import React, { FunctionComponent, ReactElement, useMemo } from 'react'; + +const generateMatrix = ( + value: string, + errorCorrectionLevel: QRCodeUtil.QRCodeErrorCorrectionLevel +) => { + const arr = Array.prototype.slice.call( + QRCodeUtil.create(value, { errorCorrectionLevel }).modules.data, + 0 + ); + const sqrt = Math.sqrt(arr.length); + return arr.reduce( + (rows, key, index) => + (index % sqrt === 0 ? rows.push([key]) : rows[rows.length - 1].push(key)) && rows, + [] + ); +}; + +export const QRCode: FunctionComponent<{ + errorCorrectionLevel?: QRCodeUtil.QRCodeErrorCorrectionLevel; + logoUrl?: string; + logoSize?: number; + size?: number; + value: string; +}> = ({ errorCorrectionLevel = 'M', logoSize = 50, logoUrl, size = 280, value }) => { + const dots = useMemo(() => { + const dots: ReactElement[] = []; + const matrix = generateMatrix(value, errorCorrectionLevel); + const cellSize = size / matrix.length; + let qrList = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 0, y: 1 }, + ]; + + qrList.forEach(({ x, y }) => { + const x1 = (matrix.length - 7) * cellSize * x; + const y1 = (matrix.length - 7) * cellSize * y; + for (let i = 0; i < 3; i++) { + dots.push( + + ); + } + }); + + const clearArenaSize = Math.floor(logoSize / cellSize); + const matrixMiddleStart = matrix.length / 2 - clearArenaSize / 2; + const matrixMiddleEnd = matrix.length / 2 + clearArenaSize / 2 - 1; + + matrix.forEach((row: QRCodeUtil.QRCode[], i: number) => { + row.forEach((_: any, j: number) => { + if (matrix[i][j]) { + if ( + !( + (i < 7 && j < 7) || + (i > matrix.length - 8 && j < 7) || + (i < 7 && j > matrix.length - 8) + ) + ) { + if ( + !( + i > matrixMiddleStart && + i < matrixMiddleEnd && + j > matrixMiddleStart && + j < matrixMiddleEnd + ) + ) { + dots.push( + + ); + } + } + } + }); + }); + + return dots; + }, [errorCorrectionLevel, logoSize, size, value]); + + const logoPosition = size / 2 - logoSize / 2; + + return ( +
+
+ {logoUrl && ( +
+ Wallet logo +
+ )} + + + {dots} + +
+
+ ); +}; diff --git a/components/react/views/Connecting.tsx b/components/react/views/Connecting.tsx index c5c4a55..9a46043 100644 --- a/components/react/views/Connecting.tsx +++ b/components/react/views/Connecting.tsx @@ -4,7 +4,6 @@ import { Dialog } from '@headlessui/react'; import { XMarkIcon } from '@heroicons/react/24/outline'; import { ChevronLeftIcon } from '@heroicons/react/20/solid'; import { getRealLogo } from '@/utils'; -import { useTheme } from '@/contexts'; export const Connecting = ({ onClose, @@ -21,7 +20,8 @@ export const Connecting = ({ title: string; subtitle: string; }) => { - const { theme } = useTheme(); + const isDarkMode = document.documentElement.classList.contains('dark'); + return (
@@ -46,9 +46,7 @@ export const Connecting = ({
{name} void; logo: string; }) => { - const { theme } = useTheme(); + const isDarkMode = document.documentElement.classList.contains('dark'); + return (
@@ -48,9 +49,9 @@ export const Error = ({
Wallet type logo - + Reconnect
diff --git a/components/react/views/NotExist.tsx b/components/react/views/NotExist.tsx index b9d710b..0504a7b 100644 --- a/components/react/views/NotExist.tsx +++ b/components/react/views/NotExist.tsx @@ -3,7 +3,6 @@ import { Dialog } from '@headlessui/react'; import { XMarkIcon, ArrowDownTrayIcon } from '@heroicons/react/24/outline'; import { ChevronLeftIcon } from '@heroicons/react/20/solid'; import { getRealLogo } from '@/utils'; -import { useTheme } from '@/contexts'; export const NotExist = ({ onClose, @@ -18,7 +17,8 @@ export const NotExist = ({ logo: string; name: string; }) => { - const { theme } = useTheme(); + const isDarkMode = document.documentElement.classList.contains('dark'); + return (
@@ -43,9 +43,7 @@ export const NotExist = ({
{name} void; - onReturn: () => void; - qrUri?: string; - name?: string; -}) => { - return ( -
-
- - - {name} - - -
-
-
- -
-
-
- ); -}; diff --git a/components/react/views/QRCodeView.tsx b/components/react/views/QRCodeView.tsx new file mode 100644 index 0000000..4309ae1 --- /dev/null +++ b/components/react/views/QRCodeView.tsx @@ -0,0 +1,199 @@ +/* eslint-disable @next/next/no-img-element */ + +import { Dialog } from '@headlessui/react'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { ChevronLeftIcon } from '@heroicons/react/20/solid'; + +import React, { Suspense, useEffect, useMemo, useState } from 'react'; +import { ChainWalletBase, State, ExpiredError } from '@cosmos-kit/core'; + +// Lazy load the QR code component +const QRCode = React.lazy(() => import('../qrCode').then(module => ({ default: module.QRCode }))); + +// Skeleton loader for QR code +const QRCodeLoader = ({ + logoUrl, + logoSize, + message, +}: { + logoUrl: string; + logoSize: number; + message?: string; +}) => ( +
+
+
+
+ Wallet logo +
+
+ + {message &&

{message}

} +
+); + +export const QRCodeView = ({ + onClose, + onReturn, + wallet, +}: { + onClose: () => void; + onReturn: () => void; + wallet: ChainWalletBase; +}) => { + const qrUrl = wallet?.qrUrl; + + // Enhanced error detection + const isExpired = + qrUrl?.message === ExpiredError.message || + (wallet?.message && wallet.message.includes('Proposal expired')); + + const hasError = + qrUrl?.state === State.Error || + (wallet?.message && + (wallet.message.includes('No matching key') || + wallet.message.includes('Record was recently deleted') || + wallet.message.includes('Proposal expired'))); + + const statusDict: Record = { + [State.Pending]: 'pending', + [State.Done]: 'done', + [State.Error]: isExpired ? 'expired' : 'error', + [State.Init]: undefined, + }; + + // If the wallet has an error message, treat it as "error" + const status = hasError ? 'error' : statusDict[qrUrl?.state ?? State.Init]; + + const errorTitle = isExpired ? 'QR Code Expired' : 'Connection Error'; + const errorMessage = isExpired + ? 'Click to refresh and try again' + : wallet?.message || qrUrl?.message || 'Failed to establish connection'; + + // Add error boundary to handle runtime errors + const handleRetry = () => { + try { + wallet?.connect(false); + } catch (error) { + console.error('Retry connection error:', error); + // Force error state if retry fails + if (error instanceof Error) { + wallet.setMessage?.(error.message); + } + } + }; + + // If the user specifically expects the QR code but there's no data or recognized status, + // show a "Re-establishing connection" fallback rather than a blank screen: + if (!status) { + return ( +
+
+ + + {wallet?.walletInfo.prettyName} + + +
+
+ +
+
+ ); + } + + // Normal flow if status = 'error' | 'expired' | 'pending' | 'done': + return ( +
+
+ + + {wallet?.walletInfo.prettyName} + + +
+ +
+ {(status === 'error' || status === 'expired') && ( +
+ {/* Dimmed QR background */} +
+
+ +
+ {/* Error overlay */} +
+

{errorTitle}

+

{errorMessage}

+
+ + +
+
+
+ )} + + {status === 'pending' && ( + <> + + + )} + + {status === 'done' && qrUrl?.data && ( + + } + > + + + )} +
+
+ ); +}; diff --git a/components/react/views/WalletList.tsx b/components/react/views/WalletList.tsx index 026f7bd..48b17d7 100644 --- a/components/react/views/WalletList.tsx +++ b/components/react/views/WalletList.tsx @@ -10,7 +10,7 @@ export const WalletList = ({ wallets, }: { onClose: () => void; - onWalletClicked: (name: string) => void; + onWalletClicked: (name: string, isMobileConnect?: boolean) => void; wallets: ChainWalletBase[]; }) => { const isDarkMode = document.documentElement.classList.contains('dark'); @@ -27,11 +27,17 @@ export const WalletList = ({ ); const mobile = wallets.filter(wallet => - ['Wallet Connect', 'Keplr Mobile', 'Cosmostation Mobile', 'Leap Mobile'].includes( - wallet.walletInfo.prettyName - ) + ['Wallet Connect', 'Keplr Mobile', 'Leap Mobile'].includes(wallet.walletInfo.prettyName) ); + const { isMobile } = useDeviceDetect(); + const hasMobileVersion = (prettyName: string) => { + return mobile.some(w => w.walletInfo.prettyName.startsWith(prettyName)); + }; + + const getMobileWalletName = (browserName: string) => { + return mobile.find(w => w.walletInfo.prettyName.startsWith(browserName))?.walletInfo.name; + }; return (

Connect Wallet

@@ -47,24 +53,37 @@ export const WalletList = ({
{browser.map(({ walletInfo: { name, prettyName, logo } }) => ( - +
+ +
))}
diff --git a/components/react/views/index.ts b/components/react/views/index.ts index ee96802..8620b19 100644 --- a/components/react/views/index.ts +++ b/components/react/views/index.ts @@ -2,7 +2,7 @@ export * from './Connected'; export * from './Connecting'; export * from './Error'; export * from './NotExist'; -export * from './QRCode'; +export * from './QRCodeView'; export * from './WalletList'; export * from './Contacts'; export * from './EmailInput'; diff --git a/components/wallet.tsx b/components/wallet.tsx index dad1247..d37dab8 100644 --- a/components/wallet.tsx +++ b/components/wallet.tsx @@ -1,4 +1,4 @@ -import React, { MouseEventHandler, useEffect, useMemo, useState } from 'react'; +import React, { MouseEventHandler, useEffect, useMemo, useState, useRef } from 'react'; import { ArrowDownTrayIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; import { ArrowUpIcon, CopyIcon } from './icons'; @@ -38,20 +38,32 @@ export const WalletSection: React.FC = ({ chainName }) => { const { connect, openView, status, username, address } = useChain(chainName); const [localStatus, setLocalStatus] = useState(status); + const timeoutRef = useRef>(); useEffect(() => { - let timeoutId: ReturnType; - if (status === WalletStatus.Connecting) { - timeoutId = setTimeout(() => { + // Clear any existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + // Set new timeout + timeoutRef.current = setTimeout(() => { setLocalStatus(WalletStatus.Error); - }, 10000); // 10 seconds timeout + }, 30000); // 30 seconds timeout } else { setLocalStatus(status); + // Clear timeout when status changes + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } } + // Cleanup on unmount return () => { - if (timeoutId) clearTimeout(timeoutId); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } }; }, [status]); @@ -204,7 +216,11 @@ export const IconWallet: React.FC = ({ chainName }) => { } let onClick; - if (status === WalletStatus.Disconnected || status === WalletStatus.Rejected) + if ( + status === WalletStatus.Disconnected || + status === WalletStatus.Rejected || + status === WalletStatus.Error + ) onClick = onClickConnect; else onClick = openView; @@ -213,12 +229,10 @@ export const IconWallet: React.FC = ({ chainName }) => { return (
diff --git a/package.json b/package.json index d0ca262..67bb19a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "@types/react": "18.0.25", "@types/react-dom": "18.0.9", "**/@chain-registry/types": "0.25.0", - "**/@cosmjs/stargate": "npm:@liftedinit/stargate@0.32.4-ll.3" + "**/@cosmjs/stargate": "npm:@liftedinit/stargate@0.32.4-ll.3", + "**/@hexxagon/feather.js": "npm:@terra-money/feather.js@1.0.9" }, "dependencies": { "@chain-registry/assets": "^1.70.67", @@ -47,7 +48,7 @@ "@tanstack/react-query-devtools": "^5.55.0", "@types/file-saver": "^2.0.7", "@types/react-syntax-highlighter": "^15.5.13", - "apexcharts": "^3.53.0", + "apexcharts": "^3.54.0", "autoprefixer": "^10.4.20", "babel-plugin-glsl": "^1.0.0", "bad-words": "^4.0.0", @@ -66,7 +67,7 @@ "next": "^14.2.8", "octokit": "^4.0.2", "postcss": "^8.4.45", - "qrcode.react": "^3.1.0", + "qrcode": "^1.5.4", "react": "18.3.1", "react-apexcharts": "^1.4.1", "react-confetti": "^6.1.0", @@ -91,6 +92,7 @@ "@types/bad-words": "^3.0.3", "@types/crypto-js": "^4.2.2", "@types/identicon.js": "^2.3.4", + "@types/qrcode": "^1.5.5", "@types/react": "18.3.5", "@types/react-dom": "18.3.0", "@types/react-scroll": "^1.8.10", @@ -98,7 +100,7 @@ "bun-types": "^1.1.29", "codecov": "^3.8.3", "dotenv": "^16.4.7", - "eslint": "8.56.0", + "eslint": "8.57.0", "eslint-config-next": "13.0.5", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", diff --git a/pages/_app.tsx b/pages/_app.tsx index b208ec4..de6b8b1 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -11,6 +11,7 @@ import { useEffect, useMemo, useState } from 'react'; import SignModal from '@/components/react/authSignerModal'; import { manifestAssets, manifestChain } from '@/config'; import { SignerOptions, wallets } from 'cosmos-kit'; + import { wallets as cosmosExtensionWallets } from '@cosmos-kit/cosmos-extension-metamask'; import { ChainProvider } from '@cosmos-kit/react'; import { Registry } from '@cosmjs/proto-signing'; @@ -154,7 +155,11 @@ function ManifestApp({ Component, pageProps }: ManifestAppProps) { ); // combine the web3auth wallets with the other wallets - const combinedWallets = [...web3AuthWallets, ...wallets, ...cosmosExtensionWallets]; + const combinedWallets = [ + ...web3AuthWallets, + ...wallets.for('keplr', 'cosmostation', 'leap', 'station', 'ledger'), + ...cosmosExtensionWallets, + ]; // this is stop ssr errors when we render the web3auth signing modal const [isBrowser, setIsBrowser] = useState(false);