Closed taylormodernized closed 4 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.
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.
Here's a screenshot of the Authorize.NET payment method being loaded in via this method:
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
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:
Magento Stores>Configuration>Sales>Payment Methods
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?