Components/Marketplace
QuickBuyButton
A one-click purchase button for marketplace listings with built-in transaction handling and confirmation flows
A streamlined one-click purchase button designed for marketplace listings, providing instant buying capabilities with built-in transaction handling, optional confirmation dialogs, and comprehensive error management for a frictionless purchase experience.
Installation
npx bigblocks add quick-buy-button
Import
import { QuickBuyButton } from 'bigblocks';
Props
Prop | Type | Required | Default | Description |
---|---|---|---|---|
listingId | string | Yes | - | Marketplace listing ID to purchase |
price | number | Yes | - | Price in satoshis |
onSuccess | (transaction: Transaction) => void | No | - | Success callback with transaction details |
onError | (error: MarketError) => void | No | - | Error callback |
confirmPurchase | boolean | No | true | Show confirmation dialog before purchase |
variant | 'default' | 'outline' | 'ghost' | No | 'default' | Button style variant |
size | 'sm' | 'md' | 'lg' | No | 'md' | Button size |
disabled | boolean | No | false | Disable the button |
loading | boolean | No | false | Show loading state |
className | string | No | - | Additional CSS classes |
Transaction Interface
interface Transaction {
txid: string; // Blockchain transaction ID
listingId: string; // Purchased listing ID
price: number; // Amount paid in satoshis
seller: {
address: string;
idKey?: string; // BAP identity key
};
buyer: {
address: string;
idKey?: string;
};
timestamp: number;
status: 'pending' | 'confirmed' | 'failed';
confirmations?: number;
blockHeight?: number;
}
interface MarketError {
code:
| 'INSUFFICIENT_FUNDS'
| 'LISTING_NOT_FOUND'
| 'LISTING_SOLD'
| 'PAYMENT_FAILED'
| 'NETWORK_ERROR'
| 'AUTH_REQUIRED';
message: string;
details?: {
required?: number;
available?: number;
listingStatus?: string;
};
}
Basic Usage
import { QuickBuyButton } from 'bigblocks';
export default function ListingCard({ listing }) {
const handlePurchaseSuccess = (transaction: Transaction) => {
console.log('Purchase successful!');
console.log('Transaction ID:', transaction.txid);
console.log('Listing purchased:', transaction.listingId);
// Navigate to success page
window.location.href = `/purchases/${transaction.txid}`;
};
const handlePurchaseError = (error: MarketError) => {
console.error('Purchase failed:', error);
if (error.code === 'INSUFFICIENT_FUNDS') {
alert(`Need ${error.details?.required} satoshis, but only have ${error.details?.available}`);
} else {
alert(error.message);
}
};
return (
<div className="listing-card">
<h3>{listing.title}</h3>
<p className="price">{listing.price} sats</p>
<QuickBuyButton
listingId={listing.id}
price={listing.price}
onSuccess={handlePurchaseSuccess}
onError={handlePurchaseError}
/>
</div>
);
}
Advanced Usage
Complete Marketplace Integration
import { QuickBuyButton } from 'bigblocks';
import { useState, useEffect } from 'react';
interface MarketplaceListing {
id: string;
title: string;
description: string;
price: number;
images: string[];
seller: {
name: string;
address: string;
rating: number;
};
status: 'active' | 'sold' | 'pending';
}
export default function AdvancedMarketplace() {
const [listings, setListings] = useState<MarketplaceListing[]>([]);
const [purchases, setPurchases] = useState<Transaction[]>([]);
const [processingId, setProcessingId] = useState<string | null>(null);
const handlePurchase = async (listingId: string) => {
setProcessingId(listingId);
};
const handlePurchaseSuccess = async (transaction: Transaction) => {
console.log('Purchase completed:', transaction);
// Update local state
setPurchases(prev => [transaction, ...prev]);
setListings(prev =>
prev.map(listing =>
listing.id === transaction.listingId
? { ...listing, status: 'sold' as const }
: listing
)
);
// Track analytics
await fetch('/api/analytics/purchase', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
txid: transaction.txid,
listingId: transaction.listingId,
price: transaction.price,
timestamp: Date.now()
})
});
// Show success notification
const notification = new Notification('Purchase Successful!', {
body: `Transaction ${transaction.txid.slice(0, 8)}... confirmed`,
icon: '/icon-192x192.png'
});
setProcessingId(null);
// Navigate to purchase details after delay
setTimeout(() => {
window.location.href = `/purchases/${transaction.txid}`;
}, 2000);
};
const handlePurchaseError = (error: MarketError) => {
setProcessingId(null);
switch (error.code) {
case 'INSUFFICIENT_FUNDS':
showFundingModal(error.details?.required || 0);
break;
case 'LISTING_SOLD':
alert('This item has already been sold.');
refreshListings();
break;
case 'PAYMENT_FAILED':
alert('Payment processing failed. Please try again.');
break;
default:
alert(`Purchase failed: ${error.message}`);
}
};
return (
<div className="advanced-marketplace">
<h1>Marketplace</h1>
<div className="listings-grid">
{listings.map((listing) => (
<div key={listing.id} className="listing-item">
<img src={listing.images[0]} alt={listing.title} />
<h3>{listing.title}</h3>
<p className="seller">by {listing.seller.name}</p>
<p className="price">{listing.price.toLocaleString()} sats</p>
{listing.status === 'active' ? (
<QuickBuyButton
listingId={listing.id}
price={listing.price}
onSuccess={handlePurchaseSuccess}
onError={handlePurchaseError}
loading={processingId === listing.id}
disabled={processingId !== null && processingId !== listing.id}
variant="default"
size="md"
/>
) : (
<span className="sold-badge">SOLD</span>
)}
</div>
))}
</div>
{purchases.length > 0 && (
<div className="recent-purchases">
<h2>Your Recent Purchases</h2>
<ul>
{purchases.slice(0, 5).map((purchase) => (
<li key={purchase.txid}>
<a href={`/purchases/${purchase.txid}`}>
{purchase.txid.slice(0, 8)}...
</a>
<span className="purchase-price">
{purchase.price.toLocaleString()} sats
</span>
</li>
))}
</ul>
</div>
)}
</div>
);
}
Skip Confirmation for Small Purchases
import { QuickBuyButton } from 'bigblocks';
export default function MicropaymentMarketplace() {
const MICROPAYMENT_THRESHOLD = 10000; // 10k satoshis
const handleQuickPurchase = (listing: any) => {
const skipConfirmation = listing.price < MICROPAYMENT_THRESHOLD;
return (
<QuickBuyButton
listingId={listing.id}
price={listing.price}
confirmPurchase={!skipConfirmation}
onSuccess={(tx) => {
if (skipConfirmation) {
console.log('Micropayment processed instantly:', tx);
} else {
console.log('Purchase confirmed:', tx);
}
}}
size="sm"
variant="outline"
/>
);
};
return (
<div className="micropayment-marketplace">
<h2>Digital Downloads</h2>
<p className="info">
Purchases under {MICROPAYMENT_THRESHOLD.toLocaleString()} sats are instant!
</p>
<div className="downloads-grid">
{digitalGoods.map((item) => (
<div key={item.id} className="download-item">
<h4>{item.title}</h4>
<p className="price">{item.price} sats</p>
{handleQuickPurchase(item)}
</div>
))}
</div>
</div>
);
}
Common Patterns
NFT Marketplace Quick Buy
import { QuickBuyButton } from 'bigblocks';
interface NFTListing {
id: string;
tokenId: string;
collection: string;
name: string;
price: number;
rarity: 'common' | 'rare' | 'epic' | 'legendary';
owner: string;
}
export default function NFTMarketplace() {
const [nfts, setNfts] = useState<NFTListing[]>([]);
const handleNFTPurchase = async (transaction: Transaction) => {
console.log('NFT purchased:', transaction);
// Transfer NFT ownership on-chain
await transferNFTOwnership(transaction.listingId, transaction.buyer.address);
// Update collection
await updateCollection(transaction.buyer.idKey, transaction.listingId);
// Show success with confetti
showConfetti();
setTimeout(() => {
window.location.href = `/collection/${transaction.buyer.idKey}`;
}, 3000);
};
const getRarityColor = (rarity: string) => {
const colors = {
common: 'gray',
rare: 'blue',
epic: 'purple',
legendary: 'orange'
};
return colors[rarity] || 'gray';
};
return (
<div className="nft-marketplace">
<h1>NFT Marketplace</h1>
<div className="nft-grid">
{nfts.map((nft) => (
<div key={nft.id} className="nft-card">
<div className={`rarity-badge ${nft.rarity}`}>
{nft.rarity.toUpperCase()}
</div>
<img src={`/api/nft/image/${nft.tokenId}`} alt={nft.name} />
<div className="nft-details">
<h3>{nft.name}</h3>
<p className="collection">{nft.collection}</p>
<p className="price">{nft.price.toLocaleString()} sats</p>
<QuickBuyButton
listingId={nft.id}
price={nft.price}
onSuccess={handleNFTPurchase}
onError={(error) => {
if (error.code === 'LISTING_SOLD') {
alert('This NFT has already been sold!');
refreshNFTs();
}
}}
variant="default"
className={`rarity-${nft.rarity}`}
/>
</div>
</div>
))}
</div>
</div>
);
}
Auction Quick Buy-It-Now
import { QuickBuyButton } from 'bigblocks';
import { useState, useEffect } from 'react';
interface AuctionListing {
id: string;
title: string;
currentBid: number;
buyItNowPrice: number;
endTime: number;
bidCount: number;
}
export default function AuctionMarketplace() {
const [auctions, setAuctions] = useState<AuctionListing[]>([]);
const handleBuyItNow = (transaction: Transaction) => {
console.log('Buy It Now executed:', transaction);
// Remove from active auctions
setAuctions(prev => prev.filter(a => a.id !== transaction.listingId));
// Show success message
alert('You won the auction with Buy It Now!');
};
const getTimeRemaining = (endTime: number) => {
const remaining = endTime - Date.now();
const hours = Math.floor(remaining / 3600000);
const minutes = Math.floor((remaining % 3600000) / 60000);
return `${hours}h ${minutes}m`;
};
return (
<div className="auction-marketplace">
<h1>Live Auctions</h1>
<div className="auctions-grid">
{auctions.map((auction) => (
<div key={auction.id} className="auction-card">
<h3>{auction.title}</h3>
<div className="auction-stats">
<div className="current-bid">
<span className="label">Current Bid</span>
<span className="value">{auction.currentBid.toLocaleString()} sats</span>
<span className="bid-count">({auction.bidCount} bids)</span>
</div>
<div className="time-remaining">
<span className="label">Time Left</span>
<span className="value">{getTimeRemaining(auction.endTime)}</span>
</div>
</div>
<div className="buy-it-now">
<p className="or-divider">— OR —</p>
<p className="bin-label">Buy It Now</p>
<QuickBuyButton
listingId={auction.id}
price={auction.buyItNowPrice}
onSuccess={handleBuyItNow}
confirmPurchase={true}
variant="default"
size="lg"
className="buy-it-now-button"
/>
</div>
</div>
))}
</div>
</div>
);
}
Service Marketplace Quick Purchase
import { QuickBuyButton } from 'bigblocks';
interface ServiceListing {
id: string;
title: string;
description: string;
price: number;
deliveryTime: string;
provider: {
name: string;
rating: number;
completedJobs: number;
};
}
export default function ServiceMarketplace() {
const [services, setServices] = useState<ServiceListing[]>([]);
const handleServicePurchase = async (transaction: Transaction) => {
console.log('Service purchased:', transaction);
// Create service order
const order = await createServiceOrder({
txid: transaction.txid,
serviceId: transaction.listingId,
buyerId: transaction.buyer.idKey,
sellerId: transaction.seller.idKey
});
// Send notification to service provider
await notifyProvider(transaction.seller.idKey, order.id);
// Redirect to order tracking
window.location.href = `/orders/${order.id}`;
};
return (
<div className="service-marketplace">
<h1>Hire Bitcoin Developers</h1>
<div className="services-grid">
{services.map((service) => (
<div key={service.id} className="service-card">
<div className="provider-info">
<span className="provider-name">{service.provider.name}</span>
<span className="rating">
⭐ {service.provider.rating} ({service.provider.completedJobs} jobs)
</span>
</div>
<h3>{service.title}</h3>
<p>{service.description}</p>
<div className="service-details">
<span className="delivery">🚚 {service.deliveryTime}</span>
<span className="price">{service.price.toLocaleString()} sats</span>
</div>
<QuickBuyButton
listingId={service.id}
price={service.price}
onSuccess={handleServicePurchase}
variant="outline"
size="md"
className="hire-button"
/>
</div>
))}
</div>
</div>
);
}
Subscription Quick Purchase
import { QuickBuyButton } from 'bigblocks';
interface SubscriptionPlan {
id: string;
name: string;
price: number;
period: 'monthly' | 'yearly';
features: string[];
popular?: boolean;
}
export default function SubscriptionPlans() {
const plans: SubscriptionPlan[] = [
{
id: 'basic',
name: 'Basic',
price: 50000,
period: 'monthly',
features: ['Feature A', 'Feature B']
},
{
id: 'pro',
name: 'Pro',
price: 100000,
period: 'monthly',
features: ['Everything in Basic', 'Feature C', 'Feature D'],
popular: true
},
{
id: 'enterprise',
name: 'Enterprise',
price: 250000,
period: 'monthly',
features: ['Everything in Pro', 'Feature E', 'Priority Support']
}
];
const handleSubscriptionPurchase = async (transaction: Transaction) => {
const plan = plans.find(p => p.id === transaction.listingId);
console.log(`Subscribed to ${plan?.name} plan`);
// Create subscription
await createSubscription({
planId: transaction.listingId,
txid: transaction.txid,
startDate: Date.now(),
endDate: Date.now() + 30 * 24 * 60 * 60 * 1000 // 30 days
});
// Update user permissions
await updateUserPermissions(transaction.buyer.idKey, plan?.name);
// Redirect to dashboard
window.location.href = '/dashboard';
};
return (
<div className="subscription-plans">
<h1>Choose Your Plan</h1>
<div className="plans-grid">
{plans.map((plan) => (
<div key={plan.id} className={`plan-card ${plan.popular ? 'popular' : ''}`}>
{plan.popular && <span className="popular-badge">Most Popular</span>}
<h2>{plan.name}</h2>
<p className="price">
{plan.price.toLocaleString()} sats
<span className="period">/{plan.period}</span>
</p>
<ul className="features">
{plan.features.map((feature, index) => (
<li key={index}>✓ {feature}</li>
))}
</ul>
<QuickBuyButton
listingId={plan.id}
price={plan.price}
onSuccess={handleSubscriptionPurchase}
confirmPurchase={true}
variant={plan.popular ? 'default' : 'outline'}
size="lg"
className="subscribe-button"
/>
</div>
))}
</div>
</div>
);
}
Authentication Requirements
The QuickBuyButton requires Bitcoin authentication for transaction processing:
import {
BitcoinAuthProvider,
BitcoinQueryProvider,
QuickBuyButton
} from 'bigblocks';
function App() {
return (
<BitcoinQueryProvider>
<BitcoinAuthProvider config={{
apiUrl: '/api',
walletMode: 'integrated' // Required for payments
}}>
<QuickBuyButton
listingId="listing123"
price={10000}
onSuccess={(tx) => console.log('Purchased!', tx)}
/>
</BitcoinAuthProvider>
</BitcoinQueryProvider>
);
}
API Integration
The QuickBuyButton integrates with marketplace purchase endpoints:
Purchase Endpoints
// Execute quick purchase
POST /api/market/quick-buy
{
listingId: string;
price: number;
}
Response:
{
success: boolean;
transaction: {
txid: string;
listingId: string;
price: number;
seller: { address: string; idKey: string; };
buyer: { address: string; idKey: string; };
timestamp: number;
status: string;
};
}
// Verify listing availability
GET /api/market/listings/{listingId}/availability
Response:
{
available: boolean;
price: number;
seller: string;
status: 'active' | 'sold' | 'pending';
}
Features
- One-Click Purchase: Instant buying with minimal friction
- Confirmation Dialog: Optional purchase confirmation for safety
- Transaction Handling: Complete blockchain transaction management
- Error Recovery: Comprehensive error handling and user feedback
- Loading States: Visual feedback during transaction processing
- Style Variants: Multiple button styles for different contexts
- Size Options: Flexible sizing for various layouts
- Status Updates: Real-time transaction status tracking
Error Handling
Comprehensive Error Management
import { QuickBuyButton } from 'bigblocks';
export default function RobustQuickBuy() {
const handleError = (error: MarketError) => {
switch (error.code) {
case 'INSUFFICIENT_FUNDS':
const needed = error.details?.required || 0;
const have = error.details?.available || 0;
alert(`Insufficient funds. Need ${needed} sats but only have ${have}.`);
// Show funding options
showFundingModal(needed - have);
break;
case 'LISTING_SOLD':
alert('This item has already been sold.');
// Refresh the listing
window.location.reload();
break;
case 'AUTH_REQUIRED':
alert('Please sign in to make purchases.');
window.location.href = '/signin';
break;
case 'NETWORK_ERROR':
alert('Network error. Please check your connection and try again.');
break;
default:
alert(`Purchase failed: ${error.message}`);
}
};
return (
<QuickBuyButton
listingId="abc123"
price={50000}
onError={handleError}
onSuccess={(tx) => {
console.log('Success!', tx);
}}
/>
);
}
Styling
The component includes three style variants:
/* Default variant */
.quick-buy-button.default {
background: var(--primary-color);
color: white;
border: none;
}
.quick-buy-button.default:hover {
background: var(--primary-hover);
}
/* Outline variant */
.quick-buy-button.outline {
background: transparent;
color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.quick-buy-button.outline:hover {
background: var(--primary-light);
}
/* Ghost variant */
.quick-buy-button.ghost {
background: transparent;
color: var(--text-color);
border: none;
}
.quick-buy-button.ghost:hover {
background: var(--hover-bg);
}
/* Size variations */
.quick-buy-button.sm {
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
}
.quick-buy-button.md {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.quick-buy-button.lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
/* Loading state */
.quick-buy-button.loading {
opacity: 0.7;
cursor: not-allowed;
}
.quick-buy-button .spinner {
animation: spin 1s linear infinite;
}
Performance Optimization
Optimistic UI Updates
import { QuickBuyButton } from 'bigblocks';
import { useOptimistic } from 'react';
export default function OptimisticPurchase({ listing }) {
const [optimisticStatus, setOptimisticStatus] = useOptimistic(
listing.status,
(state, newStatus) => newStatus
);
const handlePurchase = async (transaction: Transaction) => {
// Optimistically update UI
setOptimisticStatus('sold');
try {
// Actual purchase handled by component
console.log('Purchase confirmed:', transaction);
} catch (error) {
// Revert on error
setOptimisticStatus('active');
}
};
return (
<div className={`listing ${optimisticStatus}`}>
<h3>{listing.title}</h3>
{optimisticStatus === 'active' ? (
<QuickBuyButton
listingId={listing.id}
price={listing.price}
onSuccess={handlePurchase}
/>
) : (
<span className="sold-badge">SOLD</span>
)}
</div>
);
}
Troubleshooting
Common Issues
Button not responding
- Verify Bitcoin authentication is set up
- Check that user has sufficient balance
- Ensure listing ID and price are valid
- Confirm wallet is unlocked
Confirmation dialog not showing
- Check
confirmPurchase
prop is true (default) - Verify no JavaScript errors in console
- Ensure modal container is present
Transaction failures
- Check wallet balance for funds + fees
- Verify listing is still available
- Ensure network connectivity
- Check for rate limiting
Loading state stuck
- Verify API endpoints are responding
- Check for network timeouts
- Ensure error callbacks are implemented
Related Components
- BuyListingButton - Full purchase flow with details
- CreateListingButton - Create marketplace listings
- CompactMarketTable - Display listings
- MarketTable - Full marketplace table
API Reference
QuickBuyButton Component
interface QuickBuyButtonProps {
listingId: string;
price: number;
onSuccess?: (transaction: Transaction) => void;
onError?: (error: MarketError) => void;
confirmPurchase?: boolean;
variant?: 'default' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
className?: string;
}
Usage with React Query
import { useMutation } from '@tanstack/react-query';
import { QuickBuyButton } from 'bigblocks';
function QuickBuyWithQuery({ listing }) {
const purchaseMutation = useMutation({
mutationFn: (data: { listingId: string; price: number }) =>
fetch('/api/market/quick-buy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}).then(res => res.json()),
onSuccess: (data) => {
console.log('Purchase completed:', data.transaction);
queryClient.invalidateQueries(['market-listings']);
}
});
return (
<QuickBuyButton
listingId={listing.id}
price={listing.price}
loading={purchaseMutation.isLoading}
onSuccess={(tx) => {
console.log('Component success:', tx);
}}
/>
);
}