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

[12.x] Stripe Checkout Support #1007

Merged
merged 11 commits into from
Jan 27, 2021
Merged

[12.x] Stripe Checkout Support #1007

merged 11 commits into from
Jan 27, 2021

Conversation

driesvints
Copy link
Member

@driesvints driesvints commented Sep 29, 2020

This PR adds functionality for Stripe checkout and is a continuation of #652. All concerns on the original PR have been resolved since.

Docs: laravel/docs#6465

Usage

Checking out a single priced product:

$checkout = Auth::user()->checkout('price_xxx');

return view('checkout', compact('checkout'));

Checking out a single priced product with a specific quantity:

$checkout = Auth::user()->checkout(['price_xxx' => 5]);

return view('checkout', compact('checkout'));

Checking out multiple priced products and optionally assign quantities to some:

$checkout = Auth::user()->checkout(['price_xxx' => 5, 'price_yyy']);

return view('checkout', compact('checkout'));

Checking out a single priced product and allow promotional codes to be applied:

$checkout = Auth::user()->allowPromotionCodes()->checkout('price_xxx');

return view('checkout', compact('checkout'));

Checking out a new $12 priced product (this will create a new product in the dashboard):

$checkout = Auth::user()->checkoutCharge(1200, 'T-shirt');

return view('checkout', compact('checkout'));

Checking out a new $12 priced product together with a Price ID product:

$checkout = Auth::user()->checkout([
    'price_xxx', 
    [
        'price_data' => [
            'currency' => Auth::user()->preferredCurrency(),
            'product_data' => [
                'name' => 'T-Shirt',
            ],
            'unit_amount' => 1200,
        ],
    ],
]);

return view('checkout', compact('checkout'));

Check out a subscription:

$checkout = Auth::user()->newSubscription('default', 'price_xxx')->checkout();

return view('checkout', compact('checkout'));

Check out a subscription and allow promotional codes to be applied:

$checkout = Auth::user()->newSubscription('default', 'price_xxx')
    ->allowPromotionCodes()
    ->checkout();

return view('checkout', compact('checkout'));

Then place any of these in a view:

{!! $checkout->button() !!}

Button Styling

By default a generated checkout button will have the following styling:

Screenshot 2020-10-05 at 18 00 30

It's however easy to style this. Pass either a custom style or class attribute:

{!! $checkout->button('Buy', ['class' => 'text-white bg-blue-500 p-4']) !!}

Todos

  • Subscribing to plans
  • Charging products
  • Single charges
  • Webhooks
  • Style button
  • Tests
  • Write documentation

Closes #637

This was referenced Oct 2, 2020
@driesvints
Copy link
Member Author

Wrote the docs for this today: laravel/docs#6465

@driesvints
Copy link
Member Author

We've updated checkout($amount, $productName, ...) and checkoutProduct($productPrice, ...) to checkout($productPrice, ...) & checkoutCharge($amount, $productName, ...) since you'd mostly want to checkout predefined products instead of creating ad-hoc products which pollute your dashboard.

@JhumanJ
Copy link

JhumanJ commented Oct 12, 2020

Hi,
I've been following this issue for some time, and I'm looking for it to be merged. I needed this functionality so I built something similar on my own while this gets merged. However, I'm having an issue, so I was curious to know if you were also facing it.

Whenever I create a subscription via checkout, Stripe send to webhooks, one for the subscription creation (with a status incomplete) and one for the subscription update (setting the status to active). These two hooks usually arrive at the exact same time (or very close). Therefore when the update hook handle method runs, it occurs that the subscription was not created yet in my database. Because of that, the subscription will not be active in my DB causing some obvious logical isssues...

Any thoughts on that ?

@cjavilla-stripe
Copy link

@JhumanJ The order that webhook events are delivered isn't guaranteed. There are a couple options. (1) listen for the checkout.session.completed event. That'll let you know when the session completes and you'll know the Subscription is started. (2) push the events into a queue, then process later in the background in order.

@JhumanJ
Copy link

JhumanJ commented Oct 12, 2020

@cjavilla-stripe I understand the issue, but I'm worried that everyone will be facing it with the current state of this pull request...

@driesvints
Copy link
Member Author

Copy link

@dannywp dannywp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any code that would update the User's default payment method. Am I missing something or has it been overlooked?

@driesvints
Copy link
Member Author

@dannywp the current customer.updated webhook already handles that in Cashier.

@dannywp
Copy link

dannywp commented Oct 23, 2020

@driesvints correct, but only if the stripe customer has a default payment method. I have been implementing stripe hosted checkout for subscriptions and it seems like the card that is attached to the customer is not set as the default payment method.

@driesvints
Copy link
Member Author

@dannywp last time I tried it it worked for me. You need to explicitly check it as the new default payment method.

@driesvints
Copy link
Member Author

@JhumanJ heya, you're gonna have to help me walk through on how to recreate your situation. I haven't had a situation yet where the original subscription was set to incomplete first and the subscription update event was triggered. Can you share the specifics on how to recreate your use case?

@cjavilla-stripe
Copy link

cjavilla-stripe commented Oct 23, 2020

@dannywp to jump in with a bit more context. When creating a subscription with Checkout, a new PaymentMethod is created and a new Subscription is created. You can either create the Checkout session with or without a customer object. If no customer object is provided, then a new customer will also be created.

When collecting recurring payments for a subscription, Stripe will determine which payment method to use for the invoice by looking in a few places.

First, we'll look at the Subscription itself to see if it has a default_payment_method set. This is where the payment method is set in the case of Checkout in subscription mode. This payment method will be used when collecting future payments for this subscription unless the default payment method on the subscription is changed or removed.

Second, we'll look at the Subscriptions default_source. This is a deprecated field and is not commonly set, is not used by Checkout, and I'd recommend against using default_source anymore.

