Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.0] Implement Stripe Checkout #652

Closed
wants to merge 7 commits into from
Closed

[10.0] Implement Stripe Checkout #652

wants to merge 7 commits into from

Conversation

driesvints
Copy link
Member

@driesvints driesvints commented Apr 23, 2019

This PR adds functionality for Stripe checkout. At the moment it'll initiate a new server side session so we can add the checkout to an existing user. Unfortunately I just discovered that this isn't implemented by Stripe yet: https://stripe.com/docs/payments/checkout/server#span-classstepoptionalspan-using-existing-customers

You may specify an existing Customer object for Checkout to use when making one-time payments (this does not yet work with plans and subscriptions).

We'll have to wait until Stripe implements this before continuing with subscriptions. We can already see at implementing checkout for single charges though.

Todo

Concerns

Below I'll try to list all concerns I encounter during development.

Existing customers can't yet be passed along when subscribing to a plan ✅

This is vital for Cashier to work so until this gets implemented on Stripe's end this PR is basically blocked.

Screen Shot 2019-04-26 at 20 48 58

https://stripe.com/docs/payments/checkout/migration#client-subscriptions

Update 02/10/20: Stripe has since provided support for this.

Trial days seem to work differently for Checkout sessions ✅

Current api for subscriptions: https://stripe.com/docs/api/subscriptions/create#create_subscription-trial_end

Screen Shot 2019-04-26 at 20 55 06

Api when creating Checkout sessions: https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-subscription_data-trial_end

Screen Shot 2019-04-26 at 20 54 41

This basically means that you're always required to give either a 48h period or 1 day period when subscribing through Checkout. This seems unwanted and weird. I'll check this over with Stripe.

Update 30/04: Stripe confirmed that the 48h limitation is in place for the following reason:

Checkout Sessions are active for 24 hours after their creation and within that timeframe, your customer can complete the payment at any time. We require the trial start least 48 hours in the future so that there is still at least a 1 day trial if your customer pays at the end of the 24 hours.

I've implemented this here: ff9946d

Unclear how to upgrade or downgrade using Checkout ✅

The migration guide covers creating new subscriptions but no mention is made on how to upgrade or downgrade plans. We'll need this before support for plan swapping can be given.

Update 29/04: Stripe has confirmed that this isn't included yet.

Update 02/10/20: Stripe has since released their new customer portal that takes care of swapping plans.

Unclear how one-off invoices work ✅

At the moment it's unclear how the invoice method on the billable entity would work with Checkout (or payment intents). I've asked this question to Stripe on Twitter and awaiting their feedback.

Update 02/10/20: I'm not sure why I brought this up back when I was working on Stripe Checkout support. This isn't needed at all for Checkout.

@driesvints driesvints mentioned this pull request Apr 23, 2019
@driesvints driesvints force-pushed the checkout branch 2 times, most recently from 26a6992 to 03b39c2 Compare April 26, 2019 11:53
@driesvints
Copy link
Member Author

While working on the webhook after a session was completed I was doubting between choosing the subscription created webhook or checkout session completed. Will need to investigate further.

Also thinking about maybe creating the subscription already in the database when creating the session since we have a subscription ID normally: https://stripe.com/docs/api/checkout/sessions/create

@driesvints driesvints force-pushed the checkout branch 3 times, most recently from 7f8a751 to aa4fd0b Compare May 3, 2019 13:46
@Lemmings19
Copy link

Lemmings19 commented May 29, 2019

