@jim4565/dapp-wrapper-react
Version:
React components and hooks for @jim4565/dapp-wrapper with persistent wallet connection
504 lines (399 loc) ⢠13.1 kB
Markdown
# @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**.
[](https://www.typescriptlang.org/)
[](https://reactjs.org/)
[](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*