Сбор платежей

  • Интегрируйте сеансы оплаты, чтобы принимать разовые платежи в вашем приложении
  • Внедрить систему выставления счетов по подписке для моделей с регулярным поступлением доходов
  • Настройте обработчики веб-хуков для надежной проверки платежей
  • Применять передовые методы обеспечения безопасности платежей
  • Алгоритм оформления заказа: кнопка «Оплатить», при нажатии на которую открывается защищенная страница оплаты
  • Оплата подписки: варианты разового и регулярного платежа
  • Обработчик веб-хуков — безопасный и надежный механизм обработки платежей в вашем MVP
  • Студенты разработали Ценовая стратегия примерный бюджет для своего проекта.
  • У студентов есть репозиторий MVP на GitHub с средой Codespaces.
  • Студенты реализовали аутентификацию пользователей в своей минимальной работоспособной версии (MVP).

Ваше приложение работает. Пользователи протестировали его, а вы устранили ошибки. Вы развернули его на такой платформе, как Vercel , так что оно доступно для всех. Теперь возникает вопрос, с которым сталкивается каждый стартап: 

Как мы получаем оплату?

На уроке по финансовой грамотности вы составили простой бюджет с прогнозами доходов и выбрали Ценовая стратегия своего проекта. Вы рассчитали, сколько пользователей вам потребуется, какую цену установить и когда вы планируете выйти на уровень безубыточности. Эти цифры были приблизительными. В этом разделе вы превратите эти приблизительные расчеты в реальную платежную систему.

Примечание: для участия в этом акселераторе прием платежей не является обязательным. Однако это следует рассматривать как необходимый следующий шаг.

Мы будем использовать Stripe— платежную платформу, которой пользуются компании в более чем 40 странах. Stripe сложные и ответственные аспекты обработки платежей, такие как проверка карт, выявление мошенничества, конвертация валют, а также обеспечение соответствия нормативным требованиям и безопасности — чтобы вам не пришлось этим заниматься. Ваша задача — подключить приложение к Stripe, указать сумму списания и обработать результат.

Stripe компании в более чем 40 странах, однако доступность сервиса зависит от региона. Если Stripe в вашей стране, у вас есть другие варианты. Проверьте доступность сервиса в вашем регионе и ознакомьтесь с альтернативными решениями, которые хорошо себя зарекомендовали. Вот несколько предложений:

Шаблоны кода, представленные в этом разделе — создание сеансов оформления заказа, веб-хуки и выставление счетов по подписке — работают одинаково на большинстве платежных платформ, поэтому полученные здесь знания можно сразу применить на практике. Выполняйте задания, чтобы освоить основные концепции, а затем адаптируйте их с помощью документации вашей платформы.

На протяжении всего этого раздела мы будем по-прежнему использовать TutorMatch в качестве примера для изучения. В этом разделе в TutorMatch добавляется возможность оплачивать отдельные занятия с репетитором или оформлять подписку на месячный тарифный план.

Вы будете следовать примерам из TutorMatch, адаптируя эти шаблоны для своего собственного MVP.

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

Начало работы с платформой для платежей

В предыдущих уроках вы настроили GitHub в качестве хранилища кода. Вы также настроили Supabase в качестве облачной базы данных. Теперь вам предстоит настроить Stripe или альтернативную платформу, в зависимости от страны) в качестве платежной системы. Алгоритм действий остаётся прежним:

  1. создать аккаунт
  2. получить ключи API
  3. подключите его к своему приложению
stripe logo

Stripe для платежей Stripe , что Supabase для хранения данных в базах: он берет на себя все сложные задачи, чтобы вы могли сосредоточиться на своем продукте. Когда пользователь оплачивает покупку через Stripe, Stripe:

  • Проверяет номер карты и выявляет попытки мошенничества
  • Шифрует платежные данные, чтобы они никогда не попадали в чужие руки
  • Перечисляет деньги на ваш счет (за вычетом комиссии за транзакцию)
  • Обеспечивает конвертацию валюты, если пользователь оплачивает покупку в другой валюте
  • Обеспечивает соблюдение нормативных требований в каждой стране

Без такой платформы, как Stripe, вам пришлось бы создавать всё это самостоятельно, что заняло бы месяцы и потребовало бы прохождения сертификации по безопасности. Stripe все эти сложности, благодаря чему вы можете интегрировать платежи в своё приложение с помощью небольшого объёма кода.

Существует несколько платформ для оплаты. Вот как они сравниваются с точки зрения разработчика, создающего MVP:

Платформа

Настройка

Начните бесплатно

Комиссия за транзакцию

Лучше всего подходит для

Stripe

Простой

Да

~2,9 % + комиссия за каждую транзакцию (зависит от страны)

MVP, веб-приложения, подписки

PayPal

Простой

Да

~3,49 % + комиссия

Торговые площадки, защита прав покупателей

Square

Простой

Да

~2,6 % + комиссия

Оплата на месте, розничная торговля

Создайте свой собственный

Очень сложно

Н/Д

Нет (кроме расходов на сервер)

Ни в коем случае не рекомендуется для MVP

Для этих задач мы будем использовать Stripe потому что:

  • Он предназначен для разработчиков и интегрируется с Express.js (вашим бэкендом из предыдущих уроков)
  • Бесплатный тариф подходит для тестирования и разработки прототипов на ранних этапах без ежемесячной платы
  • Вы платите комиссию за транзакцию только при получении реальных платежей
  • Этот код работает в более чем 40 странах и с более чем 135 валютами
  • Он предлагает встроенную систему выставления счетов по подписке для моделей с регулярным поступлением дохода

Еще раз рекомендуем ознакомиться с лучшим вариантом обработки платежей в вашем регионе или стране и применить тот же алгоритм действий к выбранной вами платформе.

Подключение платежной платформы к вашему приложению

Существует несколько способов интеграции системы обработки платежей. Понимание доступных вариантов поможет вам сделать правильный выбор для вашего MVP.

Подход

Что это означает

Время настройки

Безопасность

Настройка

Лучше всего подходит для

Оформление заказа на сайте

Платформа размещает страницу оплаты. Вы перенаправляете пользователей на эту страницу.

~30 мин

«Самый высокий» — платформа берет все на себя

Низкий — внешний вид определяется платформой

Лучшие продукты и стартапы (наш выбор)

Встроенная корзина

Пользовательский интерфейс платформы отображается на вашей странице в виде компонента

~1 час

Высокий — платформа по-прежнему обрабатывает данные карт

Средний — подходит для макета вашей страницы

Приложения, для которых важна единообразие бренда

Элемент оплаты

Безопасный готовый компонент пользовательского интерфейса (iframe), предоставляемый платежной платформой, который вы «вставляете» в свой собственный код интерфейса.

~2 часа

Высокий — но при этом приходится управлять большим объемом кода

Высокий — полный контроль над макетом

Приложения, требующие уникального процесса оформления заказа

Пользовательская форма

Создавайте всё, начиная с scratch

Дни

Вы обеспечиваете соответствие требованиям PCI

Полный контроль

Не рекомендуется для MVP

Вы будете использовать Hosted Checkout. Вот почему:

Самый быстрый в сборке

