Chameleon

Payment System

Stripe payment integration and subscription management

Payment System

Chameleon integrates with Stripe for payment processing, supporting one-time payments and recurring subscriptions.

Overview

The payment system provides:

  • ✅ One-time product purchases
  • ✅ Monthly and yearly subscriptions
  • ✅ Subscription upgrades and downgrades
  • ✅ Stripe Customer Portal for self-service
  • ✅ Webhook handling for payment notifications
  • ✅ Automatic credit allocation on payment

Stripe Setup

Step 1: Create Stripe Account

  1. Sign up at stripe.com
  2. Verify your email
  3. Complete business information

Step 2: Get API Keys

  1. Go to Developers > API keys
  2. Copy your keys:
    • Publishable key: pk_test_... (for client-side)
    • Secret key: sk_test_... (for server-side)

Use test mode keys during development. Switch to live mode keys for production.

Step 3: Configure Environment Variables

Add to .env.development:

STRIPE_PRIVATE_KEY="sk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
NEXT_PUBLIC_WEB_URL="http://localhost:3000"

For Vercel deployment, add these in Environment Variables.

Step 4: Create Products and Prices

  1. Go to Products in Stripe Dashboard

  2. Click Add product

  3. For each product category (Starter, Professional, Enterprise):

    • Create one-time price
    • Create monthly recurring price
    • Create yearly recurring price
  4. Copy the Price ID (starts with price_...)

You need the Price ID, not the Product ID. Click into the product to see its prices.

Step 5: Configure Price IDs

Configure Stripe Price IDs directly in the pricing configuration files for each language.

Edit src/i18n/pages/pricing/en.json (or your target language file):

{
  "pricing": {
    "items": [
      {
        "title": "Starter",
        "description": "Get started with your first SaaS startup.",
        "interval": "one-time",
        "amount": 19900,
        "currency": "USD",
        "price": "$199",
        "product_id": "starter",
        "product_name": "Chameleon Boilerplate Starter",
        "stripe_price_id": "price_1Sxxxxxxxxxxxxxx",
        "credits": 100,
        "valid_months": 1
      },
      {
        "title": "Starter Monthly",
        "interval": "month",
        "amount": 1900,
        "currency": "USD",
        "price": "$19",
        "product_id": "starter-monthly",
        "product_name": "Chameleon Boilerplate Starter Monthly",
        "stripe_price_id": "price_1Sxxxxxxxxxxxxxx",
        "credits": 100,
        "valid_months": 1
      },
      {
        "title": "Starter Yearly",
        "interval": "year",
        "amount": 19900,
        "currency": "USD",
        "price": "$199",
        "product_id": "starter-yearly",
        "product_name": "Chameleon Boilerplate Starter Yearly",
        "stripe_price_id": "price_1Sxxxxxxxxxxxxxx",
        "credits": 1200,
        "valid_months": 12
      }
    ]
  }
}

Important:

  • amount is in cents (1900 = $19.00)
  • stripe_price_id is your Stripe Price ID (starts with price_...)
  • product_id must be unique for each pricing item
  • Configure pricing for each language you support
  • Update both en.json and other language files (e.g., zh.json, de.json)

Step 6: Set Up Webhooks

Stripe webhooks notify your app about payment events.

  1. Go to Developers > Webhooks

  2. Click Add endpoint

  3. Set endpoint URL:

    https://your-domain.com/api/pay/notify/stripe
    
  4. Select events to listen for:

    • checkout.session.completed
    • invoice.payment_succeeded
    • customer.subscription.updated
    • customer.subscription.deleted
  5. Copy the Signing secret (starts with whsec_...)

  6. Add to environment variables:

    STRIPE_WEBHOOK_SECRET="whsec_..."
    

Step 7: Configure Customer Portal

The Customer Portal allows users to manage their subscriptions.

  1. Go to Settings > Billing > Customer portal

  2. Configure settings:

    • ✅ Allow customers to update payment methods
    • ✅ Allow customers to update billing information
    • ✅ Allow customers to view invoice history
    • ✅ Allow customers to switch plans
    • ✅ Allow customers to cancel subscriptions
  3. Set cancellation behavior:

    • Recommended: Cancel at end of billing period
    • Or: Cancel immediately with prorated refund
  4. Save settings

Payment Flow

One-Time Purchase

User clicks "Buy" → Creates checkout session → Redirects to Stripe → 
Payment → Webhook notification → Credits added → User redirected back

Code Flow:

  1. User clicks buy button on /pricing page

  2. Frontend calls /api/checkout:

    const response = await fetch("/api/checkout", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        product_id: "starter",
        interval: "onetime",
      }),
    });
    
  3. Backend creates Stripe checkout session

  4. User completes payment on Stripe

  5. Stripe sends webhook to /api/pay/notify/stripe

  6. Order status updated to paid

  7. Credits added to user account

  8. User redirected to /my-orders

Subscription Purchase

Same flow as one-time, but:

  • interval is month or year
  • Creates Stripe subscription
  • Subscription renews automatically
  • Credits added on each billing cycle

Subscription Upgrade

  1. User has active subscription (e.g., Starter Monthly $19)

  2. User clicks "Upgrade" on Professional plan ($29)

  3. System calculates proration:

    • Remaining time on current plan
    • Cost difference: $29 - $19 = $10
    • Prorated charge for remaining period
  4. Stripe Customer Portal handles the upgrade

  5. Immediate effect, prorated charge applied

