Build A Custom Payment/Checkout Form using Stripe Elements + Cloud Functions

   Raja Tamil • Jun 13 •

In this tutorial, I will be showing you how to build a custom payment/checkout form using stripe elements with STEP by STEP instructions.

After that, I am going to be using the firebase cloud function to talk to stripe API in order to create a charge.

At the end of the tutorial, you’ll be able to build a payment form fully functional like this below

stripe-element-cloud-functions

I expect you to be familiar with a basic understanding of vue and how components work and how to Create A Project in Firebase  and Make Queries to the Cloud Firestore.

Let’s get started!

STEP #1 Download Starter Vue Project
STEP #2 Instantiate Stripe Object
STEP #3 Create and Mount Custom Elements to Stripe Elements
STEP #4 Show Stripe Validation Error Messages 
STEP #5 Create A Stripe Token 
STEP #6 Save StripeObject Data to Firestore
STEP #7 Trigger Firebase Cloud Function to Create A Charge

I have also created an infographic which depicts the workflow.

 

STEP #1 Download Starter Vue Project

I have already created a Vue project with some simple HTML and CSS code and you can download it here .

When you run the project, you should have a page with the form like the screenshot below. 

As you can see on the Checkout.vue template, .card-element divs are not input fields.

Because stripe will attach the input field to each .card-element div behind the scene with some client-side validation when they are mounted to stripe elements.

STEP #2 Instantiate Stripe Object

The First step is to add the stripe JS CDN link to index.html file inside <head> tag

<script src="https://js.stripe.com/v3/"></script>

Then, 

Get a stripe publishable key by going to your Stripe account then go to DashboardDevelopersAPI Keys → publishable key (pk_test_******).

Note: Make sure to replace your own publishable key otherwise it won’t work.

Once that’s out of the way…

The final step would be to instantiate the stripe object inside mounted() function using the publishable key. 

And assign it to the stripe property that is declared on the data() object 

export default {
 data() {
  return {
  stripe:null,
  }
 },
 mounted() {
  this.stripe = Stripe("pk_test_******")
  this.createAndMountFormElements()
 },
 methods: {
  createAndMountFormElements() {
 
  }
 }
}

As you can see, I have also invoked a function called createAndMountFormElements() in which I will be creating stripe elements and mount custom HTML .card-element elements to stripe elements. 

let’s see how to do that next…

STEP #3 Create and Mount Custom Elements to Stripe Elements

The first thing is to create an element object by invoking elements() method on the stripe object inside createAndMountFormElements(), like so

var elements = this.stripe.elements();

And, add these three properties on the data object. 

data() {
...,
cardNumberElement:null,
cardExpiryElement:null,
cardCVCElement:null
}

Then, create a stripe element by calling create() method on the elements object by passing a name called cardNumber as a first argument and assign it to this.cardNumberElement

Note: You can also pass a javascript object as a second argument in which you can add some extra styling, placeholder and so, for more info have a look the stripe elements page.

After that, attach custom element #card-number-element to stripe element this.cardNumberElement object using mount() method

createAndMountFormElement(){

var elements = this.stripe.elements();

this.cardNumberElement = elements.create("cardNumber");
this.cardNumberElement.mount("#card-number-element");

this.cardExpiryElement=elements.create("cardExpiry");
this.cardExpiryElement.mount("#card-expiry-element");

this.cardCvcElement=elements.create("cardCvc");
this.cardCvcElement.mount("#card-cvc-element");
}

Do the same for cardExpriyElement and cardCvcElement.

if you run the application at this stage, the form should look the same as before.

Now, the inputs are editable. 

As you can see, stripe elements are already giving some validation out of the box. For Example, making the input values red when they’re invalid.

Nice!

It also gives an error message when there is an invalid input. 

let’s see how to show it to the top of the form next

STEP #4 Show Stripe Validation Error Message

First, attach change event to the stripe elements.

this.cardNumberElement.on("change", this.setValidationError);
this.cardExpiryElement.on("change", this.setValidationError);
this.cardCVCElement.on("change", this.setValidationError);

Next, create a property called stripeValidationError:null inside the data() object.

And, define the change event function called setValidationError inside methods:{} object in which set stripeValidationError property using event.error.message.

