Back to Stripe for Shopaholic Support

jlarson69874
jlarson69874

Let me first explain what I am doing.

I have a development server setup to test e-commerce functionality for a live site I will be pushing in the next several weeks. I am using the shopaholic sneakers plugin for this, and I am integrating the Stripe for Shopaholic plugin into that, as the customer uses stripe.

Currently, I have looked through the documentation and I have setup my site so that I can add items to the cart, then select stripe as an option. Upon clicking "Pay", I am brought to a screen that just has the "Credit or Debit Card" text, with the stripe element under it for my card info.

Once I put in my test data, I hit submit, only to have the page refreshed. According to the plugin, I am supposed to be redirecting this to the order-complete page.

Regardless, in my Stripe API I am only seeing the payment intent come through, and it tells me the payment is incomplete. I have a PaymentIntent status of "requires_payment_method" and a tip box under there that says "The PaymentIntent requires a payment method: Set an existing payment method on the paymentIntent or have the customer enter a new payment method"

In shopaholic orders, my order is in there, but the status has not changed, nor do I have any updated info on the transaction ID or anything.

Any help would be greatly appreciated.

jlarson69874
jlarson69874

In the new code for PaymentIntents, there is a section on line 213 that unsets the payment_method object from $paymentIntent: public function getPaymentIntentClientSecret() { // payment method $paymentMethod = $this->obOrderItem->payment_method->getObject();

    // create gateway
    $gw = Omnipay::create('Stripe\PaymentIntents');
    $gw->initialize([
        'apiKey' => $paymentMethod->getProperty('apiKey'),
    ]);

    $paymentIntent = $gw->authorize([
        'amount' => $this->obOrder->total_price_data->price_with_tax_value,
        'currency' => $paymentMethod->gateway_currency,
        'payment_method' => 'card',
    ]);
    $data = $paymentIntent->getData();
    unset($data['payment_method']);

    $response = $paymentIntent->sendData($data);
    $data = $response->getData();

    return $data['client_secret'] ?? null;
}

}

This line seems like it would mean we never send a payment_method, but if I remove this line I get an actual error in Stripe that says "payment method "card" is not valid".

Vojta Svoboda
Vojta Svoboda

Yes same here, when uncomment unset($data['payment_method']) I get No such PaymentMethod: 'card'.

With actual code, I'm at least able to generate clientSecret.

Last updated

jlarson69874
jlarson69874

Vojta Svoboda said:

Yes same here, when uncomment unset($data['payment_method']) I get No such PaymentMethod: 'card'.

With actual code, I'm at least able to generate clientSecret.

I'm glad to see I'm not the only one. I am digging in a bit with the stripe PaymentIntents API and I think the problem really does seem to be that payment_method part of the code.

It doesn't seem like Stripe wants the payment_method: card to be sent when a PaymentIntent is requested, but it seems like it is requiring it when the payment submission is attempted.

Vojta Svoboda
Vojta Svoboda

It is related to this issue: https://github.com/thephpleague/omnipay-stripe/issues/186

Payment method is not required by Stripe API, but Omnipay requires it. So I add a payment method for authorization() to satisfy Omnipay, but remove it from sendData() because it is not required by Stripe.

jlarson69874
jlarson69874

Vojta Svoboda said:

It is related to this issue: https://github.com/thephpleague/omnipay-stripe/issues/186

Payment method is not required by Stripe API, but Omnipay requires it. So I add a payment method for authorization() to satisfy Omnipay, but remove it from sendData() because it is not required by Stripe.

This makes sense to me, but it doesn't quite explain why all of my test transactions I am sending to Stripe have messages that say "Incomplete" and "PaymentIntent status: requires_payment_method"

Are you able to successfully complete a payment? It is possible I'm missing something, but I'm having a heck of a time figuring that out.

Vojta Svoboda
Vojta Svoboda

I've tried three ways how to complete payment:

A) Stripe Charge - the "old way", fully supported by this plugin. Create "thank-you" page when placing OrderPage component and in JS part create card element:

var card = elements.create('payment');

Save token by:

