Magento 2 – Custom Payment Method

Custom Payment Methods

Sometimes, clients can request specific payment methods. These offer much more than out-of-the-box Magento ones. In order to customize all the fields based on clients’ needs, you need to create a custom payment method. It also allows you to customize the checkout and API logic used for implementing the payment method.

Additional payment methods can bring the diversity of customer choices when they buy on your site. On the other hand, multiple payment methods are great for reaching out to the global marketplace.

Payment Method Configuration

Now, let’s create our own Payment Gateway integration in Magento 2 stores. After launching the new payment method, you can find and configure it here: 

Admin Panel > Stores > Settings > Configuration > Sales > Payment Methods.

In order to create a custom payment method, you’ll need to add a couple of configuration settings to the module. Moreover, you will surely have many additional settings. However, today, we will only add a few very basic ones.

So, here is what you need:

  • an “enabled” setting
  • a title to display for it
  • a setting for accepted credit card types

All of these will be added to the etc/adminhtml/system.xml file.

registration.php and module.xml

Firstly, add configuration settings for your payment method. The files are registration.php and module.xml

file: app/code/Vendor/Module/registration.php

<?php
   \Magento\Framework\Component\ComponentRegistrar::register(
   \Magento\Framework\Component\ComponentRegistrar::MODULE,
   SyncIt_Payment,
   __DIR__
);

file: app/code/Vendor/Module/etc/module.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
   <module name="SyncIt_Payment" setup_version="0.0.1">
       <sequence>
           <module name="Magento_Sales"/>
           <module name="Magento_Payment"/>
           <module name="Magento_Checkout"/>
       </sequence>
   </module>
</config>

system.xml

Then, in the system.xml file, you can allow the merchant to configure:

  • a title to display for your payment method
  • a setting for acceptable credit card types
  • password and other necessary stuff from the admin panel

file: app/code/Vendor/Module/etc/adminhtml/system.xml

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="payment">
            <group id="method" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
                <label> Payment Gateway</label>
                <field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
                <field id="merchant_shop_id" translate="label"  sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Merchant Shop ID</label>
                </field>
                <field id="merchant_gateway_store_key" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Merchant Gateway Store Key</label>
                    <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
                </field>
                <field id="shop_username" translate="label"  sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Merchant API Username</label>
                </field>

                <field id="shop_password" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Merchant API Password</label>
                    <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
                </field>
                <field id="test_mode" translate="label" type="select" sortOrder="85" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Test mode</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="payment_action" translate="label" type="select" sortOrder="110" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Payment Action</label>
                    <source_model>SyncIt\Payment\Model\Adminhtml\Source\PaymentAction</source_model>
                </field>
                <field id="allowspecific" translate="label" type="allowspecific" sortOrder="130" showInDefault="9"
                       showInWebsite="1" showInStore="1">
                    <label>Payment From Applicable Countries</label>
                    <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model>
                </field>
                <field id="specificcountry" translate="label" type="multiselect" sortOrder="140" showInDefault="1"
                       showInWebsite="1" showInStore="1">
                    <label>Payment From Specific Countries</label>
                    <source_model>Magento\Directory\Model\Config\Source\Country</source_model>
                </field>
                <field id="sort_order" translate="label" type="text" sortOrder="160" showInDefault="1" showInWebsite="1"
                       showInStore="1">
                    <label>Sort Order</label>
                    <frontend_class>validate-number</frontend_class>
                </field>
                <field id="min_order_total" translate="label" type="text" sortOrder="260" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Minimum Order Total</label>
                </field>
                <field id="max_order_total" translate="label" type="text" sortOrder="270" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Maximum Order Total</label>
                </field>
                <field id="failed_payment_email" translate="label comment" type="select" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
                    <label>Failed Payment Email</label>
                    <comment>Email sent when there is ann error while processing payment.</comment>
                    <source_model>Magento\Config\Model\Config\Source\Email\Template</source_model>
                </field>
            </group>
        </section>
    </system>
</config>

di.xml

