Stripe Payment Integration, Flutter + Laravel

Table of contents

Seriously, all I am doing is copy-pasting the code for later use.

If you want to copy all these, feel free to. However, there will be pretty few explanations of the things here.

There are a lot of config changes that need to be made on Flutter. The necessary requirements are mentioned here:

https://github.com/flutter-stripe/flutter_stripe#android

MainActivity.kt

package com.mydvls.ticketingapp

import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.android.FlutterFragmentActivity // newly added line

class MainActivity : FlutterFragmentActivity() { // updated line
}

Find the styles.xml files under, values-night, values, and values-v31, or any other folder with the name values, in the android/app/main/res/ directory.

There'll be a 'style' XML tag. That'll be something like this:

<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
        <item name="android:windowBackground">?android:colorBackground</item>
    </style>

Change it to this;

<style name="NormalTheme" parent="Theme.MaterialComponents"> <!-- only this bit has changed -->
        <item name="android:windowBackground">?android:colorBackground</item></style>

Make sure your Android and Kotlin versions are as specified below.

That's it for android.
You may work with the ios configuration too.

Now, we can get to the dart code part. But first, you need to get your stripe publishable key in place.

In your .env file in the main directory, define an environment variable: call it anything you like.

STRIPE_PUBLISHABLE_KEY="your publishable key here"

We'll be using two packages for this blog in particular.

  1. dotenv ( to retrieve the environment variables )

  2. flutter_stripe ( to interact with the stripe API )

In your main.dart file:

...
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_stripe/flutter_stripe.dart';

void main() async {
    await dotenv.load(fileName: '.env');
    Stripe.publishableKey = dotenv.env['STRIPE_PUBLISHABLE_KEY'];

    ...
}

We are indeed ready to interact with the backend. but wait, we haven't configured the backend yet. Let's do that now.

laravel stuff

Let's set up the context first before we directly dive into the code:

Let's say that we have tickets that can be sold to individual users; Let's say that those tickets may be for some particular event:

In this case, you may need the following migrations:

php artisan make:migration create_users_table // already present I presume
php artisan make:migraiton create_events_table
php artisan make:migration create_tickets_table
php artisan make:migration create_event_payment_intent_table

I could make a database schema illustration as to how these tables are related, but I'm too lazy to include those in this post.

But, for now, perhaps the following table will suffice:

userseventsticketsevent_payment_intent
idididintent_id
priceuser_idevent_id
events_iduser_id

Please note that these are only the fields that we'll need for this demo. There may be other fields that'll be needed for your app. ( obviously )

I am guessing you can already imagine the relationship structure, so let's move on.

Create a controller that'll accept requests regarding payments/ ( buying of tickets ). For this example, I can use a more generic name. how about TicketController.php

How unique indeed. And also, don't forget to create models for those tables.

php artisan make:model EventPaymentIntent
php artsian make:model Ticket
php artisan make:model Event

and for god's sake, include the fields mentioned, in the protected $fillable property.

TicketController.php

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\TicketResource; // don't worry about this, create and use it if you like, let it be if not needed.
use App\Models\EventPaymentIntent;
use App\Models\Ticket;
use App\Models\Event;
use Illuminate\Http\Request;
use Str;
use Stripe;

class TicketController extends Controller
{

    // our controller methods will go here;
}

... and how could I forget, in your .env file, make sure you've got the secret key setup.

STRIPE_SECRET_KEY="Your secret key here"

In your config/services.php file, add this:

... 
'stripe' => [
     'STRIPE_SECRET_KEY' => env('STRIPE_SECRET_KEY'),
]
...

now for the methods:

(1) -> paymentIntent

public function paymentIntent(Request $request)
    {
        $request->validate([
            'event_id' => 'required|int',
        ]);
        $event = Event::findOrFail($request->event_id);

        $stripeSecret = config('services.stripe.STRIPE_SECRET_KEY');

        $stripeClient = new \Stripe\StripeClient(
            $stripeSecret
        );

        $intentResponse = $stripeClient->paymentIntents->create([
            'amount' => $event->price * 100,
            'currency' => 'usd',
            'automatic_payment_methods' => [
                'enabled' => true,
            ],
        ]);

        // store the intent id in the database so that we can identify the user and event assocaited with the payment
        EventPaymentIntent::create([
            'intent_id' => $intentResponse->id,
            'event_id' => $event->id,
            'user_id' => auth()->user()->id,
        ]);

        return response()->json([
            'data' => [
                'intent' => $intentResponse
            ],
            'success' => true,
        ], 200);
    }

Here, we are:

  1. checking for the presents of event_id in requests

  2. getting the event model

  3. getting the STRIPE_SECRET_KEY from env

  4. instantiating StripeClient with the secret key

  5. creating a payment intent, using the stripe client.

  6. creating a record of EventPaymentIntent in our own database ( we'll use it later for identifying the event and user associated with the payment intent )

  7. returning a response with the payment intent

For the next method, we'll be accepting webhook requests from Stripe itself.

You may want to refer to the following documentation(s):

https://stripe.com/docs/webhooks

https://stripe.com/docs/webhooks/test

  public function stripePaymentComplete(Request $request)
    {
        new \Stripe\StripeClient(
            config('services.stripe.STRIPE_SECRET_KEY')
        );
        $sig_header = $request->header('stripe-signature');

        $stripeEvent = null;

        try {
            $stripeEvent = \Stripe\Event::constructFrom($request->toArray(), $sig_header);
        } catch (\UnexpectedValueException $e) {
            http_response_code(400);
            exit();
        }


        switch ($event->type) {
            case 'payment_intent.succeeded':
                $paymentIntent = $stripeEvent->data->id;
                $getPaymentintent = EventPaymentIntent::where('intent_id', $paymentIntent)->first();
                Ticket::create([
                    'event_id' => $getPaymentintent->event_id,
                    'user_id' => $getPaymentintent->user_id,
                    'code' => 'TICKET-' . Str::random(50),
                ]);
                return 'PaymentIntent was successful!';

            default:
                return 'Received unknown event type ' . $event->type;
        }
    }

I'm too tired to explain what's going on in here.

It's been 1.5 hours since I've begun writing this blog. you can't blame me.

Be happy with what you have.

Uff, jezz, too tired. I'm going to just copy-pastes code now, not even going to explain.

for testing: get the stripe cli => https://stripe.com/docs/stripe-cli

stripe login
stripe listen --forward-to localhost:8000/api/ticket/stripe/webhook

In a new terminal instance:

stripe trigger payment_intent.succeeded

... and please do not expect any of this to work. --- WITHOUT REGISTERING THE ENDPOINTS.

api.php

Route::post('/ticket/stripe/webhook', [App\Http\Controllers\Api\TicketController::class, 'stripePaymentComplete']);

 Route::post('/ticket/buy', [App\Http\Controllers\Api\TicketController::class, 'paymentIntent'])->middleware('auth.sanctum');

we've come to the last bits of this blog:

and if I keep explaining there'll be a lot of explaining to do:

so here just get some guidelines as to what to do:

  1. send a request to the /ticket/buy endpoint. make sure you make an authenticated request.

  2. suppose you parse the data into a variable called 'response'.
    Get the payment intent client secret with:
    response['data']['intent']['client_secret']

  3. Call the following function after a button click or something:

  4. 
       Future<void> stripePaymentSheet(String paymentIntentClientSecret) async {
         await Stripe.instance.initPaymentSheet(
             paymentSheetParameters: SetupPaymentSheetParameters(
           paymentIntentClientSecret: paymentIntentClientSecret, // this is where we need to pass the client_secret to.
           merchantDisplayName: 'Ticketing App',
         ));
         await Stripe.instance.presentPaymentSheet().then((value) {});
       }
    

And there you go, everything you need is complete. adios, sayonara, I don't know any more words. ✌