HowTo: Create a custom Payment Method in Magento 2


Recently I was tasked with creating a custom payment method for a project. While doing some research I discovered that there are two ways to create a payment method in Magento 2.

  1. The legacy way, similar to how it used to be in M1, extending AbstractMethod.
  2. The new way, using the Payment Provider Gateway .

The gateway is the recommended approach for anything that talks to a real processor. But for a simple offline-style method the lightweight way is to configure a payment Adapter through di.xml, no custom model needed. That is the one I will show here.

This post assumes that there is already a custom module created in app/code/Vendor/Payment. If you need to know how to create a custom module, look at How to create a custom module for Magento 2.

The configuration

First the default values of our method in config.xml.

xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Payment/etc/config.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <payment>
            <vendor_custom>
                <active>1</active>
                <model>VendorCustomPaymentFacade</model>
                <title>My Custom Payment</title>
                <order_status>pending</order_status>
                <payment_action>authorize</payment_action>
            </vendor_custom>
        </payment>
    </default>
</config>

Then the admin fields in system.xml, so the merchant can enable and rename it from Stores > Configuration > Payment Methods.

xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Payment/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="vendor_custom" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>My Custom Payment</label>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
            </group>
        </section>
    </system>
</config>

The method (no custom model)

Instead of writing a model, we declare a virtualType based on the payment Adapter. This is enough for a simple method.

xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Payment/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <virtualType name="VendorCustomPaymentFacade" type="Magento\Payment\Model\Method\Adapter">
        <arguments>
            <argument name="code" xsi:type="const">Vendor\Payment\Model\Ui\ConfigProvider::CODE</argument>
            <argument name="formBlockType">Magento\Payment\Block\Form</argument>
            <argument name="infoBlockType">Magento\Payment\Block\Info</argument>
            <argument name="valueHandlerPool" xsi:type="object">VendorCustomValueHandlerPool</argument>
        </arguments>
    </virtualType>
</config>

The frontend

Finally, register the checkout renderer through checkout_index_index.xml and a small js component that points to the default renderer. I keep the template as a copy of Magento_Payment::payment/default.phtml unless I need something special.

xml
<?xml version="1.0"?>
<!-- app/code/Vendor/Payment/view/frontend/layout/checkout_index_index.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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="children" xsi:type="array">
                                                <item name="payment" xsi:type="array">
                                                    <item name="children" xsi:type="array">
                                                        <item name="renders" xsi:type="array">
                                                            <item name="children" xsi:type="array">
                                                                <item name="vendor_custom" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Vendor_Payment/js/view/payment/vendor-custom</item>
                                                                </item>
                                                            </item>
                                                        </item>
                                                    </item>
                                                </item>
                                            </item>
                                        </item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

After a setup:upgrade and a cache flush, the method shows up both in the admin configuration and on the checkout.

Hope this helps.

If you have any question or know a better way to achieve this, feel free to share in the comments below.