Stripeでサブスク実装

Stripeでサブスクリプションを実装する機会があったので、メモがてら流れを書いていこうと思います。

  • Laravel 7.0
  • Nuxt.js 2.14.6

Nuxt.jsがSPAで動いており、LaravelがAPIを返す構成になっています。

流れ

契約

  1. stripeのダッシュボード上でプランを作成しておく
  2. [クライアント] プランを選択してサーバーサイドへ送信
  3. [サーバーサイド] 顧客情報を作成して、DBへ保存。セッションを作成してクライアントに渡す
  4. [クライアント] セッションidをstripe checkout(stripeの決済用外部ページ)に渡し、支払い情報を入力させる。成功すれば/checkout/success?session_id=XXXなどのURLにリダイレクトする
  5. [クライアント] successページのqueryからセッションidを取得。サーバーサイドに渡す
  6. [サーバーサイド] セッションidから顧客情報を取得、DBへ必要な情報を登録する。

キャンセル

  1. [クライアント]キャンセル情報をサーバーサイドへ送信
  2. [サーバーサイド]キャンセル処理(期末で解約とする)
  3. 期末で解約。stripeからwebhookでサーバーサイドのAPIを叩くことができる。DBに解約の情報を登録。有料機能の制限などを行う

ざっとこんな感じの流れです。

stripe checkoutとelements

まず、使用を迷ったのが、この2つです。
https://stripe.com/docs/billing/subscriptions/checkout
https://stripe.com/docs/billing/subscriptions/fixed-price

これらは、決済の入力フォームを実装するコンポーネントになります。

npmはこちらを使用しました。
checkoutもelementsもこれ1つで利用できます。
https://github.com/jofftiquez/vue-stripe-checkout

checkoutとelementsの違いは、checkoutが外部のサイトで決済処理が完結するのに対して、
elementsの方がカスタマイズ性が高く、サーバーサイドの処理を必要とします。

checkoutでは、外部サイトからリダイレクトで自サイトに戻ることになるので、ユーザーIDなどを渡すのは難しそうに思っていましたが、session_idから簡単に取得できました。必要があれば、metadataとして定義することもできます。

基本的には、checkoutを選択することになるのかなと思いました。

キャンセルの実装について

即キャンセルではなく、支払済の期末での解約にしました。
ちなみに、オプションを'cancel_at_period_end' => true,とすると、期末のキャンセルにできます。

\Stripe\Subscription::update(
  $subscriptionId,
  [
    'cancel_at_period_end' => true,
  ]
);

(ちなみに即キャンセルの場合は、updateではなく、\Stripe\Subscription::cancelとなります。)

解約時のwebhook

期末になり、実際の解約となるタイミングで、
stripe上でcustomer.subscription.deletedというイベントが走ります。

webhookを設定すると指定のイベントでAPIを叩くことができます。
URLは一律になるので、それをtypeでケースを分けて、処理をする形になります。

stripe cliを使うことで、localでもデバッグすることができます。
https://stripe.com/docs/stripe-cli/webhooks

stripe listen --forward-to localhost:5000/hooks

これで、stripe上でイベントが流れるたびに、localhost:5000/hooksが叩かれることになります。

stripe trigger payment_intent.created

などcli上でイベントを発生させてもいいですし、(イベント一覧)
実際にダッシュボード上や開発中のシステムでstripeのAPIを叩いた場合でも、有効です。

期末解約の実装確認では、キャンセル予定日を確認したのち、stripeのダッシュボード上で即時キャンセルして確認していました。

その他、支払い完了などのタイミングでメールの送信をシステムから行いたい場合などでも使えそうです。

余談: vue-stripe-checkoutのelementsで郵便番号の入力を非表示にする

https://github.com/jofftiquez/vue-stripe-checkout/issues/10オプションがstylesしか渡せず、困ったのでイシューを探したらありました。nuxt.js

<template>
  <div>
    <stripe-elements
      ref="elementsRef"
      :publishableKey="publishableKey"
      :amount="amount"
      @token="tokenCreated"
      @loading="loading = $event"
      locale="ja"
    >
    </stripe-elements>
    <v-btn>送信</v-btn>
  </div>
</template>
import { StripeElements } from 'vue-stripe-checkout'
export default {
  data() {
    return {
      loading: false,
      publishableKey: process.env.STRIPE_PUBLIC_KEY,
      amount: 1000,
      token: null,
      stripeConfig: {
        hidePostalCode: true
      }
    }
  },
  mounted() {
    // configをupdateしてhidePostalCodeする
    setTimeout(
      () => this.$refs.elementsRef.card.update(this.stripeConfig),
      1000
    )
  },
}