Problem/Motivation
We need to be able to configure whether permanent payment methods are always created, never created, or whether the user has a choice.
Proposed resolution
Add a setting on the payment information page which controls storage of payment methods
- Always create payment methods for customers for later use (forced)
- Allow customers to save payment method for later use (opt-in)
- Never save payment methods for later use
Remaining tasks
TBD once final resolution discussed. Will probably require action in contrib modules and a change record.
Example of what we do in Square (currently does not save payment methods because their terms require it to be opt-in)
// @todo Make payment methods reusable. Currently they represent 24hr nonce.
// @see https://docs.connect.squareup.com/articles/processing-recurring-payments-ruby
// Meet specific requirements for reusable, permanent methods.
$payment_method->setReusable(FALSE);
// Nonces expire after 24h. We reduce that time by 5s to account for the
// time it took to do the server request after the JS tokenization.
$expires = $this->time->getRequestTime() + (3600 * 24) - 5;
$payment_method->setExpiresTime($expires);
$payment_method->save();
User interface changes
Adds new option to payment information pane form
API changes
Adds new option to payment gateways on a strategy for handling payment methods
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #100 | commerce-n2871483-450-control-default-off-100.patch | 775 bytes | kerasai |
| #99 | commerce-2871483-stripe-support.patch | 525 bytes | wxactly |
| #99 | commerce-2871483-authnet-support.patch | 518 bytes | wxactly |
| #97 | commerce-n2871483-450-control-default.patch | 4.71 KB | damienmckenna |
| #90 | commerce-2871483_90.patch | 9.58 KB | gcb |
Issue fork commerce-2871483
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
mglamanPicking this up
Comment #3
mglamanComment #4
mglamanComment #5
mglamanHere's an example screencast. The summary logic is wrong. But the flow is at least set as an example.

Comment #6
mglamanPatch. Not functional. Just config form. But so much word bikeshedding to be done, getting this up now.
Comment #7
mglamanIt works-ish. needs more testings and bikeshedding.
I had to add logic in PaymentMethodAddForm to support the checkbox. So that the payment method is flagged properly as reusable before passing to the payment gateway. Which forces the form to consider checkout logic. Not sure how we can decouple this, easily, due to elements.
Comment #9
johnrosswvsu commentedNeed to add a pseudo path that could be applied to 8.5.0 that I will be using.
Comment #10
mglamanI don't know what this means. And no interdiff was posted, and we lost 1kb of changes.
Comment #11
zengenuity commentedHere with some bikeshedding: What if we have multiple payment gateways? Can this functionality be enabled/disabled per gateway? From the gif above, it appears not.
Comment #12
arthur.baghdasar commentedCannot apply patch to the latest dev branch or latest release.
Comment #13
brunodboLooks like this needs a reroll. @mglaman, should we be rerolling #7 or #9?
Comment #14
mglamanIt would be #7
Comment #15
brunodboAttempt at rerolling the patch in #7. Note that the patch is smaller in size because of changes that were made in Commerce between #7 and now (e.g.,
\Drupal\Tests\commerce_payment\FunctionalJavascript\PaymentCheckoutTest::testFreeOrderalready exists).The interdiff command failed with this message:
The rejected hunk:
Not sure exactly why that's failing.
Also, it looks like
\Drupal\commerce_payment\Plugin\Commerce\CheckoutPane\PaymentInformation::buildPaneFormhas been changed to hide the payment method form by default for a free order, so we should change that to check the configuration (in case you want to collect payment information on free orders).Comment #17
brunodboThis moves the check for
$collect_billing_onlyup in\Drupal\commerce_payment\Plugin\Commerce\CheckoutPane\PaymentInformation::buildPaneForm, since\Drupal\commerce_payment\Plugin\Commerce\CheckoutPane\PaymentInformation::collectBillingProfileOnlychecks whether the order is 0 as well.Comment #18
handkerchiefAny news on this? This is a very important function. Thanks for your work.
Comment #19
yanniboi commentedWe need this for a client, so I'm going to take a look at it and see if it works as a proof of concept.
Attached reroll patches against both latest release (2.13) and HEAD (8.x-2.x).
Comment #20
yanniboi commentedThis post may end up getting a bit confusing because I am going to talk about the
PaymentMethodAddFormclass, however in commerce alone there are 3 classes with this name.We can completely ignore the commerce_payment_example one as that is not relevant.
The patches so far have made changes to
Form\PaymentMethodAddForm, notPluginForm\PaymentMethodAddForm. As far as I can tell, the only process on the site that usesForm\PaymentMethodAddFormis in routing for the/user/{user}/payment-methods/addpath. If like I suspect, there is no other use for theForm\PaymentMethodAddForm, then at present it is not relevant, as the configuration options we have added so far is for a specific checkout flow, and the payment options added are completely independent of checkout flow.Also there is an inherent assumption in adding a payment method to a user account that the user wants to store the payment method for future use, so we don't need to worry about that use case.
If we are talking about the use case that we want to add the option for a user to decide whether they want to store payment details during checkout (or globally disable storage) then I think we want to make changes to
PluginForm\PaymentMethodAddForm, as this is actually used when rendering the Pane form in the checkout flow.I am going to start working on these changes, please feel free to correct me if I am wrong or missing something..
Comment #21
yanniboi commentedThis is patch for making the PaymentInformation pane form actually work with the reusability settings.
Comment #23
kimlop commentedThe last patch is not applicable to the version 2.14 of Commerce
Comment #24
luongosb commentedAny update on this?
Comment #25
slydevil commentedRe-rolled the patch for 2.16 (and it also applies to the 8.x-2.x branch). I didn't change a thing and it seems to work well. Still testing.
Comment #26
slydevil commentedComment #27
slydevil commentedComment #28
mglamanUploading this for myself to cover some history. I forgot this issue was meant to include collecting payment methods on free orders as well -- like free trials.

Comment #29
czigor commentedFor modern gateways with a temporary payment token like Stripe and Braintree this should not be a big issue. Chase's Hosted Payment Form (HPF) iframe however serves for both creating a payment method and a payment. The actual use depends on the iframe parameters. So if the customer opts in to store the payment method we want to display the payment method version of HPF in the order_information checkout step and create the payment upon review submission using the chase soap api and the created payment method. If the customer does not want to store the payment method we don't display the HPF in the 'order_information' checkout step. On the other hand we display the payment version of HPF in the 'payment' checkout step.
Comment #30
czigor commentedFixing some tests.
Comment #31
czigor commentedRerolling #30.
Comment #33
czigor commentedRe-allowing the non-reusable order payment method which also fixes some tests.
Comment #35
czigor commentedComment #37
czigor commentedFixing some tests and changing the new test to Functional.
Comment #39
czigor commentedMore (failing) tests, code refactoring, coding standards and documentation.
Comment #41
mglamanHave not reviewed, yet. This will require patches in all payment gateways.
* If not opt-in, flag
reusableas false.* All payment gateways need to be updated to respect the set
resuableflag.So Stripe should still save a local payment method entity, but it references the payment instrument nonce generated, not an actual remote payment profile. This is how Square works. Each saved payment method entity is a non-reusable way to store the payment nonce for processing.
Comment #42
recidive commentedThe patch in #39 is failing tests since the rendered billing profile is not showing up in "Payment information" on review page of checkout and the test assert this.
Comment #43
czigor commentedThe catch with the tests is that onsite billing information is not displayed on checkout review while offsite billing information is. I don't think this is intentional. See
PaymentInformation::buildPaneSummary().For onsite gateways we have a payment method on checkout review and we render it. The billing profile uid however is 0 (see #3022850: [Addressbook, part 1] Rework the ownership model for customer profiles why) so the user has no access to it (checked by the ER field formatter). As a consequence the billing information won't be displayed.
In the offsite case there's no payment method yet, so we render the billing profile directly, without an access check.
That's why the test has been changed to assert the profile field value and not the page display.
Comment #45
czigor commentedComment #47
czigor commentedFixing tests.
Comment #48
recidive commented@czigor, looks like this block would be more succinct and readable as following:
Other than that, I think the patch looks great!
Comment #49
czigor commentedThanks @recidive, that makes sense.
Comment #50
czigor commentedAdding the changes of #48.
Comment #51
mglamanAssigning for review
Comment #52
devnyc commentedHi all - i've applied this patch and it seems to work, except that for Stripe payments, the previously used card still appears as an option when making a purchase, even if it is not a stored payment method. Thoughts?
Comment #53
jeff veit commentedThis is for a follow-on...
Clearly this is a change which is needed.
For a site which sells individual products and also subscriptions, the individual products do not need an ongoing payment method, so storing the details should be never, or optional.
For subscriptions, a long-lasting payment method is necessary unless the user is prepared to manually renew each year. This patch does not check whether the products require a re-usable payment method. I don't think there's a general method for determining whether an order requires re-usable, but perhaps the best would be to check whether the order was for a recurring product type, and display a message to the user that their subscription will fail if they do not choose to store customer data.
There's also a class of post-paid subscriptions. For these the initial order total is zero, but I think the payment method details should be collected. However I'm not certain of that.
Edit: These should be covered by collecting payment info for free. But this setting does not come from the requirements of the order.
Comment #54
jeff veit commentedJust tested this set for customers to opt in, purchasing as a guest. Is it correct that the tickbox to store appears? As a guest, there's nowhere to store this, is there? So how can the tickbox make sense?
But what if the guest has the option to make an account after the purchase? By then it may be too late.
My test was run using Stripe, and the product was a recurring product, so it's possible that one of these are the cause of the issue.
Comment #55
finex commentedUpdated patch #50
Comment #56
vmarchukPatch from this comment https://www.drupal.org/project/commerce/issues/2871483#comment-13992456 also require some work for each of the payment methods (Paypal, Braintree, Square and Authorize.net etc) because they are not respecting new settings (as it’s described in the issue description for Square).
It means that after installing this patch, new configurations won’t work because additional changes should be done for each of the payment methods.
In general patch works but for each of the payment methods need replace $payment_method->setReusable(FALSE); by code that respecting new settings.
Comment #57
rszrama commentedThanks for the review, Vitaliy! What this means to me is that we need to have some way to only show the checkbox when we know a payment gateway module has been updated to respect this setting. The challenge is to do this without breaking backwards compatibility. My proposal is:
PaymentGatewayBaseplugin (and related interface) calledsupportsReusableOption()that returnsFALSEby default.PaymentMethodAddFormwhere we add the "reusable" checkbox, we should only do so if$payment_method->getPaymentGateway()->supportsReusableOption()returnsTRUE.TRUEonce they've been updated to properly support the feature.In Commerce 3.x, I'd expect us to move this function into the
SupportsStoredPaymentMethodsInterfaceand require all payment gateway modules to implement it in order to upgrade to Commerce 3.x.I haven't done a thorough review of the rest of the patch, but one thing sticks out to me worth reconsidering ... I see an "always_save" boolean being passed around, but it's my understanding we always save the payment method if we can. The intention seems to be more like "always_make_reusable" or something similar.
Comment #58
jonathanshawWe used an annotation for a very similar transition with requires_billing_information. Maybe better BC with an annotation.
Comment #59
jonathanshawStripe distinguishes between 2 different kinds of reusability: on_session and off_session. The difference has SCA implications.
I don't think we need to consider this here, it seems like commerce_stripe can add a Stripe-specific payment information pane extending the work here and adding an additional option. Just mentioning in case anyone else can see an implication from this use case.
Comment #60
singularoRe-rolled so it applies with current dev-2.x
Comment #61
socialnicheguru commentedI think this had more to do with a commerce payment module update than anything else.
When I use this I get the following:
PHP Fatal error: Could not check compatibility between Drupal\commerce_payment\PaymentMethodStorage::loadMultipleByUser(Drupal\commerce_payment\AccountInterface $account) and Drupal\commerce_payment\PaymentMethodStorageInterface::loadMultipleByUser(Drupal\Core\Session\AccountInterface $account), because class Drupal\commerce_payment\AccountInterface is not available in drupal9/html/modules/contrib/commerce/modules/payment/src/PaymentMethodStorage.php on line 98If I add
use Drupal\commerce_payment\AccountInterfaceI get the following:Fatal error: Class Drupal\commerce_payment\PaymentMethodStorage contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Drupal\commerce_payment\PaymentMethodStorageInterface::loadMultipleByUser) in drupal9/html/modules/contrib/commerce/modules/payment/src/PaymentMethodStorage.php on line 17Comment #62
socialnicheguru commentedI selected "always"
Used square sandbox as my payment gateway
and no card was saved.
Comment #63
attisanwould be great to have a way to give this kind of option to
OffsitePaymentGatewayInterfaceimplementingSupportsStoredPaymentMethodsInterfacetoo. atm, it is a struggle to give the user the choice and this patch wouldn't help either as implementingSupportsCreatingPaymentMethodsInterfaceis off the table as it requires to know the payment_type upfront AND creates the $payment_method - what shouldn't be done as that is within the authority of theOffsitePaymentGatewayInterface(see my issue #3274863: Payment options for OffsitePaymentGatewayBase implementing SupportsStoredPaymentMethodsInterface are inconsistent).Comment #64
aaronbaumanExpected behavior (when opting out or disabling stored payment method):
- payment method not reusable at checkout
- payment method does not get written to database
Observed behavior:
- old payment method not visible at checkout
- payment method still written to database
Comment #65
chrisscrumping commentedJust been testing this patch and seeing the same as AaronBauman in a fresh commerce install.
I tried changing the saved method to reusable and it shows in the payment details tab on the account and on checkout but you can't use it or delete it. So my assumption is that this is just a place holder for the order payment details?
Other than that patch #60 seems to be working with Braintree Commerce for me
Comment #66
mglamanComment #67
aaronbaumanMore testing with patch #60, and I'm seeing that the payment method marked `reusable = 0` is in fact reusable.
After configuring the default (and only) payment flow for "no reusable payments", I can reuse a payment by:
1. put a product in the cart
2. go to checkout
3. enter credit card and click through to "review" step
4. do not submit payment, but go back to cart
5. empty cart
6. add a product to cart again
7. go to checkout - the payment method from 3 is there
Expected behavior would be that, after step 4 or 5, the payment method is no longer visible or usable.
Comment #68
jsacksick commentedWell "reusable" means reusable for other orders, if you saved a card for a particular order, then ofc you can use it for that order...
If we'd empty the credit card when emptying the cart, I'm sure most other people would complain.
Comment #69
glennnz commentedFrom a site builder's perspective, having the ability for a site user to NOT have their credit card details stored in my database is essential.
I'm not interested in taking on the security implications of storing a user's credit card details.
I think this issue needs upgrading to at least major.
Comment #70
johnpitcairn commented@glennnz: Raw credit card details are never stored, and are in fact not even received by drupal. The card fields are iframes hosted elsewhere (for on site gateways).
The resulting payment method id received back from the gateway is stored. Only the last 4 digits of the card can be retrieved using that.
Comment #71
glennnz commented@john-pitcairn Ah, excellent! Thanks.
Comment #74
tbkot commentedComment #75
tonytheferg commentedJust jumping in here, patch 60 seems to be working in initial testing.
Merge request from #74 seems to be missing the "Save this payment method for later use" checkbox at checkout for some reason.
@tBKot can you provide a patch and interdiff.txt between your work from 74 and the patch from #60?
Comment #76
loze commentedI am seeing the same thing as @tonytheferg
patch in #60 seems to work, but the MR does not show the "Save this payment method for later use" checkbox on the checkout form.
Comment #77
loze commentedHere is an interdiff of #60 and MR 149
It appears that with the MR, in order for the checkbox to appear on the checkout form, the payment gateway needs to have the public method
When it does, the checkbox appears and the payment method is stored at checkout when checked.
This follows the suggestions in #57
I think this is a good addition, but gateway contrib modules will need to support this.
Comment #78
gcbHere's a patch version of the PR as of comment 74, along with patches for commerce_stripe and commerce_authnet to make them compatible.
I suspect those probably belong in the other modules' issue queues, but until we are sure this is how things are being done I think it makes sense to keep it all in one place.
Comment #79
gcbI found that the patch above caused a crash on free orders that didn't have a payment method attached when trying to produce the payment information summary pane. The attached version makes a small check to avoid that crash. (I initially reported this as a separate issue, which I will close, but there are more details there if you are interested.
Comment #80
zaporylieHandling of free orders is actually not mentioned in the issue summary here but sneaked into the patch ;) Because currently we always make payment methods reusable, and here this option is squeezed under always reusable, I am creating #3468552: Require creating payment method on free orders and will extract the relevant part of #79 in there.
Currently not commenting on the reusability feature but will come back to comment on that soon.
Comment #81
zaporylieHere are some objectives that should be included in this issue's scope:
- customer must be able to opt-out from reusable payment method (
::setReusable(FALSE)). Non-reusable payment methods will only be used to create initial payment.- gateway must be able to declare whether the payment method it creates is reusable. We must be able to filter out all unreusable payment methods. whether existing or new, depending on the context. For example, commerce_recurring must be able to enforce checkout with a reusable payment method for the sake of subsequent charges
- some gateways, depending on configuration, may be reusable but only in the Customer Initiated Transaction (CIT) scenario. This is already possible for Stripe. Such payment methods should not be reusable for Merchant Initiated Transactions (MIT). We must be able to filter them out #3469534: Define payment method's reusability context
I don't think we currently have one but this probably calls for a meta issue so we can divide the scope into some smaller chunks and work on them one by one,
Comment #82
loze commentedShould we be making this feature for 3.x now?
It looks like 3.0 already handles free orders with a new setting for
require_payment_methodto 'Collect payment methods on orders with zero balance'Comment #83
anybodyComment #84
nitrocad commentedI don't think so that the automated collection of payment methods, (commerce_stripe credit card details) without user concern is a good behaver, (GDPR compliant).
So this would be great to implement.
Comment #85
jsacksick commentedYes, any MR should target 3.0.x.
Comment #86
jsacksick commentedLooking at the patch, I don't really understand why
OffsiteRedirectoverrides supportsReusableOption() and returns TRUE.Also, not sure "option" is fully necessary for the method name, as it seems to imply "Forms API" / Fullstack Drupal though I'm not really sure how this could resurface this "setting" on a Headless setup.
Several comments:
Also, I have a problem with a general method / setting at the gateway level that basically indicates whether the gateway supports reusable payment methods.
The main issue I see here is a payment gateway like Braintree or Stripe for example that supports both PayPal and credit cards for example. Some payment method types might be reusable, while others might not.
Also, I see that the Stripe module defines an isReusable() method at the payment method type level, the "Affirm" payment methods aren't reusable for instance.
The current approach is too simplistic, and doesn't take into account the current state of the different contrib payment gateways, I'm really concerned we could end up with a crappy UX where the reusable checkbox is exposed when it shouldn't and vice versa.
Regarding:
We could go with "supports_reusable_payment_methods", which probably needs to default to TRUE, to take into account the existing behavior (i.e. for BC reasons).
I think similarly to "requires_billing_information", we need an associated method (similar to
collectsBillingInformation(), where the default implementation reads from the annotation/ gateway attribute, but more complex use cases like the Stripe one could have conditional logic based on the payment method type.In such case, I think
supportsReusableOption()needs to either accept a $payment_method or a $payment_method_type. Passing the payment method gives more flexibility as the payment method type can be obtained like the following:$payment_method_type = $payment_method->getType();From the PaymentInformation pane, we could call
supportsReusableOption()(or its equivalent right after calling creating the payment method via thecreateForCustomer()method.So the code would look like the following:
Additionally, rather than passing flags via the
$pane_form['add_payment_method']array, we should probably extend the PaymentGatewayForm configuration.From:
To:
One last thing, we either need as @zaporylie mentioned a way for commerce_recurring to enforce saving a payment method as always reusable... Or... Since this is the default behavior that should be explicitly turned off, we can assume that somebody using commerce_recurring is not going to change the default behavior?
Since the same site could in theory sell digital subscriptions and physical products, it does make sense to have this configurable at the checkout pane level so the behavior can be tweaked per checkout flow.
Comment #87
socialnicheguru commentedThis patch conflicts with a commit to Commerce 3.x #2907504: Display payment method in the CheckoutPane Payment informations even if only single payment gateway available.
Comment #89
socialnicheguru commentedMR450 is blank
Comment #90
gcbThe above MR as a patch for builds.
Comment #91
socialnicheguru commentedAre the patches in #78 still needed?
Comment #92
gcbif you are using those payment processors and want payment methods using them to be reusable, then yes.
Comment #93
tbkot commentedComment #94
konordo commentedI was able to fully remove the “By providing your card information…” message from Stripe Elements in Drupal Commerce by hooking into the Stripe Payment Intent creation event and unsetting the setup_future_usage parameter.
Stripe shows that message automatically whenever setup_future_usage is present (even if the value is null). Some Drupal Commerce payment gateways add it implicitly.
To force single-use payments only and prevent Stripe from displaying the saved-card consent message, I added an event subscriber:
How to implement (folder structure + file)
Create a custom module (example: konordo_stripe_event_subscriber) with the following structure:
konordo_stripe_event_subscriber.info.yml
konordo_stripe_event_subscriber.services.yml
src/EventSubscriber/KonordoStripeEventSubscriber.php
Result
Stripe Elements no longer displays:
“By providing your card information, you allow to charge your card for future payments…”
The payment now behaves as a real one-time charge, with no implication of future billing.
This is very useful for Commerce sites that do not store cards or use subscriptions.
Comment #95
damienmckennaGreat work! Glad to see this finally possible.
Some suggestions:
* This would need a note in the release notes documenting the change, including the different steps that must be taken to disable it, as imho it's a major change.
* Default lock_payment_method_reusability to FALSE; I would wager most sites do not want to force this for visitors.
* The option during checkout to save the payment method should default to FALSE, or at least the site builder should be able to change the default.
Comment #96
damienmckennaFWIW in my local testing this does seem to work correctly for new orders.
Comment #97
damienmckennaHere's a patch that works on top of MR #450 that allows controlling whether the payments are saved by default.
Comment #98
jsacksick commentedComment #99
wxactly commentedAttaching new patches for commerce_stripe and commerce_authnet that are compatible with the latest MR.
Comment #100
kerasai commentedFound a case that wasn't working as expected: Not allowing payment methods to be saved.
Here is a patch that works on top of #97 to allow `reusable` to be set to FALSE when locked. Previously it was only considering if the payment method had it `reusable` field set (which they do by default) but not respecting the configured default.