How to Add a Payment Method to Magento 2

May 27, 2021

Introduction

Customers want to use preferred payment methods they recognize and trust. To meet customer expectations, ecommerce stores need to offer as many payment options as possible.

Magento 2.4 enables store owners to accept payments using built-in PayPal/Braintree payment solutions. You can also add new payment options by implementing third-party extensions from the Magento Marketplace.

However, adding a custom payment method allows merchants to:

  • Integrate Magento 2 with a preferred ecommerce payment processor.
  • Customize the Magento checkout process to fit a particular business model.
  • Offer customers region-specific payment options.

In this article, learn how to add a custom payment method to Magento 2 and maximize your store’s conversion rates.

Adding a different payment methods to pay in a Magento store.

Add a Custom Payment Method in Magento 2

To add a new payment option in Magento 2, you need to build a custom payment module, render the payment method during checkout, and configure the module to processes payment data in conjunction with Magento’s systems.

Magento is a flexible, open-source platform, and it is not necessary to modify the native Magento code. Create the custom payment method as a separate module dependent on the existing Magento checkout module.


Note: Choosing the right payment gateway that delivers a smooth payment flow when building a Magento website is important for the growth and development of your ecommerce business.


Step 1: Register Payment Module in Magento

Create a registration.php file in the payment module’s root directory. In this example, the file path is app/code/CCBill/CustomPaymentOption/registration.php.

Add the following code to register the module (component) in Magento’s system:

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

Edit the vendor (CCBill) and module name (CustomPaymentOption) to match your module’s name.


Note: For easy integration with Magento, CCBill maintains a GitHub repository for its public-facing API. For any additional information, reach out to CCBill Support.


Step 2: Declare Payment Module Name

Access the custom module’s app/code/CCBill/CustomPaymentOption/etc/ directory and create a module.xml file. Define the module name within the module.xml file:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="CCBill_ CustomPaymentOption" setup_version="2.2.0">
<sequence>
            <module name="Magento_Sales"/>
            <module name="Magento_Payment"/>
            <module name="Magento_Checkout"/>
</sequence>
    </module>
</config>

The <strong><sequence></strong> tag controls the load order and ensures that the system loads the listed components before the custom payment module.

Step 3: Add Payment Method Configuration Settings

Configure and display the payment method in the Magento Admin by using a system.xml file. Create the system.xml file in the app/code/CCBill/CustomPaymentOption/etc/adminhtml/ directory and add the following code:

<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="ccbill_custompaymentoption" translate="label comment" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Custom Payment Option</label>
                <field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
                <field id="cctypes" translate="label" type="multiselect" sortOrder="75" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Credit Card Types</label>
                    <source_model>Magento\Payment\Model\Source\Cctype</source_model>
                </field>
            </group>
        </section>
    </system>
</config>

Activate the module using the <strong>Enabled</strong> label, display its <strong>Title</strong>, and add a preferred payment option, such as <strong>Credit Card Types</strong>.

Step 4: Add Default System Configuration Values

Define default values for the system.xml file by creating a config.xml file. Create the config.xml file in the app/code/CCBill/CustomPaymentOption/etc/ directory and add the following code:

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <payment>
            <ccbill_custompaymentoption>
  <title>Custom Payment Option</title>
                <active>1</active>
  <payment_action>preferred_method</payment_action>
                <model>CCBill\CustomPaymentOption\Model\PaymenMethod</model>
                <order_status>pending_payment</order_status>
            </ccbill_custompaymentoption>
        </payment>
    </default>
</config>

Use the <strong>payment_action</strong> tag to set preferred payment methods as the default value.

Step 5: Create Payment Method Model

Access the app/code/CCBill/CustomPaymentOption/Model/ directory and create a custompaymentoption.php file. Copy the following code to establish a payment method model class in the .php file:

<?php
namespace CCBill\CustomPaymentOption\Model;

class CustomPaymentOption extends \Magento\Payment\Model\Method\Cc 
{
    const METHOD_CODE        = 'ccbill_custompaymentoption';
 
    protected $_code               = self::METHOD_CODE;
 
    protected $_custompayments;
 
    protected $_isGateway                   = true;
    protected $_canCapture                  = true;
    protected $_canCapturePartial           = true;
    protected $_canRefund                   = true;
    protected $_minOrderTotal = 0;
    protected $_supportedCurrencyCodes = array('USD','GBP','EUR');
 