Afterward, you should define additional configuration in the di.xml file. So, here is how the di.xml configuration for CommandPool, GatewayCommand, and other classes might look like.

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <!-- Payment Method Facade configuration -->
    <virtualType name="SamplePaymentGatewayFacade" type="Magento\Payment\Model\Method\Adapter">
        <arguments>
            <argument name="code" xsi:type="const">\SyncIt\Payment\Model\Ui\ConfigProvider::CODE</argument>
            <argument name="formBlockType" xsi:type="string">Magento\Payment\Block\Form</argument>
            <argument name="infoBlockType" xsi:type="string">SyncIt\Payment\Block\Info</argument>
            <argument name="valueHandlerPool" xsi:type="object">SamplePaymentGatewayValueHandlerPool</argument>
            <argument name="commandPool" xsi:type="object">SamplePaymentGatewayCommandPool</argument>
        </arguments>
    </virtualType>

    <!-- Configuration reader -->
    <virtualType name="SamplePaymentGatewayConfig" type="Magento\Payment\Gateway\Config\Config">
        <arguments>
            <argument name="methodCode" xsi:type="const">\SyncIt\Payment\Model\Ui\ConfigProvider::CODE</argument>
        </arguments>
    </virtualType>

    <!-- Logger, initialized with SamplePaymentGatewayConfig -->
    <virtualType name="SamplePaymentGatewayLogger" type="Magento\Payment\Model\Method\Logger">
        <arguments>
            <argument name="config" xsi:type="object">SamplePaymentGatewayConfig</argument>
        </arguments>
    </virtualType>

    <type name="SyncIt\Payment\Gateway\Http\Client\ClientMock">
        <arguments>
            <argument name="logger" xsi:type="object">SamplePaymentGatewayLogger</argument>
        </arguments>
    </type>

    <!-- Commands infrastructure -->
    <virtualType name="SamplePaymentGatewayCommandPool" type="Magento\Payment\Gateway\Command\CommandPool">
        <arguments>
            <argument name="commands" xsi:type="array">
                <item name="authorize" xsi:type="string">SamplePaymentGatewayAuthorizeCommand</item>
                <item name="capture" xsi:type="string">SamplePaymentGatewayCaptureCommand</item>
                <item name="void" xsi:type="string">SamplePaymentGatewayVoidCommand</item>
            </argument>
        </arguments>
    </virtualType>

    <!-- Authorize command -->
    <virtualType name="SamplePaymentGatewayAuthorizeCommand" type="Magento\Payment\Gateway\Command\GatewayCommand">
        <arguments>
            <argument name="requestBuilder" xsi:type="object">SamplePaymentGatewayAuthorizationRequest</argument>
            <argument name="handler" xsi:type="object">SamplePaymentGatewayResponseHandlerComposite</argument>
            <argument name="transferFactory" xsi:type="object">SyncIt\Payment\Gateway\Http\TransferFactory</argument>
            <argument name="client" xsi:type="object">SyncIt\Payment\Gateway\Http\Client\ClientMock</argument>
        </arguments>
    </virtualType>

    <!-- Authorization Request -->
    <virtualType name="SamplePaymentGatewayAuthorizationRequest" type="Magento\Payment\Gateway\Request\BuilderComposite">
        <arguments>
            <argument name="builders" xsi:type="array">
                <item name="transaction" xsi:type="string">SyncIt\Payment\Gateway\Request\AuthorizationRequest</item>
                <item name="mockData" xsi:type="string">SyncIt\Payment\Gateway\Request\MockDataRequest</item>
            </argument>
        </arguments>
    </virtualType>
    <type name="SyncIt\Payment\Gateway\Request\AuthorizationRequest">
        <arguments>
            <argument name="config" xsi:type="object">SamplePaymentGatewayConfig</argument>
        </arguments>
    </type>

    <!-- Capture command -->
    <virtualType name="SamplePaymentGatewayCaptureCommand" type="Magento\Payment\Gateway\Command\GatewayCommand">
        <arguments>
            <argument name="requestBuilder" xsi:type="object">SyncIt\Payment\Gateway\Request\CaptureRequest</argument>
            <argument name="handler" xsi:type="object">SyncIt\Payment\Gateway\Response\TxnIdHandler</argument>
            <argument name="transferFactory" xsi:type="object">SyncIt\Payment\Gateway\Http\TransferFactory</argument>
            <argument name="validator" xsi:type="object">SyncIt\Payment\Gateway\Validator\ResponseCodeValidator</argument>
            <argument name="client" xsi:type="object">SyncIt\Payment\Gateway\Http\Client\ClientMock</argument>
        </arguments>
    </virtualType>

    <!-- Capture Request -->
    <type name="SyncIt\Payment\Gateway\Request\CaptureRequest">
        <arguments>
            <argument name="config" xsi:type="object">SamplePaymentGatewayConfig</argument>
        </arguments>
    </type>

    <!-- Void command -->
    <virtualType name="SamplePaymentGatewayVoidCommand" type="Magento\Payment\Gateway\Command\GatewayCommand">
        <arguments>
            <argument name="requestBuilder" xsi:type="object">SyncIt\Payment\Gateway\Request\VoidRequest</argument>
            <argument name="handler" xsi:type="object">SyncIt\Payment\Gateway\Response\TxnIdHandler</argument>
            <argument name="transferFactory" xsi:type="object">SyncIt\Payment\Gateway\Http\TransferFactory</argument>
            <argument name="validator" xsi:type="object">SyncIt\Payment\Gateway\Validator\ResponseCodeValidator</argument>
            <argument name="client" xsi:type="object">SyncIt\Payment\Gateway\Http\Client\ClientMock</argument>
        </arguments>
    </virtualType>

    <!-- Void Request -->
    <type name="SyncIt\Payment\Gateway\Request\VoidRequest">
        <arguments>
            <argument name="config" xsi:type="object">SamplePaymentGatewayConfig</argument>
        </arguments>
    </type>

    <!-- Response handlers -->
    <virtualType name="SamplePaymentGatewayResponseHandlerComposite" type="Magento\Payment\Gateway\Response\HandlerChain">
        <arguments>
            <argument name="handlers" xsi:type="array">
                <item name="txnid" xsi:type="string">SyncIt\Payment\Gateway\Response\TxnIdHandler</item>
                <item name="fraud" xsi:type="string">SyncIt\Payment\Gateway\Response\FraudHandler</item>
            </argument>
        </arguments>
    </virtualType>

    <!-- Value handlers infrastructure -->
    <virtualType name="SamplePaymentGatewayValueHandlerPool" type="Magento\Payment\Gateway\Config\ValueHandlerPool">
        <arguments>
            <argument name="handlers" xsi:type="array">
                <item name="default" xsi:type="string">SamplePaymentGatewayConfigValueHandler</item>
            </argument>
        </arguments>
    </virtualType>
    <virtualType name="SamplePaymentGatewayConfigValueHandler" type="Magento\Payment\Gateway\Config\ConfigValueHandler">
        <arguments>
            <argument name="configInterface" xsi:type="object">SamplePaymentGatewayConfig</argument>
        </arguments>
    </virtualType>

    <type name="SyncIt\Payment\Block\Info">
        <arguments>
            <argument name="config" xsi:type="object">SamplePaymentGatewayConfig</argument>
        </arguments>
    </type>

    <type name="SyncIt\Payment\Logger\Handler">
        <arguments>
            <argument name="filesystem" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument>
        </arguments>
    </type>
    <type name="SyncIt\Payment\Logger\Logger">
        <arguments>
            <argument name="name" xsi:type="string">paymentIntegration</argument>
            <argument name="handlers"  xsi:type="array">
                <item name="system" xsi:type="object">SyncIt\Payment\Logger\Handler</item>
            </argument>
        </arguments>
    </type>
    <type name="Magento\Sales\Model\Order\Email\Container\OrderIdentity">
        <plugin name="change_is_enable_method" type="SyncIt\Payment\Plugin\Sales\Order\Email\Container\OrderIdentityPlugin"/>
    </type>

    <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
        <arguments>
            <argument name="collections" xsi:type="array">
                <item name="Payment_transaction_listing_data_source" xsi:type="string">SyncIt\Payment\Model\ResourceModel\PaymentTransaction\Grid\Collection</item>
            </argument>
        </arguments>
    </type>

    <type name="SyncIt\Payment\Model\ResourceModel\PaymentTransaction\Grid\Collection">
        <arguments>
            <argument name="mainTable" xsi:type="string">syncit_Payment_payment_transaction</argument>
            <argument name="eventPrefix" xsi:type="string">syncit_Payment_transaction_grid_collection</argument>
            <argument name="eventObject" xsi:type="string">syncit_Payment_transaction_grid_collection</argument>
            <argument name="resourceModel" xsi:type="string">\SyncIt\Payment\Model\ResourceModel\PaymentTransaction</argument>
        </arguments>
    </type>
