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.
Installation
npx bigblocks add tap-button
Import
import { TapButton } from 'bigblocks';
Props
Prop | Type | Required | Default | Description |
---|---|---|---|---|
onSuccess | (result: TapResult) => void | No | - | Success callback with tap details |
onError | (error: Error) => void | No | - | Error callback |
buttonText | string | No | 'Tap Droplit' | Button label text |
variant | 'solid' | 'soft' | 'outline' | 'ghost' | No | 'solid' | Button style variant |
size | '1' | '2' | '3' | '4' | No | '2' | Button size |
disabled | boolean | No | false | Disable button |
className | string | No | - | Additional CSS classes |
showTapCount | boolean | No | true | Display 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
- Use for High-Frequency Interactions: Ideal for engagement tracking without blockchain costs
- Implement Rate Limiting: Prevent abuse with client and server-side limits
- Add Visual Feedback: Show users their progress with counters and achievements
- Consider Hybrid Models: Combine with BSV transactions for premium features
- Track Analytics: Monitor engagement patterns for optimization
- 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
Related Components
- DataPushButton - Send data to Droplit
- QuickDonateButton - BSV-based donations
- LoadingButton - Generic loading button
- AuthButton - Bitcoin authentication
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>
</>
);
}