Rachel Cole

Rachel Cole

1683272925

Stripe Payment Gateway with ReactJS and Node.js

Stripe payment gateway with ReactJS and Node.js | Mini Full stack Ecommerce Project

If you are searching for a Stripe payment gateway for a Node.js eCommerce project or How to integrate Stripe with React and Node.js then this is the video for you. Learn Stripe Checkout for ReactJS and Node.js with this mini e-commerce full-stack project. With react hooks and tailwindcss learn state management and responsive designs. With node js and express js learn to implement the backend server.

Github Link of the project
https://github.com/aindrila22/stripe_pay_reactjs_nodejs 

#stripe #payment #react #node #reactjs #nodejs #expressjs #tailwindcss

Stripe Payment Gateway with ReactJS and Node.js

Как принимать платежи с помощью Stripe, Vue.js и Flask

В этом руководстве мы разработаем веб-приложение для продажи книг с использованием Stripe (для обработки платежей), Vue.js (клиентское приложение) и Flask (серверный API).

Окончательное приложение :

финальное приложение

Основные зависимости:

  • Посмотреть v3.2.47
  • Узел v20.0.0
  • нпм v9.6.4
  • Колба v2.2.3
  • Питон v3.11.3

Цели

К концу этого урока вы сможете:

  1. Работа с существующим приложением CRUD на базе Vue и Flask.
  2. Создайте компонент Vue для оформления заказа
  3. Проверка информации о кредитной карте и обработка платежей с помощью Stripe Checkout

Настройка проекта

Клонируйте базовый проект Flask и Vue из репозитория flask-vue-crud :

$ git clone https://github.com/testdrivenio/flask-vue-crud flask-vue-stripe
$ cd flask-vue-stripe

Создайте и активируйте виртуальную среду, а затем запустите приложение Flask:

$ cd server
$ python3.11 -m venv env
$ source env/bin/activate
(env)$

(env)$ pip install -r requirements.txt
(env)$ flask run --port=5001 --debug

Приведенные выше команды для создания и активации виртуальной среды могут отличаться в зависимости от вашей среды и операционной системы. Не стесняйтесь менять virtualenv и Pip на Poetry или Pipenv . Подробнее см. в Modern Python Environments .

Укажите в своем браузере адрес http://localhost:5001/ping . Тебе следует увидеть:

"pong!"

Затем установите зависимости и запустите приложение Vue в другом окне терминала:

$ cd client
$ npm install
$ npm run dev

Перейдите по адресу http://localhost:5173 . Убедитесь, что основные функции CRUD работают должным образом:

базовое приложение

Хотите узнать, как построить этот проект? Ознакомьтесь с учебным пособием «Разработка одностраничного приложения с помощью Flask и Vue.js» .

Что мы строим?

Наша цель — создать веб-приложение, позволяющее конечным пользователям покупать книги.

Клиентское приложение Vue отобразит книги, доступные для покупки, и перенаправит конечного пользователя к форме оформления заказа через Stripe.js и Stripe Checkout . После завершения процесса оплаты пользователи будут перенаправлены либо на страницу успеха, либо на страницу отказа, также управляемую Vue.

Тем временем приложение Flask использует библиотеку Stripe Python для взаимодействия с Stripe API для создания сеанса оформления заказа.

финальное приложение

Как и в предыдущем уроке « Разработка одностраничного приложения с помощью Flask и Vue.js» , мы будем иметь дело только с удачным путем через приложение. Проверьте свое понимание, включив правильную обработку ошибок самостоятельно.

Книги

Во-первых, давайте добавим цену покупки в существующий список книг на стороне сервера и обновим соответствующие функции CRUD на клиенте — GET, POST и PUT.

ПОЛУЧАТЬ

Начните с добавления priceк каждому словарю в BOOKSсписке в 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'
    }
]

Затем обновите таблицу в Booksкомпоненте client/src/components/Books.vue , чтобы отобразить цену покупки:

<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>

Теперь вы должны увидеть:

добавить цену покупки в компонент «Книги»

ПОЧТА

Добавьте новый ввод формы в addBookModal, между вводом формы author и read:

<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>

Теперь модальное окно должно выглядеть так:

<!-- 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">&times;</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>

Затем добавьте priceк состоянию:

addBookForm: {
  title: '',
  author: '',
  read: [],
  price: '',
},

Состояние теперь привязано к входному значению формы. Подумайте, что это значит. Когда состояние обновляется, ввод формы также будет обновлен — и наоборот. Вот пример этого в действии с расширением браузера vue-devtools :

привязка модели состояния

Добавьте метод priceв payloadметод handleAddSubmitследующим образом:

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();
},

Обновите initForm, чтобы очистить значение после того, как конечный пользователь отправит форму или нажмет кнопку «Сброс»:

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 = [];
},

Наконец, обновите маршрут в 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)

Проверьте это!

добавить книгу

Не забывайте обрабатывать ошибки как на клиенте, так и на сервере!

ПОМЕЩАТЬ

Сделайте то же самое самостоятельно для редактирования книги:

  1. Добавить новый ввод формы в модальный
  2. Обновление editBookFormв состоянии
  3. Добавьте priceв payloadметод handleEditSubmit_
  4. ОбновлятьinitForm
  5. Обновите маршрут на стороне сервера

Нужна помощь? Еще раз просмотрите предыдущий раздел. Вы также можете получить окончательный код из репозитория flask-vue-stripe .

Кнопка покупки

Добавьте кнопку «Купить» в Booksкомпонент, чуть ниже кнопки «Удалить»:

<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>

Затем добавьте handlePurchaseBookк компоненту methods:

handlePurchaseBook(book) {
  console.log(book.id);
},

Проверьте это:

кнопка покупки

Полосатые ключи

Зарегистрируйте учетную запись Stripe , если у вас ее еще нет.

Сервер

Установите библиотеку Stripe Python:

(env)$ pip install stripe==5.4.0

Возьмите ключи API тестового режима с панели управления Stripe:

полоса приборной панели

Установите их как переменные среды в окне терминала, где вы запускаете сервер:

(env)$ export STRIPE_PUBLISHABLE_KEY=<YOUR_STRIPE_PUBLISHABLE_KEY>
(env)$ export STRIPE_SECRET_KEY=<YOUR_STRIPE_SECRET_KEY>

Импортируйте библиотеку Stripe в server/app.py и назначьте ключи, stripe.api_keyчтобы они использовались автоматически при взаимодействии с 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()

Затем добавьте новый обработчик маршрута, который возвращает публикуемый ключ:

@app.route('/config')
def get_publishable_key():
    stripe_config = {'publicKey': stripe_keys['publishable_key']}
    return jsonify(stripe_config)

Это будет использоваться на стороне клиента для настройки библиотеки Stripe.js.

Клиент

Перейдя к клиенту, добавьте Stripe.js в 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>

Затем добавьте к компоненту новый метод Booksс именем getStripePublishableKey:

getStripePublishableKey() {
  fetch('http://localhost:5001/config')
    .then((result) => result.json())
    .then((data) => {
      // Initialize Stripe.js
      this.stripe = Stripe(data.publicKey);
    });
},

Вызовите этот метод в createdхуке:

created() {
  this.getBooks();
  this.getStripePublishableKey();
},

Теперь, после создания экземпляра, будет сделан вызов http://localhost:5001/config, который ответит публикуемым ключом Stripe. Затем мы будем использовать этот ключ для создания нового экземпляра Stripe.js.

Доставка на производство? Вы захотите использовать переменную среды для динамической установки базового URL-адреса на стороне сервера (в настоящее время это http://localhost:5001). Просмотрите документы для получения дополнительной информации.

Добавьте stripeк `состоянию:

data() {
  return {
    activeAddBookModal: false,
    activeEditBookModal: false,
    addBookForm: {
      title: '',
      author: '',
      read: [],
      price: '',
    },
    books: [],
    editBookForm: {
      id: '',
      title: '',
      author: '',
      read: [],
      price: '',
    },
    message: '',
    showMessage: false,
    stripe: null,
  };
},

Полоса

Затем нам нужно сгенерировать новый идентификатор сеанса проверки на стороне сервера. После нажатия кнопки покупки на сервер будет отправлен запрос AJAX для создания этого идентификатора. Сервер отправит идентификатор обратно, и пользователь будет перенаправлен на кассу.

Сервер

Добавьте следующий обработчик маршрута:

@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

Мы тут-

  1. Определен domain_urlдля перенаправления пользователя обратно к клиенту после завершения покупки.
  2. Получена информация о книге
  3. Создан сеанс проверки
  4. Отправил ID обратно в ответ

Обратите внимание на success_urlи cancel_url. Пользователь будет перенаправлен обратно на эти URL-адреса в случае успешной оплаты или отмены соответственно. Вскоре мы настроим маршруты /successи /cancelledна клиенте.

Кроме того, вы заметили, что мы преобразовали число с плавающей запятой в целое число с помощью round(float(book_to_purchase['price']) * 100)? Stripe допускает только целочисленные значения цены. Для производственного кода вы, вероятно, захотите сохранить цену как целочисленное значение в базе данных — например, $3,99 следует хранить как 399.

Добавьте импорт вверху:

import json

Клиент

На клиенте обновите handlePurchaseBookметод:

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);
    });
},

Здесь, после разрешения result.json()промиса, мы вызвали redirectToCheckoutметод с идентификатором Checkout Session из разрешенного промиса.

Давайте проверим это. Перейдите по адресу http://localhost:5173 . Нажмите одну из кнопок покупки. Вы должны быть перенаправлены на экземпляр Stripe Checkout (размещенная на Stripe страница для безопасного сбора платежной информации) с основной информацией о продукте:

полоса

Вы можете протестировать форму, используя один из нескольких номеров тестовых карточек , которые предоставляет Stripe. Давайте использовать 4242 4242 4242 4242.

  • Электронная почта: действующая электронная почта
  • Номер карты:4242 4242 4242 4242
  • Срок действия: любая дата в будущем
  • CVC: любые три числа
  • Имя: что угодно
  • Почтовый индекс: любые пять цифр

Платеж должен пройти успешно, но перенаправление не удастся, так как мы еще не настроили маршрут /success.

Вы должны увидеть покупку на панели инструментов Stripe :

полоса приборной панели

Перенаправление страниц

Наконец, давайте настроим маршруты и компоненты для обработки успешного платежа или отмены.

Успех

Когда платеж будет успешным, мы перенаправим пользователя на страницу завершения заказа, поблагодарив его за совершение покупки.

Добавьте новый файл компонента с именем OrderSuccess.vue в «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>

Обновите маршрутизатор в 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

финальное приложение

Наконец, вы можете отобразить информацию о покупке, используя session_idпараметр запроса:

http://localhost:5173/success?session_id=cs_test_a1qw4pxWK9mF2SDvbiQXqg5quq4yZYUvjNkqPq1H3wbUclXOue0hES6lWl

Вы можете получить к нему доступ следующим образом:

<script>
export default {
  mounted() {
    console.log(this.$route.query.session_id);
  },
};
</script>

Оттуда вы захотите настроить обработчик маршрута на стороне сервера, чтобы искать информацию о сеансе через файлы stripe.checkout.Session.retrieve(id). Попробуйте это самостоятельно.

Отмена

Для /canceledперенаправления добавьте новый компонент с именем 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>

Затем обновите маршрутизатор:

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

Проверьте это в последний раз.

Заключение

Вот и все! Обязательно просмотрите цели сверху. Вы можете найти окончательный код в репозитории flask-vue-stripe на GitHub.

Ищете больше?

  1. Добавьте клиентские и серверные модульные и интеграционные тесты.
  2. Создайте корзину, чтобы клиенты могли покупать несколько книг одновременно.
  3. Добавьте Postgres для хранения книг и заказов.
  4. Контейнеризируйте Vue и Flask (и Postgres, если вы его добавите) с помощью Docker, чтобы упростить рабочий процесс разработки.
  5. Добавьте изображения в книги и создайте более надежную страницу продукта.
  6. Захватывайте электронные письма и отправляйте подтверждения по электронной почте (см. раздел « Отправка электронных писем с подтверждением с помощью Flask, Redis Queue и Amazon SES »).
  7. Разверните статические файлы на стороне клиента в AWS S3, а приложение на стороне сервера — в экземпляр EC2.
  8. Запускаем производство? Подумайте, как лучше всего обновить ключи Stripe, чтобы они были динамическими в зависимости от среды.

Оригинальный источник статьи: https://testdriven.io/

#flask #stripe #vue 

Как принимать платежи с помощью Stripe, Vue.js и Flask
津田  淳

津田 淳

1682397060

如何使用 Stripe、Vue.js 和 Flask 接受付款

在本教程中,我们将使用Stripe(用于支付处理)、Vue.js(客户端应用程序)和Flask(服务器端 API)开发一个用于销售书籍的 Web 应用程序。

最终应用

最终应用

主要依赖:

  • 查看 v3.2.47
  • 节点 v20.0.0
  • npm v9.6.4
  • 烧瓶 v2.2.3
  • Python v3.11.3

目标

在本教程结束时,您将能够:

  1. 使用由 Vue 和 Flask 提供支持的现有 CRUD 应用程序
  2. 创建订单结账 Vue 组件
  3. 使用 Stripe Checkout 验证信用卡信息并处理付款

项目设置

从flask-vue-crud仓库克隆基础 Flask 和 Vue 项目:

$ git clone https://github.com/testdrivenio/flask-vue-crud flask-vue-stripe
$ cd flask-vue-stripe

创建并激活虚拟环境,然后启动 Flask 应用程序:

$ cd server
$ python3.11 -m venv env
$ source env/bin/activate
(env)$

(env)$ pip install -r requirements.txt
(env)$ flask run --port=5001 --debug

上述用于创建和激活虚拟环境的命令可能因您的环境和操作系统而异。随意将 virtualenv 和 Pip 换成PoetryPipenv。有关更多信息,请查看现代 Python 环境

将您选择的浏览器指向http://localhost:5001/ping。你应该看到:

"pong!"

然后,安装依赖项并在不同的终端窗口中运行 Vue 应用程序:

$ cd client
$ npm install
$ npm run dev

导航到http://localhost:5173。确保基本的 CRUD 功能按预期工作:

基础应用

想了解如何构建这个项目?查看使用 Flask 和 Vue.js 教程开发单页应用程序。

我们在建造什么?

我们的目标是构建一个允许最终用户购买书籍的网络应用程序。

客户端 Vue 应用程序将显示可供购买的书籍,并通过Stripe.jsStripe Checkout将最终用户重定向到结帐表单。付款过程完成后,用户将被重定向到同样由 Vue 管理的成功或失败页面。

同时,Flask 应用程序使用Stripe Python 库与 Stripe API 交互以创建结帐会话。

最终应用

与之前的教程“使用 Flask 和 Vue.js 开发单页应用程序”一样,我们将只处理通过应用程序的快乐路径。通过自己结合适当的错误处理来检查您的理解。

图书增删改查

首先,让我们将购买价格添加到服务器端的现有图书列表中,并在客户端更新相应的 CRUD 函数——GET、POST 和 PUT。

得到

首先将 添加到server/app.py列表price中的每个字典:BOOKS

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'
    }
]

Books然后,更新组件中的表client/src/components/Books.vue,以显示购买价格:

<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>

你现在应该看到:

将购买价格添加到 Books 组件

邮政

addBookModal在 author 和 read 表单输入之间添加一个新的表单输入:

<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>

模态现在应该看起来像:

<!-- 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">&times;</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>

然后,添加price到状态:

addBookForm: {
  title: '',
  author: '',
  read: [],
  price: '',
},

状态现在绑定到表单的输入值。想想这意味着什么。当状态更新时,表单输入也将更新——反之亦然。这是一个使用vue-devtools浏览器扩展的例子:

状态模型绑定

像这样在方法中price添加:payloadhandleAddSubmit

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();
},

更新initForm以在最终用户提交表单或单击“重置”按钮后清除该值:

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 = [];
},

最后,更新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)

测试一下!

添加图书

不要忘记处理客户端和服务器上的错误!

自己做同样的事情来编辑一本书:

  1. 向模式添加新的表单输入
  2. editBookForm状态更新
  3. 在方法price中添加payloadhandleEditSubmit
  4. 更新initForm
  5. 更新服务端路由

需要帮忙?再次回顾上一节。您还可以从flask-vue-stripe存储库中获取最终代码。

购买按钮

在组件中添加一个“购买”按钮Books,就在“删除”按钮下方:

<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>

接下来,添加handlePurchaseBook到组件的methods

handlePurchaseBook(book) {
  console.log(book.id);
},

测试一下:

购买按钮

条纹键

注册一个Stripe帐户,如果您还没有的话。

服务器

安装条纹 Python 库:

(env)$ pip install stripe==5.4.0

从 Stripe 仪表板获取测试模式 API 密钥:

条纹仪表板

在运行服务器的终端窗口中将它们设置为环境变量:

(env)$ export STRIPE_PUBLISHABLE_KEY=<YOUR_STRIPE_PUBLISHABLE_KEY>
(env)$ export STRIPE_SECRET_KEY=<YOUR_STRIPE_SECRET_KEY>

将 Stripe 库导入server/app.py并将键分配给,stripe.api_key以便在与 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()

接下来,添加一个返回可发布密钥的新路由处理程序:

@app.route('/config')
def get_publishable_key():
    stripe_config = {'publicKey': stripe_keys['publishable_key']}
    return jsonify(stripe_config)

这将在客户端用于配置 Stripe.js 库。

客户

转向客户端,将Stripe.js添加到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>

Books接下来,向名为的组件添加一个新方法getStripePublishableKey

getStripePublishableKey() {
  fetch('http://localhost:5001/config')
    .then((result) => result.json())
    .then((data) => {
      // Initialize Stripe.js
      this.stripe = Stripe(data.publicKey);
    });
},

在钩子中调用这个方法created

created() {
  this.getBooks();
  this.getStripePublishableKey();
},

现在,在创建实例后,将调用http://localhost:5001/config,这将使用 Stripe 可发布密钥进行响应。然后我们将使用这个键来创建一个新的 Stripe.js 实例。

运送到生产?您需要使用环境变量来动态设置基本服务器端 URL(当前为http://localhost:5001)。查看文档以获取更多信息。

添加stripe到`状态:

data() {
  return {
    activeAddBookModal: false,
    activeEditBookModal: false,
    addBookForm: {
      title: '',
      author: '',
      read: [],
      price: '',
    },
    books: [],
    editBookForm: {
      id: '',
      title: '',
      author: '',
      read: [],
      price: '',
    },
    message: '',
    showMessage: false,
    stripe: null,
  };
},

条纹结帐

接下来,我们需要在服务器端生成一个新的 Checkout Session ID。单击购买按钮后,将向服务器发送 AJAX 请求以生成此 ID。服务器将发回 ID,用户将被重定向到结帐处。

服务器

添加以下路由处理程序:

@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

在这里,我们-

  1. 定义了一个domain_url用于在购买完成后将用户重定向回客户端
  2. 获取图书信息
  3. 创建结帐会话
  4. 在响应中发回 ID

注意success_urlcancel_url。如果付款成功或取消,用户将分别被重定向回这些 URL。我们将很快在客户端上设置/success和路由。/cancelled

此外,您是否注意到我们通过 将浮点数转换为整数round(float(book_to_purchase['price']) * 100)?Stripe 只允许价格的整数值。对于生产代码,您可能希望将价格作为整数值存储在数据库中——例如,3.99 美元应存储为399.

将导入添加到顶部:

import json

客户

在客户端,更新handlePurchaseBook方法:

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);
    });
},