</config>

In di.xml we also define a database model for storing results/responses from Payment Provider, Logger Class, etc.

Payment Adapter Class

Custom Payment Method class typically extends the Magento\Payment\Model\Method\AbstractMethod class. Well, great news – you no longer needed it!

file: app/code/Vendor/Module/registration.php

<?php
   namespace Magento\Payment\Model\Method;
 
use Magento\Framework\Event\ManagerInterface;
use Magento\Payment\Gateway\Command\CommandPoolInterface;
use Magento\Payment\Gateway\Config\ValueHandlerPoolInterface;
use Magento\Payment\Gateway\Data\PaymentDataObjectFactory;
use Magento\Payment\Gateway\Validator\ValidatorPoolInterface;
use Magento\Payment\Model\MethodInterface;
/* more uses */
 
class Adapter implements MethodInterface
{
   
    public function __construct(
        ManagerInterface $eventManager,
        ValueHandlerPoolInterface $valueHandlerPool,
        ValidatorPoolInterface $validatorPool,
        CommandPoolInterface $commandPool,
        PaymentDataObjectFactory $paymentDataObjectFactory,
        $code,
        $formBlockType,
        $infoBlockType
    ) {
       /* CODE */
    }
 
    /* Your custom CODE */
}

You can find more great information here.

