Skip to main content
Crave uses Stripe Connect to process payments. This guide covers how to create payment intents, integrate Stripe.js on the frontend, and test in development.

How payments work

1

Cart is finalized

The customer has added items, set fulfillment, and provided their details.
2

Create a payment intent

Your server calls the Crave API to get a Stripe clientSecret.
3

Collect payment on the client

Stripe.js securely collects card details using the clientSecret.
4

Stripe confirms the charge

The payment is processed through the restaurant’s connected Stripe account.
Crave handles Stripe Connect setup for each restaurant. You only need to integrate Stripe.js on your storefront frontend.

Create a payment intent

const payment = await storefront.payments.createIntent('loc_123', cartId);

// payment.clientSecret — use with Stripe.js
// payment.stripeAccountId — the restaurant's connected account
The response includes:
FieldDescription
clientSecretThe Stripe PaymentIntent client secret
stripeAccountIdThe connected Stripe account to pass to stripe.confirmPayment

Install Stripe.js

pnpm add @stripe/stripe-js @stripe/react-stripe-js

Set up the Stripe provider

Wrap your checkout page with the Stripe Elements provider.
'use client';

import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';

const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
  // For Stripe Connect, pass the connected account
  // { stripeAccount: payment.stripeAccountId }
);

export default function CheckoutLayout({ children }: { children: React.ReactNode }) {
  return (
    <Elements stripe={stripePromise}>
      {children}
    </Elements>
  );
}
You must use the restaurant’s stripeAccountId from the payment intent response when initializing Stripe. This routes the payment to the correct connected account.

Collect payment

'use client';

import { useState } from 'react';
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';

export function PaymentForm({ clientSecret }: { clientSecret: string }) {
  const stripe = useStripe();
  const elements = useElements();
  const [error, setError] = useState<string | null>(null);
  const [processing, setProcessing] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!stripe || !elements) return;

    setProcessing(true);
    setError(null);

    const { error: stripeError } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: `${window.location.origin}/order/confirmation`,
      },
    });

    if (stripeError) {
      setError(stripeError.message ?? 'Payment failed');
      setProcessing(false);
    }
    // If successful, Stripe redirects to return_url
  }

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      {error && <p className="text-red-600 mt-2 text-sm">{error}</p>}
      <button
        type="submit"
        disabled={!stripe || processing}
        className="mt-4 w-full py-3 bg-teal-600 text-white rounded-lg disabled:opacity-50"
      >
        {processing ? 'Processing...' : 'Pay now'}
      </button>
    </form>
  );
}

Full checkout integration

'use client';

import { useEffect, useState } from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import { storefront } from '@/lib/storefront';
import { PaymentForm } from '@/components/PaymentForm';

export default function CheckoutPage() {
  const [clientSecret, setClientSecret] = useState<string | null>(null);
  const [stripePromise, setStripePromise] = useState<ReturnType<typeof loadStripe> | null>(null);

  useEffect(() => {
    async function initPayment() {
      const cartId = localStorage.getItem('cartId');
      if (!cartId) return;

      const payment = await storefront.payments.createIntent(
        process.env.NEXT_PUBLIC_LOCATION_ID!,
        cartId
      );

      setClientSecret(payment.clientSecret);
      setStripePromise(
        loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, {
          stripeAccount: payment.stripeAccountId,
        })
      );
    }
    initPayment();
  }, []);

  if (!clientSecret || !stripePromise) {
    return <p>Loading payment...</p>;
  }

  return (
    <Elements stripe={stripePromise} options={{ clientSecret }}>
      <PaymentForm clientSecret={clientSecret} />
    </Elements>
  );
}

Testing payments

Use Stripe’s test card numbers in development:
Card NumberScenario
4242 4242 4242 4242Successful payment
4000 0000 0000 3220Requires 3D Secure authentication
4000 0000 0000 0002Card declined
4000 0025 0000 3155Requires authentication (SCA)
Use any future expiry date and any 3-digit CVC.
Set your environment to use Stripe test keys (pk_test_... / sk_test_...) during development. Crave sandbox locations automatically use Stripe test mode.

Environment variables

Add these to your .env.local:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_key
The Stripe secret key is managed by Crave on the backend — you only need the publishable key on the client.

Next steps