Вы создаете сессию оформления заказа на своем сервере, а платежная платформа берет на себя все остальное — отображение страницы оплаты, проверку данных карты, вывод сообщений об ошибках и формирование квитанции.

Наиболее безопасный

Ваше приложение никогда не обрабатывает номера карт. Платформа собирает их на своей собственной странице, а это значит, что вы автоматически обеспечиваете соответствие стандарту PCI.

Нам доверяют пользователи

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

Встроенные функции

Хостируемая страница Stripe автоматически поддерживает несколько валют, Apple Pay, Google Pay, UPI и другие способы оплаты в зависимости от местоположения клиента. Большинство платежных платформ обладают аналогичными функциями.

Получение первого платежа

Любая платежная система начинается с одной транзакции. В этом первом задании вы реализуете процесс оформления заказа в своём приложении.

В следующих инструкциях вы узнаете, как добавить в приложение кнопку «Оплатить», которая перенаправляет пользователей на хостинговую страницу оформления заказа Stripe— профессиональную и безопасную страницу, на которой пользователи вводят свои платежные данные.

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

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

Те из вас, кто использует альтернативную платежную платформу, могут адаптироваться к особенностям её работы, руководствуясь при этом соответствующей документацией.

 

Откройте свой «Простой бюджет » из урока по финансовой грамотности и ознакомьтесь со своей Ценовая стратегия».

Ответьте на следующие вопросы:

  • За что вы взимаете плату? (За товар, услугу, доступ к функции?)
  • Это разовый платеж или регулярный? (В данном упражнении мы начнем с разового платежа, а затем добавим подписки в « Задание 3»)
  • Сколько? (Используйте цену из ваших прогнозов доходов в Simple Budget)
  • Что происходит после оплаты? (Получает ли пользователь немедленный доступ? Приходит ли письмо с подтверждением? Можно ли скачать файл?)

В нашем примере с TutorMatch ответы следующие:

  • Стоимость: единовременная оплата за занятие с репетитором
  • Тип: разовый платеж (мы добавим подписки, добавленные в версии « Задание 3»)
  • Цена: адаптируйте к условиям вашего рынка 

Запишите свои ответы. Они понадобятся вам в следующем задании.

ПРИМЕЧАНИЕ: Для использования Stripe вам необходимо указать URL-адрес любого веб-сайта, профиля в социальной сети или мобильного приложения, которые вы используете для продвижения своего бизнеса или продажи товаров. Кроме того, вам потребуется указать банковский счет и подтвердить свою личность. При выполнении приведенного ниже задания вы можете пропустить этот шаг, однако для приема реальных платежей вам необходимо будет выполнить эти действия.

ЗАДАНИЕ 1

Настройка Stripe приема платежей

Примерное время: 20 минут

  1. Перейдите на сайт stripe.com и нажмите «Начать сейчас», чтобы создать бесплатную учетную запись, указав свой адрес электронной почты.
  2. Подтвердите свой адрес электронной почты и войдите в панель Stripe .
  3. Выберите в настройках оплаты вариант «Одноразовый платеж ». Вы можете изменить этот параметр в любое время.
  4. Пока выберите тестовую среду. Чтобы активировать аккаунт и начать принимать платежи, вам необходимо будет указать банковский счет, URL-адрес веб-сайта или мобильного приложения, а также подтвердить свою личность.
  5. Вы попадёте на главную страницу панели управления — уделите минутку, чтобы осмотреться.

Stripe — это ваш центр управления платежами. Вот основные разделы, которыми вам придется пользоваться:

  • Платежи: Просмотреть все транзакции (тестовые и реальные)
  • Подписки: управление периодическим списанием средств (эта функция будет доступна в версии « Задание 3»)
  • Разработчики → Ключи API: Ваши учетные данные API
  • Разработчики → Веб-хуки: уведомления об оплате на ваш сервер (настройку можно выполнить в « Задание , 2»)

Stripe вам два набора ключей API — аналогично ключам Supabase для публикации и секретным ключам. Ключ для публикации можно безопасно использовать в интерфейсе пользователя, а секретный ключ следует использовать только на серверной стороне.

  1. В панели Stripe нажмите «Разработчики » на левой боковой панели
  2. Нажмите «API-ключи»
  3. Скопируйте оба ключа — они вам понадобятся оба.

Тест тестовый в префиксе ключа означает, что вы находитесь в тестовом режиме. С вас не будет списано ни копейки. Когда вы будете готовы принимать реальные платежи, переключитесь на рабочие ключи (pk_live_, sk_live_).

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

В приложении TutorMatch мы планируем добавить возможность одноразовой оплаты занятий с репетитором и периодической оплаты подписки на приложение.

  1. Перейдите в раздел «Каталог продукции » в главном меню.
  2. Нажмите Создать товар.
    • Назовите свой продукт 
    • Добавить описание
    • Выберите «Периодически » или «Один раз»
    • Введите цену и выберите валюту
  3. Добавьте товар.
adding a stripe product
  1. Как только товар появится в списке, нажмите на него, чтобы просмотреть его.
  2. На строке с ценой нажмите на три точки и выберите «Копировать идентификатор цены».
stripe price id
  1. Сохраните идентификатор цены. Он понадобится вам при программировании приложения.

Добавьте все остальные позиции, за которые вы взимаете плату, не забыв скопировать идентификатор цены и указав, к какому товару он относится.

Теперь, когда у вас есть Stripe и настроены продукты, можно приступить к добавлению функции оплаты в ваше приложение. 

Прежде всего, вам следует проанализировать пользовательский путь в вашем приложении.

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

  1. Пользователь регистрируется или входит в систему.
  2. На главном экране появляется новая кнопка «Оплатить занятие с репетитором ».
  3. Пользователь нажимает кнопку и попадает на Stripe .
  4. Пользователь вводит платежные данные.
  5. Пользователь возвращается в приложение.
  6. Пользователь получает сообщение о том, что оплата подтверждена.
  7. Если пользователь отменяет платеж на Stripe :
    1. Они возвращаются в приложение
    2. Появляется сообщение об отмене

При выполнении следующих действий мы исходим из того, что вы развернули своё приложение с помощью Vercel. Это означает, что используется новая бессерверная архитектура, поэтому редактирование кода и его запуск в Codespace будут немного отличаться от прежних процедур. Раньше мы запускали фронтенд отдельно от бэкенда Express в Codespace. Два процесса, две среды.

С помощью бессерверного решения « Vercel »:

  • Серверный процесс не работает
  • Каждая функция в файле api/index.js запускается по требованию при поступлении соответствующего запроса
  • Vercel вставляет переменные среды из файла .env пользователя root ( в локальной среде) или из панели управления Vercel (в производственной среде) непосредственно в каждый вызов функции
  • Фронтенд (Vite) также считывает данные из того же корневого файла .env, но в браузер передаются только переменные с префиксом VITE_. Все остальные переменные среды защищены. 
ЗАДАНИЕ 2

Принимайте платежи в своем приложении

Примерная продолжительность: 45 минут

Прежде чем приступить к написанию кода, вам предстоит изучить собственные примеры приложений Stripeс открытым исходным кодом. Это эталонные реализации, опубликованные на сайте github.com/stripe-samples — те же шаблоны кода, которые Stripe для производственных приложений. Изучение этих примеров даст вам представление об архитектуре интеграции платежей, что ускорит разработку вашей собственной реализации и станет отправной точкой для отладки в случае возникновения неполадок.

