Stripeでサブスクリプションを実装する機会があったので、メモがてら流れを書いていこうと思います。
- Laravel 7.0
- Nuxt.js 2.14.6
Nuxt.jsがSPAで動いており、LaravelがAPIを返す構成になっています。
流れ
契約
- stripeのダッシュボード上でプランを作成しておく
- [クライアント] プランを選択してサーバーサイドへ送信
- [サーバーサイド] 顧客情報を作成して、DBへ保存。セッションを作成してクライアントに渡す
- [クライアント] セッションidをstripe checkout(stripeの決済用外部ページ)に渡し、支払い情報を入力させる。成功すれば
/checkout/success?session_id=XXX
などのURLにリダイレクトする - [クライアント] successページのqueryからセッションidを取得。サーバーサイドに渡す
- [サーバーサイド] セッションidから顧客情報を取得、DBへ必要な情報を登録する。
キャンセル
- [クライアント]キャンセル情報をサーバーサイドへ送信
- [サーバーサイド]キャンセル処理(期末で解約とする)
- 期末で解約。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で郵便番号の入力を非表示にする
<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 ) }, }