BigBlocks Docs
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.

View Component Preview →

Installation

npx bigblocks add quick-buy-button

Import

import { QuickBuyButton } from 'bigblocks';

Props

PropTypeRequiredDefaultDescription
listingIdstringYes-Marketplace listing ID to purchase
pricenumberYes-Price in satoshis
onSuccess(transaction: Transaction) => voidNo-Success callback with transaction details
onError(error: MarketError) => voidNo-Error callback
confirmPurchasebooleanNotrueShow confirmation dialog before purchase
variant'default' | 'outline' | 'ghost'No'default'Button style variant
size'sm' | 'md' | 'lg'No'md'Button size
disabledbooleanNofalseDisable the button
loadingbooleanNofalseShow loading state
classNamestringNo-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

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);
      }}
    />
  );
}