I would like to create a restricted page using Cashier. In a nutshell I would like the page to start a subscription to have these limitations:
Accessible to users who do not have a subscription
Not accessible to users who have an incomplete invoice/subscription and therefore an open invoice.
This is my code:
if(Auth::user()->subscribed('default')) {
return redirect()->route('index')->with('error', 'You already have an active subscription right now.');
}
if(Auth::user()->subscribed('default') || Auth::user()->subscription('default')->hasIncompletePayment()){
return redirect()->route('account.invoices')->with('warning', 'You have a payment invoice for a pending subscription. Make payment or cancel.');
}
With this code of mine the only problem is when I try to access the page with a user who has no subscription, this error appears:
Call to a member function hasIncompletePayment() on null
if(Auth::user()->subscribed('default')) {
return redirect()->route('index')->with('error', 'You already have an active subscription right now.');
}
if(!empty(Auth::user()->subscription('default'))){
if(Auth::user()->subscribed('default') || Auth::user()->subscription('default')->hasIncompletePayment()){
return redirect()->route('account.invoices')->with('warning', 'You have a payment invoice for a pending subscription. Make payment or cancel.');
}
}
Check subscription == null with empty
My payment model idea for my application is really simple: Having a (laravel) website with a member area and some special funcionality, whereas a member account costs 19.90 / year. I wanted to integrate Stripe to my registration flow to allow a payment to happen. When the payment has succeeded, I create a subscription which will then automatically renew this payment each year.
So good so far - I managed to get it working using the Guide on how to set up a subscription by Stripe. However, cards that required 3D Secure authentication did not work yet, and this is a must-have.
So I read further and used a PaymentIntent (API Docs). However, current behavior is the following:
I create a PaymentIntent and pass the public key to the frontend
Customer enters credentials and submits
3D Secure Authentication happens correctly, returning me a payment_method_id
On the server side, I retrieve the PaymentIntent again. It has status succeeded and the payment is recieved on my Stripe Dashboard.
I then create the customer object (with the payment method I got from the PaymentIntent), and with that customer, create the subscription
The subscription has status incomplete and it seems that the subscription tries to again charge the customer but fails because of the 3D Secure validation that would be necessary the second time.
So my actual question is: How can I create a subscription which notices somehow that the customer has already paid with my PaymentIntent and the PaymentMethod that I'm passing to it?
Some Code
Create the PaymentIntent and pass that to the frontend
\Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
$intent = \Stripe\PaymentIntent::create([
'amount' => '1990',
'currency' => 'chf',
]);
$request->session()->put('stripePaymentIntentId',$intent->id);
return view('payment.checkout')->with('intentClientSecret',$intent->client_secret);
Frontend Checkout when clicking "Buy"
// I have stripe elements (the card input field) ready and working
// using the variable "card". The Stripe instance is saved in "stripe".
// Using "confirmCardPayment", the 3DS authentication is performed successfully.
stripe.confirmCardPayment(intentClientSecret,{
payment_method: {card: mycard},
setup_future_usage: 'off_session'
}).then(function(result) {
$('#card-errors').text(result.error ? result.error.message : '');
if (!result.error) {
submitMyFormToBackend(result.paymentIntent.payment_method);
}
else {
unlockPaymentForm();
}
});
Backend after submitting
// Get the PaymentMethod id from the frontend that was submitted
$payment_method_id = $request->get('stripePaymentMethodId');
// Get the PaymentIntent id which we created in the beginning
$payment_intent_id = $request->session()->get('stripePaymentIntentId');
\Stripe\Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
// Get the Laravel User
$user = auth()->user();
// Firstly load Payment Intent to have this failing first if anything is not right
$intent = \Stripe\PaymentIntent::retrieve($payment_intent_id);
if ($intent instanceof \Stripe\PaymentIntent) {
// PaymentIntent loaded successfully.
if ($intent->status == 'succeeded') {
// The intent succeeded and at this point I believe the money
// has already been transferred to my account, so it's paid.
// Setting up the user with the paymentMethod given from the frontend (from
// the 3DS confirmation).
$customer = \Stripe\Customer::create([
'payment_method' => $payment_method_id,
'email' => $user->email,
'invoice_settings' => [
'default_payment_method' => $payment_method_id,
],
]);
$stripeSub = \Stripe\Subscription::create([
'customer' => $customer->id,
'items' => [
[
'plan' => env('STRIPE_PLAN_ID'),
]
],
'collection_method' => 'charge_automatically',
'off_session' => false,
]);
// If the state of the subscription would be "active" or "trialing", we would be fine
// (depends on the trial settings on the plan), but both would be ok.
if (in_array($stripeSub->status,['active','trialing'])) {
return "SUCCESS";
}
// HOWEVER the state that I get here is "incomplete", thus it's an error.
else {
return "ERROR";
}
}
}
I finally got a working solution running for my site. It goes like this:
1 - Backend: Create a SetupIntent
I created a SetupIntent (SetupIntent API Docs) to cover the checkout flow entirely. The difference to a PaymentIntent (PaymentIntent API Docs) is that the PaymentIntent goes from collecting the card details, preparing the payment and effectively transferring the amount to the account, while the SetupIntent only prepares card collection, but does not yet execute the payment. You will get a PaymentMethod (PaymentMethod API Docs) from it, which you can use later.
$intent = SetupIntent::create([
'payment_method_types' => ['card'],
]);
Then I passed the $intent->client_secret key to my client side JavaScript.
2 - Frontend: Collect card details with Elements
On the frontend, I placed the Stripe card element to collect the card details.
var stripe = Stripe(your_stripe_public_key);
var elements = stripe.elements();
var style = { /* my custom style definitions */ };
var card = elements.create('card',{style:style});
card.mount('.my-cards-element-container');
// Add live error message listener
card.addEventListener('change',function(event) {
$('.my-card-errors-container').text(event.error ? event.error.message : '');
}
// Add payment button listener
$('.my-payment-submit-button').on('click',function() {
// Ensure to lock the Payment Form while performing async actions
lockMyPaymentForm();
// Confirm the setup without charging it yet thanks to the SetupIntent.
// With 3D Secure 2 cards, this will trigger the confirmation window.
// With 3D Secure cards, this will not trigger a confirmation.
stripe.confirmCardSetup(setup_intent_client_secret, {
payment_method: {card: card} // <- the latter is the card object variable
}).then(function(result) {
$('.my-card-errors-container').text(event.error ? event.error.message : '');
if (!result.error) {
submitPaymentMethodIdToBackend(result.setupIntent.payment_method);
}
else {
// There was an error so unlock the payment form again.
unlockMyPaymentForm();
}
});
}
function lockMyPaymentForm() {
$('.my-payment-submit-button').addClass('disabled'); // From Bootstrap
// Get the card element here and disable it
// This variable is not global so this is just sample code that does not work.
card.update({disabled: true});
}
function unlockMyPaymentForm() {
$('.my-payment-submit-button').removeClass('disabled'); // From Bootstrap
// Get the card element here and enable it again
// This variable is not global so this is just sample code that does not work.
card.update({disabled: false});
}
3 - Backend: Create Customer and Subscription
On the backend, I received the $payment_method_id which I submitted from the frontend.
Firstly, we need now to create a Customer (Customer API Docs) if it does not yet exist. On the customer, we will attach the payment method from the SetupIntent. Then, we create the Subscription (Subscription API Docs) which will start the charge from the SetupIntent.
$customer = \Stripe\Customer::create([
'email' => $user->email, // A field from my previously registered laravel user
]);
$paymentMethod = \Stripe\PaymentMethod::retrieve($payment_method_id);
$paymentMethod->attach([
'customer' => $customer->id,
]);
$customer = \Stripe\Customer::update($customer->id,[
'invoice_settings' => [
'default_payment_method' => $paymentMethod->id,
],
]);
$subscription = \Stripe\Subscription::create([
'customer' => $customer->id,
'items' => [
[
'plan' => 'MY_STRIPE_PLAN_ID',
],
],
'off_session' => TRUE, //for use when the subscription renews
]);
Now we have a Subscription object. With regular cards, the state should be active or trialing, depending on your trial days setting on the subscription. However when dealing with 3D Secure test cards, I got the subscription still in an incomplete state. According to my Stripe support contact, this can also be a problem because of not yet fully working 3D Secure test cards. However I assume that this can also happen on production environments with some sort of cards, so we have to deal with it.
On subscriptions with status incomplete you can retrieve the latest invoice from $subscription->latest_invoice like so:
$invoice = \Stripe\Invoice::retrieve($subscription->latest_invoice);
On your invoice object, you will find a status and a hosted_invoice_url. When the status is still open, I now present the user the URL to the hosted invoice which he has to complete first. I let him open the link in a new window, which shows a nice looking invoice hosted by stripe. There, he is free to again confirm his credit card details including the 3D Secure workflow. In case he succeeds there, the $subscription->status changes to active or trialing after you re-retrieve the subscription from Stripe.
This is some sort of fool proof strategy that if anything with your implementation goes wrong, just send them to Stripe to complete it. Just be sure to hint the user that in case he has to confirm his card twice, it won't be charged twice but only once!
I was not able to create a working version of #snieguu's solution because I wanted to use Elements and not collect the credit card details separately to then create a PaymentMethod by myself.
Have You considered the opposite approach that payment intents(Also the first one) will be generated by subscription - not created manually?
So the flow will be:
Create a payment method
Create Customer(using the payment method)
Create Subscription(using Customer and payment method) - that creates also the first invoice
Retrieve payment intent from Subscription by latest_invoice.payment_intent.id. Here You can choose if this should be handled by You or Stripe. See this: How to get PaymentIntent next_action.type = redirect_to_url instead of use_stripe_sdk for Subscription
Allow finishing 3D secure flow
You have a constant price for a subscription, so it will be charged upfront:
https://stripe.com/docs/billing/subscriptions/multiplan#billing-periods-with-multiple-plans
Conventional plans that charge a fixed amount on an interval are billed at the start of each billing cycle.
We are using Laravel Cashier (Braintree) with Laravel version 5.8. We have a case where a user is subscribed to same plan with same name multiple times for different orders.
We want to give the ability to user to cancel their subscription.
we tried below statement to cancel the subscription with subscription name as suggested by manual here https://laravel.com/docs/5.8/braintree#cancelling-subscriptions.
$user->subscription('main')->cancel();
$user->subscription('main')->cancelNow();
We are passing the subscription name. It works fine as expected and also updating the date in "ends_at" column of subscription table.
The problem here is that as we have same name for the subscriptions where user is subscribed to. So in our case it returns the last subscribed order here and cancel that. It's fine as what it is suppose to do.
But we want to cancel the subscription based on braintree_id stored in subscriptions table. Can we do that ?
As of now we tried it like below:-
use Braintree\Subscription;
$subcriptionObj = Subscription::find($subscription); //where $subscription is braintree_id from subscriptions table.
if ($subcriptionObj->status == 'Canceled')
abort(400, 'Subscription Not Active');
Subscription::cancel($subscription);
This however cancel the subscription at Braintree but not updating the column "ends_at" in subscriptions table.
Can anyone suggests a workaround for this ? Any help would be appreciated.
Since the Laravel Braintree Cashier module is internally using Braintree Subscription library. So I thought to use the same directly into my controller.
I used namespace into my controller for subscription to call the Braintree subscription class directly. the below is the code to cancel the subscription by subscription ID.
use Braintree\Subscription;
public function cancelsubscription(User $user, $subscriptionId)
{
$subcriptionObj = Subscription::find($subscriptionId);
if(is_null($subcriptionObj)){
abort(400, 'Subscription is not found.');
}
if ($subcriptionObj->status == 'Canceled')
abort(400, 'Subscription is not Active.');
// In below line we are finding the Subscription DB Obj using cashier module here to update the ends_at date column
$subsDbObj = $user->subscriptions->filter(function($sub) use ($user,$subscriptionId){
return $sub->braintree_id == $subscriptionId && $sub->user_id == $user->id;})->values();
Subscription::cancel($subscriptionId);
if(! is_null($subsDbObj[0])){
//Internally cashier module doing the same to update the subscription table
$subsDbObj[0]->ends_at = Carbon::now();
$subsDbObj[0]->save();
}
return 'Cancelled';
}
My code is simple:
if($user->subscribed('main')){
$user->subscription('main')->cancel();
}
I get this response:
Stripe \ Error \ InvalidRequest
No such subscription: sub_EQTvxKjit2Ak6i
The subscription was in fact previously cancelled, so it shouldn't be giving a true response.
I tried returning $user->subscribed('main') and it came back as true.
This user has other subscriptions, but 'main' is not active.
Am I missing something?
Update Cashier v13.4
You can first sync the database entry with Stripe using:
optional($user->subscription('main'))->syncStripeStatus();
Then call
if ($user->subscribed('main')) { ... }
$user->subscribed() only checks the database for subscription status--it doesn't query the Stripe API at all. But when you try to cancel that subscription, then it queries the API.
So that could produce an error like this if your database is out of sync with your data in Stripe. Maybe your database has subscription from Stripe test mode, but you're querying the Stripe production API? Or vice versa?
If you read Billable trait then you will find this function there
public function subscribed($subscription = 'default', $plan = null)
{
$subscription = $this->subscription($subscription);
if (is_null($subscription)) {
return false;
}
if (is_null($plan)) {
return $subscription->valid();
}
return $subscription->valid() &&
$subscription->stripe_plan === $plan;
}
This function only checks subscription record exists in subscriptions table or not.
According to above function if subscription does not exist then it will return false.
Make sure you are not doing any mistake.
I've just updated Laravel cashier package from 5 version to the latest 6 version. It supports multiple subscriptions and it's really cool. But I've got one problem with renewing subscription after subscription cancellation.
I'm removing subscription manually from stripe dashboard and customer.subscription.deleted event is firing.
Cashier method is catching this event:
\Laravel\Cashier\Http\Controllers\WebhookController#handleWebhook
And $subscription->markAsCancelled(); is firing.
From that moment subscription cannot be renewed. I've tried to use resume() function, but subscription can be resumed only(!) on grace period.
In previous version of cashier I was using swap() method to resume subscription. Now it returns:
Stripe\Error\InvalidRequest: Customer cus_*** does not have a subscription with ID sub_***** in /**/vendor/stripe/stripe-php/lib/ApiRequestor.php:103 from API request 'req_****'
Creating new customer and subscription is not very efficient way. What your thoughts about this issue?
My solution at this moment:
public function resume()
{
$user = Auth::user();
$subscription = $user->subscription(ServicePackageRepository::SUBSCRIPTION_NAME);
if ($subscription->cancelled() && $subscription->onGracePeriod()) {
//if it was cancelled by user in grace period
$subscription->resume();
return $this->respondWithSaved([]);
} else { //if cancelled by payment failure or smth else...
if($user->subscription(ServicePackageRepository::SUBSCRIPTION_NAME)) {
$user->newSubscription(ServicePackageRepository::SUBSCRIPTION_NAME,
$user->subscription(ServicePackageRepository::SUBSCRIPTION_NAME)->stripe_plan)
->create();
return $this->respondWithSaved([]);
} else {
return $this->respondWithError([]);
}
}
}