Subscription Downgrade

  1. User clicks "Switch Plan" on lower tier
  2. Redirected to Stripe Customer Portal
  3. User selects downgrade
  4. Takes effect on next billing cycle
  5. No refund for current period

Upgrades are immediate with prorated charges. Downgrades take effect at the end of the current billing period.

Webhook Handling

Events Handled

checkout.session.completed

  • Triggered when user completes checkout
  • Updates order status to paid
  • Adds credits to user account
  • Creates subscription record if applicable

invoice.payment_succeeded

  • Triggered for subscription renewals
  • Updates subscription status
  • Adds credits for the new billing period

customer.subscription.updated

  • Triggered when subscription changes (upgrade/downgrade)
  • Updates subscription details

customer.subscription.deleted

  • Triggered when subscription is cancelled
  • Marks subscription as inactive

Webhook Security

The webhook endpoint verifies the signature:

const sig = headers.get("stripe-signature");
const event = stripe.webhooks.constructEvent(
  body,
  sig,
  process.env.STRIPE_WEBHOOK_SECRET
);

This prevents unauthorized webhook calls.

Customer Portal

Users can manage their subscriptions at /my-orders by clicking "Manage Subscription".

Available Actions:

  • Update payment method
  • View invoices and billing history
  • Upgrade to higher plan (immediate)
  • Downgrade to lower plan (next billing cycle)
  • Cancel subscription

Implementation:

// In src/services/order.ts
export async function getStripeBilling(sub_id: string) {
  const subscription = await stripe.subscriptions.retrieve(sub_id);
  
  const billing = await stripe.billingPortal.sessions.create({
    customer: subscription.customer as string,
    return_url: `${baseUrl}/my-orders`,
  });
  
  return billing;
}

Testing

Test Mode

Use Stripe test mode during development:

  1. Use test API keys (sk_test_..., pk_test_...)

  2. Use test card numbers:

    • Success: 4242 4242 4242 4242
    • Requires authentication: 4000 0025 0000 3155
    • Declined: 4000 0000 0000 9995
  3. Use any future expiry date and any CVC

Test Webhook Locally

Use Stripe CLI:

# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login
stripe login

# Forward webhooks to localhost
stripe listen --forward-to localhost:3000/api/pay/notify/stripe

# In another terminal, trigger test event
stripe trigger checkout.session.completed

Credit Allocation

Credit Amounts

Configured in pricing files (e.g., src/i18n/pages/pricing/en.json):

{
  "pricing": {
    "items": [
      {
        "title": "Starter Monthly",
        "amount": 1900,
        "credits": 1000,  // 1000 credits per month
        "stripe_price_id": "price_1Sxxxxxxxxxxxxxx"
      }
    ]
  }
}

Credit Expiration

  • One-time purchases: Credits expire in 1 year
  • Subscriptions: Credits expire at next billing cycle
  • Unused credits: Not carried over (subscriptions)

Allocation Logic

// On payment success
await increaseCredits({
  user_uuid,
  trans_type: CreditsTransType.OrderPay,
  credits: pricingItem.credits,
  expired_at: getOneYearLaterTimestr(), // or next billing date
});

Subscription Management

Active Subscription Check

import { getUserActiveSubscription } from "@/services/order";

const subscription = await getUserActiveSubscription(user_uuid);

if (subscription) {
  // User has active subscription
  console.log(subscription.product_id);
  console.log(subscription.interval);
}

Pricing Page Logic

The pricing page shows different buttons based on subscription status:

  • Current Plan: Badge, no button
  • Higher Tier (Same Interval): "Upgrade" button with price difference
  • Lower Tier (Same Interval): "Switch Plan" (effective next cycle)
  • Different Interval: "Get Chameleon" (new subscription)

Common Issues

Checkout fails with "Invalid URL"

Problem: NEXT_PUBLIC_WEB_URL not set

Solution:

# Add to .env.development
NEXT_PUBLIC_WEB_URL="http://localhost:3000"

# Add to Vercel
NEXT_PUBLIC_WEB_URL="https://your-domain.com"

Webhook signature verification failed

Problem: Wrong STRIPE_WEBHOOK_SECRET

Solution:

  1. Get the correct secret from Stripe Dashboard > Webhooks
  2. Update environment variable
  3. Redeploy

Customer Portal not working

Problem: Stripe Portal not configured or Price IDs missing

Solution:

  1. Configure Customer Portal in Stripe Dashboard
  2. Ensure all Price IDs in pricing files are correct
  3. Use Price ID (price_...), not Product ID (prod_...)
  4. Check that stripe_price_id is set in your pricing configuration files

Credits not added after payment

Problem: Webhook not received or failed

Solution:

  1. Check Vercel logs for webhook errors
  2. Verify webhook secret is correct
  3. Check Stripe Dashboard > Webhooks > Attempts for errors
  4. Ensure pricing files have credit amounts configured

Going Live

Production Checklist

  • Switch to live API keys in Vercel
  • Update webhook endpoint to production URL
  • Configure Customer Portal in live mode
  • Test a real payment (then refund)
  • Set up Stripe Radar for fraud prevention
  • Configure tax settings if applicable
  • Set up email receipts
  • Monitor Stripe Dashboard regularly

Security

  • Never expose STRIPE_PRIVATE_KEY in client-side code
  • Always verify webhook signatures
  • Use HTTPS only in production
  • Regularly review Stripe logs for suspicious activity

Next Steps