Displaying the New Payment During Checkout

There are two important JS files to add: 

  1. the payment method renderer
  2. the component that registers the renderer

Payment Method Renderer

 file: view/frontend/web/js/view/payment/payment.js

/*global define*/
define(
   [
       'uiComponent',
       'Magento_Checkout/js/model/payment/renderer-list'
   ],
   function (
       Component,
       rendererList
   ) {
       'use strict';
       rendererList.push(
           {
               type: payment,
               component: 'SyncIt_Payment/js/view/payment/method-renderer/payway'
           }
       );
       /** Add view logic here if needed */
       return Component.extend({});
   }
);

file: view/frontend/web/js/view/payment/method-renderer/payment.js
define(
   [
     'jquery',
       'Magento_Checkout/js/view/payment/default',
     'Magento_Checkout/js/model/payment/additional-validators',
     'Magento_Paypal/js/action/set-payment-method',
     'Magento_Customer/js/customer-data'
   ],
   function ($, Component, additionalValidators, setPaymentMethodAction, customerData) {
       'use strict';

       return Component.extend({
           defaults: {
               template: 'SyncIt_Payment/payment/form',
               transactionResult: ''
           },

           initObservable: function () {

               this._super()
                   .observe([
                       'transactionResult'
                   ]);
               return this;
           },

           getCode: function() {
               return payment;
           },

           getData: function() {
               return {
                   'method': this.item.method,
                   'additional_data': {
                       'transaction_result': this.transactionResult()
                   }
               };
           },

           getTransactionResults: function() {
               return _.map(window.checkoutConfig.payment.payment.transactionResults, function(value, key) {
                   return {
                       'value': key,
                       'transaction_result': value
                   }
               });
           },

        /** Redirect to payment provider */
        continueToPayWay: function () {
           if (additionalValidators.validate()) {
              this.selectPaymentMethod();
              setPaymentMethodAction(this.messageContainer).done(
                 function () {
                    customerData.invalidate(['cart']);
                    $.mage.redirect(
                       window.checkoutConfig.payment.payment.redirectUrl
                    );
                 }
              );

              return false;
           }
        }
       });
   }
);

JS Renderer

The JS renderer uses Knockout in order to render the uiComponent. 

