e commerce – What are the checkout flows for Wechat Pay and AliPay?

I am designing a checkout flow that includes AliPay and Wechat Pay as payment options. However, I am struggling to understand what the user should see after they have paid and come back to our website.

My best guess is the “Review Order” page where they recheck all their info before clicking “Submit Order” (Similar to a Paypal checkout flow, where the “Submit Order” CTA initiates the transaction).

However, it seems like both AliPay and Wechat pay initiate the transaction via their app, and not when the user clicks “Submit Payment” in the website. Does anyone know if this is correct?

8 – Commerce framework: How on earth do I offer recurring license subscriptions?

Commerce License is in alpha. That means it is not generally ready for production use.

Further, Commerce Recurring is in beta, but the module page itself notes, as of January 2021:

This module is not production ready! Expect pain.

Using these modules now will require writing custom code and debugging; there is no out-of-the-box easy solution at this time.

However, I needed a license solution today, so I decided to implement one in Commerce without using these modules. Here’s a rough sketch of how I did it. Note that I am renewing the subscriptions manually, which is prone to human error; the following flow only provides a UI for users to purchase and configure the period of their subscriptions.

However, this example will hopefully give you an idea of the large amount of work involved in getting subscriptions set up. It’s not easy; if you want something simpler but still not easy, and in which the subscriptions actually recur properly, you could look at Recurly or other SaaS.

  1. Add a role, Premium user. Give this role the additional permissions that users will purchase.
  2. Add a datetime field to the user account, Subscription expire date. Add another list text field to the user account, Subscription type.
  3. Add custom code to restrict permission for the Subscription expire date field to admins only.
  4. Configure your store + checkout flow in Commerce.
  5. Create a product type “My license.” Add product variations “One year autorenew,” “One month,” etc.
  6. Add an EventSubscriber to Commerce to automatically set the user account field Subscription expire date when the purchase is complete. Also, add a redirect to skip the cart because for individual users, having a cart makes no sense for licenses + I don’t want users to purchase multiple subscriptions.

Skipping carts requires this patch in composer.json:

    "drupal/commerce": {
        "allow order types to have no carts": "https://www.drupal.org/files/issues/2020-10-05/2810723-130.patch"
    },

Even with all this effort, you will still have to renew the subscriptions manually. I did this by adding a view for the admin of “expiring subscriptions” and then using the token stored in the user account by Commerce Stripe to make the recurring payment.

Custom EventSubscriber

Notes about this code: It includes an additional field, a list text field on the user account to let the users change between different subscription types. This allows members to change from a monthly to an annual subscription, or vice versa.

It also assumes that “monthly” subscriptions are always 30 days.

First, services.yml:

services:
  mymodule_subscriber:
    class: DrupalMYMODULEEventSubscriberMyModuleEventSubscriber
    arguments: ('@request_stack', '@logger.factory', '@datetime.time')
    tags:
      - { name: 'event_subscriber' }

And here’s the Event Subscriber from mymodule/src/EventSubscriber/MyModuleEventSubscriber.php:

<?php

namespace DrupalMYMODULEEventSubscriber;

use DrupalComponentDatetimeTimeInterface;
use DrupalCoreLoggerLoggerChannelFactory;
use DrupaldatetimePluginFieldFieldTypeDateTimeItemInterface;
use Drupalstate_machineEventWorkflowTransitionEvent;
use SymfonyComponentEventDispatcherEventSubscriberInterface;
use Drupalcommerce_cartEventCartEntityAddEvent;
use Drupalcommerce_cartEventCartEvents;
use DrupalCoreUrl;
use SymfonyComponentHttpFoundationRedirectResponse;
use SymfonyComponentHttpFoundationRequestStack;
use SymfonyComponentHttpKernelKernelEvents;
use SymfonyComponentHttpKernelEventFilterResponseEvent;

/*
 * Event Subscriber MyModuleEventSubscriber.
 */
class MyModuleEventSubscriber implements EventSubscriberInterface {

  /**
   * The request stack.
   *
   * @var SymfonyComponentHttpFoundationRequestStack
   */
  protected $requestStack;

  /**
   * Logger factory.
   *
   * @var DrupalCoreLoggerLoggerChannelFactory
   */
  protected $loggerFactory;