Прежде чем приступать к написанию кода, ознакомьтесь с тем, как инженеры Stripeреализуют интеграцию платежей.

  1. Откройте этот репозиторий в браузере: github.com/stripe-samples/accept-a-payment
  2. Перейдите в папку prebuilt-checkout-page и ознакомьтесь с двумя файлами:
    • Сервер ( server/node/server.js) — всего около 100 строк. Основной шаблон:
				
					// The sample creates a Checkout Session on the server
app.post('/create-checkout-session', async (req, res) => {
  const session = await stripe.checkout.sessions.create({
    mode: 'payment',
    line_items: [{ price: process.env.PRICE, quantity: 1 }],
    success_url: `${domainURL}/success.html?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${domainURL}/canceled.html`,
  });
  return res.redirect(303, session.url);
});

				
			
    • Клиент — две разные части
      • Триггером оплаты является одна кнопка в форме.
				
					<form  action="/ru/create-checkout-session/" method="POST">
  <button id="submit">Buy</button>
</form>
				
			
      • Главный файл App.jsx должен обрабатывать ответы от Stripe.
				
					
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/success.html" element={<Success />} />
        <Route path="/canceled.html" element={<Canceled />} />
        <Route path="/" element={<Checkout />} />
      </Routes>
    </BrowserRouter>
  );
}
				
			

Вот и весь код кнопки оплаты, выполняемый на стороне клиента. Алгоритм работы следующий:

  • ваше приложение отправляет запрос на ваш сервер →
  • ваш сервер создает сеанс оформления заказа →
  • Stripe хостинговую страницу оплаты →
  • пользователь перенаправляется туда →
  • После оплаты они возвращаются в ваше приложение →
  • приложение перенаправляет пользователя в зависимости от того, завершилась ли операция успешно или была отменена

Обратите внимание на три особенности этой архитектуры:

  • Ваше приложение не имеет доступа к данным кредитных карт. Данные для оплаты собираются на хостинговой странице оформления заказа Stripe. Благодаря этому вы автоматически обеспечиваете соответствие стандарту PCI — вам не нужно беспокоиться о соблюдении стандартов безопасности при работе с кредитными картами.
  • Сервер определяет стоимость. Ассортимент, цена и валюта устанавливаются на стороне сервера, где пользователи не могут их изменить.
  • URL-адрес страницы подтверждения содержит идентификатор сеанса. Это позволяет странице подтверждения получить данные об оплате и отобразить соответствующее подтверждение.

Это ваш эталонный код. Если при настройке собственной Stripe что-то не работает, вы можете сравнить свой код с этим примером, чтобы найти различия.

Хотя вы можете добавить свои ключи в файл .env в Codespace, добавьте все, что можно, в Codespace Secrets для вашего репозитория. 

  1. Перейдите на сайт GitHub и нажмите на свой профиль в правом верхнем углу.
  2. Нажмите « Настройки»
  3. В левом меню нажмите « Codespaces».
  4. Нажмите «Новый секрет».
  5. Введите свой STRIPE_SECRET_KEY, скопировав ключ, полученный от Stripe.
  6. Добавьте ещё один секретный ключ для вашего PRICE_ID. Присвойте ему понятное название. Например, для Tutormatch наш секретный ключ называется STRIPE_PRICE_ID_TUTOR.
  1. Перейдите в свой репозиторий на GitHub и создайте новую ветку. Помните: теперь, когда вы выполнили развертывание через Vercel, любую новую разработку следует вести в ветке, отдельной от основной.
  2. Откройте свой Codespace (или создайте новый) и перейдите в новую ветку.

При развертывании приложения вы добавили файл vite.config.js , чтобы обеспечить возможность локальной разработки как с фронтенд-сервером, так и с бэкенд-сервером. Теперь, когда мы перешли на бессерверную архитектуру с помощью Vercel, мы удалим код прокси из файла vite.config.js.

Обновите файл. Вот весь код, который сейчас нужен:

				
					import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({ 
    plugins: [react()], 
})
				
			

Для обработки платежей вам понадобятся некоторые переменные среды. Stripe знать URL-адрес, на который следует перенаправить пользователя в случае успешного проведения платежа. Это наш основной URL-адрес интерфейса.

  1. Установите пакет « vercel », чтобы запустить среду разработки vercel . В окне терминала введите:
    npm install -g vercel
    vercel link
  2. Вам будет предложено подключить свою учетную запись GitHub и репозиторий.
    Нажмите «Да», чтобы подключить проект и загрузить переменные среды.
  3. Возможно, вам также понадобится выполнить команду ` npm install`, чтобы заново установить модули Node.js в вашем кодовом пространстве.
  4. в вашем root .env файле добавьте следующую строку:
    FRONTEND_URL=your_github_URL
    Мы можем использовать файл корневой файл .env , так как мы перешли на бессерверную архитектуру Vercel . Запустите приложение, чтобы получить URL-адрес на GitHub и вставить его сюда.

    ВАЖНО: Удалите символ «/» в конце URL-адреса!

  5. Несмотря на то, что мы добавили Stripe и price_id в «Секреты Codespace», давайте добавим их в .env а также для тестирования. Также добавьте
    STRIPE_SECRET_KEY=ваш_секретный_ключ_Stripe
    STRIPE_PRICE_ID_TUTOR=идентификатор_вашей_платежной_цены 
    до .env. Присвойте PRICE_ID подходящее имя.

    Можно смело добавить свой Stripe и price_id в корневой каталог .env. Пока переменная не содержит префикса VITE_, она остается защищенной.

    Убедитесь, что значения переменных среды совпадают в Codespace Secrets и .env. Codespace Secrets имеет приоритет над .env. Если вы обновляете ключ в одном месте, обновите его и в другом — несовпадение значений часто становится причиной ошибок, которые сложно отлаживать.

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

  1. В окне терминала введите команду npm install react-router-dom

Здесь много всего нужно учесть, поэтому мы будем действовать постепенно, в частности, попросим Copilot или другой LLM) сгенерировать код за вас. 

  1. Попросите LLM обновить бэкэнд api/index.js (или как бы ни назывался файл вашего бэкэнд-сервера). Помните, что при переходе в производственную среду мы переносим код бэкэнда сюда.
    Мы попросим LLM написать POST-запрос к Stripe, указав ему спецификации вашей конкретной структуры оплаты.

Добавить конечную точку Stripe на мой сервер Express в файле api/index.js.

  • Используйте STRIPE_SECRET_KEY из файла .env
  • Создайте маршрут POST /api/create-checkout-session.
  • Здесь следует использовать priceid, взяв значение STRIPE_PRICE_ID_TUTOR из файла .env
  • Используйте команду `stripe.checkout.sessions.create` со следующими параметрами:
    • line_items: массив, содержащий STRIPE_PRICE_ID_TUTOR из файла .env и количество: 1.
    • режим: установите значение «оплата» (поскольку это разовое занятие с репетитором).
    • success_url: Construct the base URL using:
      const baseURL = process.env.FRONTEND_URL || `https://${process.env.VERCEL_URL}`
      Then use baseURL + ‘/success’ for the success_url
    • cancel_url: Использовать baseURL (тот же базовый URL, без пути)
  •  Верните URL сессии в формате JSON, чтобы интерфейс мог перенаправить пользователя. Пока не вносите никаких изменений в код интерфейса.

