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:

I expect you to be familiar with a basic understanding of vue, how components work, 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.

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.

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.

The first step is to add the stripe JS CDN link to index.html file inside the <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 Dashboard → Developers → API Keys → publishable key (pk_test_******).

Note: Make sure to replace with 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 the mounted() function using the publishable key.

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…

The first thing to do 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, a placeholder and so. For more info have a look at the stripe elements page.

After that, attach custom element #card-number-element to the 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 at the top of the form next.

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.

Then, 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!

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 and declare placeOrderButtonPressed function inside methods:{} .

Invoke createToken method on the stripe object by passing any one of the three stripe elements as an 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 and create another property called amount to the data() object and give it an initial value of 25, then bind it to the template where it says 25.

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

It’s important to know that you 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 what other properties you can add to it here.

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

Thanks to Firebase Cloud Functions 🙂 I do not have to create my own server for this demo.

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 that it’s out of the way…

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

The first step is to import Firebase.

import firebase from 'firebase'

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 on 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, declare a reference to the collection called charges .

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

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

Note: For testing purposes, I have set Firebase Security Rules in the Rules Tab on Firebase console so 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.

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.

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

Go ahead log into your Stripe account and go to Dashboard → Developers → API Keys → Secret key (sk_test_******).

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

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

Time to write an actual cloud function called createStripeCharge.

This function will be triggered when there is a new document added to the charges collection which is exactly what onCreate is for.

As you can see in 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 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 a success 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.

This lets you 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 you to talk to third-party API with the FREE ( spark ) plan at least at the time of writing this article. You 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 in the screenshot below which means the payment has gone through.

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

Time to show 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 you want to see in this article?

Wouldn’t it be cool if a user could save a credit card (or multiple cards) and reuse one of them the next time without having to give all the information again?

Let me know your thoughts in the comment section below.

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