    public function __construct(
        \Magento\Framework\Model\Context $context,
        \Magento\Framework\Registry $registry,
        \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory,
        \Magento\Framework\Api\AttributeValueFactory $customAttributeFactory,
        \Magento\Payment\Helper\Data $paymentData,
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Payment\Model\Method\Logger $logger,
        \Magento\Framework\Module\ModuleListInterface $moduleList,
        \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
        \Extension\Extension $Extension,
        array $data = array()
    ) {
        parent::__construct(
            $context,
            $registry,
            $extensionFactory,
            $customAttributeFactory,
            $paymentData,
            $scopeConfig,
            $logger,
            $moduleList,
            $localeDate,
            null,
            null,
            $data
        );
 
        $this->_code = 'Extension';
        $this->_Extension = $Extension;
        $this->_Extension->setApiKey($this->getConfigData('api_key'));
 
        $this->_minOrderTotal = $this->getConfigData('min_order_total');
 
    }
 
    public function canUseForCurrency($currencyCode)
    {
        if (!in_array($currencyCode, $this->_supportedCurrencyCodes)) {
            return false;
        }
        return true;
    }
 
    public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount)
 {
        $order = $payment->getOrder();
        $billing = $order->getBillingAddress();
        try{
            $charge = \Extension\Charge::create(array(
                'amount' => $amount*100,
                'currency' => strtolower($order->getBaseCurrencyCode()),
                'card'      => array(
                    'number' => $payment->getCcNumber(),
                    'exp_month' => sprintf('%02d',$payment->getCcExpMonth()),
                    'exp_year' => $payment->getCcExpYear(),
                    'cvc' => $payment->getCcCid(),
                    'name' => $billing->getName(),
                    'address_line1' => $billing->getStreet(1),
                    'address_line2' => $billing->getStreet(2),
                    'address_zip' => $billing->getPostcode(),
                    'address_state' => $billing->getRegion(),
                    'address_country' => $billing->getCountry(),
                ),
                'description' => sprintf('#%s, %s', $order->getIncrementId(), $order->getCustomerEmail())
            ));
           $payment->setTransactionId($charge->id)->setIsTransactionClosed(0);
 
            return $this;
 
        }catch (\Exception $e){
            $this->debugData(['exception' => $e->getMessage()]);
            throw new \Magento\Framework\Validator\Exception(__('Payment capturing error.'));
        }
    }
 
    public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount)
    {
        $transactionId = $payment->getParentTransactionId();
 
        try {
            \Extension\Charge::retrieve($transactionId)->refund();
        } catch (\Exception $e) {
            $this->debugData(['exception' => $e->getMessage()]);
            throw new \Magento\Framework\Validator\Exception(__('Payment refunding error.'));
        }
 
        $payment
            ->setTransactionId($transactionId . '-' . \Magento\Sales\Model\Order\Payment\Transaction::TYPE_REFUND)
            ->setParentTransactionId($transactionId)
            ->setIsTransactionClosed(1)
            ->setShouldCloseParentTransaction(1);
 
        return $this;
    }
 
    public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null){
        $this->_minOrderTotal = $this->getConfigData('min_order_total');
        if($quote && $quote->getBaseGrandTotal() < $this->_minOrderTotal) {
            return false;
        }
        return $this->getConfigData('api_key', ($quote ? $quote->getStoreId() : null))
        && parent::isAvailable($quote);
    }
}

To check if the new payment method is available, access the Magento Admin and go to Stores>Configuration.

Access Configuration menu in Magento

Expand the Sales drop down and open the Payment Methods menu.

Custom Payment Method in Magetno Admin.

Confirm that the settings correspond with the values defined in the config.xml and system.xml files.

Step 6: Configure Dependency Injection

To inject the custom payment method into the existing Magento <strong>CcGenericConfigProvider </strong>create a new <em>di.xml</em> file<em>.</em> Place the di.xml file within the app/code/CCBill/CustomPaymentOption/etc/frontend/ directory and add the payment method code as an argument:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Payment\Model\CcGenericConfigProvider">
        <arguments>
            <argument name="methodCodes" xsi:type="array">
                <item name="CustomPaymentOption" xsi:type="const">CCBill\CustomPaymentOption\Model\Payment::METHOD_CODE</item>
            </argument>
        </arguments>
    </type>
