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.
- npm i thirdweb
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 democonst 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 configurationconst NFT_PRICE_ETH = "0.01"; // 0.01 ETH per NFTconst 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" },],};For production, deploy your NFT contract on mainnet and update the chain configuration. You can use thirdweb's contract deployment tools to create ERC721 or ERC1155 contracts easily.
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 onboardingconst wallet = inAppWallet({auth: {options: ["google", "apple", "facebook", "email"],},});// Authentication componentfunction 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 connectedreturn (<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"><ConnectButtonclient={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>);}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 processconst 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 monitoringlocalStorage.setItem("currentOnrampSession",JSON.stringify({id: onrampSession.id,provider,timestamp: Date.now(),}),);// Open onramp in new window/tabwindow.open(onrampSession.link, "_blank");// Start monitoring the sessionmonitorOnrampAndMint(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 secondssetTimeout(() => 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 selectionbreak;}} 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 transactionconst mintTransaction = mintTo({contract: nftContract,to: account.address,nft: NFT_METADATA,});// Execute mint transactionconst result = await sendTransaction({transaction: mintTransaction,account,});console.log("NFT minted successfully:", result.transactionHash);setState({step: "success",mintTxHash: result.transactionHash,});// Clean up session storagelocalStorage.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"><imgsrc={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) => (<divkey={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" && (<SuccessSteptxHash={state.mintTxHash!}nftMetadata={NFT_METADATA}/>)}{state.step === "error" && (<ErrorSteperror={state.error!}onRetry={() => setState({ step: "payment" })}/>)}</div>);}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) => (<buttonkey={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-leadingproviders. Your payment information is never stored on ourservers.</p></div></div>);}function ProcessingStep({provider,}: {provider: "stripe" | "coinbase" | "transak";}) {const [currentStep, setCurrentStep] = useState<"payment" | "confirmation" | "minting">("payment");// Auto-progress through steps for demo purposesuseEffect(() => {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"><divclassName={`flex items-center space-x-3 p-3 rounded-lg ${currentStep === "payment"? "bg-blue-50 border border-blue-200": "bg-gray-50"}`}><divclassName={`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><divclassName={`flex items-center space-x-3 p-3 rounded-lg ${currentStep === "confirmation"? "bg-blue-50 border border-blue-200": "bg-gray-50"}`}><divclassName={`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><divclassName={`flex items-center space-x-3 p-3 rounded-lg ${currentStep === "minting"? "bg-blue-50 border border-blue-200": "bg-gray-50"}`}><divclassName={`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 updatewhen 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 onthe blockchain. This usually takes 30-60 seconds.</p></div>)}</div>);}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 yourwallet.</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><ahref={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"><buttononClick={() => window.location.reload()}className="w-full bg-green-500 text-white py-2 px-4 rounded-lg font-semibold">Buy Another NFT</button><buttononClick={() => {// Share functionalitynavigator.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"><buttononClick={onRetry}className="w-full bg-blue-500 text-white py-2 px-4 rounded-lg font-semibold">Try Again</button><buttononClick={() => window.location.reload()}className="w-full border border-gray-300 text-gray-700 py-2 px-4 rounded-lg">Start Over</button></div></div>);}Put it all together in your main application:
import { ThirdwebProvider } from "thirdweb/react";// Import all the components we defined earlierimport {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;