Set up future payments on Stripe using Laravel

I use the Laravel PHP framework and love the platform. I’ve been a tech CEO for the last 10 years and wanted to really get deeper on the technology side prior to launching into another startup so I’ve been building a pet project at iCollect.money that has taught me PHP, JavaScript, HTML, Bootstrap, TensorFlow, OpenCV, Postgres, AWS, Forge and numerous other technologies including now–Stripe’s API.

I was originally using Stripe.com to enable one-time payments from customers.  This was extremely easy and straight forward.  However, when I started building out an auction platform I realized that I needed to store payments and I found this a bit more challenging as there were no good articles (something a new developer relies on) that explained how to store payment information for customers prior to charging the customer using Stripes new “Checkout” platform (something an auction platform has to do).  There are many articles (here is a good one recommended by my friends at Laracast -a necessary site for novice Laravel devs like me) that detail how to use Stripe’s legacy platform where your app collects card information from the customer and sends the token to the server to charge it.

However, Stripe’s new Checkout platform requires you to create a Checkout Session on your server then pass the session ID to the client and redirect the customer to Checkout to complete the payment.  This new way of doing things is more secure and opened up a lot of new features (Stripe’s migration guide explains the differences).  So to find a solution, I first looked into Laravel Cashier, however, I found that this is primarily for SaaS subscriptions—something I did not need.

‘Laravel Cashier provides an expressive, fluent interface to Stripe’s subscription billing services. It handles almost all of the boilerplate subscription billing code you are dreading writing. ‘

I then looked around Stripe’s very well documented site and found their tutorial on how to Setup Future Payments and it linked to some great sample code on GitHub. The problem is that the PHP/JavaScript does not use Laravel and it also uses SLIM (a micro-framework).  

So this article will attempt to show you how to do the same thing that is outlined in the GitHub code but using Laravel.  Note that this article assumes you have a basic understanding of Laravel and you already have your Stripe account/API keys and they are in your .env file.  If not, review one of the many legacy setup articles such as the one linked above for how to do this… And don’t forget to install the libraries:

# Install the PHP library via Composer
composer require stripe/stripe-php

Realize, that I am a new developer and I am really not qualified to write this.. however, I both wanted to give back (i’m been only taking at this point) and I wanted to document some of where I was in this journey.

Ok, let’s go…

There are generally 3 things you will need to do, and the remainder of this article will show you how to do each:

  1. Check to see if your Laravel ‘user’ has an associated ‘customer’ account in Stripe
  2. If not, prompt the user to add their payment method (example: credit card)
  3. At some time in the future, after services rendered, charge your customers payment method at Stripe.

To outline how to go about this, we will build a simple app that displays a table with 4 columns with the following features:

  1. Display your app’s ‘Users’ email (column 1) – yes, the sample app assumes you build a user table using Laravel’s auth feature.
  2. Checks Stripe to see if there is a Stripe customer and payment method for that user (column 2)
  3. Has a button to allow you to add that user to Stripe as a customer with a payment method (column 3)
  4. Allows you to charge that account $10.00 (column 4)

The UI will look like the following:

and when a user presses the button in column 3 to add the user and payment method they should see :

and when the user presses the button in column 4 to charge the user they should see:

We will build this demo with 2 files (home.blade.php for the HTML/JavaScript and HomeController.php for the PHP support) and support via a couple others (web.php and VerifyCsrfToken.php). Note: you also need normalize.css and global.css from the original code found here.

Let us review the support files first.

Web.php for setting up the routes 

Essentially there is a route for getting the public key, another for creating the customer, and yet another for charging the customer

Route::get('/', 'HomeController@index');
Auth::routes();
Route::get('/public-key', 'HomeController@publickey')->middleware('auth');
Route::post('/createcustomerinstripe/{email}', 'HomeController@CreateCustomerInStripe')->middleware('auth');
Route::post('/chargethecustomer/{email}/{value2charge}', 'HomeController@ChargeTheCustomer')->middleware('auth');

VerifyCsrfToken.php for excluding route URI from CSRF

If you do not  do this you will get 419 errors.  More here on this.

 protected $except = [
        'stripe/*',
        'createsetupintent/*',
        'createcustomerinstripe/*',
        'chargethecustomer/*'
    ];

This also requires you to add the following to your fetch() calls:

  fetch("/chargethecustomer/"+email+"/"+value2charge, {
    method: "post",
    headers: {      
      'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
      "Content-Type": "application/json"            
    }
  })

Now let us go through the JavaScript and PHP required to support each of these 3 features.

1.      Check to see if your Laravel ‘user’ has an associated ‘customer’ account in Stripe

When the app first initializes the “/” route, the Php Index() function queries the User table and hands the data off to the home.blade.php file.

 public function index()
    {
        $users = User::select('email')->get();
        return view('home', compact('users'));
    }

To check each user, we can easily do this in the HTML with some blade syntax and call our PHP function DoesCustomerExistAtStripe()–see the @if() below. The function returns

0 = there is no account for the customer at Stripe
1 = there is an account but no payment method
2 = there is an account and payment method

@foreach($users as $user)
    <tr>
        <td class="text-center" id="email"> {{$user->email}} </td>                
        @if( App\Http\Controllers\HomeController::DoesCustomerExistAtStripe($user->email) == 2)
        <td class="text-center" id="isInStripeOrNot"> 
                Account+PaymentMethod
            </td>
            <td class="text-center">
                <div class="pl-1">
                   <input class="form-control btn-block btn-secondary savecard" 
                        email="{{$user->email}}" type="submit" value="Card/User already in Stripe" disabled>
                </div>
            </td>
            <td class="text-center">                   
                <div class="pl-1">
                    <input class="form-control btn-block btn-warning chargecard" 
                        email="{{$user->email}}" type="submit" value="Charge Card $10.00">
                </div>
            </td>
            @elseif( App\Http\Controllers\HomeController::DoesCustomerExistAtStripe($user->email) == 1)
            <td class="text-center" id="isInStripeOrNot"> 
                Account "No" PaymentMethod
            </td>
            <td class="text-center">
                <div class="pl-1">
                     <input class="form-control btn-block btn-warning savecard" 
                          email="{{$user->email}}" type="submit" value="User (no card) in Stripe">
                </div>
            </td>
            <td class="text-center">                   
                <div class="pl-1">
                      <input class="form-control btn-block btn-secondary chargecard" 
                           email="{{$user->email}}" type="submit" value="Charge Card $10.00" disabled>
                </div>
            </td>
            @else
            <td class="text-center" id="isInStripeOrNot">                     
                NO Account
            </td>                        
            <td class="text-center">
                <div class="pl-1">
                    <input class="form-control btn-block btn-warning savecard" 
                          email="{{$user->email}}" type="submit" value="Add user card to Stripe">
                </div>
            </td>
            <td class="text-center">                   
                <div class="pl-1">
                     <input class="form-control btn-block btn-secondary chargecard" 
                           email="{{$user->email}}" type="submit" value="Charge Card $10.00" disabled>
                     </div>
            </td>
            @endif                 
    </tr>
@endforeach    

The Php function “DoesCustomerExistAtStripe()” is as follows:

    /****************************************************************

        Check if the customer exists in your Stripe account by 
        looking up the email address and check if there is
        a payment method

    *****************************************************************/

    public static function DoesCustomerExistAtStripe ($email){

        Stripe::setApiKey(env('STRIPE_SECRET'));

	// Search for the Laravel user (note that we passed this in the function) at Stripe
	// more on the customer object here: https://stripe.com/docs/api/customers/object
        $customer = \Stripe\Customer::all([
            'email' => $email,
            'limit' => 1,
        ]);

        if (isset($customer['data']) && count($customer['data'])) {            
        
            // Get customer's payment method.  Read more about them here: https://stripe.com/docs/api/payment_methods
            $paymentMethod = PaymentMethod::all([
                'customer' => $customer['data'][0]->id,
                'type' => 'card'
            ]);

            if(isset($paymentMethod['data'][0]->id)){
                $customerExists = 2;
            }
            else{
                $customerExists = 1;
            }
        
        } else {
            $customerExists = 0;
        }

        return $customerExists;
    }

2.      Prompt the user to add their payment method (example: credit card)

There are 3 sub-steps here:

A. JavaScript asks our server to get the public key.

/*********************(Stripe Setup Support)**********************

    User clicked on the button to save the user and their 
    payment method to Stripe.  This gets the public key and 
    kicks off a call to getSetupIntent() to get the process started

*****************************************************************/

    $(document).on('click','.savecard', function(event) {        
        
        //get the email of the user for the selected row
        var email = $(this).attr('email');
        //update the modal so the user doesn't have to retype the email address
        $('#modalemail').val(email)
        
        fetch("/public-key", {
            method: "get",
            headers: {
              'X-CSRF-TOKEN': document.querySelector(
                              'meta[name=csrf-token]').content,                
                              "Content-Type": "application/json"
            }
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(response) {
            getSetupIntent(response.publicKey, email);
        });    
 
    });

PHP responds

public function publickey () {
      $pub_key = env('STRIPE_KEY');

      // Send publishable key details to client
      return response()->json(array('publicKey' => $pub_key));
    }

B. JavaScript asks the server to create the customer in Stripe

/*****************(Stripe Setup Support)**************************    

    Calls PHP to CREATE customer in Stripe and SetupIntent
    then calls our function stripeElements()

*****************************************************************/

var getSetupIntent = function(publicKey, email) {
  return fetch("/createcustomerinstripe/"+email, {
    method: "post",
    headers: {      
      'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
      "Content-Type": "application/json"            
    }
  })
  .then(function(response) {      
      return response.json();
  })
  .then(function(setupIntent) {      
      stripeSetupIntent(publicKey, setupIntent);
  });
};

PHP does the work, then responds. Note that the first call (\Stripe\Customer::create) is to search for the email (we can make this stronger in production) and then to set up the intent to pay in the future (basically creating the customer’s record)–done via (\Stripe\SetupIntent::create)

    /****************************************************************

        Create customer in Stripe with a payment method

    *****************************************************************/

    public function CreateCustomerInStripe ($email) {  
        
        Stripe::setApiKey(env('STRIPE_SECRET'));

        // More here: https://stripe.com/docs/api/customers/create 
    
        $customer = \Stripe\Customer::create(
            ['email' => $email]
        );  
  
        // More here: https://stripe.com/docs/api/setup_intents/create  

        $setupIntent = \Stripe\SetupIntent::create([
            'customer' => $customer->id
          ]);        
  
          // Send Setup Intent details to client
          return response()->json($setupIntent);
    }

C. Then we need to show the user a modal, allow them to enter their payment method (credit card, CSV, expiration date), and save that to the user’s account.

This is all done in JavaScript and here are the major calls you need to be aware of:

  • stripe.elements() – Stripe Elements is a set of pre-built UI components, like inputs and buttons, for building your checkout flow.  You can read all about them here.
  • elements.create() – create an instance of the element.
  • card.mount() – attach the element to the DOM
  • card.on() – the only way to communicate with an element is to listen to for an event. We need to listen for the ‘focus’ event. This event is triggered when the Element gains focus.
  • stripe.confirmCardSetup() – then we need to attach the payment method.

/*******************(Stripe Setup Support)************************

    Creates the Stripe Elements for user to type CC info into

*****************************************************************/

var stripeSetupIntent = function(publicKey, setupIntent) {
  var stripe = Stripe(publicKey);  
  var elements = stripe.elements();

  $('#modal-block-entercard').modal();

  // Element styles
  var style = {
    base: {
      fontSize: "16px",
      color: "#32325d",
      fontFamily:
        "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif",
      fontSmoothing: "antialiased",
      "::placeholder": {
        color: "rgba(0,0,0,0.4)"
      }
    }
  };

  var card = elements.create("card", { style: style });  

  card.mount("#card-element");

  // Element focus ring
  card.on("focus", function() {    
    var el = document.getElementById("card-element");
    el.classList.add("focused");
  });

  card.on("blur", function() {
    var el = document.getElementById("card-element");
    el.classList.remove("focused");
  });

  // Handle payment submission when user clicks the pay button.
  var button = document.getElementById("submit");
  button.addEventListener("click", function(event) {
    
    event.preventDefault();
    changeLoadingState(true);
    var email = document.getElementById("email").value;    

    stripe
      .confirmCardSetup(setupIntent.client_secret, {
        payment_method: {
          card: card,
          billing_details: { email: email }
        }
      })
      .then(function(result) {
        if (result.error) {
          changeLoadingState(false);
          var displayError = document.getElementById("card-errors");
          displayError.textContent = result.error.message;
        } else {
          // The PaymentMethod was successfully set up
          orderComplete(stripe, setupIntent.client_secret);          
        }
      });
  });
}; 

3.      At some time in the future, after services rendered, charge your customer’s payment method at Stripe

Now we need to do the same basic sub-steps as above for triggering the payment.

A. Get the public key

/*********************(Stripe Charge Support)**********************

    User clicked on the button to charge the user and thier 
    payment method already saved at Stripe.  This gets the public key and 
    kicks off a call to getPaymentIntent() to get the process started

*****************************************************************/

    $(document).on('click','.chargecard', function(event) {        
        
        //get the email of the user for the selected row
        var email = $(this).attr('email');
        var value2charge = 1000;
                
        fetch("/public-key", {
            method: "get",
            headers: {
            'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
            "Content-Type": "application/json"
            }
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(response) {
            getPaymentIntent(response.publicKey, email, value2charge);
        });    
 
    });

PHP responds

  public function publickey () {
      $pub_key = env('STRIPE_KEY');

      // Send publishable key details to client
      return response()->json(array('publicKey' => $pub_key));
    }

B. JavaScript asks the server to make the charge against the payment method

/*****************(Stripe Charge Support)**************************    

    getPaymentIntent calls PHP to make the PaymentIntent call

*****************************************************************/

var getPaymentIntent = function(publicKey, email, value2charge) {
  return fetch("/chargethecustomer/"+email+"/"+value2charge, {
    method: "post",
    headers: {      
      'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
      "Content-Type": "application/json"            
    }
  })
  .then(function(response) {      
      return response.json();
  })
  .then(function(paymentIntent) {      
      stripePaymentIntent(publicKey, email, paymentIntent);
  });
};

PHP does the work. We first have to look up the payment method (PaymentMethod::all)

Then we create the array. All those options can be found here. ‘offsession’ and ‘confirm’ are required to charge a saved payment method so don’t forget them like I did the first time… 🙂

    /****************************************************************

        Charge the customer

    *****************************************************************/
    
    public function ChargeTheCustomer ($email, $value2charge){

        // Set up payment data
        $data = [];

        Stripe::setApiKey(env('STRIPE_SECRET'));

        // Get the customer, search by email
        $customer = \Stripe\Customer::all([
            'email' => $email,
            'limit' => 1,
        ]);

        // Get customer's payment method
        $paymentMethod = PaymentMethod::all([
            'customer' => $customer['data'][0]->id,
            'type' => 'card'
        ]);

        //List of options can be found here: https://stripe.com/docs/api/payment_intents/create
        $data['payment_method'] = $paymentMethod['data'][0]->id;        
        $data['amount'] = $value2charge;
        $data['currency'] = 'usd';
        $data['customer'] = $customer['data'][0]->id;
        $data['off_session'] = true;  //Read more here about why we need these paramenters 
                                      //https://stripe.com/docs/payments/save-and-reuse#web-create-payment-intent-off-session 
        $data['confirm'] = true;

        $paymentIntent = PaymentIntent::create($data);

        return response()->json($paymentIntent);        

    }

Then return to JavaScript to clean it all up… and handle the errors if there are any. This doc lists a much better way of handling the errors.

/*******************(Stripe Charge Support)************************

    This function call's Stripes confirmCardPayment API after paymentIntent()
    previously.  The API doc can be found here: 
    https://stripe.com/docs/js/payment_intents/confirm_card_payment 
    then lets the user know if the payment was successful or not

*****************************************************************/

var stripePaymentIntent = function(publicKey, email, paymentIntent) {
  var stripe = Stripe(publicKey);  
  var elements = stripe.elements();
  var cardElement = elements.getElement('card'); //from https://stripe.com/docs/js/elements_object/get_element 
  
  $('#modal-block-chargecardresponse').modal();  

    console.log("PAYEMENT STATUS: ", paymentIntent.status);

    if(paymentIntent.status === "succeeded"){
        $('#modalchargecardresponse_data').html("Payment was successful");  
    }
    else{
        //see better error handeling on step 5 here: https://stripe.com/docs/payments/save-and-reuse#web-create-payment-intent-off-session
        stripe
        .confirmCardPayment(paymentIntent.client_secret, {
            payment_method: {
                card: cardElement,
                billing_details: { name: email },
            },
        })
        .then(function(result) {
            if (result.error) {            
                $('#modalchargecardresponse_data').html("Payment had an error: " + result.error.message);                               
            } else {
                // The Payment was successful 
                $('#modalchargecardresponse_data').html("Payment was successful");                         
            }
        });
    }

}; 

As a review, here is the basic flow:

Note that I added ‘Edit Customer’ above… That is not in the current demo code but you can find out how to easily add this here.

Here is a zipfile you can use that contains all the code mentioned above.

Test it out via the test card’s Stripe provides found here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s