</config>

After configuring the payment method module, the system needs to render the user interface in the store’s frontend.

Step 7: Add Payment Method Renderer

Create a custompayments.js file within the app/code/CCBill/CustomPaymentOption/view/frontend/web/js/view/payment/ directory. Add the following code:

define(
    [
        'uiComponent',
        'Magento_Checkout/js/model/payment/renderer-list'
    ],
    function (
        Component,
        rendererList
    ) {
        'use strict';
        rendererList.push(
            {
                type: 'ccbill_custompaymentopyion',
                component: 'CCBill_CustomPaymentOption/js/view/payment/method-renderer/custompayments'
            }
        );

The primary function of the .js file in this example is to add a payment method renderer.  

Step 8: Declare Payment Method Renderer

Create a .js file in the app/code/CCBill/CustomPaymentOption/view/frontend/web/js/view/payment/method-renderer/ directory. Use the following code to control the frontend logic of the new payment method:

define(
    [
        'Magento_Payment/js/view/payment/cc-form',
        'jquery',
        'Magento_Checkout/js/action/place-order',
        'Magento_Checkout/js/model/full-screen-loader',
        'Magento_Checkout/js/model/payment/additional-validators',
        'Magento_Payment/js/model/credit-card-validation/validator'
    ],
    function (Component, $) {
        'use strict';
 
        return Component.extend({
            defaults: {
                template: 'CCBill_CustomPaymentOption/payment/custompayments'
            },
 
            getCode: function() {
                return 'ccbill_custompaymentoption';
            },
 
            isActive: function() {
                return true;
            },
 
            validate: function() {
                var $form = $('#' + this.getCode() + '-form');
                return $form.validation() && $form.validation('isValid');
            }
        });
    }
);

If the custom module has several payment methods, register the renderers in the same .js file.

Step 9: Create Payment Method Template

The system renders the UI Component using the Knockout JavaScript framework. Build a template that contains a title, billing address, and radio button for the payment method.

Create the .html file in the app/code/CCBill/CustomPaymentOption/view/frontend/web/js/view/payment/ directory.

<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 data-bind="attr: {'for': getCode()}" class="label"><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>
 
        <form class="form" data-bind="attr: {'id': getCode() + '-form'}">
            <!-- ko template: 'Magento_Payment/payment/cc-form' --><!-- /ko -->
        </form>
 
        <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: placeOrder,
                        attr: {title: $t('Place Order')},
                        css: {disabled: !isPlaceOrderActionAllowed()},
                        enable: (getCode() == isChecked())
                        "
                        disabled>
                    <span data-bind="text: $t('Place Order')"></span>
                </button>
            </div>
        </div>
    </div>
</div

For convenience, use templates from existing Magento payment modules. The .html templates are located in the <random_module >/frontend/web/template/payment/ payment module directories.

Step 10: Show Payment Method in Checkout

To display the payment method on the checkout page, create the checkout_index_index.xml file within the app/code/CCBill/CustomPaymentOption/view/frontend/layout/ directory.

<?xml version="1.0"?>
<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="paymentoptions-payments" xsi:type="array">
                      <item name="component" xsi:type="string">CCBill_CustomPaymentOption/js/view/payment/paymentoptions</item>
                       <item name="methods" xsi:type="array">
                         <item name="CustomPaymentOptions" 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>

If Magento is in production mode, deploy static content and flush the Magento cache before testing the functionality of the new payment method.

To flush Magento cache, use:

php bin/magento cache:flush

Conclusion

You have successfully added a custom payment method to your Magento 2 store. Use the same approach to implement new payment options and enable customers to pay using their preferred method.

The number of devices and methods customers use to make online payments is increasing rapidly. It is challenging to maintain a satisfying customer experience with a traditional web-based model.

To help merchants reach their customers more effectively, platforms like Magento are introducing headless commerce and API-driven features.

About the author
Vladimir Kaplarevic
Vladimir is a resident Tech Writer at CCBill. He has more than 8 years of experience in implementing e-commerce and online payment solutions with various global IT services providers. His engaging writing style provides practical advice and aims to spark curiosity for innovative technologies.
Talk to a Merchant Support Specialist