Примечание: не усложняйте — пока никаких баз данных и кода Supabase.

Важно: НЕ указывайте payment_method_types (позвольте Stripe их динамически)

LLM должен обновить код в файле index.js. 

  1. Просмотрите изменения и утвердите их (или, при необходимости, попросите внести поправки или дать разъяснения).
  2. Попросите LLM обновить главный файл интерфейса App.jsx, чтобы он поддерживал обработку платежей.
  3. Copilot сделать это за вас, но вам необходимо установить Stripe: npm install stripe.

Примечание: Обратите внимание, как мы указали в файле Copilot создание baseURL для URL-адресов перенаправления в случае успешного выполнения и отмены. Мы указали, что следует использовать FRONTEND_URL или process.env.VERCEL_URL. FRONTEND_URL — это наша переменная среды, которая используется здесь, в Codespaces, для тестирования. При переходе в производственную среду перенаправление должно осуществляться на URL нашего приложения Vercel . Vercel автоматически создает эту переменную. Нам остаётся лишь добавить её в наш код, чтобы она работала как в тестовой, так и в производственной среде.

Проверьте файл index.js, чтобы убедиться, что в него включены переменные FRONTEND_URL и VERCEL_URL.

LLM должен обновить код в файле index.js. 

  1. Просмотрите изменения и утвердите их (или, при необходимости, попросите внести поправки или дать разъяснения).
  2. Попросите LLM обновить главный файл интерфейса App.jsx, чтобы он поддерживал обработку платежей.
  3. Copilot сделать это за вас, но вам необходимо установить Stripe: npm install stripe.

Обновите мой файл App.jsx для обработки платежей. Пожалуйста, добавьте следующее:

  • Обновить файл App.jsx для реализации базовой маршрутизации и обработки события оплаты.
  • Настройте два маршрута: / (Главная) и /success (Страница успеха).
  • На странице /, если пользователь авторизован, отобразить кнопку «Забронировать сеанс (60 $)».
  • Создайте функцию handlePay для этой кнопки. Она должна:
    • Получить Stripe из моего бэкенда (/api/create-checkout-session).
    • Перенаправить на URL-адрес, возвращенный сервером.
  • Если пользователь не авторизован, отобразить стандартную форму авторизации.
  1. промпт создать страницы «Успех» и «Отмена» после получения ответа от Stripe.

Создайте простую страницу React с именем Success.jsx и назначьте её маршрутом успешного выполнения.

  • Должно появиться сообщение: «Оплата прошла успешно!».
  • Добавьте краткое сообщение: «Спасибо за бронирование занятия с репетитором. В ближайшее время мы вышлем вам подробную информацию по электронной почте».
  • Добавьте кнопку «Вернуться на главную страницу», которая использует метод useNavigate из react-router-dom для перенаправления пользователя обратно на маршрут /

Создайте простую страницу React с именем Cancel.jsx.

  • Должно появиться сообщение примерно такого содержания: «Платеж отменен. С вашей карты не было списано».
  • Добавьте кнопку «Попробовать ещё раз», которая с помощью useNavigate возвращает пользователя на главную панель управления (/).
  • Сделайте интерфейс дружелюбным, чтобы пользователь не почувствовал, что он сделал что-то не так.
  1. Запустите приложение, введя в окне терминала команду `vercel dev`. Эта команда заменяет предыдущие команды ` npm run dev ` и запускает как фронтенд, так и бэкенд одновременно на одном порту.
  2. Нажмите кнопку «Оплатить сейчас». Вы будете перенаправлены на страницу оформления заказа, Stripe, где будут указаны название и цена вашего товара.
  3. Воспользуйтесь тестовой картой Stripeдля оформления оплаты:
    • Номер карты: 4242 4242 4242 4242
    • Срок действия: любая дата в будущем (например, 30.12)
    • CVC: любые 3 цифры (например, 123)
    • Имя: Любое имя
  4. После отправки вы должны попасть на страницу подтверждения, на которой вам будет выражена благодарность за оплату.

  5. Чтобы убедиться, что платеж прошел успешно, перейдите в Stripe и нажмите «Платежи». Там вы должны увидеть свой тестовый платеж.

  6. Попробуйте ещё раз. На этот раз отмените оплату на странице Stripe . 
  7. Вы должны перейти на страницу отмены, где сможете повторить попытку. 

После того как вы убедились, что процесс оплаты работает корректно, сохраните результаты своей работы — это хороший этап проверки перед добавлением веб-хуков.

Вы можете выполнить фиксацию с помощью панели «Управление версиями» (щелкните значок «Управление версиями» на левой боковой панели, введите сообщение о фиксации и нажмите на галочку) или с помощью терминала:

Веб-хуки

Процесс оформления заказа работает, и на странице подтверждения отображаются данные об оплате. Но вот в чём проблема: страница подтверждения не является надёжным способом подтверждения оплаты.

Подумайте, что может пойти не так:

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

Во всех этих случаях Stripe платеж, но ваше приложение об этом даже не узнало. Пользователь оплатил покупку, но на вашем сервере нет никаких записей об этом.

Веб-хуки решают эту проблему. Веб-хук — это сообщение, которое Stripe прямо на ваш сервер, когда происходит какое-то событие, например, завершение платежа. Вашему серверу не нужно спрашивать Stripe : «Они Stripe ?» — Stripe сам проактивно Stripe вашему серверу.

Понимание принципа работы веб-хуков

Предотвращение поддельных подтверждений оплаты имеет решающее значение. Без проверки через веб-хуки кто-то может отправить на ваш сервер запрос, в котором будет указано, что оплата произведена, хотя на самом деле это не так.

Вот как работают веб-хуки в вашем процессе оплаты:

Пользователь нажимает «Оплатить» → Stripe → Пользователь оплачивает → Происходит следующее:

  1. Пользователь видит страницу успешного завершения (ненадежно)
  2. Stripe веб-хук на ваш сервер (надежный)

Ваш сервер получает веб-хук независимо от того, какие действия выполняет пользователь в браузере. Именно поэтому веб-хуки являются надежным источником достоверной информации для подтверждения платежа.

ЗАДАНИЕ 3

Создать обработчик веб-хуков

Примерное время: 30 минут

Вам предстоит создать на сервере Express конечную точку, которая будет принимать подтверждения об оплате от Stripe, проверять их подлинность, а также тестировать сценарии как успешных, так и неудачных платежей.

Вам необходимо добавить конечную точку веб-хука на ваш сервер бэкэнда. К конечной точке веб-хука предъявляется особое требование: для проверки подписи ей требуется исходное тело запроса. Если Express сначала преобразует тело в формат JSON, проверка подписи завершится сбоем. 

Важно: если на вашем сервере используется строка app.use(express.json()), маршрут веб-хука должен быть определен до этой строки. В противном случае Express преобразует тело запроса в формат JSON до того, как его увидит обработчик веб-хука, и проверка подписи завершится сбоем.

  1. Откройте панель Copilot и задайте вопрос:

