Components/Wallet
HandCashConnector
Connect and authenticate with HandCash wallet for seamless Bitcoin SV transactions and identity management
A comprehensive OAuth integration component for connecting with HandCash wallets, enabling seamless Bitcoin SV transactions, balance management, and social features in your application.
Installation
npx bigblocks add handcash-connectorImport
import { HandCashConnector } from 'bigblocks';Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| config | HandCashConfig | Yes | - | HandCash OAuth configuration |
| onConnect | (user: HandCashUser) => void | No | - | Callback when connection succeeds |
| onError | (error: HandCashError) => void | No | - | Callback when connection fails |
| onDisconnect | () => void | No | - | Callback when wallet is disconnected |
| className | string | No | - | Additional CSS classes |
HandCashConfig Interface
interface HandCashConfig {
appId: string; // HandCash app ID (required)
redirectUri?: string; // OAuth callback URL
permissions?: HandCashPermission[]; // Requested permissions
environment?: 'production' | 'sandbox'; // HandCash environment
}
interface HandCashPermission {
scope:
| 'user_profile_read' // Read user profile data
| 'wallet_balance_read' // Read wallet balance
| 'payments_write' // Send payments
| 'friends_read' // Access friends list
| 'user_activity_read' // Read transaction history
| 'data_storage_write'; // Store app data
required: boolean; // Whether permission is mandatory
}HandCashUser Interface
interface HandCashUser {
handle: string; // HandCash handle (@username)
publicKey: string; // Bitcoin public key
paymail: string; // Paymail address
profile?: {
displayName?: string; // Display name
avatarUrl?: string; // Profile picture URL
bio?: string; // User biography
verified: boolean; // Verification status
};
wallet: {
balance: {
bsv: number; // BSV balance
usd: number; // USD equivalent
pending: number; // Pending amount
lastUpdated: number; // Last update timestamp
};
limits: {
daily: number; // Daily spending limit
monthly: number; // Monthly spending limit
perTransaction: number; // Per-transaction limit
remainingDaily: number; // Remaining daily limit
remainingMonthly: number; // Remaining monthly limit
};
};
}Basic Usage
import { HandCashConnector } from 'bigblocks';
export default function WalletConnect() {
return (
<HandCashConnector
config={{
appId: 'your-handcash-app-id'
}}
/>
);
}Advanced Usage
Complete OAuth Integration
import { HandCashConnector } from 'bigblocks';
import { useState } from 'react';
export default function HandCashIntegration() {
const [connectedUser, setConnectedUser] = useState<HandCashUser | null>(null);
const [error, setError] = useState<string | null>(null);
const handleConnect = (user: HandCashUser) => {
setConnectedUser(user);
setError(null);
console.log('Connected to HandCash:', user.handle);
console.log('Balance:', user.wallet.balance.bsv, 'BSV');
};
const handleError = (error: HandCashError) => {
setError(error.message);
console.error('HandCash connection failed:', error);
};
const handleDisconnect = () => {
setConnectedUser(null);
setError(null);
console.log('Disconnected from HandCash');
};
return (
<div className="handcash-integration">
{!connectedUser ? (
<HandCashConnector
config={{
appId: process.env.NEXT_PUBLIC_HANDCASH_APP_ID!,
redirectUri: `${window.location.origin}/auth/handcash/callback`,
permissions: [
{ scope: 'user_profile_read', required: true },
{ scope: 'wallet_balance_read', required: true },
{ scope: 'payments_write', required: false },
{ scope: 'friends_read', required: false }
],
environment: 'production'
}}
onConnect={handleConnect}
onError={handleError}
onDisconnect={handleDisconnect}
/>
) : (
<div className="connected-wallet">
<h3>Connected: {connectedUser.handle}</h3>
<p>Balance: {connectedUser.wallet.balance.bsv} BSV</p>
<p>USD Value: ${connectedUser.wallet.balance.usd}</p>
<button onClick={handleDisconnect}>Disconnect</button>
</div>
)}
{error && (
<div className="error-message">
<p>Error: {error}</p>
</div>
)}
</div>
);
}Production Configuration
import { HandCashConnector } from 'bigblocks';
export default function ProductionHandCash() {
return (
<HandCashConnector
config={{
appId: process.env.NEXT_PUBLIC_HANDCASH_APP_ID!,
redirectUri: process.env.NEXT_PUBLIC_HANDCASH_REDIRECT_URI!,
permissions: [
{ scope: 'user_profile_read', required: true },
{ scope: 'wallet_balance_read', required: true },
{ scope: 'payments_write', required: true },
{ scope: 'friends_read', required: false },
{ scope: 'user_activity_read', required: false }
],
environment: 'production'
}}
onConnect={(user) => {
// Store user data securely
localStorage.setItem('handcash_user', JSON.stringify(user));
// Navigate to dashboard
window.location.href = '/dashboard';
}}
onError={(error) => {
// Log error for monitoring
console.error('HandCash OAuth failed:', error);
// Show user-friendly error
alert('Failed to connect HandCash wallet. Please try again.');
}}
className="bg-green-50 border-2 border-green-200 rounded-lg p-6"
/>
);
}Sandbox Testing
import { HandCashConnector } from 'bigblocks';
export default function HandCashSandbox() {
return (
<div className="sandbox-testing">
<h2>HandCash Sandbox Testing</h2>
<p>Use test credentials for development</p>
<HandCashConnector
config={{
appId: 'sandbox-app-id',
environment: 'sandbox',
permissions: [
{ scope: 'user_profile_read', required: true },
{ scope: 'wallet_balance_read', required: true },
{ scope: 'payments_write', required: true }
]
}}
onConnect={(user) => {
console.log('Sandbox connection successful:', user);
}}
onError={(error) => {
console.log('Sandbox error (expected):', error);
}}
/>
</div>
);
}Common Patterns
Wallet Selection Dashboard
import { HandCashConnector, YoursWalletConnector } from 'bigblocks';
import { useState } from 'react';
export default function WalletSelectionDashboard() {
const [selectedWallet, setSelectedWallet] = useState<string | null>(null);
return (
<div className="wallet-selection">
<h2>Connect Your Wallet</h2>
<div className="wallet-options">
<div className="wallet-option">
<h3>HandCash</h3>
<p>Popular mobile wallet with social features</p>
<HandCashConnector
config={{
appId: process.env.NEXT_PUBLIC_HANDCASH_APP_ID!
}}
onConnect={(user) => {
setSelectedWallet('handcash');
console.log('Connected HandCash:', user.handle);
}}
/>
</div>
<div className="wallet-option">
<h3>Yours Wallet</h3>
<p>Browser extension wallet</p>
<YoursWalletConnector
onSuccess={(result) => {
setSelectedWallet('yours');
console.log('Connected Yours:', result.publicKey);
}}
/>
</div>
</div>
{selectedWallet && (
<div className="selected-wallet">
<p>Connected wallet: {selectedWallet}</p>
</div>
)}
</div>
);
}Payment Integration
import { HandCashConnector } from 'bigblocks';
import { useState } from 'react';
export default function PaymentIntegration() {
const [user, setUser] = useState<HandCashUser | null>(null);
const sendPayment = async (recipient: string, amount: number) => {
if (!user) return;
try {
const response = await fetch('/api/wallets/handcash/payments/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
handle: user.handle,
recipients: [{
destination: recipient,
amount: amount * 100000000 // Convert BSV to satoshis
}],
description: 'Payment from my app'
})
});
const result = await response.json();
if (result.success) {
console.log('Payment sent:', result.payment.transactionId);
alert('Payment sent successfully!');
}
} catch (error) {
console.error('Payment failed:', error);
alert('Payment failed. Please try again.');
}
};
return (
<div className="payment-integration">
{!user ? (
<HandCashConnector
config={{
appId: process.env.NEXT_PUBLIC_HANDCASH_APP_ID!,
permissions: [
{ scope: 'user_profile_read', required: true },
{ scope: 'wallet_balance_read', required: true },
{ scope: 'payments_write', required: true }
]
}}
onConnect={setUser}
/>
) : (
<div className="payment-interface">
<h3>Send Payment</h3>
<p>Balance: {user.wallet.balance.bsv} BSV</p>
<button onClick={() => sendPayment('@recipient', 0.001)}>
Send 0.001 BSV to @recipient
</button>
<button onClick={() => sendPayment('1ABC...xyz', 0.01)}>
Send 0.01 BSV to address
</button>
</div>
)}
</div>
);
}Social Features Integration
import { HandCashConnector } from 'bigblocks';
import { useState, useEffect } from 'react';
export default function SocialFeaturesIntegration() {
const [user, setUser] = useState<HandCashUser | null>(null);
const [friends, setFriends] = useState<any[]>([]);
useEffect(() => {
if (user) {
fetchFriends();
}
}, [user]);
const fetchFriends = async () => {
if (!user) return;
try {
const response = await fetch(`/api/wallets/handcash/friends?handle=${user.handle}`);
const result = await response.json();
setFriends(result.friends || []);
} catch (error) {
console.error('Failed to fetch friends:', error);
}
};
return (
<div className="social-features">
{!user ? (
<HandCashConnector
config={{
appId: process.env.NEXT_PUBLIC_HANDCASH_APP_ID!,
permissions: [
{ scope: 'user_profile_read', required: true },
{ scope: 'friends_read', required: true }
]
}}
onConnect={setUser}
/>
) : (
<div className="social-interface">
<h3>Welcome, {user.profile?.displayName || user.handle}!</h3>
<div className="friends-list">
<h4>Your HandCash Friends ({friends.length})</h4>
{friends.map((friend) => (
<div key={friend.handle} className="friend-item">
<img src={friend.avatarUrl} alt={friend.displayName} />
<span>{friend.displayName || friend.handle}</span>
<button onClick={() => sendPayment(friend.handle, 0.001)}>
Send Tip
</button>
</div>
))}
</div>
</div>
)}
</div>
);
}Authentication Requirements
The HandCashConnector requires Bitcoin authentication context for secure operation:
import {
BitcoinAuthProvider,
BitcoinQueryProvider,
HandCashConnector
} from 'bigblocks';
function App() {
return (
<BitcoinQueryProvider>
<BitcoinAuthProvider config={{ apiUrl: '/api' }}>
<HandCashConnector
config={{ appId: 'your-app-id' }}
/>
</BitcoinAuthProvider>
</BitcoinQueryProvider>
);
}API Integration
The HandCashConnector integrates with several backend endpoints:
OAuth Flow Endpoints
// Initialize OAuth flow
POST /api/wallets/handcash/auth/init
{
appId: string;
redirectUri?: string;
permissions: HandCashPermission[];
}
// Complete OAuth flow
POST /api/wallets/handcash/auth/callback
{
code: string;
state: string;
sessionId: string;
}
// Refresh access token
POST /api/wallets/handcash/auth/refresh
{
handle: string;
refreshToken: string;
}Wallet Operations
// Get wallet status
GET /api/wallets/handcash/status?handle=<handle>
// Send payment
POST /api/wallets/handcash/payments/send
{
handle: string;
recipients: PaymentRecipient[];
description?: string;
}
// Get transaction history
GET /api/wallets/handcash/transactions?handle=<handle>&type=<type>
// Get friends list
GET /api/wallets/handcash/friends?handle=<handle>Real-time Updates
// WebSocket for live updates
WS /api/wallets/handcash/stream
// Subscribe to events
{
type: 'subscribe',
handle: string,
events: ['balance', 'transaction', 'friend']
}Error Handling
Common Error Types
interface HandCashError {
code:
| 'AUTH_FAILED' // OAuth authentication failed
| 'INVALID_TOKEN' // Access token invalid/expired
| 'INSUFFICIENT_BALANCE' // Not enough BSV for operation
| 'LIMIT_EXCEEDED' // Spending limit exceeded
| 'WALLET_NOT_FOUND' // HandCash wallet not found
| 'PERMISSION_DENIED' // Required permission not granted
| 'RATE_LIMITED' // API rate limit exceeded
| 'SERVICE_UNAVAILABLE'; // HandCash service temporarily down
message: string;
details?: {
handle?: string;
requiredPermissions?: string[];
limitType?: string;
remaining?: number;
retryAfter?: number;
};
}Error Handling Examples
import { HandCashConnector } from 'bigblocks';
export default function ErrorHandlingExample() {
const handleError = (error: HandCashError) => {
switch (error.code) {
case 'AUTH_FAILED':
console.error('HandCash authentication failed:', error.message);
alert('Failed to connect to HandCash. Please try again.');
break;
case 'PERMISSION_DENIED':
console.error('Permission denied:', error.details?.requiredPermissions);
alert('Please grant the required permissions to continue.');
break;
case 'RATE_LIMITED':
console.error('Rate limited. Retry after:', error.details?.retryAfter);
alert(`Too many requests. Please wait ${error.details?.retryAfter} seconds.`);
break;
case 'SERVICE_UNAVAILABLE':
console.error('HandCash service unavailable');
alert('HandCash service is temporarily unavailable. Please try again later.');
break;
default:
console.error('Unknown HandCash error:', error);
alert('An unexpected error occurred. Please try again.');
}
};
return (
<HandCashConnector
config={{ appId: 'your-app-id' }}
onError={handleError}
/>
);
}Environment Configuration
Development Setup
// .env.local
NEXT_PUBLIC_HANDCASH_APP_ID=your-sandbox-app-id
NEXT_PUBLIC_HANDCASH_REDIRECT_URI=http://localhost:3000/auth/handcash/callback
HANDCASH_APP_SECRET=your-app-secret
HANDCASH_ENVIRONMENT=sandboxProduction Setup
// .env.production
NEXT_PUBLIC_HANDCASH_APP_ID=your-production-app-id
NEXT_PUBLIC_HANDCASH_REDIRECT_URI=https://yourapp.com/auth/handcash/callback
HANDCASH_APP_SECRET=your-production-app-secret
HANDCASH_ENVIRONMENT=productionSecurity Considerations
Token Security
- Access tokens are encrypted before storage
- Refresh tokens are rotated on each use
- All tokens have expiration times
- OAuth state parameter prevents CSRF attacks
// Secure token handling
const handleConnect = (user: HandCashUser) => {
// Never store tokens in localStorage
// Use secure HTTP-only cookies instead
document.cookie = `handcash_session=${user.sessionId}; HttpOnly; Secure; SameSite=Strict`;
};Permission Management
// Request minimal permissions
const minimumPermissions: HandCashPermission[] = [
{ scope: 'user_profile_read', required: true },
{ scope: 'wallet_balance_read', required: false }
];
// Allow users to opt-in to additional permissions
const optionalPermissions: HandCashPermission[] = [
{ scope: 'payments_write', required: false },
{ scope: 'friends_read', required: false }
];Performance Optimization
Connection Caching
import { HandCashConnector } from 'bigblocks';
import { useEffect, useState } from 'react';
export default function CachedConnection() {
const [cachedUser, setCachedUser] = useState<HandCashUser | null>(null);
useEffect(() => {
// Check for existing connection
const checkExistingConnection = async () => {
try {
const response = await fetch('/api/wallets/handcash/status');
if (response.ok) {
const user = await response.json();
setCachedUser(user);
}
} catch (error) {
console.log('No existing connection');
}
};
checkExistingConnection();
}, []);
if (cachedUser) {
return (
<div className="cached-connection">
<p>Already connected: {cachedUser.handle}</p>
<p>Balance: {cachedUser.wallet.balance.bsv} BSV</p>
</div>
);
}
return (
<HandCashConnector
config={{ appId: 'your-app-id' }}
onConnect={setCachedUser}
/>
);
}Troubleshooting
Common Issues
OAuth flow fails silently
- Verify
appIdis correct for the environment - Check
redirectUrimatches registered callback URL - Ensure app has the correct permissions configured
Connection established but balance is zero
- User may be on testnet vs mainnet
- Check environment configuration
- Verify HandCash app has correct network settings
Payments fail with "insufficient balance"
- Check actual balance vs displayed balance
- Account for transaction fees (~0.0001 BSV)
- Verify spending limits aren't exceeded
Token expires quickly
- Implement automatic token refresh
- Handle token expiration gracefully
- Store refresh tokens securely
Debug Mode
<HandCashConnector
config={{
appId: 'your-app-id',
environment: 'sandbox' // Use sandbox for debugging
}}
onConnect={(user) => {
console.log('HandCash connection debug:', {
handle: user.handle,
balance: user.wallet.balance,
permissions: user.permissions
});
}}
onError={(error) => {
console.error('HandCash error debug:', error);
}}
/>Related Components
- YoursWalletConnector - Alternative wallet connection
- SendBSVButton - Send BSV transactions
- WalletOverview - Display wallet information
- DonateButton - Accept Bitcoin donations
API Reference
HandCashConnector Component
interface HandCashConnectorProps {
config: HandCashConfig;
onConnect?: (user: HandCashUser) => void;
onError?: (error: HandCashError) => void;
onDisconnect?: () => void;
className?: string;
}Usage with React Query
import { useQuery, useMutation } from '@tanstack/react-query';
import { HandCashConnector } from 'bigblocks';
function HandCashWithQuery() {
const { data: connectionStatus } = useQuery({
queryKey: ['handcash-status'],
queryFn: () => fetch('/api/wallets/handcash/status').then(res => res.json()),
refetchInterval: 30000 // Refresh every 30 seconds
});
const connectMutation = useMutation({
mutationFn: (authCode: string) =>
fetch('/api/wallets/handcash/auth/callback', {
method: 'POST',
body: JSON.stringify({ code: authCode })
}).then(res => res.json()),
onSuccess: (user) => {
console.log('Connected:', user.handle);
}
});
return (
<HandCashConnector
config={{ appId: 'your-app-id' }}
onConnect={(user) => connectMutation.mutate(user.authCode)}
/>
);
}