For what it's worth, passing an existing customer while creating a subscription with Checkout still isn't available. :(

image

https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-customer

@driesvints
Copy link
Member Author

Since Stripe still hasn't updated their API with the necessary changes in order to let this move forward we're taking this out of the next release. We'll have to wait and see when Stripe makes the necessary changes listed above.

@taylorotwell
Copy link
Member

I'm going to close this for now until we decide to revisit it.

@driesvints
Copy link
Member Author

@taylorotwell sure thing 👍

@driesvints driesvints deleted the checkout branch July 12, 2019 14:04
@smartsyncio
Copy link

Just wanted to add here that seems you can now link existing customers https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-customer

@driesvints
Copy link
Member Author

@smartsyncio nice. Maybe we can pick it up for a future version.

@NSpehler
Copy link

@driesvints Would you kindly be able to give up pointers on how to implement that ourselves? Not sure what is missing from your current PR.

@driesvints
Copy link
Member Author

@NSpehler see the concerns above

@rdegregorio
Copy link

rdegregorio commented Sep 11, 2019

@driesvints thanks for raising these issues. I reached out to Stripe and this is what they responded:

"there is the ability with New Checkout to use existing customers for subscriptions, this just has to be done through the API. See here: https://stripe.com/docs/payments/checkout/server#using-existing-customers"

Is/Will Cashier be updated ? Any info greatly appreciated :) If you could help prioritize this it would be greatly appreciated.

Ricardo

@driesvints
Copy link
Member Author

I still want to get this in but probably won't have the time in the upcoming months sorry. We're still open to pull requests.

@faustbrian
Copy link

faustbrian commented Dec 8, 2019

@driesvints is it still planned to implement this? I recently implemented subscriptions with cashier and the stripe checkout based on your code without too much pain. The only thing that had to be added was a handleCheckoutSessionCompleted method to add the payment method that the user chose to their account. If you don't collect the payment details via stripe checkout than it seems to work just fine without that extra handler.

What exactly were the main issues for not implementing and closing this?

<?php

namespace App\Http\Controllers;

use App\Services\Stripe;
use Illuminate\Support\Carbon;
use Laravel\Cashier\Http\Controllers\WebhookController as Controller;
use Symfony\Component\HttpFoundation\Response;

class WebhookController extends Controller
{
    private $stripe;

    public function __construct(Stripe $stripe)
    {
        parent::__construct();

        $this->stripe = $stripe;
    }

    protected function handleCheckoutSessionCompleted(array $payload)
    {
        // Setup Intent...
        $setupIntent = $this->stripe->retrieveSetupIntent($payload['data']['object']['setup_intent']);

        // Payment Method...
        $paymentMethod = $this->stripe->retrievePaymentMethod($setupIntent->payment_method);

        // Customer...
        if ($user = $this->getUserByStripeId($payload['data']['object']['customer'])) {
            // Update Default Payment Method...
            $user->updateDefaultPaymentMethod($paymentMethod);

            // Update Billing Address...
            $address = $paymentMethod->billing_details->address;

            $user->forceFill([
                'billing_address'        => $address->line1,
                'billing_address_line_2' => $address->line2,
                'billing_city'           => $address->city,
                'billing_state'          => $address->state,
                'billing_zip'            => $address->postal_code,
                'billing_country'        => $address->country,
            ])->save();
        }

        return $this->successMethod();
    }

    protected function handleCustomerSubscriptionCreated(array $payload)
    {
        $user = $this->getUserByStripeId($payload['data']['object']['customer']);

        file_put_contents(storage_path('test.json'), json_encode($user->toArray()));

        if ($user) {
            $data = $payload['data']['object'];

            if (! $user->subscriptions->contains('stripe_id', $data['id'])) {
                if (isset($data['trial_end'])) {
                    $trialEndsAt = Carbon::createFromTimestamp($data['trial_end']);
                } else {
                    $trialEndsAt = null;
                }

                $user->subscriptions()->create([
                    'name'          => $data['metadata']['name'],
                    'stripe_id'     => $data['id'],
                    'stripe_plan'   => $data['plan']['id'],
                    'quantity'      => $data['quantity'],
                    'trial_ends_at' => $trialEndsAt,
                    'ends_at'       => null,
                ]);
            }
        }

        return new Response('Webhook Handled', 200);
    }
}

@driesvints
Copy link
Member Author

@faustbrian the concerns listed on this PR in the first comment weren't all resolved at the time of making this PR. They could be by now.

@rdegregorio
Copy link

@faustbrian were you able to migrate to the new stripe version ? I am still using the legacy one, concerning is that the legacy one isn't going to be updated anymore. It is also out of compliance in Europe.

@driesvints is this going to be released in Q1 / Q2 ? or at this stage there are no plans for updating cashier to support the new version of Stripe ?

Thanks

@rdegregorio
Copy link

ok thanks @driesvints

@driesvints
Copy link
Member Author

Hey Sorry, was a bit too quick to reply. We'd still welcome prs for this but aren't planning to be working on this ourselves at least not at this time.

@faustbrian
Copy link

faustbrian commented Jan 10, 2020

@faustbrian were you able to migrate to the new stripe version ? I am still using the legacy one, concerning is that the legacy one isn't going to be updated anymore. It is also out of compliance in Europe.

I got Stripe Checkout working without any problems with the code I posted above. Have a full test-suite for it with dusk, integration and unit tests and everything works as expected so should be fine to integrate it into Cashier directly.

@cihantas
Copy link

@faustbrian Would you be so nice and also add a code snippet of your session creation? AFAIK Stripe Checkout expects a session token, which differs from a setup intent.

@faustbrian
Copy link

<?php

namespace App\Services;

use App\Models\Plan;
use App\Models\Team;
use Carbon\Carbon;
use Laravel\Cashier\Cashier;
use Stripe\Checkout\Session;
use Stripe\PaymentMethod;
use Stripe\SetupIntent;

class Stripe
{
    public function createSession(Team $team, Plan $plan): Session
    {
        return Session::create([
            'customer'                   => $team->stripe_id,
            'payment_method_types'       => ['card'],
            'billing_address_collection' => 'required',
            'subscription_data'          => [
                'items' => [
                    [
                        'plan'     => $plan->stripe_id,
                        'quantity' => 1,
                    ],
                ],
                'metadata' => [
                    'name' => 'default',
                ],
                'trial_end' => Carbon::now()->addDays($plan->trial_days)->getTimestamp(),
            ],
            'success_url' => url('/'), // todo
            'cancel_url'  => url('/'), // todo
        ], Cashier::stripeOptions());
    }
}

@cihantas Hope that helps.

@larslommen
Copy link

@faustbrian I tried implementing your code, and it works as expected. Except for when stripe sends the "customer.subscription.updated" webhook before the "customer.subscription.created" webhook. The subscription gets created in the database, but the stripe_status column gets stuck at incomplete. Do you have a fix for this? Thanks.

@tonjohn
Copy link

tonjohn commented Jul 9, 2020

@faustbrian I'm in the process of implementing this but I'm missing App\Services\Stripe; / retrieveSetupIntent. Any tips?

@faustbrian
Copy link

@larslommen I have since then switched to paddle so would have to take a look but looking at my Stripe webhook dashboard it sends the right data.

@tonjohn retrieveSetupIntent was simply a wrapper for https://stripe.com/docs/api/setup_intents/retrieve. There is an example code snippet for PHP on the right side of the page.

@tylerwiegand
Copy link

@tonjohn I've also used @faustbrian method and it's working so far. Maybe it's my stripe settings or something, but I'm always getting NULL from the setup_intent key from the PaymentIntent object from Stripe. It doesn't matter much, as that only effects the local User model's card_brand, card_last_four etc which I'm not really using (since those are stored on the subscription itself).

Also needed to add stripe_status in the subscription.

Would I be correct in guessing that Cashier has largely ignored a lot of the new Checkout stuff until things seem to stabilize? Ive spun up a few apps with Stripe and Cashier over the last few years and each time I go to implement it its almost completely different lol

@faustbrian
Copy link

@tylerwiegand if you do anything Checkout related with Cashier you are on your own with the Stripe SDK like in my example. When it gets integrated into a Cashier is a question mark as it doesn't seem to be high priority which isn't that big of an issue since the implementation with the SDK is still fairly simple and quick.

@driesvints
Copy link
Member Author

Like I said on this pr a couple of times already: anyone is welcome to continue to work on a PR for this.

@driesvints driesvints mentioned this pull request Sep 29, 2020
7 tasks
@driesvints
Copy link
Member Author

I've picked up work on this in a new PR here: #1007

All concerns from listed on the original comment from this PR have been resolved ever since.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.