在这里,在解决result.json()承诺后,我们redirectToCheckout使用已解决承诺中的 Checkout Session ID 调用该方法。

让我们测试一下。导航到http://localhost:5173。单击其中一个购买按钮。您应该被重定向到带有基本产品信息的 Stripe Checkout 实例(Stripe 托管的页面,用于安全地收集付款信息):

条纹结帐

您可以使用 Stripe 提供的多个测试卡号之一来测试表单。让我们使用4242 4242 4242 4242

  • 电子邮件:一个有效的电子邮件
  • 卡号:4242 4242 4242 4242
  • 到期日:未来的任何日期
  • CVC:任意三个数字
  • 姓名:随便
  • 邮政编码:任意五个数字

付款应该成功处理,但重定向将失败,因为我们/success还没有设置路由。

您应该在Stripe Dashboard中看到购买:

条纹仪表板

重定向页面

最后,让我们设置用于处理成功付款或取消的路由和组件。

成功

付款成功后,我们会将用户重定向到订单完成页面,感谢他们的购买。

添加一个名为OrderSuccess.vue的新组件文件到“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>

更新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

最终应用

最后,您可以使用查询参数显示有关购买的信息session_id

http://localhost:5173/success?session_id=cs_test_a1qw4pxWK9mF2SDvbiQXqg5quq4yZYUvjNkqPq1H3wbUclXOue0hES6lWl

您可以像这样访问它:

<script>
export default {
  mounted() {
    console.log(this.$route.query.session_id);
  },
};
</script>

从那里,您需要在服务器端设置路由处理程序,以通过 查找会话信息stripe.checkout.Session.retrieve(id)。自己试试看。

消除

对于重定向,添加一个名为client/src/components/OrderCanceled.vue/canceled的新组件:

<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>

然后,更新路由器:

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

最后一次测试一下。

结论

就是这样!一定要从头开始审查目标。您可以在 GitHub 上的flask-vue-stripe存储库中找到最终代码。

寻找更多?

  1. 添加客户端和服务器端单元和集成测试。
  2. 创建一个购物车,以便客户一次可以购买多本书。
  3. 添加 Postgres 来存储书籍和订单。
  4. 使用 Docker 将 Vue 和 Flask(以及 Postgres,如果你添加的话)容器化以简化开发工作流程。
  5. 将图像添加到书籍中并创建更强大的产品页面。
  6. 捕获电子邮件并发送电子邮件确认(查看使用 Flask、Redis 队列和 Amazon SES 发送确认电子邮件)。
  7. 将客户端静态文件部署到 AWS S3,将服务器端应用程序部署到 EC2 实例。
  8. 投入生产?考虑更新 Stripe 密钥的最佳方式,以便它们根据环境动态变化。

文章原文出处:https: //testdriven.io/

#flask #stripe #vue 

如何使用 Stripe、Vue.js 和 Flask 接受付款
Gordon  Matlala

Gordon Matlala

1682396700

How to Accepting Payments with Stripe, Vue.js, and Flask

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).

Final app:

final app

Main dependencies:

  • Vue v3.2.47
  • Node v20.0.0
  • npm v9.6.4
  • Flask v2.2.3
  • Python v3.11.3

Objectives

By the end of this tutorial, you will be able to:

  1. Work with an existing CRUD app, powered by Vue and Flask
  2. Create an order checkout Vue component
  3. Validate credit card information and process payments with Stripe Checkout

Project Setup

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:

"pong!"

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:

base app

Want to learn how to build this project? Check out the Developing a Single Page App with Flask and Vue.js tutorial.

What are we building?

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.

final app

Like 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.

Books CRUD

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.

GET

Start 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:

add purchase price to Books component

POST

Add 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">&times;</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:

state model bind

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!

add book

Don't forget to handle errors on both the client and server!

PUT

Do the same, on your own, for editing a book:

  1. Add a new form input to the modal
  2. Update editBookForm in the state
  3. Add the price to the payload in the handleEditSubmit method
  4. Update initForm
  5. Update the server-side route

Need help? Review the previous section again. You can also grab the final code from the flask-vue-stripe repo.

Purchase Button

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:

purchase button

Stripe Keys

Sign up for a Stripe account, if you don't already have one.

Server

Install the Stripe Python library:

(env)$ pip install stripe==5.4.0

Grab the test mode API keys from the Stripe dashboard:

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.

Client

Turning 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.

Server

Add 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-

  1. Defined a domain_url for redirecting the user back to the client after a purchase is complete
  2. Obtained the book info
  3. Created the Checkout Session
  4. Sent the ID back in the response

Take 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:

import json

Client

On 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:

stripe checkout

You can test the form by using one of the several test card numbers that Stripe provides. Let's use 4242 4242 4242 4242.

  • Email: a valid email
  • Card number: 4242 4242 4242 4242
  • Expiration: any date in the future
  • CVC: any three numbers
  • Name: anything
  • Postal code: any five numbers

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:

stripe dashboard

Redirect Pages

Finally, let's set up routes and components for handling a successful payment or cancellation.

Success

When 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

final app

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.

Cancellation

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.

Conclusion

That'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?

  1. Add client and server-side unit and integration tests.
  2. Create a shopping cart so customers can purchase more than one book at a time.
  3. Add Postgres to store the books and the orders.
  4. Containerize Vue and Flask (and Postgres, if you add it) with Docker to simplify the development workflow.
  5. Add images to the books and create a more robust product page.
  6. Capture emails and send email confirmations (review Sending Confirmation Emails with Flask, Redis Queue, and Amazon SES).
  7. Deploy the client-side static files to AWS S3 and the server-side app to an EC2 instance.
  8. Going into production? Think about the best way to update the Stripe keys so they are dynamic based on the environment.

Original article source at: https://testdriven.io/

#flask #stripe #vue 

How to Accepting Payments with Stripe, Vue.js, and Flask
React Dev

React Dev

1680496065

Build a Full Ecommerce Websites with React JS, Tailwind, Strapi and Stripe

Full Stack ECommerce Website With React JS, Tailwind, Strapi And Stripe

⏱ Timestamps
00:00:00 Project Intro
00:06:31 Project Setup
00:08:49 Routes
00:13:28 Strapi
01:23:12 Header
01:30:13 Cart Context
02:23:44 Product Details Page
02:43:45 Cart
03:52:08 Footer
04:03:43 Stripe
04:19:28 Final Project

Get Source Code :  https://www.buymeacoffee.com/cristianmihai/e/126806 

Download Images :  https://drive.google.com/drive/folders/1k9oTrPqClZTN90RgPGRBEsXydFH716Id?usp=sharing 

Start project from scratch : https://github.com/cristianmihai01/photoland-starter 

Subscribe: https://www.youtube.com/@cristianmihai01/featured 

#tailwindcss  #react #strapi #stripe #javascript 

Build a Full Ecommerce Websites with React JS, Tailwind, Strapi and Stripe
Lawrence  Lesch

Lawrence Lesch

1678185195

Staart API: SaaS Backend & API Framework Based on @nestjs

Staart API

Staart API is a Node.js backend starter for SaaS startups written in TypeScript. It has all the features you need to build a SaaS product, like user management and authentication, billing, organizations, GDPR tools, API keys, rate limiting, superadmin impersonation, and more.

⭐ Features

🆕 New in v2

  • Casbin-powered permission management
  • JWT-powered single-use coupon codes
  • Redis-powered queues for outbound emails and logs
  • Cloud agnostic, no longer specific to AWS
  • Staart scripts for building and deploying
  • Async JSON response and smart controller injection

🔐 Security

  • JWT-powered authentication and user management
  • TOTP-powered two-factor authentication (2FA)
  • OAuth2 login with third-party accounts
  • Location-based login verification
  • Security event logging and history

💳 SaaS

  • Stripe-powered recurring billing
  • Teams with managed user permissions
  • CRUD invoices, methods, transactions, etc.
  • Rich HTML transactional emails
  • GDPR-compliant data export and delete
  • API gateway with API keys and rate limiting
  • Domain verification with auto-approve members

👩‍💻 Developer utilities

  • OvernightJS-powered decorators and class syntax
  • Injection-proof helpers for querying databases
  • Data pagination and CRUD utilities for all tables
  • Authorization helpers
  • Caching and invalidation for common queries
  • User impersonation for super-admin
  • Easy redirect rules in YAML
  • ElasticSearch-powered server and event logs

🛠 Usage

  1. Use this template or fork this repository
  2. Install dependencies with npm install
  3. Add a .env file based on config.ts.
  4. Create MariaDB/MySQL tables based on schema.sql
  5. Add your controllers in the ./src/controllers directory
  6. Generate your app.ts file using staart controllers
  7. Build with staart build and deploy with staart launch

Updating Staart

To update your installation of Staart, run the following:

staart update api

If you've used the "Use this template" option on GitHub, you might have to force pull from staart/api the first time since the histories wouldn't match. You can use the flag --allow-unrelated-histories in this case.

💻 Docs

View docs site →

View TypeDoc →

View API demo →

View frontend demo →

🏗️ Built with Staart

🏁 Staart Ecosystem

The Staart ecosystem consists of open-source projects to build your SaaS startup, written in TypeScript.

Package  
🛠️ Staart APINode.js backend with RESTful APIsBuild status Docs npm package version
🌐 Staart UIFrontend Vue.js Progressive Web AppBuild status Docs npm package version
📑 Staart SiteStatic site generator for docs/helpdeskBuild status Docs npm package version
📱 Staart NativeReact Native app for Android and iOSBuild status Docs npm package version
🎨 Staart.cssSass/CSS framework and utilitiesBuild status Docs npm package version
📦 Staart PackagesHelper functions and utility packagesBuild status Custom badge

 Status
BuildNode CI Snyk Vulnerabilities for GitHub Repo Dependencies Dev dependencies
PRsPull Request Labeler PR Generator CI Merge PRs
CommunityContributors GitHub Type definitions npm package version semantic-release

Staart API is build to work with Staart UI, the frontend PWA starter for SaaS startups.

⚠️ v3 BETA WARNING: The master branch and all 3.x releases are currently in beta. For production, use v1.x instead.


Download Details:

Author: Staart
Source Code: https://github.com/staart/api 
License: MIT license

#typescript #nodejs #admin #stripe #auth #nest 

Staart API: SaaS Backend & API Framework Based on @nestjs
React Dev

React Dev

1675656264

Build a Full Stack E-Commerce Website with React 18, Strapi & Stripe

Build a complete ecommerce website from scratch, In this video we will cover all important topics of react js such as jsx, components, props, state,  lifecycle of components, conditional rendering, lists, keys, context api & more. At the end of this tutorial you would have a fully functional ecommerce website which you can use for your own business or you can sell out if to the others.

In this tutorial you’ll learn:
- React 18 and its latest features
- React functional components and their reusability
- React hooks and state management
- How to use Context API
- Advance concepts or Context API
- Rapid UI building with SCSS
- Most important concepts of SCSS
- Mobile first responsive approach
- How to use Strapi Headless CMS
- CRUD Operations in Strapi
- Functional Programming through utility methods
- How to use Axios in react app
- How to integrate Stripe payment gateway

👇 Time Stamps
00:00:00 Intro & Project Demo
00:03:58 Project Setup & Walkthrough of Folder Structure
00:08:39 React Router DOM Setup
00:19:21 Header Section
00:58:05 Homepage Hero Banner Section
01:19:38 Newsletter Section
01:34:35 Footer Section
01:59:28 Homepage Popular Products Section
02:13:58 Category Page
02:23:16 Product Details Page
02:49:04 Related Products for Product Details Page
02:52:07 Shopping Cart
03:30:35 Search Product Modal
03:50:22 Strapi Integration & Walkthrough
04:06:00 API Testing 
04:17:20 API Setup for Homepage Categories & Products Section 
04:28:17 Homepage Categories API Integration 
04:31:24 Homepage Products API Integration 
04:34:40 Category Page API Integration
04:52:53 Product Details Page API Integration
05:12:28 Product Details Page Related Products API Integration
05:17:17 Shopping Cart Functionality
05:50:03 Live Search Functionality
05:59:20 Stripe Payment Gateway Integration

⭐ React - https://reactjs.org/docs/getting-started.html 
⭐ Strapi - https://strapi.io/ 
⭐ Stripe - https://stripe.com/en-in 

📚 Materials/References:
GitHub Repository Client (give it a star ⭐): http://bit.ly/3hBeEjc 
GitHub Repository Api (give it a star ⭐): http://bit.ly/3v12SSo 
Gist: http://bit.ly/3jfyZuU 
Assets: http://bit.ly/3jcQYlD 
Products: http://bit.ly/3FOpUAy 

Installed NPM Packages:
Axios - https://www.npmjs.com/package/axios-react 
React Router DOM - https://www.npmjs.com/package/react-router-dom 
React Icons - https://www.npmjs.com/package/react-icons 

Download node 16 LTS version: https://drive.google.com/file/d/1CbEP6fQHqLLKgPMm-7yabGnwNyHagayn/view?usp=sharing 

Subscribe: https://www.youtube.com/@jsdevhindi/featured 

#react #javascript #strapi #stripe 

Build a Full Stack E-Commerce Website with React 18, Strapi & Stripe
Sheldon  Grant

Sheldon Grant

1672848126

How to Stripe Payment Gateway Integration in Scala

Introduction

In modern day there are a lot of companies implementing digital payment systems on their website to make easy and secure payment processes for customers.
There are a lot of payment methods available. Here is the list of payment methods:

  1. Paypal
  2. Stripe
  3. Razorpay
  4. PayU
  5. Instamojo
  6. CCAvenue
  7. Paytm

As an organization you need to be careful while choosing your payment gateway. There are some factors before choosing the correct option. Here is the list of factors:

  1. Customer experience
  2. Security
  3. Simple integration
  4. Multiple currency support
  5. Reputation & service support
  6. Cost of transaction
  7. Recurring Billing

In this blog we will be talking about how stripe checkout works when a customer makes an order.

Stripe Checkout Lifecycle

  1. Client sends order information
  2. Create a checkout session (server side)
  3. Stripe sends back checkout session url
  4. Redirect customer to url from checkout session
  5. Customer completes their payment
  6. Webhook
  7. Customer returns to your application

Implementation with source code

First of all we need to add stripe dependency in application

// https://mvnrepository.com/artifact/com.stripe/stripe-java

libraryDependencies += "com.stripe" % "stripe-java" % "22.0.0"

Client sends order information:

As part of this client collects order information selected by customer. After collecting these info the client sends back to the back-end server to create checkout session.

Create a checkout session:

A Checkout Session represents your customer’s session as they pay for one-time purchases or subscriptions through Checkout or Payment Links. It is recommended to create a new checkout each time when your customer attempts to pay. To create a checkout session you need to make a call to stripe api with the following details:

Scala code:

  import play.api.libs.ws._
  
  import scala.concurrent.ExecutionContext.Implicits.global

  def createCheckout() = Action { implicit request: Request[AnyContent] =>

    val url = "https://api.stripe.com/v1/checkout/sessions"

    val data = Map(

      "success_url" -> "https://example.com/success",

      "cancel_url" -> "https://example.com/cancel",

      "line_items[0][price]" -> "price_XXXXXX",

      "line_items[0][quantity]" -> "2",

      "mode" -> "payment"
    )

    val sk = "sk_test_XXXXXXX"

    ws.url(url).withAuth(sk,"",WSAuthScheme.BASIC)

    .post(data).map {

      res=>

        println(s"Stripe checkout response: ${res.json.toString()}")

    }

    Ok("Checkout session Generated successfully")
  }

