React Sample MiniApp

This section showcases a sample React MiniApp:

/**
 * React Usage Examples
 * Shows how to use the Simple API with React hooks and components
 */

import React, { useEffect, useState } from 'react';
import {
  // Simple React hooks
  useSimpleMiniApp,
  useSimpleTrade,
  useSimpleMarketData,
  useSimplePositions,
  useSimpleBalance,
  useSimplePermissions,
  useSimpleConnection,
  
  // Context provider and hooks
  SimpleMiniAppProvider,
  useSimpleMiniAppContext,
  useSimpleTrading,
  useSimpleAccount,
  useSimplePermissionsContext,
  
  // Types
  SimpleOrderData,
  SimpleMarketData,
  SimplePermission
} from '../index';

// ============================================
// Example 1: Basic Hook Usage
// ============================================

/**
 * Simple trading app using individual hooks
 * This shows the most basic usage pattern
 */
export function BasicTradingApp() {
  const { client, connected, sessionId, error } = useSimpleMiniApp({
    appId: 'basic-trader',
    debug: true
  });

  const { placeOrder, loading: tradingLoading, error: tradingError } = useSimpleTrade(client);
  const { data: marketData, subscribe } = useSimpleMarketData(client);
  const { permissions, requestTradingPermissions } = useSimplePermissions(client);

  useEffect(() => {
    if (connected) {
      // Subscribe to market data on connection
      subscribe(['ETH', 'BTC', 'SOL']);
      
      // Request trading permissions
      requestTradingPermissions();
    }
  }, [connected, subscribe, requestTradingPermissions]);

  const handleQuickBuy = async (symbol: string, size: number) => {
    try {
      await placeOrder({
        symbol,
        side: 'buy',
        orderType: 'market',
        size
      });
      alert(`Bought ${size} ${symbol}!`);
    } catch (err: any) {
      alert(`Order failed: ${err.message}`);
    }
  };

  if (error) {
    return <div style={{ color: 'red' }}>Error: {error.message}</div>;
  }

  if (!connected) {
    return <div>Connecting to Based.One Terminal...</div>;
  }

  return (
    <div style={{ padding: '20px', fontFamily: 'monospace' }}>
      <h2>🚀 Simple Trader</h2>
      <p>Session: {sessionId}</p>
      <p>Permissions: {permissions.join(', ')}</p>
      
      <div style={{ marginBottom: '20px' }}>
        <h3>📊 Market Data</h3>
        {Object.entries(marketData).map(([symbol, data]) => (
          <div key={symbol} style={{ margin: '10px 0' }}>
            <strong>{symbol}:</strong> ${data.price.toFixed(2)}
            <button 
              onClick={() => handleQuickBuy(symbol, 0.1)}
              disabled={tradingLoading}
              style={{ marginLeft: '10px', padding: '5px 10px' }}
            >
              {tradingLoading ? 'Placing...' : 'Quick Buy 0.1'}
            </button>
          </div>
        ))}
      </div>
      
      {tradingError && (
        <div style={{ color: 'red' }}>Trading Error: {tradingError.message}</div>
      )}
    </div>
  );
}

// ============================================
// Example 2: Context Provider Usage
// ============================================

/**
 * App using the context provider for global state management
 * This is the recommended pattern for larger applications
 */
export function ContextProviderApp() {
  return (
    <SimpleMiniAppProvider
      config={{ 
        appId: 'context-trader',
        debug: true 
      }}
      autoConnect={true}
      autoRequestPermissions={true}
      defaultMarketSubscriptions={['ETH', 'BTC', 'SOL']}
    >
      <TradingDashboard />
    </SimpleMiniAppProvider>
  );
}

function TradingDashboard() {
  const { 
    connected, 
    sessionId, 
    marketData, 
    placeOrder, 
    hasPermission, 
    loading,
    error 
  } = useSimpleMiniAppContext();

  if (!connected) {
    return <div>Connecting...</div>;
  }

  return (
    <div style={{ padding: '20px' }}>
      <h2>🏗️ Trading Dashboard</h2>
      <p>Session: {sessionId}</p>
      
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
        <MarketDataPanel />
        <TradingPanel />
      </div>
      
      <AccountPanel />
      
      {error && (
        <div style={{ color: 'red', marginTop: '20px' }}>
          Error: {error.message}
        </div>
      )}
    </div>
  );
}

function MarketDataPanel() {
  const { marketData } = useSimpleMiniAppContext();

  return (
    <div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '8px' }}>
      <h3>📊 Market Data</h3>
      {Object.entries(marketData).map(([symbol, data]) => (
        <div key={symbol} style={{ 
          display: 'flex', 
          justifyContent: 'space-between',
          padding: '8px 0',
          borderBottom: '1px solid #eee'
        }}>
          <span><strong>{symbol}</strong></span>
          <span>${data.price.toFixed(2)}</span>
          {data.changePercent24h !== undefined && (
            <span style={{ 
              color: data.changePercent24h >= 0 ? 'green' : 'red' 
            }}>
              {data.changePercent24h >= 0 ? '+' : ''}{data.changePercent24h.toFixed(2)}%
            </span>
          )}
        </div>
      ))}
    </div>
  );
}

function TradingPanel() {
  const { placeOrder, loading, hasPermission } = useSimpleMiniAppContext();
  const [orderForm, setOrderForm] = useState<Partial<SimpleOrderData>>({
    symbol: 'ETH',
    side: 'buy',
    orderType: 'market',
    size: 0.1
  });

  const canTrade = hasPermission('place_orders');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!placeOrder || !orderForm.symbol || !orderForm.side || !orderForm.orderType || !orderForm.size) {
      return;
    }

    try {
      await placeOrder({
        symbol: orderForm.symbol,
        side: orderForm.side,
        orderType: orderForm.orderType,
        size: orderForm.size,
        price: orderForm.price
      });
      alert('Order placed successfully!');
    } catch (err: any) {
      alert(`Order failed: ${err.message}`);
    }
  };

  return (
    <div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '8px' }}>
      <h3>📈 Place Order</h3>
      
      {!canTrade && (
        <div style={{ color: 'orange', marginBottom: '10px' }}>
          ⚠️ Trading permissions required
        </div>
      )}
      
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '10px' }}>
          <label>Symbol:</label>
          <select 
            value={orderForm.symbol || ''} 
            onChange={(e) => setOrderForm(prev => ({ ...prev, symbol: e.target.value }))}
            style={{ marginLeft: '10px', padding: '5px' }}
          >
            <option value="ETH">ETH</option>
            <option value="BTC">BTC</option>
            <option value="SOL">SOL</option>
          </select>
        </div>
        
        <div style={{ marginBottom: '10px' }}>
          <label>Side:</label>
          <select 
            value={orderForm.side || ''} 
            onChange={(e) => setOrderForm(prev => ({ ...prev, side: e.target.value as 'buy' | 'sell' }))}
            style={{ marginLeft: '10px', padding: '5px' }}
          >
            <option value="buy">Buy</option>
            <option value="sell">Sell</option>
          </select>
        </div>
        
        <div style={{ marginBottom: '10px' }}>
          <label>Type:</label>
          <select 
            value={orderForm.orderType || ''} 
            onChange={(e) => setOrderForm(prev => ({ ...prev, orderType: e.target.value as 'market' | 'limit' }))}
            style={{ marginLeft: '10px', padding: '5px' }}
          >
            <option value="market">Market</option>
            <option value="limit">Limit</option>
          </select>
        </div>
        
        <div style={{ marginBottom: '10px' }}>
          <label>Size:</label>
          <input 
            type="number" 
            step="0.001"
            value={orderForm.size || ''} 
            onChange={(e) => setOrderForm(prev => ({ ...prev, size: parseFloat(e.target.value) }))}
            style={{ marginLeft: '10px', padding: '5px' }}
          />
        </div>
        
        {orderForm.orderType === 'limit' && (
          <div style={{ marginBottom: '10px' }}>
            <label>Price:</label>
            <input 
              type="number" 
              step="0.01"
              value={orderForm.price || ''} 
              onChange={(e) => setOrderForm(prev => ({ ...prev, price: parseFloat(e.target.value) }))}
              style={{ marginLeft: '10px', padding: '5px' }}
            />
          </div>
        )}
        
        <button 
          type="submit" 
          disabled={loading || !canTrade}
          style={{ 
            padding: '10px 20px', 
            backgroundColor: canTrade ? '#007bff' : '#ccc',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: canTrade ? 'pointer' : 'not-allowed'
          }}
        >
          {loading ? 'Placing Order...' : 'Place Order'}
        </button>
      </form>
    </div>
  );
}

function AccountPanel() {
  const { positions, balances, refreshPositions, refreshBalances } = useSimpleAccount();
  
  useEffect(() => {
    refreshPositions();
    refreshBalances();
  }, [refreshPositions, refreshBalances]);

  return (
    <div style={{ marginTop: '20px' }}>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
        <div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '8px' }}>
          <h3>📋 Positions</h3>
          {positions.length === 0 ? (
            <p>No open positions</p>
          ) : (
            positions.map(position => (
              <div key={position.symbol} style={{ 
                padding: '8px 0',
                borderBottom: '1px solid #eee'
              }}>
                <div><strong>{position.symbol}</strong></div>
                <div>Size: {position.size}</div>
                <div>Entry: ${position.entryPrice.toFixed(2)}</div>
                <div style={{ 
                  color: position.pnl >= 0 ? 'green' : 'red' 
                }}>
                  PnL: ${position.pnl.toFixed(2)} ({position.pnlPercent.toFixed(2)}%)
                </div>
              </div>
            ))
          )}
          <button 
            onClick={refreshPositions}
            style={{ marginTop: '10px', padding: '5px 10px' }}
          >
            Refresh
          </button>
        </div>
        
        <div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '8px' }}>
          <h3>💰 Balances</h3>
          {Object.keys(balances).length === 0 ? (
            <p>No balances available</p>
          ) : (
            Object.entries(balances).map(([asset, balance]) => (
              <div key={asset} style={{ 
                padding: '8px 0',
                borderBottom: '1px solid #eee'
              }}>
                <div><strong>{asset}</strong></div>
                <div>Free: {balance.free.toFixed(4)}</div>
                <div>Locked: {balance.locked.toFixed(4)}</div>
                <div>USD Value: ${balance.usdValue.toFixed(2)}</div>
              </div>
            ))
          )}
          <button 
            onClick={() => refreshBalances()}
            style={{ marginTop: '10px', padding: '5px 10px' }}
          >
            Refresh
          </button>
        </div>
      </div>
    </div>
  );
}

