UNPKG

@jim4565/dapp-wrapper-react

Version:

React components and hooks for @jim4565/dapp-wrapper with persistent wallet connection

504 lines (399 loc) • 13.1 kB
# @jim4565/dapp-wrapper-react šŸš€ **React components and hooks for [@jim4565/dapp-wrapper](https://github.com/jim4565/dapp-wrapper) with persistent wallet connection** Built with **industry standards** following **wagmi/RainbowKit patterns** and **MetaMask-level persistence**. [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/) [![React](https://img.shields.io/badge/React-18+-green.svg)](https://reactjs.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) --- ## šŸŽÆ What This Library Does This library **extends** your existing `@jim4565/dapp-wrapper` with: āœ… **Persistent Wallet Connection** - Auto-reconnect after page refresh without popups āœ… **React Integration** - Hooks, context, and components for seamless React usage āœ… **Cross-Page Persistence** - Maintain connection across routes and page refreshes āœ… **3-Tier Reconnection Strategy** - Silent → Session → Full Connect (industry standard) āœ… **Your Familiar API** - Keep using `contract.playGame(true).withValue("0.1")` syntax āœ… **Real-time Balance** - Automatic balance updates with 5-strategy fallback system **Key Benefit:** Your existing contract interaction code works exactly the same, but now with React hooks and persistent connection! --- ## šŸš€ Installation & Setup ### 1. Install the Package ```bash npm install @jim4565/dapp-wrapper-react @jim4565/dapp-wrapper react react-dom ``` ### 2. Critical Setup - Provider Configuration **āš ļø IMPORTANT:** The provider MUST wrap your entire app to enable persistent connection across all pages. #### Next.js Setup (`pages/_app.tsx` or `app/layout.tsx`) ```tsx // pages/_app.tsx (Pages Router) import { IncentivWalletProvider } from '@jim4565/dapp-wrapper-react'; import type { AppProps } from 'next/app'; // Your existing contract ABIs const gameABI = [ { "inputs": [{"internalType": "bool", "name": "_isActive", "type": "bool"}], "name": "playGame", "outputs": [], "stateMutability": "payable", "type": "function" } // ... rest of your ABI ]; const contracts = [ { name: "Game", address: "0x1234567890...", abi: gameABI }, { name: "Lottery", address: "0x0987654321...", abi: lotteryABI } ]; export default function App({ Component, pageProps }: AppProps) { return ( <IncentivWalletProvider config={{ contracts, // Register your contracts autoConnect: true, // Auto-reconnect on app start reconnectOnMount: true, // Try reconnection when mounted pollingInterval: 30000 // Balance polling (30s) }} enableLogging={process.env.NODE_ENV === 'development'} > {/* Your entire app - ALL pages will have persistent wallet connection */} <Component {...pageProps} /> </IncentivWalletProvider> ); } ``` ```tsx // app/layout.tsx (App Router) 'use client'; import { IncentivWalletProvider } from '@jim4565/dapp-wrapper-react'; const contracts = [ { name: "Game", address: "0x1234567890...", abi: gameABI } ]; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body> <IncentivWalletProvider config={{ contracts, autoConnect: true, reconnectOnMount: true, pollingInterval: 30000 }} enableLogging={process.env.NODE_ENV === 'development'} > {/* All pages and components will have persistent wallet connection */} {children} </IncentivWalletProvider> </body> </html> ); } ``` #### React Router Setup (`src/App.tsx`) ```tsx // src/App.tsx import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { IncentivWalletProvider } from '@jim4565/dapp-wrapper-react'; import { HomePage } from './pages/HomePage'; import { GamePage } from './pages/GamePage'; import { LeaderboardPage } from './pages/LeaderboardPage'; const contracts = [ { name: "Game", address: "0x1234567890...", abi: gameABI } ]; function App() { return ( <IncentivWalletProvider config={{ contracts, autoConnect: true, reconnectOnMount: true, pollingInterval: 30000 }} enableLogging={process.env.NODE_ENV === 'development'} > <BrowserRouter> {/* All routes will have persistent wallet connection */} <Routes> <Route path="/" element={<HomePage />} /> <Route path="/game" element={<GamePage />} /> <Route path="/leaderboard" element={<LeaderboardPage />} /> </Routes> </BrowserRouter> </IncentivWalletProvider> ); } export default App; ``` --- ## šŸ“± Basic Usage ### Wallet Connection Hook ```tsx import { useIncentivWallet } from '@jim4565/dapp-wrapper-react'; function WalletInfo() { const { address, // Current wallet address isConnected, // Connection status isConnecting, // Loading state balance, // Current balance in ETH isBalanceLoading, // Balance loading state refreshBalance, // Manual balance refresh connect, // Connect function disconnect, // Disconnect function error // Any connection errors } = useIncentivWallet(); if (!isConnected) { return ( <button onClick={connect} disabled={isConnecting}> {isConnecting ? 'Connecting...' : 'Connect Wallet'} </button> ); } return ( <div> <p>Address: {address}</p> <p>Balance: {isBalanceLoading ? 'Loading...' : `${balance} ETH`}</p> <button onClick={disconnect}>Disconnect</button> <button onClick={refreshBalance}>Refresh Balance</button> </div> ); } ``` ### Contract Interaction **The beauty:** Your existing contract interaction code works exactly the same! ```tsx import { useContract, useIncentivWallet } from '@jim4565/dapp-wrapper-react'; import { useState } from 'react'; function GameComponent() { const { isConnected } = useIncentivWallet(); const { contract: gameContract, isLoading, error } = useContract("Game"); const [isPlaying, setIsPlaying] = useState(false); const playGame = async () => { if (!gameContract || !isConnected) return; try { setIsPlaying(true); // Your exact @jim4565/dapp-wrapper syntax - no changes needed! await gameContract.playGame(true).withValue("0.1"); alert('Game played successfully!'); } catch (error) { console.error('Game failed:', error); alert('Game failed: ' + error.message); } finally { setIsPlaying(false); } }; if (!isConnected) { return <p>Connect your wallet to play</p>; } if (isLoading) { return <p>Loading contract...</p>; } if (error) { return <p>Error loading contract: {error.message}</p>; } return ( <button onClick={playGame} disabled={isPlaying}> {isPlaying ? 'Playing...' : 'Play Game (0.1 ETH)'} </button> ); } ``` ### Ready-to-Use Components ```tsx import { ConnectButton, WalletStatus } from '@jim4565/dapp-wrapper-react'; function Navigation() { return ( <nav> {/* Ready-to-use wallet components */} <WalletStatus showBalance={true} /> <ConnectButton /> </nav> ); } ``` --- ## āš ļø Critical Navigation Rule **ALWAYS use framework routing components to preserve wallet connection:** ```tsx // āœ… CORRECT - Preserves wallet connection import Link from 'next/link'; // Next.js // import { Link } from 'react-router-dom'; // React Router function Navigation() { return ( <nav> <Link href="/game">Game</Link> {/* āœ… Client-side routing */} <Link href="/leaderboard">Leaderboard</Link> {/* āœ… Wallet stays connected */} </nav> ); } // āŒ WRONG - Forces page reload and wallet reconnection function BadNavigation() { return ( <nav> <a href="/game">Game</a> {/* āŒ Page reload */} <a href="/leaderboard">Leaderboard</a> {/* āŒ Wallet must reconnect */} </nav> ); } ``` --- ## šŸ”§ API Reference ### Main Hooks #### `useIncentivWallet()` Main wallet hook providing connection state and actions. ```tsx const { // Connection State address, // string | undefined isConnected, // boolean isConnecting, // boolean error, // Error | null // Balance State balance, // string | undefined - current balance in ETH isBalanceLoading, // boolean - balance loading state refreshBalance, // () => Promise<void> - manual balance refresh // Actions connect, // () => Promise<void> disconnect, // () => void reconnect // () => Promise<void> } = useIncentivWallet(); ``` #### `useContract(contractName)` Access registered smart contracts. ```tsx const { contract, // Contract instance with your methods isLoading, // boolean error // Error | null } = useContract("GameContract"); // Use your contract exactly as before await contract.playGame(true).withValue("0.1"); ``` ### Components #### `<ConnectButton />` Professional wallet connection button. ```tsx <ConnectButton showAddress={true} showBalance={false} size="medium" variant="primary" /> ``` #### `<WalletStatus />` Comprehensive wallet status display. ```tsx <WalletStatus showBalance={true} balanceDecimals={4} showFullAddress={false} size="medium" /> ``` --- ## šŸ—ļø How Persistence Works ### 3-Tier Reconnection Strategy The library implements a professional reconnection strategy: 1. **Tier 1: Silent Check** - Check existing session without popups 2. **Tier 2: Session Restore** - Restore previous session silently 3. **Tier 3: Full Connect** - Show connection popup only if user connected before This ensures: - āœ… **No unwanted popups** for first-time users - āœ… **Seamless reconnection** for returning users - āœ… **Fallback strategies** if silent methods fail ### What Gets Persisted - āœ… **Wallet address** - Securely stored in localStorage - āœ… **Connection status** - Whether user was connected - āœ… **Ever connected flag** - Prevents popups for new users - āœ… **Data expiration** - Auto-clears old data (30 days) --- ## šŸ” Troubleshooting ### Provider Not Found Error ```tsx // āŒ Wrong: Component used outside provider function App() { return ( <div> <MyWalletComponent /> {/* Error: hook called outside provider */} <IncentivWalletProvider> <OtherComponents /> </IncentivWalletProvider> </div> ); } // āœ… Correct: All components inside provider function App() { return ( <IncentivWalletProvider> <div> <MyWalletComponent /> {/* Works: inside provider */} <OtherComponents /> </div> </IncentivWalletProvider> ); } ``` ### Wallet Disconnects When Navigating ```tsx // āŒ Problem: Using <a> tags forces page reload <a href="/game">Game</a> // Page reload → wallet disconnect // āœ… Solution: Use framework routing components <Link href="/game">Game</Link> // Client-side → wallet preserved ``` ### Auto-Reconnect Not Working ```tsx // Debug what's stored: import { PersistenceService } from '@jim4565/dapp-wrapper-react'; const { address, wasConnected, hasEverConnected } = PersistenceService.loadWalletData(); console.log('Stored data:', { address, wasConnected, hasEverConnected }); // For auto-reconnect to work, you need: // āœ… address: "0x..." (valid address) // āœ… wasConnected: true (was connected last session) // āœ… hasEverConnected: true (user has connected before) ``` ### Debug Mode Enable logging to see what's happening: ```tsx <IncentivWalletProvider config={config} enableLogging={true} // āœ… Enable debug logs > ``` --- ## šŸŽÆ Migration from Raw @jim4565/dapp-wrapper ```tsx // āŒ Before: Raw wrapper usage import { IncentivWrapper } from '@jim4565/dapp-wrapper'; const wrapper = new IncentivWrapper(); await wrapper.connect(); wrapper.registerContract("Game", address, abi); const contract = wrapper.getContract("Game"); await contract.playGame(true).withValue("0.1"); ``` ```tsx // āœ… After: React wrapper (no changes to contract calls!) import { IncentivWalletProvider, useContract } from '@jim4565/dapp-wrapper-react'; // 1. Wrap your app <IncentivWalletProvider config={{ contracts: [...] }}> <App /> </IncentivWalletProvider> // 2. Use hooks in components function GameComponent() { const { contract } = useContract("Game"); // Same syntax! No changes needed! await contract.playGame(true).withValue("0.1"); } ``` **Benefits of migrating:** - āœ… Persistent connection across pages - āœ… Automatic reconnection - āœ… React hooks and components - āœ… TypeScript improvements - āœ… **Your contract code stays exactly the same!** --- ## šŸ“„ License MIT License - see [LICENSE](LICENSE) file for details. --- **Built with ā¤ļø for the Incentiv ecosystem** *Making Web3 development as easy as Web2*