$.request('PaymentCardForm::onUpdateToken')

And then trigger payment process:

$.request('OrderPage::onPurchase');

It will work great, but it has a big disadvantage - this method is not compatible with SCA and Stripe will probably send you an email when somebody tries to pay with a payment card with 3D Secure.

So there are two other ways, which are SCA-Compatible: Stripe Payment Intents API and Stripe Checkout.

B) Stripe Payments Intents API - for this, you have to use code example in plugin's documentation and in your HTML there has to be clientSecret filled (check it).

But you have to change $('#payment-form').on('submit') part and NOT stripe.createToken, but stripe.confirmCardPayment which force to show special Stripe modal window where user is able to confirm 3DS (by fingerprint / SMS etc).

Next thing you have to do is in PaymentCardForm->getPaymentIntentClientSecret() method add:

$data['confirmation_method'] = 'automatic';
$data['capture_method'] = 'automatic';

To automatic charge the card. Payment is successfully created in Stripe dashboard, but in your database order remains as NEW. So you have to create webhook and mark order as paid. This method is not supported by this plugin, just creating Payment Intent.

C) Stripe Checkout - also compatible with 3DS and this is the method I'm using in the last project and it is also not supported by this plugin :-)

At first, you have to install the latest omnipay-stripe library: composer require omnipay-stripe:master where Stripe Checkout is supported and also add my update: https://github.com/thephpleague/omnipay-stripe/pull/212 (if it is not merged yet).

Then you have to use the modified Stripeshopaholic version which I'm now preparing for the author.

JS part is then easy:

<script>
  $('#payment-form').on('submit', function (e) {
    $.request('OrderPage::onPurchase');
  });
</script>

You will be redirected to the Stripe payment gateway, where 3DS is managed and your customer fills card details on the trusted Stripe website, not on your shop.

For more info DM me in my profile.

Last updated

jlarson69874
jlarson69874

Vojta Svoboda said:

B) Stripe Payments Intents API - for this, you have to use code example in plugin's documentation and in your HTML there has to be clientSecret filled (check it).

But you have to change $('#payment-form').on('submit') part and NOT stripe.createToken, but stripe.confirmCardPayment which force to show special Stripe modal window where user is able to confirm 3DS (by fingerprint / SMS etc).

Next thing you have to do is in PaymentCardForm->getPaymentIntentClientSecret() method add:

$data['confirmation_method'] = 'automatic';
$data['capture_method'] = 'automatic';

To automatic charge the card. Payment is successfully created in Stripe dashboard, but in your database order remains as NEW. So you have to create webhook and mark order as paid. This method is not supported by this plugin, just creating Payment Intent.

Okay so I tried to DM you but October doesn't give me an option to send you a message. I can only "report as spammer" and I'm sure you don't want that :)

So here's what I got so far. I have implemented your payment Intent example as per the docs and I have also updated the PaymentCardForm.php with the snippets you provided.

success.htm:

url = "/checkout/:slug"
layout = "main"
title = "Success"
is_hidden = 0

[OrderPage]
slug = "{{ :slug }}"
slug_required = 1

[PaymentCardForm]
mode = "ajax"
redirect_on = 1
redirect_page = "order-complete"
slug = "{{ :slug }}"
slug_required = 1
==
{% set payment = PaymentCardForm.get().payment_method.getObject() %}

{% if payment.code == "stripe" %}
    <form method="post" id="payment-form">
        <label for="payment-element">Credit or debit card</label>
        <div id="payment-element"></div>
        <div id="error-message" role="alert" style="color: black; font-size: 4rem;"></div>
        <button id="submit">Submit payment</button>
    </form>

    {% set paymentIntent = PaymentCardForm.getPaymentIntentClientSecret() %}
    <script src="https://js.stripe.com/v3/"></script>
    <script>
      var stripe = Stripe('{{ payment.getProperty("api_key_public") }}');
      var elements = stripe.elements({
        'clientSecret': '{{ paymentIntent }}',
      });
      var card = elements.create('payment');
      card.mount('#payment-element');

      $('#payment-form').on('submit', function (e) {
        e.preventDefault();
        stripe.confirmCardPayment(card).then(function (result) {
          if (result.token) {
            // save token to the order
            $.request('PaymentCardForm::onUpdateToken', {
              'data': {
                'stripeToken': result.token.id,
              }
            });

            // trigger payment request
            $.request('OrderPage::onPurchase');
          }
        });
      });
    </script>
{% endif %}

PaymentCardForm.php:

<?php namespace Vdomah\StripeShopaholic\Components;

use Input;
use Event;
use Lovata\OrdersShopaholic\Classes\Item\OrderItem;
use Omnipay\Omnipay;
use Redirect;

use Lovata\Toolbox\Classes\Helper\PageHelper;
use Lovata\Toolbox\Classes\Helper\UserHelper;
use Lovata\Toolbox\Classes\Component\ComponentSubmitForm;
use Lovata\Toolbox\Traits\Helpers\TraitValidationHelper;
use Lovata\Toolbox\Traits\Helpers\TraitComponentNotFoundResponse;

use Lovata\Shopaholic\Models\Settings;
use Lovata\OrdersShopaholic\Models\Order;
use Vdomah\StripeShopaholic\Classes\Event\ExtendOrderCreateHandler;

/**
 * Class PaymentCardForm
 * @package Vdomah\StripeShopaholic\Classes\Event
 * @author Artem Rybachuk, alchemistt@ukr.net
 */
class PaymentCardForm extends ComponentSubmitForm
{
    use TraitValidationHelper;
    use TraitComponentNotFoundResponse;

    /** @var \Lovata\OrdersShopaholic\Models\Order */
    protected $obOrder;

    /** @var  \Lovata\OrdersShopaholic\Classes\Item\OrderItem */
    protected $obOrderItem = null;

    /**
     * @return array
     */
    public function componentDetails()
    {
        return [
            'name'        => 'vdomah.stripeshopaholic::lang.component.payment_card_form_name',
            'description' => 'vdomah.stripeshopaholic::lang.component.payment_card_form_description',
        ];
    }

    /**
     * @return array
     */
    public function defineProperties()
    {
        $arResult = $this->getModeProperty();

        $arResult = array_merge($arResult, $this->getElementPageProperties());

        return $arResult;
    }

    /**
     * Get element item
     * @return \Lovata\Toolbox\Classes\Item\ElementItem
     */
    public function get()
    {
        return $this->obOrderItem;
    }

    /**
     * Get redirect page property list
     * @return array
     */
    protected function getRedirectPageProperties()
    {
        if (!Result::status() || empty($this->obOrder)) {
            return [];
        }

        $arResult = [
            'id'     => $this->obOrder->id,
            'number' => $this->obOrder->order_number,
            'key'    => $this->obOrder->secret_key,
        ];

        $sRedirectPage = $this->property(self::PROPERTY_REDIRECT_PAGE);
        if (empty($sRedirectPage)) {
            return $arResult;
        }

        $arPropertyList = PageHelper::instance()->getUrlParamList($sRedirectPage, 'OrderPage');
        if (!empty($arPropertyList)) {
            $arResult[array_shift($arPropertyList)] = $this->obOrder->secret_key;
        }

        return $arResult;
    }

    /**
     * Init plugin method
     */
    public function init()
    {
        $this->bCreateNewUser = Settings::getValue('create_new_user');
        $this->obUser = UserHelper::instance()->getUser();

        parent::init();

        //Get element slug
        $sElementSlug = $this->property('slug');
        if (empty($sElementSlug)) {
            return;
        }

        //Get element by slug
        $this->obOrder = $this->getElementObject($sElementSlug);
        if (empty($this->obOrder)) {
            return;
        }

        $this->obOrderItem = $this->makeItem($this->obOrder->id, $this->obOrder);
    }

    /**
     * Set payment token
     * @return \Illuminate\Http\RedirectResponse|null
     * @throws \Exception
     */
    public function onRun()
    {
        if ($this->sMode != self::MODE_SUBMIT) {
            return null;
        }

        $sPaymentToken = Input::get(ExtendOrderCreateHandler::STRIPE_TOKEN_PARAM_NAME);
        if (empty($sPaymentToken)) {
            return null;
        }

        $this->obOrder->payment_token = $sPaymentToken;
        $this->obOrder->save();

        $sRedirectURL = $this->property(self::PROPERTY_REDIRECT_PAGE);

        return $this->getResponseModeForm($sRedirectURL);
    }

    /**
     * Set payment token (AJAX)
     * @return \Illuminate\Http\RedirectResponse|array
     * @throws \Exception
     */
    public function onUpdateToken()
    {
        $sPaymentToken = Input::get(ExtendOrderCreateHandler::STRIPE_TOKEN_PARAM_NAME);

        $this->obOrder->payment_token = $sPaymentToken;
        $this->obOrder->save();

        $sRedirectURL = $this->property(self::PROPERTY_REDIRECT_PAGE);

        return $this->getResponseModeAjax($sRedirectURL);
    }

    /**
     * Get element object
     * @param string $sElementSlug
     * @return Order
     */
    protected function getElementObject($sElementSlug)
    {
        if (empty($sElementSlug)) {
            return null;
        }

        $this->obUser = UserHelper::instance()->getUser();

        $obElement = Order::getBySecretKey($sElementSlug)->first();
        if (!empty($obElement) && !empty($this->obUser) && $obElement->user_id != $this->obUser->id) {
            $obElement = null;
        }

        return $obElement;
    }

    /**
     * @param int    $iElementID
     * @param Order $obElement
     * @return OrderItem
     */
    protected function makeItem($iElementID, $obElement)
    {
        return OrderItem::make($iElementID, $obElement);
    }

    /**
     * @return string|null
     */
    public function getPaymentIntentClientSecret()
    {
        // payment method
        $paymentMethod = $this->obOrderItem->payment_method->getObject();

        // create gateway
        $gw = Omnipay::create('Stripe\PaymentIntents');
        $gw->initialize([
            'apiKey' => $paymentMethod->getProperty('apiKey'),
        ]);

        $paymentIntent = $gw->authorize([
            'amount' => $this->obOrder->total_price_data->price_with_tax_value,
            'currency' => $paymentMethod->gateway_currency,
            'payment_method' => 'card',
            'confirm' => true,
            'returnUrl' => 'https://complexity.liquidcreative.net/order-complete/',
        ]);
        $data = $paymentIntent->getData();
        unset($data['payment_method']);
        $data['confirmation_method'] = 'automatic';
        $data['capture_method'] = 'automatic';

        $response = $paymentIntent->sendData($data);
        $data = $response->getData();

        return $data['client_secret'] ?? null;
    }
}

This pretty much does the same thing, but I do see that we are passing it a few new items. I can see that in Stripe in my logs.

However, I am still getting "Incomplete" payment statuses in Stripe itself. If I click these payments in Stripe, it tells me that "The PaymentIntent requires a payment method: PaymentIntent status: requires_payment_method"

I suspect it is because we are doing this:

 unset($data['payment_method']);

However, if I remove that block of code, I get an actual error log that says

resource_missing - payment_method

No such PaymentMethod: 'card'

I was told this is because OmniPay doesn't accept that currently, but this is where we are with PaymentIntent.

If StripeCheckout was supported, I would be willing to go that route as well, but as you might be able to tell, this is an aspect I don't have a lot of experience, and it seems that adding in the "October Factor" complicates things a bit with the Stripe API documentation.

Vojta Svoboda
Vojta Svoboda

Try it with card element:

      var card = elements.create('card');
      card.mount('#card-element');

      $('#payment-form').on('submit', function (e) {
        e.preventDefault();
        stripe.confirmCardPayment('{{ paymentIntent }}', {
          payment_method: {
            card: card
          },
        }).then(function (result) {
          console.log(result);
        });
      });

It works for me.

vdomah
vdomah

new plugin version available: 1.0.5 Stripe payment method to Stripe Charge; Stripe Payment Intents and Stripe Checkout methods added. Thanks to Vojta Svoboda

1-10 of 10