Build Your First Checkout Funnel
Create a complete 3-step funnel: Checkout → Upsell → Thank You . Learn how data flows automatically between steps!
What you’ll learn:
✅ Setup a React plugin project
✅ Create multiple pages
✅ Pass data between pages
✅ Use static resources (for upsells/offers)
✅ Deploy to TagadaPay
Related Guides:
Time : ~20 minutes | Difficulty : Beginner
🎯 What You’ll Build
A 3-page checkout funnel:
Checkout Page - Collect payment → Create order
Upsell Page - Show special offer using static resources
Thank You Page - Display order confirmation
Data Flow :
Checkout → (order data) → Upsell → (order + upsell) → Thank You
Step 1: Create Your Project
# Create project
npm create vite@latest my-checkout -- --template react-ts
cd my-checkout
# Install SDK
npm install @tagadapay/plugin-sdk wouter
# Install dependencies
npm install
Step 2: Create Plugin Manifest
The manifest tells TagadaPay about your plugin’s pages and what data they need.
Create plugin.manifest.json in your project root:
{
"name" : "My First Checkout" ,
"pluginId" : "my-first-checkout" ,
"version" : "1.0.0" ,
"description" : "A simple checkout with upsell" ,
"mode" : "direct-mode" ,
"category" : "checkout" ,
"tags" : [ "checkout" , "upsell" ],
"configuration" : {
"schema" : "./config/schema.json" ,
"uiSchema" : "./config/ui-schema.json" ,
"presets" : [
{
"id" : "default" ,
"name" : "Default" ,
"description" : "Default configuration" ,
"config" : "./config/default.config.json"
}
]
},
"pages" : [
{
"path" : "/checkout" ,
"features" : [
{
"type" : "checkout" ,
"requirements" : [
{
"resource" : "checkoutSession" ,
"from" : [
{
"name" : "checkoutToken" ,
"type" : "query"
}
]
}
]
}
],
"remappable" : true
},
{
"path" : "/upsell" ,
"features" : [
{
"type" : "offer" ,
"requirements" : [
{
"resource" : "order" ,
"from" : [
{
"name" : "id" ,
"type" : "context"
}
]
},
{
"resource" : "offer" ,
"from" : [
{
"name" : "id" ,
"type" : "static"
}
]
}
]
}
],
"remappable" : true
},
{
"path" : "/thankyou/:orderId" ,
"features" : [
{
"type" : "thankyou" ,
"requirements" : [
{
"resource" : "order" ,
"from" : [
{
"name" : "id" ,
"type" : "path"
}
]
}
]
}
],
"remappable" : true
}
],
"router" : {
"basePath" : "/" ,
"matcher" : ".*" ,
"excluder" : null
},
"requirements" : {
"sdk" : "^2.2.0" ,
"sdkVersion" : "v2"
}
}
📖 Understanding the Manifest
Basic Info:
name, pluginId, version - Identify your plugin
mode: "direct-mode" - Plugin runs directly (no iframe)
category - Helps organize in marketplace
Pages Array:
Each page defines:
path - The URL path (e.g., /checkout, /upsell/:id)
features - What type of page it is (checkout, offer, thankyou)
requirements - What data this page needs
remappable - Allows custom URLs (highly recommended!)
Requirements Explained: {
"resource" : "order" , // What resource you need
"from" : [ // Where to get it from
{
"name" : "id" , // The field name
"type" : "context" // Source type
}
]
}
Resource Types:
query - From URL query params (?checkoutToken=abc)
path - From URL path (/thankyou/:orderId)
context - From previous step’s funnel context
static - From CRM configuration (offers, products, etc.)
🎯 Static Resources Explained
What are Static Resources? Static resources are IDs configured by merchants in the CRM. They let you build flexible plugins where merchants choose which products/offers to show without changing your code . Example Use Case:
You build an upsell page. Different merchants want to show different products. Instead of hardcoding product IDs, you use a static resource!In Manifest: {
"resource" : "offer" , // Name it "offer"
"from" : [{
"name" : "id" , // It's an ID
"type" : "static" // From CRM config
}]
}
In Your Code: // Access the offer ID configured by merchant
const offerId = funnel . context ?. static ?. offer ?. id ;
For Local Testing:
Create config/resources.static.json:{
"offer" : {
"id" : "offer_test123"
}
}
The SDK automatically loads this file in local development!
🔄 Configuration & Presets
Configuration lets merchants customize your plugin without code changes. Structure:
schema.json - Defines what settings are available
uiSchema.json - How settings appear in CRM UI
presets - Pre-made configurations merchants can choose
Example Preset: {
"id" : "premium" ,
"name" : "Premium Theme" ,
"description" : "High-end checkout design" ,
"config" : "./config/premium.config.json"
}
Each config file should have metadata: {
"meta" : {
"id" : "premium" ,
"name" : "Premium Theme" ,
"description" : "High-end checkout design"
},
"branding" : {
"primaryColor" : "#000000" ,
"companyName" : "Premium Store"
}
}
Access in code with usePluginConfig() hook!
Step 3: Setup Provider
The TagadaProvider wraps your app and provides all functionality.
Create src/main.tsx:
import React from 'react' ;
import ReactDOM from 'react-dom/client' ;
import { TagadaProvider } from '@tagadapay/plugin-sdk/v2' ;
import App from './App' ;
import './index.css' ;
ReactDOM . createRoot ( document . getElementById ( 'root' ) ! ). render (
< React.StrictMode >
< TagadaProvider >
< App />
</ TagadaProvider >
</ React.StrictMode >
);
That’s it! TagadaProvider automatically:
✅ Creates a session
✅ Initializes the funnel
✅ Enables debug mode (local dev)
✅ Handles redirects
No manual initialization needed!
Step 4: Build Checkout Page
Create src/pages/CheckoutPage.tsx:
import { useState } from 'react' ;
import { useFunnel , useCheckout , useCurrency } from '@tagadapay/plugin-sdk/v2' ;
export default function CheckoutPage () {
const funnel = useFunnel ();
const { data : checkout , isLoading } = useCheckout ();
const { formatMoney } = useCurrency ();
const [ email , setEmail ] = useState ( '' );
const [ name , setName ] = useState ( '' );
const handlePayment = async ( e : React . FormEvent ) => {
e . preventDefault ();
// Create mock order
const order = {
id: `ord_ ${ Date . now () } ` ,
amount: checkout ?. total || 4900 ,
currency: checkout ?. currency || 'USD' ,
items: checkout ?. lineItems || []
};
const customer = {
id: `cus_ ${ Date . now () } ` ,
email ,
name
};
// Navigate to upsell page with order data
await funnel . next ({
type: 'payment_success' ,
data: {
resources: {
order , // ← Order data flows to next page
customer // ← Customer data flows too
}
}
});
// SDK auto-redirects to upsell page!
};
if ( isLoading || ! funnel . context ) {
return < div className = "loading" > Loading checkout... </ div > ;
}
return (
< div className = "checkout-page" >
< h1 > Checkout </ h1 >
{ checkout && (
< div className = "order-summary" >
< h2 > Order Summary </ h2 >
< div className = "line-items" >
{ checkout . lineItems ?. map (( item : any ) => (
< div key = { item . id } className = "line-item" >
< span > { item . quantity } x { item . title } </ span >
< span > { formatMoney ( item . price * item . quantity , checkout . currency ) } </ span >
</ div >
)) }
</ div >
< div className = "total" >
< strong > Total: </ strong > { formatMoney ( checkout . total , checkout . currency ) }
</ div >
</ div >
) }
< form onSubmit = { handlePayment } className = "checkout-form" >
< div className = "form-group" >
< label htmlFor = "email" > Email </ label >
< input
id = "email"
type = "email"
value = { email }
onChange = { ( e ) => setEmail ( e . target . value ) }
placeholder = "[email protected] "
required
/>
</ div >
< div className = "form-group" >
< label htmlFor = "name" > Full Name </ label >
< input
id = "name"
type = "text"
value = { name }
onChange = { ( e ) => setName ( e . target . value ) }
placeholder = "John Doe"
required
/>
</ div >
< div className = "form-group" >
< label htmlFor = "card" > Card Number (Test: 4242...) </ label >
< input
id = "card"
type = "text"
defaultValue = "4242424242424242"
placeholder = "4242 4242 4242 4242"
required
/>
</ div >
< button type = "submit" className = "btn-primary" >
Pay { formatMoney ( checkout ?. total || 4900 , checkout ?. currency || 'USD' ) }
</ button >
</ form >
</ div >
);
}
Step 5: Build Upsell Page (Using Static Resources!)
This page shows an offer. The merchant configures which offer in the CRM using static resources .
Create src/pages/UpsellPage.tsx:
import { useFunnel , useOffer , useCurrency } from '@tagadapay/plugin-sdk/v2' ;
export default function UpsellPage () {
const funnel = useFunnel ();
const { formatMoney } = useCurrency ();
// 🎯 Get offer ID from STATIC resource (configured by merchant)
const offerId = funnel . context ?. static ?. offer ?. id ;
// 📦 Get order from PREVIOUS page (from context)
const order = funnel . context ?. resources ?. order ;
const customer = funnel . context ?. resources ?. customer ;
// Fetch offer details
const { data : offers } = useOffer ({
offerIds: offerId ? [ offerId ] : [],
orderId: order ?. id
});
const offer = offers ?.[ 0 ];
const handleAccept = async () => {
// Add upsell to order and go to thank you
await funnel . next ({
type: 'offer_accepted' ,
data: {
resources: {
order: {
... order ,
upsell: {
id: offer . id ,
amount: offer . price
}
},
customer
}
}
});
};
const handleDecline = async () => {
// Skip upsell, go to thank you
await funnel . next ({
type: 'offer_declined' ,
data: {
resources: {
order ,
customer
}
}
});
};
if ( ! funnel . context || ! offerId ) {
return < div className = "loading" > Loading... </ div > ;
}
if ( ! offer ) {
// No offer configured - skip to thank you
handleDecline ();
return < div className = "loading" > Loading... </ div > ;
}
return (
< div className = "upsell-page" >
< h1 > 🎉 Special Offer Just For You! </ h1 >
< div className = "offer-card" >
< img src = { offer . imageUrl } alt = { offer . title } />
< h2 > { offer . title } </ h2 >
< p > { offer . description } </ p >
< div className = "pricing" >
< div className = "price" >
< span className = "original" > { formatMoney ( offer . compareAtPrice , order . currency ) } </ span >
< span className = "sale" > { formatMoney ( offer . price , order . currency ) } </ span >
</ div >
< span className = "savings" >
Save { formatMoney ( offer . compareAtPrice - offer . price , order . currency ) } !
</ span >
</ div >
< button onClick = { handleAccept } className = "btn-primary" >
Yes! Add To My Order
</ button >
< button onClick = { handleDecline } className = "btn-secondary" >
No Thanks, Continue
</ button >
</ div >
< div className = "order-info" >
< p > Your order: { formatMoney ( order . amount , order . currency ) } </ p >
< p > Customer: { customer ?. email } </ p >
</ div >
</ div >
);
}
🎯 How Static Resources Work
Step by Step:
In Manifest - Declare you need an “offer” resource:
{
"resource" : "offer" ,
"from" : [{ "name" : "id" , "type" : "static" }]
}
In Code - Access it from funnel.context.static:
const offerId = funnel . context ?. static ?. offer ?. id ;
Merchant Configures - In CRM, they select which offer to show:
Funnel Step → Upsell Page → Static Resources → Offer → (select from dropdown)
For Local Testing - Create config/resources.static.json:
{
"offer" : {
"id" : "offer_test123"
}
}
Why This is Powerful:
Same plugin code works for all merchants
Merchants customize without touching code
Easy A/B testing (change offer ID in CRM)
No redeployment needed to change offers
📦 Context vs Static Resources
Context Resources (from previous pages):// Data passed from checkout page
const order = funnel . context ?. resources ?. order ;
const customer = funnel . context ?. resources ?. customer ;
✅ Dynamic data from previous steps
✅ Changes per user/session
✅ Example: order details, customer info
Static Resources (configured in CRM):// Configured by merchant in CRM
const offerId = funnel . context ?. static ?. offer ?. id ;
✅ Fixed per funnel configuration
✅ Same for all users
✅ Example: offer IDs, product IDs, variant IDs
Step 6: Build Thank You Page
Create src/pages/ThankYouPage.tsx:
import { useFunnel , useCurrency } from '@tagadapay/plugin-sdk/v2' ;
import { useParams } from 'wouter' ;
export default function ThankYouPage () {
const { orderId } = useParams <{ orderId : string }>();
const funnel = useFunnel ();
const { formatMoney } = useCurrency ();
// Get data from previous pages
const order = funnel . context ?. resources ?. order ;
const customer = funnel . context ?. resources ?. customer ;
const upsell = order ?. upsell ;
if ( ! funnel . context || ! order ) {
return < div className = "loading" > Loading... </ div > ;
}
const totalAmount = order . amount + ( upsell ?. amount || 0 );
return (
< div className = "thankyou-page" >
< div className = "success-icon" > ✅ </ div >
< h1 > Thank You for Your Purchase! </ h1 >
< p > Order # { order . id } </ p >
< div className = "order-details" >
< h2 > Order Summary </ h2 >
< div className = "order-line" >
< span > Original Order </ span >
< span > { formatMoney ( order . amount , order . currency ) } </ span >
</ div >
{ upsell && (
< div className = "order-line upsell" >
< span > ✨ Special Offer </ span >
< span > { formatMoney ( upsell . amount , order . currency ) } </ span >
</ div >
) }
< div className = "order-line total" >
< strong > Total Paid </ strong >
< strong > { formatMoney ( totalAmount , order . currency ) } </ strong >
</ div >
</ div >
< div className = "customer-info" >
< p >< strong > Email: </ strong > { customer ?. email } </ p >
< p >< strong > Name: </ strong > { customer ?. name } </ p >
</ div >
< p className = "confirmation" >
A confirmation email has been sent to { customer ?. email }
</ p >
< button onClick = { () => window . location . href = '/' } className = "btn-primary" >
Continue Shopping
</ button >
</ div >
);
}
Step 7: Setup Routing
Create src/App.tsx:
import { Route , Switch } from 'wouter' ;
import CheckoutPage from './pages/CheckoutPage' ;
import UpsellPage from './pages/UpsellPage' ;
import ThankYouPage from './pages/ThankYouPage' ;
export default function App () {
return (
< Switch >
< Route path = "/checkout" component = { CheckoutPage } />
< Route path = "/upsell" component = { UpsellPage } />
< Route path = "/thankyou/:orderId" component = { ThankYouPage } />
< Route path = "/" component = { CheckoutPage } />
</ Switch >
);
}
Step 8: Create Config for Local Testing
Create config/resources.static.json:
{
"offer" : {
"id" : "offer_test123"
}
}
What this does:
When developing locally, the SDK automatically loads this file. This lets you test your upsell page without configuring anything in the CRM!The SDK detects local environment and loads static resources from this file. In production, resources come from the CRM configuration.
Step 9: Add Basic Styles
Create src/index.css:
* {
margin : 0 ;
padding : 0 ;
box-sizing : border-box ;
}
body {
font-family : system-ui , -apple-system , sans-serif ;
line-height : 1.6 ;
color : #333 ;
}
.checkout-page ,
.upsell-page ,
.thankyou-page {
max-width : 600 px ;
margin : 0 auto ;
padding : 2 rem ;
}
h1 {
font-size : 2 rem ;
margin-bottom : 1 rem ;
}
h2 {
font-size : 1.5 rem ;
margin-bottom : 0.75 rem ;
}
/* Order Summary */
.order-summary {
background : #f5f5f5 ;
padding : 1.5 rem ;
border-radius : 8 px ;
margin : 2 rem 0 ;
}
.line-items {
margin : 1 rem 0 ;
}
.line-item {
display : flex ;
justify-content : space-between ;
padding : 0.5 rem 0 ;
border-bottom : 1 px solid #ddd ;
}
.total {
display : flex ;
justify-content : space-between ;
padding-top : 1 rem ;
font-size : 1.25 rem ;
}
/* Forms */
.checkout-form {
margin-top : 2 rem ;
}
.form-group {
margin-bottom : 1.5 rem ;
}
.form-group label {
display : block ;
margin-bottom : 0.5 rem ;
font-weight : 500 ;
}
.form-group input {
width : 100 % ;
padding : 0.75 rem ;
border : 1 px solid #ddd ;
border-radius : 6 px ;
font-size : 1 rem ;
}
.form-group input :focus {
outline : none ;
border-color : #0070f3 ;
box-shadow : 0 0 0 3 px rgba ( 0 , 112 , 243 , 0.1 );
}
/* Buttons */
.btn-primary ,
.btn-secondary {
width : 100 % ;
padding : 1 rem ;
border : none ;
border-radius : 6 px ;
font-size : 1 rem ;
font-weight : 600 ;
cursor : pointer ;
margin-top : 0.5 rem ;
transition : all 0.2 s ;
}
.btn-primary {
background : #0070f3 ;
color : white ;
}
.btn-primary:hover {
background : #0051cc ;
transform : translateY ( -1 px );
box-shadow : 0 4 px 12 px rgba ( 0 , 112 , 243 , 0.3 );
}
.btn-secondary {
background : transparent ;
color : #666 ;
border : 1 px solid #ddd ;
}
.btn-secondary:hover {
background : #f5f5f5 ;
}
/* Upsell Page */
.offer-card {
background : white ;
border : 2 px solid #0070f3 ;
border-radius : 12 px ;
padding : 2 rem ;
margin : 2 rem 0 ;
text-align : center ;
box-shadow : 0 4 px 20 px rgba ( 0 , 112 , 243 , 0.1 );
}
.offer-card img {
max-width : 100 % ;
border-radius : 8 px ;
margin-bottom : 1 rem ;
}
.pricing {
margin : 1.5 rem 0 ;
}
.price {
display : flex ;
justify-content : center ;
align-items : center ;
gap : 1 rem ;
margin-bottom : 0.5 rem ;
}
.price .original {
text-decoration : line-through ;
color : #999 ;
font-size : 1.25 rem ;
}
.price .sale {
color : #0070f3 ;
font-size : 2 rem ;
font-weight : bold ;
}
.savings {
display : inline-block ;
background : #10b981 ;
color : white ;
padding : 0.5 rem 1 rem ;
border-radius : 20 px ;
font-weight : 600 ;
}
.order-info {
text-align : center ;
color : #666 ;
font-size : 0.9 rem ;
margin-top : 2 rem ;
padding-top : 2 rem ;
border-top : 1 px solid #ddd ;
}
/* Thank You Page */
.thankyou-page {
text-align : center ;
}
.success-icon {
font-size : 4 rem ;
margin-bottom : 1 rem ;
}
.order-details {
background : #f5f5f5 ;
padding : 2 rem ;
border-radius : 8 px ;
margin : 2 rem 0 ;
text-align : left ;
}
.order-line {
display : flex ;
justify-content : space-between ;
padding : 0.75 rem 0 ;
border-bottom : 1 px solid #ddd ;
}
.order-line.upsell {
color : #10b981 ;
font-weight : 500 ;
}
.order-line.total {
border-bottom : none ;
padding-top : 1 rem ;
font-size : 1.25 rem ;
}
.customer-info {
text-align : left ;
background : #f9fafb ;
padding : 1 rem ;
border-radius : 6 px ;
margin : 1 rem 0 ;
}
.confirmation {
color : #666 ;
margin : 2 rem 0 ;
}
/* Loading */
.loading {
text-align : center ;
padding : 4 rem ;
color : #666 ;
}
Step 10: Test Locally
# Start development server
npm run dev
# Visit http://localhost:5173/checkout
Test Checkout
Fill in email and name
Use card: 4242424242424242
Click “Pay”
Should redirect to upsell page ✅
Test Upsell
Should show your test offer (from resources.static.json)
Order info from checkout should be visible
Click “Accept” or “Decline”
Should redirect to thank you page ✅
Test Thank You
Should show order confirmation
Should show upsell if accepted
Should display customer email
Check the Console! The SDK logs helpful debug information:
✅ Funnel Initialized - Funnel is ready
🚀 Auto-redirecting to: - Navigation happening
Check funnel.context to see all available data
Step 11: Deploy to TagadaPay
# Build your plugin
npm run build
# Install CLI
npm install -g @tagadapay/cli
# Login
tgdcli login
# Deploy
tgdcli deploy
Follow the prompts to deploy your plugin!
Go to Funnels → Create new funnel
Add Checkout Step → Select your plugin → /checkout page
Add Upsell Step → Select your plugin → /upsell page
⚠️ Important : Configure Static Resources:
Resource: offer
Select an offer from the dropdown
Add Thank You Step → Select your plugin → /thankyou/:orderId page
Save & Test!
Don’t forget to configure the static resource for the upsell page! Without it, the upsell won’t show. In the CRM:
Funnel → Upsell Step → Static Resources section
Select “offer” → Choose an offer from your catalog
🎉 Congratulations!
You’ve built a complete checkout funnel with:
✅ Multi-step navigation
✅ Automatic data flow between pages
✅ Static resources for flexible configuration
✅ Professional UI
✅ Deployed to production
📚 What You Learned
Funnel Navigation Pass data between steps using funnel.next() with resources
Static Resources Let merchants configure offers/products without code changes
Context Resources Access data from previous steps via funnel.context.resources
Auto-redirect SDK handles navigation automatically
Next Steps
Common Questions
How do I add more static resources?
In Manifest - Add to requirements:{
"resource" : "product" ,
"from" : [{ "name" : "id" , "type" : "static" }]
}
In Code - Access it:const productId = funnel . context ?. static ?. product ?. id ;
For Local Testing :{
"offer" : { "id" : "offer_123" },
"product" : { "id" : "prod_456" }
}
Can I pass custom data between pages?
Yes! Add anything to resources:await funnel . next ({
data: {
resources: {
order: { ... },
customer: { ... },
customData: {
selectedPlan: 'premium' ,
addons: [ 'support' , 'backup' ],
anything: 'you want!'
}
}
}
});
Access on next page: const customData = funnel . context ?. resources ?. customData ;
What if merchant doesn't configure static resource?
How do I debug funnel context?
In your component: useEffect (() => {
console . log ( 'Funnel Context:' , funnel . context );
console . log ( 'Static Resources:' , funnel . context ?. static );
console . log ( 'Context Resources:' , funnel . context ?. resources );
}, [ funnel . context ]);
Or use the SDK debug drawer (in development mode)!