In this tutorial, we'll develop a web app for selling books using Stripe (for payment processing), Vue.js (the client-side app), and Flask (the server-side API).
This is an intermediate-level tutorial. It assumes that you a have basic working knowledge of Vue and Flask. Review the following resources for more info:
Final app:
Main dependencies:
By the end of this tutorial, you will be able to:
Clone the base Flask and Vue project from the flask-vue-crud repo:
$ git clone https://github.com/testdrivenio/flask-vue-crud flask-vue-stripe
$ cd flask-vue-stripe
Create and activate a virtual environment, and then spin up the Flask app:
$ cd server
$ python3.11 -m venv env
$ source env/bin/activate
(env)$
(env)$ pip install -r requirements.txt
(env)$ flask run --port=5001 --debug
The above commands, for creating and activating a virtual environment, may differ depending on your environment and operating system. Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.
Point your browser of choice at http://localhost:5001/ping. You should see:
Then, install the dependencies and run the Vue app in a different terminal window:
$ cd client
$ npm install
$ npm run dev
Navigate to http://localhost:5173. Make sure the basic CRUD functionality works as expected:
What are we building?Want to learn how to build this project? Check out the Developing a Single Page App with Flask and Vue.js tutorial.
Our goal is to build a web app that allows end users to purchase books.
The client-side Vue app will display the books available for purchase and redirect the end user to the checkout form via Stripe.js and Stripe Checkout. After the payment process is complete, users will be redirected to either a success or failure page also managed by Vue.
The Flask app, meanwhile, uses the Stripe Python Library for interacting with the Stripe API to create a checkout session.
Books CRUDLike the previous tutorial, Developing a Single Page App with Flask and Vue.js, we'll only be dealing with the happy path through the app. Check your understanding by incorporating proper error-handling on your own.
First, let's add a purchase price to the existing list of books on the server-side and update the appropriate CRUD functions on the client -- GET, POST, and PUT.
GETStart by adding the price
to each dict in the BOOKS
list in server/app.py:
BOOKS = [
{
'id': uuid.uuid4().hex,
'title': 'On the Road',
'author': 'Jack Kerouac',
'read': True,
'price': '19.99'
},
{
'id': uuid.uuid4().hex,
'title': 'Harry Potter and the Philosopher\'s Stone',
'author': 'J. K. Rowling',
'read': False,
'price': '9.99'
},
{
'id': uuid.uuid4().hex,
'title': 'Green Eggs and Ham',
'author': 'Dr. Seuss',
'read': True,
'price': '3.99'
}
]
Then, update the table in the Books
component, client/src/components/Books.vue, to display the purchase price:
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">Read?</th>
<th scope="col">Purchase Price</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(book, index) in books" :key="index">
<td>{{ book.title }}</td>
<td>{{ book.author }}</td>
<td>
<span v-if="book.read">Yes</span>
<span v-else>No</span>
</td>
<td>${{ book.price }}</td>
<td>
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-warning btn-sm"
@click="toggleEditBookModal(book)">
Update
</button>
<button
type="button"
class="btn btn-danger btn-sm"
@click="handleDeleteBook(book)">
Delete
</button>
</div>
</td>
</tr>
</tbody>
</table>
You should now see:
POSTAdd a new form input to addBookModal
, between the author and read form inputs:
<div class="mb-3">
<label for="addBookPrice" class="form-label">Purchase price:</label>
<input
type="number"
step="0.01"
class="form-control"
id="addBookPrice"
v-model="addBookForm.price"
placeholder="Enter price">
</div>
The modal should now look like:
<!-- add new book modal -->
<div
ref="addBookModal"
class="modal fade"
:class="{ show: activeAddBookModal, 'd-block': activeAddBookModal }"
tabindex="-1"
role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add a new book</h5>
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
@click="toggleAddBookModal">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="addBookTitle" class="form-label">Title:</label>
<input
type="text"
class="form-control"
id="addBookTitle"
v-model="addBookForm.title"
placeholder="Enter title">
</div>
<div class="mb-3">
<label for="addBookAuthor" class="form-label">Author:</label>
<input
type="text"
class="form-control"
id="addBookAuthor"
v-model="addBookForm.author"
placeholder="Enter author">
</div>
<div class="mb-3">
<label for="addBookPrice" class="form-label">Purchase price:</label>
<input
type="number"
step="0.01"
class="form-control"
id="addBookPrice"
v-model="addBookForm.price"
placeholder="Enter price">
</div>
<div class="mb-3 form-check">
<input
type="checkbox"
class="form-check-input"
id="addBookRead"
v-model="addBookForm.read">
<label class="form-check-label" for="addBookRead">Read?</label>
</div>
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-primary btn-sm"
@click="handleAddSubmit">
Submit
</button>
<button
type="button"
class="btn btn-danger btn-sm"
@click="handleAddReset">
Reset
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div v-if="activeAddBookModal" class="modal-backdrop fade show"></div>
Then, add price
to the state:
addBookForm: {
title: '',
author: '',
read: [],
price: '',
},
The state is now bound to the form's input value. Think about what this means. When the state is updated, the form input will be updated as well -- and vice versa. Here's an example of this in action with the vue-devtools browser extension:
Add the price
to the payload
in the handleAddSubmit
method like so:
handleAddSubmit() {
this.toggleAddBookModal();
let read = false;
if (this.addBookForm.read[0]) {
read = true;
}
const payload = {
title: this.addBookForm.title,
author: this.addBookForm.author,
read, // property shorthand
price: this.addBookForm.price,
};
this.addBook(payload);
this.initForm();
},
Update initForm
to clear out the value after the end user submits the form or clicks the "reset" button:
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
this.addBookForm.price = '';
this.editBookForm.id = '';
this.editBookForm.title = '';
this.editBookForm.author = '';
this.editBookForm.read = [];
},
Finally, update the route in server/app.py:
@app.route('/books', methods=['GET', 'POST'])
def all_books():
response_object = {'status': 'success'}
if request.method == 'POST':
post_data = request.get_json()
BOOKS.append({
'id': uuid.uuid4().hex,
'title': post_data.get('title'),
'author': post_data.get('author'),
'read': post_data.get('read'),
'price': post_data.get('price')
})
response_object['message'] = 'Book added!'
else:
response_object['books'] = BOOKS
return jsonify(response_object)
Test it out!
PUTDon't forget to handle errors on both the client and server!
Do the same, on your own, for editing a book:
editBookForm
in the stateprice
to the payload
in the handleEditSubmit
methodinitForm
Purchase ButtonNeed help? Review the previous section again. You can also grab the final code from the flask-vue-stripe repo.
Add a "purchase" button to the Books
component, just below the "delete" button:
<td>
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-warning btn-sm"
@click="toggleEditBookModal(book)">
Update
</button>
<button
type="button"
class="btn btn-danger btn-sm"
@click="handleDeleteBook(book)">
Delete
</button>
<button
type="button"
class="btn btn-primary btn-sm"
@click="handlePurchaseBook(book)">
Purchase
</button>
</div>
</td>
Next, add handlePurchaseBook
to the component's methods
:
handlePurchaseBook(book) {
console.log(book.id);
},
Test it out:
Stripe KeysSign up for a Stripe account, if you don't already have one.
ServerInstall the Stripe Python library:
(env)$ pip install stripe==5.4.0
Grab the test mode API keys from the Stripe dashboard:
Set them as environment variables within the terminal window where you're running the server:
(env)$ export STRIPE_PUBLISHABLE_KEY=<YOUR_STRIPE_PUBLISHABLE_KEY>
(env)$ export STRIPE_SECRET_KEY=<YOUR_STRIPE_SECRET_KEY>
Import the Stripe library into server/app.py and assign the keys to stripe.api_key
so that they will be used automatically when interacting with the API:
import os
import uuid
import stripe
from flask import Flask, jsonify, request
from flask_cors import CORS
...
# configuration
DEBUG = True
# instantiate the app
app = Flask(__name__)
app.config.from_object(__name__)
# configure stripe
stripe_keys = {
'secret_key': os.environ['STRIPE_SECRET_KEY'],
'publishable_key': os.environ['STRIPE_PUBLISHABLE_KEY'],
}
stripe.api_key = stripe_keys['secret_key']
# enable CORS
CORS(app, resources={r'/*': {'origins': '*'}})
...
if __name__ == '__main__':
app.run()
Next, add a new route handler that returns the publishable key:
@app.route('/config')
def get_publishable_key():
stripe_config = {'publicKey': stripe_keys['publishable_key']}
return jsonify(stripe_config)
This will be used on the client side to configure the Stripe.js library.
ClientTurning to the client, add Stripe.js to client/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<script src="https://js.stripe.com/v3/"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Next, add a new method to the Books
component called getStripePublishableKey
:
getStripePublishableKey() {
fetch('http://localhost:5001/config')
.then((result) => result.json())
.then((data) => {
// Initialize Stripe.js
this.stripe = Stripe(data.publicKey);
});
},
Call this method in the created
hook:
created() {
this.getBooks();
this.getStripePublishableKey();
},
Now, after the instance is created, a call will be made to http://localhost:5001/config
, which will respond with the Stripe publishable key. We'll then use this key to create a new instance of Stripe.js.
Shipping to production? You'll want to use an environment variable to dynamically set the base server-side URL (which is currently
http://localhost:5001
). Review the docs for more info.
Add stripe
to `the state:
data() {
return {
activeAddBookModal: false,
activeEditBookModal: false,
addBookForm: {
title: '',
author: '',
read: [],
price: '',
},
books: [],
editBookForm: {
id: '',
title: '',
author: '',
read: [],
price: '',
},
message: '',
showMessage: false,
stripe: null,
};
},
Stripe Checkout
Next, we need to generate a new Checkout Session ID on the server-side. After clicking the purchase button, an AJAX request will be sent to the server to generate this ID. The server will send the ID back and the user will be redirected to the checkout.
ServerAdd the following route handler:
@app.route('/create-checkout-session', methods=['POST'])
def create_checkout_session():
domain_url = 'http://localhost:5173'
try:
data = json.loads(request.data)
# get book
book_to_purchase = ''
for book in BOOKS:
if book['id'] == data['book_id']:
book_to_purchase = book
# create new checkout session
checkout_session = stripe.checkout.Session.create(
success_url=domain_url +
'/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url=domain_url + '/canceled',
payment_method_types=['card'],
mode='payment',
line_items=[
{
'name': book_to_purchase['title'],
'quantity': 1,
'currency': 'usd',
'amount': round(float(book_to_purchase['price']) * 100),
}
]
)
return jsonify({'sessionId': checkout_session['id']})
except Exception as e:
return jsonify(error=str(e)), 403
Here, we-
domain_url
for redirecting the user back to the client after a purchase is completeTake note of the success_url
and cancel_url
. The user will be redirected back to those URLs in the event of a successful payment or cancellation, respectively. We'll set the /success
and /cancelled
routes up shortly on the client.
Also, did you notice that we converted the float to an integer via round(float(book_to_purchase['price']) * 100)
? Stripe only allows integer values for the price. For production code, you'll probably want to store the price as an integer value in the database -- e.g., $3.99 should be stored as 399
.
Add the import to the top:
ClientOn the client, update the handlePurchaseBook
method:
handlePurchaseBook(book) {
// Get Checkout Session ID
fetch('http://localhost:5001/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ book_id: book.id }),
})
.then((result) => result.json())
.then((data) => {
console.log(data);
// Redirect to Stripe Checkout
return this.stripe.redirectToCheckout({ sessionId: data.sessionId });
})
.then((res) => {
console.log(res);
});
},
Here, after resolving the result.json()
promise, we called the redirectToCheckout
method with the Checkout Session ID from the resolved promise.
Let's test it out. Navigate to http://localhost:5173. Click one of the purchase buttons. You should be redirected to an instance of Stripe Checkout (a Stripe-hosted page to securely collect payment information) with the basic product information:
You can test the form by using one of the several test card numbers that Stripe provides. Let's use 4242 4242 4242 4242
.
4242 4242 4242 4242
The payment should be processed successfully, but the redirect will fail since we have not set up the /success
route yet.
You should see the purchase back in the Stripe Dashboard:
Redirect PagesFinally, let's set up routes and components for handling a successful payment or cancellation.
SuccessWhen a payment is successful, we'll redirect the user to an order complete page, thanking them for making a purchase.
Add a new component file called OrderSuccess.vue to "client/src/components":
<template>
<div class="container">
<div class="row">
<div class="col-sm-10">
<h1>Thanks for purchasing!</h1>
<hr><br>
<router-link to="/" class="btn btn-primary btn-sm">Back Home</router-link>
</div>
</div>
</div>
</template>
Update the router in client/src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router'
import Books from '../components/Books.vue'
import OrderSuccess from '../components/OrderSuccess.vue'
import Ping from '../components/Ping.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Books',
component: Books,
},
{
path: '/ping',
name: 'ping',
component: Ping
},
{
path: '/success',
name: 'OrderSuccess',
component: OrderSuccess,
},
]
})
export default router
Finally, you could display info about the purchase using the session_id
query param:
http://localhost:5173/success?session_id=cs_test_a1qw4pxWK9mF2SDvbiQXqg5quq4yZYUvjNkqPq1H3wbUclXOue0hES6lWl
You can access it like so:
<script>
export default {
mounted() {
console.log(this.$route.query.session_id);
},
};
</script>
From there, you'll want to set up a route handler on the server-side, to look up the session info via stripe.checkout.Session.retrieve(id)
. Try this out on your own.
For the /canceled
redirect, add a new component called client/src/components/OrderCanceled.vue:
<template>
<div class="container">
<div class="row">
<div class="col-sm-10">
<h1>Your payment was cancelled.</h1>
<hr><br>
<router-link to="/" class="btn btn-primary btn-sm">Back Home</router-link>
</div>
</div>
</div>
</template>
Then, update the router:
import { createRouter, createWebHistory } from 'vue-router'
import Books from '../components/Books.vue'
import OrderCanceled from '../components/OrderCanceled.vue'
import OrderSuccess from '../components/OrderSuccess.vue'
import Ping from '../components/Ping.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Books',
component: Books,
},
{
path: '/ping',
name: 'ping',
component: Ping
},
{
path: '/success',
name: 'OrderSuccess',
component: OrderSuccess,
},
{
path: '/canceled',
name: 'OrderCanceled',
component: OrderCanceled,
},
]
})
export default router
Test it out one last time.
ConclusionThat's it! Be sure to review the objectives from the top. You can find the final code in the flask-vue-stripe repo on GitHub.
Looking for more?
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4