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

Payment Intents #636

Closed
driesvints opened this issue Apr 11, 2019 · 17 comments
Closed

Payment Intents #636

driesvints opened this issue Apr 11, 2019 · 17 comments
Milestone

Comments

@driesvints
Copy link
Member

driesvints commented Apr 11, 2019

The new Payment Intents API is needed to make payments SCA compliant. This will require some significant changes in the public API of Cashier as well as making Cashier relient on webhooks to verify payments.

SCA Info: https://stripe.com/docs/strong-customer-authentication
Payment Intents: https://stripe.com/docs/payments/payment-intents
Billing migration guide: https://stripe.com/docs/billing/migration/strong-customer-authentication

@driesvints driesvints added this to the v10 milestone Apr 11, 2019
@MarGul
Copy link
Contributor

MarGul commented Apr 12, 2019

Instead of being reliant on webhooks we could maybe use the Manual confirmation? The code doesn't look to hard to write. You would have to have some type of API that handles the different statuses and send back responses to the front-end. Here is the docs: https://stripe.com/docs/payments/payment-intents/quickstart#flow-manual-confirmation

@driesvints
Copy link
Member Author

@MarGul nice. They only recently posted that I believe. Gonna look at this in detail when I get to this issue.

@garygreen
Copy link

garygreen commented Apr 12, 2019

I too am looking into this, not only as a way to be SCA compliant but also to help prevent bank disputes. It's a pretty critical feature.

The flow seems a bit ambiougs to me. If anyone can make sense of it / clarify could you comment? It would also help Cashier implement a flow that works with plans/subscriptions.

The Payment Intent Flow (as I understand it) - using stripe js / php and with a subscription / plan

  1. You create a payment intent on server side when you hit the checkout page (and store this in session for future use) using the details of your plan:
$plan = \Stripe\Plan::retrieve('your-subscription-plan-id', [
    'api_key' => config('services.stripe.secret'),
]);

$paymentIntent = \Stripe\PaymentIntent::create([
    'amount'               => $plan->amount,
    'currency'             => $plan->currency,
    'payment_method_types' => ['card'],
], [
    'api_key' => config('services.stripe.secret'),
]);

You can then pass the payment intent details onto your view / form so that it can be used in js when you eventually make a call to handleCardPayment

  1. In javascript, you collect card details / mount etc in the usual way and instead of making a call to stripe.createToken() instead you call stripe.handleCardPayment():
stripe.handleCardPayment(document.querySelector('input[name="payment_intent_secret"]').value, card).then(function(result) {
    if (result.error) {
        // Payment failed.
    } else {
        // Payment successful.
    }
});
  1. This is where I get a bit confused, but I'm assuming at that point if the payment was successful, you can create a token for the user and susbcribe them to the plan. I would imagine you would want it to skip the first payment as you have already paid for the first month in the payment intent? Maybe this is where trial period comes into play? I'm also not sure where the web hook comes into play here, as I would assume if the callback is successful then you don't need to do any further checks?

  2. At that point on the server you can clear the payment intent - maybe this is where the web hook comes into play? Is it needed though when you can just get the payment intent when you hit the checkout page, check its status to see if its "expired" and instead use a new one:

$paymentIntent = null;
if (session('paymentIntentId')) {
    $paymentIntent = \Stripe\PaymentIntent::retrieve(session('paymentIntentId'), [
        'api_key' => config('services.stripe.secret'),
    ]);
}

$expiredPaymentIntent = $paymentIntent && in_array($paymentIntent->status, ['canceled', 'succeeded']);

if (! $paymentIntent || $expiredPaymentIntent) {
   // Create new payment intent here, as its either not created or old one has expired.
   // ... 
   // ..
   // Remember it in session so we don't create a new payment intent everytime checkout page is reloaded:
   session()->put('paymentIntentId', $paymentIntent->id, now()->addDay());
}

If anyone can shed light on the flow / clarify how they've got it working in their app that would be much appreciated.

@driesvints
Copy link
Member Author

Hey @garygreen, thanks for helping out! I'll look into your reply as soon as I get to the issue.

@garygreen
Copy link

Ok so I spoke with Stripe and they have informed me this is the best place to understand the workflow for payment intents with subscriptions: https://stripe.com/docs/billing/subscriptions/payment

(that docmentation only went live on Thursday, so its still a WIP but it's much better place to understand than the other docs because they focus on one-off payments rather than subscriptions)

The key factor is a subscription may now enter a new status called incomplete which means it will have a payment_intent object and a client secret - you then need to use something like Stripe.js to go back to the browser with the payment intent secret and call the Stripe.js with:

var stripe = Stripe('key here');

// This can be found on invoice.payment_intent.client_secret
var paymentIntentSecret = 'pi_91_secret_W9';

stripe.handleCardPayment(
    paymentIntentSecret
).then(function (result) {
    if (result.error) {
        // Display error.message in your UI.
    } else {
        // The payment has succeeded. Display a success message.
    }
})

... that will then prompt for any 3D secure payments / other things needed to complete the payment. A webhook will then fire to note that the payment went thru invoice.payment_succeeded

Hopefully that helps. The docs explain it better but that's how I've understood it.

@driesvints
Copy link
Member Author

@garygreen that helps! We actually need to revert the behavior I added in #631 because of the incomplete status you mentioned above. It was a good fix for 9.x because it reverted Cashier to the same behavior as before but with Payment Intents we indeed need to accommodate for a window where the customer is making the payment with 3D secure, etc. Webhooks will later update the subscription if payment failed.

@garygreen
Copy link

garygreen commented Apr 16, 2019

Bit more info - you can also pass a expand parameter during the creation of the subscription which allows you to access the PaymentIntent object without creating multiple API requests. This will be very useful when needing to go back to the browser/client with the payment intent secret to do any nessscary steps to complete the payment.

Example in node, but hopefully get the idea:

let subscription = await stripe.subscriptions.create({
    customer: customer.id,
    items: [
        {
            plan: "plan_DQYe83yUGgx1LE",
        },
    ],
    expand : ["latest_invoice.payment_intent"]
}); 

@garygreen
Copy link

Also I've had to override the SubcriptionBuilder payload method in order for Stripe to not fail payment instantly if needing 3D check:

<?php

namespace App\Cashier;

use Laravel\Cashier\SubscriptionBuilder as LaravelSubscriptionBuilder;

class SubscriptionBuilder extends LaravelSubscriptionBuilder
{
	/**
	 * Build the payload for subscription creation.
	 *
	 * @return array
	 */
	protected function buildPayload()
	{
		return array_merge(parent::buildPayload(), [
			'enable_incomplete_payments' => true
		]);
	}
}

I've got payment intents mostly working locally now with current version of Cashier. Still a work in progress (understanding the hooks etc), but it's looking good so far.

@bilfeldt bilfeldt mentioned this issue Apr 23, 2019
@bilfeldt
Copy link

bilfeldt commented Apr 23, 2019

Just wanted to share information about upcoming changes being added to Stripe on Juli 1, 2019 regarding Subscriptions.

Further changes needed for subscription in EU

If you are based in Europe and preparing for Strong Customer Authentication (SCA) is a new regulatory requirement coming into effect on September 14, 2019 which will impact many European online payments. It requires customers to use two-factor authentication like 3D Secure to verify their purchase, you will need to make further changes after July 1 in order to perform authentication when saving a card for subsequent off-session payments to qualify for off-session exemptions. This API will be available by July 1. Source

@driesvints
Copy link
Member Author

@bilfeldt thanks for noting that. I'll create a separate issue for this. We'll release an update for this once the API has been released.

@driesvints
Copy link
Member Author

I've been working on a PR for this and it's coming along nicely. Follow along here: #667

driesvints added a commit that referenced this issue Jun 28, 2019
These changes bring support for the new payment intents api to charges and subscriptions. As there are quite some breaking changes here let's go over the most prominent ones below:

Any payment action will now throw an exception when a payment either fails or when the payment requires a secondary action in order to be completed. This goes for single charges, invoicing customers directly, subscribing to a new plan or swapping plans. Developers can catch these exceptions and decide for themselves how to handle these by either letting Stripe handle everything for them (to be set up in the Stripe dashboard) or use the custom built-in solution which will be added in the next commit.

A new status column is introduced for subscriptions as well. Whenever an attempt is made to subscribe to a plan but a secondary payment action is required, the subscription will be put into a state of "incomplete" while payment confirmation is awaited. As soon as payment has been properly processed, a webhook will update the subscription's status to active.

After these changes, webhooks will be a fundamental part of how Cashier works and they're now required in order to properly handle any payment confirmations and off-session updates to subscriptions & customers.

The charge method now only accepts a payment method instead of a token. Developers will need to update their JS integration to retrieve a payment method id instead of a source token. These changes were done because this is now the recommended way by Stripe to work with payment methods. More info about that can be found here: https://stripe.com/docs/payments/payment-methods#transitioning
In an upcoming update all card methods as well as the create method on the subscription builder will be updated as well.

Closes #636
driesvints added a commit that referenced this issue Jul 2, 2019
These changes bring support for the new payment intents api to charges and subscriptions. As there are quite some breaking changes here let's go over the most prominent ones below:

Any payment action will now throw an exception when a payment either fails or when the payment requires a secondary action in order to be completed. This goes for single charges, invoicing customers directly, subscribing to a new plan or swapping plans. Developers can catch these exceptions and decide for themselves how to handle these by either letting Stripe handle everything for them (to be set up in the Stripe dashboard) or use the custom built-in solution which will be added in the next commit.

A new status column is introduced for subscriptions as well. Whenever an attempt is made to subscribe to a plan but a secondary payment action is required, the subscription will be put into a state of "incomplete" while payment confirmation is awaited. As soon as payment has been properly processed, a webhook will update the subscription's status to active.

After these changes, webhooks will be a fundamental part of how Cashier works and they're now required in order to properly handle any payment confirmations and off-session updates to subscriptions & customers.

The charge method now only accepts a payment method instead of a token. Developers will need to update their JS integration to retrieve a payment method id instead of a source token. These changes were done because this is now the recommended way by Stripe to work with payment methods. More info about that can be found here: https://stripe.com/docs/payments/payment-methods#transitioning
In an upcoming update all card methods as well as the create method on the subscription builder will be updated as well.

Closes #636
@driesvints
Copy link
Member Author

The PR was merged 🎉

@damayantinama
Copy link

I too am looking into this, not only as a way to be SCA compliant but also to help prevent bank disputes. It's a pretty critical feature.

The flow seems a bit ambiougs to me. If anyone can make sense of it / clarify could you comment? It would also help Cashier implement a flow that works with plans/subscriptions.

The Payment Intent Flow (as I understand it) - using stripe js / php and with a subscription / plan

  1. You create a payment intent on server side when you hit the checkout page (and store this in session for future use) using the details of your plan:
$plan = \Stripe\Plan::retrieve('your-subscription-plan-id', [
    'api_key' => config('services.stripe.secret'),
]);

$paymentIntent = \Stripe\PaymentIntent::create([
    'amount'               => $plan->amount,
    'currency'             => $plan->currency,
    'payment_method_types' => ['card'],
], [
    'api_key' => config('services.stripe.secret'),
]);

You can then pass the payment intent details onto your view / form so that it can be used in js when you eventually make a call to handleCardPayment

  1. In javascript, you collect card details / mount etc in the usual way and instead of making a call to stripe.createToken() instead you call stripe.handleCardPayment():
stripe.handleCardPayment(document.querySelector('input[name="payment_intent_secret"]').value, card).then(function(result) {
    if (result.error) {
        // Payment failed.
    } else {
        // Payment successful.
    }
});
  1. This is where I get a bit confused, but I'm assuming at that point if the payment was successful, you can create a token for the user and susbcribe them to the plan. I would imagine you would want it to skip the first payment as you have already paid for the first month in the payment intent? Maybe this is where trial period comes into play? I'm also not sure where the web hook comes into play here, as I would assume if the callback is successful then you don't need to do any further checks?
  2. At that point on the server you can clear the payment intent - maybe this is where the web hook comes into play? Is it needed though when you can just get the payment intent when you hit the checkout page, check its status to see if its "expired" and instead use a new one:
$paymentIntent = null;
if (session('paymentIntentId')) {
    $paymentIntent = \Stripe\PaymentIntent::retrieve(session('paymentIntentId'), [
        'api_key' => config('services.stripe.secret'),
    ]);
}

$expiredPaymentIntent = $paymentIntent && in_array($paymentIntent->status, ['canceled', 'succeeded']);

if (! $paymentIntent || $expiredPaymentIntent) {
   // Create new payment intent here, as its either not created or old one has expired.
   // ... 
   // ..
   // Remember it in session so we don't create a new payment intent everytime checkout page is reloaded:
   session()->put('paymentIntentId', $paymentIntent->id, now()->addDay());
}

If anyone can shed light on the flow / clarify how they've got it working in their app that would be much appreciated.

show this error after upgrade cashier 10 from 9

image

How to solve it?

Thanks in advance!

@u01jmg3
Copy link
Contributor

u01jmg3 commented Sep 26, 2019

From the upgrade guide:

The useCurrency method has been replaced by a configuration option in the new Cashier configuration file and the usesCurrency method has been removed.

./config/cashier.php: https://github.com/laravel/cashier/blob/10.0/config/cashier.php#L73


💡 I would suggest reading the whole of the upgrade guide as there could be other gotchas.

@damayantinama
Copy link

usesCurrency

for this Url not geeting where and which one file nedd to change.

@damayantinama
Copy link

may this problem create cashier 10 and spark 7+

@driesvints
Copy link
Member Author

I've already asked you twice to please ask this on a support channel. Please read the upgrade guide thoroughly.

@laravel laravel locked as off-topic and limited conversation to collaborators Sep 26, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants