-
-
Save corbin88/043414c2c20169772352f01dfbab0f78 to your computer and use it in GitHub Desktop.
Laravel Vue Subscriptions using stripe, Laravel Cashier, and Laravel Sanctum
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Http\Controllers\Api; | |
use App\Plan; | |
use App\Http\Controllers\Controller; | |
use Illuminate\Http\Request; | |
class CheckoutController extends Controller | |
{ | |
public function processCheckout(Request $request) | |
{ | |
try { | |
$plan = Plan::findOrFail($request->billing_plan_id); | |
$user = $user = auth()->user(); | |
//If user has no subscriptions subscribe them to new plan | |
if($user->subscriptions->count() === 0){ | |
$user->newSubscription($plan->name, $plan->stripe_plan_id)->create($request->payment_method['id']); | |
return response([], 201); | |
} | |
} catch (\Exception $e) { | |
return response([], 500); | |
} | |
} | |
public function updateSubscription(Request $request) | |
{ | |
$newPlan = Plan::findOrFail($request->new_plan_id); | |
$oldPlan = Plan::where('name', $request->old_plan_name)->first(); | |
try { | |
$user = auth()->user(); | |
$user->subscription($oldPlan->name)->swap($newPlan->stripe_plan_id)->update(['name' => $newPlan->name]); | |
return response([], 201); | |
} catch (\Exception $e) { | |
return response([], 500); | |
} | |
} | |
public function cancelSubscription(Request $request) | |
{ | |
$oldPlan = Plan::findOrFail($request->old_plan_id); | |
try { | |
auth()->user()->subscription($oldPlan->name)->cancel(); | |
return response([], 201); | |
} catch (\Exception $e) { | |
return response([], 500); | |
} | |
} | |
public function resumeSubscription(Request $request) | |
{ | |
$oldPlan = Plan::findOrFail($request->old_plan_id); | |
try { | |
auth()->user()->subscription($oldPlan->name)->resume(); | |
return response([], 201); | |
} catch (\Exception $e) { | |
return response([], 500); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div> | |
<p>Please pay for {{planName}} plan</p> | |
<form :action="computedAction" method="POST" id="payment-form" @submit.prevent="processPayment()"> | |
<label for="">Card Holder Name</label> | |
<input id="card-holder-name" v-model="cardHolderName" type="text"> | |
<!-- Stripe Elements Placeholder --> | |
<div ref="card"></div> | |
<button type="submit"> | |
Process Payment | |
</button> | |
<div v-if="stripeErrorMessage"> | |
<p>{{stripeErrorMessage}}</p> | |
</div> | |
<div v-if="serverErrorMessage"> | |
<p>{{serverErrorMessage}}</p> | |
</div> | |
</form> | |
</div> | |
</template> | |
<script> | |
import {loadStripe} from '@stripe/stripe-js'; | |
export default { | |
name: 'PaymentForm1', | |
props: ['planId', 'planName'], | |
data () { | |
return { | |
cardHolderName: '', | |
stripeErrorMessage: null, | |
serverErrorMessage: null, | |
stripe: '', | |
elements: '', | |
card: undefined, | |
isLoaded: false | |
} | |
}, | |
created(){ | |
loadStripe(`pk_test_A7M3ZumCaS2KHkOTjp6EUgx8`) | |
.then ( (result) => | |
{ | |
this.stripe = result | |
this.elements = result.elements() | |
this.isLoaded = true; | |
this.card = this.elements.create('card', { | |
style: { | |
base: { | |
iconColor: '#c4f0ff', | |
//color: '#fff', | |
fontWeight: 500, | |
fontFamily: 'Roboto, Open Sans, Segoe UI, sans-serif', | |
fontSize: '16px', | |
fontSmoothing: 'antialiased', | |
':-webkit-autofill': { | |
color: '#fce883', | |
}, | |
'::placeholder': { | |
color: '#87BBFD', | |
}, | |
}, | |
invalid: { | |
iconColor: '#FFC7EE', | |
color: '#FFC7EE', | |
}, | |
}, | |
}); | |
this.card.mount(this.$refs.card); | |
}) | |
}, | |
mounted: function () { | |
}, | |
computed: { | |
computedAction() { | |
return '/api/checkout'; | |
}, | |
computedPlanId() { | |
return this.planId | |
} | |
}, | |
methods: { | |
processPayment(){ | |
let self = this; | |
self.stripe.createPaymentMethod( | |
'card', self.card, { | |
billing_details: { name: this.cardHolderName } | |
}).then(function(result) { | |
self.submitPaymentForm(result.paymentMethod); | |
if (result.error) { | |
self.stripeErrorMessage = result.error.message; | |
self.hasCardErrors = true; | |
self.$forceUpdate(); // Forcing the DOM to update so the Stripe Element can update. | |
return; | |
} | |
}); | |
}, | |
submitPaymentForm(paymentMethod){ | |
this.$http({ | |
method: 'POST', | |
url: 'api/checkout', | |
data: { | |
billing_plan_id: this.planId, | |
payment_method: paymentMethod | |
} | |
}).then(function (response) { | |
this.$store.dispatch('getUser') | |
}.bind(this)) | |
.catch(error => { | |
this.serverErrorMessage = "An error occurred. Please try again."; | |
console.log('submit payment'+error); | |
}); | |
}, | |
}, | |
} | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div> | |
<div class="subscription-card"> | |
<div class="flex-container"> | |
<div class="flex-item" v-for="(plan, id) in plans" v-bind:key="id"> | |
<ul class="package"> | |
<li class="header highlight">{{plan.name}}</li> | |
<li>${{plan.price}}</li> | |
<li class="gray"> | |
<button v-if="user.subscriptions < 1 || user.subscriptions == undefined" class="sub-button" @click="subscribeToPlan(plan.id, plan.name)">Subscribe</button> | |
<button v-else-if="user.subscriptions[0].name !== plan.name" class="sub-button" @click="changePlan(plan.id)">Change Subscribition</button> | |
<button v-else-if="user.subscriptions[0].name === plan.name && user.subscriptions[0].end_date === null" class="sub-button" @click="unSubscribeToPlan(plan.id)">Unsubscribe</button> | |
<button v-else class="sub-button" @click="resumeSubscription(plan.id)">Resume Subscription</button> | |
</li> | |
</ul> | |
</div> | |
</div> | |
<div v-if="planId && user.subscriptions < 1 || user.subscriptions == undefined" class="flex-container"> | |
<payment-form :plan-id="planId" :plan-name="planName" :subscription-checkout="true" :change-payment-method="false"></payment-form> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
//import PaymentForm from './PaymentForm.vue' | |
export default { | |
name: 'Subscriptions', | |
//components: PaymentForm, | |
data() { | |
return { | |
plans : null, | |
planId: null, | |
planName: null, | |
} | |
}, | |
created() { | |
this.getPlans(); | |
}, | |
computed: { | |
user() { | |
return this.$store.state.user | |
} | |
}, | |
methods: { | |
getPlans(){ | |
this.$http({ | |
method: 'GET', | |
url: 'api/billing', | |
}).then(function (response) { | |
this.plans = response.data.data | |
}.bind(this)) | |
.catch(error => { | |
error => this.status = error.response.data.status; | |
}); | |
}, | |
subscribeToPlan(id, planName){ | |
this.planId = id; | |
this.planName = planName; | |
}, | |
changePlan(id){ | |
this.$http({ | |
method: 'POST', | |
url: 'api/subscription/change', | |
data: { | |
new_plan_id: id, | |
old_plan_name: this.user.subscriptions[0].name | |
} | |
}).then(function (response) { | |
console.log("Changed Plan!"); | |
this.$store.dispatch('getUser') | |
}.bind(this)) | |
.catch(error => { | |
this.serverErrorMessage = "An error occurred. Please try again."; | |
console.log('cancelation error '+error); | |
}); | |
}, | |
resumeSubscription(id){ | |
this.$http({ | |
method: 'POST', | |
url: 'api/subscription/resume', | |
data: { | |
old_plan_id: id, | |
} | |
}).then(function (response) { | |
console.log("Resumed Plan!"); | |
this.$store.dispatch('getUser') | |
}.bind(this)) | |
.catch(error => { | |
this.serverErrorMessage = "An error occurred. Please try again."; | |
console.log('cancelation error '+error); | |
}); | |
}, | |
unSubscribeToPlan(id){ | |
this.$http({ | |
method: 'POST', | |
url: 'api/subscription/cancel', | |
data: { | |
old_plan_id: id, | |
} | |
}).then(function (response) { | |
this.$store.dispatch('getUser') | |
}.bind(this)) | |
.catch(error => { | |
this.serverErrorMessage = "An error occurred. Please try again."; | |
console.log('cancelation error '+error); | |
}); | |
} | |
} | |
} | |
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Tests\Feature; | |
use Illuminate\Foundation\Testing\RefreshDatabase; | |
use Illuminate\Foundation\Testing\WithFaker; | |
use Tests\TestCase; | |
use App\User; | |
class SubscriptionsPlansTest extends TestCase | |
{ | |
use RefreshDatabase; | |
protected $monthlyPlan; | |
protected $yearlyPlan; | |
protected function setUp(): void | |
{ | |
parent::setUp(); | |
$this->monthlyPlan = factory('App\Plan')->create(['name' => 'Monthly', 'stripe_plan_id' => 'plan_HDORvViGHNDEOU']); | |
$this->yearlyPlan = factory('App\Plan')->create(['name' => 'Yearly', 'stripe_plan_id' => 'plan_HDRXIkKqxQ78hu']); | |
} | |
/** @test */ | |
public function a_user_can_view_all_plans() | |
{ | |
$this->withoutExceptionHandling(); | |
$this->actingAs($user = factory('App\User')->create()); | |
$response = $this->json('GET', '/api/billing') | |
->assertStatus(200); | |
} | |
/** @test */ | |
public function a_user_can_subscribe_to_a_plan() | |
{ | |
$this->withoutExceptionHandling(); | |
$this->actingAs($user = factory('App\User')->create()); | |
\Stripe\Stripe::setApiKey(\Config::get('services.stripe.secret')); | |
$payment_method = \Stripe\PaymentMethod::create([ | |
'type' => 'card', | |
'card' => [ | |
'number' => '4242424242424242', | |
'exp_month' => 5, | |
'exp_year' => 2021, | |
'cvc' => '314', | |
], | |
]); | |
$response = $this->json('POST', '/api/checkout', ['billing_plan_id'=> 1, 'payment_method' => $payment_method ] ) | |
->assertStatus(201); | |
$this->assertTrue($user->subscribed('Monthly')); | |
} | |
/** @test */ | |
public function a_user_can_change_their_subscription() | |
{ | |
//$this->withoutExceptionHandling(); | |
$this->actingAs($user = factory('App\User')->create()); | |
\Stripe\Stripe::setApiKey(\Config::get('services.stripe.secret')); | |
$payment_method = \Stripe\PaymentMethod::create([ | |
'type' => 'card', | |
'card' => [ | |
'number' => '4242424242424242', | |
'exp_month' => 5, | |
'exp_year' => 2021, | |
'cvc' => '314', | |
], | |
]); | |
$user->newSubscription($this->monthlyPlan->name, $this->monthlyPlan->stripe_plan_id)->create($payment_method['id']); | |
$response = $this->json('POST', '/api/subscription/change', ['old_plan_name'=> $this->monthlyPlan->name, 'new_plan_id' => $this->yearlyPlan->id] ) | |
->assertStatus(201); | |
$this->assertTrue($user->subscribed('Yearly')); | |
} | |
/** @test */ | |
public function a_user_can_cancel_their_subscription() | |
{ | |
$this->withoutExceptionHandling(); | |
$this->actingAs($user = factory('App\User')->create()); | |
\Stripe\Stripe::setApiKey(\Config::get('services.stripe.secret')); | |
$payment_method = \Stripe\PaymentMethod::create([ | |
'type' => 'card', | |
'card' => [ | |
'number' => '4242424242424242', | |
'exp_month' => 5, | |
'exp_year' => 2021, | |
'cvc' => '314', | |
], | |
]); | |
$user->newSubscription($this->monthlyPlan->name, $this->monthlyPlan->stripe_plan_id)->create($payment_method['id']); | |
$response = $this->json('POST', '/api/subscription/cancel', ['old_plan_id'=> $this->monthlyPlan->id] ) | |
->assertStatus(201); | |
$this->assertSoftDeleted('subscriptions', [ | |
'user_id' => $user->id, | |
'name' => $this->monthlyPlan->name, | |
]); | |
} | |
/** @test */ | |
public function a_user_can_resume_their_canceled_subscription() | |
{ | |
$this->withoutExceptionHandling(); | |
$this->actingAs($user = factory('App\User')->create()); | |
\Stripe\Stripe::setApiKey(\Config::get('services.stripe.secret')); | |
$payment_method = \Stripe\PaymentMethod::create([ | |
'type' => 'card', | |
'card' => [ | |
'number' => '4242424242424242', | |
'exp_month' => 5, | |
'exp_year' => 2021, | |
'cvc' => '314', | |
], | |
]); | |
$user->newSubscription($this->monthlyPlan->name, $this->monthlyPlan->stripe_plan_id)->create($payment_method['id']); | |
$user->subscription($this->monthlyPlan->name)->cancel(); | |
$response = $this->json('POST', '/api/subscription/resume', ['old_plan_id'=> $this->monthlyPlan->id] ) | |
->assertStatus(201); | |
$this->assertTrue($user->subscription($this->monthlyPlan->name)->ends_at === null); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment