【後編】Laravel Cashier + Stripeで月額サブスクを実装する
前回はLaravel Cashierパッケージの導入とStripeアカウントの設定を行いました。最後に動作確認を行い、テスト環境でStripeと通信できた状態かと思います。今回は、本題の月額サブスクを実装をしていきます。
基本的な使い方は前回と同様にLaravel Cashierの公式ドキュメント(日本語訳)1を参照します。
この記事でやること
- ユーザーが、カスタマーポータルが開けるようにする
- ユーザーが、サブスクリプションが購入できるようする
- アプリから、購入状況を参照して制御できるようする
前提
- Stripeアカウントが準備できていること
-
- Stripe Taxが有効化されていること
- カスタマーポータルの有効化されていること
- サブスクリプションが1つに制限されていること
- プロジェクトにLaravel Cashier が導入済みであること
-
- Laravel Cashierパッケージのインストール
- Billingモデルの実装
- Stripe, Laravel Cashierの各種設定と動作確認
- フロントエンドがReact + TypeScriptを構築されていること(オプション)
-
サンプルとしてはReactのコードを記載しています。Laravel側の説明が中心であり、複雑なフロントエンド実装というのは出ませんので、他のフレームワークを使っている場合は置き換えながら進めて頂ければ幸いです。
まだプロジェクトが準備できていない場合は前回の記事をご覧ください。
実装
エンドポイントの作成
決済系のエンドポイントを作成していきます。Stripeと連携してURLを取得するのですが、これらは後ほどの以下の処理を実装するのに利用します。コントローラー名等の命名はプロジェクトに合わせて変えてください。
- Stripe Checkoutセッションを開始
- 支払い状況の確認
- 決済コントローラーを作成
-
use Illuminate\Http\Request; class CheckoutController extends Controller { /** * Stripe CheckoutセッションのURLを返す */ public function index(Request $request) { $user = $request->user(); $lookupKey = $request->input('lookup_key'); $checkout_session = $user->newSubscription($lookupKey, config("services.stripe.$lookupKey"))->checkout([ 'success_url' => url('/') . '?success=true&session_id={CHECKOUT_SESSION_ID}', 'cancel_url' => url('/') . '?canceled=true', 'payment_method_types' => ['card'], 'locale' => app()->getLocale() ]); return response()->json(['url' => $checkout_session->url]); } /** * カスタマーポータルのURLを返す */ public function billing(Request $request) { $url = $request->user()->billingPortalUrl(null, [ 'locale' => app()->getLocale() ]); return response()->json(['url' => $url]); } }
- ルーティングを設定
-
内部apiのため
sanctum
ミドルウェアで制限しています。Route::middleware('auth:sanctum')->group(function () { Route::post('/checkout', [CheckoutController::class, 'index'])->name('checkout'); Route::get('/checkout/billing', [CheckoutController::class, 'billing'])->name('checkout-billing'); });
フロントエンド実装
フロントエンド実装をしていくのですが、おそらくお使いのライブラリーによって内容が異なるため、全てのコードを載せると主旨から外れますのであまり関係のない部分は省略します。要点を押さえてコードを載せますので、開発環境に置き換えて進めてもらえればと思います。
- アップグレード画面に遷移(Stripe Checkoutセッションを開始)
-
「アップグレード」ボタンみたいものを配置するとした場合のクリック処理のサンプルです。
決済画面のURLを/api/checkout
エンドポイントから取得してきて、そちらに遷移させます。const handleClickUpgrade = async (e) => { e.preventDefault(); try { const response = await hostClient.post("/api/checkout", { lookup_key: 'professional_plan', }); window.location.href = response.data.url; } catch (error) { console.error(error); } };
決済画面が開けましたでしょうか?以下のような画面が開ければOKです。
- カスタマーポータルに遷移(支払い状況の確認)
-
「アップグレード」ボタンみたいものを配置するとした場合のクリック処理です。
決済画面のURLを/api/checkout
エンドポイントから取得してきて、そちらに遷移させます。const handleClickBilling = async (e) => { e.preventDefault(); try { const response = await hostClient.get("/api/checkout/billing"); window.location.href = response.data.url; } catch (error) { console.error(error); } };
以下のようにカスタマーポータルを開いて、支払い状況が確認できればOKです。
課金状況の受け渡し
最後に購入状況をフロントエンドに渡して、プランの有効・無効や種類に応じた表示ができるようにします。
ミドルウェアの設定
inertiajsのミドルウェアを利用して、ログイン中のユーザーの購入状況をフロントエンドに渡しています。
用途によりますが、有効なサブスクに絞り込んだりして必要な情報のみを渡してやりましょう。
public function share(Request $request): array
{
$user = $request->user();
return [
...parent::share($request),
'auth' => [
'user' => $user,
'subscriptions' => $user ? $user->subscriptions()->active()->get() : []
],
];
}
フロントエンド実装
取得した購入状況をフロントエンドで扱っていきます。ただし、実装はサービスによりけりなため、こちらのコードは参考程度に見てもらえれば良いです。ends_at
カラムに日付が入っていなければ課金中、入っていればキャンセル済みとなります。
import { usePage } from "@inertiajs/react";
const professionalPlan = "professional_plan";
const starterPlan = "starter_plan";
export const useSubscriptions = () => {
const subscriptions = usePage().props.auth.subscriptions;
const subscriptionPlan = subscriptions
.filter((item) => !item.ends_at)
.at(0);
return {
subscriptionPlan,
};
};
終わりに
前後編に分けて、Laravel Cashier と Stripe を使用して月額サブスクリプションを実装する方法を紹介しました。主要な機能として、カスタマーポータルへのリダイレクト、サブスクリプションの購入、購入状況の参照を実装しました。
分量的にあまり触れませんでしたが、本番で稼働させるにあたっては、以下の点に注意が必要です。
- エラーハンドリング:支払いの失敗や不完全な支払いに適切に対応すること。
- セキュリティ:API エンドポイントの保護と適切な認証の実装。
- テスト:Stripe のテストモードを使用して、様々なシナリオをテストすること。
- ユーザーエクスペリエンス:支払いプロセスをスムーズにし、明確なフィードバックを提供すること。
また、決済に関する法的要件や、定期的な請求の管理、解約処理なども考慮に入れる必要があります。