Skip to content

Instantly share code, notes, and snippets.

@corbin88
Last active June 16, 2020 08:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save corbin88/043414c2c20169772352f01dfbab0f78 to your computer and use it in GitHub Desktop.
Save corbin88/043414c2c20169772352f01dfbab0f78 to your computer and use it in GitHub Desktop.
Laravel Vue Subscriptions using stripe, Laravel Cashier, and Laravel Sanctum
<?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);
}
}
}
<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>
<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>
<?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