Search

Laravel 8 Stripe card payment integration using paymentintent

post-title

Stripe payment provide payment intent method.  This method will also accept cards which requere 3d secure authentication.

In this article, we will going through create Stripe payment integration without any package. We will use payment intent method to create card payment. We will go through step by step from the scratch. So let's start from creating new Laravel project.

If you have existing project, don't worry. Just skip first part and start on it.

Step 1: Create Laravel application

First of all open Terminal or command prompt and from the workspace, run the below composer command to create new Laravel project.

composer create-project laravel/laravel stripe

Now change directory in Terminal.

cd stripe

Step 2: Stripe account and secret keys

We will need stripe account which accepts payment from the clients. Go through Register process and create a new account. For the article, we will use testing account and testing keys details.

After you have created and logged into stripe dashboard, find the Developer section, and go to API Keys menu for testing secret key. We will use this secret key in controller class.

Step 3: Controller and routes

In the third step, we will create controller and routes to view and handle data. First run the artisan command to create controller class. We will discuss class methods in the next step.

php artisan make:controller StripeController

In routes/web.php create these three routes, first route for payment form, second route to submit data and communicate with stripe server and third method to get response.

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\StripeController;

Route::get('stripe-form', [StripeController::class, 'form'])->name('stripeForm');
Route::post('stripe-form/submit', [StripeController::class, 'submit'])->name('stripeSubmit');
Route::get('stripe-response/{id}', [StripeController::class, 'response'])->name('stripeResponse');

Step 4: Create form method in controller and view file.

In this step, first create form() method in StripeController class. This method will create form where client will input class data.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class StripeController extends Controller
{
    /**
     * show payment page
     *
     * @return void
     */
    public function form()
    {
        return view('form');
    }
}

Also create form.blade.php file in resource/views directory. Add these HTML code for view. 

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Stripe form</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <div class="mt-3">
            <h1>Stripe form</h1>
            <p class="lead">Amount: INR100</p>
            <form method="post" action="{{ route('stripeSubmit') }}" class="row">
                <input type="hidden" name="amount" value="100">
                <input type="hidden" name="currency" value="INR">
                @csrf
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="first_name" class="col-sm-2 col-form-label">First name</label>
                        <div class="col-sm-10">
                            <input type="text" id="first_name" class="form-control" name="first_name">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="last_name" class="col-sm-2 col-form-label">Last name</label>
                        <div class="col-sm-10">
                            <input type="text" id="last_name" class="form-control" name="last_name">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="email" class="col-sm-2 col-form-label">Email</label>
                        <div class="col-sm-10">
                            <input type="text" id="email" class="form-control" name="email">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="phone" class="col-sm-2 col-form-label">Phone</label>
                        <div class="col-sm-10">
                            <input type="text" id="phone" class="form-control" name="phone">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="postal_code" class="col-sm-2 col-form-label">Postal code</label>
                        <div class="col-sm-10">
                            <input type="text" id="postal_code" class="form-control" name="postal_code">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="line1" class="col-sm-2 col-form-label">Address</label>
                        <div class="col-sm-10">
                            <input type="text" id="line1" class="form-control" name="line1">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="city" class="col-sm-2 col-form-label">City</label>
                        <div class="col-sm-10">
                            <input type="text" id="city" class="form-control" name="city">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="state" class="col-sm-2 col-form-label">State</label>
                        <div class="col-sm-10">
                            <input type="text" id="state" class="form-control" name="state">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="country" class="col-sm-2 col-form-label">Country</label>
                        <div class="col-sm-10">
                            <input type="text" id="country" class="form-control" name="country">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="card_no" class="col-sm-2 col-form-label">Card no</label>
                        <div class="col-sm-10">
                            <input type="text" id="card_no" class="form-control" name="card_no">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="exp_month" class="col-sm-2 col-form-label">Card Exp. month</label>
                        <div class="col-sm-10">
                            <input type="text" id="exp_month" class="form-control" name="exp_month">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="exp_year" class="col-sm-2 col-form-label">Card Exp. year</label>
                        <div class="col-sm-10">
                            <input type="text" id="exp_year" class="form-control" name="exp_year">
                        </div>
                    </div>
                </div>
                <div class="col-6">
                    <div class="mb-3 row">
                        <label for="cvc" class="col-sm-2 col-form-label">CVV</label>
                        <div class="col-sm-10">
                            <input type="text" id="cvc" class="form-control" name="cvc">
                        </div>
                    </div>
                </div>
                <input type="submit" name="Submit" class="btn btn-primary">
            </form>
            <div class="card mt-3">
                <div class="card-body">
                    <p class="lead">Stripe docs links:</p>
                    <p>Test card available here: <a href="https://stripe.com/docs/testing#cards" target="_blank">Link</a></p>
                    <p>Payment method request docs: <a href="https://stripe.com/docs/api/payment_methods/create" target="_blank">Link</a></p>
                    <p>Payment intent request docs: <a href="https://stripe.com/docs/api/payment_intents/create" target="_blank">Link</a></p>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

