React Payment Gateway Integration with Stripe

React Payment Gateway Integration with Stripe

Implementing a React payment gateway is crucial for any modern e-commerce application. This comprehensive guide will show you how to integrate a secure payment gateway into your React application using Stripe, one of the most popular payment processing solutions.

Introduction

What is Stripe?

Stripe is a suite of payment APIs that powers commerce for businesses of all sizes. It provides the technical infrastructure and security features needed to operate online payment systems.

Why Choose Stripe?

  • Developer-friendly APIs: Well-documented and easy to implement
  • Extensive feature set: Support for 135+ currencies and various payment methods
  • Strong security: PCI compliance and fraud prevention tools
  • Detailed analytics: Real-time reporting and insights
  • Global reach: Available in 40+ countries

Understanding Stripe’s Architecture

Key Concepts

  1. Payment Intents: Represents the intention to collect payment from a customer
  2. Payment Methods: Different ways customers can pay (cards, bank transfers, etc.)
  3. Customers: Stored customer information for recurring payments
  4. Charges: Actual payment transactions
  5. Refunds: Payment reversals
  6. Webhooks: Real-time event notifications

Payment Flow

graph LR
    A[Customer] --> B[Payment Form]
    B --> C[Create Payment Intent]
    C --> D[Confirm Payment]
    D --> E[Process Payment]
    E --> F[Webhook Notification]
    F --> G[Update Order Status]

Project Setup

Development Environment

# Create new React project with TypeScript
npx create-react-app stripe-payment-app --template typescript
cd stripe-payment-app

# Install dependencies
npm install @stripe/stripe-js @stripe/react-stripe-js
npm install @types/stripe-v3  # TypeScript definitions
npm install axios styled-components
npm install express cors dotenv stripe

# Backend dependencies
npm install --save-dev nodemon typescript ts-node
npm install --save-dev @types/express @types/cors

Project Structure

stripe-payment-app/
├── src/
│   ├── components/
│   │   ├── payment/
│   │   │   ├── PaymentForm.tsx
│   │   │   ├── CardSection.tsx
│   │   │   ├── PaymentStatus.tsx
│   │   │   └── AddressForm.tsx
│   │   └── common/
│   │       ├── Button.tsx
│   │       ├── Input.tsx
│   │       └── Loading.tsx
│   ├── contexts/
│   │   └── PaymentContext.tsx
│   ├── services/
│   │   ├── api.ts
│   │   └── stripe.ts
│   ├── utils/
│   │   ├── validation.ts
│   │   └── formatting.ts
│   └── types/
│       └── payment.ts
├── server/
│   ├── src/
│   │   ├── controllers/
│   │   │   └── paymentController.ts
│   │   ├── middlewares/
│   │   │   ├── auth.ts
│   │   │   └── errorHandler.ts
│   │   ├── routes/
│   │   │   └── payment.ts
│   │   └── utils/
│   │       └── stripe.ts
│   └── index.ts
├── .env.example
└── tsconfig.json

Environment Configuration

# .env.development
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_test_your_key
REACT_APP_API_URL=http://localhost:3001
REACT_APP_ENV=development

# .env.production
REACT_APP_STRIPE_PUBLISHABLE_KEY=pk_live_your_key
REACT_APP_API_URL=https://api.yourapp.com
REACT_APP_ENV=production

# server/.env
STRIPE_SECRET_KEY=sk_test_your_key
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
CORS_ORIGIN=http://localhost:3000
PORT=3001

Frontend Implementation

Payment Context

// src/contexts/PaymentContext.tsx
import React, { createContext, useContext, useState, useCallback } from 'react';
import { loadStripe, Stripe } from '@stripe/stripe-js';

interface PaymentContextType {
  stripe: Stripe | null;
  processing: boolean;
  error: string | null;
  handlePayment: (amount: number) => Promise<void>;
  clearError: () => void;
}

const PaymentContext = createContext<PaymentContextType | undefined>(undefined);

export const PaymentProvider: React.FC = ({ children }) => {
  const [stripe, setStripe] = useState<Stripe | null>(null);
  const [processing, setProcessing] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Initialize Stripe
  React.useEffect(() => {
    const initStripe = async () => {
      const stripeInstance = await loadStripe(
        process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY!
      );
      setStripe(stripeInstance);
    };
    initStripe();
  }, []);

  const handlePayment = useCallback(async (amount: number) => {
    setProcessing(true);
    try {
      // Payment logic here
    } catch (err) {
      setError(err.message);
    } finally {
      setProcessing(false);
    }
  }, []);

  const clearError = useCallback(() => setError(null), []);

  const value = {
    stripe,
    processing,
    error,
    handlePayment,
    clearError,
  };

  return (
    <PaymentContext.Provider value={value}>
      {children}
    </PaymentContext.Provider>
  );
};

export const usePayment = () => {
  const context = useContext(PaymentContext);
  if (context === undefined) {
    throw new Error('usePayment must be used within PaymentProvider');
  }
  return context;
};

Enhanced Payment Form

// src/components/payment/PaymentForm.tsx
import React, { useState } from 'react';
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import styled from 'styled-components';
import { usePayment } from '../../contexts/PaymentContext';
import { Input } from '../common/Input';
import { Button } from '../common/Button';
import { validatePaymentInput } from '../../utils/validation';

interface PaymentFormProps {
  amount: number;
  onSuccess?: () => void;
  onError?: (error: string) => void;
}

const CardElementStyles = {
  style: {
    base: {
      fontSize: '16px',
      color: '#424770',
      letterSpacing: '0.025em',
      '::placeholder': {
        color: '#aab7c4'
      }
    },
    invalid: {
      color: '#9e2146'
    }
  }
};

const PaymentForm: React.FC<PaymentFormProps> = ({
  amount,
  onSuccess,
  onError
}) => {
  const stripe = useStripe();
  const elements = useElements();
  const { processing, handlePayment } = usePayment();
  const [billingDetails, setBillingDetails] = useState({
    name: '',
    email: '',
    address: {
      line1: '',
      city: '',
      state: '',
      postal_code: ''
    }
  });

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();

    if (!stripe || !elements) {
      return;
    }

    // Validate input
    const validationError = validatePaymentInput(billingDetails, amount);
    if (validationError) {
      onError?.(validationError);
      return;
    }

    try {
      const response = await fetch('/api/create-payment-intent', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          amount,
          currency: 'usd',
          billing_details: billingDetails
        })
      });

      const { clientSecret } = await response.json();

      const result = await stripe.confirmCardPayment(clientSecret, {
        payment_method: {
          card: elements.getElement(CardElement)!,
          billing_details: billingDetails
        }
      });

      if (result.error) {
        onError?.(result.error.message!);
      } else {
        onSuccess?.();
      }
    } catch (error) {
      onError?.(error.message);
    }
  };

  return (
    <StyledForm onSubmit={handleSubmit}>
      <Input
        label="Name"
        value={billingDetails.name}
        onChange={(e) => setBillingDetails({
          ...billingDetails,
          name: e.target.value
        })}
        required
      />

      <Input
        label="Email"
        type="email"
        value={billingDetails.email}
        onChange={(e) => setBillingDetails({
          ...billingDetails,
          email: e.target.value
        })}
        required
      />

      <AddressSection>
        {/* Address fields */}
      </AddressSection>

      <CardElementWrapper>
        <CardElement options={CardElementStyles} />
      </CardElementWrapper>

      <Button
        type="submit"
        disabled={!stripe || processing}
      >
        {processing ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}`}
      </Button>
    </StyledForm>
  );
};

const StyledForm = styled.form`
  width: 100%;
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
`;

const CardElementWrapper = styled.div`
  padding: 10px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  background: white;
  margin: 20px 0;
`;

const AddressSection = styled.div`
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin: 20px 0;
`;

export default PaymentForm;

Backend Implementation

React Payment Gateway Integration with Stripe

Express Server with TypeScript

// server/src/index.ts
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import paymentRoutes from './routes/payment';
import { errorHandler } from './middlewares/errorHandler';

dotenv.config();

const app = express();

// Middleware
app.use(cors({
  origin: process.env.CORS_ORIGIN,
  methods: ['GET', 'POST'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

app.use(express.json());

// Routes
app.use('/api/payments', paymentRoutes);

// Error handling
app.use(errorHandler);

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Payment Controller

// server/src/controllers/paymentController.ts
import { Request, Response, NextFunction } from 'express';
import Stripe from 'stripe';
import { validatePaymentIntent } from '../utils/validation';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16'
});

export const createPaymentIntent = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const { amount, currency, billing_details } = req.body;

    // Validate input
    validatePaymentIntent(amount, currency);

    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency,
      payment_method_types: ['card'],
      metadata: {
        billing_details: JSON.stringify(billing_details)
      }
    });

    res.json({ clientSecret: paymentIntent.client_secret });
  } catch (error) {
    next(error);
  }
};

export const handleWebhook = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const sig = req.headers['stripe-signature'];

  try {
    const event = stripe.webhooks.constructEvent(
      req.body,
      sig!,
      process.env.STRIPE_WEBHOOK_SECRET!
    );

    switch (event.type) {
      case 'payment_intent.succeeded':
        await handlePaymentSuccess(event.data.object);
        break;
      case 'payment_intent.payment_failed':
        await handlePaymentFailure(event.data.object);
        break;
    }

    res.json({ received: true });
  } catch (error) {
    next(error);
  }
};

async function handlePaymentSuccess(paymentIntent: Stripe.PaymentIntent) {
  // Update order status
  // Send confirmation email
  // Update inventory
  // etc.
}

async function handlePaymentFailure(paymentIntent: Stripe.PaymentIntent) {
  // Log failure
  // Notify customer
  // Update order status
  // etc.
}

Advanced Features

Saved Cards & Customer Management

// server/src/controllers/customerController.ts
export const saveCard = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const { customerId, paymentMethodId } = req.body;

    // Attach payment method to customer
    await stripe.paymentMethods.attach(paymentMethodId, {
      customer: customerId,
    });

    // Set as default payment method
    await stripe.customers.update(customerId, {
      invoice_settings: {
        default_payment_method: paymentMethodId,
      },
    });

    res.json({ success: true });
  } catch (error) {
    next(error);
  }
};

Subscription Management

// server/src/controllers/subscriptionController.ts
export const createSubscription = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const { customerId, priceId } = req.body;

    const subscription = await stripe.subscriptions.create({
      customer: customerId,
      items: [{ price: priceId }],
      payment_behavior: 'default_incomplete',
      expand: ['latest_invoice.payment_intent'],
    });

    res.json({
      subscriptionId: subscription.id,
      clientSecret: subscription.latest_invoice.payment_intent.client_secret,
    });
  } catch (error) {
    next(error);
  }
};

Security & Best Practices

Input Validation

// server/src/utils/validation.ts
export const validatePaymentIntent = (amount: number, currency: string) => {
  if (!amount || amount <= 0) {
    throw new Error('Invalid amount');
  }

  if (!currency || currency.length !== 3) {
    throw new Error('Invalid currency');
  }

  // Additional validation...
};

Error Handling (continued)

// server/src/middlewares/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import Stripe from 'stripe';

interface AppError extends Error {
  statusCode?: number;
  code?: string;
}

export const errorHandler = (
  error: AppError,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  if (error instanceof Stripe.errors.StripeError) {
    return handleStripeError(error, res);
  }

  console.error('Error:', error);
  res.status(error.statusCode || 500).json({
    error: error.message || 'Internal server error',
    code: error.code || 'INTERNAL_ERROR'
  });
};

const handleStripeError = (error: Stripe.errors.StripeError, res: Response) => {
  const errorResponses = {
    'StripeCardError': { status: 400, code: 'CARD_ERROR' },
    'StripeRateLimitError': { status: 429, code: 'RATE_LIMIT' },
    'StripeInvalidRequestError': { status: 400, code: 'INVALID_REQUEST' },
    'StripeAuthenticationError': { status: 401, code: 'AUTHENTICATION_ERROR' },
    'StripeAPIError': { status: 500, code: 'API_ERROR' },
    'StripeConnectionError': { status: 503, code: 'CONNECTION_ERROR' }
  };

  const errorType = error.type as keyof typeof errorResponses;
  const response = errorResponses[errorType] || { status: 500, code: 'UNKNOWN_ERROR' };

  res.status(response.status).json({
    error: error.message,
    code: response.code
  });
};

Advanced Testing Strategies

Unit Testing Payment Components

// src/__tests__/PaymentForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
import PaymentForm from '../components/payment/PaymentForm';

const mockStripe = {
  confirmCardPayment: jest.fn()
};

jest.mock('@stripe/stripe-js', () => ({
  loadStripe: jest.fn(() => Promise.resolve(mockStripe))
}));

describe('PaymentForm', () => {
  beforeEach(() => {
    mockStripe.confirmCardPayment.mockReset();
  });

  it('handles successful payment', async () => {
    mockStripe.confirmCardPayment.mockResolvedValueOnce({ paymentIntent: { status: 'succeeded' } });

    render(
      <Elements stripe={loadStripe('dummy_key')}>
        <PaymentForm amount={1000} onSuccess={jest.fn()} />
      </Elements>
    );

    // Fill form
    fireEvent.change(screen.getByLabelText(/name/i), {
      target: { value: 'John Doe' }
    });

    fireEvent.change(screen.getByLabelText(/email/i), {
      target: { value: 'john@example.com' }
    });

    // Submit form
    fireEvent.click(screen.getByText(/pay/i));

    await waitFor(() => {
      expect(mockStripe.confirmCardPayment).toHaveBeenCalled();
    });
  });
});

Integration Testing with Mock Stripe Server

// tests/integration/payment.test.ts
import request from 'supertest';
import app from '../../server/src';
import { createMockStripeServer } from '../utils/mockStripe';

describe('Payment Integration', () => {
  let mockStripeServer;

  beforeAll(async () => {
    mockStripeServer = await createMockStripeServer();
  });

  afterAll(async () => {
    await mockStripeServer.close();
  });

  it('creates payment intent successfully', async () => {
    const response = await request(app)
      .post('/api/payments/create-intent')
      .send({
        amount: 1000,
        currency: 'usd'
      });

    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty('clientSecret');
  });
});

Performance Optimization

Lazy Loading Components

// src/App.tsx
import React, { Suspense } from 'react';

const PaymentForm = React.lazy(() => import('./components/payment/PaymentForm'));
const PaymentStatus = React.lazy(() => import('./components/payment/PaymentStatus'));

const App: React.FC = () => {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      {/* Other components */}
      <PaymentForm amount={1000} />
      <PaymentStatus />
    </Suspense>
  );
};

Caching Strategies

// src/utils/cache.ts
interface CacheConfig {
  maxAge: number;
  maxSize: number;
}

class PaymentCache {
  private cache: Map<string, any>;
  private config: CacheConfig;

  constructor(config: CacheConfig) {
    this.cache = new Map();
    this.config = config;
  }

  set(key: string, value: any): void {
    if (this.cache.size >= this.config.maxSize) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }

    this.cache.set(key, {
      value,
      timestamp: Date.now()
    });
  }

  get(key: string): any {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() - item.timestamp > this.config.maxAge) {
      this.cache.delete(key);
      return null;
    }

    return item.value;
  }
}

export const paymentCache = new PaymentCache({
  maxAge: 5 * 60 * 1000, // 5 minutes
  maxSize: 100
});

Production Deployment

Docker Configuration

# Dockerfile
FROM node:16-alpine

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm install

# Copy source code
COPY . .

# Build application
RUN npm run build

# Start server
CMD ["npm", "start"]

Deployment Checklist

  1. Environment Variables
  • Switch to production Stripe keys
  • Update API URLs
  • Set secure CORS origins
  1. Security Headers
// server/src/middlewares/security.ts
import helmet from 'helmet';
import { Express } from 'express';

export const configureSecurityHeaders = (app: Express) => {
  app.use(helmet());
  app.use(helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", 'https://js.stripe.com'],
      frameSrc: ["'self'", 'https://js.stripe.com'],
      connectSrc: ["'self'", 'https://api.stripe.com']
    }
  }));
};
  1. Monitoring Setup
// server/src/utils/monitoring.ts
import winston from 'winston';

export const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.simple()
  }));
}

Troubleshooting Common Issues

1. Payment Intent Creation Failures

Common causes and solutions:

  • Invalid amount formatting
  • Currency mismatches
  • Network timeouts
  • API key issues

2. Card Element Rendering Issues

Solutions for common rendering problems:

  • Style conflicts
  • Mobile responsiveness
  • Cross-browser compatibility

3. Webhook Integration Problems

Tips for debugging webhook issues:

  • Using Stripe CLI for local testing
  • Proper error handling
  • Event verification
  • Retry mechanisms

Best Practices Summary

  1. Security
  • Always use HTTPS
  • Implement proper CORS policies
  • Validate all inputs
  • Use environment variables
  • Regular security audits
  1. Performance
  • Implement caching
  • Lazy load components
  • Optimize bundle size
  • Use proper error boundaries
  1. User Experience
  • Clear error messages
  • Loading states
  • Input validation
  • Mobile responsiveness
  1. Maintenance
  • Proper logging
  • Monitoring
  • Documentation
  • Type safety
  • Test coverage

Helpful Resources

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *