Components/Wallet
QuickDonateButton
A streamlined donation button for accepting Bitcoin SV donations with preset amounts and one-click processing
A streamlined donation button component that provides a frictionless experience for accepting Bitcoin SV donations with smart preset amounts, real-time validation, and comprehensive transaction tracking.
Installation
npx bigblocks add quick-donate-buttonImport
import { QuickDonateButton } from 'bigblocks';Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| recipientAddress | string | Yes | - | Bitcoin address to receive donations |
| amounts | number[] | No | [0.001, 0.005, 0.01] | Preset BSV amounts for quick selection |
| defaultAmount | number | No | First amount | Default selected amount |
| onSuccess | (txid: string) => void | No | - | Callback when donation succeeds |
| onError | (error: QuickDonationError) => void | No | - | Callback when donation fails |
| label | string | No | 'Donate' | Button label text |
| compact | boolean | No | true | Use compact display mode |
| className | string | No | - | Additional CSS classes |
QuickDonationError Interface
interface QuickDonationError {
code:
| 'INSUFFICIENT_FUNDS' // Not enough BSV for donation
| 'INVALID_ADDRESS' // Malformed recipient address
| 'AMOUNT_TOO_SMALL' // Below minimum donation amount
| 'AMOUNT_TOO_LARGE' // Above maximum transaction limit
| 'DAILY_LIMIT_EXCEEDED' // Daily donation limit reached
| 'RATE_LIMITED' // Too many requests
| 'RECIPIENT_BLOCKED'; // Recipient not allowed
message: string;
details?: {
minimumAmount?: number; // Minimum required amount
maximumAmount?: number; // Maximum allowed amount
dailyLimit?: number; // Daily limit amount
dailyUsed?: number; // Amount used today
resetTime?: number; // When limits reset
available?: number; // Available balance
required?: number; // Required amount
};
}Basic Usage
import { QuickDonateButton } from 'bigblocks';
export default function DonationSection() {
return (
<QuickDonateButton
recipientAddress="1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
amounts={[0.001, 0.01, 0.1]}
onSuccess={(txid) => {
console.log('Donation sent! TxID:', txid);
}}
/>
);
}Advanced Usage
Complete Donation Integration
import { QuickDonateButton } from 'bigblocks';
import { useState } from 'react';
export default function AdvancedDonation() {
const [donationHistory, setDonationHistory] = useState<string[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const handleDonationSuccess = (txid: string) => {
setDonationHistory(prev => [txid, ...prev]);
setIsProcessing(false);
// Track donation for analytics
console.log('Donation successful:', txid);
// Show success notification
alert(`Thank you for your donation! Transaction: ${txid.slice(0, 8)}...`);
// Optional: Navigate to success page
// router.push(`/donation/success?txid=${txid}`);
};
const handleDonationError = (error: QuickDonationError) => {
setIsProcessing(false);
switch (error.code) {
case 'INSUFFICIENT_FUNDS':
alert(`Insufficient funds. You need ${error.details?.required} satoshis but only have ${error.details?.available}.`);
break;
case 'DAILY_LIMIT_EXCEEDED':
alert(`Daily donation limit of ${error.details?.dailyLimit} satoshis exceeded. Limit resets at ${new Date(error.details?.resetTime || 0).toLocaleTimeString()}.`);
break;
case 'AMOUNT_TOO_SMALL':
alert(`Minimum donation amount is ${error.details?.minimumAmount} satoshis.`);
break;
default:
alert(`Donation failed: ${error.message}`);
}
};
return (
<div className="donation-section">
<h2>Support Our Project</h2>
<p>Your donations help us continue building amazing Bitcoin applications.</p>
<QuickDonateButton
recipientAddress="1BitcoinDeveloperAddressHere..."
amounts={[0.001, 0.005, 0.01, 0.05, 0.1]}
defaultAmount={0.005}
onSuccess={handleDonationSuccess}
onError={handleDonationError}
label="Support Development"
compact={false}
className="bg-gradient-to-r from-orange-500 to-red-500 text-white font-bold"
/>
{donationHistory.length > 0 && (
<div className="donation-history mt-4">
<h3>Your Recent Donations</h3>
<ul className="text-sm text-gray-600">
{donationHistory.slice(0, 5).map((txid) => (
<li key={txid}>
<a
href={`https://whatsonchain.com/tx/${txid}`}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{txid.slice(0, 8)}...{txid.slice(-8)}
</a>
</li>
))}
</ul>
</div>
)}
</div>
);
}Smart Presets with Dynamic Amounts
import { QuickDonateButton } from 'bigblocks';
import { useEffect, useState } from 'react';
export default function SmartPresetDonation() {
const [smartAmounts, setSmartAmounts] = useState<number[]>([0.001, 0.005, 0.01]);
useEffect(() => {
// Fetch smart presets based on recipient and donor history
const fetchSmartPresets = async () => {
try {
const response = await fetch('/api/wallet/quick-donate/presets?currency=BSV&recipient=1BitcoinAddress...');
const data = await response.json();
if (data.presets) {
const amounts = data.presets.map((preset: any) =>
parseFloat(preset.amountBSV)
);
setSmartAmounts(amounts);
}
} catch (error) {
console.error('Failed to fetch smart presets:', error);
}
};
fetchSmartPresets();
}, []);
return (
<div className="smart-donation">
<h3>Smart Donation Amounts</h3>
<p>Amounts personalized based on your donation history and recipient preferences.</p>
<QuickDonateButton
recipientAddress="1BitcoinRecipientAddress..."
amounts={smartAmounts}
onSuccess={(txid) => {
console.log('Smart donation successful:', txid);
// Update user's donation profile for better future recommendations
}}
className="border-2 border-blue-500 hover:bg-blue-50"
/>
</div>
);
}Charity Campaign Integration
import { QuickDonateButton } from 'bigblocks';
import { useState, useEffect } from 'react';
interface CampaignInfo {
name: string;
goal: number;
raised: number;
description: string;
endDate: string;
}
export default function CharityCampaign() {
const [campaign, setCampaign] = useState<CampaignInfo | null>(null);
const [progress, setProgress] = useState(0);
useEffect(() => {
// Fetch campaign information
const fetchCampaign = async () => {
// This would typically come from your API
setCampaign({
name: "Bitcoin Education Fund",
goal: 10, // 10 BSV
raised: 6.5, // 6.5 BSV raised so far
description: "Help us educate developers about Bitcoin applications",
endDate: "2025-12-31"
});
};
fetchCampaign();
}, []);
useEffect(() => {
if (campaign) {
setProgress((campaign.raised / campaign.goal) * 100);
}
}, [campaign]);
const handleCampaignDonation = (txid: string) => {
console.log('Campaign donation:', txid);
// Update campaign progress (in real app, this would come from backend)
if (campaign) {
setCampaign(prev => prev ? {
...prev,
raised: prev.raised + 0.001 // Assume small donation for demo
} : null);
}
// Send campaign donation tracking
fetch('/api/campaigns/track-donation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
campaignId: 'bitcoin-education-fund',
txid,
timestamp: Date.now()
})
});
};
if (!campaign) return <div>Loading campaign...</div>;
return (
<div className="charity-campaign">
<div className="campaign-info mb-6">
<h2 className="text-2xl font-bold">{campaign.name}</h2>
<p className="text-gray-600 mb-4">{campaign.description}</p>
<div className="progress-section mb-4">
<div className="flex justify-between text-sm mb-2">
<span>{campaign.raised} BSV raised</span>
<span>{campaign.goal} BSV goal</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className="bg-green-600 h-2.5 rounded-full transition-all duration-500"
style={{ width: `${Math.min(progress, 100)}%` }}
></div>
</div>
<p className="text-sm text-gray-500 mt-2">
{Math.round(progress)}% complete • Ends {new Date(campaign.endDate).toLocaleDateString()}
</p>
</div>
</div>
<QuickDonateButton
recipientAddress="1CharityBitcoinAddress..."
amounts={[0.001, 0.01, 0.1, 0.5]}
defaultAmount={0.01}
onSuccess={handleCampaignDonation}
onError={(error) => {
console.error('Campaign donation failed:', error);
alert('Donation failed. Please try again.');
}}
label="Donate to Campaign"
className="w-full bg-green-600 hover:bg-green-700 text-white font-semibold py-3"
/>
</div>
);
}Common Patterns
Multi-Recipient Donation Dashboard
import { QuickDonateButton } from 'bigblocks';
const recipients = [
{
name: "Open Source Developer",
address: "1Developer1Address...",
description: "Building Bitcoin tools",
category: "development",
amounts: [0.001, 0.01, 0.1]
},
{
name: "Bitcoin Charity",
address: "1Charity1Address...",
description: "Helping those in need",
category: "charity",
amounts: [0.01, 0.1, 1.0]
},
{
name: "Content Creator",
address: "1Creator1Address...",
description: "Educational Bitcoin content",
category: "creator",
amounts: [0.001, 0.005, 0.01]
}
];
export default function DonationDashboard() {
const handleDonation = (recipient: string, txid: string) => {
console.log(`Donation to ${recipient}: ${txid}`);
// Track donation analytics
fetch('/api/analytics/donation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient,
txid,
source: 'dashboard',
timestamp: Date.now()
})
});
};
return (
<div className="donation-dashboard">
<h1>Support the Bitcoin Ecosystem</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{recipients.map((recipient) => (
<div key={recipient.address} className="recipient-card p-6 border rounded-lg">
<h3 className="font-bold text-lg">{recipient.name}</h3>
<p className="text-gray-600 mb-2">{recipient.description}</p>
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded mb-4">
{recipient.category}
</span>
<QuickDonateButton
recipientAddress={recipient.address}
amounts={recipient.amounts}
onSuccess={(txid) => handleDonation(recipient.name, txid)}
label="Support"
className="w-full"
/>
</div>
))}
</div>
</div>
);
}Subscription-Style Recurring Donations
import { QuickDonateButton } from 'bigblocks';
import { useState } from 'react';
export default function RecurringDonation() {
const [isRecurring, setIsRecurring] = useState(false);
const [recurringAmount, setRecurringAmount] = useState(0.01);
const handleRecurringDonation = async (txid: string) => {
console.log('Initial recurring donation:', txid);
if (isRecurring) {
// Set up recurring donation schedule
try {
await fetch('/api/wallet/recurring-donations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipientAddress: "1RecurringRecipient...",
amount: recurringAmount * 100000000, // Convert to satoshis
frequency: 'monthly',
startDate: Date.now(),
initialTxid: txid
})
});
alert('Recurring donation set up successfully!');
} catch (error) {
console.error('Failed to set up recurring donation:', error);
}
}
};
return (
<div className="recurring-donation">
<h3>Monthly Support</h3>
<div className="recurring-options mb-4">
<label className="flex items-center">
<input
type="checkbox"
checked={isRecurring}
onChange={(e) => setIsRecurring(e.target.checked)}
className="mr-2"
/>
Make this a monthly recurring donation
</label>
</div>
<QuickDonateButton
recipientAddress="1RecurringRecipient..."
amounts={[0.01, 0.05, 0.1]}
defaultAmount={recurringAmount}
onSuccess={handleRecurringDonation}
label={isRecurring ? "Start Monthly Support" : "One-time Donation"}
className={isRecurring ? "bg-purple-600 hover:bg-purple-700" : ""}
/>
{isRecurring && (
<p className="text-sm text-gray-600 mt-2">
You'll be charged {recurringAmount} BSV monthly. Cancel anytime in settings.
</p>
)}
</div>
);
}Authentication Requirements
The QuickDonateButton requires Bitcoin authentication context for wallet operations:
import {
BitcoinAuthProvider,
BitcoinQueryProvider,
QuickDonateButton
} from 'bigblocks';
function App() {
return (
<BitcoinQueryProvider>
<BitcoinAuthProvider config={{
apiUrl: '/api',
walletMode: 'integrated' // Enables wallet features
}}>
<QuickDonateButton
recipientAddress="1BitcoinAddress..."
/>
</BitcoinAuthProvider>
</BitcoinQueryProvider>
);
}API Integration
The QuickDonateButton integrates with several backend endpoints for comprehensive donation processing:
Core Donation Endpoints
// Send quick donation
POST /api/wallet/quick-donate
{
recipientAddress: string;
amount: number; // satoshis
message?: string;
source?: string;
metadata?: {
page?: string;
context?: string;
campaign?: string;
};
}
// Response includes transaction details and updated balance
{
success: boolean;
txid: string;
quickDonation: {
id: string;
amount: number;
recipient: string;
status: 'pending' | 'confirmed';
// ... full donation details
};
balance: {
remaining: number;
canDonateAgain: boolean;
};
}Smart Presets API
// Get personalized donation amounts
GET /api/wallet/quick-donate/presets?currency=BSV&recipient=<address>
// Returns optimized amounts based on:
// - Recipient category (charity, development, creator)
// - Donor's previous donation history
// - Current BSV market price
// - Donor's wallet balance
{
presets: [
{
amount: number;
amountBSV: string;
amountUSD?: string;
label: string; // "Small", "Medium", "Large"
recommended?: boolean;
}
];
recipient?: {
name?: string;
verified?: boolean;
category?: string;
};
limits: {
minimum: number;
maximum: number;
dailyLimit?: number;
};
}Validation and Security
// Validate donation before processing
POST /api/wallet/quick-donate/validate
{
recipientAddress: string;
amount: number;
}
// Comprehensive validation response
{
valid: boolean;
validation: {
address: {
valid: boolean;
type?: 'p2pkh' | 'p2sh' | 'p2wpkh';
network: 'mainnet' | 'testnet';
};
amount: {
valid: boolean;
withinLimits: boolean;
belowBalance: boolean;
aboveDust: boolean;
};
fees: {
estimated: number;
total: number;
affordable: boolean;
};
};
warnings?: string[];
suggestions?: {
adjustedAmount?: number;
feeOptimization?: string;
};
}Real-time Updates
// Server-Sent Events for donation updates
GET /api/wallet/quick-donate/stream
Events:
- donation_sent: { txid: string, amount: number, recipient: string }
- donation_confirmed: { txid: string, confirmations: number }
- balance_updated: { balance: number, canDonate: boolean }
- daily_limit_reached: { limit: number, resetTime: number }Error Handling
Comprehensive Error Management
import { QuickDonateButton } from 'bigblocks';
export default function RobustDonationHandler() {
const handleDonationError = (error: QuickDonationError) => {
switch (error.code) {
case 'INSUFFICIENT_FUNDS':
// Show user their balance and suggest smaller amount
console.log(`Need ${error.details?.required} satoshis, have ${error.details?.available}`);
showBalanceModal(error.details);
break;
case 'AMOUNT_TOO_SMALL':
// Suggest minimum amount
console.log(`Minimum donation: ${error.details?.minimumAmount} satoshis`);
suggestMinimumAmount(error.details?.minimumAmount);
break;
case 'DAILY_LIMIT_EXCEEDED':
// Show limit info and reset time
const resetTime = new Date(error.details?.resetTime || 0);
console.log(`Daily limit exceeded. Resets at ${resetTime.toLocaleTimeString()}`);
showLimitExceededModal(error.details);
break;
case 'RATE_LIMITED':
// Show retry countdown
console.log('Too many donation attempts. Please wait.');
showRateLimitWarning();
break;
case 'INVALID_ADDRESS':
// Show address validation error
console.log('Invalid recipient address');
showAddressError();
break;
default:
// Generic error handler
console.error('Unknown donation error:', error);
showGenericError(error.message);
}
};
return (
<QuickDonateButton
recipientAddress="1BitcoinAddress..."
amounts={[0.001, 0.01, 0.1]}
onError={handleDonationError}
onSuccess={(txid) => {
console.log('Donation successful:', txid);
showSuccessNotification(txid);
}}
/>
);
}Error Recovery Patterns
import { QuickDonateButton } from 'bigblocks';
import { useState } from 'react';
export default function ErrorRecoveryDonation() {
const [lastError, setLastError] = useState<QuickDonationError | null>(null);
const [suggestedAmount, setSuggestedAmount] = useState<number | null>(null);
const handleError = (error: QuickDonationError) => {
setLastError(error);
// Auto-suggest recovery actions
if (error.code === 'AMOUNT_TOO_SMALL' && error.details?.minimumAmount) {
setSuggestedAmount(error.details.minimumAmount / 100000000); // Convert to BSV
} else if (error.code === 'INSUFFICIENT_FUNDS' && error.details?.available) {
// Suggest 90% of available balance to account for fees
const maxSafe = (error.details.available * 0.9) / 100000000;
setSuggestedAmount(maxSafe);
}
};
const retryWithSuggestedAmount = () => {
setLastError(null);
setSuggestedAmount(null);
// Component will re-render with new suggested amount
};
return (
<div className="error-recovery-donation">
<QuickDonateButton
recipientAddress="1BitcoinAddress..."
amounts={suggestedAmount ? [suggestedAmount] : [0.001, 0.01, 0.1]}
onError={handleError}
onSuccess={() => {
setLastError(null);
setSuggestedAmount(null);
}}
/>
{lastError && (
<div className="error-recovery mt-4 p-4 bg-red-50 border border-red-200 rounded">
<h4 className="font-semibold text-red-800">Donation Failed</h4>
<p className="text-red-600">{lastError.message}</p>
{suggestedAmount && (
<div className="mt-2">
<p className="text-sm text-red-600">
Suggested amount: {suggestedAmount} BSV
</p>
<button
onClick={retryWithSuggestedAmount}
className="mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Retry with {suggestedAmount} BSV
</button>
</div>
)}
</div>
)}
</div>
);
}Performance Optimization
Preset Caching Strategy
import { QuickDonateButton } from 'bigblocks';
import { useState, useEffect, useMemo } from 'react';
export default function OptimizedDonation() {
const [cachedPresets, setCachedPresets] = useState<Map<string, any>>(new Map());
const recipientAddress = "1BitcoinAddress...";
// Memoized preset loading with cache
const presets = useMemo(() => {
const cacheKey = `${recipientAddress}:BSV`;
if (cachedPresets.has(cacheKey)) {
const cached = cachedPresets.get(cacheKey);
// Check if cache is still valid (5 minute TTL)
if (Date.now() - cached.timestamp < 300000) {
return cached.presets;
}
}
// Load fresh presets
fetch(`/api/wallet/quick-donate/presets?currency=BSV&recipient=${recipientAddress}`)
.then(res => res.json())
.then(data => {
setCachedPresets(prev => new Map(prev).set(cacheKey, {
presets: data.presets.map((p: any) => parseFloat(p.amountBSV)),
timestamp: Date.now()
}));
})
.catch(console.error);
// Return default while loading
return [0.001, 0.01, 0.1];
}, [recipientAddress, cachedPresets]);
return (
<QuickDonateButton
recipientAddress={recipientAddress}
amounts={presets}
/>
);
}Security Considerations
Rate Limiting and Abuse Prevention
The QuickDonateButton implements several security measures:
- Transaction Rate Limiting: Maximum 20 donations per hour per user
- Daily Spending Limits: Configurable daily donation limits (default: 1 BSV)
- Address Validation: Real-time Bitcoin address format validation
- Amount Validation: Enforced minimum (dust limit) and maximum amounts
- Recipient Verification: Optional recipient verification system
Safe Donation Practices
import { QuickDonateButton } from 'bigblocks';
export default function SecureDonation() {
const verifyRecipient = async (address: string) => {
try {
const response = await fetch(`/api/wallet/recipients/${address}`);
const data = await response.json();
return {
verified: data.recipient?.verified || false,
trustScore: data.trustScore || 0,
category: data.recipient?.category
};
} catch {
return { verified: false, trustScore: 0 };
}
};
const handleSecureDonation = async (txid: string) => {
// Log successful donation for audit trail
await fetch('/api/audit/donation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
txid,
timestamp: Date.now(),
source: 'quick-donate-button',
userAgent: navigator.userAgent
})
});
console.log('Secure donation completed:', txid);
};
return (
<div className="secure-donation">
<div className="security-notice mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-sm text-yellow-800">
🔒 All donations are processed securely using Bitcoin signatures.
Verify recipient addresses before donating.
</p>
</div>
<QuickDonateButton
recipientAddress="1VerifiedRecipientAddress..."
amounts={[0.001, 0.01, 0.1]}
onSuccess={handleSecureDonation}
onError={(error) => {
// Log security-related errors
if (error.code === 'RECIPIENT_BLOCKED') {
console.warn('Attempted donation to blocked recipient');
}
}}
/>
</div>
);
}Troubleshooting
Common Issues
Button doesn't respond to clicks
- Ensure
BitcoinAuthProvideris properly configured - Check that user has sufficient balance for minimum donation
- Verify recipient address format is valid
Donations fail with "insufficient funds"
- Check user's actual BSV balance
- Account for transaction fees (~0.0001 BSV)
- Verify no daily spending limits are exceeded
Preset amounts not loading
- Check API endpoint
/api/wallet/quick-donate/presetsis accessible - Verify recipient address is valid
- Check network connectivity for preset fetching
Daily limit errors
- Daily limits reset at midnight UTC
- Check user's donation history for the current day
- Limits can be configured per user in admin settings
Debug Mode
import { QuickDonateButton } from 'bigblocks';
export default function DebugDonation() {
const debugDonation = (txid: string) => {
console.log('Debug donation success:', {
txid,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
recipientAddress: '1BitcoinAddress...'
});
};
const debugError = (error: QuickDonationError) => {
console.error('Debug donation error:', {
code: error.code,
message: error.message,
details: error.details,
timestamp: new Date().toISOString()
});
};
return (
<div className="debug-donation">
<h3>Debug Mode Donation</h3>
<QuickDonateButton
recipientAddress="1BitcoinAddress..."
amounts={[0.001, 0.01, 0.1]}
onSuccess={debugDonation}
onError={debugError}
className="border-2 border-gray-300"
/>
</div>
);
}Related Components
- DonateButton - Full-featured donation interface
- SendBSVButton - General BSV sending
- WalletOverview - Wallet balance and status
- CompactWalletOverview - Compact wallet display
API Reference
QuickDonateButton Component
interface QuickDonateButtonProps {
recipientAddress: string;
amounts?: number[];
defaultAmount?: number;
onSuccess?: (txid: string) => void;
onError?: (error: QuickDonationError) => void;
label?: string;
compact?: boolean;
className?: string;
}Advanced Usage with React Query
import { useQuery, useMutation } from '@tanstack/react-query';
import { QuickDonateButton } from 'bigblocks';
function QuickDonateWithQuery() {
// Query for smart presets
const { data: presets } = useQuery({
queryKey: ['donation-presets', recipientAddress],
queryFn: () => fetch(`/api/wallet/quick-donate/presets?currency=BSV&recipient=${recipientAddress}`).then(res => res.json()),
staleTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false
});
// Mutation for donation processing
const donationMutation = useMutation({
mutationFn: (donationData: { recipientAddress: string; amount: number }) =>
fetch('/api/wallet/quick-donate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(donationData)
}).then(res => res.json()),
onSuccess: (data) => {
console.log('Donation processed:', data.txid);
},
onError: (error) => {
console.error('Donation failed:', error);
}
});
const amounts = presets?.presets.map((p: any) => parseFloat(p.amountBSV)) || [0.001, 0.01, 0.1];
return (
<QuickDonateButton
recipientAddress={recipientAddress}
amounts={amounts}
onSuccess={(txid) => console.log('Success:', txid)}
onError={(error) => console.error('Error:', error)}
/>
);
}