Предоставьте пользователям SaaS доступ к платным функциям с помощью веб-крючков

Когда пользователь оплачивает вашу услугу, вы хотите предоставить ему доступ к платным функциям. Предоставление доступа иногда называют «инициализацией». Предоставление ролей или разрешений происходит после создания пользователя и настройки аутентификации. Аутентификация говорит нам, что пользователь является тем, за кого себя выдает. Теперь мы собираемся добавить авторизацию, которая говорит о том, что пользователю разрешен доступ или выполнение определенного действия.

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

Обзор аутентификации

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

В Rails принято иметь обратный вызов before_action, который гарантирует, что пользователь вошел в систему. Этот before_action применяется к контроллерам, которые обрабатывают запросы на аутентифицированный контент. Если вы не используете Ruby on Rails, вы найдете нечто подобное в своем веб-фреймворке, например, хук useUser в Remix или декоратор @login_required в Django.

# Define a method to be used as a callback on a base class
class ApplicationController < ActionController::Base
  def authenticate_user!
    if !logged_in?
      redirect_to "/login"
    end
  end
end

# Use the callback so that users cannot access any of the /account resources
# without being logged in.
class AccountsController < ApplicationController
  before_action :authenticate_user!
end
Вход в полноэкранный режим Выход из полноэкранного режима

Думайте об этом как о шаге на сервере, который спрашивает «знаю ли я, кто текущий пользователь?» и, если да, пропускает запрос. Если нет, запрос останавливается, и пользователь перенаправляется на страницу входа в систему.

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

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

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

Вариант A: Хранить некоторые данные о подписке в базе данных

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

Когда подписка создается впервые, уведомление о событии customer.subscription.created webhook будет отправлено вашему обработчику webhook. Поскольку мы храним ID пользователя в metadata подписки, мы можем найти пользователя по ID и установить ID подписки, ID цены и статус подписки.

case event.type
when 'customer.subscription.created', 'customer.subscription.updated', 'customer.subscription.deleted'
  subscription = event.data.object # contains a Stripe::Subscription object
  user = User.find_by(stripe_customer_id: subscription.metadata.user_id)
  user.update(
    stripe_subscription_id: subscription.id,
    stripe_subscription_status: subscription.status,
    stripe_price_id: subscription.items.data.first.id
  )
end
Вход в полноэкранный режим Выход из полноэкранного режима

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

  • Плюсы
    • Эффективно, если состояние подписки нужно проверять много раз, пока пользователь входит в систему (возможно, на каждой странице?).
  • Минусы
    • Увеличивает накладные расходы на управление состоянием

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

class User < ApplicationRecord
  def subscribed?
    ['trialing', 'active'].include?(stripe_subscription_status)
  end
end
class ApplicationController < ActionController::Base
  #...
  def require_active_subscription!
    if !current_user.subscribed?
      redirect_to '/pricing'
    end
  end
end
class BlogsController < ApplicationController
  before_action :require_active_subscription!
  #...
end
Вход в полноэкранный режим Выход из полноэкранного режима

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

class User < ApplicationRecord
  #...
  def startup?
    subscribed? && stripe_product_id == 'prod_abc123'
  end
  def enterprise?
    subscribed? && stripe_product_id == 'prod_xyz232'
  end
end
class ApplicationController < ActionController::Base
  #...
  def require_startup!
    if !current_user.startup? || !current_user.enterprise?
      redirect_to '/pricing'
    end
  end

  def require_enterprise!
    if !current_user.enterprise?
      redirect_to '/pricing'
    end
  end
end
class BlogsController < ApplicationController
  before_action :require_startup!, only: [:index]
  before_action :require_enterprise!, only: [:create, :update, :destroy]
  #...
end
Вход в полноэкранный режим Выход из полноэкранного режима

Вариант B: Обратиться к API

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

subscriptions = Stripe::Subscription.list({
  customer: current_user.stripe_customer_id,
})
subscriptions.any? {|s| s.status == 'trialing' || s.status == 'active'}
Вход в полноэкранный режим Выйти из полноэкранного режима

Опять же, ответ зависит от вашей бизнес-модели. Если вы отправляете коробку с подпиской раз в месяц и вам нужно проверять статус раз в месяц для каждого клиента, то вариант B может упростить интеграцию и использовать Stripe в качестве источника правды. Вариант B требует очень редких проверок API вне диапазона (когда клиент не просматривает сайт). Если у вас несколько уровней для вашего SaaS или вы планируете часто проверять статус, то я рекомендую вариант A.

Дополнительные соображения

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

Одна из лучших практик обработки уведомлений webhook, связанных с Stripe Checkout и подписками, заключается в использовании функции expand API для получения дополнительных данных, которые нам нужны в базе данных.

Вот пример обработки события webhook для checkout.session.completed при использовании встроенной таблицы цен. Вспомните, что при использовании встроенной таблицы цен мы не можем передать существующую ссылку на клиента, поэтому мы лениво создаем ее на основе client_reference_id из сессии (переданной в веб-компонент таблицы цен на странице ценообразования).

Обратите внимание, что когда мы повторно получаем подписку, мы передаем expand: ['items.data.price.product'], который вернет данные Подписки из API с ценами и продуктами связанного элемента линии. Таким образом, мы можем отобразить название продукта для клиента позже без необходимости повторного получения подписки из API.

def handle_checkout_session_completed(event)
  checkout_session = event.data.object
  customer = Customer.find_by(stripe_id: checkout_session.customer)
  if customer.nil?
    user = User.find_by(id: checkout_session.client_reference_id)
    customer = Customer.create!(
      user: user,
      stripe_id: checkout_session.customer
    )
  end

  subscription = Stripe::Subscription.retrieve(
    id: checkout_session.subscription,
    expand: ['items.data.price.product']
  )

  Subscription.create!(
    customer: customer,
    stripe_id: subscription.id,
    stripe_price_id: subscription.items.data.first.price.id,
    stripe_product_name: subscription.items.data.first.price.product.name,
    status: subscription.status,
    quantity: subscription.items.data.first.quantity
  )
end
Вход в полноэкранный режим Выход из полноэкранного режима

Подписки могут изменяться несколькими способами — вот как я обычно обрабатываю уведомления о событиях customer.subscription.updated, чтобы мы отслеживали новый статус подписки, идентификатор цены, название продукта и количество (подумайте о количестве мест!).

def handle_subscription_updated(event)
  subscription = Stripe::Subscription.retrieve(
    id: event.data.object.id,
    expand: ['items.data.price.product']
  )
  # lookup the subscription in the database:
  sub = Subscription.find_by(stripe_id: subscription.id)
  sub.update!(
    status: subscription.status,
    stripe_price_id: subscription.items.data.first.price.id,
    stripe_product_name: subscription.items.data.first.price.product.name,
    quantity: subscription.items.data.first.quantity
  )
end
Вход в полноэкранный режим Выход из полноэкранного режима

Следующие шаги

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

Об авторе

CJ Avilla (@cjav_dev) — представитель разработчиков в Stripe, разработчик Ruby on Rails и YouTuber. Он любит изучать и преподавать новые языки программирования и веб-фреймворки. Когда он не за компьютером, он проводит время с семьей или катается на велосипеде 🚲.

Оцените статью
devanswers.ru
Добавить комментарий