Here we have passed only the required field for stripe checkout session. You can attach more fields while creating a session. You can also attach metadata. Metadata is useful for storing additional, structured information on an object. As an example, you could store your user’s full name and corresponding unique identifier from your system on a Stripe Customer object. Metadata is not used by Stripe.

Stripe sends back checkout session url

After the successful checkout response. The stripe sends back a checkout session url. You can redirect your customer to the payment page.

Redirect customer to url from checkout session

This step is covered in step3.

Customer completes their payment

Customers enter their payment details on the payment page and complete the transaction. UI payment demo is shown in below image:

Webhook

After the transaction, a webhook fulfils the order using the checkout.session.completed event.
A webhook enables Stripe to push real-time notifications to your app. Stripe uses HTTPS to send these notifications to your app as a JSON payload. You can then use these notifications to execute actions in your backend systems.
We will discuss in more details about stripe webhook workflow in the upcoming session.

Customer returns to your application:

After the payment is successfully made you can show them a success page or the order page which you have passed the url while creating the checkout session.

Conclusion

We have seen stripe online payment gateway is reliable, convenient and easy to integrate. It is the second most popular payment processing software after PayPal. Stripe payment gateway is choosen by the top companies. Some of them are Amazon, Apple, Walmart, Wayfair, Shopify, Figma, instacart, pelton etc.

Original article source at: https://blog.knoldus.com/

#scala #stripe #integration 

How to Stripe Payment Gateway Integration in Scala
Hermann  Frami

Hermann Frami

1672461120

Ecommerce-netlify: Ecommerce Store with Netlify Functions and Stripe

🛍 Ecommerce Store with Netlify Functions and Stripe

A serverless function to process stripe payments with Nuxt, Netlify, and Lambda

Demo site is here: E-Commerce Store

screenshot of site

There are two articles explaining how this site is set up:

Build Setup

# install dependencies
$ yarn install or npm run install

# serve with hot reload at localhost:3000
$ yarn dev or npm run dev

# build for production and launch server
$ yarn build or npm run build
$ yarn start or npm run start

# generate static project
$ yarn generate or npm run generate

For detailed explanation on how things work, checkout Nuxt.js docs.

Download Details:

Author: sdras
Source Code: https://github.com/sdras/ecommerce-netlify 

#serverless #stripe #nuxt 

Ecommerce-netlify: Ecommerce Store with Netlify Functions and Stripe

Build Your Own SaaS with PostgreSQL + Stripe API + Twilio + SMTP

Build Your Own SaaS - PagerDuty Clone. You'll Learn PostgreSQL + Stripe API + Twilio + SMTP

Learn how to build your own SaaS app. You will create your own PagerDuty clone using PostgreSQL, Stripe, Twilio, SMTP, and Retool.

You will build a dashboard that lets you know if your app goes down, and then notifies you through email and SMS.

⭐️ Contents ⭐️
00:00 Introduction
02:51 Tutorial Starts
03:41 Working with pre-made UI Components
16:26 Setting up our Postgres database
18:58 Creating Tables in Postgres
29:00 Feeding in Data to our Dashboard
39:09 Adding new Incidents
48:53 Deleting Incidents
50:49 The Team members page
1:04:42 Hooking up the Twilio and SMPT API

#sass #postgresql #stripe #api #twilio 

Build Your Own SaaS with PostgreSQL + Stripe API + Twilio + SMTP
Web  Dev

Web Dev

1669435680

Full Stack Ecommerce App with React, Strapi and Stripe

In this Shopping App tutorial, you'll learn how to build and deploy a full-stack E-commerce App with React, Strapi and Stripe. Full-stack React e-commerce app tutorial for beginners.


00:00 Introduction
05:36 Installation  
07:12 React Router Dom v6.4 Tutorial
14:05 React Navbar Design
25:46 React Footer Design
32:11 React Slider Tutorial (without any library)
44:00 Featured Products and Product Card Design
55:21 Grid System Using CSS Flexbox
01:09:13 React Product List Page Design
01:21:07 React Single Product Page Design
01:34:00 React Shopping Cart Design
01:43:15 Strapi Tutorial for Beginners
01:56:20 React Strapi E-commerce App (Connection)
02:07:56 React Strapi E-commerce App (Fetching Items)
02:17:35 React Strapi Rest Api Filtering Tutorial
02:28:35 React E-commerce Fetch a Single Product
02:31:26  React Redux Shopping Cart Tutorial
02:27:40 React Redux Toolkit How to Persist State
02:50:05 Strapi Stripe Payment Using a React App
03:11:40 How to Deploy React app to a Shared Hosting?
03:13:22 How to Deploy Strapi app to a VPS Server?
03:30:15 Outro

Source Code:

https://github.com/safak/youtube2022/tree/e-commerce  ( Coming soon (updating files) )

Deployment source:

https://github.com/safak/youtube/tree/mern-deployment

#react #strapi #stripe #programming #developer #morioh #softwaredeveloper #computerscience #webdev #webdeveloper #webdevelopment 

Full Stack Ecommerce App with React, Strapi and Stripe

How to integrate Stripe Connect Into A Django Application

Stripe Connect is a service designed for processing and managing payments on behalf of others. It's used by marketplaces and platforms (e.g., Uber, Shopify, Kickstarter, and Airbnb) that need to pay multiple parties. We use it at TestDriven.io to power our payments platform so that we can easily pay content creators and affiliates.

This tutorial looks at how to integrate Stripe Connect into a Django application.

This is an intermediate-level tutorial. It assumes that you are familiar with both Stripe and Django. Review the Django Stripe Tutorial post for more info.

Learning Objectives

By the end of this tutorial, you should be able to:

  1. Explain what Stripe Connect is and why you may need to use it
  2. Describe the similarities and differences between the types of Stripe Connect accounts
  3. Integrate Stripe Connect into an existing Django app
  4. Link a Stripe account to your Django app using an Oauth-like flow
  5. Explain the differences between Direct and Destination charges

Stripe Connect Accounts

With Stripe Connect, you first need to decide the type(s) of user accounts that you want to work with on your platform:

  1. Standard
  2. Express
  3. Custom

"Platform" refers to your marketplace web application while "user" is the person being paid for selling goods or services through your platform.

With Standard and Express accounts, users on your platform go through an OAuth-like flow, where they are sent to Stripe, create or link their Stripe accounts, and then are redirected back to your platform. This can be quite disruptive. The user also needs to maintain two accounts - one for your platform and one for Stripe - which is not ideal. This probably won't work if you expect to onboard a large number of users per month. On the other hand, if you're just getting started and want to get going quickly, stick with Standard accounts. Start there. If you find that you need more control over the onboarding experience, to make it more seamless, then you may want to switch to integrating with Express or Custom accounts.

Besides the UX, with Express and Custom accounts, you (the platform) are ultimately liable for fraud and must handle disputed transactions.

For reference, the TestDriven.io platform uses both Standard and Express accounts, depending on the type of transaction that is made and how many parties are involved.

When choosing the type(s) of accounts to work with, ask yourself:

  1. How seamless does the onboarding experience need to be?
  2. Who should handle fraud and payment disputes?

We'll stick with Standard accounts in this tutorial. For more, review the Choosing an approach and Best Practices guides from Stripe.

Workflow

Again, with Standard (and Express) accounts, users go through an OAuth-like flow to connect their Stripe accounts:

  1. Authenticated users on your platform click a link that takes them to Stripe
  2. They then connect a Stripe account by either logging in to an existing account or creating a new account
  3. Once connected, users are redirected back to your platform with an authorization code
  4. You then make a request to Stripe with that code in order to get the information needed for processing payments

stripe connect flow

Initial Setup

To get going, clone down the django-stripe-connect repo, and then check out the v1 tag to the master branch:

$ git clone https://github.com/testdrivenio/django-stripe-connect --branch v1 --single-branch
$ cd django-stripe-connect
$ git checkout tags/v1 -b master

Create a virtual environment and install the dependencies:

$ pipenv shell
$ pipenv install

Apply the migrations, create a superuser, and add the fixtures to the DB:

$ python manage.py migrate
$ python manage.py createsuperuser
$ python manage.py loaddata fixtures/users.json
$ python manage.py loaddata fixtures/courses.json

Run the server:

$ python manage.py runserver

At http://localhost:8000/ you should see:

example app

Make sure you are able to log in as the superuser:

example app

example app

Try logging in as both a buyer and a seller.

Buyer:

  • email: test@buyer.com
  • password: justatest

Seller:

  • email: test@seller.com
  • password: justatest

Essentially, this sample application is similar to the TestDriven.io platform - users can create and sell courses. The CustomUser model extends the built-in User model, creating sellers and buyers. Sellers can buy and sell courses while buyers can only, well, buy courses. When a new user is added, they are a buyer by default. Superusers can change a user's status.

Take a quick look at the project structure before moving on:

├── Pipfile
├── Pipfile.lock
├── apps
│   ├── courses
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── migrations
│   │   ├── models.py
│   │   ├── tests.py
│   │   ├── urls.py
│   │   └── views.py
│   └── users
│       ├── __init__.py
│       ├── admin.py
│       ├── apps.py
│       ├── forms.py
│       ├── managers.py
│       ├── migrations
│       ├── models.py
│       ├── signals.py
│       ├── tests.py
│       └── views.py
├── fixtures
│   ├── courses.json
│   └── users.json
├── manage.py
├── my_project
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── static
│   └── bulma.min.css
└── templates
    ├── _base.html
    ├── courses
    │   ├── course_detail.html
    │   └── course_list.html
    ├── home.html
    ├── login.html
    └── nav.html

Configure Stripe