  /**
   * The time service.
   *
   * @var DrupalComponentDatetimeTimeInterface
   */
  protected $time;

  /**
   * MyModuleEventSubscriber constructor.
   *
   * @param SymfonyComponentHttpFoundationRequestStack $request_stack
   *   The request stack.
   * @param DrupalCoreLoggerLoggerChannelFactory $logger_factory
   *   The logger factory.
   * @param DrupalComponentDatetimeTimeInterface $time
   *   The time service.
   */
  public function __construct(RequestStack $request_stack, LoggerChannelFactory $logger_factory, TimeInterface $time) {
    $this->requestStack = $request_stack;
    // Specify that logs should be for this module.
    $this->loggerFactory = $logger_factory->get('mymodule');
    $this->time = $time;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events = (
      'commerce_order.place.post_transition' => 'postPlaceTransition',
      'mymodule_order.fulfill.post_transition' => 'onFulfillTransition',
      CartEvents::CART_ENTITY_ADD => 'tryJumpToCheckout',
      KernelEvents::RESPONSE => ('checkRedirectIssued', -10),
    );
    return $events;
  }

  /**
   * Set fields and roles for user accounts that purchase subscriptions.
   */
  public function postPlaceTransition(WorkflowTransitionEvent $event) {
    $order = $event->getEntity();

    $order_type = $order->bundle();

    if ($order_type === 'MYORDERTYPE') {
      $price_monthly = 500;
      $price_annual = 5000;
      $subscription_monthly = 'monthly';
      $subscription_annual = 'annual';
      $customer = $order->getCustomer();

      $total_price_array = $order->get('total_price')->getValue();
      $total_price = intval(round($total_price_array(0)('number')));
      $drupal_now_timestamp = $this->time->getCurrentTime();
      $days_in_month = 30;
      $seconds_in_month = 60 * 60 * 24 * $days_in_month;
      $seconds_in_year = 60 * 60 * 24 * 365;
      $subscription_end_timestamp = '';
      // If the user has already purchased, extend the subscription.
      if ($customer->hasRole('subscriber')) {
        $current_subscription_datetime = $customer->get('field_datetime_sub_end')->value;
        $current_subscription_datetime_object = DateTime::createFromFormat(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $current_subscription_datetime);
        $current_subscription_timestamp = $current_subscription_datetime_object->getTimestamp();
        if ($total_price === $price_monthly) {
          $subscription_end_timestamp = $current_subscription_timestamp + $seconds_in_month;
          $customer->set('field_list_text_sub_period', $subscription_monthly);
        }
        elseif ($total_price === $price_annual) {
          $subscription_end_timestamp = $current_subscription_timestamp + $seconds_in_year;
          $customer->set('field_list_text_sub_period', $subscription_annual);
        }
        else {
          $this->loggerFactory->error('Total price was outside the assigned range for subscription. (has subscriber role)');
        }
      }
      // Set the date for a new subscription.
      else {
        // The user is henceforth a subscriber.
        $customer->addRole('subscriber');

        if ($total_price === $price_monthly) {
          $subscription_end_timestamp = $drupal_now_timestamp + $seconds_in_month;
          $customer->set('field_list_text_sub_period', $subscription_monthly);
        }
        elseif ($total_price === $price_annual) {
          $subscription_end_timestamp = $drupal_now_timestamp + $seconds_in_year;
          $customer->set('field_list_text_sub_period', $subscription_annual);
        }
        else {
          $this->loggerFactory->notice('Total price was outside the assigned range for subscription. (no subscriber role)');
        }
      }
      $subscription_end_date_object = new DateTime();
      $subscription_end_date_object->setTimezone(timezone_open('UTC'));
      $subscription_end_date_object->setTimestamp("$subscription_end_timestamp");
      $subscription_end_date_value = $subscription_end_date_object->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
      $customer->set('field_datetime_sub_end', $subscription_end_date_value);

      $customer->save();
    }
  }

