Checkout Session Management Guide
Overview
The checkout system has been migrated from URL query parameters to a session-based approach using localStorage. This provides better UX, security, and prepares for backend session management.
Before (❌ Old Approach)
/checkout?amount=571.95&product=Cricket+Bat+x1,Ball+x2&email=user@example.com
Problems:
- Ugly, long URLs
- Sensitive data visible in URL
- URLs are not shareable (security risk)
- Limited data that can be passed
- URL encoding issues
After (✅ New Approach)
/checkout
Benefits:
- ✅ Clean URLs
- ✅ Data not visible in URL
- ✅ More secure
- ✅ Can store complex data (images, metadata)
- ✅ Easy to extend with more fields
- ✅ Prepared for backend migration
Architecture
Current: LocalStorage-based Sessions
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ Shop Page │ ─────> │ localStorage │ ─────> │ Checkout Page│
│ (create) │ │ (session) │ │ (read) │
└──────────────┘ └───────────────┘ └──────────────┘
Future: Backend API Sessions
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ Shop Page │ ─────> │ Backend API │ <──── │ Checkout Page│
│ (create) │ POST │ (database) │ GET │ (read) │
└──────────────┘ └───────────────┘ └──────────────┘
Data Structure
CheckoutSessionData
interface CheckoutSessionData {
sessionId: string; // Unique session identifier
amount: number; // Total amount
items: CheckoutItem[]; // Array of items
email?: string; // Customer email (optional)
createdAt: number; // Timestamp (milliseconds)
expiresAt: number; // Expiration timestamp
}
CheckoutItem
interface CheckoutItem {
id?: string; // Optional product ID
name: string; // Product name
quantity: number; // Quantity
price: number; // Price per unit
imageUrl?: string; // Optional product image URL
}
Usage
1. Creating a Checkout Session
From your shop/cart page, create a session before redirecting to checkout:
import { createCheckoutSession } from "@/utils/checkoutSession";
// Example: When user clicks "Proceed to Checkout"
function handleCheckout() {
const items = [
{
id: "prod_123",
name: "Professional Cricket Bat",
quantity: 1,
price: 114.39,
imageUrl: "/images/cricket-bat.jpg",
},
{
id: "prod_456",
name: "Cricket Ball Set (6 balls)",
quantity: 2,
price: 114.39,
imageUrl: "/images/cricket-balls.jpg",
},
];
const email = "user@example.com"; // Optional
// Create session and get session ID
const sessionId = createCheckoutSession(items, email);
// Redirect to checkout with clean URL
router.push("/checkout");
}
2. Reading Checkout Session
The checkout page automatically reads the session:
import { getCheckoutSession } from "@/utils/checkoutSession";
// Checkout form component
const checkoutSession = getCheckoutSession();
if (!checkoutSession) {
// No valid session found
return <div>No checkout data found. Please add items to cart.</div>;
}
// Use session data
const { amount, items, email } = checkoutSession;
3. Updating Email
Update the email during checkout:
import { updateCheckoutEmail } from "@/utils/checkoutSession";
function handleEmailChange(newEmail: string) {
updateCheckoutEmail(newEmail);
}
4. Extending Session
Extend the session when user is still active:
import { extendCheckoutSession } from "@/utils/checkoutSession";
// Extend session on user interaction
function handleUserActivity() {
extendCheckoutSession();
}
5. Clearing Session
Clear session after successful payment or cancellation:
import { clearCheckoutSession } from "@/utils/checkoutSession";
// After successful payment
function handlePaymentSuccess() {
clearCheckoutSession();
router.push("/checkout/success");
}
// Or when user cancels
function handleCancel() {
clearCheckoutSession();
router.push("/shop");
}
6. Checking Session Status
Check if a valid session exists:
import { hasValidCheckoutSession } from "@/utils/checkoutSession";
if (!hasValidCheckoutSession()) {
// Redirect to shop
router.push("/shop");
}
Session Expiration
- Duration: 30 minutes by default
- Automatic Cleanup: Expired sessions are automatically removed
- Extension: Sessions can be extended when user is active
- Page Load: Session is automatically extended when checkout page loads
Customizing Duration
To change the session duration, modify the constant in checkoutSession.ts:
const SESSION_DURATION = 30 * 60 * 1000; // 30 minutes
// Change to:
const SESSION_DURATION = 60 * 60 * 1000; // 60 minutes
Backward Compatibility
The checkout page still supports the old query parameter approach during migration:
// Old URL format still works:
/checkout?amount=571.95&product=Cricket+Bat+x1,Ball+x2&email=user@example.com
// The system will:
// 1. Check for checkout session first
// 2. Fall back to query params if no session found
// 3. Log a warning to console
Migration Strategy
-
Phase 1 (Current): Both approaches work
- New features use session
- Old links still work
-
Phase 2: Deprecate query params
- Show warning when query params used
- Encourage users to update bookmarks
-
Phase 3: Remove query param support
- Sessions only
- Backend API integration
Example: Complete Checkout Flow
Step 1: Shop Page - Add to Cart
// components/ProductCard.tsx
import { useState } from "react";
function ProductCard({ product }) {
const [cart, setCart] = useState<CheckoutItem[]>([]);
function addToCart() {
setCart([
...cart,
{
id: product.id,
name: product.name,
quantity: 1,
price: product.price,
imageUrl: product.imageUrl,
},
]);
}
return <button onClick={addToCart}>Add to Cart</button>;
}
Step 2: Cart Page - Review Cart
// app/cart/page.tsx
import { createCheckoutSession } from "@/utils/checkoutSession";
import { useRouter } from "next/navigation";
export default function CartPage() {
const router = useRouter();
const [cart, setCart] = useState<CheckoutItem[]>([]);
const [email, setEmail] = useState("");
function handleCheckout() {
if (cart.length === 0) {
alert("Cart is empty");
return;
}
// Create checkout session
createCheckoutSession(cart, email);
// Redirect to checkout
router.push("/checkout");
}
return (
<div>
{/* Cart items */}
{cart.map((item) => (
<div key={item.id}>
{item.name} x {item.quantity} = ${item.price * item.quantity}
</div>
))}
{/* Email input */}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email for receipt"
/>
{/* Checkout button */}
<button onClick={handleCheckout}>Proceed to Checkout</button>
</div>
);
}
Step 3: Checkout Page - Process Payment
// app/checkout/page.tsx
import {
getCheckoutSession,
clearCheckoutSession,
} from "@/utils/checkoutSession";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
export default function CheckoutPage() {
const router = useRouter();
const [session, setSession] = useState(null);
useEffect(() => {
const checkoutSession = getCheckoutSession();
if (!checkoutSession) {
// No session, redirect to cart
router.push("/cart");
return;
}
setSession(checkoutSession);
}, []);
async function handlePayment() {
// Process payment...
const success = await processPayment(session);
if (success) {
// Clear session after successful payment
clearCheckoutSession();
router.push("/checkout/success");
}
}
if (!session) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Checkout</h1>
{/* Order Summary */}
<div>
{session.items.map((item, index) => (
<div key={index}>
{item.quantity} x {item.name} = ${item.price * item.quantity}
</div>
))}
<div>Total: ${session.amount}</div>
</div>
{/* Payment Form */}
<button onClick={handlePayment}>Pay Now</button>
</div>
);
}
Debugging
Console Messages
The system logs helpful messages for debugging:
✅ Checkout session created: {
sessionId: "checkout_1234567890_abc123",
itemCount: 3,
totalAmount: 571.95,
expiresIn: "30 minutes"
}
✅ Using checkout session data
⏱️ Checkout session extended
🗑️ Checkout session cleared
⚠️ Using query params (consider creating a session)
⚠️ Checkout session expired, clearing...
❌ No checkout session or valid query params found
Inspecting LocalStorage
To view the stored session in browser DevTools:
- Open DevTools (F12)
- Go to Application tab
- Navigate to Storage > Local Storage
- Look for key:
checkout_session
Example stored data:
{
"sessionId": "checkout_1234567890_abc123",
"amount": 571.95,
"items": [
{
"id": "prod_123",
"name": "Professional Cricket Bat",
"quantity": 1,
"price": 114.39,
"imageUrl": "/images/cricket-bat.jpg"
}
],
"email": "user@example.com",
"createdAt": 1704067200000,
"expiresAt": 1704069000000
}
Migration to Backend API
Step 1: Create Backend API Endpoints
// app/api/checkout/session/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(request: NextRequest) {
const { items, email } = await request.json();
// Calculate total
const amount = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
// Create session in database
const session = await prisma.checkoutSession.create({
data: {
sessionId: generateSessionId(),
amount,
items: JSON.stringify(items),
email,
expiresAt: new Date(Date.now() + 30 * 60 * 1000),
},
});
return NextResponse.json({ sessionId: session.sessionId });
}
export async function GET(request: NextRequest) {
const sessionId = request.nextUrl.searchParams.get("sessionId");
if (!sessionId) {
return NextResponse.json({ error: "Session ID required" }, { status: 400 });
}
const session = await prisma.checkoutSession.findUnique({
where: { sessionId },
});
if (!session || session.expiresAt < new Date()) {
return NextResponse.json(
{ error: "Session not found or expired" },
{ status: 404 }
);
}
return NextResponse.json({
sessionId: session.sessionId,
amount: session.amount,
items: JSON.parse(session.items),
email: session.email,
createdAt: session.createdAt.getTime(),
expiresAt: session.expiresAt.getTime(),
});
}
Step 2: Update Utility Functions
Replace localStorage calls with API calls:
// utils/checkoutSession.ts (Backend version)
export async function createCheckoutSession(
items: CheckoutItem[],
email?: string
): Promise<string> {
const response = await fetch("/api/checkout/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items, email }),
});
const data = await response.json();
// Store session ID in cookie or localStorage
document.cookie = `checkout_session=${data.sessionId}; path=/; max-age=1800`;
return data.sessionId;
}
export async function getCheckoutSession(): Promise<CheckoutSessionData | null> {
// Get session ID from cookie
const sessionId = getCookie("checkout_session");
if (!sessionId) {
return null;
}
const response = await fetch(`/api/checkout/session?sessionId=${sessionId}`);
if (!response.ok) {
return null;
}
return await response.json();
}
Step 3: Add Database Schema
// prisma/schema.prisma
model CheckoutSession {
id String @id @default(cuid())
sessionId String @unique
amount Float
items String // JSON string
email String?
createdAt DateTime @default(now())
expiresAt DateTime
@@index([sessionId])
@@index([expiresAt])
}
Best Practices
1. Always Clear Session After Payment
// ✅ Good
async function handleSuccessfulPayment() {
clearCheckoutSession();
router.push("/success");
}
// ❌ Bad - Session left in storage
async function handleSuccessfulPayment() {
router.push("/success"); // Session not cleared!
}
2. Validate Session on Checkout Page Load
// ✅ Good
useEffect(() => {
const session = getCheckoutSession();
if (!session) {
router.push("/cart");
return;
}
setCheckoutData(session);
}, []);
// ❌ Bad - No validation
useEffect(() => {
const session = getCheckoutSession();
setCheckoutData(session); // Could be null!
}, []);
3. Extend Session on User Activity
// ✅ Good
useEffect(() => {
const handleActivity = () => extendCheckoutSession();
window.addEventListener("mousemove", handleActivity);
window.addEventListener("keypress", handleActivity);
return () => {
window.removeEventListener("mousemove", handleActivity);
window.removeEventListener("keypress", handleActivity);
};
}, []);
4. Handle Session Expiration Gracefully
// ✅ Good
const session = getCheckoutSession();
if (!session) {
return (
<div>
<p>Your checkout session has expired.</p>
<button onClick={() => router.push("/cart")}>Return to Cart</button>
</div>
);
}
Troubleshooting
Session Not Found
Problem: Checkout page shows "No checkout data found"
Solutions:
- Check browser console for error messages
- Verify session is being created: Look for "✅ Checkout session created" log
- Check localStorage in DevTools
- Ensure cookies/storage are not blocked
- Try in incognito mode to rule out extension interference
Session Expires Too Quickly
Problem: Session expires before payment completes
Solutions:
- Increase
SESSION_DURATIONconstant - Implement automatic session extension on user activity
- Show countdown timer to user
- Extend session when user starts payment
Data Not Persisting
Problem: Session data disappears on page refresh
Solutions:
- Check if localStorage is enabled
- Verify session hasn't expired
- Check browser's storage quota (unlikely)
- Test in different browser
Old URLs Not Working
Problem: Query parameter URLs show error
Solutions:
- Ensure backward compatibility code is present
- Check console for "⚠️ Using query params" message
- Verify
parseQueryParamsToItemsfunction is correct
Summary
The checkout session management system provides:
✅ Clean URLs: No more ugly query parameters
✅ Better Security: Data not exposed in URL
✅ Flexible Data: Can store complex objects
✅ Backward Compatible: Old links still work
✅ Future-Proof: Ready for backend API migration
✅ Better UX: 30-minute sessions with automatic extension
✅ Easy to Use: Simple API with clear functions
✅ Well-Documented: Complete examples and troubleshooting
The system is production-ready and provides a solid foundation for future enhancements like backend sessions, user accounts, and saved carts!