Why a new payment gateway for Payment Element? How to switch from Card Element to Payment Element without losing stored credit cards and end up with two Stripe accounts per user?

Created on 4 December 2023, 7 months ago
Updated 11 December 2023, 7 months ago

Hello,

I would like to start using the Payment Element instead of Card Element on an already working website, obviously in order to offer multiple ways to pay to our clients.

During my tests, I have discovered that you guys have chosen to implement Payment Element as a totally new Payment Gateway although we're still using Stripe and using the same Payment Intent API.

This technical choice makes it complicated to switch from Card Element to Payment Element on an existing website because by design, it seems both payment methods and remote customer ID cannot be shared between two separate payment gateways.

So:
1/ previously stored credit cards with Card Element are no longer suggested at checkout although it seems to me that they could totally still be working since we're still using the same Payment Intent API to process payments (gotta verify this though). This is due to 'commerce_payment_method' db table having a 'payment_gateway' column that is worth 'stripe' with Card Element and 'stripe_payment_element' with PE.

2/ the remote Stripe Customer account already attached to each user with Card Element is ignored by Payment Element and a new Stripe Customer account is then created. Whe end up having two Stripe accounts for each user which after contacting Stripe I can confirm is not required at all. This time, it's db table user__commerce_remote_id that has a commerce_remote_id_provider column that once again will be 'stripe' or 'stripe_payment_element'

From my understanding, Card Element and Payment Element are just two JS components allowing us to add new payment methods to one same Payment Gateway : Stripe. So I would like to understand why have you decided to create a new payment gateway for Payment Element with the consequences it involves?

Wouldn't it have been better to just add an option inside the Stripe payment gateway to choose between Card Element and Payment Element and keep the same payment gateway?

Is it something you did not think about at the time and now it's too late we gotta keep it as is? Or are there good reasons for which you had to go this way?

How would you recommend people like me to switch from Card Element to Payment Element? I have the feeling that a choice has been made and even though I wish a new payment gateway would not have been created, it is too late and you guys won't make a u-turn on that. So that my next best choice is to duplicate all the payment methods in the database I have under 'stripe' with the key 'stripe_payment_element' so that they can also be suggested when Payment Element is activated. Same thing for the rows inside user__commerce_remote_id I should duplicate all lines with 'stripe' with 'stripe_payment_element' so that Payment Element keeps using the already existing Stripe accounts. What do you think?

πŸ’¬ Support request
Status

Active

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