  /**
   * Tries to jump to checkout, skipping cart after adding certain items.
   *
   * @param Drupalcommerce_cartEventCartEntityAddEvent $event
   *   The add to cart event.
   */
  public function tryJumpToCheckout(CartEntityAddEvent $event) {
    // $purchased_entity = $event->getEntity();
    // if ($purchased_entity->bundle() === 'training_event') {
    $checkout_url = Url::fromRoute('commerce_checkout.form', (
      'commerce_order' => $event->getCart()->id(),
    ))->toString();
    $this->requestStack->getCurrentRequest()->attributes->set('mymodule_jump_to_checkout_url', $checkout_url);
    // Clear status message "Product added to your cart".
    // @todo fix deprecated code.
    // drupal_get_messages('status');.
  }

  /**
   * Checks if a redirect url has been set.
   *
   * Redirects to the provided url if there is one.
   *
   * @param SymfonyComponentHttpKernelEventFilterResponseEvent $event
   *   The response event.
   */
  public function checkRedirectIssued(FilterResponseEvent $event) {
    $request = $event->getRequest();
    $redirect_url = $request->attributes->get('mymodule_jump_to_checkout_url');
    if (isset($redirect_url)) {
      $event->setResponse(new RedirectResponse($redirect_url));
    }
  }

}

theming – Adding Drupal 8/9 Commerce Wishlist Share Mail Template Variables from Form

I am trying to add 2 additional form fields to the Wishlist Share form where the user input will be rendered in the email. I have been able to add the fields to the form, but I am not sure how to add the user’s input in the email twig template.

