Overview

Crave handles payment processing through Stripe, but you’ll need to integrate Stripe’s client-side components in your storefront. This guide shows you how to add secure payment processing to your storefront.

Prerequisites

  • Stripe publishable key (get this from your merchant)
  • Working storefront with cart functionality
  • SSL certificate for production

Setup

1. Environment Variables

Add your Stripe publishable key to .env.local:
# Stripe Configuration
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

# Crave API Configuration (already set up)
CRAVEUP_API_KEY=your_api_key_here
CRAVEUP_API_BASE_URL=https://api.cravejs.com
NEXT_PUBLIC_LOCATION_ID=your_location_id_here

2. Install Stripe Dependencies

npm install @stripe/stripe-js @stripe/react-stripe-js

Payment Flow

The payment process works like this:
  1. Customer adds items to cart (your storefront handles this)
  2. Customer clicks checkout (your storefront initiates payment)
  3. Create payment intent (Crave API creates this)
  4. Customer enters payment info (Stripe handles this securely)
  5. Payment is processed (Stripe & Crave handle this)
  6. Order is created (Crave creates the order automatically)

Implementation

1. Create Payment Intent

Create an API route to get payment intent from Crave:
// pages/api/create-payment-intent.js
import { craveApi } from '../../lib/api-client';

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }
  
  const { cartItems, locationId } = req.body;
  
  try {
    // Calculate total from cart items
    const total = cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    
    // Create payment intent via Crave API
    const data = await craveApi('/api/v1/stripe/payment-intent', {
      method: 'POST',
      body: JSON.stringify({
        amount: total,
        currency: 'usd',
        location_id: locationId
      })
    });
    
    res.json({ clientSecret: data.client_secret });
  } catch (error) {
    console.error('Payment intent creation failed:', error);
    res.status(500).json({ error: 'Payment setup failed' });
  }
}

2. Create Checkout Component

Create components/Checkout.js:
import { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);

export default function Checkout({ cartItems, locationId, onSuccess }) {
  const [clientSecret, setClientSecret] = useState('');
  const [error, setError] = useState(null);
  
  useEffect(() => {
    if (cartItems.length === 0) return;
    
    // Create payment intent
    fetch('/api/create-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ cartItems, locationId })
    })
    .then(res => res.json())
    .then(data => {
      if (data.clientSecret) {
        setClientSecret(data.clientSecret);
      } else {
        setError('Failed to setup payment');
      }
    })
    .catch(err => setError(err.message));
  }, [cartItems, locationId]);
  
  const options = {
    clientSecret,
    appearance: {
      theme: 'stripe',
      variables: {
        colorPrimary: '#007bff',
        colorBackground: '#ffffff',
        colorText: '#30313d',
        fontFamily: 'system-ui, sans-serif'
      }
    }
  };
  
  if (error) {
    return <div className="error">Error: {error}</div>;
  }
  
  if (!clientSecret) {
    return <div>Loading payment...</div>;
  }
  
  return (
    <Elements options={options} stripe={stripePromise}>
      <CheckoutForm onSuccess={onSuccess} />
    </Elements>
  );
}

function CheckoutForm({ onSuccess }) {
  const stripe = useStripe();
  const elements = useElements();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const handleSubmit = async (event) => {
    event.preventDefault();
    
    if (!stripe || !elements) return;
    
    setLoading(true);
    setError(null);
    
    const result = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: `${window.location.origin}/order-success`
      },
      redirect: 'if_required'
    });
    
    if (result.error) {
      setError(result.error.message);
    } else {
      // Payment succeeded
      onSuccess(result.paymentIntent);
    }
    
    setLoading(false);
  };
  
  return (
    <form onSubmit={handleSubmit} className="checkout-form">
      <PaymentElement />
      
      {error && <div className="error">{error}</div>}
      
      <button type="submit" disabled={!stripe || loading} className="pay-button">
        {loading ? 'Processing...' : 'Pay Now'}
      </button>
    </form>
  );
}

3. Add to Your Main Component

Update your main page to include checkout:
// pages/index.js
import { useState } from 'react';
import Menu from '../components/Menu';
import Cart from '../components/Cart';
import Checkout from '../components/Checkout';

export default function Home() {
  const [cartItems, setCartItems] = useState([]);
  const [showCheckout, setShowCheckout] = useState(false);
  const locationId = process.env.NEXT_PUBLIC_LOCATION_ID;
  
  // ... existing cart functions ...
  
  const handleCheckout = () => {
    if (cartItems.length === 0) {
      alert('Your cart is empty');
      return;
    }
    setShowCheckout(true);
  };
  
  const handlePaymentSuccess = (paymentIntent) => {
    // Payment succeeded, order will be created automatically by Crave
    alert('Payment successful! Your order is being processed.');
    setCartItems([]);
    setShowCheckout(false);
  };
  
  return (
    <div style={{ display: 'flex', gap: '2rem', padding: '2rem' }}>
      <div style={{ flex: 2 }}>
        <Menu locationId={locationId} onAddToCart={addToCart} />
      </div>
      <div style={{ flex: 1 }}>
        <Cart 
          items={cartItems}
          onUpdateQuantity={updateQuantity}
          onRemoveItem={removeFromCart}
        />
        
        {cartItems.length > 0 && (
          <button onClick={handleCheckout} className="checkout-button">
            Proceed to Checkout
          </button>
        )}
        
        {showCheckout && (
          <div className="checkout-section">
            <h3>Payment</h3>
            <Checkout 
              cartItems={cartItems}
              locationId={locationId}
              onSuccess={handlePaymentSuccess}
            />
          </div>
        )}
      </div>
    </div>
  );
}

4. Add Checkout Styling

Add to your styles/globals.css:
.checkout-button {
  width: 100%;
  background: #007bff;
  color: white;
  padding: 1rem;
  border: none;
  border-radius: 8px;
  font-size: 1.1rem;
  cursor: pointer;
  margin-top: 1rem;
}

.checkout-button:hover {
  background: #0056b3;
}

.checkout-section {
  margin-top: 2rem;
  padding: 1rem;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.checkout-form {
  margin-top: 1rem;
}

.pay-button {
  width: 100%;
  background: #28a745;
  color: white;
  padding: 1rem;
  border: none;
  border-radius: 8px;
  font-size: 1.1rem;
  cursor: pointer;
  margin-top: 1rem;
}

.pay-button:disabled {
  background: #6c757d;
  cursor: not-allowed;
}

.error {
  color: #dc3545;
  margin: 1rem 0;
  padding: 0.5rem;
  background: #f8d7da;
  border: 1px solid #f5c6cb;
  border-radius: 4px;
}

Testing

1. Use Test Cards

For testing, use these Stripe test card numbers:
const testCards = {
  success: '4242424242424242',
  declined: '4000000000000002',
  requiresAuth: '4000002500003155'
};

2. Test the Flow

  1. Add items to cart
  2. Click “Proceed to Checkout”
  3. Enter test card 4242424242424242
  4. Use any future expiry date and any CVC
  5. Complete payment
  6. Verify success message

Error Handling

Common payment errors and how to handle them:
function getErrorMessage(error) {
  switch (error.code) {
    case 'card_declined':
      return 'Your card was declined. Please try a different card.';
    case 'insufficient_funds':
      return 'Insufficient funds. Please try a different card.';
    case 'expired_card':
      return 'Your card has expired. Please use a different card.';
    case 'incorrect_cvc':
      return 'Your card security code is incorrect.';
    default:
      return 'Payment failed. Please try again.';
  }
}

Security Notes

  • Never include your secret key in client-side code
  • Always use HTTPS in production
  • Payment processing is handled securely by Stripe
  • Crave automatically creates orders when payments succeed

What Happens Next

After successful payment:
  1. Stripe processes the payment securely
  2. Crave receives the payment confirmation via webhook
  3. Order is automatically created in the merchant’s system
  4. Customer receives confirmation (handled by merchant)
  5. Order appears in merchant dashboard for fulfillment
Your storefront now has secure payment processing! The merchant will receive orders automatically and can fulfill them through their dashboard.