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
Cart is finalized
The customer has added items, set fulfillment, and provided their details.
Create a payment intent
Your server calls the Crave API to get a Stripe clientSecret.
Collect payment on the client
Stripe.js securely collects card details using the clientSecret.
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:
| Field | Description |
|---|
clientSecret | The Stripe PaymentIntent client secret |
stripeAccountId | The 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 Number | Scenario |
|---|
4242 4242 4242 4242 | Successful payment |
4000 0000 0000 3220 | Requires 3D Secure authentication |
4000 0000 0000 0002 | Card declined |
4000 0025 0000 3155 | Requires 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