Stripe Checkout has been pre-configured as well as the Stripe Python Library. In order to process payments, create a Stripe account (if you don't already have one) and add your test secret and test publishable keys to the bottom of the settings.py file:

STRIPE_PUBLISHABLE_KEY = '<your test publishable key here>'
STRIPE_SECRET_KEY = '<your test secret key here>'

Ensure you can process a charge. First, log in with the buyer account:

  • email: test@buyer.com
  • password: justatest

Then, purchase a course:

example app

You should see the charge on the Stripe dashboard under "Payments":

stripe dashboard

Need help? Refer to the Add Stripe section from the Django Stripe Tutorial blog post.

To register your platform with Stripe Connect, click on "Connect" in the left sidebar on the Stripe dashboard:

stripe dashboard

Then, click the "Get started" button. Once you're registered, click the "Settings" link and grab your test client ID:

stripe dashboard

Add this to the bottom of settings.py:

STRIPE_CONNECT_CLIENT_ID = '<your test connect client id here>'

Back on the dashboard, use http://localhost:8000/users/oauth/callback as the redirect URI. Feel free to update the "Branding" section as well:

stripe dashboard

Connect Account

Moving on, let's add a link to the "Connect Stripe Account" button on the home page that sends users to Stripe so they can link their account.

example app

Redirect to Stripe

Add a view to apps/users/views.py:

import urllib

from django.urls import reverse
from django.http import HttpResponseRedirect
from django.views import View
from django.conf import settings
from django.shortcuts import redirect


class StripeAuthorizeView(View):

    def get(self, request):
        if not self.request.user.is_authenticated:
            return HttpResponseRedirect(reverse('login'))
        url = 'https://connect.stripe.com/oauth/authorize'
        params = {
            'response_type': 'code',
            'scope': 'read_write',
            'client_id': settings.STRIPE_CONNECT_CLIENT_ID,
            'redirect_uri': f'http://localhost:8000/users/oauth/callback'
        }
        url = f'{url}?{urllib.parse.urlencode(params)}'
        return redirect(url)

If the user is authenticated, we create the OAuth link from the response_type, scope, client_id, and redirect_uri and then redirect them to Stripe via the authorize URL.

scope can be read_only or read_write:

  1. Use read_only when the platform just needs view access.
  2. Use read_write when the platform needs view, create, and modify access in order to perform charges on behalf of the connected account.

For more, review What permissions does the platform receive when connecting a Stripe account?

Update the project-level URLs in my_project/urls.py:

urlpatterns = [
    path('', TemplateView.as_view(template_name='home.html'), name='home'),
    path('login/', LoginView.as_view(template_name='login.html'), name='login'),
    path('logout/', LogoutView.as_view(), {'next_page': settings.LOGOUT_REDIRECT_URL}, name='logout'),
    path('courses/', include('apps.courses.urls')),
    path('users/', include('apps.users.urls')),
    path('admin/', admin.site.urls),
]

Then, add the app-level URLs by adding a urls.py file to "apps/users":

from django.urls import path

from .views import StripeAuthorizeView


urlpatterns = [
  path('authorize/', StripeAuthorizeView.as_view(), name='authorize'),
]

Add the href to the "Connect Stripe Account" button in the home.html template:

<a href="{% url 'authorize' %}" class="button is-info">Connect Stripe Account</a>

To test, run the Django server, and then log in with the seller account:

  • email: test@seller.com
  • password: justatest

Make sure you're redirected to Stripe when you click "Connect Stripe Account":

stripe dashboard

Don't do anything just yet as we still need to set up the redirect view.

Redirect Back

Add a new view to apps/users/views.py

class StripeAuthorizeCallbackView(View):

    def get(self, request):
        code = request.GET.get('code')
        if code:
            data = {
                'client_secret': settings.STRIPE_SECRET_KEY,
                'grant_type': 'authorization_code',
                'client_id': settings.STRIPE_CONNECT_CLIENT_ID,
                'code': code
            }
            url = 'https://connect.stripe.com/oauth/token'
            resp = requests.post(url, params=data)
            print(resp.json())
        url = reverse('home')
        response = redirect(url)
        return response

After the Stripe account is connected, the user is redirected back to the platform, where we'll use the provided authorization code to call the access token URL to obtain the user's Stripe credentials.

Install the requests library:

$ pipenv install requests==2.21.0

Add the import to apps/users/views.py:

import requests

Add the callback URL to apps/users/urls.py

from django.urls import path

from .views import StripeAuthorizeView, StripeAuthorizeCallbackView


urlpatterns = [
  path('authorize/', StripeAuthorizeView.as_view(), name='authorize'),
  path('oauth/callback/', StripeAuthorizeCallbackView.as_view(), name='authorize_callback'),
]

Next, create a new Stripe account for testing purposes, which you'll use to connect to the platform account. Once done, you can test the full OAuth process:

  1. Navigate to http://localhost:8000/ in an incognito or private browser window
  2. Log in to the platform with test@seller.com / justatest
  3. Click "Connect Stripe Account"
  4. Log in with the new Stripe Account
  5. Click the "Connect my Stripe account" button, which will redirect you back to the Django app

In your terminal you should see the output from print(resp.json()):

{
  'access_token': 'sk_test_nKM42TMNPm6M3c98U07abQss',
  'livemode': False,
  'refresh_token': 'rt_5QhvTKUgPuFF1EIRsHV4b4DtTxDZgMQiQRvOoMewQptbyfRc',
  'token_type': 'bearer',
  'stripe_publishable_key': 'pk_test_8iD6CpftCZLTp40k1pAl22hp',
  'stripe_user_id': 'acct_i3qMgnSiH35BL8aU',
  'scope': 'read_write'
}

We can now add the access_token and stripe_user_id to the Seller model:

class StripeAuthorizeCallbackView(View):

    def get(self, request):
        code = request.GET.get('code')
        if code:
            data = {
                'client_secret': settings.STRIPE_SECRET_KEY,
                'grant_type': 'authorization_code',
                'client_id': settings.STRIPE_CONNECT_CLIENT_ID,
                'code': code
            }
            url = 'https://connect.stripe.com/oauth/token'
            resp = requests.post(url, params=data)
            # add stripe info to the seller
            stripe_user_id = resp.json()['stripe_user_id']
            stripe_access_token = resp.json()['access_token']
            seller = Seller.objects.filter(user_id=self.request.user.id).first()
            seller.stripe_access_token = stripe_access_token
            seller.stripe_user_id = stripe_user_id
            seller.save()
        url = reverse('home')
        response = redirect(url)
        return response

Add the import to the top:

from .models import Seller

You may want to save the refresh_token as well so you can request a new access_token.

To recap, after a user connects a Stripe account, you are given a temporary authorization code that is used to request the user's access token and id - which are then used for connecting to Stripe and processing payments on the user's behalf, respectively.

Test it out once more. Once done, log out, log back in as the superuser, and verify that the seller was updated in the Django admin:

django admin

Finally, hide the "Connect Stripe Account" button in the home template if the user has already connected their Stripe account:

{% if user.is_seller and not user.seller.stripe_user_id %}
  <a href="{% url 'authorize' %}" class="button is-info">Connect Stripe Account</a>
{% endif %}

With that, we can turn our attention to the purchasing side of things.

Buy a Course

First, you need to make a decision on the how you want the charge to be handled:

  1. Direct charge
  2. Destination charge
  3. Separate Charge and Transfer

We'll look at the first two approaches in this tutorial. Just keep in mind that the account type together with the payment approach determines the liability:

Account TypePayment ApproachLiability
StandardDirectUser
StandardDestinationPlatform
ExpressDirectUser
ExpressDestinationPlatform
ExpressSeparate Charge and TransferPlatform
CustomDirectUser
CustomDestinationPlatform
CustomSeparate Charge and TransferPlatform

There are exceptions to this, so be sure to review the Choosing an approach guide for more info on the differences between the three approaches.

TestDriven.io uses destination charges, since all charges and customers are "owned" by the platform rather than the connected account.

Direct

Use Direct charges when you want the payment to be handled by the connected Stripe account rather than the platform account.

Update CourseChargeView in apps/courses/views.py like so:

class CourseChargeView(View):

    def post(self, request, *args, **kwargs):
        stripe.api_key = settings.STRIPE_SECRET_KEY
        json_data = json.loads(request.body)
        course = Course.objects.filter(id=json_data['course_id']).first()
        try:
            charge = stripe.Charge.create(
                amount=json_data['amount'],
                currency='usd',
                source=json_data['token'],
                description=json_data['description'],
                stripe_account=course.seller.stripe_user_id,
            )
            if charge:
                return JsonResponse({'status': 'success'}, status=202)
        except stripe.error.StripeError as e:
            return JsonResponse({'status': 'error'}, status=500)

Try this out.

Did you notice that the charge only shows up on the connected account's dashboard? With Direct charges the customer is technically purchasing from the business associated with the connected account, not the platform. The connected account is responsible for paying the Stripe fees along with any potential refunds or chargebacks. If you need to review the charge, you can retrieve it from the API.

You can also charge a customer object. Think about where you want that customer to live - platform account, connected account, or both?

class CourseChargeView(View):

    def post(self, request, *args, **kwargs):
        stripe.api_key = settings.STRIPE_SECRET_KEY
        json_data = json.loads(request.body)
        course = Course.objects.filter(id=json_data['course_id']).first()
        try:
            customer = stripe.Customer.create(
                email=self.request.user.email,
                source=json_data['token'],
                stripe_account=course.seller.stripe_user_id,
            )
            charge = stripe.Charge.create(
                amount=json_data['amount'],
                currency='usd',
                customer=customer.id,
                description=json_data['description'],
                stripe_account=course.seller.stripe_user_id,
            )
            if charge:
                return JsonResponse({'status': 'success'}, status=202)
        except stripe.error.StripeError as e:
            return JsonResponse({'status': 'error'}, status=500)

In this example, the customer is created on the connected account and that customer id is then used to process the charge.

What if the customer already exists on the connected account?

class CourseChargeView(View):

    def post(self, request, *args, **kwargs):
        stripe.api_key = settings.STRIPE_SECRET_KEY
        json_data = json.loads(request.body)
        course = Course.objects.filter(id=json_data['course_id']).first()
        try:
            customer = get_or_create_customer(
                self.request.user.email,
                json_data['token'],
                course.seller.stripe_access_token,
                course.seller.stripe_user_id,
            )
            charge = stripe.Charge.create(
                amount=json_data['amount'],
                currency='usd',
                customer=customer.id,
                description=json_data['description'],
                stripe_account=course.seller.stripe_user_id,
            )
            if charge:
                return JsonResponse({'status': 'success'}, status=202)
        except stripe.error.StripeError as e:
            return JsonResponse({'status': 'error'}, status=500)


# helpers

def get_or_create_customer(email, token, stripe_access_token, stripe_account):
    stripe.api_key = stripe_access_token
    connected_customers = stripe.Customer.list()
    for customer in connected_customers:
        if customer.email == email:
            print(f'{email} found')
            return customer
    print(f'{email} created')
    return stripe.Customer.create(
        email=email,
        source=token,
        stripe_account=stripe_account,
    )

Test it out, making sure a customer object is created only if it does not already exist.

What if you wanted to "share" the customer between the two accounts? In this case you'd probably want to store the customer info on the platform account so you can charge that customer directly, when the connected account is not involved, and then use that same customer object to process charges, when the connected account is involved. There's no need to create the customer twice if you don't have to. Implement this on your on. Refer to the Shared Customers guide for more info.

Before moving on to the next approach, let's quickly look at how the platform can collect a convenience fee on each transaction:

class CourseChargeView(View):

    def post(self, request, *args, **kwargs):
        stripe.api_key = settings.STRIPE_SECRET_KEY
        json_data = json.loads(request.body)
        course = Course.objects.filter(id=json_data['course_id']).first()
        fee_percentage = .01 * int(course.fee)
        try:
            customer = get_or_create_customer(
                self.request.user.email,
                json_data['token'],
                course.seller.stripe_access_token,
                course.seller.stripe_user_id,
            )
            charge = stripe.Charge.create(
                amount=json_data['amount'],
                currency='usd',
                customer=customer.id,
                description=json_data['description'],
                application_fee=int(json_data['amount'] * fee_percentage),
                stripe_account=course.seller.stripe_user_id,
            )
            if charge:
                return JsonResponse({'status': 'success'}, status=202)
        except stripe.error.StripeError as e:
            return JsonResponse({'status': 'error'}, status=500)

Now, after a charge is processed, you should see the a collected fee on the platform account:

stripe dashboard

Destination

Destination charges work best when you (the platform) want to maintain ownership over the customer. With this approach, the platform account charges the customer and is responsible for paying Stripe fees and any potential refunds or chargebacks.

class CourseChargeView(View):

    def post(self, request, *args, **kwargs):
        stripe.api_key = settings.STRIPE_SECRET_KEY
        json_data = json.loads(request.body)
        course = Course.objects.filter(id=json_data['course_id']).first()
        fee_percentage = .01 * int(course.fee)
        try:
            customer = get_or_create_customer(
                self.request.user.email,
                json_data['token'],
            )
            charge = stripe.Charge.create(
                amount=json_data['amount'],
                currency='usd',
                customer=customer.id,
                description=json_data['description'],
                destination={
                    'amount': int(json_data['amount'] - (json_data['amount'] * fee_percentage)),
                    'account': course.seller.stripe_user_id,
                },
            )
            if charge:
                return JsonResponse({'status': 'success'}, status=202)
        except stripe.error.StripeError as e:
            return JsonResponse({'status': 'error'}, status=500)

# helpers

def get_or_create_customer(email, token):
    stripe.api_key = settings.STRIPE_SECRET_KEY
    connected_customers = stripe.Customer.list()
    for customer in connected_customers:
        if customer.email == email:
            print(f'{email} found')
            return customer
    print(f'{email} created')
    return stripe.Customer.create(
        email=email,
        source=token,
    )

Test this out. This should create a customer and a charge on the platform. You should also see a transfer to the connected account:

stripe dashboard

Conclusion

This tutorial took you through the process of setting up Stripe Connect to securely manage payments on behalf of others.

You should now be able to:

  1. Explain what Stripe Connect is and why you may need to use it
  2. Describe the similarities and differences between the types of Stripe Connect accounts
  3. Integrate Stripe Connect into an existing Django app
  4. Link a Stripe account to your Django app using an Oauth-like flow
  5. Explain the differences between Direct and Destination charges

Looking for some challenges?

  1. Add a sign up form and a user account page
  2. Send emails asynchronously to the buyer and seller after a sale is made
  3. Experiment with Custom accounts in order to make the Stripe Connect integration seamless for end users
  4. Handle subscriptions
  5. Add a sales dashboard page for buyers

You can find the final code in the django-stripe-connect repo on GitHub. Cheers!

Original article source at: https://testdriven.io/

#django #stripe #integrate 

How to integrate Stripe Connect Into A Django Application

Quickly add Stripe to accept payments on a Django/Python website

In this tutorial I'll demonstrate how to configure a new Django website from scratch to accept one-time payments with Stripe.

Need to handle subscription payments? Check out Django Stripe Subscriptions.

Stripe Payment Options

There are currently three ways to accept one-time payments with Stripe:

  1. Charges API (legacy)
  2. Stripe Checkout (the focus of this tutorial)
  3. Payment Intents API (often coupled with Stripe Elements)

Which one should you use?

  1. Use Checkout if you want to get up and running fast. If you're familiar with the old modal version of Checkout, this is the way to go. It provides a ton of features out-of-the-box, supports multiple languages, and includes an easy path to implementing recurring payments. Most importantly, Checkout manages the entire payment process for you, so you can begin accepting payments without even having to add a single form!
  2. Use the Payment Intents API If you want a more custom experience for your end users.

Although you can still use the Charges API, if you're new to Stripe do not use it since it does not support the latest banking regulations (like SCA). You will see a high rate of declines. For more, review the Charges vs. Payment Intents APIs page from the official Stripe docs.

Still using the Charges API? If most of your customers are based in the US or Canada you don't need to migrate just yet. Review the Checkout migration guide guide for more info.

Project Setup

Create a new project directory along with a new Django project called djangostripe:

$ mkdir django-stripe-checkout && cd django-stripe-checkout
$ python3.10 -m venv env
$ source env/bin/activate

(env)$ pip install django==3.2.9
(env)$ django-admin startproject djangostripe .

Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.

Next, create a new app called payments:

(env)$ python manage.py startapp payments

Now add the new app to the INSTALLED_APPS configuration in settings.py:

# djangostripe/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # Local
    'payments.apps.PaymentsConfig', # new
]

Update the project-level urls.py file with the payments app:

# djangostripe/urls.py

from django.contrib import admin
from django.urls import path, include # new

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('payments.urls')), # new
]

Create a urls.py file within the new app, too:

(env)$ touch payments/urls.py

Then populate it as follows:

# payments/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
]

Now add a views.py file:

# payments/views.py

from django.views.generic.base import TemplateView

class HomePageView(TemplateView):
    template_name = 'home.html'

And create a dedicated "templates" folder and file for our homepage.

(env)$ mkdir templates
(env)$ touch templates/home.html

Then, add the following HTML:

<!-- templates/home.html -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
    <script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <button class="button is-primary" id="submitBtn">Purchase!</button>
    </div>
  </section>
  </body>
</html>

Make sure to update the settings.py file so Django knows to look for a "templates" folder:

# djangostripe/settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'], # new
        ...

Finally run migrate to sync the database and runserver to start Django's local web server.

(env)$ python manage.py migrate
(env)$ python manage.py runserver

That's it! Check out http://localhost:8000/ and you'll see the homepage:

Django

Add Stripe

Time for Stripe. Start by installing it:

(env)$ pip install stripe==2.63.0

Next, register for a Stripe account (if you haven't already done so) and navigate to the dashboard. Click on "Developers":

Stripe Developers

Then in the left sidebar click on "API keys":

Stripe Developers Key

Each Stripe account has four API keys: two for testing and two for production. Each pair has a "secret key" and a "publishable key". Do not reveal the secret key to anyone; the publishable key will be embedded in the JavaScript on the page that anyone can see.

Currently the toggle for "Viewing test data" in the upper right indicates that we're using the test keys now. That's what we want.

At the bottom of your settings.py file, add the following two lines including your own test secret and test publishable keys. Make sure to include the '' characters around the actual keys.

# djangostripe/settings.py

STRIPE_PUBLISHABLE_KEY = '<your test publishable key here>'
STRIPE_SECRET_KEY = '<your test secret key here>'

Finally, you'll need to specify an "Account name" within your "Account settings" at https://dashboard.stripe.com/settings/account:

Stripe Account Name

Create a Product

Next, we need to create a product to sell.

Click "Products" and then "Add product":

Stripe Add Product

Add a product name, enter a price, and select "One time":

Stripe Add Product

Click "Save product".

User Flow

After the user clicks the purchase button we need to do the following:

Get Publishable Key

  • Send an XHR request from the client to the server requesting the publishable key
  • Respond with the key
  • Use the key to create a new instance of Stripe.js

Create Checkout Session

  • Send another XHR request to the server requesting a new Checkout Session ID
  • Generate a new Checkout Session and send back the ID
  • Redirect to the checkout page for the user to finish their purchase

Redirect the User Appropriately

  • Redirect to a success page after a successful payment
  • Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks

  • Set up the webhook endpoint
  • Test the endpoint using the Stripe CLI
  • Register the endpoint with Stripe

Get Publishable Key

JavaScript Static File

Let's start by creating a new static file to hold all of our JavaScript:

(env)$ mkdir static
(env)$ touch static/main.js

Add a quick sanity check to the new main.js file:

// static/main.js

console.log("Sanity check!");

Then update the settings.py file so Django knows where to find static files:

# djangostripe/settings.py

STATIC_URL = '/static/'

# for django >= 3.1
STATICFILES_DIRS = [BASE_DIR / 'static']  # new

# for django < 3.1
# STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]  # new

Add the static template tag along with the new script tag inside the HTML template:

<!-- templates/home.html -->

{% load static %} <!-- new -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
    <script src="{% static 'main.js' %}"></script>   <!-- new -->
    <script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <button class="button is-primary" id="submitBtn">Purchase!</button>
    </div>
  </section>
  </body>
</html>

Run the development server again. Navigate to http://localhost:8000/, and open up the JavaScript console. You should see the sanity check:

JavaScript Sanity Check

View

Next, add a new view to payments/views.py to handle the XHR request:

# payments/views.py

from django.conf import settings # new
from django.http.response import JsonResponse # new
from django.views.decorators.csrf import csrf_exempt # new
from django.views.generic.base import TemplateView


class HomePageView(TemplateView):
    template_name = 'home.html'


# new
@csrf_exempt
def stripe_config(request):
    if request.method == 'GET':
        stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
        return JsonResponse(stripe_config, safe=False)

Add a URL as well:

# payments/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
    path('config/', views.stripe_config),  # new
]

XHR Request

Next, use the Fetch API to make an XHR (XMLHttpRequest) request to the new /config/ endpoint in static/main.js:

// static/main.js

console.log("Sanity check!");

// new
// Get Stripe publishable key
fetch("/config/")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);
});

The response from a fetch request is a ReadableStream. result.json() returns a promise, which we resolved to a JavaScript object -- e.g., data. We then used dot-notation to access the publicKey in order to obtain the publishable key.

Include Stripe.js in templates/home.html like so:

<!-- templates/home.html -->

{% load static %}

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
    <script src="https://js.stripe.com/v3/"></script>  <!-- new -->
    <script src="{% static 'main.js' %}"></script>
    <script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <button class="button is-primary" id="submitBtn">Purchase!</button>
    </div>
  </section>
  </body>
</html>

Now, after the page load, a call will be made to /config/, which will respond with the Stripe publishable key. We'll then use this key to create a new instance of Stripe.js.

Flow:

Get Publishable Key

  • Send an XHR request from the client to the server requesting the publishable key
  • Respond with the key
  • Use the key to create a new instance of Stripe.js

Create Checkout Session

  • Send another XHR request to the server requesting a new Checkout Session ID
  • Generate a new Checkout Session and send back the ID
  • Redirect to the checkout page for the user to finish their purchase

Redirect the User Appropriately

  • Redirect to a success page after a successful payment
  • Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks

  • Set up the webhook endpoint
  • Test the endpoint using the Stripe CLI
  • Register the endpoint with Stripe

Create Checkout Session

Moving on, we need to attach an event handler to the button's click event which will send another XHR request to the server to generate a new Checkout Session ID.

View

First, add the new view:

# payments/views.py

@csrf_exempt
def create_checkout_session(request):
    if request.method == 'GET':
        domain_url = 'http://localhost:8000/'
        stripe.api_key = settings.STRIPE_SECRET_KEY
        try:
            # Create new Checkout Session for the order
            # Other optional params include:
            # [billing_address_collection] - to display billing address details on the page
            # [customer] - if you have an existing Stripe Customer ID
            # [payment_intent_data] - capture the payment later
            # [customer_email] - prefill the email input in the form
            # For full details see https://stripe.com/docs/api/checkout/sessions/create

            # ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
            checkout_session = stripe.checkout.Session.create(
                success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
                cancel_url=domain_url + 'cancelled/',
                payment_method_types=['card'],
                mode='payment',
                line_items=[
                    {
                        'name': 'T-shirt',
                        'quantity': 1,
                        'currency': 'usd',
                        'amount': '2000',
                    }
                ]
            )
            return JsonResponse({'sessionId': checkout_session['id']})
        except Exception as e:
            return JsonResponse({'error': str(e)})

Here, if the request method is GET, we defined a domain_url, assigned the Stripe secret key to stripe.api_key (so it will be sent automatically when we make a request to create a new Checkout Session), created the Checkout Session, and sent the ID back in the response. Take 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 those views up shortly.

Don't forget the import:

import stripe

Add the URL:

# payments/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
    path('config/', views.stripe_config),
    path('create-checkout-session/', views.create_checkout_session), # new
]

XHR Request

Add the event handler and subsequent XHR request to static/main.js:

// static/main.js

console.log("Sanity check!");

// Get Stripe publishable key
fetch("/config/")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);

  // new
  // Event handler
  document.querySelector("#submitBtn").addEventListener("click", () => {
    // Get Checkout Session ID
    fetch("/create-checkout-session/")
    .then((result) => { return result.json(); })
    .then((data) => {
      console.log(data);
      // Redirect to Stripe Checkout
      return stripe.redirectToCheckout({sessionId: data.sessionId})
    })
    .then((res) => {
      console.log(res);
    });
  });
});

Here, after resolving the result.json() promise, we called redirectToCheckout with the Checkout Session ID from the resolved promise.

Navigate to http://localhost:8000/. On button click you should be redirected to an instance of Stripe Checkout (a Stripe-hosted page to securely collect payment information) with the T-shirt product information:

Stripe Checkout

We can test the form by using one of the several test card numbers that Stripe provides. Let's use 4242 4242 4242 4242. Make sure the expiration date is in the future. Add any 3 numbers for the CVC and any 5 numbers for the postal code. Enter any email address and name. If all goes well, the payment should be processed, but the redirect will fail since we have not set up the /success/ URL yet.

Flow:

Get Publishable Key

  • Send an XHR request from the client to the server requesting the publishable key
  • Respond with the key
  • Use the key to create a new instance of Stripe.js

Create Checkout Session

  • Send another XHR request to the server requesting a new Checkout Session ID
  • Generate a new Checkout Session and send back the ID
  • Redirect to the checkout page for the user to finish their purchase

Redirect the User Appropriately

  • Redirect to a success page after a successful payment
  • Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks

  • Set up the webhook endpoint
  • Test the endpoint using the Stripe CLI
  • Register the endpoint with Stripe

Redirect the User Appropriately

Finally, let's wire up the templates, views, and URLs for handling the success and cancellation redirects.

Success template:

<!-- templates/success.html -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
    <script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <p>Your payment succeeded.</p>
    </div>
  </section>
  </body>
</html>

Cancelled template:

<!-- templates/cancelled.html -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Django + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
    <script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
  </head>
  <body>
  <section class="section">
    <div class="container">
      <p>Your payment was cancelled.</p>
    </div>
  </section>
  </body>
</html>

Views:

# payments/views.py

class SuccessView(TemplateView):
    template_name = 'success.html'


class CancelledView(TemplateView):
    template_name = 'cancelled.html'

URLs:

# payments/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
    path('config/', views.stripe_config),
    path('create-checkout-session/', views.create_checkout_session),
    path('success/', views.SuccessView.as_view()), # new
    path('cancelled/', views.CancelledView.as_view()), # new
]

Ok, refresh the web page at http://localhost:8000/. Click on the payment button and use the credit card number 4242 4242 4242 4242 again along with the rest of the dummy info. Submit the payment. You should be redirected back to http://localhost:8000/success/.

To confirm a charge was actually made, go back to the Stripe dashboard under "Payments":

Stripe Payments

To review, we used the secret key to create a unique Checkout Session ID on the server. This ID was then used to create a Checkout instance, which the end user gets redirected to after clicking the payment button. After the charge occurred, they are then redirected back to the success page.

Flow:

Get Publishable Key

  • Send an XHR request from the client to the server requesting the publishable key
  • Respond with the key
  • Use the key to create a new instance of Stripe.js

Create Checkout Session

  • Send another XHR request to the server requesting a new Checkout Session ID
  • Generate a new Checkout Session and send back the ID
  • Redirect to the checkout page for the user to finish their purchase

Redirect the User Appropriately

  • Redirect to a success page after a successful payment
  • Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks

  • Set up the webhook endpoint
  • Test the endpoint using the Stripe CLI
  • Register the endpoint with Stripe

Confirm Payment with Stripe Webhooks

Our app works well at this point, but we still can't programmatically confirm payments and perhaps run some code if a payment was successful. We already redirect the user to the success page after they check out, but we can't rely on that page alone since payment confirmation happens asynchronously.

There are two types of events in Stripe and programming in general: Synchronous events, which have an immediate effect and results (e.g., creating a customer), and asynchronous events, which don't have an immediate result (e.g., confirming payments). Because payment confirmation is done asynchronously, the user might get redirected to the success page before their payment is confirmed and before we receive their funds.

One of the easiest ways to get notified when the payment goes through is to use a callback or so-called Stripe webhook. We'll need to create a simple endpoint in our application, which Stripe will call whenever an event occurs (i.e., when a user buys a T-shirt). By using webhooks, we can be absolutely sure the payment went through successfully.

In order to use webhooks, we need to:

  1. Set up the webhook endpoint
  2. Test the endpoint using the Stripe CLI
  3. Register the endpoint with Stripe

This section was written by Nik Tomazic.

Endpoint

Create a new view called stripe_webhook which prints a message every time a payment goes through successfully:

# payments/views.py

@csrf_exempt
def stripe_webhook(request):
    stripe.api_key = settings.STRIPE_SECRET_KEY
    endpoint_secret = settings.STRIPE_ENDPOINT_SECRET
    payload = request.body
    sig_header = request.META['HTTP_STRIPE_SIGNATURE']
    event = None

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, endpoint_secret
        )
    except ValueError as e:
        # Invalid payload
        return HttpResponse(status=400)
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return HttpResponse(status=400)

    # Handle the checkout.session.completed event
    if event['type'] == 'checkout.session.completed':
        print("Payment was successful.")
        # TODO: run some custom code here

    return HttpResponse(status=200)

stripe_webhook now serves as our webhook endpoint. Here, we're only looking for checkout.session.completed events which are called whenever a checkout is successful, but you can use the same pattern for other Stripe events.

Make sure to add the HttpResponse import to the top:

from django.http.response import JsonResponse, HttpResponse

The only thing left to do to make the endpoint accessible is to register it in urls.py:

# payments/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.HomePageView.as_view(), name='home'),
    path('config/', views.stripe_config),
    path('create-checkout-session/', views.create_checkout_session),
    path('success/', views.SuccessView.as_view()),
    path('cancelled/', views.CancelledView.as_view()),
    path('webhook/', views.stripe_webhook), # new
]

Testing the webhook

We'll use the Stripe CLI to test the webhook.

Once downloaded and installed, run the following command in a new terminal window to log in to your Stripe account:

$ stripe login

This command should generate a pairing code:

Your pairing code is: peach-loves-classy-cozy
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser (^C to quit)

By pressing Enter, the CLI will open your default web browser and ask for permission to access your account information. Go ahead and allow access. Back in your terminal, you should see something similar to:

> Done! The Stripe CLI is configured for Django Test with account id acct_<ACCOUNT_ID>

Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.

Next, we can start listening to Stripe events and forward them to our endpoint using the following command:

$ stripe listen --forward-to localhost:8000/webhook/

This will also generate a webhook signing secret:

> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)

In order to initialize the endpoint, add the secret to the settings.py file:

# djangostripe/settings.py

STRIPE_ENDPOINT_SECRET = '<your webhook signing secret here>'

Stripe will now forward events to our endpoint. To test, run another test payment through with 4242 4242 4242 4242. In your terminal, you should see the Payment was successful. message.

Once done, stop the stripe listen --forward-to localhost:8000/webhook/ process.

If you'd like to identify the user making the purchase, you can use the client_reference_id to attach some sort of user identifier to the Stripe session.

For example:

# payments/views.py @csrf_exempt def create_checkout_session(request):    if request.method == 'GET':        domain_url = 'http://localhost:8000/'        stripe.api_key = settings.STRIPE_SECRET_KEY        try:            checkout_session = stripe.checkout.Session.create(                # new                client_reference_id=request.user.id if request.user.is_authenticated else None,                success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',                cancel_url=domain_url + 'cancelled/',                payment_method_types=['card'],                mode='payment',                line_items=[                    {                        'name': 'T-shirt',                        'quantity': 1,                        'currency': 'usd',                        'amount': '2000',                    }                ]            )            return JsonResponse({'sessionId': checkout_session['id']})        except Exception as e:            return JsonResponse({'error': str(e)})

Register the endpoint

Finally, after deploying your app, you can register the endpoint in the Stripe dashboard, under Developers > Webhooks. This will generate a webhook signing secret for use in your production app.

For example:

Django webhook

Flow:

Get Publishable Key

  • Send an XHR request from the client to the server requesting the publishable key
  • Respond with the key
  • Use the key to create a new instance of Stripe.js

Create Checkout Session

  • Send another XHR request to the server requesting a new Checkout Session ID
  • Generate a new Checkout Session and send back the ID
  • Redirect to the checkout page for the user to finish their purchase

Redirect the User Appropriately

  • Redirect to a success page after a successful payment
  • Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks

  • Set up the webhook endpoint
  • Test the endpoint using the Stripe CLI
  • Register the endpoint with Stripe

What's Next

On a live website it's required to have HTTPS so your connection is secure. Also while we hardcoded our API keys and webhook signing secret for simplicity, these should really be stored in environment variables. You'll probably want to store the domain_url as an environment variable as well.

Original article source at: https://testdriven.io/

#stripe #django #python 

Quickly add Stripe to accept payments on a Django/Python website
Bongani  Ngema

Bongani Ngema

1668694631

Quickly Add Stripe to A Flask App in Order To Accept Payments

This tutorial shows how to add Stripe to a Flask application for accepting one-time payments.

Need to handle subscription payments? Check out Flask Stripe Subscriptions.

Stripe Payment Strategies

Stripe currently has three strategies for accepting one-time payments:

  1. Charges API (legacy)
  2. Stripe Checkout (the focus of this tutorial)
  3. Payment Intents API (often coupled with Stripe Elements)

Which strategy should you use?

  1. Use Stripe Checkout if you want to get up and running fast. If you've used the old modal version of Checkout and are looking for a similar approach, then this is the way to go. It provides a number of powerful features out-of-the-box, supports multiple languages, and can even be used for recurring payments. Most importantly, Checkout manages the entire payment process for you, so you can begin accepting payments without even having to add a single form!
  2. Use the Payment Intents API (along with Elements) if you want to customize the payment experience for your end users.

What about the Charges API?

  1. While you still can use the Charges API, if you're new to Stripe do not use it since it does not support the latest banking regulations (like SCA). You will see a high rate of declines if used. For more, review the Charges vs. Payment Intents APIs page from the official Stripe docs.
  2. Still using the Charges API? If most of your customers are based in the US or Canada you don't need to migrate just yet. Review the Checkout migration guide guide for more info.

Initial Setup

The first step is to set up a basic Python environment and install Flask.

Create a new project folder, create and activate a virtual environment, and install Flask:

$ mkdir flask-stripe-checkout && cd flask-stripe-checkout
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install flask

Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.

Next, create a file called app.py, and add the code for a basic "Hello World" app:

# app.py

from flask import Flask, jsonify

app = Flask(__name__)


@app.route("/hello")
def hello_world():
    return jsonify("hello, world!")


if __name__ == "__main__":
    app.run()

Fire up the server:

(env)$ FLASK_ENV=development python app.py

You should see "hello, world!" in your browser at http://127.0.0.1:5000/hello.

Add Stripe

Time for Stripe. Start by installing it:

(env)$ pip install stripe

Next, register for a new Stripe account (if you don't already have one) and navigate to the dashboard. Click on "Developers":

Stripe Developers

Then in the left sidebar click on "API keys":

Stripe Developers Key

Each Stripe account has four API keys: two keys for testing and two for production. Each pair has a "secret key" and a "publishable key". Do not reveal the secret key to anyone; the publishable key will be embedded in the JavaScript on the page that anyone can see.

Currently the toggle for "Viewing test data" in the upper right indicates that we're using the test keys now. That's what we want.

Store your test API keys as environment variables like so:

(env)$ export STRIPE_PUBLISHABLE_KEY=<YOUR_STRIPE_PUBLISHABLE_KEY>
(env)$ export STRIPE_SECRET_KEY=<YOUR_STRIPE_SECRET_KEY>

Next, add the Stripe keys to your app:

# app.py

import os

import stripe
from flask import Flask, jsonify

app = Flask(__name__)

stripe_keys = {
    "secret_key": os.environ["STRIPE_SECRET_KEY"],
    "publishable_key": os.environ["STRIPE_PUBLISHABLE_KEY"],
}

stripe.api_key = stripe_keys["secret_key"]


@app.route("/hello")
def hello_world():
    return jsonify("hello, world!")


if __name__ == "__main__":
    app.run()

Finally, you'll need to specify an "Account name" within your "Account settings" at https://dashboard.stripe.com/settings/account:

Stripe Account Name

Create a Product

Next, we need to create a product to sell.

Click "Products" in the top navigation bar and then "Add product":

Stripe Add Product

Add a product name, enter a price, and select "One time":

Stripe Add Product

Click "Save product".

With the API keys in place and a product setup, we can now start adding Stripe Checkout to process payments.

Workflow

After the user clicks the purchase button we need to do the following:

Get Publishable Key

  • Send an AJAX request from the client to the server requesting the publishable key
  • Respond with the key
  • Use the key to create a new instance of Stripe.js

Create Checkout Session

  • Send another AJAX request to the server requesting a new Checkout Session ID
  • Generate a new Checkout Session and send back the ID
  • Redirect to the checkout page for the user to finish their purchase

Redirect the User Appropriately

  • Redirect to a success page after a successful payment
  • Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks

  • Set up the webhook endpoint
  • Test the endpoint using the Stripe CLI
  • Register the endpoint with Stripe

Get Publishable Key

JavaScript Static File

Let's start by creating a new static file to hold all of our JavaScript.

Add a new folder called "static", and then add a new file to that folder called main.js:

// static/main.js

console.log("Sanity check!");

Next, add a new route to app.py that serves up an index.html template:

# app.py

@app.route("/")
def index():
    return render_template("index.html")

Make sure to import render_template as well:

from flask import Flask, jsonify, render_template

For the template, first add a new folder called "templates", and then add a base template called base.html, which includes the script tag for serving up the main.js static file:

<!-- templates/base.html -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flask + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
    <script src="{{ url_for('static', filename='main.js') }}"></script>
    <script defer src="https://use.fontawesome.com/releases/v5.14.0/js/all.js"></script>
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>

Next, add a payment button to a new template called index.html:

<!-- templates/index.html -->

{% extends "base.html" %}

{% block content %}
  <section class="section">
    <div class="container">
      <button class="button is-primary" id="submitBtn">Purchase!</button>
    </div>
  </section>
{% endblock %}

Run the development server again:

(env)$ FLASK_ENV=development python app.py

Navigate to http://127.0.0.1:5000, and open up the JavaScript console. You should see the sanity check:

JavaScript Sanity Check

Route

Next, add a new route to app.py to handle the AJAX request:

# app.py

@app.route("/config")
def get_publishable_key():
    stripe_config = {"publicKey": stripe_keys["publishable_key"]}
    return jsonify(stripe_config)

AJAX Request

Next, use the Fetch API to make an AJAX request to the new /config endpoint in static/main.js:

// static/main.js

console.log("Sanity check!");

// new
// Get Stripe publishable key
fetch("/config")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);
});

The response from a fetch request is a ReadableStream. result.json() returns a promise, which we resolved to a JavaScript object -- i.e., data. We then used dot-notation to access the publicKey in order to obtain the publishable key.

Include Stripe.js in templates/base.html like so:

<!-- templates/base.html -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Flask + Stripe Checkout</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
    <script src="https://js.stripe.com/v3/"></script>  <!-- new -->
    <script src="{{ url_for('static', filename='main.js') }}"></script>
    <script defer src="https://use.fontawesome.com/releases/v5.14.0/js/all.js"></script>
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>

Now, after the page load, a call will be made to /config, which will respond with the Stripe publishable key. We'll then use this key to create a new instance of Stripe.js.

Workflow:

Get Publishable Key

  • Send an AJAX request from the client to the server requesting the publishable key
  • Respond with the key
  • Use the key to create a new instance of Stripe.js

Create Checkout Session

  • Send another AJAX request to the server requesting a new Checkout Session ID
  • Generate a new Checkout Session and send back the ID
  • Redirect to the checkout page for the user to finish their purchase

Redirect the User Appropriately

  • Redirect to a success page after a successful payment
  • Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks

  • Set up the webhook endpoint
  • Test the endpoint using the Stripe CLI
  • Register the endpoint with Stripe

Create Checkout Session

Moving on, we need to attach an event handler to the button's click event which will send another AJAX request to the server to generate a new Checkout Session ID.

Route

First, add the new route:

# app.py

@app.route("/create-checkout-session")
def create_checkout_session():
    domain_url = "http://127.0.0.1:5000/"
    stripe.api_key = stripe_keys["secret_key"]

    try:
        # Create new Checkout Session for the order
        # Other optional params include:
        # [billing_address_collection] - to display billing address details on the page
        # [customer] - if you have an existing Stripe Customer ID
        # [payment_intent_data] - capture the payment later
        # [customer_email] - prefill the email input in the form
        # For full details see https://stripe.com/docs/api/checkout/sessions/create

        # ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
        checkout_session = stripe.checkout.Session.create(
            success_url=domain_url + "success?session_id={CHECKOUT_SESSION_ID}",
            cancel_url=domain_url + "cancelled",
            payment_method_types=["card"],
            mode="payment",
            line_items=[
                {
                    "name": "T-shirt",
                    "quantity": 1,
                    "currency": "usd",
                    "amount": "2000",
                }
            ]
        )
        return jsonify({"sessionId": checkout_session["id"]})
    except Exception as e:
        return jsonify(error=str(e)), 403

Here, we-

  1. Defined a domain_url (for the redirects)
  2. Assigned the Stripe secret key to stripe.api_key (so it will be sent automatically when we make a request to create a new Checkout Session)
  3. Created the Checkout Session
  4. Sent the ID back in the response

Take note of the success_url and cancel_url, which use the domain_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.

AJAX Request

Add the event handler and subsequent AJAX request to static/main.js:

// static/main.js

console.log("Sanity check!");