setValidationError(event) {
      this.stripeValidationError = event.error ? event.error.message : "";
 },

Finally, show the error at the top of the form

<div class="error red center-align white-text”
   {{stripeValidationError}}
</div>

As you can see in the <template>, there is an error HTML element already in place, you just need to bind the property inside it.

Pretty straight forward eh!

STEP #5 Create Stripe Token

First, attach a click event to place order button.

<div class="col s12 place-order-button-block">
 <button class="btn col s12 #e91e63 pink" @click="placeOrderButtonPressed">PlaceOrder</button>
</div>

Then, go ahead declare placeOrderButtonPressed function inside methods:{}

And, Invoke createToken method on the stripe object by passing any one of the three stripe elements as argument which will give back a promise.

In that response, you will have either an error object or the token object. 

 
this.stripe.createToken(this.cardNumberElement).then(result => {
    if (result.error) {
       this.stripeValidationError = result.error.message;
    } else {
          var stripeObject = {
               amount: this.amount,
               source: result.token
           }

          this.saveDataToFireStore(stripeObject)
    }
});

Note: Inside createToken function, I can also check server side card errors such as insufficient funds, etc. by using stripe test cards.

Go ahead create another property called amount to the data() object and give it an initial value 25, then bind it to the template where it says 25.

Inside the else block, I have defined a stripeObject and ready to send it to the server.

It’s important that you would need at least three stripe properties which are amount, source and currency in order to create a charge.

So, I will be adding the currency property on the server end for fun but you could add it in here as well. 

Note: Stripe request object is not just limited to three properties. Have a look at it what are the other properties you can add it to it here.

As you know, stripe only creates charges on the server-side.

Thanks to Firebase Cloud Functions! 🙂

So that I do not have to create my own server for this demo. 

STEP #6 Save StripeObject Data to Firestore

Install Firebase to your project.

npm install --save firebase

Next, go ahead create a firebase project and obtain configuration code.

After that, initialize Firebase on main.js

import firebase from 'firebsae'
var firebaseConfig = {
apiKey: "******************",
authDomain: "******************",
databaseURL: "******************",
projectId: "******************",
storageBucket: "******************",
messagingSenderId: "******************",
appId: "******************"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

Note: If you want to know how to create a firebase project in order to get the configuration data, check it out here

Now, it’s out of the way…

Next is to save stripeObject to the Firestore Database inside checkout.vue

The first step is to import firebase 

import firebase from 'firebsae'
Then, declare place order button click callback function inside methods:{} object.
saveDataToFireStore(stripeObject) {
 const db = firebase.firestore()
 const chargesRef = db.collection("charges")
 const pushId = chargesRef.doc().id
 db.collection("charges").doc(pushId).set(stripeObject)
}

In the above code, I have added a new document to the Firestore. If you want to brush up how queries work on the Firestore, you can check out Firestore Querying and Filtering Data for Web [A Complete Guide]

Inside the function, I have defined an instance of Firestore database called db. Also, declared a reference to the collection called charges.

It’s cool that Firebase lets to create a unique id (push key) manually so that I can use the id to save data as well as I can make another query later with the same id without any additional queries.   

Finally, save data using set() method by passing the stripeObject.

Note: For the testing purposes, I have set Firebase Security Rules in the Rules Tab on Firebase console that anyone can read and write, but in a real-world scenario, you MUST have security rules in place according to what you’re doing.

Go to your Firestore database and you can see the new document added successfully. 

07 Trigger Firebase Cloud Function to Create A Charge

You need to create a Node.js environment (A different project) in order to write cloud functions.

Let’s create the environment first.

Open up your terminal and create a folder with the name of your choice,

Then, CD to it and run the command

npm install -g firebase-tools

After that, run the login command

firebase login

if you have already logged in with your firebase account, initialize firebase functions

firebase init functions

After that, it will show you all the Firebase projects that you have created under the account. 

And, once you have chosen a Firebase Project, it will ask what language would you like to use to write Cloud Functions?

Choose JavaScript

Say, No to ESLint

Finally, say Yes to install dependencies with npm. After a few seconds, the project is ready to use!

Time to install Stripe Server SDK to this project.

To do that, CD to the functions folder in your terminal and run

npm install --save stripe

Then,

Go ahead log into your stripe account then go to DashboardDevelopersAPI Keys → Secret key (sk_test_******).

stripe-api-keys-secret-key

Once you have the secret key, go to Functions folder  → open up index.js, add the following code

const stripe = require('stripe')('sk_test_***9iJcKEldcYX3*****')

Time to write an actual cloud function called createStripeCharge.

And, this function will be trigger when there is a new document added to the charges collection which is exactly onCreate is for.

As you can see the code below, I am using try-catch block so that if there is an error talking to stripe API,  I can get that error object and write back to this document.

Now, I need to get the stripeObject data from the Firestore.

Luckily, I can access the newly added document to the Firestore inside my cloud function using snap.data()

As I mentioned earlier, I am going to create a request javascript object with three mandatory properties which are amount, source and currency.

I need to multiply the dollar amount by 100 as Stripe only accepts cents.

exports.createStripeCharge = functions.firestore
    .document('charges/{pushId}')
    .onCreate(async (snap, context) => {
    try {
        const charge = {
            amount : snap.data().amount * 100,
            source: snap.data().source.id,
            currency: 'cad'
        }
        const idempotencyKey = context.params.pushId
        const response = await stripe.charges.create(charge, {
            idempotency_key: idempotencyKey
        })

        await snap.ref.set(response, {
            merge: true
        })

    }  catch (error) {
        await snap.ref.set({
            error: userFacingMessage(error)
        }, {
            merge: true
        })
    }
})

function userFacingMessage(error) {
    return error.type ? error.message : 'An error occurred, developers have been alerted';

}

It’s very important to send idempotencyKey when creating a charge.

idempotencyKey is a computer science concept that stripe uses to make sure that to charge a credit card only once as long as the token and the amount is the same when sending multiple requests for some reason.

idempotencyKey has to be unique so I am assigning the pushId which can be obtained using context.params.pushId.

The Final and Important step is to create a charge…

To do that, invoke stripe.charges.create() method passing charge object as well as idempotencyKey which will give a response back whether the payment is successful or failure. 

After that, write back the response to the same document using snap.ref.set() method.

If there is an error,  capture it in the catch block and write back the error message to the document as well.

So, that you can track what’s going on when firebase cloud function is talking to Stripe API.

Finally, deploy your cloud function to live,

To do that, CD to your cloud functions folder, then run

firebase deploy --functions

This will take a few seconds then you will get a Deploy Complete! message.

Now, go to the application, hit place an order.

And you may get an error 

Billing account not configured. External network is not accessible and quotas are severely limited. Configure billing account to remove these restrictions

This is because…

Firebase does not allow to talk to third-party API with the FREE (spark) plan at least at this time of writing this article. You would need to have a paid plan in order to make it work

Once you change the plan, hit the place order button,

And… you can see a succeeded message like the screenshot below which means payment has gone through.

You can also go to Stripe Dashboard  → Payments to check the payment as well.

stripe-payment-succeeded

 

Time to how the response message to the user from the Firestore Database. 

If yes, show the error message as an alert message. 

saveDataToFireStore(stripeObject) {
  ...
  chargesRef.doc(pushId).onSnapshot(snapShot => {
    const charge = snapShot.data();
  
    if (charge.error) {
      alert.log(charge.error);
      chargesRef
      .doc(pushId)
      .delete();
      return;
     }
     if (charge.status && charge.status == "succeeded") {
         console.log(charge.status);
    }
  })
}

To do that, listen for any changes happening to the document using  onSnapShot method.

Once you get the charge data using snapShot.data(), you can check if there is an error property in that charge object.

 

 

if it’s succeeded, you can show the message using charge.status property.

 

That’s it!

Now, You know how to create your own custom payment/checkout form that matches your theme using stripe elements. 

After that, I showed you how to create a stripe token and send it to the server using the firebase cloud function to make a charge without creating your own server.

Here is the full source code 

What are the other functionalities do you want to see in this article?

Would that be cool when a user can save a credit card (or multiple cards) and reuse one of them next time without giving all the information?

let me know your thoughts on the comment section below.

NEXT, Build A Login Page with FirebaseUI for Vue in 20 mins

Sharing is caring!