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 Element integration documentation #1541

Open
clementmas opened this issue Jun 19, 2023 · 12 comments
Open

Payment Element integration documentation #1541

clementmas opened this issue Jun 19, 2023 · 12 comments
Assignees

Comments

@clementmas
Copy link
Contributor

The Cashier Stripe documentation is only explaining how to use the Stripe UI Card Element, which is now marked as "Legacy". While the new Payment Element is not documented.

It seems like the JS code should now call await stripe.confirmPayment instead of await stripe.confirmCardSetup. But it requires defining a return_url. A payment_intent_client_secret value is then passed as GET query string parameter (not ideal), which I guess can be used with Cashier.

I think the documentation should explain how to set this up properly. Or is this not supported officially?

Thanks!

@clementmas
Copy link
Contributor Author

From my understanding of the Stripe documentation, they now recommend using the Stripe UI Payment Element with Setup Intents for subscriptions or Payment Intents for single charges.

Setup Intents should be confirmed using Stripe JS's stripe.confirmSetup() method and Payments Intent shoud call stripe.confirmPayment().

A return_url needs to be passed in. I believe it should redirect to a custom "processing" page that waits a few seconds before redirecting to a success page.
Ideally, it should poll the backend API to see if the payment succeeded before redirecting.

Meanwhile, new webhooks need to be configured:

  • setup_intent.succeeded for subscriptions only

    • it finds the User and its associated Stripe Customer account
    • this is where $user->newSubscription()->create() should be called
  • payment_intent.succeeded

    • it finds the User and its associated Stripe Customer account
    • fulfill the order (activate paid account or send goods)

Other webhooks should also be set up for error handling (setup_failed, payment_failed, requires_action, etc.).

Of course there are many ways to approach this. We could also fulfill the order listening to the invoice or charge webhooks but it seems like this would be the prefered solution.

What do you think?

@quantumwebco
Copy link
Contributor

I have this all set up and working with the payment element for both subscriptions and one off payments. I had to do a few workarounds as cashier doesn't seem to have all the required functionality to handle failed payments due to 3DS, but I am creating PRs to add this functionality

The payment element actually checks for validity of card before redirecting (insufficient funds, lost/stolen etc.), so certain errors are caught before it hits your server side code. 3DS also happens here.

When the payment element redirects you if it is a setup_intent it means the card was registered and if it's payment_intent it means the payment was successful. Therefore you don't need to listen for the webhook events and have a pending page and you have the payment/setup intent id in the url params which you can use to retrieve the intent from the Stripe API

If using setup_intent my code creates the payment method, sets it as default, then uses it to create a subscription "off_session". If this fails due to 3DS then it redirects to a form to confirm the payment

Mostly the same for payment intents and single charges charging the default payment method, except it just creates an order in our db or redirects if action is required

I'm happy to share examples

@driesvints
Copy link
Member

Thanks all. I'll dig into this once I have time for it.

@driesvints driesvints self-assigned this Jul 7, 2023
@clementmas
Copy link
Contributor Author

@quantumwebco thanks for sharing. That's a good set up. I might move some of the logic out of webhooks to simplify the handling of failed payments.

I followed parts of this Stripe guide to set up my form: https://stripe.com/docs/payments/accept-a-payment-deferred

There's a 3rd use case I forgot to mention: updating the default payment method for subscriptions.
I'm using the same form but calling await stripe.createPaymentMethod() instead. Then I manually submit the form to call Cashier's $user->updateDefaultPaymentMethod($paymentMethodId).

@quantumwebco
Copy link
Contributor

quantumwebco commented Jul 14, 2023

To add a new payment method, in the backend I call $setupIntent = $user->createSetupIntent($options); then in the front end I use the setup intent client secret to create and mount the payment element, then on submit call stripe.confirmSetup(options); which redirects with the setup intent id in the params, to a method which gets the setupIntent from the stripe API (I am doing this manually but there was a recent pr that added a getSetupIntent method but not sure if it's merged yet) and then calls $user->addPaymentMethod($paymentMethod->id); $user->updateDefaultPaymentMethod($paymentMethod->id);

That is also the way I handle subscriptions. It runs through that flow first and then creates a subscription like $sSubscription->create($paymentMethod->id);

It's the same for single payments but instead of createSetupIntent you use $paymentIntent = $user->pay($amount); use the payment intent client secret for the payment element in the front end and then call stripe.confirmPayment(options) which redirects to the speicified url with the paymentIntent id in the url params.

I will put some code in to gists which shows the flows I am using (no webhooks, direct to API) and share them here

@quantumwebco
Copy link
Contributor

I think these docs will be better for you https://stripe.com/docs/payments/quickstart you can basically follow that but replace the backend stuff with the cashier methods. It's the pretty much the same for setup intent as it is for payment intent. You can also use a payment intent and set an option to save the card for future use

@timgavin
Copy link

To add a new payment method, in the backend I call $setupIntent = $user->createSetupIntent($options); then in the front end I use the setup intent client secret to create and mount the payment element, then on submit call stripe.confirmSetup(options); which redirects with the setup intent id in the params, to a method which gets the setupIntent from the stripe API (I am doing this manually but there was a recent pr that added a getSetupIntent method but not sure if it's merged yet) and then calls $user->addPaymentMethod($paymentMethod->id); $user->updateDefaultPaymentMethod($paymentMethod->id);

I will put some code in to gists which shows the flows I am using (no webhooks, direct to API) and share them here

@quantumwebco Is this still working for you? I'm trying to do the same thing and keep getting a The provided PaymentMethod cannot be attached. To reuse a PaymentMethod, you must attach it to a Customer first. error.

@quantumwebco
Copy link
Contributor

Yep, all still working fine in my apps. Are you adding it via a setup intent or a payment intent? If a payment intent make sure you are setting the off_session parameter correctly. If you dump out the exception what are the error codes?

@timgavin
Copy link

@quantumwebco Thanks for the reply; I'm issuing a setup intent

@quantumwebco
Copy link
Contributor

Ah I see, I have replied on Laracasts. You need to get the card server side rather than client side. The redirect happens before the else and adds the intent id to the return_urlquery params, so you should set the redirect return_url to go to a controller method that gets the $request->setup_intent and uses that to attach the card. Does that make sense?

@sts-ryan-holton
Copy link

Just chipping in here. I'm using a Nuxt JS front-end here. I've tried switching card element to payment using:

async mounted () {
  await this.card()

  if (this.$stripe) {
    const clientSecret  = this.subscription?.intent?.client_secret ?? ''
    const elements = this.$stripe.elements({ clientSecret })
    this.subscription.elements = elements.create('payment', {
      layout: {
        type: 'accordion',
        defaultCollapsed: true,
        radios: true
      }
    })
    this.subscription.elements.mount('#card-element')
  }
},

Can I just confirm, would sending the payment type back to the server via setupIntent (the token) work as intended? I'm using subscriptions & single charges, but primarily subscriptions, here's my JS for capturing the setup intent:

const clientSecret = this.subscription.intent.client_secret ?? ''
const { setupIntent, error } = await this.$stripe.confirmCardSetup(
  clientSecret, {
    payment_method: {
      card: this.subscription.elements,
      billing_details: {
        name: this.billing.name
      }
    }
  }
)

And in Cashier in my backend I'm implementing the following:

try {
    $user = User::find(Auth::id());
    $user->newSubscription('default', $plan->stripe_id)->withCoupon($coupon)->create($request->input('token'));
} catch (IncompletePayment $e) {
    if ($e->payment->requiresAction()) {
        return new ApiSuccessResponse([
            'requiresAction' => true,
            'redirectTo' => route('cashier.payment', [
                $e->payment->id,
                'redirect' => $frontendURL.'account/subscriptions/',
            ]),
        ], [
            'message' => "We require some payment action from you, this usually means you need to open your banking app to approve this subscription.",
        ], 400);
    } else if ($e->payment->requiresPaymentMethod()) {
        return new ApiSuccessResponse([
            'requiresAction' => true,
            'redirectTo' => route('cashier.payment', [
                $e->payment->id,
                'redirect' => $frontendURL.'account/subscriptions/',
            ]),
        ], [
            'message' => "The payment method currently set up is invalid or has sufficient funds, we're going to redirect you to complete this transaction, please wait.",
        ], 400);
    } else if ($e->payment->requiresConfirmation()) {
        return new ApiSuccessResponse([
            'requiresAction' => true,
            'redirectTo' => route('cashier.payment', [
                $e->payment->id,
                'redirect' => $frontendURL.'account/subscriptions/',
            ]),
        ], [
            'message' => "We jsut need you to confirm this payment.",
        ], 400);
    } else {
        return new ApiSuccessResponse([
            'requiresConfirmation' => true,
        ], [
            'message' => "We require you to confirm this payment."
        ], 400);
    }
}

return new ApiSuccessResponse([
    'complete' => true
], [
    'message' => "You've successfully upgraded your account.",
], 201);

@quantumwebco
Copy link
Contributor

This looks like code for the older style integration so you might need to refactor a fair bit. Hopefully this will help you https://laracasts.com/discuss/channels/javascript/stripe-payment-element-error#reply-899320

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

No branches or pull requests

5 participants