file: /view/frontend/web/template/payment/form.html

/*global define*/
<!--
 ~ SyncIt Group
 ~
 ~ This source file is subject to the SyncIt Software License, which is available at https://syncitgroup.com/.
 ~ Do not edit or add to this file if you wish to upgrade to the newer versions in the future.
 ~ If you wish to customize this module for your needs.
 ~ Please refer to http://www.magentocommerce.com for more information.
 ~
 ~ @category  SyncIt
 ~ @package   SyncIt_PayWay
 ~ @author    Igor Stajić <[email protected]>
 ~ @link      https://syncitgroup.com/
 ~ @license   http://opensource.org/licenses/gpl-license.php GNU Public License
 ~ @copyright Copyright (C) 2019 SyncIt (https://syncitgroup.com/)
 ~
 -->
<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}">
   <div class="payment-method-title field choice">
       <input type="radio"
              name="payment[method]"
              class="radio"
              data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()"/>
       <label class="label" data-bind="attr: {'for': getCode()}">
           <span data-bind="text: getTitle()"></span>
       </label>
   </div>

   <div class="payment-method-content">
       <!-- ko foreach: getRegion('messages') -->
       <!-- ko template: getTemplate() --><!-- /ko -->
       <!--/ko-->
       <div class="payment-method-billing-address">
           <!-- ko foreach: $parent.getRegion(getBillingAddressFormName()) -->
           <!-- ko template: getTemplate() --><!-- /ko -->
           <!--/ko-->
       </div>
       <div class="checkout-agreements-block">
           <!-- ko foreach: $parent.getRegion('before-place-order') -->
           <!-- ko template: getTemplate() --><!-- /ko -->
           <!--/ko-->
       </div>
       <div class="actions-toolbar">
           <div class="primary">
               <button class="action primary checkout"
                       type="submit"
                       data-bind="click: continueToPayment, enable: (getCode() == isChecked())"
                       disabled>
                   <span data-bind="i18n: 'Continue to Payment'"></span>
               </button>
           </div>
       </div>
   </div>
</div>

When a customer clicks on ‘Place Order’ they will be redirected to the custom URL. Of course, you can prepare the necessary data for the Payment Provider.

Finally, tell Magento where to include these JS files.

file: view/frontend/web/js/view/payment/payment.js

<?xml version="1.0"?>
<!--
/**
* Copyright © 2016 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
   <body>
       <referenceBlock name="checkout.root">
           <arguments>
               <argument name="jsLayout" xsi:type="array">
                   <item name="components" xsi:type="array">
                       <item name="checkout" xsi:type="array">
                           <item name="children" xsi:type="array">
                               <item name="steps" xsi:type="array">
                                   <item name="children" xsi:type="array">
                                       <item name="billing-step" xsi:type="array">
                                           <item name="component" xsi:type="string">uiComponent</item>
                                           <item name="children" xsi:type="array">
                                               <item name="payment" xsi:type="array">
                                                   <item name="children" xsi:type="array">
                                                       <item name="renders" xsi:type="array">
                                                           <!-- merge payment method renders here -->
                                                           <item name="children" xsi:type="array">
                                                               <item name="payment" xsi:type="array">
                                                                   <item name="component" xsi:type="string">SyncIt_Payment/js/view/payment/payment</item>
                                                                   <item name="methods" xsi:type="array">
                                                                       <item name="payment" xsi:type="array">
                                                                           <item name="isBillingAddressRequired" xsi:type="boolean">true</item>
                                                                       </item>
                                                                   </item>
                                                               </item>
                                                           </item>
                                                       </item>
                                                   </item>
                                               </item>
                                           </item>
                                       </item>
                                   </item>
                               </item>
                           </item>
                       </item>
                   </item>
               </argument>
           </arguments>
       </referenceBlock>
   </body>
</page>

Conclusion

In case you want to build a loyal customer base, add a Custom Payment Method to your eCommerce store. As a result, sales and conversion rates are sure to blow the roof off.

Of course, we will gladly create a custom M2 solution just for you. In case you need one, feel free to contact us at [email protected]

4.5 2 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments