Create add-payment-method for Stripe Payment Element

Created on 7 December 2023, 12 months ago
Updated 26 August 2024, 3 months ago

Hello,

I've been studying the code inside PaymentInformation->submitPaneForm
I notice there is a different treatment whether the Payment Gateway does extend SupportsCreatingPaymentMethodsInterface or not.

Stripe.php (Card Element) does extend SupportsCreatingPaymentMethodsInterface
StripePaymentElement does not. (It only extends SupportsStoredPaymentMethodsInterface)

So a different piece of code is executed whether we use Card Element or PaymentElement, even for one same Payment Method of type credit card.

Could anyone explain to me why StripePaymentElement does not extend SupportsCreatingPaymentMethodsInterface whereas is seems to me we can definitely create new Payment Methods with the Payment Element component?

Could you explain why inside PaymentInformation->submitPaneForm, we need a different treatment for payment gateways supporting SupportsCreatingPaymentMethodsInterface or not?

For example with Card Element, on line 407 we check whether $payment_method_profile exists before assigning it to the order.
On the other hand, with Payment Element, on line 445, no check is made on the existence of the billing profile of the payment method before attempting to assign it to the order, so code crashes if the billing profile is missing from the payment method. Is this just an oversight? Or it this made on purpose? I would like to understand why.

In our case we made the custom decision to never store the billing profile on the payment method side but only on the user's side. So this code never crashed when we were using Card Element. Now it crashes with Payment Element because the payment method does not have a billing profile. I would like to understand why this difference so that I can make the best decision on how to fix this situation for us.

Thank you

💬 Support request
Status

Needs work

Version

1.1

Component

Payment Element

Created by

🇫🇷France nicolas bouteille

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @nicolas bouteille
  • 🇫🇷France nicolas bouteille

    Sorry I realize PaymentInformation belongs to Commerce core so the only question that belong here is the following:
    Why did you not create Payment Element payment gateway as an OnsitePaymentGateway like Card Element was but OffsitePaymentGateway instead?
    Rectifying title of the issue...

    I do appreciate the fact that the user can choose to either reuse a stored payment method or enter a new credit card, all that on one same page / pane. From what I understand, this is possible thanks to the OnsitePaymentGateway and the add-payment-method form declared in the annotations.
    I would have wanted to be able to do the same with Payment Element. On one same Pane, suggest stored payment methods, or offer the Payment Element JS component to choose a new payment method. Why did you not go this way? Why did you choose to implement OffsitePaymentGateway and make the Payment Element component appear only at review step?

    We also allow our users to add a new credit card at any time from their user account, so that they can replace their default credit card that has expired, before we renew their subscription. I am wondering how I am going to be able to do that with Payment Element since it does not seem to have an add-payment-method form.

  • 🇫🇷France nicolas bouteille

    Ok so allow me to keep documenting what I found out after digging a little bit more on that topic.
    I understand that an Onsite payment method like Stripe.php requires the customer to first create a payment method before they can actually pay. I have to admit that this might not be to the liking of all customers. I for example always refuse websites to save my credit card info for later when I can…
    An Offsite payment method like StripePaymentElement.php has been implemented does not require the user to create a payment method before proceeding to the payment. Stripe first handles the payment in ajax, then the payment method can be stored afterwards if the payment is successful (by calling the return_url route).

    I'm thinking that not only this may reassure customers that don't want their payment method to be saved, but this might actually reduce the number of authorizations required. I'm not sure but I guess that with an Onsite PG, the user could potentially be asked to authenticate first at the payment method creation with SetupIntent, and a second time when the payment is actually done with PaymentIntent. So that could be another good point in favor of the Offsite PG type.

    By the way my research allow me to confirm that using Offsite PG for Payment Element was not a requirement due to Payment Element itself, but a choice. Indeed, guys from Stripe confirmed me that Payment Element can totally be used to just create a new payment method without making an actual payment, using the SetupIntent, just like what's done in PaymentMethodAddForm.php. So this is a choice. A choice that I'm starting to understand and like from what I just said above, but a choice to change the way things will be done from now on.

    Also, if I'm not wrong, this means that, since we don't need to create the payment method first during checkout, there has been no need to create the equivalent of PaymentMethodAddForm.php for Payment Element so far.
    So, at the time of this writing, we don't have a form that allows the customer to add any of the enabled payment methods with Payment Element but without making an actual payment.
    So if you want to offer your customers the possibility to update their expired credit card or new Paypal or bank account before you try and renew their subscription, well as of right now, there is no form in Commerce Stripe that can help you with that yet. This is something I actually want to implement and I guess I will have to do it on my own.

  • 🇺🇸United States andyg5000 North Carolina, USA

    I need to implement Payment Element and meet the following goals:

    1) Use Stripe Payment Element as the payment interface during checkout (works out-of-the-box)
    2) Allow customers to add/delete payment methods from their account.
    3) Allow administrators to add payments to a back-end order.
    4) Allow administrators to add/delete payment methods for a customer.

    Adding the following to the @CommercePaymentGateway annotation/definition for StripePaymentElement solves goals 3 & 4:

     *   forms = {
     *     "add-payment" = "Drupal\commerce_payment\PluginForm\OnsitePaymentAddForm",
     *     "add-payment-method" = "Drupal\commerce_stripe\PluginForm\Stripe\PaymentMethodAddForm",
     *   },
    

    The condition in PaymentMethodAddForm is what prevents goal 2. It requires that the payment method implement SupportsCreatingPaymentMethodsInterface. However, the checkout PaymentInformation checkout pane also uses this interface to determine if a credit card form should be rendered on the checkout pane. Since Stripe Payment Element uses its own OffsitePaymentGatewayBase based workflow, this doesn’t work.

    An option to fix this is to define a new interface for payment gateways that's only checked in PaymentMethodAddForm and not in the PaymentInformation checkout pane. Something like SupportsCreatingOrderlessPaymentMethodsInterface. Then the PaymentMethodAddForm logic could look like this:

    $payment_gateways = array_filter($payment_gateways, function ($payment_gateway) {
      return $payment_gateway->getPlugin() instanceof SupportsCreatingPaymentMethodsInterface || $payment_gateway->getPlugin() instanceof SupportsCreatingOrderlessPaymentMethodsInterface;
    });
    

    After making the changes above, I can achieve all my goals. Note that the payment form on the admin and customer payment method pages is the Stripe iframe payment form and not the fancy one that's rendered in checkout, but it works!

  • Assigned to andyg5000
  • 🇺🇸United States andyg5000 North Carolina, USA

    Updating the title since I think the main goal here is to add support for adding payment methods via "Stripe Payment Element" from the user payment method page. I've realized that we don't need an "add-payment" form, but if you have 0 payment methods with that form, the add payment tab on the commerce order throws a 403 and doesn't let you select from saved payments or click the "add a new payment method" button. I'll look at commerce core for that issue separately.

    Assigning to me because I have this mostly working locally and will commit to a MR branch ASAP.

  • 🇺🇸United States TomTech

    Hi @andyg5000 !

    Adding (2) makes sense, and would be an awesome addition. I presume you are using SetupIntent to do so? (This would also be the pre-text for supporting ACH manual verification.)

    For (3) and (4), there are issues, though. (Except for delete.)

    I presume this is for what Stripe refers to as a MOTO. (Mail Order/Telephone Order) See: https://stripe.com/resources/more/moto-payments-101

    Issues include:

    1. MOTO requires the merchant to request this be enabled on their Stripe account. It is not enabled by default.
    2. MOTO incurs higher fees
    3. Stripe recommends setting up a separate account for MOTO. (Presumably to avoid normal operations from being shut down in the case that too many issues occur on a MOTO enabled account.)
    4. MOTO moves the merchant out of PCI SAQ-A, since an "admin" would be receiving and inputting the card data.
    5. MOTO has many additional best practices to minimize fraud and chargebacks. See: https://stripe.com/resources/more/moto-payments-101#best-practices-for-m...
    6. AFAICT, MOTO is not supported in the standard Api, only in the terminal device API. See: https://docs.stripe.com/terminal/features/mail-telephone-orders/payments
    7. If MOTO actually is supported in the standard API, most payment methods other than card would not work. e.g. ApplePay, GooglePay, CashApp, Klarna, etc...
  • 🇺🇸United States andyg5000 North Carolina, USA

    Hey Tom!

    Yep, SetupIntent, basically the same as the Stripe payment method, but using the PM interface.

    Thanks for providing the additional info regarding MOTO. I'll push my updates for `add-payment-method` shortly and we can determine the best route to go for MOTO. Maybe an option that's basically a permission for adding cards on-behalf of customers. FWIW, the Stripe payment gateway plugin already does this by allowing admins to add payment methods for the customer via the CC form.

  • Issue was unassigned.
  • Status changed to Needs review 4 months ago
  • 🇺🇸United States andyg5000 North Carolina, USA

    Hey Tom,

    I pushed up the work (to issue branch) to add the StripePaymentElement SetupIntent form which can be used by the customer (or admins) to add a payment method to the account.

    One issue is that `SupportsCreatingPaymentMethodsInterface` is required to access this form on the user account page and this is also what checkout uses. We don't want this form to load during checkout, only on Add Payment Method. I mentioned a possible solution above, but that requires an update to core. Note this form actually works in checkout just fine with my code, but it doesn't follow the same process as the existing implementation that puts the stripe form on the review page. I'm actually not sure the reasoning behind the existing pattern, but I'm sure there is one : )

    If you like this approach, we'll need to:

    1) Address the SupportsCreatingPaymentMethodsInterface issue above
    2) Add javascript to send the billing information when not using an existing profile (e.g. new address)
    3) Write tests.

    Let me know your thoughts and I can pick back up on this!

  • Pipeline finished with Success
    3 months ago
    Total: 682s
    #249396
  • Pipeline finished with Success
    3 months ago
    Total: 812s
    #259368
  • 🇺🇸United States andyg5000 North Carolina, USA

    I have updated the merge request to address points 1 and 2 from my previous comment.

    Billing information is now passed to Stripe from both the checkout form and the add payment form (from a user dashboard).

    I added the core onsite base form to the "add-payment" method for the StripePaymentElement payment gateway plugin (commit 5499e95c8b2c7621319b5c1d6053c3eef02dbef7). This change allows payment methods to be added during checkout, to a back-end order, and by customers on the payment methods page. Using this commit replaces the review stage payment processing that the payment gateway handled prior to my merge request. Everything still works, but the payment form is now loaded on the payment page rather than the review page. I'm not sure why it functioned that way before, but this approach aligns with the other payment flows (like the base Stripe plugin in this module).

  • Pipeline finished with Success
    3 months ago
    Total: 712s
    #259513
  • 🇫🇷France nicolas bouteille

    Using this commit replaces the review stage payment processing that the payment gateway handled prior to my merge request. Everything still works, but the payment form is now loaded on the payment page rather than the review page. I'm not sure why it functioned that way before, but this approach aligns with the other payment flows (like the base Stripe plugin in this module).

    I'm a little skeptical reading this…
    A choice has been made to implement Payment Element as an Offsite Payment Gateway with the form loading at review step.
    I opened this issue so that the people that made this choice could explain why. Which is why the title of the issue used to be
    "Why Payment Element an Offsite instead of Onsite PaymentGateway? How to provide an add-payment-method form?"
    No one ever explained this new approach and I had to adapt our website in order to use Payment Element as an Offsite PG. So we now have a production website that uses Payment Element form at review step.

    Do I understand correctly that your new code would revert this decision and make Payment Element an Onsite Payment Gateway instead? Which means Payment Element form cannot be used at review step any more, that the user does not have a choice but to add and save a payment method first (with SetupIntent) and can no longer make a one time payment without saving his payment method?

    If this gets released, I'm afraid this would break sites like mine that have already used Payment Element as an Offsite payment gateway at review step…
    I think a discussion should be held before making breaking changes.
    I would like for this issue title to get back to "Why Payment Element an Offsite instead of Onsite PaymentGateway? How to provide an add-payment-method form?"
    I'd like for the people who decided to make Payment Element an Offsite PG to explain why they did so. Did they do it on purpose? Does it have advantages? How do they suggest that we provide a way to add a new payment method from the user account? Is switching back to an Onsite PG the only way to do so? What consequences will it have? Should/can we offer the admins to chose between Offsite and Onsite PG with Payment Element?

  • 🇺🇸United States andyg5000 North Carolina, USA

    Hey Nicolas,

    Sorry for potentially hijacking your issue here. The merge I built basically adds all the things you asked for, making the "payment element" behave more like "card" and providing an "add-payment" form so users can add payment methods outside of the context of an order. It does not answer your question about why it was built the way it was initially, and I too am trying to get that question answered.

    You can see in my comment #11 above that I'm pointing out the one commit that changes the functionality we're both questioning. The problem is that without that commit, Commerce Core doesn't support a form that can be used on /user/[uid]/payment-methods, but not available in checkout. With the commit, the core form includes the payment element during checkout at the same position as other on-site (and existing stored payment methods). The review pane is simply skipped when a stored payment method is selected from that step. This commit can be omitted from the MR, but I believe core will need another issue to support this.

    My goal here is to get your original question answered and add the missing functionality that I need. Knowing that, feel free to rename the issue back as you see fit. If you could test the MR, that would be nice too, assuming you still need part of the functionality. Hopefully Centarro will chime in soon.

    Thanks,
    Andy

  • 🇫🇷France nicolas bouteille

    Sorry I won't have time to test your MR for now.
    I definitely think such a rollback in the architecture cannot be pushed just like that and potentially break all sites using Payment Element as an Offsite PG so far. This needs to be discussed IMHO.

  • 🇺🇸United States TomTech

    Hi all! Appreciate the dialog!

    I'll try and address several items here:

    1. Allowing customers to create payment methods in their profile (/user/[uid]/payment-methods) via setup intents. This is a desired feature, so if we get it in this MR, sweet!!

    2. Admin created payment methods, on behalf of customers. (Mail Order/Telephone Order, aka MOTO). As noted in comment above #3406817-7: Create add-payment-method for Stripe Payment Element , there are a lot of requirements about implementing this, including manual activation of MOTO by contacting stripe, MOTO parameters to be passed (which I can only find documented in other APIs and do not see in this MR), and the additional PCI requirements this entails. While other gateways may have supported this in the past, holding a lot of reservations about adding this to new ones. (Or continuing to keep them for previous ones.)

    3. Why is the payment element on the review step, rather than a prior step? Payment processing has evolved. :)

    The traditional multi-step checkout flow has been:

    a. On Payment information step, customer provides their payment information, and a token is returned (or a stored payment token is selected)
    b. On Review step, customer confirms their details. (order, payment, shipping, custom items, etc...) They can "complete" the order, moving on to steps (c) -> (d), or they can go back, and make changes to the order, payment method selection, etc...
    c. On the (invisible) payment step, the token is used server side to make an API call and create the payment with the payment processor
    d. On success, the complete step was displayed. The payment in step (c) could fail, moving us back to step (a).

    In the contemporary flow, the payment processor expects to be the last step and creating the token creates the payment. If we revert to creating the token on step (a), and the customer lands on (b), they will have the false impression that they can "go back" and makes changes to the order, or abandon their cart. But...their payment has already been charged. With the implementation of 🐛 Payment entity created after webhook Fixed , we now create a payment on a webhook event, which will complete the order. Any action the customer takes on the review step in a reverted flow, will be irrelevant, as the order will have already moved to completion.

    This flow looks more like this:

    a. On Payment information step, customer selects an existing payment method, or to create a new one
    b. On Review step, customer confirms order details; if creating a new payment method, provides payment information; confirms the order. A payment intent token is returned. (A payment has already been made on Stripe.)
    c. On the (invisible) payment step, the token is used to retrieve(rather than create) the payment information from Stripe. Then, we create/update the payment method and payment on the commerce order and complete it. (If the customer happens to lose network connection, or close their browser, the webhook will create the payment method/payment and complete the order without the return browser side.)
    d. The complete step is displayed

    Note that this flow isn't really new. This is a similar flow to PayPal. The Adyen implementation also takes place on the review step.

    4. Differences between card element and payment element. Card element is a stripe implementation that only supports card payments. payment element is their newer and preferred implementation that adds ApplePay, GooglePay, ACH, and many more payment methods in one implementation. It does behave differently, and therefore the implementation is different. Future development is planned around the payment element. (There is more nuance to this, but avoiding going too deep into the weeds.)

    HTH clarify things!

  • 🇺🇸United States andyg5000 North Carolina, USA

    Hey Tom,

    Thanks for the feedback. The MR above does address "Allowing customers to create payment methods in their profile".

    For the MOTO thing, if a stripe developers are required to enforce this, we should reject admins from accessing `/user/[uid]/payment-methods` for the payment element form. In my opinion, the MOTO is up to the store owner to maintain and not our requirement. Most payment gateways, including stripe, have a virtual terminal for merchants to charge the customer on their behalf. I don't see this feature as being any different. Perhaps a new configurable access check and warning need to be added to commerce payment modules if this is an actual requirement. All of the major payment gateway modules for Drupal Commerce do not have this limitation (to my knowledge) and allow adding payment methods as admin.

    I understand the difference in checkout flows you mention above, my MR does NOT run a PaymentIntent, it runs a SetupIntent, so the customer is not charged until the order is submitted and it functions the same as them selecting an previous payment element payment method. The can cancel, return, abandon, etc...

    I'm not suggesting we ditch the contemporary flow, only that commerce core doesn't support solving the main issue here currently (Allowing customers to create payment methods in their profile). See my comments on SupportsCreatingPaymentMethodsInterface above. If we can solve that, then simply removing the add-payment property commit (5499e95c8b2c7621319b5c1d6053c3eef02dbef7) from my MR and updating the interface to match a core change (e.g. my example of SupportsCreatingOrderlessPaymentMethodsInterface) is all that's needed.

    Let me know your thoughts and I'll work to update this MR accordingly.

  • 🇺🇸United States TomTech

    Hi Andy!

    If a setup intent is required in the checkout flow with this change, then it would break support for Klarna, Affirm, WeChat Pay, AliPay, and any other payment method that does not support future usage.

    Certainly possible that I'm misunderstanding the proposed change, though. I've not had a chance yet to pull the MR down and give it a go. Will try that soon.

  • 🇫🇷France nicolas bouteille

    Andy, I just had a look at the MR and to my surprise you did not modify StripePaymentElementInterface to extend OnsitePaymentGatewayInterface, it still extends OffsitePaymentGatewayInterface.

    So your changes are not as big as I feared. I now understand the new workflow you suggest and that it behaves just like with already existing payment methods as soon as we reach review step. Still using the return url and stuff…

    It has an advantage over the current workflow: the Commerce Payment Method is created and stored on Drupal's end before the payment is processed. In the current workflow the Commerce Payment Method is created after a successful payment intent, when Stripe calls our return url. This leads to one big problem: if an error occurs when creating the Commerce Payment Method, the code stops and the order never gets completed but the money has actually been paid! I've been facing several situations like that since I started using Payment Element. With your workflow, if an error occurs at Payment Method creation, the customer cannot go further and no money has been involved yet. Which is safer.

    I believe there might be one disadvantage though, customers might need to validate their payment method twice. First when they add it on Payment Info step (SetupIntent) and a second time at Review step when the payment is processed (PaymentIntent) especially if their card requires Strong Customer Authentication all the time. Have you faced such a situation when testing?

    Also, as I said earlier, I believe some customers (like me) don't want websites to store their credit card details for later. So I'm afraid if the Payment details form is displayed at Payment Info step instead of Review step, some customers might stop right there. But maybe this can be fixed with a checkbox "Save my payment method for later" that would be unchecked by default, and we would make sure not to save the payment method.

    There is one important thing we shall not neglect though : we must not break or make weird changes on already working sites. Some sites are already using Payment Element form at review step right now. If the Payment Element form suddenly appears at Payment Info step after upgrading Commerce Stripe, this could be bad. Maybe they added custom code based on the fact that the payment form is on review step and this will suddenly break… so I don't think we can just change the workflow like that.

  • 🇫🇷France nicolas bouteille

    Tom that's a very good point.
    I did not know that some of Stripe's payment types did not support SetupIntent.
    It seems that is THE main reason why this workflow HAD to be implemented for Payment Element…

  • Status changed to Needs work 3 months ago
  • 🇺🇸United States andyg5000 North Carolina, USA

    Hey guys,

    Thanks for clarifying the workflow is required alternate payment methods supported by stripe payment element. That's the answer Nick and I were trying to uncover.

    Again, I was never proposing to change the workflow only that commerce core doesn't support what we're trying to do without adding an "add-payment" form property.

    It sounds like a commerce core will need to be created and merged before we can move forward with this.

  • 🇫🇷France nicolas bouteille

    In the mean time, I believe the code you wrote for the add payment method form using SetupIntent could be provided as a form that can be manually used by anyone wishing to allow customers to add payment methods from their personal account.
    This is something I also implemented earlier on my website and I guess my code is pretty similar to yours and having your code as an existing form inside Commerce Stripe would have been great so I guess it could definitely help others until we have a definitive solution...

  • 🇺🇸United States andyg5000 North Carolina, USA

    Hey Nicolas,

    Is your custom solution based on the commerce forms loaded for /user/[uid]/payment-methods for something custom?

    If the former, I'm curious how you got by the logic in https://git.drupalcode.org/project/commerce/-/blob/8.x-2.x/modules/payme... without exposing the form during checkout.

  • 🇫🇷France nicolas bouteille

    Nope I went for a fully custom form that extends FormBase and with a custom route.

Production build 0.71.5 2024