mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-09 08:13:06 +00:00
chore: simplify AppKit API providers
This commit is contained in:
parent
1bd93854ec
commit
858c1bcc35
@ -9,42 +9,19 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Bitcoin, Coins } from 'lucide-react';
|
||||
import {
|
||||
useAppKit,
|
||||
useAppKitAccount,
|
||||
useDisconnect,
|
||||
useAppKitState,
|
||||
} from '@reown/appkit/react';
|
||||
import { useAuth, useAppKitWallet } from '@opchan/react';
|
||||
|
||||
interface WalletDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConnect: () => void;
|
||||
}
|
||||
|
||||
export function WalletConnectionDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConnect,
|
||||
}: WalletDialogProps) {
|
||||
// Always call hooks to follow React rules
|
||||
const { initialized } = useAppKitState();
|
||||
const appKit = useAppKit();
|
||||
const { disconnect } = useDisconnect();
|
||||
|
||||
// Get account info for different chains
|
||||
const bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
|
||||
const ethereumAccount = useAppKitAccount({ namespace: 'eip155' });
|
||||
|
||||
// Determine which account is connected
|
||||
const isBitcoinConnected = bitcoinAccount.isConnected;
|
||||
const isEthereumConnected = ethereumAccount.isConnected;
|
||||
const isConnected = isBitcoinConnected || isEthereumConnected;
|
||||
|
||||
// Get the active account info
|
||||
const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount;
|
||||
const activeAddress = activeAccount.address;
|
||||
const activeChain = isBitcoinConnected ? 'Bitcoin' : 'Ethereum';
|
||||
const { connect, disconnect } = useAuth();
|
||||
const wallet = useAppKitWallet();
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
await disconnect();
|
||||
@ -52,35 +29,23 @@ export function WalletConnectionDialog({
|
||||
};
|
||||
|
||||
const handleBitcoinConnect = () => {
|
||||
if (!initialized || !appKit) {
|
||||
console.error('AppKit not initialized');
|
||||
if (!wallet.isInitialized) {
|
||||
console.error('Wallet not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
appKit.open({
|
||||
view: 'Connect',
|
||||
namespace: 'bip122',
|
||||
});
|
||||
onConnect();
|
||||
onOpenChange(false);
|
||||
connect('bitcoin');
|
||||
};
|
||||
|
||||
const handleEthereumConnect = () => {
|
||||
if (!initialized || !appKit) {
|
||||
console.error('AppKit not initialized');
|
||||
if (!wallet.isInitialized) {
|
||||
console.error('Wallet not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
appKit.open({
|
||||
view: 'Connect',
|
||||
namespace: 'eip155',
|
||||
});
|
||||
onConnect();
|
||||
onOpenChange(false);
|
||||
connect('ethereum');
|
||||
};
|
||||
|
||||
// Show loading state if AppKit is not initialized
|
||||
if (!initialized) {
|
||||
// Show loading state if wallet is not initialized
|
||||
if (!wallet.isInitialized) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md border-neutral-800 bg-black text-white">
|
||||
@ -98,6 +63,10 @@ export function WalletConnectionDialog({
|
||||
);
|
||||
}
|
||||
|
||||
const isConnected = wallet.isConnected;
|
||||
const activeChain = wallet.walletType === 'bitcoin' ? 'Bitcoin' : 'Ethereum';
|
||||
const activeAddress = wallet.address;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md border-neutral-800 bg-black text-white">
|
||||
|
||||
@ -2,23 +2,14 @@ import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
import { Buffer } from 'buffer';
|
||||
import { OpchanWithAppKit } from './providers/OpchanWithAppKit';
|
||||
import { WagmiProvider } from 'wagmi';
|
||||
import { AppKitProvider } from '@reown/appkit/react';
|
||||
import { appkitConfig, config } from '@opchan/core';
|
||||
import { OpChanProvider } from '@opchan/react';
|
||||
|
||||
if (!(window as Window & typeof globalThis).Buffer) {
|
||||
(window as Window & typeof globalThis).Buffer = Buffer;
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<WagmiProvider config={config}>
|
||||
<AppKitProvider {...appkitConfig}>
|
||||
<OpchanWithAppKit
|
||||
config={{ ordiscanApiKey: '6bb07766-d98c-4ddd-93fb-6a0e94d629dd' }}
|
||||
>
|
||||
<OpChanProvider config={{ ordiscanApiKey: '6bb07766-d98c-4ddd-93fb-6a0e94d629dd' }}>
|
||||
<App />
|
||||
</OpchanWithAppKit>
|
||||
</AppKitProvider>
|
||||
</WagmiProvider>
|
||||
</OpChanProvider>
|
||||
);
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
OpChanProvider,
|
||||
type WalletAdapter,
|
||||
type WalletAdapterAccount,
|
||||
} from '@opchan/react';
|
||||
import { useAppKitAccount, modal } from '@reown/appkit/react';
|
||||
import { AppKit } from '@reown/appkit';
|
||||
import type { OpChanClientConfig } from '@opchan/core';
|
||||
import { walletManager } from '@opchan/core';
|
||||
|
||||
interface Props {
|
||||
config: OpChanClientConfig;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const OpchanWithAppKit: React.FC<Props> = ({ config, children }) => {
|
||||
const btc = useAppKitAccount({ namespace: 'bip122' });
|
||||
const eth = useAppKitAccount({ namespace: 'eip155' });
|
||||
|
||||
const listenersRef = React.useRef(
|
||||
new Set<(a: WalletAdapterAccount | null) => void>()
|
||||
);
|
||||
|
||||
const getCurrent = React.useCallback((): WalletAdapterAccount | null => {
|
||||
if (btc.isConnected && btc.address)
|
||||
return { address: btc.address, walletType: 'bitcoin' };
|
||||
if (eth.isConnected && eth.address)
|
||||
return { address: eth.address, walletType: 'ethereum' };
|
||||
return null;
|
||||
}, [btc.isConnected, btc.address, eth.isConnected, eth.address]);
|
||||
|
||||
const adapter = React.useMemo<WalletAdapter>(
|
||||
() => ({
|
||||
getAccount: () => getCurrent(),
|
||||
onChange: cb => {
|
||||
listenersRef.current.add(cb);
|
||||
return () => {
|
||||
listenersRef.current.delete(cb);
|
||||
};
|
||||
},
|
||||
}),
|
||||
[getCurrent]
|
||||
);
|
||||
|
||||
// Notify listeners when AppKit account changes
|
||||
React.useEffect(() => {
|
||||
const account = getCurrent();
|
||||
listenersRef.current.forEach(cb => {
|
||||
try {
|
||||
cb(account);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
}, [getCurrent]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!modal) return;
|
||||
try {
|
||||
const hasBtc = btc.isConnected && !!btc.address;
|
||||
const hasEth = eth.isConnected && !!eth.address;
|
||||
if (hasBtc || hasEth) {
|
||||
walletManager.create(modal as AppKit, btc, eth);
|
||||
} else if (walletManager.hasInstance()) {
|
||||
walletManager.clear();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('WalletManager initialization error', err);
|
||||
}
|
||||
}, [btc, btc.isConnected, btc.address, eth, eth.isConnected, eth.address]);
|
||||
|
||||
return (
|
||||
<OpChanProvider config={config} walletAdapter={adapter}>
|
||||
{children}
|
||||
</OpChanProvider>
|
||||
);
|
||||
};
|
||||
@ -5,7 +5,7 @@ import { ForumActions } from '../lib/forum/ForumActions';
|
||||
import { RelevanceCalculator } from '../lib/forum/RelevanceCalculator';
|
||||
import { UserIdentityService } from '../lib/services/UserIdentityService';
|
||||
import { DelegationManager, delegationManager } from '../lib/delegation';
|
||||
import { walletManager } from '../lib/wallet';
|
||||
import WalletManager from '../lib/wallet';
|
||||
import { MessageService } from '../lib/services/MessageService';
|
||||
|
||||
export interface OpChanClientConfig {
|
||||
@ -22,19 +22,19 @@ export class OpChanClient {
|
||||
readonly messageService: MessageService;
|
||||
readonly userIdentityService: UserIdentityService;
|
||||
readonly delegation: DelegationManager = delegationManager;
|
||||
readonly wallet = walletManager;
|
||||
readonly wallet = WalletManager
|
||||
|
||||
constructor(config: OpChanClientConfig) {
|
||||
this.config = config;
|
||||
|
||||
const env: EnvironmentConfig = {
|
||||
|
||||
apiKeys: {
|
||||
ordiscan: config.ordiscanApiKey,
|
||||
},
|
||||
};
|
||||
|
||||
environment.configure(env);
|
||||
|
||||
|
||||
this.messageService = new MessageService(this.delegation);
|
||||
this.userIdentityService = new UserIdentityService(this.messageService);
|
||||
|
||||
@ -42,9 +42,10 @@ export { default as messageManager } from './lib/waku';
|
||||
export * from './lib/waku/network';
|
||||
|
||||
// Export wallet functionality
|
||||
export { WalletManager, walletManager } from './lib/wallet';
|
||||
export { WalletManager } from './lib/wallet';
|
||||
export * from './lib/wallet/config';
|
||||
export * from './lib/wallet/types';
|
||||
export { type WalletAdapter, setWalletAdapter, getWalletAdapter } from './lib/wallet/adapter';
|
||||
|
||||
// Primary client API
|
||||
export { OpChanClient, type OpChanClientConfig } from './client/OpChanClient';
|
||||
|
||||
@ -7,7 +7,8 @@ import {
|
||||
} from '../../types/waku';
|
||||
import { MessageService } from './MessageService';
|
||||
import { localDatabase } from '../database/LocalDatabase';
|
||||
import { walletManager, WalletManager } from '../wallet';
|
||||
import { WalletManager } from '../wallet';
|
||||
import { getWalletAdapter } from '../wallet/adapter';
|
||||
|
||||
export interface UserIdentity {
|
||||
address: string;
|
||||
@ -235,9 +236,9 @@ export class UserIdentityService {
|
||||
this.resolveOrdinalDetails(address),
|
||||
]);
|
||||
|
||||
const isWalletConnected = WalletManager.hasInstance()
|
||||
? walletManager.getInstance().isConnected()
|
||||
: false;
|
||||
const isWalletConnected = (getWalletAdapter()?.isConnected?.() ?? (WalletManager.hasInstance()
|
||||
? WalletManager.getInstance().isConnected()
|
||||
: false));
|
||||
let verificationStatus: EVerificationStatus;
|
||||
if (ensName || ordinalDetails) {
|
||||
verificationStatus = EVerificationStatus.ENS_ORDINAL_VERIFIED;
|
||||
|
||||
19
packages/core/src/lib/wallet/adapter.ts
Normal file
19
packages/core/src/lib/wallet/adapter.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export interface WalletAdapter {
|
||||
getAddress(): string | null;
|
||||
getWalletType(): 'bitcoin' | 'ethereum' | null;
|
||||
isConnected(): boolean;
|
||||
signMessage(message: string): Promise<string>;
|
||||
}
|
||||
|
||||
let currentAdapter: WalletAdapter | null = null;
|
||||
|
||||
export function setWalletAdapter(adapter: WalletAdapter | null): void {
|
||||
currentAdapter = adapter;
|
||||
}
|
||||
|
||||
export function getWalletAdapter(): WalletAdapter | null {
|
||||
return currentAdapter;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ export const wagmiAdapter = new WagmiAdapter({
|
||||
// Export the Wagmi config for the provider
|
||||
export const config = wagmiAdapter.wagmiConfig;
|
||||
|
||||
const bitcoinAdapter = new BitcoinAdapter({
|
||||
export const bitcoinAdapter = new BitcoinAdapter({
|
||||
projectId,
|
||||
});
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@ import {
|
||||
verifyMessage as verifyEthereumMessage,
|
||||
} from '@wagmi/core';
|
||||
import { ChainNamespace } from '@reown/appkit-common';
|
||||
import { config } from './config';
|
||||
import { Provider } from '@reown/appkit-controllers';
|
||||
import { config, wagmiAdapter, bitcoinAdapter } from './config';
|
||||
import { Provider, ProviderController } from '@reown/appkit-controllers';
|
||||
import { WalletInfo, ActiveWallet } from './types';
|
||||
import { Inscription } from 'ordiscan';
|
||||
export class WalletManager {
|
||||
@ -196,15 +196,15 @@ export class WalletManager {
|
||||
*/
|
||||
async signMessage(message: string): Promise<string> {
|
||||
try {
|
||||
// Access the adapter through the appKit instance
|
||||
const adapter = this.appKit.chainAdapters?.[this.namespace];
|
||||
// Select adapter based on active namespace using configured instances
|
||||
const adapter = this.namespace === 'eip155' ? wagmiAdapter : this.namespace === 'bip122' ? bitcoinAdapter : undefined;
|
||||
|
||||
if (!adapter) {
|
||||
throw new Error(`No adapter found for namespace: ${this.namespace}`);
|
||||
throw new Error(`No adapter instance configured for namespace: ${this.namespace}`);
|
||||
}
|
||||
|
||||
// Get the provider for the current connection
|
||||
const provider = this.appKit.getProvider(this.namespace);
|
||||
// Get the provider for the current connection via ProviderController
|
||||
const provider = ProviderController.getProvider(this.namespace);
|
||||
|
||||
if (!provider) {
|
||||
throw new Error(`No provider found for namespace: ${this.namespace}`);
|
||||
@ -218,7 +218,7 @@ export class WalletManager {
|
||||
const result = await adapter.signMessage({
|
||||
message,
|
||||
address: this.activeAccount.address,
|
||||
provider: provider as Provider,
|
||||
provider: provider as unknown as Provider,
|
||||
});
|
||||
|
||||
return result.signature;
|
||||
@ -267,15 +267,7 @@ export class WalletManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience exports for singleton access
|
||||
export const walletManager = {
|
||||
create: WalletManager.create,
|
||||
getInstance: WalletManager.getInstance,
|
||||
hasInstance: WalletManager.hasInstance,
|
||||
clear: WalletManager.clear,
|
||||
resolveENS: WalletManager.resolveENS,
|
||||
verifySignature: WalletManager.verifySignature,
|
||||
};
|
||||
export default WalletManager;
|
||||
|
||||
export * from './types';
|
||||
export * from './config';
|
||||
|
||||
@ -10,53 +10,16 @@ npm i @opchan/react @opchan/core react react-dom
|
||||
|
||||
### Quickstart
|
||||
|
||||
#### Basic Usage
|
||||
#### With Reown AppKit (Recommended)
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { OpChanProvider } from '@opchan/react';
|
||||
import type { OpChanClientConfig } from '@opchan/core';
|
||||
|
||||
const config: OpChanClientConfig = {
|
||||
ordiscanApiKey: 'YOUR_ORDISCAN_API_KEY',
|
||||
};
|
||||
|
||||
// Optional: bridge your wallet to OpChan
|
||||
const walletAdapter = {
|
||||
getAccount() {
|
||||
// Return { address, walletType: 'bitcoin' | 'ethereum' } or null
|
||||
return null;
|
||||
},
|
||||
onChange(cb) {
|
||||
// Subscribe to wallet changes; return an unsubscribe function
|
||||
return () => {};
|
||||
},
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<OpChanProvider config={config} walletAdapter={walletAdapter}>
|
||||
{/* your app */}
|
||||
</OpChanProvider>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />);
|
||||
```
|
||||
|
||||
#### (Suggested) With Reown AppKit Integration
|
||||
|
||||
Using Reown AppKit for wallet management:
|
||||
OpChan integrates with Reown AppKit for wallet management. The provider must be nested inside `WagmiProvider` and `AppKitProvider`:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { WagmiProvider } from 'wagmi';
|
||||
import { AppKitProvider } from '@reown/appkit/react';
|
||||
import { OpchanWithAppKit } from './providers/OpchanWithAppKit';
|
||||
|
||||
// Define your own config for networks, or use our by default (supports Bitcoin and Ethereum)
|
||||
import { OpChanProvider } from '@opchan/react';
|
||||
import { config, appkitConfig } from '@opchan/core';
|
||||
|
||||
const opchanConfig = { ordiscanApiKey: 'YOUR_ORDISCAN_API_KEY' };
|
||||
@ -65,9 +28,9 @@ function App() {
|
||||
return (
|
||||
<WagmiProvider config={config}>
|
||||
<AppKitProvider {...appkitConfig}>
|
||||
<OpchanWithAppKit config={opchanConfig}>
|
||||
<OpChanProvider config={opchanConfig}>
|
||||
{/* your app */}
|
||||
</OpchanWithAppKit>
|
||||
</OpChanProvider>
|
||||
</AppKitProvider>
|
||||
</WagmiProvider>
|
||||
);
|
||||
@ -113,13 +76,14 @@ export function Connect() {
|
||||
<button onClick={() => disconnect()}>Disconnect</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() =>
|
||||
connect({ address: '0xabc...1234', walletType: 'ethereum' })
|
||||
}
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
<>
|
||||
<button onClick={() => connect('ethereum')}>
|
||||
Connect Ethereum
|
||||
</button>
|
||||
<button onClick={() => connect('bitcoin')}>
|
||||
Connect Bitcoin
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@ -129,22 +93,16 @@ export function Connect() {
|
||||
### API
|
||||
|
||||
- **Providers**
|
||||
- **`OpChanProvider`**: High-level provider that constructs an `OpChanClient` and wires persistence/events.
|
||||
- Props:
|
||||
- `config: OpChanClientConfig` — core client configuration.
|
||||
- `walletAdapter?: WalletAdapter` — optional bridge to your wallet system.
|
||||
- `children: React.ReactNode`.
|
||||
- Types:
|
||||
- `WalletAdapterAccount`: `{ address: string; walletType: 'bitcoin' | 'ethereum' }`.
|
||||
- `WalletAdapter`:
|
||||
- `getAccount(): WalletAdapterAccount | null`
|
||||
- `onChange(cb: (a: WalletAdapterAccount | null) => void): () => void`
|
||||
- **`OpchanWithAppKit`**: Convenience wrapper around `OpChanProvider` that integrates with Reown AppKit.
|
||||
- **`OpChanProvider`**: High-level provider that constructs an `OpChanClient` and integrates with AppKit.
|
||||
- Props:
|
||||
- `config: OpChanClientConfig` — core client configuration.
|
||||
- `children: React.ReactNode`.
|
||||
- Automatically bridges AppKit wallet connections to OpChan's wallet adapter interface.
|
||||
- Requires `WagmiProvider` and `AppKitProvider` from Reown AppKit as parent providers.
|
||||
- Requirements: Must be nested inside `WagmiProvider` and `AppKitProvider` from Reown AppKit.
|
||||
- Internally provides `AppKitWalletProvider` for wallet state management.
|
||||
|
||||
- **`AppKitWalletProvider`**: Wallet context provider (automatically included in `OpChanProvider`).
|
||||
- Provides wallet state and controls from AppKit.
|
||||
|
||||
- **`ClientProvider`**: Low-level provider if you construct `OpChanClient` yourself.
|
||||
- Props: `{ client: OpChanClient; children: React.ReactNode }`.
|
||||
|
||||
@ -153,9 +111,13 @@ export function Connect() {
|
||||
|
||||
- **`useAuth()`** → session & identity actions
|
||||
- Data: `currentUser`, `verificationStatus`, `isAuthenticated`, `delegationInfo`.
|
||||
- Actions: `connect({ address, walletType })`, `disconnect()`, `verifyOwnership()`,
|
||||
- Actions: `connect(walletType: 'bitcoin' | 'ethereum')`, `disconnect()`, `verifyOwnership()`,
|
||||
`delegate(duration)`, `delegationStatus()`, `clearDelegation()`,
|
||||
`updateProfile({ callSign?, displayPreference? })`.
|
||||
|
||||
- **`useAppKitWallet()`** → AppKit wallet state (low-level)
|
||||
- Data: `address`, `walletType`, `isConnected`, `isInitialized`.
|
||||
- Actions: `connect(walletType)`, `disconnect()`.
|
||||
|
||||
- **`useContent()`** → forum data & actions
|
||||
- Data: `cells`, `posts`, `comments`, `bookmarks`, `postsByCell`, `commentsByPost`,
|
||||
|
||||
393
packages/react/docs/getting-started.md
Normal file
393
packages/react/docs/getting-started.md
Normal file
@ -0,0 +1,393 @@
|
||||
## OpChan React SDK (packages/react) — Building Forum UIs
|
||||
|
||||
This guide shows how to build a forums-based app using the React SDK. It covers project wiring, wallet connection/disconnection, key delegation, waiting for Waku/network readiness, loading content, posting/commenting, voting, moderation, bookmarks, and user display utilities.
|
||||
|
||||
The examples assume you install and use the `@opchan/react` and `@opchan/core` packages.
|
||||
|
||||
---
|
||||
|
||||
### 1) Install and basic setup
|
||||
|
||||
```bash
|
||||
npm i @opchan/react @opchan/core
|
||||
```
|
||||
|
||||
Create an app-level provider using `OpChanProvider`. You must pass a minimal client config (e.g., Ordiscan API key if you have one). OpChanProvider must be nested inside `WagmiProvider` and `AppKitProvider` from Reown AppKit.
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { OpChanProvider } from '@opchan/react';
|
||||
import { WagmiProvider } from 'wagmi';
|
||||
import { AppKitProvider } from '@reown/appkit/react';
|
||||
import { appkitConfig, config } from '@opchan/core';
|
||||
|
||||
export function AppProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<WagmiProvider config={config}>
|
||||
<AppKitProvider {...appkitConfig}>
|
||||
<OpChanProvider config={{ ordiscanApiKey: 'YOUR_API_KEY' }}>
|
||||
{children}
|
||||
</OpChanProvider>
|
||||
</AppKitProvider>
|
||||
</WagmiProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
OpChanProvider automatically integrates with AppKit for wallet management.
|
||||
|
||||
---
|
||||
|
||||
### 2) High-level hook
|
||||
|
||||
Use `useForum()` for a single entry point to the main hooks:
|
||||
|
||||
```tsx
|
||||
import { useForum } from '@opchan/react';
|
||||
|
||||
function Example() {
|
||||
const { user, content, permissions, network } = useForum();
|
||||
// user: auth + delegation
|
||||
// content: cells/posts/comments/bookmarks + actions
|
||||
// permissions: derived booleans + reasons
|
||||
// network: Waku readiness + refresh
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3) Wallets — connect/disconnect & verification
|
||||
|
||||
API: `useAuth()`
|
||||
|
||||
```tsx
|
||||
import { useAuth } from '@opchan/react';
|
||||
|
||||
function WalletControls() {
|
||||
const { currentUser, verificationStatus, connect, disconnect, verifyOwnership } = useAuth();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{currentUser ? (
|
||||
<>
|
||||
<div>Connected: {currentUser.displayName}</div>
|
||||
<button onClick={() => disconnect()}>Disconnect</button>
|
||||
<button onClick={() => verifyOwnership()}>Verify ENS/Ordinal</button>
|
||||
<div>Status: {verificationStatus}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button onClick={() => connect('bitcoin')}>
|
||||
Connect Bitcoin
|
||||
</button>
|
||||
<button onClick={() => connect('ethereum')}>
|
||||
Connect Ethereum
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `connect(walletType)` opens the AppKit modal for the specified wallet type (bitcoin/ethereum). Upon successful connection, OpChan automatically syncs the wallet state and creates a user session.
|
||||
- `verifyOwnership()` refreshes identity and sets `EVerificationStatus` appropriately (checks ENS or Bitcoin Ordinals).
|
||||
|
||||
---
|
||||
|
||||
### 4) Key delegation — create, check, clear
|
||||
|
||||
API: `useAuth()` → `delegate(duration)`, `delegationStatus()`, `clearDelegation()`; also `delegationInfo` in-session.
|
||||
|
||||
```tsx
|
||||
import { useAuth } from '@opchan/react';
|
||||
|
||||
function DelegationControls() {
|
||||
const { delegate, delegationInfo, delegationStatus, clearDelegation } = useAuth();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
Delegated: {String(delegationInfo.isValid)}
|
||||
{delegationInfo.expiresAt && `, expires ${delegationInfo.expiresAt.toLocaleString()}`}
|
||||
</div>
|
||||
<button onClick={() => delegate('7days')}>Delegate 7 days</button>
|
||||
<button onClick={() => delegate('30days')}>Delegate 30 days</button>
|
||||
<button onClick={() => delegationStatus().then(console.log)}>Check</button>
|
||||
<button onClick={() => clearDelegation()}>Clear</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- The library generates a browser keypair and requests a wallet signature authorizing it until a selected expiry.
|
||||
- All messages (cells/posts/comments/votes/moderation/profile updates) are signed with the delegated browser key and verified via the proof.
|
||||
|
||||
---
|
||||
|
||||
### 5) Network (Waku) — readiness and manual refresh
|
||||
|
||||
API: `useNetwork()`
|
||||
|
||||
```tsx
|
||||
import { useNetwork } from '@opchan/react';
|
||||
|
||||
function NetworkStatus() {
|
||||
const { isConnected, statusMessage, refresh } = useNetwork();
|
||||
return (
|
||||
<div>
|
||||
<span>{isConnected ? 'Connected' : 'Connecting...'}</span>
|
||||
<span> • {statusMessage}</span>
|
||||
<button onClick={() => refresh()}>Refresh</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The store wires Waku health events to `network.isConnected` and `statusMessage`.
|
||||
- `refresh()` triggers a lightweight cache refresh; live updates come from the Waku subscription.
|
||||
|
||||
---
|
||||
|
||||
### 6) Loading content — cells, posts, comments, bookmarks
|
||||
|
||||
API: `useContent()`
|
||||
|
||||
```tsx
|
||||
import { useContent } from '@opchan/react';
|
||||
|
||||
function Feed() {
|
||||
const { cells, posts, comments, postsByCell, commentsByPost, bookmarks, lastSync } = useContent();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Cells: {cells.length}</div>
|
||||
<div>Posts: {posts.length}</div>
|
||||
<div>Comments: {comments.length}</div>
|
||||
<div>Bookmarks: {bookmarks.length}</div>
|
||||
<div>Last sync: {lastSync ? new Date(lastSync).toLocaleTimeString() : '—'}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Derived helpers:
|
||||
- `postsByCell[cellId]: Post[]`
|
||||
- `commentsByPost[postId]: Comment[]` (sorted oldest→newest)
|
||||
- `cellsWithStats`: adds `postCount`, `activeUsers`, `recentActivity` for UI
|
||||
- `userVerificationStatus[address]`: identity verification snapshot for weighting/relevance
|
||||
- `pending.isPending(id)`: show optimistic “syncing…” indicators
|
||||
|
||||
---
|
||||
|
||||
### 7) Creating content — cells, posts, comments
|
||||
|
||||
All actions reflect to the local cache immediately and then propagate over the network.
|
||||
|
||||
```tsx
|
||||
import { useContent } from '@opchan/react';
|
||||
|
||||
function Composer({ cellId, postId }: { cellId?: string; postId?: string }) {
|
||||
const { createCell, createPost, createComment } = useContent();
|
||||
|
||||
const onCreateCell = async () => {
|
||||
await createCell({ name: 'My Cell', description: 'Description', icon: '' });
|
||||
};
|
||||
|
||||
const onCreatePost = async () => {
|
||||
if (!cellId) return;
|
||||
await createPost({ cellId, title: 'Hello', content: 'World' });
|
||||
};
|
||||
|
||||
const onCreateComment = async () => {
|
||||
if (!postId) return;
|
||||
await createComment({ postId, content: 'Nice post!' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={onCreateCell}>Create Cell</button>
|
||||
<button onClick={onCreatePost} disabled={!cellId}>Create Post</button>
|
||||
<button onClick={onCreateComment} disabled={!postId}>Create Comment</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Permissions: see `usePermissions()` below to gate UI.
|
||||
|
||||
---
|
||||
|
||||
### 8) Voting
|
||||
|
||||
```tsx
|
||||
import { useContent } from '@opchan/react';
|
||||
|
||||
function VoteButtons({ targetId }: { targetId: string }) {
|
||||
const { vote, pending } = useContent();
|
||||
const isSyncing = pending.isPending(targetId);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => vote({ targetId, isUpvote: true })}>Upvote</button>
|
||||
<button onClick={() => vote({ targetId, isUpvote: false })}>Downvote</button>
|
||||
{isSyncing && <span> syncing…</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9) Moderation (cell owner)
|
||||
|
||||
```tsx
|
||||
import { useContent, usePermissions } from '@opchan/react';
|
||||
|
||||
function Moderation({ cellId, postId, commentId, userAddress }: { cellId: string; postId?: string; commentId?: string; userAddress?: string }) {
|
||||
const { moderate } = useContent();
|
||||
const { canModerate } = usePermissions();
|
||||
|
||||
if (!canModerate(cellId)) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{postId && (
|
||||
<>
|
||||
<button onClick={() => moderate.post(cellId, postId, 'reason')}>Moderate post</button>
|
||||
<button onClick={() => moderate.unpost(cellId, postId, 'reason')}>Unmoderate post</button>
|
||||
</>
|
||||
)}
|
||||
{commentId && (
|
||||
<>
|
||||
<button onClick={() => moderate.comment(cellId, commentId, 'reason')}>Moderate comment</button>
|
||||
<button onClick={() => moderate.uncomment(cellId, commentId, 'reason')}>Unmoderate comment</button>
|
||||
</>
|
||||
)}
|
||||
{userAddress && (
|
||||
<>
|
||||
<button onClick={() => moderate.user(cellId, userAddress, 'reason')}>Moderate user</button>
|
||||
<button onClick={() => moderate.unuser(cellId, userAddress, 'reason')}>Unmoderate user</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10) Bookmarks
|
||||
|
||||
```tsx
|
||||
import { useContent } from '@opchan/react';
|
||||
|
||||
function BookmarkControls({ post, comment }: { post?: any; comment?: any }) {
|
||||
const { bookmarks, togglePostBookmark, toggleCommentBookmark, removeBookmark, clearAllBookmarks } = useContent();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Total: {bookmarks.length}</div>
|
||||
{post && <button onClick={() => togglePostBookmark(post, post.cellId)}>Toggle post bookmark</button>}
|
||||
{comment && <button onClick={() => toggleCommentBookmark(comment, comment.postId)}>Toggle comment bookmark</button>}
|
||||
<button onClick={() => bookmarks[0] && removeBookmark(bookmarks[0].id)}>Remove first</button>
|
||||
<button onClick={() => clearAllBookmarks()}>Clear all</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11) Permissions helper
|
||||
|
||||
API: `usePermissions()` exposes booleans and user-friendly reasons for gating UI.
|
||||
|
||||
```tsx
|
||||
import { usePermissions } from '@opchan/react';
|
||||
|
||||
function ActionGates({ cellId }: { cellId: string }) {
|
||||
const p = usePermissions();
|
||||
return (
|
||||
<ul>
|
||||
<li>Can create cell: {String(p.canCreateCell)} ({p.reasons.createCell})</li>
|
||||
<li>Can post: {String(p.canPost)} ({p.reasons.post})</li>
|
||||
<li>Can comment: {String(p.canComment)} ({p.reasons.comment})</li>
|
||||
<li>Can vote: {String(p.canVote)} ({p.reasons.vote})</li>
|
||||
<li>Can moderate: {String(p.canModerate(cellId))} ({p.reasons.moderate(cellId)})</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 12) User display/identity helpers
|
||||
|
||||
API: `useUserDisplay(address)` returns resolved user identity for UI labels.
|
||||
|
||||
```tsx
|
||||
import { useUserDisplay } from '@opchan/react';
|
||||
|
||||
function AuthorName({ address }: { address: string }) {
|
||||
const { displayName, ensName, ordinalDetails, isLoading } = useUserDisplay(address);
|
||||
if (isLoading) return <span>Loading…</span>;
|
||||
return <span title={ensName || ordinalDetails?.ordinalId}>{displayName}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 13) End-to-end page skeleton
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { OpChanProvider, useForum } from '@opchan/react';
|
||||
|
||||
function Home() {
|
||||
const { user, content, permissions, network } = useForum();
|
||||
return (
|
||||
<div>
|
||||
<div>Network: {network.isConnected ? 'Ready' : 'Connecting…'}</div>
|
||||
{user.currentUser ? (
|
||||
<div>Welcome {user.currentUser.displayName}</div>
|
||||
) : (
|
||||
<button onClick={() => user.connect({ address: '0xabc...', walletType: 'ethereum' })}>Connect</button>
|
||||
)}
|
||||
{permissions.canPost && content.cells[0] && (
|
||||
<button onClick={() => content.createPost({ cellId: content.cells[0].id, title: 'Hi', content: 'Hello world' })}>
|
||||
New Post
|
||||
</button>
|
||||
)}
|
||||
<ul>
|
||||
{content.posts.map(p => (
|
||||
<li key={p.id}>{p.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<OpChanProvider config={{ ordiscanApiKey: '' }}>
|
||||
<Home />
|
||||
</OpChanProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 14) Notes and best practices
|
||||
|
||||
- Always gate actions with `usePermissions()` to provide clear UX reasons.
|
||||
- Use `pending.isPending(id)` to show optimistic “syncing…” states for content you just created or voted on.
|
||||
- The store is hydrated from IndexedDB on load; Waku live messages keep it in sync.
|
||||
- Identity (ENS/Ordinals/Call Sign) is resolved and cached; calling `verifyOwnership()` or updating the profile will refresh it.
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opchan/react",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.0",
|
||||
"private": false,
|
||||
"description": "React contexts and hooks for OpChan built on @opchan/core",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@ -5,8 +5,10 @@ export {
|
||||
useClient,
|
||||
} from './v1/context/ClientContext';
|
||||
|
||||
export { useAppKitWallet } from './v1/hooks/useAppKitWallet';
|
||||
|
||||
export { OpChanProvider } from './v1/provider/OpChanProvider';
|
||||
export type { WalletAdapter, WalletAdapterAccount } from './v1/provider/OpChanProvider';
|
||||
export type { OpChanProviderProps } from './v1/provider/OpChanProvider';
|
||||
|
||||
export { useAuth } from './v1/hooks/useAuth';
|
||||
export { useContent } from './v1/hooks/useContent';
|
||||
|
||||
53
packages/react/src/v1/components/AppKitErrorBoundary.tsx
Normal file
53
packages/react/src/v1/components/AppKitErrorBoundary.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { Component, ReactNode } from 'react';
|
||||
|
||||
interface AppKitErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
interface AppKitErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
export class AppKitErrorBoundary extends Component<
|
||||
AppKitErrorBoundaryProps,
|
||||
AppKitErrorBoundaryState
|
||||
> {
|
||||
constructor(props: AppKitErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): AppKitErrorBoundaryState {
|
||||
// Check if this is an AppKit initialization error
|
||||
if (error.message.includes('createAppKit') ||
|
||||
error.message.includes('useAppKitState') ||
|
||||
error.message.includes('AppKit')) {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
return { hasError: false };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.warn('[AppKitErrorBoundary] Caught AppKit error:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// Render fallback UI or the children with error handling
|
||||
return this.props.fallback || (
|
||||
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
|
||||
<h3 className="text-sm font-medium text-yellow-800">
|
||||
Wallet Connection Initializing...
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-yellow-700">
|
||||
The wallet system is still loading. Please wait a moment.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
100
packages/react/src/v1/hooks/useAppKitWallet.ts
Normal file
100
packages/react/src/v1/hooks/useAppKitWallet.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
useAppKit,
|
||||
useAppKitAccount,
|
||||
useDisconnect,
|
||||
useAppKitState,
|
||||
} from '@reown/appkit/react';
|
||||
|
||||
export interface AppKitWalletState {
|
||||
address: string | null;
|
||||
walletType: 'bitcoin' | 'ethereum' | null;
|
||||
isConnected: boolean;
|
||||
isInitialized: boolean;
|
||||
}
|
||||
|
||||
export interface AppKitWalletControls {
|
||||
connect: (walletType: 'bitcoin' | 'ethereum') => void;
|
||||
disconnect: () => Promise<void>;
|
||||
}
|
||||
|
||||
export type AppKitWalletValue = AppKitWalletState & AppKitWalletControls;
|
||||
|
||||
export function useAppKitWallet(): AppKitWalletValue {
|
||||
// Add error boundary for AppKit hooks
|
||||
let initialized = false;
|
||||
let appKit: ReturnType<typeof useAppKit> | null = null;
|
||||
let appKitDisconnect: (() => Promise<void>) | null = null;
|
||||
let bitcoinAccount: ReturnType<typeof useAppKitAccount> | null = null;
|
||||
let ethereumAccount: ReturnType<typeof useAppKitAccount> | null = null;
|
||||
|
||||
try {
|
||||
const appKitState = useAppKitState();
|
||||
initialized = appKitState?.initialized || false;
|
||||
appKit = useAppKit();
|
||||
const disconnectHook = useDisconnect();
|
||||
appKitDisconnect = disconnectHook?.disconnect;
|
||||
bitcoinAccount = useAppKitAccount({ namespace: 'bip122' });
|
||||
ethereumAccount = useAppKitAccount({ namespace: 'eip155' });
|
||||
} catch (error) {
|
||||
console.warn('[useAppKitWallet] AppKit hooks not available yet:', error);
|
||||
// Return default values if AppKit is not initialized
|
||||
return {
|
||||
address: null,
|
||||
walletType: null,
|
||||
isConnected: false,
|
||||
isInitialized: false,
|
||||
connect: () => console.warn('AppKit not initialized'),
|
||||
disconnect: async () => console.warn('AppKit not initialized'),
|
||||
};
|
||||
}
|
||||
|
||||
// Determine wallet state
|
||||
const isBitcoinConnected = bitcoinAccount?.isConnected || false;
|
||||
const isEthereumConnected = ethereumAccount?.isConnected || false;
|
||||
const isConnected = isBitcoinConnected || isEthereumConnected;
|
||||
|
||||
const walletType = useMemo<'bitcoin' | 'ethereum' | null>(() => {
|
||||
if (isBitcoinConnected) return 'bitcoin';
|
||||
if (isEthereumConnected) return 'ethereum';
|
||||
return null;
|
||||
}, [isBitcoinConnected, isEthereumConnected]);
|
||||
|
||||
const address = useMemo<string | null>(() => {
|
||||
if (isBitcoinConnected && bitcoinAccount?.address) return bitcoinAccount.address;
|
||||
if (isEthereumConnected && ethereumAccount?.address) return ethereumAccount.address;
|
||||
return null;
|
||||
}, [isBitcoinConnected, bitcoinAccount?.address, isEthereumConnected, ethereumAccount?.address]);
|
||||
|
||||
const connect = useCallback((targetWalletType: 'bitcoin' | 'ethereum') => {
|
||||
if (!initialized || !appKit) {
|
||||
console.error('AppKit not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const namespace = targetWalletType === 'bitcoin' ? 'bip122' : 'eip155';
|
||||
appKit.open({
|
||||
view: 'Connect',
|
||||
namespace,
|
||||
});
|
||||
}, [initialized, appKit]);
|
||||
|
||||
const disconnect = useCallback(async () => {
|
||||
if (appKitDisconnect) {
|
||||
await appKitDisconnect();
|
||||
}
|
||||
}, [appKitDisconnect]);
|
||||
|
||||
return {
|
||||
address,
|
||||
walletType,
|
||||
isConnected,
|
||||
isInitialized: initialized,
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,73 +1,94 @@
|
||||
import React from 'react';
|
||||
import { useClient } from '../context/ClientContext';
|
||||
import { useAppKitWallet } from '../hooks/useAppKitWallet';
|
||||
import { useOpchanStore, setOpchanState } from '../store/opchanStore';
|
||||
import {
|
||||
User,
|
||||
EVerificationStatus,
|
||||
DelegationDuration,
|
||||
EDisplayPreference,
|
||||
walletManager,
|
||||
WalletManager,
|
||||
} from '@opchan/core';
|
||||
import type { DelegationFullStatus } from '@opchan/core';
|
||||
|
||||
export interface ConnectInput {
|
||||
address: string;
|
||||
walletType: 'bitcoin' | 'ethereum';
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const client = useClient();
|
||||
const wallet = useAppKitWallet();
|
||||
const currentUser = useOpchanStore(s => s.session.currentUser);
|
||||
const verificationStatus = useOpchanStore(s => s.session.verificationStatus);
|
||||
const delegation = useOpchanStore(s => s.session.delegation);
|
||||
|
||||
// Sync AppKit wallet state to OpChan session
|
||||
React.useEffect(() => {
|
||||
const syncWallet = async () => {
|
||||
if (wallet.isConnected && wallet.address && wallet.walletType) {
|
||||
// Wallet connected - create/update user session
|
||||
const baseUser: User = {
|
||||
address: wallet.address,
|
||||
walletType: wallet.walletType,
|
||||
displayName: wallet.address.slice(0, 6) + '...' + wallet.address.slice(-4),
|
||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
|
||||
lastChecked: Date.now(),
|
||||
};
|
||||
|
||||
const connect = React.useCallback(async (input: ConnectInput): Promise<boolean> => {
|
||||
const baseUser: User = {
|
||||
address: input.address,
|
||||
walletType: input.walletType,
|
||||
displayName: input.address.slice(0, 6) + '...' + input.address.slice(-4),
|
||||
displayPreference: EDisplayPreference.WALLET_ADDRESS,
|
||||
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
|
||||
lastChecked: Date.now(),
|
||||
try {
|
||||
await client.database.storeUser(baseUser);
|
||||
// Prime identity service so display name/ens are cached
|
||||
const identity = await client.userIdentityService.getIdentity(baseUser.address);
|
||||
if (identity) {
|
||||
setOpchanState(prev => ({
|
||||
...prev,
|
||||
session: {
|
||||
...prev.session,
|
||||
currentUser: {
|
||||
...baseUser,
|
||||
...identity,
|
||||
},
|
||||
verificationStatus: identity.verificationStatus,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
setOpchanState(prev => ({
|
||||
...prev,
|
||||
session: {
|
||||
...prev.session,
|
||||
currentUser: baseUser,
|
||||
verificationStatus: baseUser.verificationStatus,
|
||||
},
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to sync wallet to session', e);
|
||||
}
|
||||
} else if (!wallet.isConnected && currentUser) {
|
||||
// Wallet disconnected - clear session
|
||||
try {
|
||||
await client.database.clearUser();
|
||||
} catch (e) {
|
||||
console.error('Failed to clear user on disconnect', e);
|
||||
}
|
||||
setOpchanState(prev => ({
|
||||
...prev,
|
||||
session: {
|
||||
currentUser: null,
|
||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||
delegation: null,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await client.database.storeUser(baseUser);
|
||||
// Prime identity service so display name/ens are cached
|
||||
const identity = await client.userIdentityService.getIdentity(baseUser.address);
|
||||
if (!identity) return false;
|
||||
setOpchanState(prev => ({
|
||||
...prev,
|
||||
session: {
|
||||
...prev.session,
|
||||
currentUser: {
|
||||
...baseUser,
|
||||
...identity,
|
||||
},
|
||||
},
|
||||
}));
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('connect failed', e);
|
||||
return false;
|
||||
}
|
||||
}, [client]);
|
||||
syncWallet();
|
||||
}, [wallet.isConnected, wallet.address, wallet.walletType, client, currentUser]);
|
||||
|
||||
const connect = React.useCallback((walletType: 'bitcoin' | 'ethereum'): void => {
|
||||
wallet.connect(walletType);
|
||||
}, [wallet]);
|
||||
|
||||
const disconnect = React.useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
await client.database.clearUser();
|
||||
} finally {
|
||||
setOpchanState(prev => ({
|
||||
...prev,
|
||||
session: {
|
||||
currentUser: null,
|
||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||
delegation: null,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, [client]);
|
||||
await wallet.disconnect();
|
||||
}, [wallet]);
|
||||
|
||||
const verifyOwnership = React.useCallback(async (): Promise<boolean> => {
|
||||
console.log('verifyOwnership')
|
||||
@ -111,7 +132,7 @@ export function useAuth() {
|
||||
const user = currentUser;
|
||||
if (!user) return false;
|
||||
try {
|
||||
const signer = ((message: string) => walletManager.getInstance().signMessage(message));
|
||||
const signer = ((message: string) => WalletManager.getInstance().signMessage(message));
|
||||
const ok = await client.delegation.delegate(
|
||||
user.address,
|
||||
user.walletType,
|
||||
|
||||
@ -1,91 +1,39 @@
|
||||
import React from 'react';
|
||||
import { OpChanClient, type OpChanClientConfig } from '@opchan/core';
|
||||
import { ClientProvider } from '../context/ClientContext';
|
||||
import { StoreWiring } from './StoreWiring';
|
||||
import { setOpchanState } from '../store/opchanStore';
|
||||
import { EVerificationStatus } from '@opchan/core';
|
||||
import type { EDisplayPreference, User } from '@opchan/core';
|
||||
import React from "react";
|
||||
import { OpChanClient, type OpChanClientConfig } from "@opchan/core";
|
||||
import { ClientProvider } from "../context/ClientContext";
|
||||
import { StoreWiring } from "./StoreWiring";
|
||||
import { WalletAdapterInitializer } from "./WalletAdapterInitializer";
|
||||
import { AppKitProvider } from "@reown/appkit/react";
|
||||
import { WagmiProvider } from "wagmi";
|
||||
import { appkitConfig, config as wagmiConfig } from "@opchan/core";
|
||||
import { AppKitErrorBoundary } from "../components/AppKitErrorBoundary";
|
||||
|
||||
export interface WalletAdapterAccount {
|
||||
address: string;
|
||||
walletType: 'bitcoin' | 'ethereum';
|
||||
}
|
||||
|
||||
export interface WalletAdapter {
|
||||
getAccount(): WalletAdapterAccount | null;
|
||||
onChange(callback: (account: WalletAdapterAccount | null) => void): () => void;
|
||||
}
|
||||
|
||||
export interface NewOpChanProviderProps {
|
||||
export interface OpChanProviderProps {
|
||||
config: OpChanClientConfig;
|
||||
walletAdapter?: WalletAdapter;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* New provider that constructs the OpChanClient and sets up DI.
|
||||
* Event wiring and store hydration will be handled in a separate effect layer.
|
||||
* OpChan provider that constructs the OpChanClient and provides wallet context.
|
||||
* Must be nested inside WagmiProvider and AppKitProvider.
|
||||
*/
|
||||
export const OpChanProvider: React.FC<NewOpChanProviderProps> = ({ config, walletAdapter, children }) => {
|
||||
export const OpChanProvider: React.FC<OpChanProviderProps> = ({
|
||||
config,
|
||||
children,
|
||||
}) => {
|
||||
const [client] = React.useState(() => new OpChanClient(config));
|
||||
|
||||
// Bridge wallet adapter to session state
|
||||
React.useEffect(() => {
|
||||
if (!walletAdapter) return;
|
||||
|
||||
const syncFromAdapter = async (account: WalletAdapterAccount | null) => {
|
||||
if (account) {
|
||||
// Persist base user and update session
|
||||
const baseUser: User = {
|
||||
address: account.address,
|
||||
walletType: account.walletType,
|
||||
displayPreference: 'wallet-address' as EDisplayPreference,
|
||||
verificationStatus: EVerificationStatus.WALLET_CONNECTED,
|
||||
displayName: account.address.slice(0, 6) + '...' + account.address.slice(-4),
|
||||
lastChecked: Date.now(),
|
||||
};
|
||||
try {
|
||||
await client.database.storeUser(baseUser);
|
||||
} catch (err) {
|
||||
console.warn('OpChanProvider: failed to persist base user', err);
|
||||
}
|
||||
setOpchanState(prev => ({
|
||||
...prev,
|
||||
session: {
|
||||
currentUser: baseUser,
|
||||
verificationStatus: baseUser.verificationStatus,
|
||||
delegation: prev.session.delegation,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
// Clear session on disconnect
|
||||
try { await client.database.clearUser(); } catch (err) {
|
||||
console.warn('OpChanProvider: failed to clear user on disconnect', err);
|
||||
}
|
||||
setOpchanState(prev => ({
|
||||
...prev,
|
||||
session: {
|
||||
currentUser: null,
|
||||
verificationStatus: EVerificationStatus.WALLET_UNCONNECTED,
|
||||
delegation: null,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Initial sync
|
||||
syncFromAdapter(walletAdapter.getAccount());
|
||||
// Subscribe
|
||||
const off = walletAdapter.onChange(syncFromAdapter);
|
||||
return () => { try { off(); } catch { /* noop */ } };
|
||||
}, [walletAdapter, client]);
|
||||
|
||||
return (
|
||||
<ClientProvider client={client}>
|
||||
<StoreWiring />
|
||||
{children}
|
||||
</ClientProvider>
|
||||
<WagmiProvider config={wagmiConfig}>
|
||||
<AppKitErrorBoundary>
|
||||
<AppKitProvider {...appkitConfig}>
|
||||
<ClientProvider client={client}>
|
||||
<WalletAdapterInitializer />
|
||||
<StoreWiring />
|
||||
{children}
|
||||
</ClientProvider>
|
||||
</AppKitProvider>
|
||||
</AppKitErrorBoundary>
|
||||
</WagmiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
59
packages/react/src/v1/provider/WalletAdapterInitializer.tsx
Normal file
59
packages/react/src/v1/provider/WalletAdapterInitializer.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { useAppKit, useAppKitAccount, useAppKitState } from '@reown/appkit/react';
|
||||
import type { UseAppKitAccountReturn } from '@reown/appkit/react';
|
||||
import { WalletManager } from '@opchan/core';
|
||||
import type { AppKit } from '@reown/appkit';
|
||||
|
||||
export const WalletAdapterInitializer: React.FC = () => {
|
||||
// Add error boundary for AppKit hooks
|
||||
let initialized = false;
|
||||
let appKit: ReturnType<typeof useAppKit> | null = null;
|
||||
let btc: UseAppKitAccountReturn | null = null;
|
||||
let eth: UseAppKitAccountReturn | null = null;
|
||||
|
||||
try {
|
||||
const appKitState = useAppKitState();
|
||||
initialized = appKitState?.initialized || false;
|
||||
appKit = useAppKit();
|
||||
btc = useAppKitAccount({ namespace: 'bip122' });
|
||||
eth = useAppKitAccount({ namespace: 'eip155' });
|
||||
} catch (error) {
|
||||
console.warn('[WalletAdapterInitializer] AppKit hooks not available yet:', error);
|
||||
// Return early if AppKit is not initialized
|
||||
return null;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
// Only proceed if AppKit is properly initialized
|
||||
if (!initialized || !appKit || !btc || !eth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isBtc = btc.isConnected && !!btc.address;
|
||||
const isEth = eth.isConnected && !!eth.address;
|
||||
const anyConnected = isBtc || isEth;
|
||||
|
||||
if (anyConnected) {
|
||||
// Initialize WalletManager for signing flows
|
||||
try {
|
||||
WalletManager.create(
|
||||
appKit as unknown as AppKit,
|
||||
btc as unknown as UseAppKitAccountReturn,
|
||||
eth as unknown as UseAppKitAccountReturn,
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('[WalletAdapterInitializer] WalletManager.create failed', e);
|
||||
}
|
||||
return () => {
|
||||
try { WalletManager.clear(); } catch (e) { console.warn('[WalletAdapterInitializer] WalletManager.clear failed', e); }
|
||||
};
|
||||
}
|
||||
|
||||
// No connection: clear manager
|
||||
try { WalletManager.clear(); } catch (e) { console.warn('[WalletAdapterInitializer] WalletManager.clear failed', e); }
|
||||
}, [initialized, appKit, btc?.isConnected, btc?.address, eth?.isConnected, eth?.address]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user