Добавить конечную точку Stripe на мой сервер Express в файле api/index.js.

Требования:

– Конечная точка POST по адресу /webhook

– Используйте для этого маршрута только промежуточный модуль express.raw() (а не express.json())

– Проверить подпись веб-хука с помощью STRIPE_WEBHOOK_SECRET

– Обработка событий checkout.session.completed и checkout.session.expired

– Записывать в журнал консоли данные об оплате (идентификатор сеанса, сумму, адрес электронной почты клиента)

– Отправьте код 200 в подтверждение получения

– Важно: для этого маршрута необходимо использовать сировый анализ тела сообщения, а не анализ JSON

  1. Проверьте файл index.js и при необходимости измените порядок. Маршрут веб-хука должен стоять первым.
				
					// 1. Webhook route FIRST (needs raw body)
app.post('/webhook', express.raw({ type: 'application/json' }), webhookHandler);

// 2. JSON parsing for all other routes AFTER
app.use(express.json());

// 3. Your other routes
app.post('/create-checkout-session', ...);
app.get('/success', ...);
				
			

Запуск веб-хука из Codespaces — дело непростое. Необходимо открыть доступ к порту вашего сервера, и даже в этом случае иногда могут возникать ошибки. Чтобы упростить тестирование, мы воспользуемся Stripe (интерфейсом командной строки) Stripe . Он создает туннель между вашим приложением и Stripe прямую связь.

  1. Установить Stripe в новом терминале Codespaces
    brew install stripe/stripe-cli/stripe
    или

    Если это не поможет (ручной способ):

    • Добавьте ключ: curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg –dearmor | sudo tee /usr/share/keyrings/stripe.gpg > /dev/null

    • Добавьте репозиторий: echo «deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main» | sudo tee /etc/apt/sources.list.d/stripe.list

    • Установка: sudo apt update && sudo apt install stripe

  2. Войти. Введите
    stripe login

  3. Вы получите код сопряжения и ссылку. Перейдите по ссылке, войдите в систему Stripe и разрешите подключение

  4. Проверьте, на каком порте работает ваше приложение. Перейдите на вкладку «Порты» и посмотрите, какой порт указан для вашего приложения. Скорее всего, это порт 3000, но может быть и другой. 
  5. Начните прослушивание. Введите:
    stripe listen –forward-to localhost:[ВАШ_ПОРТ]/api/webhook

  6. Как только он начнет прослушивать, вы увидите сообщение: «
    » Готово! Ваш секретный ключ для подписи вебхука: whsec_xxxxxxxx

  7. Скопируйте секретный ключ подписи и сразу же вставьте его в файл корневой файл .env , в поле STRIPE_WEBHOOK_SECRET.

  8. Перезапустите приложение, чтобы изменения вступили в силу.
  1. Проверьте, нажав кнопку «Оплатить сейчас» в приложении.
  2. Оформите оплату с помощью тестовой карты 4242 4242 4242 4242.
  3. Проверьте журнал консоли в терминале Codespace — там должно отобразиться следующее:
    Оплата подтверждена!
    Идентификатор сеанса: cs_test_…
    Сумма: 1500
    Электронная почта клиента: [email protected]
  4. Если это не отображается, проверьте, нет ли ошибок, и попросите Copilot код console.log присутствует и работает.

У реальных пользователей иногда бывают случаи, когда их карты не принимаются. Тестирование этого сценария поможет вам понять, что происходит при сбое платежа.

  1. Пройдите процесс оформления заказа ещё раз, но на этот раз используйте тестовую карту с отклонённой транзакцией:
    • Номер карты: 4000 0000 0000 0002
    • Прочие значения: то же, что и для допустимого платежа

На странице оформления заказа Stripe пользователю сообщение «Ваша карта была отклонена». Пользователь останется на странице Stripe и сможет попробовать использовать другую карту.

Обратите внимание на то, что происходит на вашем сервере: веб-хуки не срабатывают. Событие ` checkout.session.completed ` срабатывает только в случае успешного проведения платежа. Это нормальное поведение — ваш сервер должен реагировать только на успешные платежи

Зафиксируйте изменения с помощью панели управления версиями или терминала. Таким образом каждый этап реализации платежа будет зафиксирован отдельным коммитом.

ВАЖНО: Для тестирования в Codespaces мы использовали Stripe . При переходе на рабочую версию приложения вам необходимо настроить веб-хук в Stripe . Вам будет присвоен другой секретный ключ подписи, который нужно будет добавить в секретные данные вашей рабочей платформы, а также перенаправить веб-хук на URL рабочей версии. Инструкции приведены ниже.

  1. Перейдите в Stripe управления Stripe .
  2. В верхней части страницы найдите раздел «Webhooks». Нажмите на «Webhooks». Вы перейдете в Workbench.
  3. В Workbench перейдите на вкладку «Webhooks ».
  4. Нажмите на + Добавить пункт назначения кнопка.
    • События из: выберите «Оформление заказа» в раскрывающемся списке
    • Выбрать события: выберите checkout.session.completed и checkout.session.expired
    • Тип назначения: выберите конечную точку веб-хука.
    • URL-адрес конечной точки: вставьте URL-адрес вашей производственной среды (например, https://your-vercel.app/api/webhook).
  5. После создания конечной точки нажмите «Показать» в разделе «Секрет подписи», чтобы получить секретный ключ веб-хука (начинается с whsec_)
  6. Добавьте ключ в свои производственные секретные данные (по той же процедуре, что и для ключей Stripe ):
    • Название: STRIPE_WEBHOOK_SECRET
    • Значение: вставьте свой секретный ключ whsec_…

Вам не нужно ничего менять в своем коде.

Выставление счетов за подписку

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

Хорошая новость заключается в том, что для перехода с разовых платежей на подписки в Stripe лишь несколько изменений в уже написанный вами код. Процесс оформления заказа для пользователя останется практически неизменным. В этой части вы также узнаете, как в примере приложения Stripeдля работы с подписками представлены варианты ценообразования и как осуществляется управление подписками.

В следующем задании вы узнаете, как добавить в приложение возможность оплаты по подписке. Мы оставим вариант разовой оплаты (в нашем случае — за занятие с репетитором), но добавим второй тип оплаты — годовую подписку на доступ к функциям приложения.

ЗАДАНИЕ 4

Добавить оплату подписки

Примерная продолжительность: 45 минут

В приложении вы добавите второй тип оплаты — периодическую подписку. После успешного завершения операции вы обновите базу данных Supabase, чтобы пометить этого пользователя как оплатившего услугу.

  • Откройте этот репозиторий в браузере: github.com/stripe-samples/checkout-single-subscription

    Сайт код сервера (около 120 строк) очень похож на то, что вы создали в « Задание 1». Основные отличия заключаются в следующем:
    • режим «подписка» вместо «оплата»
    • В примере также предусмотрен конечный пункт «Портал для клиентов» — это позволяет подписчикам самостоятельно управлять своей подпиской (отменить, изменить тарифный план) без обращения к вам.

Добавить конечную точку Stripe на мой сервер Express.

Требования:

– Конечная точка POST по адресу /webhook

– Используйте для этого маршрута только промежуточный модуль express.raw() (а не express.json())

– Проверить подпись веб-хука с помощью STRIPE_WEBHOOK_SECRET

– Обработка событий checkout.session.completed и checkout.session.expired

– Записывать в журнал консоли данные об оплате (идентификатор сеанса, сумму, адрес электронной почты клиента)

– Отправьте код 200 в подтверждение получения

– Важно: для этого маршрута необходимо использовать сировый анализ тела сообщения, а не анализ JSON

  1. Проверьте файл server.js и при необходимости измените порядок. Маршрут веб-хука должен быть первым.
				
					// 1. Webhook route FIRST (needs raw body)
app.post('/webhook', express.raw({ type: 'application/json' }), webhookHandler);

// 2. JSON parsing for all other routes AFTER
app.use(express.json());

// 3. Your other routes
app.post('/create-checkout-session', ...);
app.get('/success', ...);
				
			
  1. Если вы еще этого не сделали, добавьте продукт по подписке в свою Stripe .
    1. Перейдите в раздел «Каталог продукции» в главном меню.

    2. Нажмите «Создать товар»
      • Назовите свой продукт 
      • Добавить описание
      • Выбрать повторяющийся
      • Введите цену и выберите валюту
    3. Добавьте товар.
    4. Как только товар появится в списке, нажмите на него, чтобы просмотреть его.
    5. На строке с ценой нажмите на три точки и выберите «Копировать идентификатор цены».
  1. Вернитесь в свою учетную запись GitHub и добавьте новый секретный ключ Codespace.
    1. В своём профиле нажмите «Настройки» → «Codespaces».
    2. Добавьте новый секрет и назовите его STRIPE_PRICE_ID_SUBSCRIPTION или другим подходящим именем.
    3. Вставьте идентификатор цены, скопированный из Stripe.
    4. Также перейдите в свой Codespace и добавьте его в файл .env . Эта резервная копия избавит вас от необходимости пересобирать Codespace для интеграции нового секрета Codespace. См. примечание ниже.

ПРИМЕЧАНИЕ: Возможно, вам потребуется пересобрать контейнер Codespace, чтобы он распознал новый секретный ключ. Это можно сделать в Codespace, нажав клавишу F1 для вызова панели команд и выбрав пункт «Codespaces: Rebuild Container». В качестве резервного варианта вы также можете добавить его в файл server/.env, чтобы не пересобирать контейнер в текущем сеансе.

Если мы собираемся взимать с пользователей плату за пользование нашим приложением, нам нужно отслеживать эти платежи в Supabase. В настоящее время пользователи могут регистрироваться с помощью аутентификации Supabase и входа через Google.

Мы не можем вмешиваться в процесс аутентификации пользователей Supabase, поэтому добавим новую таблицу «profiles» для хранения платежной информации.

  1. Перейдите в свой проект Supabase → «Аутентификация» → «Пользователи».
    Вы увидите всех пользователей, которые зарегистрировались в вашем приложении. 
  2. Перейдите в редактор SQL из главного меню.
  3. Вставьте эту SQL-команду, чтобы создать таблицу «profiles ». Она связывается с таблицей аутентификации пользователей с помощью идентификатора пользователя из этой таблицы.
				
					create table profiles (
  id uuid references auth.users not null primary key,
  email text,
  is_paid boolean default false
);
				
			

Необходимо создать таблицу профилей.

  1. Перейдите в редактор таблиц — там должна отобразиться пустая таблица профилей.
    Если у вас уже есть пользователи, необходимо выполнить команду, чтобы добавить этих пользователей в таблицу профилей.
  2. Вернитесь в редактор SQL и выполните эту команду.
				
					INSERT INTO public.profiles (id, email)
SELECT id, email 
FROM auth.users
ON CONFLICT (id) DO NOTHING;
				
			
  1. Вернуться в редактор таблиц.
  2. Убедитесь, что все пользователи добавлены в таблицу профилей.
  3. Посмотрите в верхней части экрана, есть ли там кнопка с надписью Политика RLS или RLS отключено.
    • Если отображается надпись «RLS отключено», нажмите на кнопку и выберите «Включить RLS для этой таблицы». 
    • Если там написано «Политика RLS», щелкните по ней. Там должно быть указано: «Пользователи могут обновлять свой профиль».
supabase screen with RLS disabled

Supabase необходимо знать, что при регистрации нового пользователя нужно создать новую строку в таблице «profiles». Это можно сделать, настроив триггер в Supabase.

  1. Перейдите в редактор SQL в Supabase и вставьте туда следующий код.
				
					create or replace function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id, email)
  values (new.id, new.email);
  return new;
end;
$$ language plpgsql security definer;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();
				
			

Необходимо создать триггер.

  1. Чтобы проверить, существует ли она, перейдите в меню «База данных» → «Функции ». Там должна быть указана функция handle_new_user .

Аутентификация пользователей в Supabase осуществлялась со стороны клиентского интерфейса с использованием ключа, доступного для публикации. Обновление таблицы профилей будет происходить со стороны серверного интерфейса, и для этого мы будем использовать секретный ключ Supabase. Это означает, что нам нужно добавить его в наше приложение в качестве переменной среды.

  1. Перейдите в свой проект Supabase и нажмите «Настройки проекта».
  2. Нажмите «API-ключи».
  3. Прокрутите страницу вниз и найдите секретный ключ. Скопируйте его.
  4. На панели управления Github перейдите в раздел «Профиль» → «Codespaces». 
  5. Добавьте новый секрет, присвоив емуимя SUPABASE_SERVICE_ROLE_KEY, и вставьте ключ в поле «Значение».
  6. Теперь добавьте SUPABASE_URL как ещё один секрет Codespace. Возможно, он уже есть в вашем корневом каталоге .env файле, но вы можете найти его в панели управления Supabase.
    1. Перейдите в «Настройки проекта» → «API данных». 
    2. Скопируйте URL-адрес API и вставьте его в качестве секретного значения в Codespace.
  7. Чтобы избежать необходимости пересобирать контейнер Codespace (см. примечание ниже), добавьте также обе эти переменные в корневой файл .env в Codespace.

ПРИМЕЧАНИЕ: Возможно, вам потребуется пересобрать контейнер Codespace, чтобы он распознал новые секретные данные. Это можно сделать в Codespace, нажав клавишу F1 для вызова панели команд и выбрав пункт «Codespaces: Rebuild Container». В качестве резервного варианта вы также можете добавить эти данные в файл server/.env, чтобы не пересобирать контейнер в текущем сеансе.

Мы будем действовать пошагово, чтобы Copilot добавил нужный код.

  1. Вернитесь в среду Codespace. Убедитесь, что все предыдущие изменения зафиксированы, чтобы начать работу с новой фиксацией.
  2. Попросите Copilot сервер. Вот пример промпт:

Сохраните существующий конечный пункт Stripe для индивидуальных занятий и добавьте новый конечный пункт для подписки в файл api/index.js.

Тип подписки, цена — STRIPE_PRICE_ID_SUBSCRIPTION

Copilot сгенерировать новый файл app/post в api/index.js. Нажмите ниже, чтобы увидеть код, сгенерированный для Tutormatch.

				
					app.post('/api/create-subscription-session', async (req, res) => {
  try {
    const priceId = process.env.STRIPE_PRICE_ID_SUBSCRIPTION;
    if (!priceId) {
      return res.status(400).json({ error: 'Subscription Price ID not configured' });
    }
    if (!process.env.FRONTEND_URL) {
      return res.status(500).json({ error: 'FRONTEND_URL not configured' });
    }
    const session = await stripe.checkout.sessions.create({
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      mode: 'subscription',
      success_url: process.env.FRONTEND_URL + '/success',
      cancel_url: process.env.FRONTEND_URL,
    });
    res.json({ url: session.url });
  } catch (error) {
    console.error('Stripe subscription error:', error);
    res.status(500).json({ error: error.message });
  }
});
				
			
  1. Попросите Copilot опцию подписки в обработчик веб-хука, чтобы он обновлял Supabase при переходе на платную подписку.

    Вот пример промпт:

Добавьте это в мой обработчик веб-хуков, чтобы он обрабатывал режим оплаты так же, как и сейчас.

Если используется режим подписки, обновите таблицу профилей в Supabase для этого пользователя. Установите значение is_paid равным true. SUPABASE_URL — это URL-адрес проекта, а секретный ключ — SUPABASE_SERVICE_ROLE_KEY

Также обработайте событие `subscription.deleted` и обновите профили этого пользователя, чтобы значение `is_paid` стало равным false.

Нажмите ниже, чтобы просмотреть код обработчика веб-хука Tutormatch. Обратите внимание: в коде много выводов console.log, которые позволяют убедиться в работоспособности веб-хука и получении платежей.

				
					app.post('/api/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const sig = req.headers['stripe-signature'];
    let event;

    try {
        // This verifies the message actually came from Stripe
        event = stripe.webhooks.constructEvent(
            req.body, 
            sig, 
            process.env.STRIPE_WEBHOOK_SECRET // This is your whsec_ key
        );
    } catch (err) {
        console.error(`Webhook Error: ${err.message}`);
        return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    // 2. HANDLE THE EVENT
      if (event.type === 'checkout.session.completed') {
          const session = event.data.object;
          const mode = session.mode;
          const email = session.customer_details?.email;

          console.log('Session mode:', mode);
          console.log('Session email:', email);
          console.log('Session details:', session);

          if (mode === 'payment') {
              console.log('Payment Successful!');
              console.log('Customer Email:', email);
              console.log('Amount Paid:', session.amount_total / 100); // Stripe uses cents
              // This is where you would update your database (e.g., User.paid = true)
          } else if (mode === 'subscription') {
              console.log('Subscription Successful!');
              console.log('Customer Email:', email);
              // Update Supabase profiles table: set is_paid true for this user
              if (email) {
                  const supabaseUrl = `${process.env.SUPABASE_URL}/rest/v1/profiles?email=eq.${encodeURIComponent(email)}`;
                  const supabaseHeaders = {
                      'Content-Type': 'application/json',
                      'apikey': process.env.SUPABASE_SERVICE_ROLE_KEY,
                      'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`,
                  };
                  const supabaseBody = JSON.stringify({ is_paid: true });
                  console.log('Supabase PATCH URL:', supabaseUrl);
                  console.log('Supabase PATCH Headers:', supabaseHeaders);
                  console.log('Supabase PATCH Body:', supabaseBody);
                  fetch(supabaseUrl, {
                      method: 'PATCH',
                      headers: supabaseHeaders,
                      body: supabaseBody
                  })
                  .then(res => {
                      console.log('Supabase PATCH response status:', res.status);
                      return res.json();
                  })
                  .then(data => {
                      console.log('Supabase update (is_paid true):', data);
                  })
                  .catch(err => {
                      console.error('Supabase update error:', err);
                  });
              } else {
                  console.log('No email found for subscription session. Supabase not updated.');
              }
          }
      } else if (event.type === 'checkout.session.expired') {
          const session = event.data.object;
          console.log('Session expired. Releasing the tutor slot for:', session.id);
      } else if (event.type === 'subscription.deleted') {
          const subscription = event.data.object;
          const email = subscription.customer_email || subscription.customer?.email;
          console.log('Subscription deleted. Email:', email);
          console.log('Subscription details:', subscription);
          // Update Supabase profiles table: set is_paid false for this user
          if (email) {
              const supabaseUrl = `${process.env.SUPABASE_URL}/rest/v1/profiles?email=eq.${encodeURIComponent(email)}`;
              const supabaseHeaders = {
                  'Content-Type': 'application/json',
                  'apikey': process.env.SUPABASE_SERVICE_ROLE_KEY,
                  'Authorization': `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}`,
              };
              const supabaseBody = JSON.stringify({ is_paid: false });
              console.log('Supabase PATCH URL:', supabaseUrl);
              console.log('Supabase PATCH Headers:', supabaseHeaders);
              console.log('Supabase PATCH Body:', supabaseBody);
              fetch(supabaseUrl, {
                  method: 'PATCH',
                  headers: supabaseHeaders,
                  body: supabaseBody
              })
              .then(res => {
                  console.log('Supabase PATCH response status:', res.status);
                  return res.json();
              })
              .then(data => {
                  console.log('Supabase update (is_paid false):', data);
              })
              .catch(err => {
                  console.error('Supabase update error:', err);
              });
          } else {
              console.log('No email found for deleted subscription. Supabase not updated.');
          }
      }

    // Return a 200 response to acknowledge receipt
    res.json({ received: true });
});
				
			

Теперь перейдем к интерфейсу. Мы разделили промпт две части. Сначала добавим кнопку для оплаты подписки, а затем реализуем логику включения/отключения функций в зависимости от того, оплачена ли подписка.

  1. Попросите Copilot новую кнопку оплаты подписки. Вот пример промпт.

В интерфейсе, в файле App.jsx, добавьте новую кнопку «Подписаться за 9,99 $ в год».
При нажатии на эту кнопку должен отправляться запрос POST с параметром create_subscription_session.

  1. Попросите Copilot логику, Copilot отключать функции, если пользователь не оплатил подписку, и включать их, если он оплатил.

Кнопки «Получить консультацию» и «Найти репетитора» должны быть отключены, если значение is_paid равно false, и включены, если оно равно true.
Добавьте информационную надпись «Разблокируйте эти функции, оформив подписку» и разместите её над двумя кнопками функций.

