1683272925
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
1682400795
В этом руководстве мы разработаем веб-приложение для продажи книг с использованием Stripe (для обработки платежей), Vue.js (клиентское приложение) и Flask (серверный API).
Окончательное приложение :
Основные зависимости:
К концу этого урока вы сможете:
Клонируйте базовый проект 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">×</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)
Проверьте это!
Не забывайте обрабатывать ошибки как на клиенте, так и на сервере!
Сделайте то же самое самостоятельно для редактирования книги:
Нужна помощь? Еще раз просмотрите предыдущий раздел. Вы также можете получить окончательный код из репозитория 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
Мы тут-
Обратите внимание на 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.
Платеж должен пройти успешно, но перенаправление не удастся, так как мы еще не настроили маршрут /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.
Ищете больше?
Оригинальный источник статьи: https://testdriven.io/
1682397060
在本教程中,我们将使用Stripe(用于支付处理)、Vue.js(客户端应用程序)和Flask(服务器端 API)开发一个用于销售书籍的 Web 应用程序。
最终应用:
主要依赖:
在本教程结束时,您将能够:
从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 换成Poetry或Pipenv。有关更多信息,请查看现代 Python 环境。
将您选择的浏览器指向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。
首先将 添加到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>
你现在应该看到:
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">×</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)
测试一下!
不要忘记处理客户端和服务器上的错误!
自己做同样的事情来编辑一本书:
需要帮忙?再次回顾上一节。您还可以从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
在这里,我们-
注意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 ID 调用该方法。
让我们测试一下。导航到http://localhost:5173。单击其中一个购买按钮。您应该被重定向到带有基本产品信息的 Stripe Checkout 实例(Stripe 托管的页面,用于安全地收集付款信息):
您可以使用 Stripe 提供的多个测试卡号之一来测试表单。让我们使用4242 4242 4242 4242。
付款应该成功处理,但重定向将失败,因为我们/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存储库中找到最终代码。
寻找更多?
文章原文出处:https: //testdriven.io/
1682396700
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:
Main dependencies:
By the end of this tutorial, you will be able to:
Clone the base Flask and Vue project from the flask-vue-crud repo:
$ git clone https://github.com/testdrivenio/flask-vue-crud flask-vue-stripe
$ cd flask-vue-stripe
Create and activate a virtual environment, and then spin up the Flask app:
$ cd server
$ python3.11 -m venv env
$ source env/bin/activate
(env)$
(env)$ pip install -r requirements.txt
(env)$ flask run --port=5001 --debug
The above commands, for creating and activating a virtual environment, may differ depending on your environment and operating system. Feel free to swap out virtualenv and Pip for Poetry or Pipenv. For more, review Modern Python Environments.
Point your browser of choice at http://localhost:5001/ping. You should see:
"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:
Want to learn how to build this project? Check out the Developing a Single Page App with Flask and Vue.js tutorial.
Our goal is to build a web app that allows end users to purchase books.
The client-side Vue app will display the books available for purchase and redirect the end user to the checkout form via Stripe.js and Stripe Checkout. After the payment process is complete, users will be redirected to either a success or failure page also managed by Vue.
The Flask app, meanwhile, uses the Stripe Python Library for interacting with the Stripe API to create a checkout session.
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.
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.
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 a new form input to addBookModal
, between the author and read form inputs:
<div class="mb-3">
<label for="addBookPrice" class="form-label">Purchase price:</label>
<input
type="number"
step="0.01"
class="form-control"
id="addBookPrice"
v-model="addBookForm.price"
placeholder="Enter price">
</div>
The modal should now look like:
<!-- add new book modal -->
<div
ref="addBookModal"
class="modal fade"
:class="{ show: activeAddBookModal, 'd-block': activeAddBookModal }"
tabindex="-1"
role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add a new book</h5>
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
@click="toggleAddBookModal">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="addBookTitle" class="form-label">Title:</label>
<input
type="text"
class="form-control"
id="addBookTitle"
v-model="addBookForm.title"
placeholder="Enter title">
</div>
<div class="mb-3">
<label for="addBookAuthor" class="form-label">Author:</label>
<input
type="text"
class="form-control"
id="addBookAuthor"
v-model="addBookForm.author"
placeholder="Enter author">
</div>
<div class="mb-3">
<label for="addBookPrice" class="form-label">Purchase price:</label>
<input
type="number"
step="0.01"
class="form-control"
id="addBookPrice"
v-model="addBookForm.price"
placeholder="Enter price">
</div>
<div class="mb-3 form-check">
<input
type="checkbox"
class="form-check-input"
id="addBookRead"
v-model="addBookForm.read">
<label class="form-check-label" for="addBookRead">Read?</label>
</div>
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-primary btn-sm"
@click="handleAddSubmit">
Submit
</button>
<button
type="button"
class="btn btn-danger btn-sm"
@click="handleAddReset">
Reset
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div v-if="activeAddBookModal" class="modal-backdrop fade show"></div>
Then, add price
to the state:
addBookForm: {
title: '',
author: '',
read: [],
price: '',
},
The state is now bound to the form's input value. Think about what this means. When the state is updated, the form input will be updated as well -- and vice versa. Here's an example of this in action with the vue-devtools browser extension:
Add the price
to the payload
in the handleAddSubmit
method like so:
handleAddSubmit() {
this.toggleAddBookModal();
let read = false;
if (this.addBookForm.read[0]) {
read = true;
}
const payload = {
title: this.addBookForm.title,
author: this.addBookForm.author,
read, // property shorthand
price: this.addBookForm.price,
};
this.addBook(payload);
this.initForm();
},
Update initForm
to clear out the value after the end user submits the form or clicks the "reset" button:
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
this.addBookForm.price = '';
this.editBookForm.id = '';
this.editBookForm.title = '';
this.editBookForm.author = '';
this.editBookForm.read = [];
},
Finally, update the route in server/app.py:
@app.route('/books', methods=['GET', 'POST'])
def all_books():
response_object = {'status': 'success'}
if request.method == 'POST':
post_data = request.get_json()
BOOKS.append({
'id': uuid.uuid4().hex,
'title': post_data.get('title'),
'author': post_data.get('author'),
'read': post_data.get('read'),
'price': post_data.get('price')
})
response_object['message'] = 'Book added!'
else:
response_object['books'] = BOOKS
return jsonify(response_object)
Test it out!
Don't forget to handle errors on both the client and server!
Do the same, on your own, for editing a book:
editBookForm
in the stateprice
to the payload
in the handleEditSubmit
methodinitForm
Need help? Review the previous section again. You can also grab the final code from the flask-vue-stripe repo.
Add a "purchase" button to the Books
component, just below the "delete" button:
<td>
<div class="btn-group" role="group">
<button
type="button"
class="btn btn-warning btn-sm"
@click="toggleEditBookModal(book)">
Update
</button>
<button
type="button"
class="btn btn-danger btn-sm"
@click="handleDeleteBook(book)">
Delete
</button>
<button
type="button"
class="btn btn-primary btn-sm"
@click="handlePurchaseBook(book)">
Purchase
</button>
</div>
</td>
Next, add handlePurchaseBook
to the component's methods
:
handlePurchaseBook(book) {
console.log(book.id);
},
Test it out:
Sign up for a Stripe account, if you don't already have one.
Install the Stripe Python library:
(env)$ pip install stripe==5.4.0
Grab the test mode API keys from the Stripe dashboard:
Set them as environment variables within the terminal window where you're running the server:
(env)$ export STRIPE_PUBLISHABLE_KEY=<YOUR_STRIPE_PUBLISHABLE_KEY>
(env)$ export STRIPE_SECRET_KEY=<YOUR_STRIPE_SECRET_KEY>
Import the Stripe library into server/app.py and assign the keys to stripe.api_key
so that they will be used automatically when interacting with the API:
import os
import uuid
import stripe
from flask import Flask, jsonify, request
from flask_cors import CORS
...
# configuration
DEBUG = True
# instantiate the app
app = Flask(__name__)
app.config.from_object(__name__)
# configure stripe
stripe_keys = {
'secret_key': os.environ['STRIPE_SECRET_KEY'],
'publishable_key': os.environ['STRIPE_PUBLISHABLE_KEY'],
}
stripe.api_key = stripe_keys['secret_key']
# enable CORS
CORS(app, resources={r'/*': {'origins': '*'}})
...
if __name__ == '__main__':
app.run()
Next, add a new route handler that returns the publishable key:
@app.route('/config')
def get_publishable_key():
stripe_config = {'publicKey': stripe_keys['publishable_key']}
return jsonify(stripe_config)
This will be used on the client side to configure the Stripe.js library.
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,
};
},
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.
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-
domain_url
for redirecting the user back to the client after a purchase is completeTake note of the success_url
and cancel_url
. The user will be redirected back to those URLs in the event of a successful payment or cancellation, respectively. We'll set the /success
and /cancelled
routes up shortly on the client.
Also, did you notice that we converted the float to an integer via round(float(book_to_purchase['price']) * 100)
? Stripe only allows integer values for the price. For production code, you'll probably want to store the price as an integer value in the database -- e.g., $3.99 should be stored as 399
.
Add the import to the top:
import json
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:
You can test the form by using one of the several test card numbers that Stripe provides. Let's use 4242 4242 4242 4242
.
4242 4242 4242 4242
The payment should be processed successfully, but the redirect will fail since we have not set up the /success
route yet.
You should see the purchase back in the Stripe Dashboard:
Finally, let's set up routes and components for handling a successful payment or cancellation.
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
Finally, you could display info about the purchase using the session_id
query param:
http://localhost:5173/success?session_id=cs_test_a1qw4pxWK9mF2SDvbiQXqg5quq4yZYUvjNkqPq1H3wbUclXOue0hES6lWl
You can access it like so:
<script>
export default {
mounted() {
console.log(this.$route.query.session_id);
},
};
</script>
From there, you'll want to set up a route handler on the server-side, to look up the session info via stripe.checkout.Session.retrieve(id)
. Try this out on your own.
For the /canceled
redirect, add a new component called client/src/components/OrderCanceled.vue:
<template>
<div class="container">
<div class="row">
<div class="col-sm-10">
<h1>Your payment was cancelled.</h1>
<hr><br>
<router-link to="/" class="btn btn-primary btn-sm">Back Home</router-link>
</div>
</div>
</div>
</template>
Then, update the router:
import { createRouter, createWebHistory } from 'vue-router'
import Books from '../components/Books.vue'
import OrderCanceled from '../components/OrderCanceled.vue'
import OrderSuccess from '../components/OrderSuccess.vue'
import Ping from '../components/Ping.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Books',
component: Books,
},
{
path: '/ping',
name: 'ping',
component: Ping
},
{
path: '/success',
name: 'OrderSuccess',
component: OrderSuccess,
},
{
path: '/canceled',
name: 'OrderCanceled',
component: OrderCanceled,
},
]
})
export default router
Test it out one last time.
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?
Original article source at: https://testdriven.io/
1680496065
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
1678185195
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.
npm install
.env
file based on config.ts../src/controllers
directoryapp.ts
file using staart controllers
staart build
and deploy with staart launch
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.
The Staart ecosystem consists of open-source projects to build your SaaS startup, written in TypeScript.
Package | ||
---|---|---|
🛠️ Staart API | Node.js backend with RESTful APIs | |
🌐 Staart UI | Frontend Vue.js Progressive Web App | |
📑 Staart Site | Static site generator for docs/helpdesk | |
📱 Staart Native | React Native app for Android and iOS | |
🎨 Staart.css | Sass/CSS framework and utilities | |
📦 Staart Packages | Helper functions and utility packages |
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.
Author: Staart
Source Code: https://github.com/staart/api
License: MIT license
1675656264
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
1672848126
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:
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:
In this blog we will be talking about how stripe checkout works when a customer makes an order.
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.
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/
1672461120
A serverless function to process stripe payments with Nuxt, Netlify, and Lambda
Demo site is here: E-Commerce Store
There are two articles explaining how this site is set up:
# 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.
Author: sdras
Source Code: https://github.com/sdras/ecommerce-netlify
1671586044
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
1669435680
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
1669096147
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.
By the end of this tutorial, you should be able to:
With Stripe Connect, you first need to decide the type(s) of user accounts that you want to work with on your platform:
"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:
We'll stick with Standard accounts in this tutorial. For more, review the Choosing an approach and Best Practices guides from Stripe.
Again, with Standard (and Express) accounts, users go through an OAuth-like flow to connect their Stripe accounts:
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:
Make sure you are able to log in as the superuser:
Try logging in as both a buyer and a seller.
Buyer:
test@buyer.com
justatest
Seller:
test@seller.com
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
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:
test@buyer.com
justatest
Then, purchase a course:
You should see the charge on the Stripe dashboard under "Payments":
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:
Then, click the "Get started" button. Once you're registered, click the "Settings" link and grab your test client ID:
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:
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.
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 beread_only
orread_write
:
- Use
read_only
when the platform just needs view access.- 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:
test@seller.com
justatest
Make sure you're redirected to Stripe when you click "Connect Stripe Account":
Don't do anything just yet as we still need to set up the redirect view.
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:
test@seller.com
/ justatest
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 newaccess_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:
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.
First, you need to make a decision on the how you want the charge to be handled:
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 Type | Payment Approach | Liability |
---|---|---|
Standard | Direct | User |
Standard | Destination | Platform |
Express | Direct | User |
Express | Destination | Platform |
Express | Separate Charge and Transfer | Platform |
Custom | Direct | User |
Custom | Destination | Platform |
Custom | Separate Charge and Transfer | Platform |
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.
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:
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:
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:
Looking for some challenges?
You can find the final code in the django-stripe-connect repo on GitHub. Cheers!
Original article source at: https://testdriven.io/
1668763876
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.
There are currently three ways to accept one-time payments with Stripe:
Which one should you use?
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.
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:
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":
Then in the left sidebar click on "API keys":
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:
Next, we need to create a product to sell.
Click "Products" and then "Add product":
Add a product name, enter a price, and select "One time":
Click "Save product".
After the user clicks the purchase button we need to do the following:
Get Publishable Key
Create Checkout Session
Redirect the User Appropriately
Confirm Payment with Stripe Webhooks
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:
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
]
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
Create Checkout Session
Redirect the User Appropriately
Confirm Payment with Stripe Webhooks
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.
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
]
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:
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
Create Checkout Session
Redirect the User Appropriately
Confirm Payment with Stripe Webhooks
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":
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
Create Checkout Session
Redirect the User Appropriately
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:
This section was written by Nik Tomazic.
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
]
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)})
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:
Flow:
Get Publishable Key
Create Checkout Session
Redirect the User Appropriately
Confirm Payment with Stripe Webhooks
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/
1668694631
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 currently has three strategies for accepting one-time payments:
Which strategy should you use?
What about the Charges API?
- 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.
- 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.
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.
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":
Then in the left sidebar click on "API keys":
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:
Next, we need to create a product to sell.
Click "Products" in the top navigation bar and then "Add product":
Add a product name, enter a price, and select "One time":
Click "Save product".
With the API keys in place and a product setup, we can now start adding Stripe Checkout to process payments.
After the user clicks the purchase button we need to do the following:
Get Publishable Key
Create Checkout Session
Redirect the User Appropriately
Confirm Payment with Stripe Webhooks
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:
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)
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
Create Checkout Session
Redirect the User Appropriately
Confirm Payment with Stripe Webhooks
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.
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-
domain_url
(for the redirects)stripe.api_key
(so it will be sent automatically when we make a request to create a new Checkout Session)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.
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:
We can test the form by using one of the several test card numbers that Stripe provides. Let's use 4242 4242 4242 4242
.
4242 4242 4242 4242
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
Create Checkout Session
Redirect the User Appropriately
Confirm Payment with Stripe Webhooks
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:
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
Create Checkout Session
Redirect the User Appropriately
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:
This section was written by Nik Tomazic.
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
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
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:
Workflow:
Get Publishable Key
Create Checkout Session
Redirect the User Appropriately
Confirm Payment with Stripe Webhooks
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:
/create-checkout-session
route to only allow POST requests./create-checkout-session
route.Cheers!
--
Grab the code from the flask-stripe-checkout repo on GitHub.
Original article source at: https://testdriven.io/
1668659900
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.
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.
In this section, we'll implement the following functionality:
You can find the eCommerce website code implemented in this section at this commit.
To create a PlanetScale account, visit this URL. Click on Get started button at the top right corner.
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.
PlanetScale Dashboard Page
You'll receive the following modal:
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:
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.
PlanetScale Database Username and Password Modal
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.
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:
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:
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:
Let's configure TailwindCSS into our project using the following command:
npx tailwindcss init -p
You'll get the following response:
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:
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:
Open http://localhost:3000 on your browser, and you'll get the following screen with a hello message:
Screen with Hello Message
Let's configure Prisma into our project using the following command:
npx prisma init
You'll get the following response:
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:
String
type because it will hold a decimal value.Int
type because it will hold a numerical value.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:
PlanetScale Two Tables Created
Click on these tables, and you'll be redirected to the following page:
PlanetScale Database Schema
In this section, we'll implement the following functionality:
You can find the eCommerce website code implemented in this section at this commit.
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:
deleteMany()
function.deleteMany()
function.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.falso
gets repeated. So we use the upsert method from @prisma/client
.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.
Once the seed command is successful, you'll get the following response:
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:
Prisma Studio
The number of Products and Categories may vary on your side or be similar, as this is random data.
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:
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.next-connect
library we are making sure that only the get
operation is allowed for the getCategories
function.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:
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:
[id].ts
under src/pages/api/categories
we are telling NextJS to convert this to the /api/categories/[id]
API.[id]
is the category id from the category table.next-connect
library we are making sure that only the get
operation is allowed for the getSingleCategory
function.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?
"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:
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:
[title].ts
under src/pages/api/products
we are informing NextJS to convert this to the /api/products/[title]
API.[title]
is the product title from the product table.next-connect
library we are making sure that only the get
operation is allowed for the getSingleProduct
function.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:
Single Product Resp
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:
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:
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:
In this section, we'll implement the following functionality:
You can find the eCommerce website code implemented in this section at this commit.
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:
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.
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:
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:
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.
Thank You Page
In this section, we'll implement the following functionality:
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:
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.
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.
Add New Project Vercel
You'll get the following the UI:
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:
Select Git Repository Vercel
Click on Import and you'll get the following UI:
Configure Project Vercel
Click on the Environment Variables and add these three there:
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:
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:
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