scandipwa / scandipwa

Next-generation front-end for Magento 2
https://scandipwa.com/?utm_source=github&utm_medium=readme&utm_campaign=general
Open Software License 3.0
547 stars 314 forks source link

Payment Methods other than Check/Money order don't show additional interface options for checkout #283

Closed taylormodernized closed 4 years ago

taylormodernized commented 5 years ago

Describe the bug After activating any of the built-in payment methods in Magento, when checking out with a payment method other than "Check / Money Order", the interface doesn't show additional fields or forms for input to allow the other checkout methods. Instead, the data from the checkout process is sent to Magento, and is then rejected because it doesn't have payment information attached.

To Reproduce Steps to reproduce the behavior:

  1. Enable Authorize.NET, Paypal or Braintree in Magento Stores>Configuration>Sales>Payment Methods
  2. Add an item to your cart
  3. Go to checkout, fill out the form, and proceed to step 2 of checkout
  4. Select the non check/money order payment method you enabled in 1
  5. Continue the checkout
  6. See that there is no form displayed for the payment method, and submitting the order fails due to payment information missing

Expected behavior A form based on the enabled payment method is displayed, and payment information is attached to the submitted order

Additional information: The majority of payment methods in Magento rely on Knockout and .html templates, as well as a large JSON blob in the checkout page that feeds information about the current quote and customer. Is there a plan for handling this in another way?

alfredsgenkins commented 5 years ago

At this point the the roadmap includes plans for checkout abstraction. We are planning to make it easy to add additional payment, shipping methods and extend the form. Currently the checkout internal implementation and the form implementation are blocking easy payments and shipping method integration / implementation. The implementation is divided into two large tasks: the first one is the checkout rewrite and abstraction itself and the form "portal" implementation.

We have not yet started implementing this. However there are already projects which implement custom payment methods.

Regarding the Magento implementation of payment methods: it is impossible to use knockout and template logic directly.

Cooperation on implementation of features like this is important.

taylormodernized commented 5 years ago

Regarding the Magento implementation of payment methods: it is impossible to use knockout and template logic directly.

I'd disagree with this statement. Magento imports their knockout with requirejs in a very specific and predictable way, and I think you could very well use the existing payment method scaffolding.

I've gotten the payment methods rendering in a ScandiPWA React page with the following code:

First, we pull in the Magento requirejs and knockout framework with

<script type="text/javascript"  src="https://m2.test/static/frontend/Magento/luma/en_US/requirejs/require.js"></script>
<script type="text/javascript"  src="https://m2.test/static/frontend/Magento/luma/en_US/mage/requirejs/mixins.js"></script>
<script type="text/javascript"  src="https://m2.test/static/frontend/Magento/luma/en_US/requirejs-config.js"></script>
<script type="text/javascript"  src="https://m2.test/static/frontend/Magento/luma/en_US/mage/polyfill.js"></script>

You can pull these in from either the luma theme or the blank theme.

Also needed will be the BASE_URL and require variables:

<script>
    var BASE_URL = 'https://m2.test/';
    var require = {
        "baseUrl": "https://m2.test/static/frontend/Magento/luma/en_US"
    };
</script>

A GraphQL endpoint would also need to be set up to pull in the two separate configurations from the checkout page. (a checkoutConfig object from the CompositeConfigProvider, and the knockout template JSON that gets built by the checkout.root block)

        $quoteIdMask = $this->quoteIdMaskFactory->create()->load($guestCartId, 'masked_id');
        $quoteId = $quoteIdMask->getQuoteId();
        $quote = $this->quoteRepository->get($quoteId);
        $quote->setIsActive(1);
        $this->quoteRepository->save($quote);
        $this->checkoutSession->replaceQuote($quote);
        $themeCollection = $this->_themesFactory->create();
        $theme = $themeCollection->getItemByColumnValue('code', 'Magento/blank');
        $this->_design->setDesignTheme($theme->getId(), Area::AREA_FRONTEND);

        $checkoutData = $this->_state->emulateAreaCode(Area::AREA_FRONTEND, function () {
            $this->checkoutSession->start();
            $this->_objectmanager->configure($this->configLoader->load('frontend'));
            $newPage = $this->_objectmanager->create(Page::class);
            $newPage->addHandle("default");
            $newPage->addHandle("checkout_index_index");
            $layout = $newPage->getLayout();
            // Force the layout to update
            $update = $layout->getUpdate();
            $newPage->getLayout()->getAllBlocks();

            $config = json_decode(
                $newPage
                    ->getLayout()
                    ->getBlock("checkout.root")
                    ->getJsLayout(),
                true
            );
            $paymentMethodConfig   = $config
                      ['components']['checkout']['children']['steps']['children']
                      ['billing-step']['children']['payment']['children']['payments-list']['children'];

            $paymentMethodConfig[] = $config['components']['checkout']['children']['steps']['children']
                      ['billing-step']['children']['payment']['children']['renders']['children'];
                // Get composite config manager in this context
                $checkoutConfig =  json_encode($this->_objectmanager->create(CompositeConfigProvider::class)->getConfig(), JSON_HEX_TAG);

            return [
                "paymentMethodConfig" => json_encode($paymentMethodConfig),
                "checkoutConfig" => $checkoutConfig
            ];
        });

In Magento, the way the payment methods are implemented requires the layout to be checkout_index_index. Because we're currently in the graphql area, we have to set specific settings to convince Magento to build the correct layout for the checkout_index_index page to give us the correct JSON layout from the checkout.root block.

$this->checkoutSession->replaceQuote($quote); We set the current quote for the session so that the payment processor block can collect the data from the current quote. (e.g. disabling specific methods for checking out)

$this->_design->setDesignTheme($theme->getId(), Area::AREA_FRONTEND); The current theme must be set to the Magento/Blank theme so that the correct layout blocks exist, otherwise the checkout.root block won't be inserted into the layout

$this->_state->emulateAreaCode(Area::AREA_FRONTEND, We have to move into a frontend emulated area in order to trigger the correct layout updates

$this->_objectmanager->configure($this->configLoader->load('frontend')); This forces the object manager to be tricked into loading the correct DI classes for the frontend in this emulated state.

$newPage->addHandle("checkout_index_index");
$newPage->getLayout()->getAllBlocks();

A new page is created, forced to have the checkout_index_index handle, and then forced to have all the blocks loaded in.

$config = json_decode(
                $newPage
                    ->getLayout()
                    ->getBlock("checkout.root")
                    ->getJsLayout(),
                true
            );
$paymentMethodConfig   = $config
                      ['components']['checkout']['children']['steps']['children']
                      ['billing-step']['children']['payment']['children']['payments-list']['children'];

$paymentMethodConfig[] = $config['components']['checkout']['children']['steps']['children']
                      ['billing-step']['children']['payment']['children']['renders']['children'];

$checkoutConfig =  json_encode($this->_objectmanager->create(CompositeConfigProvider::class)->getConfig(), JSON_HEX_TAG);

The checkout.root block is where the payment configuration layout updates reside for payment methods, but it exists in JSON, and is intermixed with the checkout forms, so we pull the entire JSON object in, decode it, and extract only the objects that are needed for rendering payment methods.

For brevity, I'm omitting the Reducers, etc. that were required for loading in the data. We just need to ensure that window.checkoutConfig is set to the value of the checkoutConfig before the knockout template is rendered. After the checkoutConfig is set, we render the knockout template with something like this:

{<div className="checkout-payment-method" id="payment-methods" data-bind="scope:'custom-payment-listing'" dangerouslySetInnerHTML={{__html: '\
                        <!-- ko template: getTemplate() --><!-- /ko -->\
                    <script type="text/x-magento-init">\
                        {\
                            "#payment-methods": {\
                                "Magento_Ui/js/core/app": {\
                                    "components": {\
                                        "custom-payment-listing": {\
                                            "children": {\
                                                "payments-list": {\
                                                    "component": "Magento_Checkout/js/view/payment/list",\
                                                    "displayArea": "payment-methods-list",\
                                                    "children": '+JSON.stringify(this.props.paymentConfig)+'\
                                                }\
                                            },\
                                            "component": "MM_IntegratedCheckout/js/payment-methods-listing",\
                                            "config": {\
                                                "template": "MM_IntegratedCheckout/custom-payment-listing"\
                                            }\
                                        }\
                                    }\
                                }\
                            }\
                        }\
                    </script>'}}></div>}

After the template is injected into the DOM, the x-magento-init won't be initially picked up by the Magento handlers. We trigger them manually with

requirejs(['jquery', 'mage/apply/main', 'ko', 'MM_IntegratedCheckout/js/payment-methods-listing'], ($, processScripts,ko, paymentslisting)=>{
                    let paymentsListing = new paymentslisting()
                    ko.applyBindings(paymentsListing, $('#payment-methods')[0]);
                    processScripts.apply();
            });

Where MM_IntegratedCheckout/js/payment-methods-listing is a custom js component with the following:

define([
    'jquery',
    'uiComponent',
    'ko',
    'Magento_Checkout/js/action/get-payment-information',
    'Magento_Checkout/js/model/payment-service',
    'Magento_Checkout/js/model/checkout-data-resolver',
    'Magento_Checkout/js/model/quote',
    'Magento_Customer/js/model/address-list',
], function (
    $,
    Component,
    ko,
    getPaymentInformation,
    paymentService,
    checkoutDataResolver,
    quote,
    addressList
) {
    'use strict';
    return Component.extend({
        isPaymentMethodsAvailable: ko.computed(function () {
            return paymentService.getAvailablePaymentMethods().length > 0;
        }),
        defaults: {
            template: 'MM_IntegratedCheckout/custom-payment-listing'
        },
        initialize: function () {
            quote.paymentMethod.subscribe(function () {
                checkoutDataResolver.resolveBillingAddress();
            }, this);
            quote.billingAddress = ko.observable(addressList()[0]);
            quote.shippingAddress = ko.observable(addressList()[0]);

            checkoutDataResolver.resolvePaymentMethod();
            getPaymentInformation();
            this._super();
        },
    });
});

and the template MM_IntegratedCheckout/custom-payment-listing is the following:

<!-- ko foreach: getRegion('payment-methods-list') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->

After the knockout template has loaded, it will pull in the available payment methods, along with their respective templates, javascript, styles and needed configuration.

As for getting the form data back into React, the methods from http://intelligiblebabble.com/making-reactjs-and-knockoutjs-play-nice/ can be employed to connect Knockout with ReactJS, and allow the data from the form to be passed into the parent React component, and passed along with the rest of the checkout data.


The downsides of this method are

To mitigate how many additional scripts are loaded via requirejs, you could probably create a custom requirejs-config.js to load in only the required scripts

I'm not fully convinced myself that this would be the best method for implementing the payment gateways for ScandiPWA, but I believe it would be the best method for leveraging existing Magento functionality.

taylormodernized commented 5 years ago

Here's a screenshot of the Authorize.NET payment method being loaded in via this method: image

alfredsgenkins commented 5 years ago

Thanks! That's a great job done! I am doubting that we will go with this solution, but is great you proved it is possible. The payment method support like: Braintree, Stripe, PayPal will become available in 2.5.0.

In 2.4.0 we created a better abstraction layer for implementing the payment methods / shipping methods.

Thanks once again for the job done. You are awesome! <3