NFT Checkout with Universal Bridge

Learn how to build a seamless NFT checkout experience that combines in-app wallet authentication, fiat-to-crypto onramps, and NFT minting. This tutorial demonstrates a complete user flow where users can purchase NFTs directly with fiat currency.

This pattern is perfect for NFT marketplaces, gaming platforms, and any application where you want to minimize friction for new crypto users.


  • Install the SDK

    npm i thirdweb
  • Setup Project and Contract

    Configure your client and prepare an NFT contract for minting:

    import { createThirdwebClient } from "thirdweb";
    import { getContract } from "thirdweb";
    import { sepolia } from "thirdweb/chains"; // Using testnet for demo
    const client = createThirdwebClient({
    clientId: "your_client_id",
    });
    // Your NFT contract (ERC721 or ERC1155)
    const nftContract = getContract({
    client,
    chain: sepolia,
    address: "0x...", // Your NFT contract address
    });
    // NFT pricing configuration
    const NFT_PRICE_ETH = "0.01"; // 0.01 ETH per NFT
    const NFT_METADATA = {
    name: "Awesome NFT Collection",
    description: "A limited edition NFT with exclusive benefits",
    image: "https://your-domain.com/nft-image.png",
    attributes: [
    { trait_type: "Rarity", value: "Rare" },
    { trait_type: "Collection", value: "Genesis" },
    ],
    };
  • Implement In-App Wallet Authentication

    Set up seamless wallet authentication for new users:

    import { inAppWallet } from "thirdweb/wallets/in-app";
    import {
    ConnectButton,
    useActiveAccount,
    useConnect,
    } from "thirdweb/react";
    // Configure in-app wallet for seamless onboarding
    const wallet = inAppWallet({
    auth: {
    options: ["google", "apple", "facebook", "email"],
    },
    });
    // Authentication component
    function WalletAuth({
    onConnect,
    }: {
    onConnect: (address: string) => void;
    }) {
    const account = useActiveAccount();
    const { connect } = useConnect();
    const handleConnect = async () => {
    try {
    const connectedAccount = await connect(async () => {
    const account = await wallet.connect({
    client,
    strategy: "google", // or other auth methods
    });
    return account;
    });
    console.log("Wallet connected:", connectedAccount.address);
    onConnect(connectedAccount.address);
    } catch (error) {
    console.error("Connection failed:", error);
    }
    };
    if (account) {
    onConnect(account.address); // Auto-proceed when wallet is connected
    return (
    <div className="text-center space-y-2">
    <p className="text-green-600">✅ Wallet Connected</p>
    <p className="text-sm text-gray-600">
    {account.address.slice(0, 6)}...{account.address.slice(-4)}
    </p>
    </div>
    );
    }
    return (
    <div className="space-y-4">
    <ConnectButton
    client={client}
    wallets={[wallet]}
    connectButton={{
    label: "Sign In to Buy NFT",
    style: {
    background:
    "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
    color: "white",
    border: "none",
    borderRadius: "8px",
    padding: "12px 24px",
    fontSize: "16px",
    fontWeight: "600",
    },
    }}
    />
    <p className="text-xs text-gray-500 text-center">
    New to crypto? No worries! Sign in with your existing account.
    </p>
    </div>
    );
    }
  • Build NFT Checkout Flow

    Create the main checkout component that handles the complete purchase flow:

    import {
    Bridge,
    NATIVE_TOKEN_ADDRESS,
    toWei,
    sendTransaction,
    } from "thirdweb";
    import { prepareContractCall } from "thirdweb";
    import { mintTo } from "thirdweb/extensions/erc721";
    import { useState, useEffect } from "react";
    interface CheckoutState {
    step: "connect" | "payment" | "processing" | "success" | "error";
    selectedProvider?: "stripe" | "coinbase" | "transak";
    onrampSession?: any;
    mintTxHash?: string;
    error?: string;
    }
    function NFTCheckout() {
    const [state, setState] = useState<CheckoutState>({
    step: "connect",
    });
    const account = useActiveAccount();
    const handleWalletConnect = (address: string) => {
    setState({ step: "payment" });
    };
    const handlePaymentSelect = async (
    provider: "stripe" | "coinbase" | "transak",
    ) => {
    if (!account) return;
    setState({ step: "processing", selectedProvider: provider });
    try {
    // Start onramp process
    const onrampSession = await Bridge.Onramp.prepare({
    client,
    onramp: provider,
    chainId: sepolia.id,
    tokenAddress: NATIVE_TOKEN_ADDRESS,
    receiver: account.address,
    amount: toWei(NFT_PRICE_ETH),
    currency: "USD",
    country: "US", // You should detect this based on user's location
    });
    console.log("Onramp session created:", onrampSession.id);
    setState({
    step: "processing",
    selectedProvider: provider,
    onrampSession,
    });
    // Store session info for monitoring
    localStorage.setItem(
    "currentOnrampSession",
    JSON.stringify({
    id: onrampSession.id,
    provider,
    timestamp: Date.now(),
    }),
    );
    // Open onramp in new window/tab
    window.open(onrampSession.link, "_blank");
    // Start monitoring the session
    monitorOnrampAndMint(onrampSession.id);
    } catch (error) {
    console.error("Failed to start payment:", error);
    setState({
    step: "error",
    error:
    error instanceof Error ? error.message : "Payment failed",
    });
    }
    };
    const monitorOnrampAndMint = async (sessionId: string) => {
    try {
    const status = await Bridge.Onramp.status({
    sessionId,
    client,
    });
    switch (status.status) {
    case "COMPLETED":
    console.log("Payment completed! Now minting NFT...");
    await mintNFT();
    break;
    case "PENDING":
    console.log("Payment still in progress...");
    // Check again in 10 seconds
    setTimeout(() => monitorOnrampAndMint(sessionId), 10000);
    break;
    case "FAILED":
    console.log("Payment failed:", status.error);
    setState({
    step: "error",
    error: "Payment failed. Please try again.",
    });
    break;
    case "CANCELLED":
    console.log("Payment was cancelled");
    setState({ step: "payment" }); // Return to payment selection
    break;
    }
    } catch (error) {
    console.error("Failed to check payment status:", error);
    setState({
    step: "error",
    error: "Failed to check payment status",
    });
    }
    };
    const mintNFT = async () => {
    if (!account) return;
    try {
    // Prepare NFT mint transaction
    const mintTransaction = mintTo({
    contract: nftContract,
    to: account.address,
    nft: NFT_METADATA,
    });
    // Execute mint transaction
    const result = await sendTransaction({
    transaction: mintTransaction,
    account,
    });
    console.log("NFT minted successfully:", result.transactionHash);
    setState({
    step: "success",
    mintTxHash: result.transactionHash,
    });
    // Clean up session storage
    localStorage.removeItem("currentOnrampSession");
    } catch (error) {
    console.error("Minting failed:", error);
    setState({
    step: "error",
    error:
    error instanceof Error ? error.message : "Minting failed",
    });
    }
    };
    return (
    <div className="max-w-md mx-auto bg-white rounded-lg shadow-lg p-6">
    <div className="text-center mb-6">
    <img
    src={NFT_METADATA.image}
    alt={NFT_METADATA.name}
    className="w-48 h-48 mx-auto rounded-lg object-cover mb-4"
    />
    <h2 className="text-2xl font-bold text-gray-800">
    {NFT_METADATA.name}
    </h2>
    <p className="text-gray-600 text-sm mt-2">
    {NFT_METADATA.description}
    </p>
    <div className="text-3xl font-bold text-blue-600 mt-4">
    {NFT_PRICE_ETH} ETH (~$30 USD)
    </div>
    </div>
    {/* Step Progress Indicator */}
    <div className="flex justify-between mb-6">
    {["connect", "payment", "processing", "success"].map(
    (step, index) => (
    <div
    key={step}
    className={`flex-1 h-2 rounded-full mx-1 ${
    [
    "connect",
    "payment",
    "processing",
    "success",
    ].indexOf(state.step) >= index
    ? "bg-blue-500"
    : "bg-gray-200"
    }`}
    />
    ),
    )}
    </div>
    {/* Step Content */}
    {state.step === "connect" && (
    <WalletAuth onConnect={handleWalletConnect} />
    )}
    {state.step === "payment" && (
    <PaymentOptions onSelect={handlePaymentSelect} />
    )}
    {state.step === "processing" && (
    <ProcessingStep provider={state.selectedProvider!} />
    )}
    {state.step === "success" && (
    <SuccessStep
    txHash={state.mintTxHash!}
    nftMetadata={NFT_METADATA}
    />
    )}
    {state.step === "error" && (
    <ErrorStep
    error={state.error!}
    onRetry={() => setState({ step: "payment" })}
    />
    )}
    </div>
    );
    }
  • Create Payment Selection

    Build the payment options component that shows provider choices:

    interface PaymentOptionsProps {
    onSelect: (provider: "stripe" | "coinbase" | "transak") => void;
    }
    function PaymentOptions({ onSelect }: PaymentOptionsProps) {
    const paymentMethods = [
    {
    provider: "stripe" as const,
    name: "Stripe",
    description: "Credit Card, Apple Pay, Google Pay",
    logo: "💳",
    fees: "3.5% + $0.30",
    popular: true,
    },
    {
    provider: "coinbase" as const,
    name: "Coinbase Pay",
    description: "Bank Transfer, Debit Card",
    logo: "🟦",
    fees: "1% (Bank) / 3.9% (Card)",
    popular: false,
    },
    {
    provider: "transak" as const,
    name: "Transak",
    description: "Multiple Payment Options",
    logo: "🌐",
    fees: "0.99% - 5.5%",
    popular: false,
    },
    ];
    return (
    <div className="space-y-4">
    <div className="text-center mb-6">
    <h3 className="text-lg font-semibold mb-2">
    Choose Payment Method
    </h3>
    <p className="text-sm text-gray-600">
    Select how you'd like to pay for your NFT
    </p>
    </div>
    <div className="space-y-3">
    {paymentMethods.map((method) => (
    <button
    key={method.provider}
    onClick={() => onSelect(method.provider)}
    className="w-full p-4 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition-all group relative"
    >
    {method.popular && (
    <div className="absolute -top-2 left-4 bg-blue-500 text-white text-xs px-2 py-1 rounded">
    Most Popular
    </div>
    )}
    <div className="flex items-center justify-between">
    <div className="flex items-center space-x-3">
    <div className="text-2xl">{method.logo}</div>
    <div className="text-left">
    <div className="font-semibold text-gray-800">
    {method.name}
    </div>
    <div className="text-sm text-gray-600">
    {method.description}
    </div>
    <div className="text-xs text-gray-500">
    Fees: {method.fees}
    </div>
    </div>
    </div>
    <div className="text-blue-500 group-hover:translate-x-1 transition-transform">
    </div>
    </div>
    </button>
    ))}
    </div>
    <div className="bg-gray-50 rounded-lg p-4 mt-6">
    <div className="flex items-center space-x-2 mb-2">
    <span className="text-green-600">🔒</span>
    <span className="font-medium text-sm">
    Secure Payment Processing
    </span>
    </div>
    <p className="text-xs text-gray-600">
    All payments are processed securely through industry-leading
    providers. Your payment information is never stored on our
    servers.
    </p>
    </div>
    </div>
    );
    }
    function ProcessingStep({
    provider,
    }: {
    provider: "stripe" | "coinbase" | "transak";
    }) {
    const [currentStep, setCurrentStep] = useState<
    "payment" | "confirmation" | "minting"
    >("payment");
    // Auto-progress through steps for demo purposes
    useEffect(() => {
    const timer1 = setTimeout(
    () => setCurrentStep("confirmation"),
    5000,
    );
    const timer2 = setTimeout(() => setCurrentStep("minting"), 10000);
    return () => {
    clearTimeout(timer1);
    clearTimeout(timer2);
    };
    }, []);
    const providerInfo = {
    stripe: { name: "Stripe", logo: "💳", color: "blue" },
    coinbase: { name: "Coinbase Pay", logo: "🟦", color: "blue" },
    transak: { name: "Transak", logo: "🌐", color: "green" },
    };
    const info = providerInfo[provider];
    return (
    <div className="text-center space-y-6">
    <div className="mb-6">
    <div className="text-4xl mb-2">{info.logo}</div>
    <h3 className="text-lg font-semibold">
    Processing with {info.name}
    </h3>
    </div>
    {/* Progress Steps */}
    <div className="space-y-4">
    <div
    className={`flex items-center space-x-3 p-3 rounded-lg ${
    currentStep === "payment"
    ? "bg-blue-50 border border-blue-200"
    : "bg-gray-50"
    }`}
    >
    <div
    className={`w-6 h-6 rounded-full flex items-center justify-center text-sm font-bold ${
    currentStep === "payment"
    ? "bg-blue-500 text-white animate-pulse"
    : currentStep === "confirmation" ||
    currentStep === "minting"
    ? "bg-green-500 text-white"
    : "bg-gray-300"
    }`}
    >
    {currentStep === "payment" ? "1" : "✓"}
    </div>
    <div className="text-left">
    <div className="font-medium">Complete Payment</div>
    <div className="text-sm text-gray-600">
    {currentStep === "payment"
    ? "Finish your purchase in the payment window"
    : "Payment completed successfully"}
    </div>
    </div>
    </div>
    <div
    className={`flex items-center space-x-3 p-3 rounded-lg ${
    currentStep === "confirmation"
    ? "bg-blue-50 border border-blue-200"
    : "bg-gray-50"
    }`}
    >
    <div
    className={`w-6 h-6 rounded-full flex items-center justify-center text-sm font-bold ${
    currentStep === "confirmation"
    ? "bg-blue-500 text-white animate-pulse"
    : currentStep === "minting"
    ? "bg-green-500 text-white"
    : "bg-gray-300"
    }`}
    >
    {currentStep === "confirmation"
    ? "2"
    : currentStep === "minting"
    ? "✓"
    : "2"}
    </div>
    <div className="text-left">
    <div className="font-medium">Confirming Transaction</div>
    <div className="text-sm text-gray-600">
    {currentStep === "confirmation"
    ? "Verifying your payment on the blockchain"
    : currentStep === "minting"
    ? "Transaction confirmed"
    : "Waiting for payment completion"}
    </div>
    </div>
    </div>
    <div
    className={`flex items-center space-x-3 p-3 rounded-lg ${
    currentStep === "minting"
    ? "bg-blue-50 border border-blue-200"
    : "bg-gray-50"
    }`}
    >
    <div
    className={`w-6 h-6 rounded-full flex items-center justify-center text-sm font-bold ${
    currentStep === "minting"
    ? "bg-blue-500 text-white animate-pulse"
    : "bg-gray-300"
    }`}
    >
    3
    </div>
    <div className="text-left">
    <div className="font-medium">Minting Your NFT</div>
    <div className="text-sm text-gray-600">
    {currentStep === "minting"
    ? "Creating your unique NFT on the blockchain"
    : "Preparing to mint your NFT"}
    </div>
    </div>
    </div>
    </div>
    {currentStep === "payment" && (
    <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
    <p className="text-sm text-yellow-800">
    <strong>Complete your payment</strong> in the {info.name}{" "}
    window to continue. This page will automatically update
    when payment is confirmed.
    </p>
    </div>
    )}
    {currentStep === "minting" && (
    <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
    <p className="text-sm text-blue-800">
    <strong>Almost done!</strong> Your NFT is being created on
    the blockchain. This usually takes 30-60 seconds.
    </p>
    </div>
    )}
    </div>
    );
    }
  • Add Success and Error Handling

    Implement the final steps of the user journey:

    import { sepolia } from "thirdweb/chains";
    function SuccessStep({
    txHash,
    nftMetadata,
    }: {
    txHash: string;
    nftMetadata: typeof NFT_METADATA;
    }) {
    const explorerUrl = `${sepolia.blockExplorers?.[0]?.url}/tx/${txHash}`;
    return (
    <div className="text-center space-y-4">
    <div className="text-6xl">🎉</div>
    <div>
    <h3 className="text-xl font-bold text-green-600">
    NFT Minted Successfully!
    </h3>
    <p className="text-gray-600 mt-2">
    Congratulations! Your NFT has been minted and is now in your
    wallet.
    </p>
    </div>
    <div className="bg-gray-50 rounded-lg p-4 space-y-2">
    <p className="font-semibold">{nftMetadata.name}</p>
    <p className="text-sm text-gray-600">
    {nftMetadata.description}
    </p>
    <a
    href={explorerUrl}
    target="_blank"
    rel="noopener noreferrer"
    className="inline-block text-blue-500 underline text-sm"
    >
    View on Block Explorer
    </a>
    </div>
    <div className="space-y-2">
    <button
    onClick={() => window.location.reload()}
    className="w-full bg-green-500 text-white py-2 px-4 rounded-lg font-semibold"
    >
    Buy Another NFT
    </button>
    <button
    onClick={() => {
    // Share functionality
    navigator.share?.({
    title: "I just minted an NFT!",
    text: `Check out my new ${nftMetadata.name} NFT`,
    url: explorerUrl,
    });
    }}
    className="w-full border border-gray-300 text-gray-700 py-2 px-4 rounded-lg"
    >
    Share Your NFT
    </button>
    </div>
    </div>
    );
    }
    function ErrorStep({
    error,
    onRetry,
    }: {
    error: string;
    onRetry: () => void;
    }) {
    return (
    <div className="text-center space-y-4">
    <div className="text-red-500 text-4xl"></div>
    <div>
    <h3 className="text-xl font-bold text-red-600">
    Something went wrong
    </h3>
    <p className="text-gray-600 mt-2">
    We couldn't complete your NFT purchase. Please try again.
    </p>
    <details className="mt-2">
    <summary className="text-sm text-gray-500 cursor-pointer">
    Error details
    </summary>
    <p className="text-xs text-red-500 mt-1 font-mono bg-red-50 p-2 rounded">
    {error}
    </p>
    </details>
    </div>
    <div className="space-y-2">
    <button
    onClick={onRetry}
    className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg font-semibold"
    >
    Try Again
    </button>
    <button
    onClick={() => window.location.reload()}
    className="w-full border border-gray-300 text-gray-700 py-2 px-4 rounded-lg"
    >
    Start Over
    </button>
    </div>
    </div>
    );
    }
  • Complete App Integration

    Put it all together in your main application:

    import { ThirdwebProvider } from "thirdweb/react";
    // Import all the components we defined earlier
    import {
    NFTCheckout,
    WalletAuth,
    PaymentOptions,
    ProcessingStep,
    SuccessStep,
    ErrorStep,
    } from "./nft-checkout-components";
    function App() {
    return (
    <ThirdwebProvider>
    <div className="min-h-screen bg-gradient-to-br from-purple-50 to-blue-50 py-8">
    <div className="container mx-auto px-4">
    <header className="text-center mb-8">
    <h1 className="text-4xl font-bold text-gray-800 mb-2">
    NFT Marketplace
    </h1>
    <p className="text-gray-600">
    Buy NFTs seamlessly with fiat currency
    </p>
    </header>
    <NFTCheckout />
    <footer className="text-center mt-8 text-sm text-gray-500">
    <p>Powered by thirdweb Universal Bridge</p>
    </footer>
    </div>
    </div>
    </ThirdwebProvider>
    );
    }
    export default App;