Payments Recipe ‐ Stripe - spinningideas/resources GitHub Wiki

Integrating Stripe Payments with React and Node.js

Other General Guides For Reference:

Table of Contents

Initial Setup

1. Stripe Account Setup

  1. Create a Stripe account at stripe.com
  2. Navigate to the Developers section in your Stripe dashboard
  3. Get your API keys (both publishable and secret keys)
  4. Store these keys securely in your environment variables:
    • STRIPE_PUBLISHABLE_KEY: For frontend use
    • STRIPE_SECRET_KEY: For backend use only

2. Create Subscription Products

  1. In your Stripe dashboard, navigate to Products → Create
  2. Create monthly subscription product:
    • Set name, description, and pricing
    • Configure billing cycle (monthly)
    • Set up any trial periods if needed
  3. Create annual subscription product:
    • Set name, description, and pricing
    • Configure billing cycle (annual)
    • Consider adding a discount compared to monthly plan
  4. Note the price_id values for each product (you'll need these later)

3. Configure Webhook Settings

  1. In Stripe dashboard, go to Developers → Webhooks
  2. Add an endpoint URL (your backend webhook URL)
  3. Generate a webhook signing secret
  4. Select events to listen for:
    • checkout.session.completed
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.payment_succeeded
    • invoice.payment_failed
  5. Save webhook configuration
  6. Store the webhook secret as STRIPE_WEBHOOK_SECRET in your environment variables

Backend Implementation

1. Set Up Environment Variables

Create or update your .env file with:

STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key
STRIPE_SECRET_KEY=sk_test_your_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret

2. Create Database Schema

Required Database Tables

-- Store user subscription information
CREATE TABLE subscriptions (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID NOT NULL REFERENCES users(id),
  stripe_subscription_id TEXT NOT NULL,
  stripe_customer_id TEXT NOT NULL,
  stripe_price_id TEXT,
  product_id TEXT,
  status TEXT NOT NULL,
  current_period_start TIMESTAMP WITH TIME ZONE,
  current_period_end TIMESTAMP WITH TIME ZONE,
  cancel_at_period_end BOOLEAN DEFAULT FALSE,
  amount INTEGER,
  currency TEXT,
  interval TEXT,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);

-- For webhook event auditing
CREATE TABLE webhook_events (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  type TEXT NOT NULL,
  stripe_event_id TEXT,
  created_at TIMESTAMP WITH TIME ZONE NOT NULL,
  data JSONB NOT NULL,
  processed_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);

Table Descriptions

  1. subscriptions - Stores all subscription data from Stripe

    • stripe_subscription_id: The unique ID from Stripe for this subscription
    • stripe_customer_id: The Stripe customer ID
    • user_id: Reference to your user
    • status: Current status (active, canceled, etc.)
    • current_period_start/end: Current billing period
    • cancel_at_period_end: Whether subscription will cancel at period end
  2. webhook_events - Audit log for all webhook events

    • Helps with debugging and tracking webhook history
    • Prevents duplicate processing of events
    • Useful for reconciliation and troubleshooting

3. Install Required Packages

npm install stripe express
# or if using yarn
yarn add stripe express

4. Create Checkout Session Endpoint

// server/routes/stripe.js
const express = require('express');
const router = express.Router();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

router.post('/create-checkout-session', async (req, res) => {
  try {
    const { priceId, userId } = req.body;
    
    if (!priceId || !userId) {
      return res.status(400).json({ error: 'Price ID and User ID are required' });
    }
    
    // Verify user exists in your database
    const user = await getUserById(userId);
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    // Check if user already has a Stripe customer ID
    let customerId = user.stripe_customer_id;
    
    if (!customerId) {
      // Create a new customer in Stripe
      const customer = await stripe.customers.create({
        email: user.email,
        metadata: {
          userId: userId
        }
      });
      
      customerId = customer.id;
      
      // Save customer ID to your database
      await updateUserStripeCustomerId(userId, customerId);
    }
    
    // Create checkout session
    const session = await stripe.checkout.sessions.create({
      customer: customerId,
      payment_method_types: ['card'],
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      mode: 'subscription',
      success_url: `${req.headers.origin}/subscription/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${req.headers.origin}/subscription/cancel`,
      metadata: {
        userId: userId
      }
    });
    
    res.json({ url: session.url });
  } catch (error) {
    console.error('Error creating checkout session:', error);
    res.status(500).json({ error: error.message });
  }
});

module.exports = router;

5. Implement Webhook Handler

// server/routes/webhook.js
const express = require('express');
const router = express.Router();
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// This must be raw for Stripe webhook verification
router.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    // Verify webhook signature
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    console.error(`Webhook Error: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Store event for auditing
  await storeWebhookEvent({
    type: event.type,
    stripe_event_id: event.id,
    created_at: new Date(event.created * 1000),
    data: event.data.object
  });

  // Handle the event based on type
  try {
    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object;
        
        // Extract user ID from metadata
        const userId = session.metadata?.userId;
        if (!userId) {
          throw new Error('User ID not found in session metadata');
        }
        
        // Get subscription details
        const subscription = await stripe.subscriptions.retrieve(session.subscription);
        
        // Store subscription in database
        await createSubscription({
          user_id: userId,
          stripe_subscription_id: subscription.id,
          stripe_customer_id: subscription.customer,
          stripe_price_id: subscription.items.data[0].price.id,
          product_id: subscription.items.data[0].price.product,
          status: subscription.status,
          current_period_start: new Date(subscription.current_period_start * 1000),
          current_period_end: new Date(subscription.current_period_end * 1000),
          cancel_at_period_end: subscription.cancel_at_period_end,
          amount: subscription.items.data[0].price.unit_amount,
          currency: subscription.items.data[0].price.currency,
          interval: subscription.items.data[0].price.recurring.interval
        });
        
        // Update user's premium status
        await updateUserPremiumStatus(userId, true);
        break;
      }
      
      case 'customer.subscription.updated': {
        const subscription = event.data.object;
        
        // Find user by customer ID
        const user = await getUserByStripeCustomerId(subscription.customer);
        if (!user) {
          throw new Error('User not found for customer ID: ' + subscription.customer);
        }
        
        // Update subscription in database
        await updateSubscription(subscription.id, {
          status: subscription.status,
          current_period_start: new Date(subscription.current_period_start * 1000),
          current_period_end: new Date(subscription.current_period_end * 1000),
          cancel_at_period_end: subscription.cancel_at_period_end
        });
        break;
      }
      
      case 'customer.subscription.deleted': {
        const subscription = event.data.object;
        
        // Find user by customer ID
        const user = await getUserByStripeCustomerId(subscription.customer);
        if (!user) {
          throw new Error('User not found for customer ID: ' + subscription.customer);
        }
        
        // Update subscription as canceled
        await updateSubscription(subscription.id, {
          status: 'canceled',
          cancel_at_period_end: false
        });
        
        // Check if user has any active subscriptions left
        const hasActiveSubscription = await checkUserHasActiveSubscription(user.id);
        
        if (!hasActiveSubscription) {
          // No active subscriptions left, remove premium status
          await updateUserPremiumStatus(user.id, false);
        }
        break;
      }
      
      case 'invoice.payment_succeeded': {
        const invoice = event.data.object;
        // Handle successful payment
        // You might want to send a receipt email or update payment history
        break;
      }
      
      case 'invoice.payment_failed': {
        const invoice = event.data.object;
        // Handle failed payment
        // You might want to notify the user or take action on their account
        break;
      }
      
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }
    
    // Return success response
    res.json({ received: true });
  } catch (error) {
    console.error(`Error processing webhook: ${error.message}`);
    res.status(500).json({ error: error.message });
  }
});

module.exports = router;

Frontend Implementation

1. Install Required Packages

npm install @stripe/react-stripe-js @stripe/stripe-js
# or if using yarn
yarn add @stripe/react-stripe-js @stripe/stripe-js

2. Set Up Stripe Provider

// src/App.jsx or src/index.jsx
import React from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import Routes from './Routes';

// Load Stripe outside of component render to avoid recreating Stripe object
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);

function App() {
  return (
    <Elements stripe={stripePromise}>
      <Routes />
    </Elements>
  );
}

export default App;

3. Create Subscription Plans Component

// src/components/SubscriptionPlans.jsx
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';

// Replace with your actual Stripe price IDs
const SUBSCRIPTION_PLANS = [
  {
    name: 'Monthly',
    description: 'Access to all premium features',
    price: '$9.99',
    interval: 'month',
    priceId: 'price_monthly_id_here',
    features: ['Feature 1', 'Feature 2', 'Feature 3']
  },
  {
    name: 'Annual',
    description: 'Save 20% with yearly billing',
    price: '$95.88',
    interval: 'year',
    priceId: 'price_annual_id_here',
    features: ['Feature 1', 'Feature 2', 'Feature 3', 'Priority support']
  }
];

export function SubscriptionPlans() {
  const navigate = useNavigate();
  const [loading, setLoading] = useState(false);
  
  const handleSubscribe = async (priceId) => {
    try {
      setLoading(true);
      
      // Get current user
      const user = await getCurrentUser();
      
      if (!user) {
        alert('Please sign in to subscribe');
        navigate('/login');
        return;
      }
      
      // Call your backend to create checkout session
      const response = await fetch('/api/create-checkout-session', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          priceId,
          userId: user.id
        })
      });
      
      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || 'Failed to create checkout session');
      }
      
      const { url } = await response.json();
      
      // Redirect to Stripe checkout
      window.location.href = url;
    } catch (error) {
      console.error('Error creating checkout session:', error);
      alert('Failed to create checkout session. Please try again.');
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div className="subscription-plans">
      <h2>Choose a Subscription Plan</h2>
      <div className="plans-container">
        {SUBSCRIPTION_PLANS.map((plan) => (
          <div key={plan.priceId} className="plan-card">
            <h3>{plan.name}</h3>
            <p>{plan.description}</p>
            <div className="price">
              <span className="amount">{plan.price}</span>
              <span className="interval">/{plan.interval}</span>
            </div>
            <ul className="features">
              {plan.features.map((feature, index) => (
                <li key={index}>{feature}</li>
              ))}
            </ul>
            <button 
              onClick={() => handleSubscribe(plan.priceId)}
              className="subscribe-button"
              disabled={loading}
            >
              {loading ? 'Processing...' : 'Subscribe'}
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

4. Create Subscription Management Component

// src/components/SubscriptionManagement.jsx
import React, { useState, useEffect } from 'react';

export function SubscriptionManagement() {
  const [subscription, setSubscription] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [cancelLoading, setCancelLoading] = useState(false);
  
  useEffect(() => {
    async function fetchSubscription() {
      try {
        const user = await getCurrentUser();
        
        if (!user) {
          setError('User not authenticated');
          setLoading(false);
          return;
        }
        
        const response = await fetch(`/api/subscriptions/${user.id}`);
        
        if (!response.ok) {
          throw new Error('Failed to fetch subscription');
        }
        
        const data = await response.json();
        setSubscription(data.subscription);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }
    
    fetchSubscription();
  }, []);
  
  const handleCancelSubscription = async () => {
    if (!window.confirm('Are you sure you want to cancel your subscription?')) {
      return;
    }
    
    try {
      setCancelLoading(true);
      const response = await fetch(`/api/subscriptions/${subscription.stripe_subscription_id}/cancel`, {
        method: 'POST',
      });
      
      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || 'Failed to cancel subscription');
      }
      
      const updatedSubscription = await response.json();
      setSubscription(updatedSubscription.subscription);
      
      alert('Your subscription has been canceled and will end at the end of the current billing period.');
    } catch (error) {
      console.error('Error canceling subscription:', error);
      alert('Failed to cancel subscription. Please try again.');
    } finally {
      setCancelLoading(false);
    }
  };
  
  if (loading) return <div>Loading subscription details...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!subscription) return <div>No active subscription found.</div>;
  
  return (
    <div className="subscription-management">
      <h2>Subscription Details</h2>
      <div className="subscription-info">
        <p><strong>Status:</strong> {subscription.status}</p>
        <p><strong>Plan:</strong> {subscription.product_name}</p>
        <p><strong>Amount:</strong> {formatCurrency(subscription.amount, subscription.currency)}</p>
        <p><strong>Billing Period:</strong> {formatDate(subscription.current_period_start)} to {formatDate(subscription.current_period_end)}</p>
        {subscription.cancel_at_period_end ? (
          <p className="cancellation-notice">
            Your subscription will end on {formatDate(subscription.current_period_end)}
          </p>
        ) : (
          <button 
            onClick={handleCancelSubscription}
            className="cancel-button"
            disabled={cancelLoading}
          >
            {cancelLoading ? 'Processing...' : 'Cancel Subscription'}
          </button>
        )}
      </div>
    </div>
  );
}

function formatDate(dateString) {
  return new Date(dateString).toLocaleDateString();
}

function formatCurrency(amount, currency) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency || 'USD',
  }).format(amount / 100);
}

Webhook Integration

1. Webhook Verification

Stripe uses webhook signatures to verify that events are sent by Stripe and not by a third party:

const event = stripe.webhooks.constructEvent(
  req.body,
  req.headers['stripe-signature'],
  process.env.STRIPE_WEBHOOK_SECRET
);

2. Handling Different Event Types

  • checkout.session.completed: When a customer completes checkout

    • Store subscription details
    • Update user's premium status
  • customer.subscription.updated: When subscription details change

    • Update stored subscription information
    • Handle plan changes if applicable
  • customer.subscription.deleted: When a subscription is canceled

    • Mark subscription as canceled
    • Update user's premium status if needed
  • invoice.payment_succeeded: When a payment is successful

    • Update payment history
    • Send receipt email (optional)
  • invoice.payment_failed: When a payment fails

    • Notify user
    • Take appropriate action on account

3. Error Handling and Retries

  • Store webhook event IDs to prevent duplicate processing
  • Add logging for all webhook events for debugging
  • Implement error handling for failed webhook processing

Testing and Deployment

1. Local Testing

  1. Use Stripe CLI for local webhook testing:
    stripe listen --forward-to localhost:3000/api/webhook
  2. Use Stripe test cards for simulating payments:
    • Success: 4242 4242 4242 4242
    • Decline: 4000 0000 0000 0002

2. Test Environment

  1. Use Stripe test mode for all testing
  2. Test the full subscription flow
  3. Verify webhook events are properly received and processed

3. Production Deployment

  1. Switch to Stripe live mode and update API keys
  2. Configure production webhook endpoints
  3. Implement proper error handling and monitoring

4. Monitoring and Maintenance

  1. Set up monitoring for webhook failures
  2. Create alerts for failed subscription events
  3. Implement a dashboard to track subscription metrics

Additional Resources


---
https://medium.com/@hikmatullahmcs/here-is-a-step-by-step-guide-on-how-to-integrate-stripe-with-a-node-js-77a25adf7064
⚠️ **GitHub.com Fallback** ⚠️