Нажмите ниже, чтобы посмотреть код интерфейса Tutormatch.

				
					function Home({ user, setUser }) {
  const [screen, setScreen] = useState('advice');
  const [isPaid, setIsPaid] = useState(false);
  const navigate = useNavigate();

  useEffect(() => {
    async function fetchProfile() {
      if (user && user.email) {
        try {
          const res = await fetch(`${import.meta.env.VITE_SUPABASE_URL}/rest/v1/profiles?email=eq.${encodeURIComponent(user.email)}`, {
            headers: {
              'apikey': import.meta.env.VITE_SUPABASE_KEY,
              'Authorization': `Bearer ${import.meta.env.VITE_SUPABASE_KEY}`,
            },
          });
          if (res.ok) {
            const data = await res.json();
            // Supabase may return booleans or truthy strings; normalize to boolean
            setIsPaid(Boolean(data[0]?.is_paid));
          } else {
            setIsPaid(false);
          }
        } catch {
          setIsPaid(false);
        }
      } else {
        setIsPaid(false);
      }
    }
    fetchProfile();
  }, [user]);

  const handlePay = async () => {
    try {
      const res = await fetch('/api/create-checkout-session', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
      });
      if (!res.ok) {
        const text = await res.text();
        alert('Payment error: ' + text);
        return;
      }
      const data = await res.json();
      if (data.url) {
        window.location.href = data.url;
      } else {
        alert('Could not start payment session.');
      }
    } catch (err) {
      alert('Payment error: ' + err.message);
    }
  };

    const handleSubscribe = async () => {
      try {
        const res = await fetch('/api/create-subscription-session', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
        });
        if (!res.ok) {
          const text = await res.text();
          alert('Subscription error: ' + text);
          return;
        }
        const data = await res.json();
        if (data.url) {
          window.location.href = data.url;
        } else {
          alert('Could not start subscription session.');
        }
      } catch (err) {
        alert('Subscription error: ' + err.message);
      }
    };

  const handleLogout = async () => {
    await supabase.auth.signOut();
    setUser(null);
  };

  return (
    <div className="app">
      <header className="header">
        <h1 className="title">TutorMatch</h1>
        <p className="subtitle">Get Personalized Tutor Advice</p>
      </header>
      {!user ? (
        <AuthForm onLogin={() => {
          supabase.auth.getSession().then(({ data }) => {
            setUser(data?.session?.user || null);
          });
        }} />
      ) : (
        <>
          <button onClick={handlePay} style={{ margin: '1rem', padding: '0.75rem 1.5rem', fontSize: '1.1rem', background: '#635bff', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>
            Book a Session ($60)
          </button>
          <button onClick={handleSubscribe} style={{ margin: '1rem', padding: '0.75rem 1.5rem', fontSize: '1.1rem', background: '#00b386', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>
            Subscribe for $9.99/year
          </button>
          {!isPaid && (
            <div style={{ margin: '1.5rem 0 0.5rem 0', color: '#b00', fontWeight: 500, fontSize: '1.05rem', textAlign: 'center' }}>
              Unlock these features by subscribing
            </div>
          )}
          <NavTabs
            screen={screen}
            setScreen={setScreen}
            onLogout={handleLogout}
            adviceDisabled={!isPaid}
            findTutorDisabled={!isPaid}
          />
          <main className="container">
            {screen === 'advice' ? <GetAdvice disabled={!isPaid} /> : <FindTutor disabled={!isPaid} />}
          </main>
        </>
      )}
    </div>
  );
}


function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const { subscription } = supabase.auth.onAuthStateChange((event, session) => {
      setUser(session?.user || null);
    });
    supabase.auth.getSession().then(({ data }) => {
      setUser(data?.session?.user || null);
    });
    return () => {
      subscription?.unsubscribe && subscription.unsubscribe();
    };
  }, []);

  return (
    <Routes>
      <Route path="/" element={<Home user={user} setUser={setUser} />} />
      <Route path="/success" element={<Success />} />
      <Route path="/cancel" element={<Cancel />} />
    </Routes>
  );
}

export default App

				
			
  1. Тестовый разовый платеж: выберите опцию «Разовый», введите данные тестовой карты 4242 4242 4242 4242 и завершите оплату
  2. Тестовая подписка: выберите вариант подписки, используйте ту же тестовую карту и оформите оплату
  3. Проверьте свой терминал — веб-хук должен регистрировать разные сообщения для каждого режима
  4. В панели Stripe перейдите в:
    • Оплата для просмотра вашего разового платежа
    • Подписки: просмотр новой подписки (этот раздел появляется после создания подписки)
  5. Перейдите на Supabase и проверьте таблицу профилей. У вашего пользователя, который произвёл оплату, значение is_paid должно быть равно true.
  6. Убедитесь в приложении, что у платного пользователя есть доступ к разблокированным функциям.
  7. Выйдите из системы и войдите в нее заново. Убедитесь, что у них по-прежнему есть доступ ко всем функциям.
  8. Выйдите из системы и войдите в качестве пользователя без подписки. Убедитесь, что доступ к функциям заблокирован.

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

Затем вы можете вернуться на GitHub и объединить свой код обработки платежей с основной веткой. Это должно привести к немедленному развертыванию ваших изменений на сайте Vercel.

Вы добавили несколько новых переменных среды в секреты Codespace. Вам необходимо добавить их в файл ` Vercel `, чтобы они продолжали работать в производственной среде.

  1. Перейдите на панель управления вашим проектом « Vercel ».
  2. Нажмите «Настройки» → «Переменные среды».
  3. Добавьте следующие переменные среды с соответствующими значениями:
    • STRIPE_SECRET_KEY
    • STRIPE_PRICE_ID_TUTOR (или как вы назвали свой идентификатор цены)
    • STRIPE_PRICE_ID_SUBSCRIPTION (или название вашего идентификатора цены)
    • STRIPE_WEBHOOK_SECRET
    • SUPABASE_URL
    • SUPABASE_SERVICE_ROLE_KEY

Вам не нужно добавлять FRONTEND_URL к адресу Vercel. Переменная среды VERCEL_URL автоматически устанавливается по адресу Vercel, и мы уже добавили её в код наших страниц «Успех» и «Отмена» (Задание 2).

Если вы ещё не сделали этого в уроке по развёртыванию, убедитесь, что URL-адрес Vercel добавлен в список разрешённых URL-адресов перенаправления Supabase — инструкции см. в уроке по развёртыванию.

Размышление

Обработка платежей — важная составляющая успеха вашего бизнеса, поэтому вам нужно убедиться, что она работает без сбоев. Задайте себе следующие вопросы:

Sunset and reflection over lake
01

Ценовая стратегия
Насколько удачно ваша первоначальная Ценовая стратегия , заложенная в бюджете, Ценовая стратегия в Stripe ? Пришлось ли вам что-то корректировать — цены, модель или структуру тарифных планов — после того, как вы увидели, как это работает с технической точки зрения?
02

Пример кода
Помог ли вам справочный код Stripe в понимании и реализации? Стали бы вы использовать этот подход — «сначала изучить, потом реализовать» — при других интеграциях?
03

Бизнес модель
Если проанализировать ваш «Простой бюджет», что будет более целесообразно для вашего проекта: разовый платеж, подписка или их сочетание?

Ключевые термины

  • Hosted Checkout — это платежная страница, Stripe, на которую ваше приложение перенаправляет пользователей. На этой странице Stripe платежные данные, благодаря чему ваше приложение не имеет доступа к данным банковских карт.

  • Сессия оформления заказа — объект на стороне сервера, создаваемый в Stripe определяет, что и на какую сумму следует списать, а также куда перенаправить пользователя после оплаты.

  • Вебхук — это HTTP-запрос, Stripe непосредственно на ваш сервер при возникновении события, связанного с оплатой, обеспечивая надежное подтверждение оплаты, независимо от действий пользователя в браузере.

  • express.raw() — промежуточное ПО Express, которое сохраняет тело запроса без изменений, что необходимо для правильной работы проверки подписи веб-хука.

  • Business Sandbox— Stripe , в которой платежи моделируются с использованием тестовых номеров карт, поэтому во время разработки с реальных счетов деньги не списываются.