事前準備
laravelのプロジェクト作成。
Stripeのアカウント作成して以下を取得。
・公開可能キー トップページから
・シークレットキー トップページから
・API ID 商品ページから
※そそぞれ本番用とテスト用がある。
cashierをインストール
composer require laravel/cashier
migrate
php artisan migrate
3ファイル実行される。
何も実行されなかったら
\vendor\laravel\cashier\database\migrations
にマイグレーションファイルが3つあるので
\database\migrations
へコピーしてからマイグレーション。
userモデルにBillable追加
App\Models\User.php
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
・use Laravel\Cashier\Billable;
・use Billable;
を追加
ローカルテストの為Stripe CLIをDL
参考
https://stripe.com/docs/stripe-cli
ここから「stripe_X.X.X_windows_x86_64.zip 」をDL
https://github.com/stripe/stripe-cli/releases/tag/v1.19.2
laravelのルートにstripe.exeを設置
stripe login
コマンドプロンプトに表示されたURLにアクセスして認証。
stripe listen --forward-to localhost/subscription/webhook
これでコマンドプロンプトでlistenできる。
WebフックのCSRF保護を外す
App\Http\Middleware\VerifyCsrfToken
protected $except = [
'stripe/*',
];
APIキー
.env
STRIPE_KEY=pk_test_〇〇〇
STRIPE_SECRET=sk_test_〇〇〇
STRIPE_BASIC_ID=price_〇〇〇
事前準備で用意したAPIキーを入力。
まずは本番用ではなくテスト用を入力。
\config\services.php
'stripe' => [
'pb_key'=>env('STRIPE_KEY'),
'st_key'=>env('STRIPE_SECRET'),
'basic_plan_id'=>env('STRIPE_BASIC_ID'),
],
// config('services.stripe.pb_key')
// でキーを取得できる。
※.envはconfigを経由して参照しないと不具合が起こる事があるため。
StripeController.php作成
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;
use Stripe\Stripe;
use Stripe\Charge;
use App\Models\User;
use Illuminate\Support\Facades\Log;
use Laravel\Cashier\Events\WebhookReceived;
use Laravel\Cashier\Http\Controllers\WebhookController;
use Illuminate\Support\Carbon;
class StripeController extends Controller
{
//トライアル日
const TRIAL_DAYS = 10;
//トップページ
public function subscription(Request $request){
$user = Auth::user();
return view('stripe', [
'intent' => $user->createSetupIntent(),
'status' => static::status(),
'user' => $user,
]);
}
// キャンセルページ
public function cancelpage(Request $request){
$input = $request->input();
$user = Auth::user();
if(!in_array(static::status()['status'], ['active', 'trial'])){
return redirect()->route('mypage');
}
return view('stripe_cancel', [
'status' => static::status(),
'submit' => $request->has('_token')??null,
]);
}
// キャンセル
public function cancel(Request $request) {
$subsc = Auth::user()->subscription('main');
if($subsc->onTrial()){
$subsc->cancelNow();
}else{
$subsc->cancel();
}
return redirect()->route('mypage')
->with(['success' => "解約致しました。ご利用ありがとうございました。"]);
}
//ステータス取得
public static function status(){
$user = Auth::user();
$result = [
'status' => '',
'endDate' => null,
'endDateNext' => null,
'trialEnd' => null,
'nextPayDate' => null,
'nextPay' => null,
'cardData' => null,
];
$subsc = $user->subscription('main');
if(!$subsc) return $result;
//解約日
$end = $subsc->ends_at??null;
if($end){
$result['endDate'] = $end->format('Y年m月d日')??null;
$result['endDateNext'] = $end->addDays(1)->format('Y年m月d日')??null;
}
//トライアル終了日
$trialEnd = $subsc->trial_ends_at??null;
if($trialEnd) $result['trialEnd'] = $trialEnd->format('Y年m月d日')??null;
// $user->hasDefaultPaymentMethod(); //デフォルトの支払いを持っているか。
//次回お支払い日
$nextPayAttempt = $user->upcomingInvoice()->next_payment_attempt??null;
if($nextPayAttempt) $result['nextPayDate'] = Carbon::createFromTimestamp($nextPayAttempt)->format('Y年m月d日')??null;
// 次回お支払い金額
$nextPay = $user->upcomingInvoice()->total??null;
if($nextPay) $result['nextPay'] = number_format($nextPay).'円';
// クレジットカード情報
$result['cardData'] = $user->pm_type.' **** **** **** '.$user->pm_last_four;
//ステータス
// trialing(トライアル中):
// active(アクティブ):
// past_due(前回の支払い期限超過):
// canceled(キャンセル):
// incomplete(不完全):
// incomplete_expired(不完全(期限切れ)):
// unpaid(今回未払い):
// $status = $subsc->stripe_status??null;
if($subsc->onGracePeriod()){ //解約待機中
$result['status'] = 'gracePeriod';
}
if($subsc->onTrial()){ //トライアル中
$result['status'] = 'trial';
}
if($subsc->ended()){ //解約済
$result['status'] = 'ended';
}
if($subsc->hasIncompletePayment()){ //滞納中
$result['status'] = 'incomplete';
}
if($subsc->recurring()){ //契約中
$result['status'] = 'active';
}
return $result;
}
//新規登録
public function create(Request $request){
$user = Auth::user();
$status = static::status();
if($status['status']=='active') return back()->with(['success' => "ご契約中です。"]);
if(!in_array($status['status'], ['', 'ended', 'incomplete'])){
return back()->with(['error' => "登録できませんでした。"]);
}
//トライアル日
$trialDays = 0;
if(!$status['status'] && StripeController::TRIAL_DAYS??null){
$trialDays = StripeController::TRIAL_DAYS;
}
$paymentMethod = $request->input('stripePaymentMethod'); //支払情報
$plan = config('services.stripe.basic_plan_id'); //プラン
$stripeCustomer = $user->createOrGetStripeCustomer();
$newSubsc = $user->newSubscription('main', $plan);
//新規申し込みならトライアルを付ける
if($trialDays) $newSubsc = $newSubsc->trialDays($trialDays);
$newSubsc->create($paymentMethod);
// $newSubsc->load('subscriptions');
return back()->with(['success' => "登録しました。"]);
}
// キャンセルを戻す(gracePeriod中のみ)
public function resume(Request $request) {
$user = Auth::user();
if(static::status()['status']=='active') return back()->with(['success' => "ご契約中です。"]);
if(static::status()['status']!='gracePeriod') return back()->with(['error' => "猶予期間中ではありません。"]);
if(!$user->hasDefaultPaymentMethod()) return back()->with(['error' => "お支払い方法が登録されていません。"]);
$user->subscription('main')->resume();
return back()->with(['success' => "解約を取り消しました。今後ともよろしくお願い致します。"]);
}
// カード変更
public function update_card(Request $request) {
$paymentMethod = $request->input('stripePaymentMethod'); //支払情報
Auth::user()->updateDefaultPaymentMethod($paymentMethod);
return back()->with(['success' => "お支払い方法を変更しました。"]);
}
}
@include('div.2')
<h2 class="text-lg font-medium text-gray-900">有料プラン</h2>
@include('components.message')
{{-- 契約中 --}}
@if($status['status']=='active' || is_local())
<p>■契約状況 : 有料プラン<br>
<br>
登録中カード : {{$status['cardData']}}<br>
次回お支払日 : {{$status['nextPayDate']}}<br>
次回お支払金額 : {{$status['nextPay']}}<br>
<br>
<span class="toggleButton">↓↓お支払方法の変更↓↓</span></p>
{{ Form::open(['url' => route('subscription.update_card'), 'id'=>"payment-form", 'class'=>"toggleContent"]) }}
登録アカウント : {{$user->email}} <br><br>
<label>クレジットカード名義<input type="test" class="form-control col-sm-5" id="card-holder-name" required></label><br><br>
<label>クレジットカード番号<div class="form-group MyCardElement col-sm-5" id="card-element"></div></label><br><br>
<div id="card-errors" role="alert" style='color:red'></div>
<button class="btn btn-primary" id="card-button" data-secret="{{ $intent->client_secret }}">変更する</button>
{{ Form::close() }}
<br>
<br>
<br>
<p style="text-align:right;">解約は<a href="{{route('subscription.cancelpage')}}">こちら</a></p>
@endif @if(is_local()) @include('div.close') @include('div.2') @endif
{{-- 解約待機中 --}}
@if($status['status']=='gracePeriod' || is_local())
<p>■契約状況 : 解約済(猶予期間中)<br>
<br>
失効日 : {{$status['endDate']}}<br>
<br>
</p>
<p>↓↓解約の取り消しはこちら↓↓</p>
{{ Form::open(['url' => route('subscription.resume')]) }}
<button class="btn btn-info">{{$status['endDateNext']}}以降も有料プランを継続</button>
{{ Form::close() }}
<br><p>※お支払い方法を変更する場合は、解約の取り消し後に行えます。</p>
@endif @if(is_local()) @include('div.close') @include('div.2') @endif
{{-- トライアル中 --}}
@if($status['status']=='trial' || is_local())
<p>■契約状況 : 無料トライアル中<br>
<br>
{{$status['trialEnd']}} から有料会員へ切り替わります。<br>
<br>
</p>
<p style="text-align:right;">解約は<a href="{{route('subscription.cancelpage')}}">こちら</a></p>
@endif @if(is_local()) @include('div.close') @include('div.2') @endif
{{-- 滞納中 --}}
@if($status['status']=='incomplete' || is_local())
<p>■契約状況 : 未清算<br>
<br>
登録中カード : {{$status['cardData']}}<br>
お支払日 : {{$status['nextPayDate']}}<br>
お支払金額 : {{$status['nextPay']}}<br>
<br>
↓↓お支払方法の変更↓↓</p>
{{ Form::open(['url' => route('subscription.update_card'), 'id'=>"payment-form"]) }}
登録アカウント : {{$user->email}} <br><br>
<label>クレジットカード名義<input type="test" class="form-control col-sm-5" id="card-holder-name" required></label><br><br>
<label>クレジットカード番号<div class="form-group MyCardElement col-sm-5" id="card-element"></div></label><br><br>
<div id="card-errors" role="alert" style='color:red'></div>
<button class="btn btn-primary" id="card-button" data-secret="{{ $intent->client_secret }}">変更する</button>
{{ Form::close() }}
<br>
<P>※利用できなかった期間の料金は発生しておりません。</P>
@endif @if(is_local()) @include('div.close') @include('div.2') @endif
{{-- 無料会員 --}}
@if($status['status']=='ended' || $status['status']=='' || is_local())
<p>■契約状況 : 未契約<br>
<br>
<br>
↓↓有料プランへのお申込みはこちら↓↓</p>
{{ Form::open(['url' => route('subscription.create'), 'id'=>"payment-form"]) }}
登録アカウント : {{$user->email}} <br><br>
<label>クレジットカード名義<input type="test" class="form-control col-sm-5" id="card-holder-name" required></label><br><br>
<label>クレジットカード番号<div class="form-group MyCardElement col-sm-5" id="card-element"></div></label><br><br>
<div id="card-errors" role="alert" style='color:red'></div>
<button class="btn btn-primary" id="card-button" data-secret="{{ $intent->client_secret }}">有料プランへ申込む</button>
{{ Form::close() }}
@endif
{{-- jquery --}}
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
{{-- stripe.js --}}
<script src="https://js.stripe.com/v3/"></script>
<script>
window.onload = my_init;
function my_init() {
const stripe = Stripe("{{ config('services.stripe.pb_key') }}");
const elements = stripe.elements();
var style = {
base: {
color: "#32325d",
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: "antialiased",
fontSize: "16px",
"::placeholder": {
color: "#aab7c4"
}
},
invalid: {
color: "#fa755a",
iconColor: "#fa755a"
}
};
const cardElement = elements.create('card', {style: style, hidePostalCode: true});
cardElement.mount('#card-element');
const cardHolderName = document.getElementById('card-holder-name');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
cardButton.addEventListener('click', async (e) => {
e.preventDefault();
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: cardHolderName.value }
}
}
);
if (error) {
console.log('error');
} else {
stripePaymentHandler(setupIntent);
}
});
}
function stripePaymentHandler(setupIntent) {
var form = document.getElementById('payment-form');
var hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', 'stripePaymentMethod');
hiddenInput.setAttribute('value', setupIntent.payment_method);
form.appendChild(hiddenInput);
form.submit();
}
</script>
雑ですいません。
後で時間ある時に綺麗にします。
参考