OmniPay Stripe Payment Intent Example

This weekend I was up late in panic fixing support for Secure Customer Authentication. The Omnipay library is excellent, but some of the documentation is lacking a full example on how to implement.

Most of my confusion stems from that Omnipay Strip-plugin is using the manual Payment Intent confirmation flow. There is a pull request in the GitHub repo the mentions this.

The advantage of using the manual payment intents over the automatic is that you don’t need to configure webhooks nor to set up a cron job to validate payments.

Also, you need to use the $gateway->purchase method if you want to charge the card directly.

So here is a quick and dirty pseudo example of how I implemented this. If you have suggestions, please let me know.


class PaymentController extends Controller 
{
    public function begin(Request $request)
    {
        return view('stripe.checkout', [
            'order' => $order,
            'apiKey' => config('stripe.public-key'),
        ]);
    }  

    public function charge(Request $request)
    {
        $paymentMethodId = $request->get('paymentMethodId');

        $gateway = Omnipay::create('Stripe\PaymentIntents');
        $gateway->initialize([
            'apiKey' => config('stripe.secret-key'),
        ]);

        $response = $gateway->purchase([
            'amount'                   => 100,
            'currency'                 => 'SEK',
            'description'              => 'My Payment Inc',
            'paymentMethod'            => $paymentMethodId,
            'returnUrl'                => route('order.stripe.return_url'),
            'confirm'                  => true,
            'metadata'          => [
                'order_id' => $order->id,
            ],
        ])->send();

        $order->payment_reference = $response->getPaymentIntentReference();
        $order->save();

        // Stripe order is OK, profit!
        if ($response->isSuccessful()) {
            $order->markPaid();
    
            return redirect(route('order.show', [
                'id' => $order->hashId(),
            ]));
        // Stripe thinks order needs additional strep
        } elseif ($response->isRedirect()) {
            return redirect($response->redirect());
        }

        // Something else happend, go back
        Session::flash('error', 'Some error occurred. Try again');

        return redirect(route('order.stripe.begin'));
    }

    // This is where you land after Stripe's redirect
    // route 'order.stripe.return_url'
    public function confirm(Request $request, Cart $cart)
    {
        $gateway = Omnipay::create('Stripe\PaymentIntents');
        $gateway->initialize([
            'apiKey' => config('stripe.secret-key'),
        ]);

        $order = Order::where('payment_reference', $request->get('payment_intent'))
            ->firstOrFail();

        $response = $gateway->confirm([
            'paymentIntentReference' => $request->get('payment_intent'),
            'returnUrl' => route('order.stripe.return_url'),
        ])->send();

        if ($response->isSuccessful()) {
            $order->markPaid();

            $cart->clearSession();
            $request->session()->forget(Order::SESSION_KEY);

            return redirect(route('order.show', [
                'id' => $order->id(),
            ]));
        }

        // The response will not be successful if the 3DS authentication process 
        // failed or the card has been declined. Either way, it's back to step (1)!
        // 
        Session::flash('error', 'Some error occurred. Try again');

        return redirect(route('order.stripe.begin'));            
    }
}

Not the Jekyll is filtering out Blade’s open and close brackets. Look for BLADE in the code.


@extends('layouts.app')

@section('header')

    <script src='https://js.stripe.com/v3/' type='text/javascript'></script>

@endsection


@section('content')

    <div class="container">
        <div class="row  justify-content-center">
            <div class="col-sm-8">

                @if (session()->has('error'))
                    <div class="alert alert-danger"></div>
                @endif

                <div class="card">
                    <h4 class="card-header">Your info</h4>
                    <div class="card-body">
                        <div class="card-text">
                            <form action="BLADE --> route('order.stripe.charge')" method="post" id="payment-form">
                                <div>
                                    <label>Card holder</label>
                                    <input id="cardholder-name" class="form-control mb-4" type="text">
                                    <!-- placeholder for Elements -->
                                    <div id="card-element" class="form-control"></div>

                                    <!-- Used to display form errors -->
                                    <div id="card-errors" role="alert"></div>
                                </div>

                                <div class="d-flex flex-row mt-4 justify-content-end align-items-center">
                                    <button id="card-button" class="btn btn-primary">
                                        Pay
                                    </button>
                                </div>
                                <input type="hidden" name="paymentMethodId" id="paymentMethodId">
                            </form>
                        </div>
                    </div>
                </div>
                <div class="text-center">
                    <img class="pt-4" style="width: 120px; height: auto" src="" alt="Powered by Stripe">
                </div>
            </div>
        </div>
    </div>

@endsection

@section('footer.scripts')

    <script>

        var style = {
            base: {
                color: '#32325d',
                lineHeight: '1.8rem'
            }
        };

        var stripe = Stripe('BLADE --> $apiKey');

        var elements = stripe.elements();
        var cardElement = elements.create('card', {style: style});
        cardElement.mount('#card-element');

        var cardholderName = document.getElementById('cardholder-name');
        var cardButton = document.getElementById('card-button');
        var paymentMethodIdField = document.getElementById('paymentMethodId');
        var myForm = document.getElementById('payment-form');

        cardButton.addEventListener('click', function(ev) {
            ev.preventDefault();
            cardButton.disabled = true;

            stripe.createPaymentMethod('card', cardElement, {
                billing_details: {name: cardholderName.value }
            }).then(function(result) {

                if (result.error) {
                    cardButton.disabled = false;
                    alert(result.error.message);
                } else {
                    paymentMethodIdField.value = result.paymentMethod.id;
                    myForm.submit();
                }
            });
        });
    </script>

@endsection