Here is how I have updated the form() function:

  public function form(array $form, FormStateInterface $form_state) {
    $form('#tree') = TRUE;
    $form('#attached')('library')() = 'core/drupal.dialog.ajax';
    // Workaround for core bug #2897377.
    $form('#id') = Html::getId($form_state->getBuildInfo()('form_id'));

    $form('to') = (
      '#type' => 'email',
      '#title' => $this->t('Recipient Email'),
      '#required' => TRUE,
    );

// COMBAK my edit

    $form('sender_name') = (
      '#type' => 'textfield',
      '#title' => $this->t('Your Name'),
      '#required' => FALSE,
    );

    $form('sender_message') = (
      '#type' => 'textarea',
      '#title' => $this->t('Your Message'),
      '#required' => FALSE,
    );

// COMBAK eo my edit

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  protected function actions(array $form, FormStateInterface $form_state) {
    $actions('submit') = (
      '#type' => 'submit',
      '#value' => $this->t('Send email'),
      '#submit' => ('::submitForm'),
    );
    if ($this->isAjax()) {
      $actions('submit')('#ajax')('callback') = '::ajaxSubmit';
    }

    return $actions;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    /** @var Drupalcommerce_wishlistEntityWishlistInterface $wishlist */
    $wishlist = $this->entity;
    $to = $form_state->getValue('to');

    // COMBAK: my added vars
    $sender_name = $form_state->getValue('sender_name');
    $sender_message = $form_state->getValue('sender_message');

    $this->wishlistShareMail->send($wishlist, $to, $sender_name, $sender_message);

    $this->messenger()->addStatus($this->t('Shared the wishlist to @recipient.', (
      '@recipient' => $to,
    )));
    $form_state->setRedirectUrl($wishlist->toUrl('user-form'));
  }

This is the function that calls the mailHandler that I have updated:

public function send(WishlistInterface $wishlist, $to, $sender_name, $sender_message) {
    $owner = $wishlist->getOwner();

    $subject = $this->t('Check out my @site-name wishlist', (
      '@site-name' => $this->configFactory->get('system.site')->get('name'),
    ));
    $body = (
      '#theme' => 'commerce_wishlist_share_mail',
      '#wishlist_entity' => $wishlist,
      // COMBAK: my added vars
      '#sender_name' => $sender_name,
      '#sender_message' => $sender_message,
    );
    $params = (
      'id' => 'wishlist_share',
      'from' => $owner->getEmail(),
      'wishlist' => $wishlist,
    );

    return $this->mailHandler->sendMail($to, $subject, $body, $params);
  }

And this is the preprocees function provided by the commerce wishlist module:

function template_preprocess_commerce_wishlist_share_mail(array &$variables) {
  /** @var Drupalcommerce_wishlistEntityWishlistInterface $wishlist */
  $wishlist = $variables('wishlist_entity');
  $wishlist_url = $wishlist->toUrl('canonical', ('absolute' => TRUE));

  $variables('wishlist_url') = $wishlist_url->toString();

  // COMBAK: my added vars
  //$sender_name = $variables('sender_name');
}

And finally the twig template for the email itself:

{#
/**
 * @file
 * Template for the wishlist share email.
 *
 * Available variables:
 * - wishlist_entity: The wishlist entity.
 * - wishlist_url: The wishlist url.
 *
 * @ingroup themeable
 */
#}
<p>
  {% trans %}Check out my wishlist!{% endtrans %}
</p>

<p>
  {% trans %}I use my wishlist for keeping track of items I am interested in.{% endtrans %} <br>
  {% trans %}To see the list in the store and buy items from it, <a href="{{ wishlist_url }}">click here</a>.{% endtrans %}
</p>

<p>
  {% trans %}Thanks for having a look!{% endtrans %}
</p>

I haven’t been able to figure out how to access the variables I added to the body() array in the twig template.

Any help would be greatly appreciated.

Thanks!

theming – Get comments count in commerce product

I set up comments for a commerce product type called “produkt”. Now I want to get the comment count by preprocessing the product in my theme.theme file and get the comment count for the field “field_produktbewertungen”.

function theme_preprocess_commerce_product(&$variables) {
    $variables('ratings_count') = $variables('commerce_product')->get('field_produktbewertungen')->comment_count;
}

Is there anything different in preprocessing products instead of nodes?
Thank you very much!

commerce – Impossible to edit billing address (if not the same than shipping adresse)

****Version : Drupal 9.0.8
****Modules : Drupal Commerce 8.x-2.23 // Commerce Stripe 8.x-1.0-rc4

****My config : Payment gateway = stripe, collect billing information is checked

In checkout pane :
User can’t edit billing adress (even if billing adress is not the same than shipping adress, checkbos not checked)
User can only edit shipping adresse.

What is wrong ?
Is a Stripe bug (like this https://www.drupal.org/project/commerce_stripe/issues/3141622)
But I don’t fine patch or help about this issue

Thank your for your help.
Aude

e commerce – “Pay with PayPal” including card payments label

I’d love to have some opinions on how to best label a the payment option “PayPal” to ensure it is clear that you can pay using your debit/credit card without having a PayPal account.

For background: Our customers come from an older demographics who I suspect may not want/have a PayPal account. Indeed they may not even know what PayPal is.

During our checkout process the customer needs to make a choice of payment methods which looks somewhat like this:

Payment Information

Now, if I just leave “PayPal” I’m afraid to loose those customers who have no idea who/what PayPal is. On the other hand there may be those who love to know we do accept PayPal as they may know and trust it.

I have thought of a few versions, but none really excite me:

  • PayPal, including Debit/Credit cards
  • Debit/Credit cards via PayPal
  • PayPal, debit, or credit card

Does anyone have any thoughts on this? Any research? Any hints how others handle this?

9 – The “Array” plugin does not exist after saving the order type form in Commerce

After saving any order type, the following error shows:

DrupalComponentPluginExceptionPluginNotFoundException: The "Array" plugin does not exist. Valid plugin IDs for Drupalstate_machineWorkflowManager are: invoice_default, order_default, order_default_validation, order_fulfillment, order_fulfillment_validation, payment_default, payment_manual, shipment_default in DrupalCorePluginDefaultPluginManager->doGetDefinition() (line 53 of core/lib/Drupal/Component/Plugin/Discovery/DiscoveryTrait.php).

Any ideas?

commerce – Move Paypal pay button to another block

Is it quite simple to move the Paypal pay buttons to another block?
Can someone let me know if this is possible?

I checked on the Cart form page, but I could not find the query/variable that shows where this lives. This may be something in code.

e commerce – How to manage shipping to multiple addresses during checkout

We are allowing shipping to multiple addresses during checkout for guest customers. The steps are a lot simpler for an existing user since we can pull addresses from their account for them to choose.

However, guest customers have to do the following for every address:

  1. Input address
  2. Choose product(s) to ship
  3. Choose shipping method

What are some good examples you have seen in the past? What are some considerations to look out for?

Commerce Payment method with custom fields

I am trying to create simple payment method with some text-fields the customer must fill out.

Spend a lot of hours on this and can not figure it out.

Right now I am trying to make a custom PaymentMethodType and ad it to a custom PaymentGateWay but i can not get it to work.

Can not find any documentation on creating PaymentMethodTypes.