// Get Stripe publishable key
fetch("/config")
.then((result) => { return result.json(); })
.then((data) => {
  // Initialize Stripe.js
  const stripe = Stripe(data.publicKey);

  // new
  // Event handler
  document.querySelector("#submitBtn").addEventListener("click", () => {
    // Get Checkout Session ID
    fetch("/create-checkout-session")
    .then((result) => { return result.json(); })
    .then((data) => {
      console.log(data);
      // Redirect to Stripe Checkout
      return 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.

Navigate to http://127.0.0.1:5000. On button click, you should be redirected to an instance of Stripe Checkout (a Stripe-hosted page to securely collect payment information) with the T-shirt product information:

Stripe Checkout

We can test the form by using one of the several test card numbers that Stripe provides. Let's use 4242 4242 4242 4242.

  • Email: a valid email
  • Card number: 4242 4242 4242 4242
  • Expiration: any date in the future
  • CVC: any three numbers
  • Name: anything
  • Postal code: any five numbers

If all goes well, the payment should be processed, but the redirect will fail since we have not set up the /success URL yet.

Workflow:

Get Publishable Key

  • Send an AJAX request from the client to the server requesting the publishable key
  • Respond with the key
  • Use the key to create a new instance of Stripe.js

Create Checkout Session

  • Send another AJAX request to the server requesting a new Checkout Session ID
  • Generate a new Checkout Session and send back the ID
  • Redirect to the checkout page for the user to finish their purchase

Redirect the User Appropriately

  • Redirect to a success page after a successful payment
  • Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks

  • Set up the webhook endpoint
  • Test the endpoint using the Stripe CLI
  • Register the endpoint with Stripe

Redirect the User Appropriately

Finally, let's wire up the templates and routes for handling the success and cancellation redirects.

Success template:

<!-- templates/success.html -->

{% extends "base.html" %}

{% block content %}
  <section class="section">
    <div class="container">
      <p>Your payment succeeded.</p>
    </div>
  </section>
{% endblock %}

Cancelled template:

<!-- templates/cancelled.html -->

{% extends "base.html" %}

{% block content %}
  <section class="section">
    <div class="container">
      <p>Your payment was cancelled.</p>
    </div>
  </section>
{% endblock %}

Routes:

# app.py

@app.route("/success")
def success():
    return render_template("success.html")


@app.route("/cancelled")
def cancelled():
    return render_template("cancelled.html")

Back at http://127.0.0.1:5000, click on the payment button and use the credit card number 4242 4242 4242 4242 again along with the rest of the dummy credit card info. Submit the payment. You should be redirected back to http://127.0.0.1:5000/success.

To confirm a charge was actually made, click "Payments" back on the Stripe dashboard:

Stripe Payments

To review, we used the secret key to create a unique Checkout Session ID on the server. This ID was then used to create a Checkout instance, which the end user gets redirected to after clicking the payment button. After the charge occurred, they are then redirected back to the success page.

Be sure to test out a cancelled payment as well.

Workflow:

Get Publishable Key

  • Send an AJAX request from the client to the server requesting the publishable key
  • Respond with the key
  • Use the key to create a new instance of Stripe.js

Create Checkout Session

  • Send another AJAX request to the server requesting a new Checkout Session ID
  • Generate a new Checkout Session and send back the ID
  • Redirect to the checkout page for the user to finish their purchase

Redirect the User Appropriately

  • Redirect to a success page after a successful payment
  • Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks

  • Set up the webhook endpoint
  • Test the endpoint using the Stripe CLI
  • Register the endpoint with Stripe

Confirm Payment with Stripe Webhooks

Our app works well at this point, but we still can't programmatically confirm payments and perhaps run some code if a payment was successful. We already redirect the user to the success page after they check out, but we can't rely on that page alone since payment confirmation happens asynchronously.

There are two types of events in Stripe and programming in general: Synchronous events, which have an immediate effect and results (e.g., creating a customer), and asynchronous events, which don't have an immediate result (e.g., confirming payments). Because payment confirmation is done asynchronously, the user might get redirected to the success page before their payment is confirmed and before we receive their funds.

One of the easiest ways to get notified when the payment goes through is to use a callback or so-called Stripe webhook. We'll need to create a simple endpoint in our application, which Stripe will call whenever an event occurs (e.g., when a user buys a T-shirt). By using webhooks, we can be absolutely sure the payment went through successfully.

In order to use webhooks, we need to:

  1. Set up the webhook endpoint
  2. Test the endpoint using the Stripe CLI
  3. Register the endpoint with Stripe

This section was written by Nik Tomazic.

Endpoint

Create a new route called stripe_webhook which prints a message every time a payment goes through successfully:

# app.py

@app.route("/webhook", methods=["POST"])
def stripe_webhook():
    payload = request.get_data(as_text=True)
    sig_header = request.headers.get("Stripe-Signature")

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, stripe_keys["endpoint_secret"]
        )

    except ValueError as e:
        # Invalid payload
        return "Invalid payload", 400
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return "Invalid signature", 400

    # Handle the checkout.session.completed event
    if event["type"] == "checkout.session.completed":
        print("Payment was successful.")
        # TODO: run some custom code here

    return "Success", 200

stripe_webhook now serves as our webhook endpoint. Here, we're only looking for checkout.session.completed events which are called whenever a checkout is successful, but you can use the same pattern for other Stripe events.

Make sure to add the request import to the top:

from flask import Flask, jsonify, render_template, request

Testing the webhook

We'll use the Stripe CLI to test the webhook.

Once downloaded and installed, run the following command in a new terminal window to log in to your Stripe account:

$ stripe login

This command should generate a pairing code:

Your pairing code is: peach-loves-classy-cozy
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser (^C to quit)

By pressing Enter, the CLI will open your default web browser and ask for permission to access your account information. Go ahead and allow access. Back in your terminal, you should see something similar to:

> Done! The Stripe CLI is configured for Flask Stripe Test with account id acct_<ACCOUNT_ID>

Please note: this key will expire after 90 days, at which point you'll need to re-authenticate.

Next, we can start listening to Stripe events and forward them to our endpoint using the following command:

$ stripe listen --forward-to 127.0.0.1:5000/webhook

This will also generate a webhook signing secret:

> Ready! Your webhook signing secret is whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (^C to quit)

In order to initialize the endpoint, save the secret as another environment variable like so:

(env)$ export STRIPE_ENDPOINT_SECRET=<YOUR_STRIPE_ENDPOINT_SECRET>

Next, add it in the stripe_keys dictionary like so:

# app.py

stripe_keys = {
    "secret_key": os.environ["STRIPE_SECRET_KEY"],
    "publishable_key": os.environ["STRIPE_PUBLISHABLE_KEY"],
    "endpoint_secret": os.environ["STRIPE_ENDPOINT_SECRET"], # new
}

Stripe will now forward events to our endpoint. To test, run another test payment through with 4242 4242 4242 4242. In your terminal, you should see the Payment was successful. message.

Once done, stop the stripe listen --forward-to 127.0.0.1:5000/webhook process.

If you'd like to identify the user making the purchase, you can use the client_reference_id to attach some sort of user identifier to the Stripe session.

For example:

@app.route("/create-checkout-session") def create_checkout_session():    domain_url = "http://127.0.0.1:5000/"    stripe.api_key = stripe_keys["secret_key"]    try:        checkout_session = stripe.checkout.Session.create(            # new            client_reference_id=current_user.id if current_user.is_authenticated else None,            success_url=domain_url + "success?session_id={CHECKOUT_SESSION_ID}",            cancel_url=domain_url + "cancelled",            payment_method_types=["card"],            mode="payment",            line_items=[                {                    "name": "T-shirt",                    "quantity": 1,                    "currency": "usd",                    "amount": "2000",                }            ]        )        return jsonify({"sessionId": checkout_session["id"]})    except Exception as e:        return jsonify(error=str(e)), 403

Register the endpoint

Finally, after deploying your app, you can register the endpoint in the Stripe dashboard, under Developers > Webhooks. This will generate a webhook signing secret for use in your production app.

For example:

Flask webhook

Workflow:

Get Publishable Key

  • Send an AJAX request from the client to the server requesting the publishable key
  • Respond with the key
  • Use the key to create a new instance of Stripe.js

Create Checkout Session

  • Send another AJAX request to the server requesting a new Checkout Session ID
  • Generate a new Checkout Session and send back the ID
  • Redirect to the checkout page for the user to finish their purchase

Redirect the User Appropriately

  • Redirect to a success page after a successful payment
  • Redirect to a cancellation page after a cancelled payment

Confirm Payment with Stripe Webhooks

  • Set up the webhook endpoint
  • Test the endpoint using the Stripe CLI
  • Register the endpoint with Stripe

Next Steps

In production, you'll need to have HTTPS so your connection is secure. You'll probably want to store the domain_url as an environment variable as well. Finally, it's a good idea to confirm that the correct product and price are being used in the /create-checkout-session route before creating a Checkout Session. To do this, you can:

  1. Add each of your products to a database.
  2. Then, when you dynamically create the product page, store the product database ID and price in data attributes within the purchase button.
  3. Update the /create-checkout-session route to only allow POST requests.
  4. Update the JavaScript event listener to grab the product info from the data attributes and send them along with the AJAX POST request to the /create-checkout-session route.
  5. Parse the JSON payload in the route handler and confirm that the product exists and that the price is correct before creating a Checkout Session.

Cheers!

--

Grab the code from the flask-stripe-checkout repo on GitHub.

Original article source at: https://testdriven.io/

#flask #stripe 

Quickly Add Stripe to A Flask App in Order To Accept Payments
Louis Jones

Louis Jones

1668659900

Build an E-Commerce Website with React, TailwindCSS, PlanetScale, and Stripe

In this tutorial, you'll learn how to build a production-ready eCommerce website using ReactJS, TailwindCSS, PlanetScale, and Stripe.

Before we begin, you should be familiar with the basics of React.js and Next.js to get the most out of this guide.

If you're not and need to brush up, I recommend you go through the ReactJS and NextJS documentation.

The stack we will use:

  1. ReactJS is a JavaScript library for building user interfaces. It is declarative and component-based.
  2. NextJS is a React-based framework that lets us render data on the server side. It helps Google crawl the application which results in SEO benefits.
  3. PlanetScale is a database as a service that is developed on Vitess, an open-source technology that powers YouTube and uses MySQL internally.
  4. TailwindCSS is a utility-first CSS framework packed with classes that can be composed to build any design, directly in our markup.
  5. Prisma is an ORM built for NodeJS and TypeScript which handles automated migrations, type-safety, and auto-completion.
  6. Vercel will host our application. It scales well, all without any configuration, and deployment is instant.
  7. Stripe is a payment gateway, and we will use it to accept online payments on the website.

Table of Contents

  1. How to Configure PlanetScale, Stripe, NextJS, Prisma and Other Libraries
  2. How to Implement Mock Data, Category-Products API and All Category-Single Category UI
  3. How to Implement Single Product UI and Stripe Checkout
  4. How to Deploy the Website to Production

I am going to divide this tutorial into four separate sections.

At the start of every section, you will find a Git commit that has the code developed in that section. Also, if you want to see the complete code, then it is available in this repository.


How to Configure PlanetScale, Stripe, NextJS, TailwindCSS, and Prisma.

In this section, we'll implement the following functionality:

  1. Create a PlanetScale Account and Database.
  2. Create a Stripe Account.
  3. Configure NextJS, TailwindCSS, and Prisma.

You can find the eCommerce website code implemented in this section at this commit.

How to Configure PlanetScale:

To create a PlanetScale account, visit this URL. Click on Get started button at the top right corner.

Screenshot-2022-10-09-at-2.01.59-PM

PlanetScale Landing Page

You can either create an account using GitHub or a traditional email-password. Once the account is created, then click on the "create" link.

Screenshot-2022-10-05-at-5.00.59-PM

PlanetScale Dashboard Page

You'll receive the following modal:

Screenshot-2022-10-05-at-5.08.12-PM

PlanetScale New Database Modal

Fill in the details and click on the Create database button. Once the database is created you'll be redirected to the following page:

Screenshot-2022-10-09-at-2.06.05-PM

PlanetScale Ecommerce Website Database Page

Click on connect and a modal will open. This modal will contain a Database URL and this password cannot be generated again. So copy and paste it into a safe location.

Screenshot-2022-10-09-at-2.07.27-PM

PlanetScale Database Username and Password Modal

How to Configure Stripe:

To create a Stripe account, go to this URL. Once you've created the account, click on the Developer Button from the Nav menu. You'll see API keys on the left side and you'll find the Publishable key and Secret key under Standard keys.

Publishable key: These are the keys that can be publicly-accessible in a web or mobile app’s client-side code.

Secret key: This is a secret credential and should be securely stored in the server code. This key is used to call the Stripe API.

How to Configure NextJS, TailwindCSS, and Prisma.

First, we will create a NextJS app using the following command:

npx create-next-app ecommerce-tut --ts --use-npm

Once the project is created, open it with your favourite editor. You'll get the following structure:

Screenshot-2022-10-05-at-6.03.07-PM

Project Structure

Let's create a directory named src. We will move the pages and styles directory to that src folder. You'll get the following structure:

Screenshot-2022-10-09-at-2.28.16-PM

Project Structure after moving Pages and Styles.

Install the following packages:

npm i @ngneat/falso @prisma/client @stripe/stripe-js @tanstack/react-query currency.js next-connect react-icons react-intersection-observer stripe

We also need to install dev dependencies:

npm i --save-dev @tanstack/react-query-devtools autoprefixer postcss tailwindcss

Let's understand each of the packages:

  1. @ngneat/falso: We will use this library to create mock data for our eCommerce website. In an ideal world, you would have an admin panel to add the products, but it is not in the scope of this tutorial.
  2. @prisma/client: We will use this library to connect to our database, run migrations, and do all CRUD operations on the database.
  3. @stripe/stripe-js: We will use this library to redirect users to the stripe checkout page and process payment.
  4. @tanstack/react-query: We will use this library for managing our asynchronous state, that is caching API responses.
  5. currency.js: We will use this library for converting our prices to two decimal format.
  6. next-connect: We will use this library for routing purposes on our Next API layer.
  7. react-icons: We will use this library for adding icons to our buttons and links.
  8. react-intersection-observer: Have you seen infinite scrolling on a lot of websites and wondered how it is implemented? We will use this library to implement that based on the viewport.
  9. stripe: We will use the Stripe library to connect with Stripe API from our Next API layer.
  10. @tanstack/react-query-devtools: We will use this library as the only dev dependency to view and manage our cache during development time.
  11. TailwindCSS: We will use this as our CSS library that also requires PostCSS and AutoPrefixer.

Let's configure TailwindCSS into our project using the following command:

npx tailwindcss init -p

You'll get the following response:

Screenshot-2022-10-09-at-2.29.52-PM

TailwindCSS Config Success

Now go to tailwind.config.js  and update it with the following code:

/** @type {import('tailwindcss').Config} */
module.exports = {
    content: [
        "./src/pages/**/*.{js,ts,jsx,tsx}",
        "./src/components/**/*.{js,ts,jsx,tsx}",
    ],
    theme: {
        extend: {},
    },
    plugins: [],
};

tailwind.config.js

To generate the CSS, Tailwind needs access to all the HTML Elements. We will be writing the UI components under pages and components only, so we pass it under content.

If you need to use any plugins, for example, typography, then you need to add them under the plugins array. If you need to extend the default theme provide by Tailwind, then you need to add it under theme.extend section.

Now go to /src/styles/globals.css and replace the existing code with the following:

@tailwind base;
@tailwind components;
@tailwind utilities;

src/styles/globals.css

We will add these three directives in our globals.css file. The meaning of each directive is as follows:

  1. @tailwind base: This injects a base style provided by Tailwind.
  2. @tailwind components: This injects classes and any other classes added by the plugin.
  3. @tailwind utilities: This injects hover, focus, responsive, dark mode and any other utility added by the plugin.

Remove the Home.module.css from src/styles directory and go to src/pages/index.ts and replace the existing code with the following:

import type { NextPage } from "next";
import Head from "next/head";

const Home: NextPage = () => {
    return (
        <div>
            <Head>
                <title>All Products</title>
                <meta name="description" content="All Products" />
                <link rel="icon" href="/favicon.ico" />
            </Head>

            <main className="container mx-auto">
                <h1 className="h-1">Hello</h1>
            </main>
        </div>
    );
};

export default Home;

src/pages/index.ts

When we run the create-next-app command to create the project, it adds some boilerplate code. Here we removed that in some instances while replacing index.ts with an h1 and text that says "Hello".

It's time to run the website using the following command:

npm run dev

You'll get the following response:

Screenshot-2022-10-09-at-2.36.15-PM

Open http://localhost:3000 on your browser, and you'll get the following screen with a hello message:

Screenshot-2022-10-09-at-2.38.49-PM

Screen with Hello Message

Let's configure Prisma into our project using the following command:

npx prisma init

You'll get the following response:

Screenshot-2022-10-09-at-2.41.15-PM

Prisma Successfully Configured Message

Under prisma/schema.prisma replace the existing code with the following code:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["referentialIntegrity"]
}

datasource db {
  provider             = "mysql"
  url                  = env("DATABASE_URL")
  referentialIntegrity = "prisma"
}

model Category {
  id        String    @id @default(cuid())
  name      String    @unique
  createdAt DateTime  @default(now())
  products  Product[]
}

model Product {
  id          String    @id @default(cuid())
  title       String    @unique
  description String
  price       String
  quantity    Int
  image       String
  createdAt   DateTime  @default(now())
  category    Category? @relation(fields: [categoryId], references: [id])
  categoryId  String?
}

prisma/schema.prisma

This file consists of our database source that is MySQL. We are using MySQL because PlanetScale supports MySQL only.

Also, we have created two models that are:

Category:

  1. name: Every category will have a unique title.
  2. createdAt: The date when a category is added.
  3. products:  A foreign relationship with the product model.

Product:

  1. title: Every product will have a unique title.
  2. description: This is just information about the product.
  3. price: It is of String type because it will hold a decimal value.
  4. quantity: It is of Int type because it will hold a numerical value.
  5. image: Representation of what the product will look like. We will use placeimg for the purpose of this tutorial.
  6. createdAt: The date when a product is added.
  7. category: A foreign relationship with the category model.

We are using cuid() instead of uuid() for the id because they are better for horizontal scaling and sequential lookup performance. Prisma has inbuilt support for CUID. You can read more about it here.

Now time to update our .env file with the following:

DATABASE_URL=

STRIPE_SECRET_KEY=

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=

.env

You'll find the Stripe secret key and publishable key under the dashboard. The database URL is the one that we had copy-pasted earlier and kept in a safe location. Update this .env with those credentials.

Also, note that the .gitignore file created by NextJS doesn't ignore the .env file. It is configured to ignore the .env.local file. But Prisma requires .env, so we will replace the .gitignore file content with the following:

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

.gitignore

Ideally, Prisma manages schema migration using the prisma migrate command. But as PlanetScale has its schema migration mechanism inbuilt, we will use that. Use the following command to push migration to our current main branch.

Note, our main branch is not yet promoted as a production branch.

npx prisma db push

Now let's generate the Prisma client using the following command:

npx prisma generate

Go to the PlanetScale Dashboard, and there you'll find two tables created:

Screenshot-2022-10-09-at-2.59.50-PM

PlanetScale Two Tables Created

Click on these tables, and you'll be redirected to the following page:

Screenshot-2022-10-09-at-3.00.39-PM

PlanetScale Database Schema

How to Implement Mock Data, Category-Products API, and All Category-Single Category UI.

In this section, we'll implement the following functionality:

  1. Create mock data
  2. Create a Category and Product API.
  3. Create an All Category and Single Category UI.

You can find the eCommerce website code implemented in this section at this commit.

How to Create the Mock Data:

Under the prisma directory, create a file named seed.ts and copy-paste the following code:

import {
    randBetweenDate,
    randNumber,
    randProduct,
    randProductAdjective,
} from "@ngneat/falso";
import { PrismaClient } from "@prisma/client";

const primsa = new PrismaClient();

const main = async () => {
    try {
        await primsa.category.deleteMany();
        await primsa.product.deleteMany();
        const fakeProducts = randProduct({
            length: 1000,
        });
        for (let index = 0; index < fakeProducts.length; index++) {
            const product = fakeProducts[index];
            const productAdjective = randProductAdjective();
            await primsa.product.upsert({
                where: {
                    title: `${productAdjective} ${product.title}`,
                },
                create: {
                    title: `${productAdjective} ${product.title}`,
                    description: product.description,
                    price: product.price,
                    image: `${product.image}/tech`,
                    quantity: randNumber({ min: 10, max: 100 }),
                    category: {
                        connectOrCreate: {
                            where: {
                                name: product.category,
                            },
                            create: {
                                name: product.category,
                                createdAt: randBetweenDate({
                                    from: new Date("10/06/2020"),
                                    to: new Date(),
                                }),
                            },
                        },
                    },
                    createdAt: randBetweenDate({
                        from: new Date("10/07/2020"),
                        to: new Date(),
                    }),
                },
                update: {},
            });
        }
    } catch (error) {
        throw error;
    }
};

main().catch((err) => {
    console.warn("Error While generating Seed: \n", err);
});

prisma/seed.ts

Here we are creating 1000 fake products and adding them to the database.

We are following these steps to add the products:

  1. Delete all the categories using the deleteMany() function.
  2. Delete all the product using the deleteMany() function.
  3. The above are optional steps, but it's always a good idea to rerun the seed script with a clean table.
  4. As the title attribute from the product table has unique property associated with it we bind it with the randProductAdjective function output to make repetitions less likely.
  5. But still, there is a probability that the title property created by the falso gets repeated. So we use the upsert method from @prisma/client.
  6. We are also creating/associating the category when we create a product.

Now go to package.json and update the following code below scripts:

"prisma": {
    "seed": "ts-node --compiler-options {\"module\":\"CommonJS\" prisma/seed.ts"
},

package.json

We will use the ts-node package to run our seed script command. The seed script is written in TypeScript while ts-node converts TypeScript code to JavaScript.

Use the following command to install the package:

npm i --save-dev ts-node

As the ts-node will convert the code to JavaScript, we can execute the following command to seed the tables with mock data:

npx prisma db seed

You'll get the following output that will show it has started running. It will take some time to seed the tables with mock data.

Screenshot-2022-10-09-at-3.18.56-PM

Once the seed command is successful, you'll get the following response:

Screenshot-2022-10-09-at-3.20.09-PM

The benefit of Prisma is that it also has a studio, which can be used to view the database in a local development environment. Use the following command to run this studio:

npx prisma studio

Open http://localhost:5555, on your browser, and you'll get the following screen with all the tables:

Screenshot-2022-10-09-at-3.22.23-PM

Prisma Studio

The number of Products and Categories may vary on your side or be similar, as this is random data.

How to Create the Category and Product APIs:

Under the src/pages/api category you'll find a file named hello.ts. Remove this file and create two directories named categories and products.

Inside those categories, create a file named index.ts and copy-paste the following code:

import type { NextApiRequest, NextApiResponse } from "next";
import nc from "next-connect";
import { prisma } from "../../../lib/prisma";
import { TApiAllCategoriesResp, TApiErrorResp } from "../../../types";

const getCategories = async (
    _req: NextApiRequest,
    res: NextApiResponse<TApiAllCategoriesResp | TApiErrorResp>
) => {
    try {
        const categories = await prisma.category.findMany({
            select: {
                id: true,
                name: true,
                products: {
                    orderBy: {
                        createdAt: "desc",
                    },
                    take: 8,
                    select: {
                        title: true,
                        description: true,
                        image: true,
                        price: true,
                    },
                },
            },
            orderBy: {
                createdAt: "desc",
            },
        });
        return res.status(200).json({ categories });
    } catch (error) {
        return res.status(500).json({
            message: "Something went wrong!! Please try again after sometime",
        });
    }
};

const handler = nc({ attachParams: true }).get(getCategories);

export default handler;

src/pages/api/index.ts

In the above snippet, we are doing the following:

  1. When we create a file under the pages>api directory, NextJS treats it as a Serverless API. So by creating a file named categories/index.ts we are informing Next that it needs to convert this to the /api/categories API.
  2. Using the next-connect library we are making sure that only the get operation is allowed for the getCategories function.
  3. Under this function, we are querying the database with an order as desc for the createdAt property and we only take the latest eight product rows for each category row. We also select a specific property from the product and category that are required by the front end.

We don't query all the products for each category in this API, because it will slow down our response time.

You'll find that we have imported prisma and types files. Let's create two directories under src named lib and types.

Under the lib directory, create a file named prisma.ts, and under the types directory create a file named index.ts.

Let's create our global Prisma constant under prisma.ts. Copy-paste the following code:

import { PrismaClient } from "@prisma/client";

declare global {
    var prisma: PrismaClient;
}

export const prisma =
    global.prisma ||
    new PrismaClient({
        log: [],
    });

if (process.env.NODE_ENV !== "production") global.prisma = prisma;

src/lib/prisma.ts

Here we are creating a global prisma variable which we can use across the project.

Let's add the types that we will use application-wide under src/types/index.ts.

Copy-paste the following code:

export type TApiAllCategoriesResp = {
    categories: {
        id: string;
        name: string;
        products: {
            title: string;
            description: string;
            image: string;
            price: string;
        }[];
    }[];
};

export type TApiSingleCategoryWithProductResp = {
    category: {
        id: string;
        name: string;
        products: {
            id: string;
            title: string;
            description: string;
            image: string;
            price: string;
            quantity: number;
        }[];
        hasMore: boolean;
    };
};

export type TApiSingleProductResp = {
    product: {
        title: string;
        description: string;
        price: string;
        quantity: number;
        image: string;
    };
};

export type TApiErrorResp = {
    message: string;
};

src/types/index.ts

Here we are creating four types which will be used across the project.

I'll be using Postman to test this API. Postman is a utility for developing APIs. You can call the APIs, and Postman will show the response based on how you structure it.

Just update the URL in Postman to:

http://localhost:3000/api/categories

And you'll get the following response:

Screenshot-2022-10-09-at-3.58.53-PM

All Categories Resp

Now let's create an API to get a single category's information with its products.

Under the src/pages/api/categories directory create a file named [id].ts and copy-paste the following code:

import type { NextApiRequest, NextApiResponse } from "next";
import nc from "next-connect";
import { prisma } from "../../../lib/prisma";
import {
    TApiErrorResp,
    TApiSingleCategoryWithProductResp
} from "../../../types";

const getSingleCategory = async (
    req: NextApiRequest,
    res: NextApiResponse<TApiSingleCategoryWithProductResp | TApiErrorResp>
) => {
    try {
        const id = req.query.id as string;
        const cursorId = req.query.cursorId;
        if (cursorId) {
            const categoriesData = await prisma.category.findUnique({
                where: {
                    id,
                },
                select: {
                    id: true,
                    name: true,
                    products: {
                        orderBy: {
                            createdAt: "desc",
                        },
                        take: 12,
                        skip: 1,
                        cursor: {
                            id: cursorId as string,
                        },
                        select: {
                            id: true,
                            title: true,
                            description: true,
                            image: true,
                            price: true,
                            quantity: true,
                        },
                    },
                },
            });

            if (!categoriesData) {
                return res.status(404).json({ message: `Category not found` });
            }

            let hasMore = true;
            if (categoriesData.products.length === 0) {
                hasMore = false;
            }

            return res
                .status(200)
                .json({ category: { ...categoriesData, hasMore } });
        }

        const categoriesData = await prisma.category.findUnique({
            where: {
                id,
            },
            select: {
                id: true,
                name: true,
                products: {
                    orderBy: {
                        createdAt: "desc",
                    },
                    take: 12,
                    select: {
                        id: true,
                        title: true,
                        description: true,
                        image: true,
                        price: true,
                        quantity: true,
                    },
                },
            },
        });
        if (!categoriesData) {
            return res.status(404).json({ message: `Category not found` });
        }

        let hasMore = true;
        if (categoriesData.products.length === 0) {
            hasMore = false;
        }

        return res
            .status(200)
            .json({ category: { ...categoriesData, hasMore } });
    } catch (error) {
        return res.status(500).json({
            message: "Something went wrong!! Please try again after sometime",
        });
    }
};

const handler = nc({ attachParams: true }).get(getSingleCategory);

export default handler;

src/pages/api/categories/[id].ts

In the above snippet, we are doing the following:

  1. By creating a file named [id].ts under src/pages/api/categories we are telling NextJS to convert this to the /api/categories/[id] API.
  2. The [id] is the category id from the category table.
  3. Using the next-connect library we are making sure that only the get operation is allowed for the getSingleCategory function.
  4. Under this function, we are querying the database with order as desc for the createdAt property and we only take the latest twelve product rows. We also select a specific property from the product that is required by the front end.

In this API, you will find that we have implemented pagination also. It helps us get more products under one category.

There are two kinds of pagination. One is cursor based, and another is offset-based pagination.

So why did we choose cursor-based pagination instead of offset-based pagination?

According to the Prisma docs,

"Offset pagination does not scale at a database level. For example, if you skip 200,00 records and take the first 10, the database still has to traverse the first 200,00 records before returning the 10 that you asked for - this negatively affects performance."

For more information read this helpful guide.

Update the URL in Postman to:

http://localhost:3000/api/categories/cl91683hp006d0mvlxlg5u176?cursorId=cl91685ht00b00mvllxjwzkqk

Our URL consists of two ids and you'll need to add cl91683hp006d0mvlxlg5u176 from the previous all-category response. This cl91685ht00b00mvllxjwzkqk id is just the cursor of the product and you can add this as the last one you want.

You'll get the following response:

Screenshot-2022-10-09-at-5.07.44-PM

Single Category Resp

Now let's create an API to get single product information.

Under the src/pages/api/products directory create a file named [title].ts and copy-paste the following code:

import type { NextApiRequest, NextApiResponse } from "next";
import nc from "next-connect";
import { prisma } from "../../../lib/prisma";
import { TApiErrorResp, TApiSingleProductResp } from "../../../types";

const getSingleProduct = async (
    req: NextApiRequest,
    res: NextApiResponse<TApiSingleProductResp | TApiErrorResp>
) => {
    try {
        const title = req.query.title as string;
        const product = await prisma.product.findUnique({
            where: {
                title,
            },
            select: {
                title: true,
                description: true,
                price: true,
                quantity: true,
                image: true,
            },
        });
        if (!product) {
            return res.status(404).json({ message: `Product not found` });
        }
        return res.status(200).json({ product });
    } catch (error) {
        return res.status(500).json({
            message: "Something went wrong!! Please try again after sometime",
        });
    }
};

const handler = nc({ attachParams: true }).get(getSingleProduct);

export default handler;

src/pages/api/products/title.ts

In the above snippet, we are doing the following:

  1. By creating a file named [title].ts under src/pages/api/products we are informing NextJS to convert this to the /api/products/[title] API.
  2. The [title] is the product title from the product table.
  3. Using the next-connect library we are making sure that only the get operation is allowed for the getSingleProduct function.
  4. Under this function, we are querying the database using the findUnique query based on the title.

Update the URL in Postman to:

http://localhost:3000/api/products/Practical Gorgeous Fresh Shoes

Here Practical Gorgeous Fresh Shoes is the title of the product we want to get. You can replace it with any product title from your database.

You'll get the following response:

Screenshot-2022-10-09-at-5.12.35-PM

Single Product Resp

How to Create the All Category and Single Category UIs:

Under src/pages/_app.tsx, replace the existing code with the following:

import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import type { AppProps } from "next/app";
import queryClient from "../lib/query";
import "../styles/globals.css";

function MyApp({ Component, pageProps }: AppProps) {
    return (
        <QueryClientProvider client={queryClient}>
            <ReactQueryDevtools initialIsOpen={false} />
            <Component {...pageProps} />
        </QueryClientProvider>
    );
}

export default MyApp;

src/pages/_app.tsx

Here we are wrapping all our components with React QueryClient Provider. But we also need to pass in the Client Context.

Under the src/lib directory create a new file named query.ts and copy-paste the following code:

import { QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient();

export default queryClient;

src/lib/query.ts

We are initiating a new QueryClient object and assigning it to the queryClient variable and exporting it as default. The reason we do this is that in this way we get to keep the queryClient object as a global context.

Under src/pages/index.tsx, replace the existing code with the following:

import { useQuery } from "@tanstack/react-query";
import type { NextPage } from "next";
import Head from "next/head";
import Navbar from "../components/Navbar";
import ProductGrid from "../components/ProductGrid";
import Skelton from "../components/Skelton";

const Home: NextPage = () => {
    const getAllCategories = async () => {
        try {
            const respJSON = await fetch("/api/categories");
            const resp = await respJSON.json();
            return resp;
        } catch (error) {
            throw error;
        }
    };

    const { isLoading, data } = useQuery(
        ["AllCategoreiesWithProducts"],
        getAllCategories
    );

    const categories = data?.categories;

    return (
        <div>
            <Head>
                <title>All Products</title>
                <meta name="description" content="All Products" />
                <link rel="icon" href="/favicon.ico" />
            </Head>

            <main className="container mx-auto">
                <Navbar />
                {isLoading ? (
                    <Skelton />
                ) : (
                    <>
                        {categories && categories?.length > 0 && (
                            <ProductGrid
                                showLink={true}
                                categories={categories}
                            />
                        )}
                    </>
                )}
            </main>
        </div>
    );
};

export default Home;

src/pages/index.tsx

Let's understand our code.

Here we are fetching the data from the /api/categories endpoint that we wrote earlier. We are using useQuery to cache this data with the key AllCategoreiesWithProducts.

But there are three components that we haven't created yet. Let's create those and understand each one.

Under the src directory, create a components directory. Under the newly created components directory, create three files named Navbar.tsx, ProductGrid.tsx and Skelton.tsx.

Under Navbar.tsx copy-paste the following code:

import NextLink from "next/link";

const Navbar = () => {
    return (
        <div className="relative bg-white mx-6">
            <div className="flex items-center justify-between pt-6 md:justify-start md:space-x-10">
                <div className="flex justify-start lg:w-0 lg:flex-1">
                    <h1 className="text-2xl">
                        <NextLink href="/" className="cursor-pointer">
                            Ecomm App
                        </NextLink>
                    </h1>
                </div>
            </div>
        </div>
    );
};

export default Navbar;

src/components/Navbar.tsx

Here we have created an h1 with the text as Ecomm App. We have wrapped this text around NextLink and set the location as /. So when user clicks on this, they will be redirected to the home page.

Under ProductGrid.tsx copy-paste the following code:

import NextImage from "next/image";
import NextLink from "next/link";
import { useEffect } from "react";
import { AiOutlineRight } from "react-icons/ai";
import { useInView } from "react-intersection-observer";
import { TApiAllCategoriesResp } from "../types";

interface IProductGrid extends TApiAllCategoriesResp {
    showLink: boolean;
    hasMore?: boolean;
    loadMoreFun?: Function;
}

const ProductGrid = (props: IProductGrid) => {
    const { categories, showLink, loadMoreFun, hasMore } = props;
    const { ref, inView } = useInView();

    useEffect(() => {
        if (inView) {
            if (loadMoreFun) loadMoreFun();
        }
    }, [inView, loadMoreFun]);

    return (
        <div className="bg-white">
            {categories.map((category) => (
                <div className="mt-12  p-6" key={category.name}>
                    <div className="flex flex-row justify-between">
                        <span className="inline-flex items-center rounded-md bg-sky-800 px-8 py-2 text-md font-medium text-white">
                            {category.name}
                        </span>
                        {showLink && (
                            <NextLink href={`/category/${category.id}`}>
                                <p className="flex flex-row gap-2 underline hover:cursor-pointer items-center">
                                    View More
                                    <AiOutlineRight />
                                </p>
                            </NextLink>
                        )}
                    </div>
                    <div className="mt-6  grid grid-cols-1 gap-y-10 gap-x-6 xl:gap-x-8 sm:grid-cols-2 lg:grid-cols-4">
                        {category?.products.map((product) => (
                            <div
                                className="p-6 group rounded-lg border border-gray-200 bg-neutral-200"
                                key={product.title}
                            >
                                <div className="min-h-80 w-full overflow-hidden rounded-md group-hover:opacity-75 lg:aspect-none lg:h-80">
                                    <NextImage
                                        priority={true}
                                        layout="responsive"
                                        width="25"
                                        height="25"
                                        src={`${product.image}`}
                                        alt={product.title}
                                        className="h-full w-full object-cover object-center lg:h-full lg:w-full"
                                    />
                                </div>
                                <div className="relative mt-2">
                                    <h3 className="text-sm font-medium text-gray-900">
                                        {product.title}
                                    </h3>
                                    <p className="mt-1 text-sm text-gray-500">
                                        {product.price}
                                    </p>
                                </div>
                                <div className="mt-6">
                                    <NextLink
                                        href={`/product/${product.title}`}
                                    >
                                        <p className="relative flex items-center justify-center rounded-md border border-transparent bg-sky-800 py-2 px-8 text-sm font-medium text-white hover:bg-sky-900 hover:cursor-pointer">
                                            View More Details
                                        </p>
                                    </NextLink>
                                </div>
                            </div>
                        ))}
                    </div>
                    {!showLink && (
                        <div className="flex items-center justify-center mt-8">
                            {hasMore ? (
                                <button
                                    ref={ref}
                                    type="button"
                                    className="inline-flex items-center rounded-md border border-transparent bg-sky-800 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-900"
                                >
                                    Loading...
                                </button>
                            ) : (
                                <div className="border-l-4 border-yellow-400 bg-yellow-50 p-4 w-full">
                                    <div className="flex">
                                        <div className="ml-3">
                                            <p className="text-sm text-yellow-700">
                                                You have viewed all the Products
                                                under this category.
                                            </p>
                                        </div>
                                    </div>
                                </div>
                            )}
                        </div>
                    )}
                    {showLink && (
                        <div className="w-full border-b border-gray-300 mt-24" />
                    )}
                </div>
            ))}
        </div>
    );
};

export default ProductGrid;

src/components/ProductGrid.tsx

Here we have created a grid that will show 1 column for the base screen. For the sm screen it will show 2 columns and for the lg screen it will show 4 columns.

Under this, we have a single card which has a Title, Price, and View More Details button. The View More Details button redirects the user to a single product page which will create a bit later.

Apart from that, we are using the useInView hook from the react-intersection-observer library to find the user's cursor on the screen. This ref is attached to a Loading... button and once user is near it then we execute the loadMoreFn function.

It makes an API call to the server to get the next twelve rows from the last cursor.

Under Skelton.tsx copy-paste the following code:

const Skelton = () => {
    return (
        <>
            <div className="mt-12 h-8 w-40 rounded-lg bg-gray-200" />
            <div className="mt-6 grid grid-cols-1 gap-y-10 gap-x-6 sm:grid-cols-2 lg:grid-cols-4 xl:gap-x-8">
                {Array(16)
                    .fill(0)
                    .map((_val, index) => (
                        <div className="rounded-2xl bg-black/5 p-4" key={index}>
                            <div className="h-60 rounded-lg bg-gray-200" />
                            <div className="space-y-4 mt-6 mb-4">
                                <div className="h-3 w-3/5 rounded-lg bg-gray-200" />
                                <div className="h-3 w-4/5 rounded-lg bg-gray-200" />
                            </div>
                        </div>
                    ))}
            </div>
        </>
    );
};

export default Skelton;

src/components/Skelton.tsx

We are using placeimg to get fake images for our product and we are using the Next Image component which requires that it be mentioned under next.config.js.

Replace the existing code in next.config.js with the following code:

/** @type {import('next').NextConfig} */
const nextConfig = {
    reactStrictMode: true,
    swcMinify: true,
    images: {
        domains: ["placeimg.com"],
    },
};

module.exports = nextConfig

next.config.js

We will need to restart our server. Use the following command to start your development server again:

npm run dev

Open http://localhost:3000/ and you'll see the following UI:

Screenshot-2022-10-09-at-5.31.29-PM

All Products Page

Now let's create a single category page that the user can go to using a View More link.

Under src/pages create a directory named category. Under this directory create a file named [id].tsx and copy paste the following code:

import { useInfiniteQuery } from "@tanstack/react-query";
import Head from "next/head";
import { useRouter } from "next/router";
import Navbar from "../../components/Navbar";
import ProductGrid from "../../components/ProductGrid";
import Skelton from "../../components/Skelton";

const SingleCategory = () => {
    const router = useRouter();

    const getSingleCategory = async ({ pageParam = null }) => {
        try {
            let url = `/api/categories/${router.query.id}`;
            if (pageParam) {
                url += `?cursorId=${pageParam}`;
            }
            const respJSON = await fetch(url);
            const resp = await respJSON.json();
            return resp;
        } catch (error) {
            throw error;
        }
    };

    const { isLoading, data, fetchNextPage, isError } = useInfiniteQuery(
        [`singleCategory ${router.query.id as string}`],
        getSingleCategory,
        {
            enabled: !!router.query.id,
            getNextPageParam: (lastPage) => {
                const nextCursor =
                    lastPage?.category?.products[
                        lastPage?.category?.products?.length - 1
                    ]?.id;
                return nextCursor;
            },
        }
    );

    const allProductsWithCategory: any = {
        name: "",
        products: [],
        hasMore: true,
    };

    data?.pages.map((page) => {
        if (page?.category) {
            if (page.category?.name) {
                allProductsWithCategory.name = page.category?.name;
            }
            if (page.category?.products && page.category?.products.length > 0) {
                allProductsWithCategory.products.push(
                    ...page.category?.products
                );
            }
        }
        return page?.category;
    });

    if (data?.pages[data?.pages.length - 1]?.category?.products.length === 0) {
        allProductsWithCategory.hasMore = false;
    }

    return (
        <div>
            <Head>
                <title>
                    {isLoading
                        ? "Loading..."
                        : `All ${allProductsWithCategory?.name} Product`}
                </title>
                <meta
                    name="description"
                    content="Generated by create next app"
                />
                <link rel="icon" href="/favicon.ico" />
            </Head>
            <main className="container mx-auto">
                <Navbar />
                {isLoading ? (
                    <Skelton />
                ) : (
                    <>
                        {allProductsWithCategory &&
                            allProductsWithCategory.products.length > 0 && (
                                <ProductGrid
                                    hasMore={allProductsWithCategory.hasMore}
                                    showLink={false}
                                    categories={[allProductsWithCategory]}
                                    loadMoreFun={fetchNextPage}
                                />
                            )}
                    </>
                )}
            </main>
        </div>
    );
};

export default SingleCategory;

src/pages/category/[id]/tsx

Here we are calling the /api/categories/[id] API to get the latest twelve products for that category id.

We are using the useInfiniteQuery hook from react query to fetch the data. This hook is useful for cursor-based pagination. We will be using the ProductGrid component that we created earlier.

Open http://localhost:3000/, click on the View More link for any of the category, and you'll see the following UI:

Screenshot-2022-10-09-at-5.38.25-PM

Single Category Page

The difference between the previous UI and the current is that we now don't have the View More Link in the top right corner. Also, when you scroll below, you'll get more products for that category.

Once we scroll through all the products in that category we will see the following warning alert:

Screenshot-2022-10-09-at-5.39.44-PM

How to Implement Single Product UI and Stripe Checkout.

In this section, we'll implement the following functionality:

  1. Create Single Product UI
  2. Create Stripe Checkout

You can find the eCommerce website code implemented in this section at this commit.

How to Create the Single Product UI:

Under the src/pages directory create a directory named product.

Under this directory create a file named [title].tsx and copy-paste the following code:

import { loadStripe, Stripe } from "@stripe/stripe-js";
import { useMutation, useQuery } from "@tanstack/react-query";
import Head from "next/head";
import NextImage from "next/image";
import { useRouter } from "next/router";
import Navbar from "../../components/Navbar";
import Skelton from "../../components/Skelton";

const stripePromiseclientSide = loadStripe(
    process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

const SingleProduct = () => {
    const router = useRouter();

    const getSingleProduct = async () => {
        try {
            const title = router?.query?.title;

            const respJSON = await fetch(`/api/products/${title}`);
            const resp = await respJSON.json();
            return resp;
        } catch (error) {
            throw error;
        }
    };

    const { mutate, isLoading: mutationIsLoading } = useMutation(
        async (body: any) => {
            try {
                const respJSON = await fetch("/api/create-checkout-session", {
                    method: "POST",
                    body: JSON.stringify(body),
                });
                const resp = await respJSON.json();
                const stripe = (await stripePromiseclientSide) as Stripe;
                const result = await stripe.redirectToCheckout({
                    sessionId: resp.id,
                });
                return result;
            } catch (error) {
                throw error;
            }
        }
    );

    const { data, isLoading } = useQuery(
        [`singleProduct, ${router?.query?.title}`],
        getSingleProduct,
        {
            enabled: !!router?.query?.title,
        }
    );

    const product = data?.product;

    return (
        <div>
            <Head>
                <title>{isLoading ? "Loading..." : `${product?.title}`}</title>
                <meta
                    name="description"
                    content="Generated by create next app"
                />
                <link rel="icon" href="/favicon.ico" />
            </Head>
            <main className="container mx-6 md:mx-auto">
                <Navbar />
                {isLoading ? (
                    <Skelton />
                ) : (
                    <div className="bg-white">
                        <div className="pt-6 pb-16 sm:pb-24">
                            <div className="mx-auto mt-8">
                                <div className="flex flex-col md:flex-row gap-x-8">
                                    <div className="min-h-80 w-full overflow-hidden rounded-md group-hover:opacity-75 lg:aspect-none lg:h-80">
                                        <NextImage
                                            layout="responsive"
                                            width="25"
                                            height="25"
                                            src={`${product.image}`}
                                            alt={product.title}
                                            className="h-full w-full object-cover object-center lg:h-full lg:w-full"
                                        />
                                    </div>
                                    <div className="lg:col-span-5 lg:col-start-8 mt-8 md:mt-0">
                                        <h1 className="text-xl font-medium text-gray-900 ">
                                            {product.title}
                                        </h1>
                                        <p className="text-xl font-light text-gray-700 mt-4">
                                            {product.description}
                                        </p>
                                        <p className="text-xl font-normal text-gray-500 mt-4">
                                            USD {product.price}
                                        </p>
                                        <button
                                            onClick={() =>
                                                mutate({
                                                    title: product.title,
                                                    image: product.image,
                                                    description:
                                                        product.description,
                                                    price: product.price,
                                                })
                                            }
                                            disabled={mutationIsLoading}
                                            type="button"
                                            className="inline-flex items-center rounded-md border border-transparent bg-sky-800 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-900  mt-4"
                                        >
                                            Buy Now
                                        </button>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                )}
            </main>
        </div>
    );
};

export default SingleProduct;

src/pages/product/[title].tsx

Here we are calling /api/products/title API to get the latest product. We have also created Stripe Interface for creating a checkout method once a user clicks the Buy Now button.

Once the user clicks the Buy Now button, we make an API call to /api/create-checkout-session using the useMutation hook. On a successful response, we redirect the user to the Stripe default checkout page.

Open http://localhost:3000/ and click on View More Details button for any product.

You'll see the following UI:

Screenshot-2022-10-09-at-5.54.40-PM

Single Product Page

You can also visit this page by clicking on the View More link and then clicking on the View More Details button for any product.

How to Set Up Stripe Checkout:

To set up Stripe Checkout, we need to add a new file under the lib directory.

Create a new file named stripe.ts under the lib directory and copy paste the following code:

import Stripe from "stripe";

const stripeServerSide = new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: "2022-08-01",
});

export { stripeServerSide };

src/lib/stripe.ts

Here we are creating server-side instances of Stripe. Now under the pages/api directory, create a new file named create-checkout-session.ts and copy-paste the following code:

import currency from "currency.js";
import type { NextApiRequest, NextApiResponse } from "next";
import nc from "next-connect";
import { stripeServerSide } from "../../lib/stripe";
import { TApiErrorResp } from "../../types";

const checkoutSession = async (
    req: NextApiRequest,
    res: NextApiResponse<any | TApiErrorResp>
) => {
    try {
        const host = req.headers.origin;
        const referer = req.headers.referer;
        const body = JSON.parse(req.body);
        const formatedPrice = currency(body.price, {
            precision: 2,
            symbol: "",
        }).multiply(100);
        const session = await stripeServerSide.checkout.sessions.create({
            mode: "payment",
            payment_method_types: ["card"],
            line_items: [
                {
                    price_data: {
                        currency: "usd",
                        product_data: {
                            name: body?.title,
                            images: [body.image],
                            description: body?.description,
                        },
                        unit_amount_decimal: formatedPrice.toString(),
                    },
                    quantity: 1,
                },
            ],
            success_url: `${host}/thank-you`,
            cancel_url: `${referer}?status=cancel`,
        });
        return res.status(200).json({ id: session.id });
    } catch (error) {
        return res.status(500).json({
            message: "Something went wrong!! Please try again after sometime",
        });
    }
};

const handler = nc({ attachParams: true }).post(checkoutSession);

export default handler;

src/pages/api/create-checkout-session.ts

In the snippet above we are doing the following:

  1. Formatting the price with precision as two and multiplying it by 100 as Stripe expects the unit_amount in cents by default.
  2. We create the session and pass the id as the response.

Now we need to create another page named thank-you.tsx under the src/pages directory. Once the product purchase is successful, Stripe Checkout will redirect to this page.

Copy-paste the following code under this file:

import Head from "next/head";
import { useRouter } from "next/router";
import { HiCheckCircle } from "react-icons/hi";
import Navbar from "../components/Navbar";

const ThankYou = () => {
    const router = useRouter();
    return (
        <div>
            <Head>
                <title>Thank You</title>
                <meta name="description" content="All Products" />
                <link rel="icon" href="/favicon.ico" />
            </Head>

            <main className="container mx-auto">
                <Navbar />
                <div className="rounded-md bg-green-50 p-4 mt-8">
                    <div className="flex">
                        <div className="flex-shrink-0">
                            <HiCheckCircle
                                className="h-5 w-5 text-green-400"
                                aria-hidden="true"
                            />
                        </div>
                        <div className="ml-3">
                            <h3 className="text-sm font-medium text-green-800">
                                Order Placed
                            </h3>
                            <div className="mt-2 text-sm text-green-700">
                                <p>
                                    Thank you for your Order. We have placed the
                                    order and your email will recieve further
                                    details.
                                </p>
                            </div>
                            <button
                                onClick={() => router.push("/")}
                                type="button"
                                className="inline-flex items-center rounded-md border border-transparent bg-sky-800 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-900 mt-4"
                            >
                                Continue Shopping
                            </button>
                        </div>
                    </div>
                </div>
            </main>
        </div>
    );
};

export default ThankYou;

src/pages/thank-you.tsx

Open http://localhost:3000/ and click on the View More Details button of any product.

Click on the Buy Now button and you'll be redirected to the following page:

Screenshot-2022-10-09-at-6.09.52-PM

Add all the details for the test card. You can use any card from this link. Stripe provides various test cards which work only during Test Mode. Once you click on Pay and payment processing happens, Stripe will redirect you to the success page.

Screenshot-2022-10-09-at-6.14.51-PM

Thank You Page

How to Deploy the Website to Production

In this section, we'll implement the following functionality:

  1. Promote our PlanetScale branch to Main.
  2. Deploy the app on Vercel.

How to Promote the PlanetScale Branch to Main:

To promote the branch to main, we can do it either via the terminal or dashboard. I'll use the dashboard for this tutorial.

Go to your project on PlanetScale and you'll find the following message on the dashboard:

Screenshot-2022-10-10-at-4.19.14-PM

PlanetScale Database Promotion

Let's click on the Promote a branch to production button and you'll get a confirmation model. Click on the Promote branch button. Once done you'll get a toast with a success message.

How to Deploy to Vercel:

If you don't have an account on Vercel, you can create one here.

You can create a project on GitHub and push it to the Main branch. If you don't know how, you can check out this tutorial.

Once the project is pushed on GitHub, go to Vercel and create an Add New button and select Project from the drop down.

Screenshot-2022-10-10-at-4.26.04-PM

Add New Project Vercel

You'll get the following the UI:

Screenshot-2022-10-10-at-4.26.39-PM

Select Git Provider Vercel

As we have pushed the code on GitHub, let's click on the Continue with GitHub button. You'll get the following UI:

Screenshot-2022-10-10-at-4.28.03-PM

Select Git Repository Vercel

Click on Import and you'll get the following UI:

Screenshot-2022-10-10-at-4.29.39-PM

Configure Project Vercel

Click on the Environment Variables and add these three there:

Screenshot-2022-10-10-at-4.31.02-PM

Add NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_SECRET_KEY, and DATABASE_URL

Once done click the Deploy button. You'll get the following UI once the deployment starts:

Screenshot-2022-10-10-at-4.31.52-PM

Deploying Vercel

Once deployed, Vercel will give you a Unique URL.

Visit this URL and you'll find its failing. Let's go to the deployment > functions and you'll see the following error:

Screenshot-2022-10-10-at-4.38.08-PM

Prisma generate Fails

We need to update our build command in package.json as follows:

"build": "npx prisma generate && next build",

Push the code again to the Git repository and you'll find that Vercel starts redeploying your project.

Once the deployment is done, you can visit your application URL and you'll find it shows all your products.

With this, we have created our production-ready eCommerce application. If you have built the website along with the tutorial, then a very big congratulations to you on this achievement.

Original article source at https://www.freecodecamp.org

#react #reactjs #tailwindcss #tailwind #stripe #programming #developer #softwaredeveloper #computerscience #webdev #webdeveloper #webdevelopment #planetscale 

Build an E-Commerce Website with React, TailwindCSS, PlanetScale, and Stripe