Comments & Activities

  • Issue created by @Nicolas Bouteille
  • πŸ‡«πŸ‡·France Nicolas Bouteille

    In the end, I decided not to go with duplicating database entries in user__commerce_remote_id and commerce_payment_method. I saw that each payment_method has field values stored in multiple tables so duplicating them would mean having to deal with database transactions I guess so I went with a different approach.

    Instead, I just made it so that payment methods stored under 'stripe' (Card Element) would be suggested even when only Payment Element is enabled. And also that when checking if a remote Stripe account already exists, remote Ids stored under 'stripe' (Card Element) would be used even if only Payment Element is enabled.

    Technically this is what I've done:

    For payment methods, I have overridden the PaymentMethodStorage so that loadReusable() not only loads payment methods stored for Payment Element but also Card Element.

    For remote Stripe customer ids, I have overridden the payment gateway class StripePaymentElement so that getRemoteCustomerId() (inherited from PaymentGatewayBase) first tries to find remote customer id for 'stripe' (Card Element) and only if none is found, then looks for remote ids under 'stripe_payment_element'

    Overriding PaymentMethodStorage is done by overriding the 'storage' handler class for entity type PaymentMethod using hook_entity_type_alter:

    function my_module_payment_entity_type_alter(array &$entity_types) {
      if (isset($entity_types['commerce_payment_method'])) {
        $commerce_payment_method_entity_type = $entity_types['commerce_payment_method'];
        $commerce_payment_method_entity_type->setHandlerClass('storage', 'Drupal\my_module\MyModulePaymentMethodStorage');
      }
    }

    Overriding StripePaymentElement is done using hook_commerce_payment_gateway_info_alter that is triggered by PaymentGatewayManager construct.

    function my_module_payment_commerce_payment_gateway_info_alter(array &$info) {
      if (isset($info['stripe_payment_element'])) {
        $info['stripe_payment_element']['class'] = MyModuleStripePaymentElement::class;
        $info['stripe_payment_element']['provider'] = 'my_module';
      }
    }

    Here is what MyModulePaymentMethodStorage looks like:

    class MyModulePaymentMethodStorage extends PaymentMethodStorage {
    
      /**
       * {@inheritdoc}
       */
      public function loadReusable(UserInterface $account, PaymentGatewayInterface $payment_gateway, array $billing_countries = []) {
        $payment_methods_of_enabled_payment_gateway = parent::loadReusable($account, $payment_gateway, $billing_countries);
    
        /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */
        $payment_gateway_storage = $this->entityTypeManager->getStorage('commerce_payment_gateway');
        /** @var PaymentGatewayInterface $stripe_card_element_payment_gateway */
        $stripe_card_element_payment_gateway = $payment_gateway_storage->load('stripe');
        $payment_methods_of_stripe_card_element = parent::loadReusable($account, $stripe_card_element_payment_gateway, $billing_countries);
    
        return $payment_methods_of_enabled_payment_gateway + $payment_methods_of_stripe_card_element;
      }
    }

    Here is what MyModuleStripePaymentElement looks like:

    class MyModuleStripePaymentElement extends StripePaymentElement {
    
      public function getRemoteCustomerId(UserInterface $account) {
        $remote_id = NULL;
        if (!$account->isAnonymous()) {
          $provider = 'stripe' . '|' . $this->getMode();
          /** @var \Drupal\commerce\Plugin\Field\FieldType\RemoteIdFieldItemListInterface $remote_ids */
          $remote_ids = $account->get('commerce_remote_id');
          $remote_id = $remote_ids->getByProvider($provider);
          if (!$remote_id) {
            $remote_id = parent::getRemoteCustomerId($account);
          }
        }
        return $remote_id;
      }
    
    }

    Please let me know what you think of it and if you would have done things differently.
    And I'm still waiting to know why a separate Payment Gateway has been created for Payment Element...

  • πŸ‡«πŸ‡·France Nicolas Bouteille

    Just a follow up.
    Now that I managed to suggest Card Element stored cards along with Payment Element stored cards, even if only Payment Element is enabled, I noticed that the Card Element code inside Stripe.php is used if I select a Card Element stored card. And code inside StripePaymentElement.php gets used for stored cards created with Payment Element.
    Even if this makes sense as a consequence of technical choices, I don't like that one exact same kind of operation is executed through two different codes depending on the way the card was created.
    Furthermore, I have noticed the two codes in Stripe.php and StripePaymentElement.php is not exactly the same. The code in StripePaymentElement.php seems to be a better rewrite that does not use old deprecated Charges API code anymore.
    So I have decided that I wanted to use the code inside StripePaymentElement even for cards created with Card Element.
    For that, I have overridden the PaymentMethod class so that I could override the getPaymentGateway() and getPaymentGatewayId() functions to return Payment Element instead of Card Element.

    Here is the override code:

    function my_module_payment_entity_type_alter(array &$entity_types) {
      if (isset($entity_types['commerce_payment_method'])) {
        $commerce_payment_method_entity_type = $entity_types['commerce_payment_method'];
        $commerce_payment_method_entity_type->setClass(MyModulePaymentMethod::class);
      }
    }
    
    class MyModulePaymentMethod extends PaymentMethod {
    
      /**
       * {@inheritdoc}
       */
      public function getPaymentGateway() {
        $target_id = $this->get('payment_gateway')->target_id;
        if ($target_id === 'stripe') {
          /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */
          $payment_gateway_storage = $this->entityTypeManager()->getStorage('commerce_payment_gateway');
          /** @var PaymentGatewayInterface $stripe_payment_element_payment_gateway */
          $stripe_payment_element_payment_gateway = $payment_gateway_storage->load('stripe_payment_element');
          return $stripe_payment_element_payment_gateway;
        }
        else {
          return $this->get('payment_gateway')->entity;
        }
      }
    
      /**
       * {@inheritdoc}
       */
      public function getPaymentGatewayId() {
        $target_id = $this->get('payment_gateway')->target_id;
        if ($target_id === 'stripe') {
          $target_id = 'stripe_payment_element';
        }
        return $target_id;
      }
    
    }
    
  • πŸ‡ΊπŸ‡ΈUnited States torgosPizza Portland, OR

    I could be mistaken, but I believe the different "methods" comes from the fact that when this module was written, only card sources were allowed. Then came sources API, and the Paymentintents API, and the maintainers felt that it was best to keep them separate rather than migrate from Cards to Payments.

    I would suggest, rather than overriding classes, writing patches for the module to allow for a cleaner migration path into using just Payments API, which would help others using this module as well as the maintainers. Perhaps that can be a target for the 2.x version of the module.

Production build 0.69.0 2024