BigBlocks Docs
Components/Developer Tools

TapButton

A Droplit-specific button component for data transactions without BSV fees, demonstrating multi-tenant wallet interactions

A specialized button component designed for Droplit multi-tenant wallet platform, enabling data transactions without BSV costs. Perfect for applications requiring frequent micro-interactions, loyalty programs, or engagement tracking without blockchain fees.

View Component Preview →

Installation

npx bigblocks add tap-button

Import

import { TapButton } from 'bigblocks';

Props

PropTypeRequiredDefaultDescription
onSuccess(result: TapResult) => voidNo-Success callback with tap details
onError(error: Error) => voidNo-Error callback
buttonTextstringNo'Tap Droplit'Button label text
variant'solid' | 'soft' | 'outline' | 'ghost'No'solid'Button style variant
size'1' | '2' | '3' | '4'No'2'Button size
disabledbooleanNofalseDisable button
classNamestringNo-Additional CSS classes
showTapCountbooleanNotrueDisplay tap counter
color'blue' | 'green' | 'orange' | 'red' | 'purple' | 'gray'No'blue'Button color theme

TapResult Interface

interface TapResult {
  message: string;      // Success message with tap count
  timestamp: string;    // ISO timestamp of the tap
  tapCount: number;     // Total number of taps in session
}

Basic Usage

import { TapButton } from 'bigblocks';

export default function BasicExample() {
  const handleTapSuccess = (result) => {
    console.log('Tap successful:', result);
    // result: { message: "Droplit access granted! Tap #1", timestamp: "2024-01-20T...", tapCount: 1 }
  };

  return (
    <TapButton onSuccess={handleTapSuccess} />
  );
}

Advanced Usage

Complete Integration Example

import { TapButton } from 'bigblocks';
import { useState } from 'react';

export default function DroplitIntegration() {
  const [totalTaps, setTotalTaps] = useState(0);
  const [lastTapTime, setLastTapTime] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [rewards, setRewards] = useState(0);

  const handleTapSuccess = async (result: TapResult) => {
    console.log('Tap recorded:', result);
    
    // Update local state
    setTotalTaps(result.tapCount);
    setLastTapTime(result.timestamp);
    setError(null);
    
    // Calculate rewards (e.g., 1 point per tap)
    const newRewards = result.tapCount;
    setRewards(newRewards);
    
    // Send to analytics
    await trackEngagement({
      action: 'droplit_tap',
      count: result.tapCount,
      timestamp: result.timestamp
    });
    
    // Check for milestones
    if (result.tapCount % 10 === 0) {
      await unlockAchievement(`tap_milestone_${result.tapCount}`);
      alert(`Milestone reached: ${result.tapCount} taps!`);
    }
  };

  const handleTapError = (error: Error) => {
    console.error('Tap failed:', error);
    setError(error.message);
  };

  return (
    <div className="droplit-integration">
      <h2>Droplit Engagement</h2>
      
      <TapButton
        onSuccess={handleTapSuccess}
        onError={handleTapError}
        buttonText="Earn Points"
        color="green"
        size="3"
      />
      
      <div className="stats">
        <p>Total Taps: {totalTaps}</p>
        <p>Reward Points: {rewards}</p>
        {lastTapTime && (
          <p>Last Tap: {new Date(lastTapTime).toLocaleTimeString()}</p>
        )}
      </div>
      
      {error && (
        <div className="error-message">
          Error: {error}
        </div>
      )}
    </div>
  );
}

Multi-Tenant Application

import { TapButton } from 'bigblocks';
import { useTenant } from '@/hooks/useTenant';

export default function TenantSpecificTap() {
  const { tenant, tenantConfig } = useTenant();
  const [interactions, setInteractions] = useState<TapResult[]>([]);

  const handleTenantTap = async (result: TapResult) => {
    // Record tenant-specific interaction
    const tenantInteraction = {
      ...result,
      tenantId: tenant.id,
      tenantName: tenant.name,
      customData: tenantConfig.tapData
    };
    
    // Store in tenant's data space
    await fetch(`/api/tenants/${tenant.id}/interactions`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(tenantInteraction)
    });
    
    setInteractions(prev => [...prev, result]);
    
    // Apply tenant-specific logic
    if (tenantConfig.rewardEnabled) {
      await processReward(tenant.id, result.tapCount);
    }
  };

  return (
    <div className="tenant-tap">
      <h3>{tenant.name} Engagement</h3>
      
      <TapButton
        onSuccess={handleTenantTap}
        buttonText={tenantConfig.buttonText || 'Tap to Engage'}
        color={tenantConfig.brandColor || 'blue'}
        showTapCount={tenantConfig.showCounter !== false}
      />
      
      {tenantConfig.showHistory && (
        <div className="interaction-history">
          {interactions.map((tap, i) => (
            <div key={i}>
              Tap #{tap.tapCount} at {new Date(tap.timestamp).toLocaleTimeString()}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Common Patterns

Gamification System

import { TapButton } from 'bigblocks';
import { useState, useEffect } from 'react';

export default function GamificationExample() {
  const [level, setLevel] = useState(1);
  const [experience, setExperience] = useState(0);
  const [achievements, setAchievements] = useState<string[]>([]);
  const [streak, setStreak] = useState(0);
  const [lastTapDate, setLastTapDate] = useState<string | null>(null);

  const calculateLevel = (totalTaps: number) => Math.floor(Math.sqrt(totalTaps / 10)) + 1;
  const calculateXP = (tapCount: number) => tapCount * 10;

  const handleGameTap = (result: TapResult) => {
    // Update experience
    const newXP = calculateXP(result.tapCount);
    setExperience(newXP);
    
    // Check level up
    const newLevel = calculateLevel(result.tapCount);
    if (newLevel > level) {
      setLevel(newLevel);
      alert(`Level Up! You're now level ${newLevel}!`);
      unlockAchievement('level_up');
    }
    
    // Check daily streak
    const today = new Date().toDateString();
    const lastTap = lastTapDate ? new Date(lastTapDate).toDateString() : null;
    
    if (lastTap !== today) {
      if (lastTap === new Date(Date.now() - 86400000).toDateString()) {
        setStreak(streak + 1);
        if (streak + 1 >= 7) {
          unlockAchievement('week_streak');
        }
      } else {
        setStreak(1);
      }
      setLastTapDate(new Date().toISOString());
    }
    
    // Special achievements
    if (result.tapCount === 100) unlockAchievement('century');
    if (result.tapCount === 1000) unlockAchievement('millennium');
  };

  const unlockAchievement = (achievement: string) => {
    if (!achievements.includes(achievement)) {
      setAchievements([...achievements, achievement]);
      console.log(`🏆 Achievement Unlocked: ${achievement}`);
    }
  };

  return (
    <div className="gamification-system">
      <div className="player-stats">
        <h3>Level {level}</h3>
        <div className="xp-bar">
          <div 
            className="xp-fill" 
            style={{ width: `${(experience % 100)}%` }}
          />
        </div>
        <p>{experience} XP | {streak} day streak</p>
      </div>
      
      <TapButton
        onSuccess={handleGameTap}
        buttonText="Tap for XP"
        color="purple"
        size="3"
      />
      
      {achievements.length > 0 && (
        <div className="achievements">
          <h4>Achievements ({achievements.length})</h4>
          {achievements.map(a => (
            <span key={a} className="achievement-badge">🏆 {a}</span>
          ))}
        </div>
      )}
    </div>
  );
}

Loyalty Program Integration

import { TapButton } from 'bigblocks';
import { useLoyaltyProgram } from '@/hooks/useLoyaltyProgram';

export default function LoyaltyTapButton() {
  const { 
    points, 
    tier, 
    addPoints, 
    checkTierUpgrade 
  } = useLoyaltyProgram();

  const POINTS_PER_TAP = 5;
  const BONUS_MULTIPLIER = {
    bronze: 1,
    silver: 1.5,
    gold: 2,
    platinum: 3
  };

  const handleLoyaltyTap = async (result: TapResult) => {
    // Calculate points with tier bonus
    const basePoints = POINTS_PER_TAP;
    const multiplier = BONUS_MULTIPLIER[tier] || 1;
    const earnedPoints = Math.floor(basePoints * multiplier);
    
    // Add points to account
    await addPoints(earnedPoints, {
      source: 'droplit_tap',
      tapCount: result.tapCount,
      timestamp: result.timestamp
    });
    
    // Check for tier upgrade
    const newTier = await checkTierUpgrade();
    if (newTier && newTier !== tier) {
      alert(`Congratulations! You've reached ${newTier} tier!`);
    }
    
    // Log for analytics
    console.log(`Earned ${earnedPoints} points (${tier} tier)`);
  };

  return (
    <div className="loyalty-tap">
      <div className="loyalty-status">
        <h4>{tier.toUpperCase()} Member</h4>
        <p>{points.toLocaleString()} points</p>
      </div>
      
      <TapButton
        onSuccess={handleLoyaltyTap}
        buttonText={`Tap for ${POINTS_PER_TAP * BONUS_MULTIPLIER[tier]} pts`}
        color={tier === 'platinum' ? 'purple' : tier === 'gold' ? 'orange' : 'blue'}
        size="3"
      />
      
      <p className="bonus-info">
        {tier} tier: {BONUS_MULTIPLIER[tier]}x points per tap
      </p>
    </div>
  );
}

A/B Testing Variant

import { TapButton } from 'bigblocks';
import { useABTest } from '@/hooks/useABTest';

export default function ABTestTapButton() {
  const { variant, trackConversion } = useABTest('tap-button-experiment');
  const [conversions, setConversions] = useState(0);

  // Different configurations for A/B test
  const variants = {
    control: {
      text: 'Tap Droplit',
      color: 'blue' as const,
      size: '2' as const
    },
    variantA: {
      text: 'Earn Rewards Now',
      color: 'green' as const,
      size: '3' as const
    },
    variantB: {
      text: '💧 Tap for Points',
      color: 'orange' as const,
      size: '3' as const
    }
  };

  const config = variants[variant] || variants.control;

  const handleABTap = async (result: TapResult) => {
    // Track conversion
    await trackConversion({
      variant,
      tapCount: result.tapCount,
      timestamp: result.timestamp
    });
    
    setConversions(prev => prev + 1);
    
    // Additional variant-specific logic
    if (variant === 'variantB' && result.tapCount % 5 === 0) {
      alert('🎉 Bonus points unlocked!');
    }
  };

  return (
    <div className="ab-test-container">
      <p>Test Variant: {variant}</p>
      
      <TapButton
        onSuccess={handleABTap}
        buttonText={config.text}
        color={config.color}
        size={config.size}
        variant="solid"
      />
      
      <p>Conversions: {conversions}</p>
    </div>
  );
}

Rate-Limited Tapping

import { TapButton } from 'bigblocks';
import { useState, useEffect } from 'react';

export default function RateLimitedTap() {
  const [remainingTaps, setRemainingTaps] = useState(10);
  const [resetTime, setResetTime] = useState<Date | null>(null);
  const [isRateLimited, setIsRateLimited] = useState(false);

  const TAP_LIMIT = 10;
  const RESET_INTERVAL = 60 * 60 * 1000; // 1 hour

  useEffect(() => {
    // Check stored rate limit data
    const stored = localStorage.getItem('tapRateLimit');
    if (stored) {
      const { count, resetAt } = JSON.parse(stored);
      const now = new Date();
      const reset = new Date(resetAt);
      
      if (now < reset) {
        setRemainingTaps(TAP_LIMIT - count);
        setResetTime(reset);
        setIsRateLimited(count >= TAP_LIMIT);
      } else {
        // Reset period has passed
        localStorage.removeItem('tapRateLimit');
        setRemainingTaps(TAP_LIMIT);
      }
    }
  }, []);

  const handleRateLimitedTap = (result: TapResult) => {
    const newRemaining = Math.max(0, remainingTaps - 1);
    setRemainingTaps(newRemaining);
    
    if (newRemaining === 0) {
      setIsRateLimited(true);
      const reset = new Date(Date.now() + RESET_INTERVAL);
      setResetTime(reset);
      
      localStorage.setItem('tapRateLimit', JSON.stringify({
        count: TAP_LIMIT,
        resetAt: reset.toISOString()
      }));
    } else {
      const stored = localStorage.getItem('tapRateLimit');
      const data = stored ? JSON.parse(stored) : { count: 0, resetAt: null };
      
      localStorage.setItem('tapRateLimit', JSON.stringify({
        count: data.count + 1,
        resetAt: data.resetAt || new Date(Date.now() + RESET_INTERVAL).toISOString()
      }));
    }
    
    console.log(`Tap recorded. ${newRemaining} taps remaining.`);
  };

  const formatTimeRemaining = () => {
    if (!resetTime) return '';
    const now = new Date();
    const diff = resetTime.getTime() - now.getTime();
    const minutes = Math.floor(diff / 60000);
    const seconds = Math.floor((diff % 60000) / 1000);
    return `${minutes}:${seconds.toString().padStart(2, '0')}`;
  };

  return (
    <div className="rate-limited-tap">
      <h3>Limited Engagement</h3>
      
      <TapButton
        onSuccess={handleRateLimitedTap}
        buttonText={isRateLimited ? 'Rate Limited' : `Tap (${remainingTaps} left)`}
        disabled={isRateLimited}
        color={isRateLimited ? 'red' : 'blue'}
      />
      
      {isRateLimited && resetTime && (
        <p className="rate-limit-message">
          Reset in: {formatTimeRemaining()}
        </p>
      )}
      
      <div className="tap-meter">
        <div 
          className="tap-meter-fill"
          style={{ width: `${(remainingTaps / TAP_LIMIT) * 100}%` }}
        />
      </div>
    </div>
  );
}

Authentication Requirements

While TapButton is designed for Droplit's feeless interactions, it can be integrated with Bitcoin authentication for hybrid experiences:

import { 
  BitcoinAuthProvider, 
  BitcoinQueryProvider,
  TapButton,
  useBitcoinAuth
} from 'bigblocks';

function AuthenticatedTap() {
  const { isAuthenticated, address } = useBitcoinAuth();
  const [hybridMode, setHybridMode] = useState(false);

  const handleAuthenticatedTap = async (result: TapResult) => {
    if (isAuthenticated) {
      // Link tap to Bitcoin identity
      await linkTapToIdentity(address, result);
      
      // Enable hybrid features after certain threshold
      if (result.tapCount >= 50 && !hybridMode) {
        setHybridMode(true);
        alert('Hybrid mode unlocked! You can now use BSV features.');
      }
    }
    
    console.log('Tap recorded for:', address || 'anonymous');
  };

  return (
    <div>
      <TapButton
        onSuccess={handleAuthenticatedTap}
        buttonText={isAuthenticated ? 'Authenticated Tap' : 'Anonymous Tap'}
        color={isAuthenticated ? 'green' : 'gray'}
      />
      
      {hybridMode && (
        <p>✨ BSV features now available!</p>
      )}
    </div>
  );
}

function App() {
  return (
    <BitcoinQueryProvider>
      <BitcoinAuthProvider config={{ apiUrl: '/api' }}>
        <AuthenticatedTap />
      </BitcoinAuthProvider>
    </BitcoinQueryProvider>
  );
}

API Integration

TapButton works with Droplit's multi-tenant architecture:

Droplit Tap Endpoints

// Record tap interaction
POST /api/droplit/tap
{
  tenantId: string;
  userId?: string;
  tapCount: number;
  timestamp: string;
  metadata?: Record<string, any>;
}

Response:
{
  success: boolean;
  interaction: {
    id: string;
    tenantId: string;
    tapCount: number;
    rewards?: number;
    achievements?: string[];
  }
}

// Get tap statistics
GET /api/droplit/stats/:userId

Response:
{
  totalTaps: number;
  dailyTaps: number;
  streak: number;
  rewards: number;
  level: number;
  achievements: string[];
}

Implementation Example

// Backend API handler
app.post('/api/droplit/tap', async (req, res) => {
  const { tenantId, userId, tapCount, timestamp } = req.body;
  
  // Multi-tenant data isolation
  const tenant = await getTenant(tenantId);
  const storage = tenant.getStorage();
  
  // Record interaction without blockchain
  const interaction = await storage.recordInteraction({
    userId,
    type: 'tap',
    count: tapCount,
    timestamp,
    cost: 0 // No BSV cost
  });
  
  // Calculate rewards based on tenant config
  const rewards = tenant.config.rewardPerTap * tapCount;
  if (rewards > 0) {
    await storage.addRewards(userId, rewards);
  }
  
  // Check achievements
  const achievements = await checkAchievements(userId, tapCount);
  
  res.json({
    success: true,
    interaction: {
      id: interaction.id,
      tenantId,
      tapCount,
      rewards,
      achievements
    }
  });
});

Features

  • Feeless Interactions: Data transactions without BSV costs
  • Multi-Tenant Support: Works with Droplit's tenant isolation
  • Engagement Tracking: Built-in tap counting and analytics
  • Visual Feedback: Loading states and success indicators
  • Customizable Appearance: Multiple colors, sizes, and variants
  • Session Persistence: Tap count maintained during session
  • Achievement System: Easy integration with gamification
  • Rate Limiting: Built-in support for interaction limits

Error Handling

Common Error Scenarios

import { TapButton } from 'bigblocks';

export default function ErrorHandlingExample() {
  const [errorLog, setErrorLog] = useState<string[]>([]);

  const handleTapError = (error: Error) => {
    const timestamp = new Date().toISOString();
    const errorEntry = `[${timestamp}] ${error.message}`;
    setErrorLog(prev => [...prev, errorEntry]);
    
    // Handle specific error types
    if (error.message.includes('rate limit')) {
      alert('Too many taps! Please wait before trying again.');
    } else if (error.message.includes('network')) {
      alert('Connection issue. Please check your internet.');
    } else if (error.message.includes('tenant')) {
      alert('Service configuration error. Contact support.');
    } else {
      alert('An unexpected error occurred. Please try again.');
    }
  };

  return (
    <div>
      <TapButton
        onError={handleTapError}
        onSuccess={(result) => console.log('Success:', result)}
      />
      
      {errorLog.length > 0 && (
        <div className="error-log">
          <h4>Error Log</h4>
          {errorLog.map((error, i) => (
            <div key={i} className="error-entry">{error}</div>
          ))}
        </div>
      )}
    </div>
  );
}

Styling

TapButton uses Radix Themes styling system:

// Color variants
<TapButton color="blue" />    // Default
<TapButton color="green" />   // Success/rewards
<TapButton color="orange" />  // Warning/special
<TapButton color="red" />     // Error/limit
<TapButton color="purple" />  // Premium/special
<TapButton color="gray" />    // Disabled/inactive

// Size variants
<TapButton size="1" />  // Extra small
<TapButton size="2" />  // Default
<TapButton size="3" />  // Large
<TapButton size="4" />  // Extra large

// Style variants
<TapButton variant="solid" />   // Default filled
<TapButton variant="soft" />    // Subtle background
<TapButton variant="outline" /> // Border only
<TapButton variant="ghost" />   // Minimal style

// Custom styling
<TapButton
  className="custom-tap-button"
  style={{ minWidth: '200px' }}
/>

Performance Optimization

Debounced Tapping

import { TapButton } from 'bigblocks';
import { useCallback } from 'react';
import { debounce } from 'lodash';

export default function DebouncedTap() {
  const processTap = useCallback(
    debounce((result: TapResult) => {
      // Process tap after debounce period
      console.log('Processing tap:', result);
      analyticsTrack('tap', result);
    }, 500),
    []
  );

  return (
    <TapButton
      onSuccess={(result) => {
        // Immediate UI feedback
        console.log('Tap registered');
        // Debounced processing
        processTap(result);
      }}
    />
  );
}

Best Practices

  1. Use for High-Frequency Interactions: Ideal for engagement tracking without blockchain costs
  2. Implement Rate Limiting: Prevent abuse with client and server-side limits
  3. Add Visual Feedback: Show users their progress with counters and achievements
  4. Consider Hybrid Models: Combine with BSV transactions for premium features
  5. Track Analytics: Monitor engagement patterns for optimization
  6. Handle Offline State: Store taps locally and sync when online

Troubleshooting

Tap count not persisting

  • Tap count is session-based by default
  • Implement localStorage or API persistence for cross-session tracking

Button not responding

  • Check if component is disabled
  • Verify onSuccess callback is defined
  • Ensure no JavaScript errors in console

Custom styling not applying

  • TapButton uses Radix Themes
  • Ensure styles don't conflict with Radix classes
  • Use className prop for additional styles

Rate limiting issues

  • Clear localStorage for testing
  • Verify server-side rate limit configuration
  • Check timezone differences for reset times

API Reference

TapButton Props

interface TapButtonProps {
  onSuccess?: (result: TapResult) => void;
  onError?: (error: Error) => void;
  buttonText?: string;
  variant?: 'solid' | 'soft' | 'outline' | 'ghost';
  size?: '1' | '2' | '3' | '4';
  disabled?: boolean;
  className?: string;
  showTapCount?: boolean;
  color?: 'blue' | 'green' | 'orange' | 'red' | 'purple' | 'gray';
}

interface TapResult {
  message: string;
  timestamp: string;
  tapCount: number;
}

Usage with Custom Hooks

// Custom hook for tap management
function useTapManager() {
  const [taps, setTaps] = useState<TapResult[]>([]);
  const [stats, setStats] = useState({ total: 0, today: 0, streak: 0 });
  
  const recordTap = useCallback((result: TapResult) => {
    setTaps(prev => [...prev, result]);
    updateStats(result);
  }, []);
  
  return { taps, stats, recordTap };
}

// Usage
function TapManager() {
  const { taps, stats, recordTap } = useTapManager();
  
  return (
    <>
      <TapButton onSuccess={recordTap} />
      <div>Total: {stats.total} | Today: {stats.today}</div>
    </>
  );
}