This will generate form as below.

Step 5: Add submit() method

Now create submit() method, which will handle data of the form. In this method you can see, we have two curl method, one to create payment method and second to create payment intent. For curl code, we have created seperate curlPost() method instead of putting code in both request.

Copy stripe secret key from the stripe dashboard and add as constant at the top of class define. Also add one constant for stripe api request base URL.

Throughout the method, we have added comment where it is required. You can modify the code as per your required. Also you need to add code for saving data into your database as per your tables.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class StripeController extends Controller
{
    const BASE_URL = 'https://api.stripe.com';
    const SECRET_KEY = 'sk_test_XXXXXXXXXXXXXXXXXXXXXXX';

    /**
     * submit payment page
     *
     * @return void
     */
    public function submit(Request $request)
    {
        $input = $request->validate([
            'card_no' => 'required',
            'exp_month' => 'required',
            'exp_year' => 'required',
            'cvc' => 'required',
            'city' => 'required',
            'state' => 'required',
            'country' => 'required',
            'line1' => 'required',
            'postal_code' => 'required',
            'email' => 'required',
            'first_name' => 'required',
            'last_name' => 'required',
            'phone' => 'required',
            'amount' => 'required',
            'currency' => 'required',
        ]);

        $input['transaction_id'] = \Str::random(18); // random string for transaction id

        // save to database
        // it is recomended to save before sending data to stripe server
        // so we can verify after return from 3ds page
        // \DB::table('transactions')
        //     ->insert($input);

        // create payment method request
        // see documentation below for options
        // https://stripe.com/docs/api/payment_methods/create
        $payment_url = self::BASE_URL.'/v1/payment_methods';

        $payment_data = [
            'type' => 'card',
            'card[number]' => $input['card_no'],
            'card[exp_month]' => $input['exp_month'],
            'card[exp_year]' => $input['exp_year'],
            'card[cvc]' => $input['cvc'],
            'billing_details[address][city]' => $input['city'],
            'billing_details[address][state]' => $input['state'],
            'billing_details[address][country]' => $input['country'],
            'billing_details[address][line1]' => $input['line1'],
            'billing_details[address][postal_code]' => $input['postal_code'],
            'billing_details[email]' => $input['email'],
            'billing_details[name]' => $input['first_name'].' '.$input['last_name'],
            'billing_details[phone]' => $input['phone'],
        ];

        $payment_payload = http_build_query($payment_data);

        $payment_headers = [
            'Content-Type: application/x-www-form-urlencoded',
            'Authorization: Bearer '.self::SECRET_KEY
        ];

        // sending curl request
        // see last function for code
        $payment_body = $this->curlPost($payment_url, $payment_payload, $payment_headers);

        $payment_response = json_decode($payment_body, true);

        // create payment intent request if payment method response contains id
        // see below documentation for options
        // https://stripe.com/docs/api/payment_intents/create
        if (isset($payment_response['id']) && $payment_response['id'] != null) {

            $request_url = self::BASE_URL.'/v1/payment_intents';

            $request_data = [
                'amount' => $input['amount'] * 100, // multiply amount with 100
                'currency' => $input['currency'],
                'payment_method_types[]' => 'card',
                'payment_method' => $payment_response['id'],
                'confirm' => 'true',
                'capture_method' => 'automatic',
                'return_url' => route('stripeResponse', $input['transaction_id']),
                'payment_method_options[card][request_three_d_secure]' => 'automatic',
            ];

            $request_payload = http_build_query($request_data);

            $request_headers = [
                'Content-Type: application/x-www-form-urlencoded',
                'Authorization: Bearer '.self::SECRET_KEY
            ];

            // another curl request
            $response_body = $this->curlPost($request_url, $request_payload, $request_headers);

            $response_data = json_decode($response_body, true);

            // transaction required 3d secure redirect
            if (isset($response_data['next_action']['redirect_to_url']['url']) && $response_data['next_action']['redirect_to_url']['url'] != null) {

                return redirect()->away($response_data['next_action']['redirect_to_url']['url']);
            
            // transaction success without 3d secure redirect
            } elseif (isset($response_data['status']) && $response_data['status'] == 'succeeded') {

                return redirect()->route('stripeResponse', $input['transaction_id'])->with('success', 'Payment success.');

            // transaction declined because of error
            } elseif (isset($response_data['error']['message']) && $response_data['error']['message'] != null) {
                
                return redirect()->route('stripeResponse', $input['transaction_id'])->with('error', $response_data['error']['message']);

            } else {

                return redirect()->route('stripeResponse', $input['transaction_id'])->with('error', 'Something went wrong, please try again.');
            }

        // error in creating payment method
        } elseif (isset($payment_response['error']['message']) && $payment_response['error']['message'] != null) {

            return redirect()->route('stripeResponse', $input['transaction_id'])->with('error', $payment_response['error']['message']);

        }
    }

    /**
     * create curl request
     * we have created seperate method for curl request
     * instead of put code at every request
     *
     * @return Stripe response
     */
    private function curlPost($url, $data, $headers)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $response = curl_exec($ch);

        curl_close ($ch);

        return $response;
    }
}

