BigBlocks Docs
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.

View Component Preview →

Installation

npx bigblocks add handcash-connector

Import

import { HandCashConnector } from 'bigblocks';

Props

PropTypeRequiredDefaultDescription
configHandCashConfigYes-HandCash OAuth configuration
onConnect(user: HandCashUser) => voidNo-Callback when connection succeeds
onError(error: HandCashError) => voidNo-Callback when connection fails
onDisconnect() => voidNo-Callback when wallet is disconnected
classNamestringNo-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=sandbox

Production 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=production

Security 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 appId is correct for the environment
  • Check redirectUri matches 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);
  }}
/>

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)}
    />
  );
}