Best for
Developers who need a polished payment modal UI scaffold and want to drop real Stripe Elements into it with minimal rework.
Stack
A ready-made Payment Modal UI you can fork, run, and customize with the prompt pack below.
What's actually inside
The honest engineer's breakdown — what the Payment Modaltemplate does, how it's wired, and where it's opinionated.
The Payment Modal template centres on a shadcn/ui Dialog that walks buyers through a three-step checkout: Summary → Payment → Confirmation. The PaymentModal Dialog wraps everything; inside, a StepIndicator shows progress dots so buyers always know where they are. ProductSummaryCard is a read-only order recap showing item, quantity, subtotal, tax, and total — it recalculates dynamically if a PromoCodeInput discount is applied. PaymentFormStep holds the card input fields (number, expiry, CVC) with validation states — these are mock fields by default, ready to be replaced by real Stripe Elements with the medium prompt below. OrderConfirmationStep is a success screen with an animated checkmark and an order reference number.
The honest caveat: the card fields in PaymentFormStep are entirely mock UI — there is no real payment processing wired in. This is the correct starting point (you add Stripe Elements to production, not a demo), but it means the template shows a clean UX flow without any actual charge capability. A second caveat is that the StepIndicator uses display toggling rather than CSS transitions between steps, so there is no animation on step change by default — the fix prompt resolves this.
Another watch point specific to this template: NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is inlined at build time. If you add it to Vercel after the first deploy, you must trigger a full redeploy or the key will be undefined in the browser — a silent failure that shows no error until you check the console.
Key UI components
PaymentModalshadcn/ui Dialog shell that wraps the full three-step checkout flow
ProductSummaryCardRead-only order recap showing item, quantity, subtotal, tax, and total
PaymentFormStepCard input fields (number, expiry, CVC) with validation states — mock by default, replaceable with Stripe Elements
OrderConfirmationStepSuccess screen with animated checkmark and order reference number
StepIndicatorProgress dots showing Summary → Payment → Confirmation step position
PromoCodeInputText field and Apply button with discount feedback and ProductSummaryCard total recalculation
Libraries it leans on
shadcn/ui DialogPaymentModal shell — the entire checkout flow lives inside this component
shadcn/ui Input / LabelPaymentFormStep card input fields and labels
shadcn/ui ProgressStepIndicator progress dots between checkout steps
Fork it and get it running
Forking the Payment Modal template takes about 5 minutes. You will open the community page, step through the modal flow, update branding for free, then publish to a live Vercel URL.
Open the community page and fork
Go to https://v0.dev/chat/community/uhV0pHveOdw in your browser. Click the Fork button in the top-right area of the community page. V0 copies the Payment Modal template into a new Project in your account. A free V0 account is sufficient — no paid plan required to fork.
Tip: Make sure you are signed in to V0 before clicking Fork so the project saves to your account.
You should see: A new V0 Project opens with the Payment Modal code loaded and the Vercel Sandbox preview starting.
Step through the modal flow in preview
Wait for the Vercel Sandbox preview to load. Click the trigger button to open PaymentModal. Step through Summary → Payment → Confirmation to verify all three steps render. Check that the StepIndicator dots advance correctly, PromoCodeInput is visible, and OrderConfirmationStep shows the animated checkmark on the final step.
Tip: If the modal does not open, the trigger button may be outside the default viewport — scroll down in the preview panel.
You should see: All three checkout steps (Summary, Payment, Confirmation) render correctly and the StepIndicator advances through each.
Update product details and branding in Design Mode
Press Option+D (Mac) or Alt+D (Windows) to open Design Mode. Click the product name and price in ProductSummaryCard to edit them directly on the canvas. Update the brand colour used in the PaymentModal header and the CTA button without spending any prompt credits. This is the fastest way to personalise the modal before adding real Stripe integration.
You should see: Your product name, price, and brand colour appear in the modal preview without using credits.
Replace mock card fields with Stripe Elements
In the V0 chat, paste the medium-difficulty 'Replace mock card fields with Stripe Elements' prompt from the Prompt Pack below. V0 will install @stripe/react-stripe-js, wrap PaymentFormStep in an Elements provider, and replace the mock inputs with a real PaymentElement. You will need to set NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY in the Vars panel (click the Vars icon in the V0 sidebar) before testing.
Tip: Set the Stripe publishable key in the Vars panel before publishing — adding it after the first deploy requires a full redeploy to take effect.
You should see: PaymentFormStep shows a real Stripe PaymentElement instead of mock card fields.
Publish to a live Vercel URL
Click Share in the V0 top navigation, open the Publish tab, and click 'Publish to Production'. The deployment takes approximately 30 seconds. Once complete you will see a live .vercel.app URL. Test the full modal flow end-to-end — use Stripe's test card 4242 4242 4242 4242 with any future expiry and CVC to confirm the PaymentElement processes correctly in test mode.
You should see: Payment Modal is live, PaymentElement loads, and a test card payment completes with OrderConfirmationStep showing.
Set STRIPE_SECRET_KEY and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY on Vercel
Go to your Vercel Dashboard, open the project, and navigate to Settings → Environment Variables. Add STRIPE_SECRET_KEY (your sk_live_ or sk_test_ key — server-only, no NEXT_PUBLIC_ prefix) and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY (your pk_ key — used by the Stripe Elements client). After adding both variables, click Redeploy in the Deployments tab to rebuild with the keys injected — the NEXT_PUBLIC_ variable will be undefined on any build that ran before you added it.
You should see: Payment Modal connects to real Stripe in production mode with both environment variables injected into the build.
The prompt pack
Copy-paste these straight into v0's chat to customize the Payment Modaltemplate. Each one names this template's own components — no generic filler.
Update product details in ProductSummaryCard
Replaces the placeholder product in ProductSummaryCard with your real item details and wires dynamic quantity-based total recalculation.
In the payment modal template, update the ProductSummaryCard to show my product details: name '[product name]', unit price $[price], a quantity selector (1–10), and a tax rate of [X]%. Recalculate the total dynamically in ProductSummaryCard when the quantity changes. If a PromoCodeInput discount is active, apply it to the subtotal before adding tax.
Add dark mode to PaymentModal
Adds a header toggle that switches the entire PaymentModal — all steps and cards — between light and dark themes using Tailwind dark: classes.
Add a dark/light mode toggle to the payment modal template. The toggle should be a sun/moon icon button in the PaymentModal header. Use Tailwind's dark: variant classes and a data-theme attribute on the PaymentModal root element so the PaymentModal, ProductSummaryCard, PaymentFormStep, and OrderConfirmationStep all switch theme correctly when toggled.
Add promo code discount logic to PromoCodeInput
Adds real discount logic to PromoCodeInput with two codes, a ProductSummaryCard discount line, and toast feedback for invalid attempts.
In the payment modal PromoCodeInput, implement promo code validation: accept code SAVE10 (10% off) and FLAT5 ($5 off). When a valid code is entered and Apply is clicked, show the discount as a separate line item in ProductSummaryCard and recalculate the total (apply discount to subtotal, then add tax). Show a shadcn/ui toast with 'Invalid code' for unrecognised inputs. Store the applied code and discount in component state so it persists across StepIndicator navigation.
Replace mock card fields with Stripe Elements
Replaces all mock card inputs in PaymentFormStep with a real Stripe PaymentElement backed by a server-side payment intent route.
Replace the PaymentFormStep mock card inputs in the payment modal with real Stripe Elements. Install @stripe/react-stripe-js and @stripe/stripe-js. Wrap the PaymentFormStep in an Elements provider loaded with loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY). Replace the card number, expiry, and CVC fields with a single <PaymentElement> component. On the 'Pay' button submit, call stripe.confirmPayment() pointing at a /api/create-payment-intent route. Show a loading spinner on the PaymentFormStep submit button while the payment is processing.
Create /api/create-payment-intent route handler
Creates the server-side /api/create-payment-intent route that the PaymentFormStep Stripe Elements client calls to initialise a real payment.
Create a Next.js App Router route handler at /api/create-payment-intent/route.ts. It should accept a POST request with a JSON body containing amount (in cents) and currency (e.g. 'usd'). Call stripe.paymentIntents.create() using STRIPE_SECRET_KEY (server-only, no NEXT_PUBLIC_ prefix). Return { clientSecret: paymentIntent.client_secret } as JSON. Add error handling: return a 400 Response if amount is missing or less than 50, and a 500 Response with the Stripe error message for any API errors. This clientSecret is consumed by the PaymentFormStep Stripe Elements provider.Add Stripe webhook to confirm order status
Adds a production-ready Stripe webhook handler that verifies signatures and updates Supabase order status on successful payment.
Add a Stripe webhook handler at /api/webhooks/stripe/route.ts. Read the raw request body with const body = await request.text() — never use request.json() here as it breaks Stripe's HMAC signature verification. Verify the signature with stripe.webhooks.constructEvent(body, request.headers.get('stripe-signature'), process.env.STRIPE_WEBHOOK_SECRET). Handle the payment_intent.succeeded event by updating the order status in a Supabase orders table using SUPABASE_SERVICE_ROLE_KEY. Return Response.json({ received: true }) with status 200 immediately — do not await any slow operations before returning.Gotchas when you extend it
The failures people actually hit when they push this template past its defaults — and the exact fix for each.
Unexpected token '<', "<!DOCTYPE "... is not valid JSON when installing shadcn Dialog locallyWhy: The shadcn registry returns an HTML error page instead of JSON for the dialog component URL. This is a known registry intermittent failure (GitHub shadcn-ui/ui Issue #6459) and affects several components beyond Dialog.
Fix: Use the V0 GitHub integration to get the component source directly in a branch rather than running npx shadcn add. Copy the Dialog source from V0's file tree if you need it locally.
Copy the shadcn Dialog component source directly into my local components/ui/dialog.tsx file rather than using the npx shadcn add command, so I avoid the registry JSON parse error.
Stripe webhook signature verification failedWhy: The route handler uses request.json() to parse the body. JSON parsing transforms the raw bytes, which breaks Stripe's HMAC verification. This is the single most common Stripe integration failure — Stripe requires the raw body bytes, not a parsed object.
Fix: Use const body = await request.text() before calling stripe.webhooks.constructEvent(). Never call request.json() in the webhook handler at /api/webhooks/stripe/route.ts.
Fix the Stripe webhook handler in /api/webhooks/stripe/route.ts to use await request.text() instead of request.json() so the raw body is preserved for signature verification with stripe.webhooks.constructEvent().
PaymentElement does not render in Vercel Sandbox previewWhy: The @stripe/react-stripe-js package resolution sometimes fails via esm.sh in the V0 preview sandbox. The import may silently fall back to undefined, so useStripe() returns null and the PaymentElement renders nothing.
Fix: Deploy to production first before testing Stripe Elements — the Vercel Sandbox preview is not a reliable environment for Stripe's client library. Test on the live deployed URL using Stripe test cards.
Add a conditional render guard around the PaymentElement in PaymentFormStep: only render it if the stripe object returned by useStripe() is not null, and show a shadcn/ui Skeleton loading placeholder otherwise.
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is undefined on Vercel despite being set in the dashboardWhy: NEXT_PUBLIC_ variables are inlined at build time, not at runtime. If you added the variable after the first deployment, the existing build does not contain it — the variable is undefined in the browser. You must trigger a new build.
Fix: Go to the Vercel Dashboard → your project → Deployments → click Redeploy on the latest deployment. Do not just save the variable and wait — a full redeploy is required.
Add a check at the top of the payment modal Stripe provider that console.warns 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set' if the variable is undefined at load time, so the missing key is immediately visible in the browser console.
StepIndicator does not animate transitions between PaymentFormStep and OrderConfirmationStepWhy: V0's generated step transition uses display:none / display:block toggling, which cannot be animated with CSS transitions. Tailwind opacity and translate-x transitions require the element to remain mounted in the DOM.
Fix: Keep both PaymentFormStep and OrderConfirmationStep mounted and use Tailwind opacity-0/opacity-100 combined with translate-x transitions, controlled by the current step index state.
Replace the display toggle in the payment modal StepIndicator with opacity and translate-x Tailwind transition classes (transition-all duration-300) so the PaymentFormStep and OrderConfirmationStep animate smoothly when the step changes.
Template vs. custom — the honest call
A forked template gets you far, fast. Here's where it holds up, and where you'll outgrow it.
The template is enough when
- SaaS or digital product with a single-price checkout that needs a branded modal instead of a Stripe Checkout redirect
- Developer prototyping the UX flow before integrating a real payment provider
- Teams that want to keep buyers on-site during checkout rather than redirecting to an external page
- Projects where the three-step Summary → Payment → Confirmation flow matches the product checkout model exactly
Go custom when
- You sell subscriptions with trials, metered billing, or plan upgrades that require Stripe Billing
- You need PCI-compliant card vaulting, saved payment methods, or one-click reorder
- The checkout requires multi-step shipping address capture, carrier rate quoting, or tax calculation via TaxJar or Avalara
- You need split payments, invoicing, or B2B purchase orders with net-30 terms
If you need the payment modal wired to real Stripe Billing, webhook order management, and production deployment, RapidDev builds that full stack — see rapidevelopers.com.
Frequently asked questions
Is the Payment Modal v0 template free to use?
Yes. V0 community templates are free to fork with any V0 account. You can fork, preview, and run the template locally without spending paid credits until you start prompting V0 for changes.
Can I use this template commercially — for example, in a client's checkout?
Yes. V0 community templates are MIT-licensed. You can use, modify, and deploy the code commercially without attribution or royalties. When you add Stripe, make sure you comply with Stripe's own usage policies for live transactions.
Why does the PaymentElement break or not show in the preview?
The Vercel Sandbox preview is not a reliable environment for @stripe/react-stripe-js — the esm.sh module resolution sometimes fails silently. Deploy to production first and test the PaymentElement on the live Vercel URL using Stripe's test card 4242 4242 4242 4242.
How do I connect real Stripe payments to the PaymentFormStep?
Use the two medium/advanced prompts from the Prompt Pack: 'Replace mock card fields with Stripe Elements' to add the PaymentElement to PaymentFormStep, then 'Create /api/create-payment-intent route handler' to add the server-side intent creation. Set NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY and STRIPE_SECRET_KEY in the V0 Vars panel before testing.
Why does my Stripe webhook keep returning 'signature verification failed'?
The webhook handler is almost certainly using request.json() to read the body. JSON parsing alters the raw bytes that Stripe uses for HMAC verification. Change the handler to use const body = await request.text() before calling stripe.webhooks.constructEvent(). This is the single most common Stripe integration failure.
Why is NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY undefined even though I added it to Vercel?
NEXT_PUBLIC_ environment variables are inlined at build time, not runtime. If you added the key after the first deployment, you must trigger a new deployment from the Vercel Dashboard (Deployments → Redeploy) before the key is available in the browser.
Can RapidDev wire this payment modal to a full production checkout?
Yes. RapidDev connects V0 payment modal prototypes to real Stripe Billing, webhook order management, and Supabase backends — all production-ready. See rapidevelopers.com for details.
Outgrowing the template?
RapidDev turns v0 prototypes into production apps — real auth, database, and payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.