// ============================================
// Example 3: Permission Gate Component
// ============================================

/**
 * Component that manages permissions and gates access to features
 */
interface PermissionGateProps {
  children: React.ReactNode;
  requiredPermissions: SimplePermission[];
  fallback?: React.ReactNode;
}

export function PermissionGate({ 
  children, 
  requiredPermissions, 
  fallback 
}: PermissionGateProps) {
  const { hasPermission, requestPermission } = useSimplePermissionsContext();
  const { loading } = useSimpleMiniAppContext();
  
  const missingPermissions = requiredPermissions.filter(permission => !hasPermission(permission));
  
  if (missingPermissions.length === 0) {
    return <>{children}</>;
  }
  
  const handleRequestPermissions = async () => {
    for (const permission of missingPermissions) {
      await requestPermission(permission);
    }
  };
  
  if (fallback) {
    return <>{fallback}</>;
  }
  
  return (
    <div style={{ 
      padding: '20px', 
      border: '2px dashed #ffa500', 
      borderRadius: '8px',
      textAlign: 'center'
    }}>
      <h4>🔐 Permissions Required</h4>
      <p>This feature requires the following permissions:</p>
      <ul style={{ textAlign: 'left', display: 'inline-block' }}>
        {missingPermissions.map(permission => (
          <li key={permission}>{permission.replace(/_/g, ' ')}</li>
        ))}
      </ul>
      <button 
        onClick={handleRequestPermissions}
        disabled={loading}
        style={{ 
          padding: '10px 20px',
          backgroundColor: '#ffa500',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer'
        }}
      >
        {loading ? 'Requesting...' : 'Grant Permissions'}
      </button>
    </div>
  );
}

// ============================================
// Example 4: Connection Status Component
// ============================================

/**
 * Component showing connection status with reconnect functionality
 */
export function ConnectionStatus() {
  const config = { appId: 'connection-status-demo', debug: true };
  const context = useSimpleMiniApp(config);
  const { client, connected, sessionId, error } = context;
  
  // Use the useSimpleConnection hook for connection management
  const { connecting, connect, disconnect } = useSimpleConnection(client);
  
  return (
    <div style={{ 
      padding: '10px', 
      backgroundColor: connected ? '#d4edda' : '#f8d7da',
      border: `1px solid ${connected ? '#c3e6cb' : '#f5c6cb'}`,
      borderRadius: '4px',
      marginBottom: '20px'
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <div>
          <strong>Status:</strong> {
            connected ? `Connected (${sessionId})` : 
            connecting ? 'Connecting...' : 
            'Disconnected'
          }
        </div>
        <div>
          {!connected && !connecting && (
            <button onClick={connect} style={{ padding: '5px 10px' }}>
              Connect
            </button>
          )}
          {connected && (
            <button onClick={disconnect} style={{ padding: '5px 10px' }}>
              Disconnect
            </button>
          )}
        </div>
      </div>
      {error && (
        <div style={{ color: 'red', marginTop: '5px' }}>
          Error: {error.message}
        </div>
      )}
    </div>
  );
}

// ============================================
// Example 5: Complete Mini-App
// ============================================

/**
 * Complete mini-app example showing all patterns together
 */
export function CompleteMiniApp() {
  return (
    <SimpleMiniAppProvider
      config={{ 
        appId: 'complete-trader',
        debug: true 
      }}
      autoConnect={true}
      autoRequestPermissions={true}
      defaultMarketSubscriptions={['ETH', 'BTC', 'SOL']}
    >
      <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
        <h1>🚀 Complete Trading Mini-App</h1>
        
        <ConnectionStatus />
        
        <PermissionGate requiredPermissions={['read_market_data']}>
          <MarketDataPanel />
        </PermissionGate>
        
        <PermissionGate 
          requiredPermissions={['place_orders', 'cancel_orders']}
          fallback={
            <div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
              Trading features require permissions
            </div>
          }
        >
          <TradingPanel />
        </PermissionGate>
        
        <PermissionGate requiredPermissions={['read_positions', 'read_balance']}>
          <AccountPanel />
        </PermissionGate>
      </div>
    </SimpleMiniAppProvider>
  );
}

Last updated