Step 6: Add response() method and response view

In this last step, add response() method in the same class which will show response of the payment. If the response is after 3d secure redirect, we will first get payment intent object and check status of the transaction. Here you also need to update the payment status in the database.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class StripeController extends Controller
{
    /**
     * response from 3ds page
     *
     * @return Stripe response
     */
    public function response(Request $request, $transaction_id)
    {
        $request_data = $request->all();

        // if only stripe response contains payment_intent
        if (isset($request_data['payment_intent']) && $request_data['payment_intent'] != null) {

            // here we will check status of the transaction with payment_intents from stripe server
            $get_url = self::BASE_URL.'/v1/payment_intents/'.$request_data['payment_intent'];

            $get_headers = [
                'Authorization: Bearer '.self::SECRET_KEY
            ];

            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $get_url);
            curl_setopt($ch, CURLOPT_HTTPHEADER, $get_headers);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

            $get_response = curl_exec($ch);

            curl_close ($ch);

            $get_data = json_decode($get_response, 1);

            // get record of transaction from database
            // so we can verify with response and update the transaction status
            // $input = \DB::table('transactions')
            //     ->where('transaction_id', $transaction_id)
            //     ->first();

            // here you can check amount, currency etc with $get_data
            // which you can check with your database record
            // for example amount value check
            if ($input['amount'] * 100 == $get_data['amount']) {
                // nothing to do
            } else {
                // something wrong has done with amount
            }

            // succeeded means transaction success
            if (isset($get_data['status']) && $get_data['status'] == 'succeeded') {

                return view('response')->with('success', 'Payment success.');

                // update here transaction for record something like this
                // $input = \DB::table('transactions')
                //     ->where('transaction_id', $transaction_id)
                //     ->update(['status' => 'success']);

            } elseif (isset($get_data['error']['message']) && $get_data['error']['message'] != null) {
                
                return view('response')->with('error', $get_data['error']['message']);

            } else {

                return view('response')->with('error', 'Payment request failed.');
            }
        } else {

            return view('response')->with('error', 'Payment request failed.');

        }
    }
}

Create a blade file response.blade.php in resource/views folder.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Stripe response</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <div class="mt-3">
            <h1>Stripe response</h1>
            @if(\Session::has('error'))
                <div class="alert alert-danger" role="alert">{{ \Session::get('error') }}</div>
            @endif
            @if(\Session::has('success'))
                <div class="alert alert-success" role="alert">{{ \Session::get('success') }}</div>
            @endif
        </div>
    </div>
</body>
</html>

That's it. Now everything coding part is done. The last step is to test your integration. Run the project with artisan command. Find the test stripe card in the documentation.

php artisan serve

In your browser go to the link http://localhost:8000/stripe-form and submit the form. If your integration is right, you will get success response.

I hope it will help you.