На этой неделе мы будем принимать платежи с помощью Stripe. Мы реализуем бессерверную функцию для списания средств с карты и внедряем веб-хуки для обновления нашего пользователя Prisma курсами, которые они приобрели.
Расширение пользовательской схемы
Чтобы отслеживать, какие курсы приобрел пользователь, нам нужно будет расширить нашу схему пользователя, чтобы она содержала поле для `stripeId`.
// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
courses Course[]
stripeId String @unique
createdAt DateTime @default(now())
}
Это будет использоваться для сопоставления пользователя Prisma с клиентом Stripe.
Эта модификация временно нарушит работу нашего приложения, поскольку `stripeId` теперь является обязательным полем, и мы не устанавливаем его при создании пользователя в нашем приложении.
Давайте создадим миграцию, чтобы применить эти изменения к нашей БД.
npx prisma migrate dev --name add-stripe-id-to-user --preview-feature
Настройка полосы
Первое, что вам нужно сделать, это создать учетную запись Stripe.
После того, как вы создали учетную запись и попали на панель инструментов Stripe, вам нужно будет ввести данные своего бизнеса, чтобы активировать свою учетную запись. Это даст вам доступ к производственным ключам API и позволит вам обрабатывать реальные платежи. Вам не нужно активировать свою учетную запись, чтобы завершить эту серию, но вы можете сделать это, если хотите использовать это в реальном мире!
Далее нам нужно установить две библиотеки Stripe в наше приложение.
npm i stripe @stripe/stripe-js
Stripe — это внутренняя библиотека, которую мы будем использовать для обработки платежей, а @stripe/stripe-js — внешняя библиотека, которую наш клиент будет использовать для инициации платежного сеанса.
Теперь нам нужно изменить наш файл .env, чтобы добавить наши новые ключи API — их можно найти на панели инструментов Stripe на панели «Получить ключи API». Убедитесь, что вы используете «тестовые» ключи для локальной разработки.
// .env
// other secrets
STRIPE_SECRET_KEY=your-secret-key
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=your-publishable-key
Мы должны добавить к переменным среды внешнего интерфейса `NEXT_PUBLIC_`. Переменные, не содержащие this, будут доступны только нашим бессерверным функциям.
Следуйте той же логике из Хостинг на Vercel, автоматическое развертывание с GitHub и настройка пользовательских доменов, чтобы добавить секреты в Vercel — без этого наше размещенное приложение не будет работать.
Здорово! Теперь у нас должна быть полоска!
Создать клиента Stripe
Нам нужно будет создать клиента Stripe, чтобы отслеживать покупки и активна ли подписка. Мы могли бы сделать это, когда пользователь совершит свою первую покупку, однако мы не знаем, будет ли это когда он приобретет определенный курс или активирует свою подписку. Это потребует от нас добавления некоторой логики в каждый из наших платежных сценариев, чтобы сначала проверить, существует ли полосатый пользователь, прежде чем списывать средства с его учетной записи. Мы можем значительно упростить эту логику, просто создав клиента Stripe одновременно с нашим пользователем Prisma — при первом входе нового пользователя в наше приложение.
Давайте изменим наш хук авторизации, чтобы создать полосатого клиента, прежде чем мы создадим пользователя в Prisma. Таким образом, мы можем использовать только что созданный Stripe ID для создания нашего пользователя.
// pages/api/auth/hooks.js
// other imports
import initStripe from 'stripe'
const stripe = initStripe(process.env.STRIPE_SECRET_KEY)
module.exports = async (req, res) => {
// other auth code
const customer = await stripe.customers.create({
email,
})
const user = await prisma.user.create({
data: { email, stripeId: customer.id },
})
}
Весь файл должен выглядеть примерно так.
// pages/api/auth/hooks.js
import { PrismaClient } from '@prisma/client'
import initStripe from 'stripe'
const prisma = new PrismaClient()
const stripe = initStripe(process.env.STRIPE_SECRET_KEY)
module.exports = async (req, res) => {
try {
const { email, secret } = JSON.parse(req.body)
if (secret === process.env.AUTH0_HOOK_SECRET) {
const customer = await stripe.customers.create({
email,
})
const user = await prisma.user.create({
data: { email, stripeId: customer.id },
})
console.log('created user')
} else {
console.log('You forgot to send me your secret!')
}
} catch (err) {
console.log(err)
} finally {
await prisma.$disconnect()
res.send({ received: true })
}
}
Отлично, теперь каждый раз, когда новый пользователь входит в систему, мы должны создавать клиента Stripe, а затем пользователя Prisma, у которого есть ссылка на идентификатор клиента.
Зарядка карты с помощью Stripe
Теперь мы хотим создать бессерверную функцию, которая может обрабатывать платежи за определенный курс. Нам нужно будет сообщить этой функции, какой курс покупает пользователь, поэтому мы будем использовать Dynamic API Route для передачи идентификатора курса. Давайте создадим новую бессерверную функцию в `/pages/api/charge-card/[courseId].js`.
// pages/api/charge-card/[courseId].js
module.exports = async (req, res) => {
const { courseId } = req.query
res.send(`charging card for course ${courseId}`)
}
Вы можете активировать эту бессерверную функцию, перейдя по адресу `https://localhost:3000/api/charge-card/any-value-you-want`. В этом случае он должен распечатать «зарядную карту для курса любой стоимости, которую вы хотите.
Следующим шагом будет определение того, сколько нам нужно взимать за курс. Мы могли бы просто передать это вместе с запросом от внешнего интерфейса, однако пользователь может легко изменить это.
Мы не можем ничему доверять от клиента!
Давайте позвоним в нашу базу данных Prisma, чтобы узнать реальную цену.
// pages/api/charge-card/[courseId].js
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
module.exports = async (req, res) => {
const { courseId } = req.query
const course = prisma.course.findUnique({
where: {
id: parseInt(courseId),
},
})
await prisma.$disconnect()
res.send(`charging ${course.price} cents for ${courseId}`)
}
Здесь мы используем parseInt(), чтобы преобразовать строку, полученную из запроса req, в целое число, которое Prisma ожидает для идентификатора.
Далее мы хотим узнать, кто пользователь, покупающий этот курс. Это означает, что мы хотим, чтобы маршрут API был доступен только зарегистрированным пользователям. Давайте завернем его в `withApiAuthRequired` и выясним, кем является пользователь, по электронной почте его сеанса.
// pages/api/charge-card/[courseId].js
import { PrismaClient } from '@prisma/client'
import { withApiAuthRequired, getSession } from '@auth0/nextjs-auth0';
const prisma = new PrismaClient()
module.exports = withApiAuthRequired(async (req, res) => {
const { courseId } = req.query
const { user: { email } } = getSession(req, res)
const course = prisma.course.findUnique({
where: {
id: parseInt(courseId),
},
})
const user = await prisma.user.findUnique({
where: {
email,
},
})
await prisma.$disconnect()
res.send(`charging ${user.email} ${course.price} cents for ${courseId}`)
})
Затем мы хотим сообщить Stripe, сколько мы на самом деле взимаем с клиента. Мы делаем это, создавая список позиций и платежную сессию.
// pages/api/charge-card/[courseId].js
//other imports
import initStripe from 'stripe'
const stripe = initStripe(process.env.STRIPE_SECRET_KEY)
module.exports = async (req, res) => {
// course and user stuff
const lineItems = [
{
price_data: {
currency: 'aud', // swap this out for your currency
product_data: {
name: course.title,
},
unit_amount: course.price,
},
quantity: 1,
},
]
const session = await stripe.checkout.sessions.create({
customer: user.stripeId,
payment_method_types: ['card'],
line_items: lineItems,
mode: 'payment',
success_url: `${process.env.CLIENT_URL}/success`,
cancel_url: `${process.env.CLIENT_URL}/cancelled`,
})
res.json({ id: session.id })
})
Нам нужно обеспечить успех и отменить URL-адрес полосы, чтобы перенаправить пользователя. Их нужно будет создать в `pages/success.js` и `pages/cancelled.js`. Кроме того, нам нужно создать переменную среды для CLIENT_URL. Выполните предыдущие шаги, чтобы добавить это в .env со значением `https://localhost:3000`, и новый секрет в Vercel со значением вашего размещенного URL-адреса — мой `https://courses-saas .vercel.приложение`.
Наконец, мы хотим обернуть все это в блок try/catch на случай, если что-то пойдет не так. Весь файл должен выглядеть примерно так.
// pages/api/charge-card/[courseId].js
import { withApiAuthRequired, getSession } from '@auth0/nextjs-auth0';
import { PrismaClient } from '@prisma/client'
import initStripe from 'stripe'
const prisma = new PrismaClient()
const stripe = initStripe(process.env.STRIPE_SECRET_KEY)
module.exports = withApiAuthRequired(async (req, res) => {
try {
const { courseId } = req.query
const { user: { email } } = getSession(req, res)
const course = prisma.course.findUnique({
where: {
id: parseInt(courseId),
},
})
const user = await prisma.user.findUnique({
where: {
email,
},
})
const lineItems = [
{
price_data: {
currency: 'aud', // swap this out for your currency
product_data: {
name: course.title,
},
unit_amount: course.price,
},
quantity: 1,
},
]
const session = await stripe.checkout.sessions.create({
customer: user.stripeId,
payment_method_types: ['card'],
line_items: lineItems,
mode: 'payment',
success_url: `${process.env.CLIENT_URL}/success`,
cancel_url: `${process.env.CLIENT_URL}/cancelled`,
})
res.json({ id: session.id })
} catch (err) {
res.send(err)
} finally {
await prisma.$disconnect()
}
})
Далее нам нужно добавить функцию в нашем интерфейсе для запуска этого платежа. Этот блок может быть запущен нажатием кнопки в любом месте приложения, и для инициирования платежа с помощью Stripe необходимо просто передать идентификатор курса.
import { loadStripe } from "@stripe/stripe-js";
import axios from 'axios'
const processPayment = async (courseId) => {
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY);
const { data } = await axios.get(`/api/charge-card/${courseId}`);
await stripe.redirectToCheckout({ sessionId: data.id });
}
Наконец, мы хотим знать, когда курс был куплен, чтобы мы могли обновить нашего пользователя в Prisma. Это стало возможным благодаря вебхукам Stripe. Как и в случае с нашим хуком Auth0, мы можем подписаться на определенные события, и когда это произойдет, Stripe вызовет нашу бессерверную функцию и сообщит нам, какой пользователь приобрел определенный курс.
Мы получаем много данных от Stripe о самой транзакции, но не о том, какой курс или пользователь Prisma. Давайте изменим нашу функцию платежной карты, чтобы передать это как метаданные с сеансом.
// pages/api/charge-card/[courseId].js
const session = await stripe.checkout.sessions.create({
// other session stuff
payment_intent_data: {
metadata: {
userId: user.id,
courseId,
},
},
})
Весь файл должен выглядеть примерно так.
// pages/api/charge-card/[courseId].js
import { withApiAuthRequired, getSession } from '@auth0/nextjs-auth0';
import { PrismaClient } from '@prisma/client'
import initStripe from 'stripe'
const prisma = new PrismaClient()
const stripe = initStripe(process.env.STRIPE_SECRET_KEY)
module.exports = withApiAuthRequired(async (req, res) => {
try {
const { courseId } = req.query
const { user: { email } } = getSession(req, res)
const course = prisma.course.findUnique({
where: {
id: parseInt(courseId),
},
})
const user = await prisma.user.findUnique({
where: {
email,
},
})
const lineItems = [
{
price_data: {
currency: 'aud', // swap this out for your currency
product_data: {
name: course.title,
},
unit_amount: course.price,
},
quantity: 1,
},
]
const session = await stripe.checkout.sessions.create({
customer: user.stripeId,
payment_method_types: ['card'],
line_items: lineItems,
mode: 'payment',
success_url: `${process.env.CLIENT_URL}/success`,
cancel_url: `${process.env.CLIENT_URL}/cancelled`,
payment_intent_data: {
metadata: {
userId: user.id,
courseId,
},
},
})
res.json({ id: session.id })
} catch (err) {
res.send(err)
} finally {
await prisma.$disconnect()
}
})
Теперь мы можем создать маршрут API, который будет обрабатывать эти события из Stripe.
// pages/api/stripe-hooks
export default async (req, res) => {
// check what kind of event stripe has sent us
res.send({ received: true })
}
Чтобы не столкнуться с той же проблемой, что и с хуками Auth0, давайте реализуем секрет подписи, чтобы подтвердить, что запрос исходит от Stripe.
Давайте сначала установим Stripe CLI, чтобы иметь возможность имитировать событие веб-перехватчика. Если у вас установлены macOS и homebrew, мы можем запустить эту команду.
brew install stripe/stripe-cli/stripe
Теперь запустите следующее, чтобы аутентифицировать CLI с помощью Stripe.
stripe login
Теперь у нас должна быть возможность запустить следующее, чтобы перенаправить события веб-перехватчика на наш локальный хост.
stripe listen --forward-to localhost:3000/api/stripe-hooks
Это распечатает секрет подписи на терминал. Скопируйте это в свой файл .env с именем STRIPE_SIGNING_SECRET.
// .env
// other secrets
STRIPE_SIGNING_SECRET=your-webhook-signing-secret
Stripe предоставляет удобную вспомогательную функцию под названием constructEvent, которая может подтвердить, был ли этот запрос отправлен от них. К сожалению, нам нужно немного поработать, чтобы это заработало в Next.js. Вот действительно хорошее руководство, которое описывает процесс.
Начнем с установки `micro`.
npm i micro
Теперь мы можем обновить маршрут API-интерфейса stripe-hooks, чтобы убедиться, что запрос исходит от Stripe.
// pages/api/stripe-hooks
import initStripe from 'stripe'
import { buffer } from 'micro'
const stripe = initStripe(process.env.STRIPE_SECRET_KEY)
export const config = { api: { bodyParser: false } }
export default async (req, res) => {
const reqBuffer = await buffer(req)
const signature = req.headers['stripe-signature']
const signingSecret = process.env.STRIPE_SIGNING_SECRET
let event
try {
event = stripe.webhooks.constructEvent(reqBuffer, signature, signingSecret)
} catch (err) {
console.log(err)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
// check what kind of event stripe has sent us
res.send({ received: true })
}
Объект `req` из Vercel структурирован не так, как ожидает Stripe, поэтому он не будет должным образом проверяться, если мы не проделаем небольшую работу.
stripe.webhooks.constructEvent() — это функция, которую Stripe рекомендует использовать для подтверждения отправки запроса. Если он может подтвердить это, он возвращает событие Stripe, в противном случае он выдает исключение, и мы возвращаем код состояния 400. Подробнее здесь.
Итак, теперь мы можем забыть об этой проверке и сосредоточиться на обработке события, которое мы получаем от Stripe.
// pages/api/stripe-hooks
export default async (req, res) => {
// signing logic
switch (event.type) {
case 'charge.succeeded':
// update user in prisma
console.log('charge succeeded')
break
default:
console.log(`Unhandled event type ${event.type}`)
}
}
event.type будет содержать строку для инициированного события. Мы расширим это позже для подписок, поэтому используйте оператор case, чтобы не запутаться.
Мы можем проверить, работает ли это, выполнив следующую команду в новом окне терминала — для этого требуются команды `stripe listen` и `npm run dev`.
stripe trigger charge.succeeded
Это должно вывести на консоль «заряд выполнен успешно».
Затем нам нужно извлечь пользователя и идентификатор курса из метаданных и обновить курсы пользователя, которые они приобрели в Prisma.
// pages/api/stripe-hooks
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default async (req, res) => {
// signing logic
const { metadata } = event.data.object
switch (event.type) {
case 'charge.succeeded':
// update user in prisma
if (metadata?.userId && metadata?.courseId) {
const user = await prisma.user.update({
where: {
id: parseInt(metadata.userId)
},
data: {
courses: {
connect: {
id: parseInt(metadata.courseId)
}
}
},
})
}
break
default:
console.log(`Unhandled event type ${event.type}`)
}
}
`connect` используется для вставки идентификатора существующего курса в массив курсов для пользователя. Если бы мы хотели создать этот курс, мы бы использовали «создать».
Полный файл должен выглядеть примерно так.
// pages/api/stripe-hooks
import initStripe from 'stripe'
import { buffer } from 'micro'
import { PrismaClient } from '@prisma/client'
const stripe = initStripe(process.env.STRIPE_SECRET_KEY)
const prisma = new PrismaClient()
export const config = { api: { bodyParser: false } }
export default async (req, res) => {
const reqBuffer = await buffer(req)
const signature = req.headers['stripe-signature']
const signingSecret = process.env.STRIPE_SIGNING_SECRET
let event
try {
event = stripe.webhooks.constructEvent(reqBuffer, signature, signingSecret)
} catch (err) {
console.log(err)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
const { metadata } = event.data.object
switch (event.type) {
case 'charge.succeeded':
// update user in prisma
if (metadata?.userId && metadata?.courseId) {
const user = await prisma.user.update({
where: {
id: parseInt(metadata.userId)
},
data: {
courses: {
connect: {
id: parseInt(metadata.courseId)
}
}
},
})
}
break
default:
console.log(`Unhandled event type ${event.type}`)
}
res.send({ received: true })
}
Теперь у нас должно быть законченное решение, в котором мы можем инициировать оплату определенного курса в нашем приложении — нам нужно сделать это из приложения, а не из интерфейса командной строки, чтобы оно включало наши метаданные. Это сделает запрос к нашей бессерверной функции платежной карты для создания сеанса оплаты для этого курса. Затем пользователь должен перейти в пользовательский интерфейс Stripe, где он может ввести данные своей кредитной карты, а затем после того, как с него будет снята оплата, он будет перенаправлен на нашу страницу успеха. В фоновом режиме Stripe вызовет нашу бессерверную функцию веб-перехватчика, которая сообщит нашему пользователю Prisma о недавно приобретенном курсе!
Удивительный! И нашему приложению не нужно ничего знать о данных кредитной карты наших пользователей!
Документация Stripe фантастическая, и я настоятельно рекомендую проверить все удивительные вещи, которые вы можете сделать помимо того, что мы рассматриваем в этой серии!
Подписывайтесь на меня
"Веб-сайт"
"YouTube"