How to integrate Stripe into Nuxt.js

Capt. Michael
7 min readOct 7, 2021

--

I’m new to Nuxt.js, so I know what a beginner wants: no bullshit, just code which can be copied & pasted directly, the only thing that needs to be changed is the API key, right? Okey-dokey, here we go!

First of all, assume you’ve already signed up for Stripe, so you need to install Stripe SDK for the client-side by the following command, and better not use any third-party plugin or module.

npm i @stripe/stripe-js

Then you need to append something in your nuxt.config.js file like below:

export default {
modules: ["@nuxtjs/axios", "@nuxtjs/proxy"],
axios: {
proxy: false,
},
build: {
vendor: ["axios"], // Description: Avoid duplicate packaging.
},
env: {
stripePublishableKey: "pk_test_51Jh6b4******",
},
}

In the official document, it uses fetch() to get the response of /create-payment-intent, but I personally prefer the axios way. And you should get your publishable key from the API Keys page, before you activate your account, you can only use test keys.

OK then, it’s time to migrate the official HTML sample to our Nuxt.js project. Create a new CSS file named stripe.css (or another name you want) and put it into /static/css folder under your own project, copy all style definitions from the official sample into stripe.css. And there probably some Stripe style conflict with the existing one, you can make some little changes. For instance, I removed body, and changed form into #payment-form, because they messed my existing style.

/* Variables */
/* * {
box-sizing:border-box;
}
*/
/* body {
font-family:-apple-system,BlinkMacSystemFont,sans-serif;
font-size:16px;
-webkit-font-smoothing:antialiased;
display:flex;
justify-content:center;
align-content:center;
height:100vh;
width:100vw;
}
*/
#payment-form {
width: 30vw;
min-width: 500px;
align-self: center;
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
border-radius: 7px;
padding: 40px;
}
input {
border-radius: 6px;
margin-bottom: 6px;
padding: 12px;
border: 1px solid rgba(50, 50, 93, 0.1);
height: 44px;
font-size: 16px;
width: 100%;
background: white;
}
.result-message {
line-height: 22px;
font-size: 16px;
}
.result-message a {
color: rgb(89, 111, 214);
font-weight: 600;
text-decoration: none;
}
.hidden {
display: none;
}
#card-error {
color: rgb(105, 115, 134);
text-align: left;
font-size: 13px;
line-height: 17px;
margin-top: 12px;
}
#card-element {
border-radius: 4px 4px 0 0;
padding: 12px;
border: 1px solid rgba(50, 50, 93, 0.1);
height: 44px;
width: 100%;
background: white;
}
#payment-request-button {
margin-bottom: 32px;
}
/* Buttons and links */
#submit-button {
background: #5469d4;
color: #ffffff;
font-family: Arial, sans-serif;
border-radius: 0 0 4px 4px;
border: 0;
padding: 12px 16px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
display: block;
transition: all 0.2s ease;
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
width: 100%;
}
#submit-button:hover {
filter: contrast(115%);
}
#submit-button:disabled {
opacity: 0.5;
cursor: default;
}
/* spinner/processing state, errors */
.spinner,
.spinner:before,
.spinner:after {
border-radius: 50%;
}
.spinner {
color: #ffffff;
font-size: 22px;
text-indent: -99999px;
margin: 0px auto;
position: relative;
width: 20px;
height: 20px;
box-shadow: inset 0 0 0 2px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.spinner:before,
.spinner:after {
position: absolute;
content: "";
}
.spinner:before {
width: 10.4px;
height: 20.4px;
background: #5469d4;
border-radius: 20.4px 0 0 20.4px;
top: -0.2px;
left: -0.2px;
-webkit-transform-origin: 10.4px 10.2px;
transform-origin: 10.4px 10.2px;
-webkit-animation: loading 2s infinite ease 1.5s;
animation: loading 2s infinite ease 1.5s;
}
.spinner:after {
width: 10.4px;
height: 10.2px;
background: #5469d4;
border-radius: 0 10.2px 10.2px 0;
top: -0.1px;
left: 10.2px;
-webkit-transform-origin: 0px 10.2px;
transform-origin: 0px 10.2px;
-webkit-animation: loading 2s infinite ease;
animation: loading 2s infinite ease;
}
@-webkit-keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@media only screen and (max-width: 600px) {
form {
width: 80vw;
}
}

Now come to our major goal, build up the Nuxt.js page, let’s name it as index.vue. And let’s go through it briefly.

First, we need to import Stripe-js, there’re two ways to import it, depends on if you need to disable Advanced Fraud Detection. Then set script and CSS file link in headers. And copy all functions from the official sample into here. The first one named loadStripeWhenModalOpens, it’s the initialization of all, and you can invoke it from create() or mounted(). And then invoke other Stripe functions in Nuxt way. Oh, don’t forget to change IDs of controls which you’ve changed at the previous step.

<script>
import { loadStripe } from "@stripe/stripe-js";
// import { loadStripe } from "@stripe/stripe-js/pure";
// loadStripe.setLoadParameters({ advancedFraudSignals: false });
// The loadStripe.setLoadParameters function is only available when
// imorting loadStripe from @stripe/stripe-js/pure.

export default {
head() {
return {
script: [
{
src: "https://js.stripe.com/v3/",
},
],
link: [
{
rel: "stylesheet",
href: "/css/stripe.css",
},
],
};
},
data() {
return {
cardStyle: {
base: {
color: "#32325d",
fontFamily: '"Arial, sans-serif',
fontSmoothing: "antialiased",
fontSize: "16px",
"::placeholder": {
color: "#32325d",
},
},
invalid: {
fontFamily: "Arial, sans-serif",
color: "#fa755a",
iconColor: "#fa755a",
},
},
};
},
created() {
this.loadStripeWhenModalOpens();
},
methods: {
async loadStripeWhenModalOpens() {
if (!stripe) {
stripe = await loadStripe(process.env.stripePublishableKey);
elements = stripe.elements();
}

const purchase = { items: [{ id: "this-is-id" }] };
const that = this;

this.$axios
.post("/create-payment-intent", purchase, {
headers: {
"Content-Type": "application/json",
},
})
.then((createPaymentIntentResponse) => {
const elements = stripe.elements();
const card = elements.create("card", { style: that.cardStyle });
// Stripe injects an iframe into the DOM
card.mount("#card-element");
card.on("change", function (event) {
/*
* Disable the Pay button if there are no card details
* in the Element.
*/
document.querySelector("button").disabled = event.empty;
document.querySelector("#card-error").textContent = event.error
? event.error.message
: "";
});

const form = document.getElementById("payment-form");
form.addEventListener("submit", function (event) {
event.preventDefault();
// Complete payment when the submit button is clicked
AV.Cloud.run("createStripePaymentIntent", {
amount: 3000, // Price in cent.
currency: "usd",
}).then((createStripePaymentIntentResult) => {
that.payWithCard(
stripe,
card,
createStripePaymentIntentResult?.clientSecret
);
});
});
})
.catch((createPaymentIntentError) => {
console.log(
"createPaymentIntentError:",
createPaymentIntentError.message
);
});
},
payWithCard: function (stripe, card, clientSecret) {
this.loading(true);

const that = this;

stripe
.confirmCardPayment(clientSecret, {
payment_method: {
card: card,
},
})
.then(function (confirmCardPaymentResult) {
if (confirmCardPaymentResult.error)
that.showError(confirmCardPaymentResult.error.message);
else that.completeOrder(confirmCardPaymentResult.paymentIntent.id);
});
},
loading(isLoading) {
if (isLoading) {
document.querySelector("button").disabled = true;
document.querySelector("#spinner").classList.remove("hidden");
document.querySelector("#button-text").classList.add("hidden");
} else {
document.querySelector("button").disabled = false;
document.querySelector("#spinner").classList.add("hidden");
document.querySelector("#button-text").classList.remove("hidden");
}
},
showError(errorMessageText) {
this.loading(false);

const errorMessage = document.querySelector("#card-error");
errorMessage.textContent = errorMessageText;

setTimeout(function () {
errorMessage.textContent = "";
}, 4000);
},
completeOrder(paymentIntentId) {
this.loading(false);

document
.querySelector(".result-message a")
.setAttribute(
"href",
`https://dashboard.stripe.com/test/payments/${paymentIntentId}`
);
document.querySelector(".result-message").classList.remove("hidden");
document.querySelector("button").disabled = true;
},
destroyStripeIbanElement() {
const ibanElement = elements?.getElement("iban");

if (ibanElement) ibanElement.destroy();
},
},
beforeDestroy() {
this.destroyStripeIbanElement();
},
};
</script>

<template>
<div class="content-widget">
<div class="container">
<div class="row">
<div class="col-12 archive-header text-center pt-3 pb-3">
<div
class="mt-5 col-lg-6 col-md-4 col-sm-3"
style="display: inline-block"
>
<div id="paypal-button-container" v-if="false" />
<form id="payment-form">
<div id="card-element">
<!-- Stripe.js injects the card element here. -->
</div>
<p id="card-error" role="alert" class="text-danger" />
<button id="submit-button">
<div class="spinner hidden" id="spinner" />
<span id="button-text">Pay now</span>
</button>
<p class="result-message hidden text-success">
Payment succeeded, see the result in your
<a href="" target="_blank">Stripe dashboard. </a>
Refresh the page to pay again.
</p>
</form>
</div>
</div>
</div>
</div>
</div>
</template>

After finish those above, you should be able to see the Stripe payment form on your page.

Stripe Payment Form

Well, let’s focus on the server-side now. The difference between Stripe and PayPal is, you have to deploy a Node.js (or Python, PHP, .NET, Java, etc) project on your server for the creation of payment intent, but this is not a required option for PayPal. By the way, my project is serverless, I don’t want to purchase a server just for this purpose either, even if it’s virtual. I use LeanCloud for the deployment of this function, and LeanCloud itself is a Node.js project (also could be Python, Java or something else), so the only thing I need to do is to install Stripe SDK for Node.js and use it in LeanCloud way. As I’m just mentioning LeanCloud but not recommending it, so I won’t introduce how to get and use it.

npm i stripe --save
/*
* Payment Intent Creation onthe Server-Side
*/

const AV = require("leanengine");
const fs = require("fs");
const path = require("path");
const { response } = require("express");
const stripe = require("stripe")("sk_test_51Jh6b4******");

fs.readdirSync(path.join(__dirname, "functions")).forEach((file) = >{
require(path.join(__dirname, "functions", file))
});

AV.Cloud.define("createStripePaymentIntent",
function(request) {
return stripe.paymentIntents.create({
amount: request.params.amount,
currency: request.params.currency,
}).then((paymentIntent) = >{
return {
clientSecret: paymentIntent.client_secret,
}
})
}
);

You shall be clear this stripe instance in Node.js is different than that one on your client-side. And the secret key for the initialization of stripe is got from the API Keys page as well. You have to do this step because Stripe forbids creating payment intent from the client-side.

After you deploy this function to your server, the whole thing should be able to work, God bless!

The credit card number for the test is 424242424242, please input like following. If everything goes well, you should get a successful result after a while you clicked the Pay now button.

God bless you!

Hope this helps! And As I’ve mentioned before, I’m new to Nuxt.js, so if you have any better approaches, please feel free to comment!

--

--

Capt. Michael

A MERN Full Stack developer, freelancer & restless slave of probability.