shopper/stripe
Stripe payment integration for the Shopper e-commerce admin panel, adding Stripe as a payment method at checkout so customers can pay by card securely. Handles Stripe payment intents and transactions within the Shopper ecosystem.
shopper/stripe is the Stripe payment driver for Shopper, the Laravel e-commerce admin framework from Shopper Labs. Treat it as a component of the framework rather than a standalone library: it plugs a Stripe gateway into Shopper's payment-method abstraction so checkout, order state, and refunds flow through the same admin UI as any other driver.
First steps
composer require shopperlabs/framework shopper/stripe
.env:
php artisan vendor:publish --tag=shopper-stripe
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
composer.json → autoload namespace and the pinned stripe/stripe-php / framework versionssrc/*ServiceProvider.php → what gets registered, config merges, route/webhook registrationconfig/stripe.php → keys, currency, capture mode, webhook pathsrc/Payments/ or src/Drivers/) → the methods Shopper calls at checkoutroutes/ → the webhook endpoint and its middleware (CSRF exclusion matters)First use case: enable Stripe as a payment method in the Shopper admin, run a test checkout that creates a PaymentIntent, confirm the card, and confirm the order flips to paid only after the payment_intent.succeeded webhook lands — not on the browser redirect.
Drive everything from the order, not the session. Create the PaymentIntent server-side with the order total and attach your own identifiers in metadata (order_id, order_reference). The webhook handler is then a pure lookup, with no dependence on cart/session state that may have expired by the time Stripe calls you.
$intent = $stripe->paymentIntents->create([
'amount' => $order->total, // minor units — see gotchas
'currency' => config('shopper.stripe.currency'),
'metadata' => ['order_id' => $order->id],
'automatic_payment_methods' => ['enabled' => true],
]);
Persist $intent->id on the order/payment record immediately; that column is your join key for webhooks, refunds, and support tickets.
Webhook as the source of truth. Structure the handler as: verify signature → find order by metadata.order_id → guard on current state → transition. Keep it idempotent, because Stripe retries.
$event = \Stripe\Webhook::constructEvent(
$request->getContent(),
$request->header('Stripe-Signature'),
config('shopper.stripe.webhook_secret')
);
match ($event->type) {
'payment_intent.succeeded' => $this->markPaid($event->data->object),
'payment_intent.payment_failed' => $this->markFailed($event->data->object),
'charge.refunded' => $this->syncRefund($event->data->object),
default => null,
};
Keep it behind Shopper's abstraction. Where possible, resolve the gateway through the container or Shopper's payment-method registry rather than new-ing Stripe classes in your controllers. That makes swapping to a test double, or adding a second provider later, a config change instead of a refactor.
Local development loop: use stripe listen --forward-to localhost:8000/stripe/webhook and copy the CLI-printed signing secret into your local env. It is a different secret from the dashboard endpoint's, and it changes each session.
Refunds and partial capture should be initiated from your admin against the stored PaymentIntent/Charge ID, then reconciled by the charge.refunded webhook — never assume the synchronous API response is the final word on order state.
shopperlabs/framework; issues and PRs belong upstream, and commits here can be force-pushed by the split job. Never vendor-patch it in place; fork or override instead.vendor/shopper/stripe/src. Pin the version in composer.json and read the upstream changelog before upgrading — the split gives you no release notes of its own.stripe/stripe-php constraint must agree with the framework version you run. A mismatch surfaces as confusing "class not found" or changed method signatures deep in the driver, not as a clean composer conflict.* 100 silently overcharges.return_url is the classic bug: users close the tab, or 3DS/SCA redirects interrupt the flow. Poll the intent on return for UX, but gate fulfilment on the webhook.200 promptly and push heavy work to a queue. Duplicate deliveries are normal; guard transitions rather than appending.php artisan config:cache after env changes; a stale cached config is the usual cause of "wrong webhook secret" in staging.event.id and event->type for every webhook before parsing. When something "didn't work," the answer is almost always that the event never arrived or failed signature verification — Stripe's dashboard → Webhooks → attempt log tells you which within seconds.How can I help you explore Laravel packages today?