Skip to main content

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

  1. Phase 1 (Current): Both approaches work

    • New features use session
    • Old links still work
  2. Phase 2: Deprecate query params

    • Show warning when query params used
    • Encourage users to update bookmarks
  3. 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:

  1. Open DevTools (F12)
  2. Go to Application tab
  3. Navigate to Storage > Local Storage
  4. 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:

  1. Check browser console for error messages
  2. Verify session is being created: Look for "✅ Checkout session created" log
  3. Check localStorage in DevTools
  4. Ensure cookies/storage are not blocked
  5. Try in incognito mode to rule out extension interference

Session Expires Too Quickly

Problem: Session expires before payment completes

Solutions:

  1. Increase SESSION_DURATION constant
  2. Implement automatic session extension on user activity
  3. Show countdown timer to user
  4. Extend session when user starts payment

Data Not Persisting

Problem: Session data disappears on page refresh

Solutions:

  1. Check if localStorage is enabled
  2. Verify session hasn't expired
  3. Check browser's storage quota (unlikely)
  4. Test in different browser

Old URLs Not Working

Problem: Query parameter URLs show error

Solutions:

  1. Ensure backward compatibility code is present
  2. Check console for "⚠️ Using query params" message
  3. Verify parseQueryParamsToItems function 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!