Third, if there is no default_payment_method or default_source set on the Subscription, we'll next look at the customer's invoice_settings.default_payment_method. This is not set by Checkout by default. This is a field that is only for Billing (Subscriptions and Invoices) and does not act as a default for one time payments with PaymentIntents. If you're creating a second subscription for a customer who's first subscription was created with Checkout, you might want to make an API call to set the customer's invoice_settings.default_payment_method to be equal to the first subscription's default_payment_method.

Fourth, we'll look at the customer's default_source also deprecated, and also not recommended.

If you are collecting payments outside of the automatic recurring payments that result from the Subscription created with Checkout, I would recommend storing the ID of the default_payment_method from the original subscription as a field in your db, then using that for future payments. (I don't know whether or not this is built into the existing Checkout flow with cashier :D)

Hope this helps 👍

@StephenGillCoop
Copy link

@driesvints I'm involved with a charity trying to solve the need for foodbanks and was thinking of waiting for this PR to be merged before implementing the "donate" function on their website.

Are you hoping to have this merged in the next few weeks, or is it more likely to be next year? Completely understand either way. If I had the expertise I would offer to help.

@driesvints
Copy link
Member Author

@StephenGillCoop We're currently evaluating the feature internally. I don't have an ETA, sorry.

@driesvints
Copy link
Member Author

@JhumanJ I again tried out scenarios with test cards from https://stripe.com/docs/testing but don't encounter any unexpected bug from happening. I think it's best that you be really specific about the scenario that you encounter if I'm to recreate it.

@andrebreia
Copy link

andrebreia commented Nov 13, 2020

I just had the same issue as @JhumanJ. In my case the customer.subscription.created webhook failed (had an error in my code) and customer.subscription.updated succeeded, which caused the subscription status to be incomplete when the created webhook was re-sent.

@driesvints
Copy link
Member Author

@andrebreia we can't account for errors in your code. If you don't have an error in your code and the webhooks work properly then I think the feature works as expected.

@andrebreia
Copy link

@driesvints That's fair. Just thought that there might be situations where the webhook could fail.

@driesvints
Copy link
Member Author

driesvints commented Dec 1, 2020

@abbasali thanks for that input. I've pushed some changes which now allow checkout to be used with multiple items. I've updated the examples in the PR description to show how you can use them. Does that seem okay to you?

These changes have removed the "quantity" parameter on the checkout method.

@abbasali
Copy link

abbasali commented Dec 1, 2020

@driesvints The changes look good, thanks. However, it doesn't support the inline pricing object. Would be nice to have inline price_data support so that dynamic prices like shipping costs can be added to the checkout.

@driesvints
Copy link
Member Author

@abbasali it does have support for that though:

$checkout = Auth::user()->checkout([
     [
          'price_data' => [...]
     ],
]);

return view('checkout', compact('checkout'));

@abbasali
Copy link

abbasali commented Dec 1, 2020

Ok cool. Wasn't in the PR docs so thought it isn't there :). Thanks.

@driesvints
Copy link
Member Author

@abbasali added an example. It's a bit more cumbersome to add a custom item together with existing price ID products. I'm not sure what we can do there to improve the API.

@abbasali
Copy link

abbasali commented Dec 1, 2020

@driesvints What if the API is changed to something like..

$checkout = $user->checkout()
    ->addItem('price_xxx', 1)
    ->addItem('price_yyy')
    ->addItem(['price_data' => [...]])
    ->create();

or something along those lines?

Otherwise, the current API looks good. Yes, the custom price is a bit cumbersome but if documented and explained properly - it should be fine.

@matheuscandido
Copy link

Can I use this branch on my Laravel project while it hasn't been merged to 12.x?

@driesvints
Copy link
Member Author

@matheuscandido purely on your own risk as we might merge and delete this branch at any time. Forking the package and requiring it as a VCS might be the better choice.

@driesvints
Copy link
Member Author

@abbasali yeah I'm just gonna leave it as is I think.

@abbasali
Copy link

abbasali commented Dec 8, 2020

Can I use this branch on my Laravel project while it hasn't been merged to 12.x?

@matheuscandido I also needed the Checkout feature. So what I did is created a custom trait PerformsCheckout and copied the PerformsCharges::checkout() and related methods in that trait. Then created a class Checkout and copied src/Checkout.php's code into it and used this in PerformsCheckout instead of cashier's Checkout.

Then in User model, I used the PerformsCheckout trait and I am now able to do $user->checkout('price_xxx');

Lastly, for the button HTML, I created a custom view (again copied the code from this branch).

I didn't need the subscription logic so left out all other modified files/logic from this branch and just the above 3 files made it work for me. Later when this branch goes into 12.x, I will simply remove the trait PerformsCheckout from my User model and everything (theoretically) should work with 12.x's new checkout code (from PerformsCharges cashier trait).

@amirkoklan
Copy link

is there any timeline for this branch to be merged?

@driesvints
Copy link
Member Author

No sorry

@driesvints driesvints marked this pull request as ready for review January 26, 2021 10:24
@taylorotwell taylorotwell merged commit e9038c6 into 12.x Jan 27, 2021
@driesvints driesvints deleted the stripe-checkout branch January 28, 2021 08:16
@driesvints
Copy link
Member Author

This will be in next week's release!

@abbasali
Copy link

Great news! Thanks for the update.

@naumanzchaudhry
Copy link

naumanzchaudhry commented Mar 31, 2021

Fixed priced items don't seem to make it to database? I am using $user->checkout($priceId, [ // options])? Any pointers? Have all the webhooks setup mentioned in the docs properly.

p.s i can get to the success url

@driesvints
Copy link
Member Author

Please open an issue

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.

Stripe Checkout