2025-10-29 17:53:59 +05:30
|
|
|
# @opchan/react
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
React hooks and providers for building decentralized forum applications on top of `@opchan/core`.
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
## Overview
|
|
|
|
|
|
|
|
|
|
`@opchan/react` provides a complete React integration layer for the OpChan protocol, featuring:
|
|
|
|
|
|
|
|
|
|
- 🔐 **Flexible Authentication** - Wallet-based (Ethereum) or anonymous sessions
|
|
|
|
|
- 🔑 **Key Delegation** - Browser-based signing to reduce wallet prompts
|
|
|
|
|
- 📝 **Content Management** - Cells, posts, comments, and votes
|
|
|
|
|
- 👤 **Identity System** - ENS resolution, call signs, and user profiles
|
|
|
|
|
- ⚖️ **Permission Management** - Role-based access control
|
|
|
|
|
- 🌐 **Network State** - Waku connection monitoring
|
|
|
|
|
- 💾 **Local-First** - IndexedDB caching with network sync
|
|
|
|
|
|
|
|
|
|
## Installation
|
2025-09-25 21:52:40 +05:30
|
|
|
|
|
|
|
|
```bash
|
2025-10-29 17:53:59 +05:30
|
|
|
npm install @opchan/react @opchan/core react react-dom
|
2025-09-25 21:52:40 +05:30
|
|
|
```
|
|
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
## Quick Start
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
### 1. Setup Provider
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
The `OpChanProvider` wraps WagmiProvider and QueryClientProvider internally:
|
2025-09-25 21:52:40 +05:30
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import React from 'react';
|
2025-10-29 17:53:59 +05:30
|
|
|
import { createRoot } from 'react-dom/client';
|
2025-10-23 12:16:25 +05:30
|
|
|
import { OpChanProvider } from '@opchan/react';
|
2025-10-29 17:53:59 +05:30
|
|
|
import { Buffer } from 'buffer';
|
|
|
|
|
import App from './App';
|
|
|
|
|
|
|
|
|
|
// Required polyfill for crypto libraries
|
|
|
|
|
if (!(window as any).Buffer) {
|
|
|
|
|
(window as any).Buffer = Buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createRoot(document.getElementById('root')!).render(
|
|
|
|
|
<OpChanProvider
|
|
|
|
|
config={{
|
|
|
|
|
wakuConfig: {
|
|
|
|
|
contentTopic: '/opchan/1/messages/proto',
|
|
|
|
|
reliableChannelId: 'opchan-messages'
|
|
|
|
|
},
|
|
|
|
|
reownProjectId: 'your-reown-project-id' // For WalletConnect
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<App />
|
|
|
|
|
</OpChanProvider>
|
|
|
|
|
);
|
|
|
|
|
```
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
### 2. Use Hooks in Your App
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { useForum } from '@opchan/react';
|
|
|
|
|
|
|
|
|
|
export function MyComponent() {
|
|
|
|
|
const { user, content, permissions, network } = useForum();
|
2025-09-25 21:52:40 +05:30
|
|
|
|
|
|
|
|
return (
|
2025-10-29 17:53:59 +05:30
|
|
|
<div>
|
|
|
|
|
<h1>Cells: {content.cells.length}</h1>
|
|
|
|
|
<p>Network: {network.isConnected ? 'Connected' : 'Disconnected'}</p>
|
|
|
|
|
{permissions.canPost && <button>Create Post</button>}
|
|
|
|
|
</div>
|
2025-09-25 21:52:40 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
## Core Concepts
|
|
|
|
|
|
|
|
|
|
### Authentication Modes
|
|
|
|
|
|
|
|
|
|
OpChan supports three authentication modes:
|
|
|
|
|
|
|
|
|
|
1. **Anonymous** (`ANONYMOUS`) - Browser-only session, no wallet required
|
|
|
|
|
- Can post, comment, and vote
|
|
|
|
|
- Cannot create cells
|
|
|
|
|
- Optional call sign for identity
|
|
|
|
|
|
|
|
|
|
2. **Wallet Connected** (`WALLET_CONNECTED`) - Ethereum wallet connected
|
|
|
|
|
- Full interaction capabilities
|
|
|
|
|
- Can create posts, comment, vote
|
|
|
|
|
- Cannot create cells (requires ENS verification)
|
|
|
|
|
|
|
|
|
|
3. **ENS Verified** (`ENS_VERIFIED`) - Wallet + ENS ownership verified
|
|
|
|
|
- Full platform access
|
|
|
|
|
- Can create cells (becomes cell admin)
|
|
|
|
|
- Enhanced relevance scoring for content
|
|
|
|
|
|
|
|
|
|
### Key Delegation
|
|
|
|
|
|
|
|
|
|
To reduce wallet signature prompts, OpChan uses browser-based key delegation:
|
|
|
|
|
|
|
|
|
|
- **For Wallet Users**: Wallet signs authorization for browser keys (7 or 30 days)
|
|
|
|
|
- **For Anonymous Users**: Browser keys generated automatically (no wallet signature)
|
|
|
|
|
|
|
|
|
|
This enables one-time wallet interaction with subsequent actions signed by browser keys.
|
|
|
|
|
|
|
|
|
|
## API Reference
|
|
|
|
|
|
|
|
|
|
### Hooks
|
|
|
|
|
|
|
|
|
|
#### `useForum()`
|
|
|
|
|
|
|
|
|
|
Convenience hook that bundles all core hooks:
|
2025-09-25 21:52:40 +05:30
|
|
|
|
|
|
|
|
```tsx
|
2025-10-29 17:53:59 +05:30
|
|
|
const { user, content, permissions, network } = useForum();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Equivalent to:
|
|
|
|
|
```tsx
|
|
|
|
|
const user = useAuth();
|
|
|
|
|
const content = useContent();
|
|
|
|
|
const permissions = usePermissions();
|
|
|
|
|
const network = useNetwork();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
#### `useAuth()`
|
|
|
|
|
|
|
|
|
|
Manages user session, authentication, and identity.
|
|
|
|
|
|
|
|
|
|
**Data:**
|
|
|
|
|
- `currentUser: User | null` - Current authenticated user
|
|
|
|
|
- `verificationStatus: EVerificationStatus` - Authentication level
|
|
|
|
|
- `isAuthenticated: boolean` - Whether user is logged in (wallet or anonymous)
|
|
|
|
|
- `delegationInfo: { hasDelegation, isValid, timeRemaining?, expiresAt? }` - Delegation status
|
|
|
|
|
|
|
|
|
|
**Actions:**
|
|
|
|
|
- `connect()` - Open wallet connection modal
|
|
|
|
|
- `disconnect()` - Disconnect wallet or exit anonymous session
|
|
|
|
|
- `startAnonymous(): Promise<string | null>` - Start anonymous session, returns sessionId
|
|
|
|
|
- `verifyOwnership(): Promise<boolean>` - Verify ENS ownership
|
|
|
|
|
- `delegate(duration: '7days' | '30days'): Promise<boolean>` - Create wallet delegation
|
|
|
|
|
- `delegationStatus(): Promise<DelegationStatus>` - Check delegation status
|
|
|
|
|
- `clearDelegation(): Promise<boolean>` - Clear stored delegation
|
|
|
|
|
- `updateProfile({ callSign?, displayPreference? }): Promise<boolean>` - Update user profile
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
**Example:**
|
|
|
|
|
```tsx
|
|
|
|
|
function AuthButton() {
|
|
|
|
|
const { currentUser, connect, startAnonymous, disconnect } = useAuth();
|
|
|
|
|
|
|
|
|
|
if (currentUser) {
|
|
|
|
|
return <button onClick={disconnect}>Disconnect</button>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<button onClick={connect}>Connect Wallet</button>
|
|
|
|
|
<button onClick={startAnonymous}>Continue Anonymously</button>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
---
|
|
|
|
|
|
|
|
|
|
#### `useContent()`
|
|
|
|
|
|
|
|
|
|
Access forum content and perform content actions.
|
|
|
|
|
|
|
|
|
|
**Data:**
|
|
|
|
|
- `cells: Cell[]` - All cells
|
|
|
|
|
- `posts: Post[]` - All posts
|
|
|
|
|
- `comments: Comment[]` - All comments
|
|
|
|
|
- `bookmarks: Bookmark[]` - User's bookmarks
|
|
|
|
|
- `postsByCell: Record<string, Post[]>` - Posts grouped by cell
|
|
|
|
|
- `commentsByPost: Record<string, Comment[]>` - Comments grouped by post
|
|
|
|
|
- `cellsWithStats: Cell[]` - Cells with computed stats (activeMembers, relevance)
|
|
|
|
|
- `userVerificationStatus: Record<string, { isVerified, hasENS, ensName? }>` - Verification cache
|
|
|
|
|
- `lastSync: number | null` - Last network sync timestamp
|
|
|
|
|
- `pending: { isPending(id), onChange(callback) }` - Pending operations tracking
|
|
|
|
|
|
|
|
|
|
**Actions:**
|
|
|
|
|
- `createCell({ name, description, icon? }): Promise<Cell | null>`
|
|
|
|
|
- `createPost({ cellId, title, content }): Promise<Post | null>`
|
|
|
|
|
- `createComment({ postId, content }): Promise<Comment | null>`
|
|
|
|
|
- `vote({ targetId, isUpvote }): Promise<boolean>`
|
|
|
|
|
- `moderate.post(cellId, postId, reason?)` - Moderate a post
|
|
|
|
|
- `moderate.unpost(cellId, postId)` - Unmoderate a post
|
|
|
|
|
- `moderate.comment(cellId, commentId, reason?)` - Moderate a comment
|
|
|
|
|
- `moderate.uncomment(cellId, commentId)` - Unmoderate a comment
|
|
|
|
|
- `moderate.user(cellId, userAddress, reason?)` - Moderate a user
|
|
|
|
|
- `moderate.unuser(cellId, userAddress)` - Unmoderate a user
|
|
|
|
|
- `togglePostBookmark(post, cellId?): Promise<void>` - Toggle post bookmark
|
|
|
|
|
- `toggleCommentBookmark(comment, postId?): Promise<void>` - Toggle comment bookmark
|
|
|
|
|
- `removeBookmark(bookmarkId): Promise<void>` - Remove specific bookmark
|
|
|
|
|
- `clearAllBookmarks(): Promise<void>` - Clear all bookmarks
|
|
|
|
|
- `refresh(): Promise<void>` - Refresh content from cache
|
|
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
```tsx
|
|
|
|
|
function CreatePostForm({ cellId }: { cellId: string }) {
|
|
|
|
|
const { createPost } = useContent();
|
|
|
|
|
const [title, setTitle] = useState('');
|
|
|
|
|
const [content, setContent] = useState('');
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
const post = await createPost({ cellId, title, content });
|
|
|
|
|
if (post) {
|
|
|
|
|
setTitle('');
|
|
|
|
|
setContent('');
|
|
|
|
|
}
|
2025-09-25 21:52:40 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-29 17:53:59 +05:30
|
|
|
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
|
|
|
|
<input value={title} onChange={(e) => setTitle(e.target.value)} />
|
|
|
|
|
<textarea value={content} onChange={(e) => setContent(e.target.value)} />
|
|
|
|
|
<button type="submit">Post</button>
|
|
|
|
|
</form>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
#### `usePermissions()`
|
|
|
|
|
|
|
|
|
|
Check user permissions for various actions.
|
|
|
|
|
|
|
|
|
|
**Data:**
|
|
|
|
|
- `canPost: boolean` - Can create posts (wallet or anonymous)
|
|
|
|
|
- `canComment: boolean` - Can create comments (wallet or anonymous)
|
2025-11-19 00:32:04 -05:00
|
|
|
- `canVote: boolean` - Can vote (wallet only)
|
2025-10-29 17:53:59 +05:30
|
|
|
- `canCreateCell: boolean` - Can create cells (ENS verified only)
|
|
|
|
|
- `canDelegate: boolean` - Can delegate keys (wallet only)
|
|
|
|
|
- `canModerate(cellId): boolean` - Can moderate cell (cell creator only)
|
|
|
|
|
- `reasons: { post, comment, vote, createCell, moderate(cellId) }` - Reason strings when permission denied
|
|
|
|
|
- `check(action, cellId?): { allowed, reason }` - Unified permission check
|
|
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
```tsx
|
|
|
|
|
function PostActions() {
|
|
|
|
|
const permissions = usePermissions();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<button disabled={!permissions.canVote}>
|
|
|
|
|
{permissions.canVote ? 'Upvote' : permissions.reasons.vote}
|
|
|
|
|
</button>
|
|
|
|
|
{!permissions.canCreateCell && (
|
|
|
|
|
<p>{permissions.reasons.createCell}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
#### `useNetwork()`
|
|
|
|
|
|
|
|
|
|
Monitor Waku network connection state.
|
|
|
|
|
|
|
|
|
|
**Data:**
|
|
|
|
|
- `isConnected: boolean` - Waku network connection status
|
|
|
|
|
- `statusMessage: string` - Human-readable status
|
|
|
|
|
- `issues: string[]` - Connection issues
|
|
|
|
|
- `isHydrated: boolean` - Whether initial data loaded
|
|
|
|
|
- `canRefresh: boolean` - Whether refresh is available
|
|
|
|
|
|
|
|
|
|
**Actions:**
|
|
|
|
|
- `refresh(): Promise<void>` - Refresh network data
|
|
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
```tsx
|
|
|
|
|
function NetworkIndicator() {
|
|
|
|
|
const { isConnected, statusMessage, refresh } = useNetwork();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<span>{statusMessage}</span>
|
|
|
|
|
{!isConnected && <button onClick={refresh}>Reconnect</button>}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
#### `useUserDisplay(address: string)`
|
|
|
|
|
|
|
|
|
|
Get display information for any user address (wallet or anonymous).
|
|
|
|
|
|
|
|
|
|
**Returns:**
|
|
|
|
|
- `address: string` - User's address or session ID
|
|
|
|
|
- `displayName: string` - Computed display name
|
|
|
|
|
- `callSign?: string` - User's call sign
|
|
|
|
|
- `ensName?: string` - ENS name (wallet users only)
|
|
|
|
|
- `ensAvatar?: string` - ENS avatar URL
|
|
|
|
|
- `verificationStatus: EVerificationStatus` - Verification level
|
|
|
|
|
- `displayPreference: EDisplayPreference` - Display preference
|
|
|
|
|
- `lastUpdated: number` - Last identity update timestamp
|
|
|
|
|
- `isLoading: boolean` - Loading state
|
|
|
|
|
- `error?: string` - Error message
|
|
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
```tsx
|
|
|
|
|
function AuthorBadge({ authorAddress }: { authorAddress: string }) {
|
|
|
|
|
const { displayName, callSign, ensName, verificationStatus } =
|
|
|
|
|
useUserDisplay(authorAddress);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<span>{displayName}</span>
|
|
|
|
|
{ensName && <span className="badge">ENS</span>}
|
|
|
|
|
{callSign && <span className="badge">Call Sign</span>}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
#### `useUIState<T>(key, defaultValue, category?)`
|
|
|
|
|
|
|
|
|
|
Persist UI state to IndexedDB with React state management.
|
|
|
|
|
|
|
|
|
|
**Parameters:**
|
|
|
|
|
- `key: string` - Unique key for the state
|
|
|
|
|
- `defaultValue: T` - Default value
|
|
|
|
|
- `category?: 'wizardStates' | 'preferences' | 'temporaryStates'` - Storage category (default: 'preferences')
|
|
|
|
|
|
|
|
|
|
**Returns:**
|
|
|
|
|
- `[value, setValue, { loading, error? }]` - Similar to useState with persistence
|
|
|
|
|
|
|
|
|
|
**Example:**
|
|
|
|
|
```tsx
|
|
|
|
|
function ThemeToggle() {
|
|
|
|
|
const [darkMode, setDarkMode] = useUIState('darkMode', true, 'preferences');
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button onClick={() => setDarkMode(!darkMode)}>
|
|
|
|
|
{darkMode ? 'Light' : 'Dark'} Mode
|
2025-09-25 21:52:40 +05:30
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
---
|
|
|
|
|
|
|
|
|
|
#### `useEthereumWallet()`
|
|
|
|
|
|
|
|
|
|
Low-level access to Ethereum wallet state (advanced use).
|
|
|
|
|
|
|
|
|
|
**Data:**
|
|
|
|
|
- `address: string | null`
|
|
|
|
|
- `isConnected: boolean`
|
|
|
|
|
- `connectors: Connector[]`
|
|
|
|
|
- `publicClient: PublicClient`
|
|
|
|
|
- `walletClient: WalletClient`
|
|
|
|
|
|
|
|
|
|
**Actions:**
|
|
|
|
|
- `connect(connectorId?): Promise<void>`
|
|
|
|
|
- `disconnect(): Promise<void>`
|
|
|
|
|
- `signMessage(message): Promise<string>`
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
#### `useClient()`
|
|
|
|
|
|
|
|
|
|
Access the underlying `OpChanClient` instance (advanced use only).
|
|
|
|
|
|
2025-09-25 21:52:40 +05:30
|
|
|
```tsx
|
2025-10-29 17:53:59 +05:30
|
|
|
const client = useClient();
|
|
|
|
|
// Access low-level APIs:
|
|
|
|
|
// - client.database
|
|
|
|
|
// - client.delegation
|
|
|
|
|
// - client.forumActions
|
|
|
|
|
// - client.userIdentityService
|
|
|
|
|
// - client.messageManager
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Usage Patterns
|
|
|
|
|
|
|
|
|
|
### Pattern 1: Anonymous-First UX
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
Allow users to interact immediately without wallet connection:
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
```tsx
|
|
|
|
|
function PostPage() {
|
|
|
|
|
const { user, permissions } = useForum();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{!user.currentUser && (
|
|
|
|
|
<div>
|
|
|
|
|
<button onClick={user.connect}>Connect Wallet</button>
|
|
|
|
|
<button onClick={user.startAnonymous}>Continue Anonymously</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{permissions.canComment && <CommentForm />}
|
|
|
|
|
|
|
|
|
|
{user.verificationStatus === 'anonymous' && !user.currentUser?.callSign && (
|
|
|
|
|
<CallSignPrompt />
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Pattern 2: Permission-Based UI
|
|
|
|
|
|
|
|
|
|
Show/hide features based on user capabilities:
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
function CellActions() {
|
|
|
|
|
const { permissions } = useForum();
|
|
|
|
|
const check = permissions.check('canCreateCell');
|
|
|
|
|
|
2025-09-25 21:52:40 +05:30
|
|
|
return (
|
|
|
|
|
<div>
|
2025-10-29 17:53:59 +05:30
|
|
|
{check.allowed ? (
|
|
|
|
|
<CreateCellButton />
|
2025-09-25 21:52:40 +05:30
|
|
|
) : (
|
2025-10-29 17:53:59 +05:30
|
|
|
<p>{check.reason}</p>
|
2025-09-25 21:52:40 +05:30
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
### Pattern 3: Real-Time Content Updates
|
|
|
|
|
|
|
|
|
|
Listen to content changes with React state:
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
```tsx
|
|
|
|
|
function PostList({ cellId }: { cellId: string }) {
|
|
|
|
|
const { postsByCell, pending } = useContent();
|
|
|
|
|
const posts = postsByCell[cellId] || [];
|
2025-10-23 12:16:25 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
{posts.map(post => (
|
|
|
|
|
<div key={post.id}>
|
|
|
|
|
{post.title}
|
|
|
|
|
{pending.isPending(post.id) && <span>Syncing...</span>}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Pattern 4: User Identity Display
|
|
|
|
|
|
|
|
|
|
Display user information with automatic updates:
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
function UserAvatar({ address }: { address: string }) {
|
|
|
|
|
const { displayName, ensName, callSign, ensAvatar } = useUserDisplay(address);
|
2025-10-23 12:16:25 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
{ensAvatar && <img src={ensAvatar} alt={displayName} />}
|
|
|
|
|
<span>{displayName}</span>
|
|
|
|
|
{ensName && <span className="badge">ENS: {ensName}</span>}
|
|
|
|
|
{callSign && <span className="badge">#{callSign}</span>}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Authentication Flows
|
|
|
|
|
|
|
|
|
|
### Anonymous User Flow
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
// 1. Start anonymous session
|
|
|
|
|
const sessionId = await startAnonymous();
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
// 2. User can immediately interact
|
|
|
|
|
await createPost({ cellId, title, content });
|
|
|
|
|
await vote({ targetId: postId, isUpvote: true });
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
// 3. Optionally set call sign
|
|
|
|
|
await updateProfile({ callSign: 'my_username' });
|
|
|
|
|
|
|
|
|
|
// 4. Later upgrade to wallet if desired
|
|
|
|
|
await connect();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Wallet User Flow
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
// 1. Connect wallet
|
|
|
|
|
await connect();
|
|
|
|
|
// User is now WALLET_CONNECTED
|
|
|
|
|
|
|
|
|
|
// 2. Optionally verify ENS ownership
|
|
|
|
|
const isVerified = await verifyOwnership();
|
|
|
|
|
// If ENS found, user becomes ENS_VERIFIED
|
|
|
|
|
|
|
|
|
|
// 3. Delegate browser keys for better UX
|
|
|
|
|
await delegate('7days');
|
|
|
|
|
// Subsequent actions don't require wallet signatures
|
|
|
|
|
|
|
|
|
|
// 4. Interact with platform
|
|
|
|
|
await createCell({ name, description });
|
|
|
|
|
await createPost({ cellId, title, content });
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Type Definitions
|
|
|
|
|
|
|
|
|
|
### User
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
type User = {
|
|
|
|
|
address: string; // 0x${string} for wallet, UUID for anonymous
|
|
|
|
|
displayName: string;
|
|
|
|
|
displayPreference: EDisplayPreference;
|
|
|
|
|
verificationStatus: EVerificationStatus;
|
|
|
|
|
callSign?: string;
|
|
|
|
|
ensName?: string;
|
|
|
|
|
ensAvatar?: string;
|
|
|
|
|
lastChecked?: number;
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### EVerificationStatus
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
enum EVerificationStatus {
|
|
|
|
|
ANONYMOUS = 'anonymous',
|
|
|
|
|
WALLET_UNCONNECTED = 'wallet-unconnected',
|
|
|
|
|
WALLET_CONNECTED = 'wallet-connected',
|
|
|
|
|
ENS_VERIFIED = 'ens-verified',
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Cell, Post, Comment
|
|
|
|
|
|
|
|
|
|
All content types include:
|
|
|
|
|
- Cryptographic signatures
|
|
|
|
|
- Author information
|
|
|
|
|
- Timestamps
|
|
|
|
|
- Relevance scores
|
|
|
|
|
- Moderation state
|
|
|
|
|
|
|
|
|
|
See `@opchan/core` for detailed type definitions.
|
|
|
|
|
|
|
|
|
|
## Advanced Usage
|
|
|
|
|
|
|
|
|
|
### Custom Permission Logic
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
function AdminPanel({ cellId }: { cellId: string }) {
|
|
|
|
|
const { permissions, content } = useForum();
|
|
|
|
|
const cell = content.cells.find(c => c.id === cellId);
|
|
|
|
|
|
|
|
|
|
if (!permissions.canModerate(cellId)) {
|
|
|
|
|
return <p>Admin access required</p>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <ModerationTools cell={cell} />;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Message Pending States
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
function PostWithSyncStatus({ post }: { post: Post }) {
|
|
|
|
|
const { pending } = useContent();
|
|
|
|
|
const [isPending, setIsPending] = useState(pending.isPending(post.id));
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return pending.onChange(() => {
|
|
|
|
|
setIsPending(pending.isPending(post.id));
|
|
|
|
|
});
|
|
|
|
|
}, [post.id]);
|
2025-10-23 12:16:25 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
{post.title}
|
|
|
|
|
{isPending && <span className="badge">Syncing...</span>}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Identity Cache Management
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
function UserList({ addresses }: { addresses: string[] }) {
|
|
|
|
|
return addresses.map(addr => {
|
|
|
|
|
const display = useUserDisplay(addr);
|
|
|
|
|
// Identity automatically cached and shared across all components
|
|
|
|
|
// Updates propagate automatically when user profiles change
|
|
|
|
|
return <UserCard key={addr} {...display} />;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Configuration
|
|
|
|
|
|
|
|
|
|
### OpChanProvider Config
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
interface OpChanProviderProps {
|
|
|
|
|
config: {
|
|
|
|
|
wakuConfig?: {
|
|
|
|
|
contentTopic?: string;
|
|
|
|
|
reliableChannelId?: string;
|
|
|
|
|
};
|
|
|
|
|
reownProjectId?: string; // For WalletConnect v2
|
|
|
|
|
};
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-10-29 18:28:40 +05:30
|
|
|
## Complete Example App
|
|
|
|
|
|
|
|
|
|
Here's a minimal working example that demonstrates all the key patterns:
|
|
|
|
|
|
|
|
|
|
### 1. Main Entry Point (`main.tsx`)
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { createRoot } from 'react-dom/client';
|
|
|
|
|
import { OpChanProvider } from '@opchan/react';
|
|
|
|
|
import { Buffer } from 'buffer';
|
|
|
|
|
import App from './App';
|
|
|
|
|
|
|
|
|
|
// Required polyfill
|
|
|
|
|
if (!(window as any).Buffer) {
|
|
|
|
|
(window as any).Buffer = Buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createRoot(document.getElementById('root')!).render(
|
|
|
|
|
<OpChanProvider
|
|
|
|
|
config={{
|
|
|
|
|
wakuConfig: {
|
|
|
|
|
contentTopic: '/opchan/1/messages/proto',
|
|
|
|
|
reliableChannelId: 'opchan-messages'
|
|
|
|
|
},
|
|
|
|
|
reownProjectId: import.meta.env.VITE_REOWN_SECRET || '2ead96ea166a03e5ab50e5c190532e72'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<App />
|
|
|
|
|
</OpChanProvider>
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. App Component (`App.tsx`)
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { useForum } from '@opchan/react';
|
|
|
|
|
|
|
|
|
|
export default function App() {
|
|
|
|
|
const { user, content, permissions, network } = useForum();
|
|
|
|
|
|
|
|
|
|
// Wait for initial data load
|
|
|
|
|
if (!network.isHydrated) {
|
|
|
|
|
return <div>Loading...</div>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gray-900 text-white">
|
|
|
|
|
<Header />
|
|
|
|
|
<main className="container mx-auto p-4">
|
|
|
|
|
{!user.currentUser ? (
|
|
|
|
|
<AuthPrompt />
|
|
|
|
|
) : (
|
|
|
|
|
<ForumInterface />
|
|
|
|
|
)}
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. Authentication Component (`AuthPrompt.tsx`)
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { useAuth } from '@opchan/react';
|
|
|
|
|
|
|
|
|
|
export function AuthPrompt() {
|
|
|
|
|
const { connect, startAnonymous } = useAuth();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="text-center space-y-4">
|
|
|
|
|
<h1 className="text-2xl font-bold">Welcome to OpChan</h1>
|
|
|
|
|
<p className="text-gray-400">Choose how you'd like to participate:</p>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={connect}
|
|
|
|
|
className="w-full bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
|
|
|
|
|
>
|
|
|
|
|
Connect Wallet
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={startAnonymous}
|
|
|
|
|
className="w-full bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded"
|
|
|
|
|
>
|
|
|
|
|
Continue Anonymously
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 4. Header Component (`Header.tsx`)
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { useAuth } from '@opchan/react';
|
|
|
|
|
|
|
|
|
|
export function Header() {
|
|
|
|
|
const { currentUser, disconnect, verificationStatus } = useAuth();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<header className="bg-gray-800 p-4">
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
<h1 className="text-xl font-bold">OpChan</h1>
|
|
|
|
|
|
|
|
|
|
{currentUser ? (
|
|
|
|
|
<div className="flex items-center space-x-4">
|
|
|
|
|
<span className="text-sm">
|
|
|
|
|
{currentUser.displayName}
|
|
|
|
|
{verificationStatus === 'anonymous' && ' (Anonymous)'}
|
|
|
|
|
{verificationStatus === 'ens-verified' && ' (ENS)'}
|
|
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={disconnect}
|
|
|
|
|
className="text-sm text-gray-400 hover:text-white"
|
|
|
|
|
>
|
|
|
|
|
Disconnect
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 5. Forum Interface (`ForumInterface.tsx`)
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { useContent, usePermissions } from '@opchan/react';
|
|
|
|
|
|
|
|
|
|
export function ForumInterface() {
|
|
|
|
|
const { cells, posts, createPost } = useContent();
|
|
|
|
|
const { canPost, canCreateCell } = usePermissions();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
<h2 className="text-xl font-semibold">Cells</h2>
|
|
|
|
|
{canCreateCell && (
|
|
|
|
|
<button className="bg-green-600 hover:bg-green-700 px-4 py-2 rounded">
|
|
|
|
|
Create Cell
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
{cells.map(cell => (
|
|
|
|
|
<CellCard key={cell.id} cell={cell} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4">Recent Posts</h3>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{posts.slice(0, 10).map(post => (
|
|
|
|
|
<PostCard key={post.id} post={post} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 6. Cell Card Component (`CellCard.tsx`)
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { useContent } from '@opchan/react';
|
|
|
|
|
|
|
|
|
|
export function CellCard({ cell }) {
|
|
|
|
|
const { postsByCell } = useContent();
|
|
|
|
|
const cellPosts = postsByCell[cell.id] || [];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-gray-800 p-4 rounded-lg">
|
|
|
|
|
<h3 className="font-semibold">{cell.name}</h3>
|
|
|
|
|
<p className="text-sm text-gray-400 mb-2">{cell.description}</p>
|
|
|
|
|
<div className="text-xs text-gray-500">
|
|
|
|
|
{cellPosts.length} posts
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 7. Post Card Component (`PostCard.tsx`)
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
import { useUserDisplay } from '@opchan/react';
|
|
|
|
|
|
|
|
|
|
export function PostCard({ post }) {
|
|
|
|
|
const { displayName, callSign, ensName } = useUserDisplay(post.author);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-gray-800 p-3 rounded">
|
|
|
|
|
<div className="flex justify-between items-start mb-2">
|
|
|
|
|
<span className="text-sm font-medium">
|
|
|
|
|
{displayName}
|
|
|
|
|
{callSign && ` (#${callSign})`}
|
|
|
|
|
{ensName && ` (${ensName})`}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-xs text-gray-500">
|
|
|
|
|
{new Date(post.timestamp).toLocaleDateString()}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<h4 className="font-medium">{post.title}</h4>
|
|
|
|
|
<p className="text-sm text-gray-400 mt-1">{post.content}</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 8. Package.json Dependencies
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"dependencies": {
|
|
|
|
|
"@opchan/react": "^1.1.0",
|
|
|
|
|
"@opchan/core": "^1.0.0",
|
|
|
|
|
"react": "^18.3.0",
|
|
|
|
|
"react-dom": "^18.3.0",
|
|
|
|
|
"buffer": "^6.0.3"
|
|
|
|
|
},
|
|
|
|
|
"devDependencies": {
|
|
|
|
|
"@types/react": "^18.3.0",
|
|
|
|
|
"typescript": "^5.3.0",
|
|
|
|
|
"vite": "^5.0.0",
|
|
|
|
|
"@vitejs/plugin-react": "^4.0.0"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 9. Vite Configuration (`vite.config.ts`)
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import { defineConfig } from 'vite';
|
|
|
|
|
import react from '@vitejs/plugin-react';
|
|
|
|
|
|
|
|
|
|
export default defineConfig({
|
|
|
|
|
plugins: [react()],
|
|
|
|
|
define: {
|
|
|
|
|
global: 'globalThis',
|
|
|
|
|
},
|
|
|
|
|
optimizeDeps: {
|
|
|
|
|
include: ['buffer'],
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 10. Environment Variables (`.env`)
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
VITE_REOWN_SECRET=your_reown_project_id_here
|
|
|
|
|
```
|
|
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
## Best Practices
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
1. **Use `useForum()` for most cases** - Cleaner than importing individual hooks
|
|
|
|
|
2. **Check permissions before showing UI** - Better UX than showing disabled buttons
|
|
|
|
|
3. **Handle anonymous users gracefully** - Offer both wallet and anonymous options
|
|
|
|
|
4. **Use `useUserDisplay` for all identity rendering** - Automatic caching and updates
|
|
|
|
|
5. **Monitor `network.isHydrated`** - Wait for initial data before rendering content
|
|
|
|
|
6. **Use `pending` helpers** - Show loading states for async operations
|
|
|
|
|
7. **Preserve verification status** - When updating anonymous users, maintain their status
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
## Migration from v1.0
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
### Breaking Changes in v2.0
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
- Added `ANONYMOUS` verification status
|
|
|
|
|
- `delegationProof` is now optional in messages
|
|
|
|
|
- `startAnonymous()` method added to `useAuth()`
|
|
|
|
|
- Permission checks now support anonymous users
|
|
|
|
|
- `User.address` is now `string` (was `0x${string}`)
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
### Migration Steps
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
1. Update permission checks to handle anonymous users
|
|
|
|
|
2. Update UI to offer anonymous option alongside wallet connection
|
|
|
|
|
3. Handle both wallet addresses and session IDs in identity display
|
|
|
|
|
4. Test message verification with optional delegation proofs
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
## Troubleshooting
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 18:28:40 +05:30
|
|
|
### Error: "useClient must be used within ClientProvider"
|
|
|
|
|
|
|
|
|
|
**Root Cause:** Components using `@opchan/react` hooks are not wrapped by `OpChanProvider`.
|
|
|
|
|
|
|
|
|
|
**Solution:**
|
|
|
|
|
```tsx
|
|
|
|
|
// ❌ WRONG - Hooks used outside provider
|
|
|
|
|
function App() {
|
|
|
|
|
const { currentUser } = useAuth(); // This will fail
|
|
|
|
|
return <div>Hello</div>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ✅ CORRECT - All hooks inside provider
|
|
|
|
|
function App() {
|
|
|
|
|
return (
|
|
|
|
|
<OpChanProvider config={config}>
|
|
|
|
|
<MainApp />
|
|
|
|
|
</OpChanProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function MainApp() {
|
|
|
|
|
const { currentUser } = useAuth(); // This works
|
|
|
|
|
return <div>Hello</div>;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Error: Wallet Connection Fails
|
|
|
|
|
|
|
|
|
|
**Root Cause:** Missing or invalid `reownProjectId` in provider config.
|
|
|
|
|
|
|
|
|
|
**Solution:**
|
|
|
|
|
```tsx
|
|
|
|
|
// ❌ WRONG - Missing reownProjectId
|
|
|
|
|
<OpChanProvider config={{ wakuConfig: {...} }}>
|
|
|
|
|
|
|
|
|
|
// ✅ CORRECT - Include reownProjectId
|
|
|
|
|
<OpChanProvider config={{
|
|
|
|
|
wakuConfig: {...},
|
|
|
|
|
reownProjectId: 'your-project-id'
|
|
|
|
|
}}>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Error: "Buffer is not defined"
|
|
|
|
|
|
|
|
|
|
**Root Cause:** Missing Buffer polyfill for crypto libraries.
|
|
|
|
|
|
|
|
|
|
**Solution:**
|
|
|
|
|
```tsx
|
|
|
|
|
import { Buffer } from 'buffer';
|
|
|
|
|
|
|
|
|
|
// Add before rendering
|
|
|
|
|
if (!(window as any).Buffer) {
|
|
|
|
|
(window as any).Buffer = Buffer;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
### Anonymous users can't interact after setting call sign
|
|
|
|
|
- Ensure `mapVerificationStatus` includes `ANONYMOUS` case
|
|
|
|
|
- Check that `updateProfile` preserves `verificationStatus`
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
### Wallet sync clearing anonymous sessions
|
|
|
|
|
- Verify wallet disconnect logic checks for anonymous users before clearing
|
2025-10-28 11:29:21 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
### Permission checks failing for anonymous users
|
|
|
|
|
- Ensure `isAnonymous` is included in permission conditions
|
|
|
|
|
- Check that `canPost/canComment/canVote` return true for anonymous
|
2025-10-28 11:29:21 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
## License
|
2025-09-25 21:52:40 +05:30
|
|
|
|
|
|
|
|
MIT
|
|
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
---
|
2025-09-25 21:52:40 +05:30
|
|
|
|
2025-10-29 17:53:59 +05:30
|
|
|
**Built with ❤️ for decentralized communities**
|