• Bonuses Payments with Stripe, vue.js, and Flask
  • Originally written by Michael Herman
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Mcskiller
  • Proofread by: Kasheemlew

In this tutorial, we will develop a Web application to sell books using Stripe (processing payment orders), vue.js (client application) and Flask (server API).

This is an advanced tutorial. We assume that you have basically mastered vue.js and Flask. If you haven’t already checked them out, check out the links below to learn more:

  1. Introduction to Vue
  2. Flaskr: Intro to Flask, Test-Driven Development (TDD), and JavaScript
  3. Develop a single page application in Flask and vue.js

End result:

Mainly rely on:

  • Vue v2.5.2
  • Vue CLI v2.9.3
  • The Node v10.3.0
  • NPM v6.1.0
  • Flask v1.0.2
  • Python v3.6.5

directory

  • purpose
  • Project installation
  • What are we going to do?
  • CRUD books
  • The order page
  • Form validation
  • Stripe
  • Order Completion page
  • conclusion

purpose

By the end of this tutorial, you can…

  1. Get an existing CRUD application, powered by Vue and Flask
  2. Create an order settlement component
  3. Validate a form using native JavaScript
  4. Verify credit card information using Stripe
  5. Payments are processed through the Stripe API

Project installation

Clone flask-vuE-CRUd repository and find v1 tags in the Master branch:

$ git clone https://github.com/testdrivenio/flask-vue-crud --branch v1 --single-branch
$ cd flask-vue-crud
$ git checkout tags/v1 -b master
Copy the code

Set up and activate a virtual environment and then run the Flask application:

$ cdServer $python3.6 -m venv env $source env/bin/activate
(env)$ pip install -r requirements.txt
(env)$ python app.py
Copy the code

The commands for setting up the environment may vary with the operating system and operating environment.

Use the browser to http://localhost:5000/ping. You will see:

"pong!"
Copy the code

Then, install the dependency and run the Vue application on another terminal:

$ cd client
$ npm install
$ npm run dev
Copy the code

Go to http://localhost:8080. Ensure basic CRUD functions are working properly:

Want to learn how to build this project? Look at developing a single page application in Flask and vue.js.

What are we going to do?

Our goal is to build a Web application that allows end users to buy books.

The client Vue application will display the books available for purchase and record the payment information, then obtain the token from Stripe, and finally send the token and payment information to the server.

Flask then picks up this information, packs it up and sends it to Stripe for processing.

Finally, we’ll use a client-side Stripe library, strip.js, which generates a proprietary token to create bills, and then interacts with the Stripe API using the Server-side Python Stripe library.

As in the previous tutorial, we will simplify the steps, and you should deal with any other questions that arise on your own, which will also strengthen your understanding.

CRUD books

First, let’s add the purchase price to the list of existing books on the server side, and then update the corresponding CRUD functions GET, POST, and PUT on the client side.

GET

Start by adding price to each dictionary element 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'}]Copy the code

Then, in the Books component client/SRC/components/Books. Vue update form 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>
        <button type="button"
                class="btn btn-warning btn-sm"
                v-b-modal.book-update-modal
                @click="editBook(book)">
            Update
        </button>
        <button type="button"
                class="btn btn-danger btn-sm"
                @click="onDeleteBook(book)">
            Delete
        </button>
      </td>
    </tr>
  </tbody>
</table>
Copy the code

You should now see:

POST

Add a new B-form-group to addBookModal, between the Author and read b-form-group classes:

<b-form-group id="form-price-group"
              label="Purchase price:"
              label-for="form-price-input">
  <b-form-input id="form-price-input"
                type="number"
                v-model="addBookForm.price"
                required
                placeholder="Enter price">
  </b-form-input>
</b-form-group>
Copy the code

The mode should now look like this:

<! -- add book modal --> <b-modal ref="addBookModal"
         id="book-modal"
        title="Add a new book"
        hide-footer>
  <b-form @submit="onSubmit" @reset="onReset" class="w-100">
    <b-form-group id="form-title-group"
                  label="Title:"
                  label-for="form-title-input">
        <b-form-input id="form-title-input"
                      type="text"
                      v-model="addBookForm.title"
                      required
                      placeholder="Enter title">
        </b-form-input>
    </b-form-group>
    <b-form-group id="form-author-group"
                  label="Author:"
                  label-for="form-author-input">
      <b-form-input id="form-author-input"
                    type="text"
                    v-model="addBookForm.author"
                    required
                    placeholder="Enter author">
      </b-form-input>
    </b-form-group>
    <b-form-group id="form-price-group"
                  label="Purchase price:"
                  label-for="form-price-input">
      <b-form-input id="form-price-input"
                    type="number"
                    v-model="addBookForm.price"
                    required
                    placeholder="Enter price">
      </b-form-input>
    </b-form-group>
    <b-form-group id="form-read-group">
        <b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
          <b-form-checkbox value="true">Read? </b-form-checkbox> </b-form-checkbox-group> </b-form-group> <b-buttontype="submit" variant="primary">Submit</b-button>
    <b-button type="reset" variant="danger">Reset</b-button>
  </b-form>
</b-modal>
Copy the code

Then, add price to the addBookForm property:

addBookForm: {
  title: ' ',
  author: ' '.read: [],
  price: ' ',},Copy the code

AddBookForm is now bound to the input value of the form. Think about what that means. When the addBookForm is updated, the input value of the form is updated, and vice versa. Here is an example of vue-devTools browser extension.

Add price to the payload of the onSubmit method like this:

onSubmit(evt) {
  evt.preventDefault();
  this.$refs.addBookModal.hide();
  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();
},
Copy the code

Update initForm function to clear existing values after user submits form by clicking “reset” button:

initForm() {
  this.addBookForm.title = ' ';
  this.addBookForm.author = ' ';
  this.addBookForm.read = [];
  this.addBookForm.price = ' ';
  this.editForm.id = ' ';
  this.editForm.title = ' ';
  this.editForm.author = ' ';
  this.editForm.read = [];
},
Copy the code

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)
Copy the code

Test it out!

Don’t forget to handle client and server errors!

PUT

Do the same thing, but this time edit a book yourself:

  1. Add a new input form to the mode
  2. Update propertieseditFormPart of the
  3. addpriceonSubmitUpdatemethodspayload
  4. updateinitForm
  5. Update the server route

Can I help you? Look again at the previous chapter. Or you can get the source code from the flask-vue-CRUd repository.

The order page

Next, let’s add an order page where users can enter their credit card information to purchase books.

TODO: Add images

Add a buy button

First add a “Buy” button to the Books component, just below the “Delete” button:

<td>
  <button type="button"
          class="btn btn-warning btn-sm"
          v-b-modal.book-update-modal
          @click="editBook(book)">
      Update
  </button>
  <button type="button"
          class="btn btn-danger btn-sm"
          @click="onDeleteBook(book)">
      Delete
  </button>
  <router-link :to="`/order/${book.id}`"
               class="btn btn-primary btn-sm">
      Purchase
  </router-link>
</td>
Copy the code

Here, we use the router – link component to generate a connection to the client/SRC/router/index. The routing of the anchor point in the js, we can use it immediately.

Create a template

Add a new component file called order. vue to client/ SRC/Components:

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10"> <h1>Ready to buy? </h1> <hr> <router-link to="/" class="btn btn-primary">
          Back Home
        </router-link>
        <br><br><br>
        <div class="row">
          <div class="col-sm-6">
            <div>
              <h4>You are buying:</h4>
              <ul>
                <li>Book Title: <em>Book Title</em></li>
                <li>Amount: <em>$Book Price</em></li>
              </ul>
            </div>
            <div>
              <h4>Use this info for testing:</h4>
              <ul>
                <li>Card Number: 4242424242424242</li>
                <li>CVC Code: any three digits</li>
                <li>Expiration: any date in the future</li>
              </ul>
            </div>
          </div>
          <div class="col-sm-6">
            <h3>One time payment</h3>
            <br>
            <form>
              <div class="form-group">
                <label>Credit Card Info</label>
                <input type="text"
                       class="form-control"
                       placeholder="XXXXXXXXXXXXXXXX"
                       required>
              </div>
              <div class="form-group">
                <input type="text"
                       class="form-control"
                       placeholder="CVC"
                       required>
              </div>
              <div class="form-group">
                <label>Card Expiration Date</label>
                <input type="text"
                       class="form-control"
                       placeholder="MM/YY"
                       required>
              </div>
              <button class="btn btn-primary btn-block">Submit</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
Copy the code

You may want to collect the buyer’s contact information, such as name, email address, shipping address, etc. It’s up to you.

Add the routing

The client/SRC/router/index. Js:

import Vue from 'vue';
import Router from 'vue-router';
import Ping from '@/components/Ping';
import Books from '@/components/Books';
import Order from '@/components/Order';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Books',
      component: Books,
    },
    {
      path: '/order/:id',
      name: 'Order',
      component: Order,
    },
    {
      path: '/ping',
      name: 'Ping',
      component: Ping,
    },
  ],
  mode: 'hash'});Copy the code

Test it out.

Obtaining product information

Next, let’s update the title and amount placeholders on the order page:

Go back to the server and update the following routing interfaces:

@app.route('/books/<book_id>', methods=['GET'.'PUT'.'DELETE'])
def single_book(book_id):
    response_object = {'status': 'success'}
    if request.method == 'GET':
        # TODO: refactor to a lambda and filter
        return_book = ' '
        for book in BOOKS:
            if book['id'] == book_id:
                return_book = book
        response_object['book'] = return_book
    if request.method == 'PUT':
        post_data = request.get_json()
        remove_book(book_id)
        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 updated! '
    if request.method == 'DELETE':
        remove_book(book_id)
        response_object['message'] = 'Book removed! '
    return jsonify(response_object)
Copy the code

We can use this route in script to add book information to the order page:

<script>
import axios from 'axios';

export default {
  data() {
    return {
      book: {
        title: ' ',
        author: ' '.read: [],
        price: ' ',}}; }, methods: {getBook() {
      const path = `http://localhost:5000/books/${this.$route.params.id}`; axios.get(path) .then((res) => { this.book = res.data.book; }) .catch((error) => { // eslint-disable-next-line console.error(error); }); }},created() { this.getBook(); }}; </script>Copy the code

Move to production? You will need to use environment variables to dynamically set the base server URL (currently http://localhost:5000). See the documentation for more information.

Then, update the first UL in the template:

<ul>
  <li>Book Title: <em>{{ book.title }}</em></li>
  <li>Amount: <em>${{ book.price }}</em></li>
</ul>
Copy the code

You will now see:

Form validation

Let’s set up some basic form validation.

Use the V-model directive to bind form input values to properties:

<form>
  <div class="form-group">
    <label>Credit Card Info</label>
    <input type="text"
           class="form-control"
           placeholder="XXXXXXXXXXXXXXXX"
           v-model="card.number"
           required>
  </div>
  <div class="form-group">
    <input type="text"
           class="form-control"
           placeholder="CVC"
           v-model="card.cvc"
           required>
  </div>
  <div class="form-group">
    <label>Card Expiration Date</label>
    <input type="text"
           class="form-control"
           placeholder="MM/YY"
           v-model="card.exp"
           required>
  </div>
  <button class="btn btn-primary btn-block">Submit</button>
</form>
Copy the code

Add a card attribute like this:

card: {
  number: ' ',
  cvc: ' ',
  exp: ' ',},Copy the code

Next, update the “Submit” button so that normal browser behavior is ignored when the button is clicked, and call the Validate method:

<button class="btn btn-primary btn-block" @click.prevent="validate">Submit</button>
Copy the code

Add an array to a property to hold validation error messages:

data() {
  return {
    book: {
      title: ' ',
      author: ' '.read: [],
      price: ' ',
    },
    card: {
      number: ' ',
      cvc: ' ',
      exp: ' ',
    },
    errors: [],
  };
},
Copy the code

Just add it to the bottom of the form and we can display all the errors in sequence:

<div v-show="errors">
  <br>
  <ol class="text-danger">
    <li v-for="(error, index) in errors" :key="index">
      {{ error }}
    </li>
  </ol>
</div>
Copy the code

Add validate method:

validate() {
  this.errors = [];
  let valid = true;
  if(! this.card.number) { valid =false;
    this.errors.push('Card Number is required');
  }
  if(! this.card.cvc) { valid =false;
    this.errors.push('CVC is required');
  }
  if(! this.card.exp) { valid =false;
    this.errors.push('Expiration date is required');
  }
  if(valid) { this.createToken(); }},Copy the code

Since all fields are mandatory, we simply verify that each field has a value. Stripe will validate the credit card information you see in the next section, so you don’t have to overvalidate the form information. In other words, just make sure the other fields you add are validated.

Finally, add the createToken method:

createToken() {
  // eslint-disable-next-line
  console.log('The form is valid! ');
},
Copy the code

Test it out.

Stripe

If you do not have a Stripe account you need to register one first and then retrieve your test mode API Publishable key.

The client

Add stripePublishableKey and stripeCheck (to disable the submit button) to data:

data() {
  return {
    book: {
      title: ' ',
      author: ' '.read: [],
      price: ' ',
    },
    card: {
      number: ' ',
      cvc: ' ',
      exp: ' ',
    },
    errors: [],
    stripePublishableKey: 'pk_test_aIh85FLcNlk7A6B26VZiNj1h',
    stripeCheck: false}; },Copy the code

Be sure to add your own Stripe key to the code above.

Also, if the form is valid, trigger the createToken method (via strip.js) to validate the credit card information and either return an error message (if invalid) or return a token (if valid) :

createToken() {
  this.stripeCheck = true;
  window.Stripe.setPublishableKey(this.stripePublishableKey);
  window.Stripe.createToken(this.card, (status, response) => {
    if (response.error) {
      this.stripeCheck = false;
      this.errors.push(response.error.message);
      // eslint-disable-next-line
      console.error(response);
    } else {
      // pass
    }
  });
},
Copy the code

If there are no errors, we send the token to the server where we complete the deduction and redirect the user back to the home page:

createToken() {
  this.stripeCheck = true;
  window.Stripe.setPublishableKey(this.stripePublishableKey);
  window.Stripe.createToken(this.card, (status, response) => {
    if (response.error) {
      this.stripeCheck = false;
      this.errors.push(response.error.message);
      // eslint-disable-next-line
      console.error(response);
    } else {
      const payload = {
        book: this.book,
        token: response.id,
      };
      const path = 'http://localhost:5000/charge';
      axios.post(path, payload)
        .then(() => {
          this.$router.push({ path: '/'}); }) .catch((error) => { // eslint-disable-next-line console.error(error); }); }}); },Copy the code

Update createToken() with the above code and add strip.js to client/index.html:

<! DOCTYPE html> <html> <head> <meta charset="utf-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0"> <title>Books! </title> </head> <body> <div id="app"></div> <! -- built files will be auto injected --> <scripttype="text/javascript" src="https://js.stripe.com/v2/"></script>
  </body>
</html>
Copy the code

Stripe Supports strip. js in v2 and V3 (Stripe Elements) versions. If you are interested in Stripe Elements and how to integrate it into Vue, see the following resources: 1. Stripe Elements Migration Guide 2. Integrate Stripe Elements with vue.js to create a custom payment form

Now, when createToken is triggered, the stripeCheck value is changed to True. To prevent double billing, we disable the “Submit” button when the stripeCheck value is true:

<button class="btn btn-primary btn-block"
        @click.prevent="validate"
        :disabled="stripeCheck">
    Submit
</button>
Copy the code

Test for invalid feedback from Stripe validation:

  1. Credit Card Number
  2. Security code
  3. The effective date

Now, let’s start setting up the server route.

The service side

Install the Stripe library:

$PIP install stripe = = 1.82.1Copy the code

Adding a routing interface:

@app.route('/charge', methods=['POST'])
def create_charge():
    post_data = request.get_json()
    amount = round(float(post_data.get('book') ['price']) * 100)
    stripe.api_key = os.environ.get('STRIPE_SECRET_KEY')
    charge = stripe.Charge.create(
        amount=amount,
        currency='usd',
        card=post_data.get('token'),
        description=post_data.get('book') ['title']
    )
    response_object = {
        'status': 'success'.'charge': charge
    }
    return jsonify(response_object), 200
Copy the code

Here we set the book price (converted to cents), the proprietary token (from the createToken method on the client), and the title of the book, and then we generate a new Stripe bill using the API Secret Key.

For more information on creating bills, refer to the official API documentation.

Update the imports:

import os
import uuid

import stripe
from flask import Flask, jsonify, request
from flask_cors import CORS
Copy the code

Obtain test mode API Secret key:

Set it to an environment variable:

$ export STRIPE_SECRET_KEY=sk_test_io02FXL17hrn2TNvffanlMSy
Copy the code

Make sure you use your own Stripe key!

Test it out!

You should see purchase records in the Stripe Dashboard:

You might also want to create customers, not just bills. This has many advantages. You can purchase multiple items at the same time to keep track of customer purchases. You can offer discounts to people who buy regularly, or contact people who haven’t bought in a while, and there are many other uses that I won’t cover here. It can also be used to prevent fraud. Refer to the following Flask project to see how to add customers.

Order Completion page

Instead of sending buyers straight back to the home page, we should redirect them to an order completion page to thank them for their purchase.

Add a new component file called orderComplete. vue to “client/ SRC/Components” :

<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <h1>Thanks forpurchasing! </h1> <hr><br> <router-link to="/" class="btn btn-primary btn-sm">Back Home</router-link>
      </div>
    </div>
  </div>
</template>
Copy the code

Update route:

import Vue from 'vue';
import Router from 'vue-router';
import Ping from '@/components/Ping';
import Books from '@/components/Books';
import Order from '@/components/Order';
import OrderComplete from '@/components/OrderComplete';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'Books',
      component: Books,
    },
    {
      path: '/order/:id',
      name: 'Order',
      component: Order,
    },
    {
      path: '/complete',
      name: 'OrderComplete',
      component: OrderComplete,
    },
    {
      path: '/ping',
      name: 'Ping',
      component: Ping,
    },
  ],
  mode: 'hash'});Copy the code

Update redirection in createToken method:

createToken() {
  this.stripeCheck = true;
  window.Stripe.setPublishableKey(this.stripePublishableKey);
  window.Stripe.createToken(this.card, (status, response) => {
    if (response.error) {
      this.stripeCheck = false;
      this.errors.push(response.error.message);
      // eslint-disable-next-line
      console.error(response);
    } else {
      const payload = {
        book: this.book,
        token: response.id,
      };
      const path = 'http://localhost:5000/charge';
      axios.post(path, payload)
        .then(() => {
          this.$router.push({ path: '/complete'}); }) .catch((error) => { // eslint-disable-next-line console.error(error); }); }}); },Copy the code

Finally, you can display the book (title, amount, etc.) that the customer just purchased on the order completion page.

Get the unique bill ID and pass it to path:

createToken() {
  this.stripeCheck = true;
  window.Stripe.setPublishableKey(this.stripePublishableKey);
  window.Stripe.createToken(this.card, (status, response) => {
    if (response.error) {
      this.stripeCheck = false;
      this.errors.push(response.error.message);
      // eslint-disable-next-line
      console.error(response);
    } else {
      const payload = {
        book: this.book,
        token: response.id,
      };
      const path = 'http://localhost:5000/charge';
      axios.post(path, payload)
        .then((res) => {
          // updates
          this.$router.push({ path: `/complete/${res.data.charge.id}`}); }) .catch((error) => { // eslint-disable-next-line console.error(error); }); }}); },Copy the code

Update client routing:

{
  path: '/complete/:id',
  name: 'OrderComplete',
  component: OrderComplete,
},
Copy the code

Then, in orderComplete. vue, get the bill ID from the URL and send it to the server:

<script>
import axios from 'axios';

export default {
  data() {
    return {
      book: ' '}; }, methods: {getChargeInfo() {
      const path = `http://localhost:5000/charge/${this.$route.params.id}`; axios.get(path) .then((res) => { this.book = res.data.charge.description; }) .catch((error) => { // eslint-disable-next-line console.error(error); }); }},created() { this.getChargeInfo(); }}; </script>Copy the code

Configure a new route on the server to retrieve the bill:

@app.route('/charge/<charge_id>')
def get_charge(charge_id):
    stripe.api_key = os.environ.get('STRIPE_SECRET_KEY')
    response_object = {
        'status': 'success'.'charge': stripe.Charge.retrieve(charge_id)
    }
    return jsonify(response_object), 200
Copy the code

Finally, update

in template:

<h1>Thanks forpurchasing - {{ this.book }}! </h1>Copy the code

One last test.

conclusion

Done! Be sure to read from the very beginning. You can find the source in GitHub’s flask-vue-crud repository.

Want more challenges?

  1. Add unit and integration tests on both client and server sides.
  2. Create a shopping cart so that customers can purchase more than one book at a time.
  3. Use Postgres to store books and orders.
  4. Integrate Vue and Flask (and Postgres, if you’re in) with Docker to simplify the development workflow.
  5. Add images to books to create a better product page.
  6. Get the email and send the email confirmation (see sending confirmation emails using Flask, Redis Queue, and Amazon SES).
  7. Deploy the client static files to AWS S3 and then deploy the server application to an EC2 instance.
  8. Put into production? Consider the best way to update Stripe keys dynamically based on the environment.
  9. Create a detached component to unsubscribe.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.