Magento 2: Creating a Custom Payment Method

Whenever the online shopping is considered, payment methods prove to be the most vital factor.

Posted on July 1, 2017 in Magento2

Stripe Payment Gateway — new Payment Method

When it comes to Magento 2, being the most popular Ecommerce platform, it provides many default payment methods. However sometimes you may need to create a custom payment method in Magento 2 store to integrate with your choice of payment gateway if it’s not already available.

In this tutorial, I will create a new payment method with stripe payment gateway step by step

Create a new Module Jeff_Stripe

module.xml file


#app/code/Jeff/Stripe/etc/module.xml

<?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="Jeff_Stripe" setup_version="0.0.1">
        <sequence>
            <module name="Magento_Payment" />
            <module name="Magento_Checkout" />
            <module name="Magento_Sales" />
            <module name="Magento_Quote" />
        </sequence>
    </module>
</config>

registration.php file


#app/code/Jeff/Stripe/registraion.php

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

composer.json file


{
    "name": "jeff/module-stripe",
    "description" : "Jeff Stripe Payment Gateway",
    "require": {
        "php" : "~5.5.0|~5.6.0|~7.0.0"
    },
    "type": "magento2-module",
    "version" : "0.0.1",
    "license" : [
        "OSL-3.0",
        "AFL-3.0"
    ],
    "autoload" : {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "Jeff\\Stripe\\": ""
        }
    }
}

Backend configuration and default values

config.xml file for default values of some settings


#app/code/Jeff/Stripe/etc/config.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../Core/etc/config.xsd">
    <default>
        <payment>
            <stripe>
                <active>1</active>
                <model>Jeff\Stripe\Model\Payment</model>
                <payment_action>autorize_capture</payment_action>
                <title>Jeff Stripe Payment Gateway</title>
                <api_key backend_model=&#8221;Magento\Config\Model\Config\Backend\Encrypted&#8221; />
                <cctypes>AE,VI,MC,DI,JCB</cctypes>
                <allowspecific>0</allowspecific>
                <min_order_total>1</min_order_total>
            </stripe>
        </payment>
    </default>
</config>

system.xml file


#app/code/Jeff/Stripe/etc/adminhtml/system.xml

<?xml version="1.0"?>
<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="stripe" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Jeff Stripe 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="api_key" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>API Secret Key</label>
                    <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
                    <comment>Test/Live Secret Key</comment>
                </field>
                <field id="cctypes" translate="label" type="multiselect" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Credit Card Types</label>
                    <source_model>Jeff\Stripe\Model\Source\Cctype</source_model>
                </field>
                <field id="allowspecific" translate="label" type="allowspecific" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Payment from Application Countries</label>
                    <source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model>
                </field>
                <field id="specificcountry" translate="label" type="multiselect" sortOrder="60" 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="min_order_total" translate="label" type="text" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Minimum Order Total</label>
                    <comment>$1 is the minimum amount allowed by Stripe Payment</comment>
                </field>
                <field id="max_order_total" translate="label" type="text" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Minimum Order Total</label>
                    <comment>If customer tries to checkout with basket value greater than the maximum allowed they will be prevented from completing the order.</comment>
                </field>
                <field id="sort_order" translate="label" type="text" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Sort Order</label>
                </field>
            </group>
        </section>
    </system>
</config>

di.xml file to modify the arguments for the constructor of class Magento\Payment\Model\CcGenericConfigProvider


#app/code/Jeff/Stripe/etc/di.xml

<?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="stripe" xsi:type="const">Jeff\Stripe\Model\Payment::METHOD_CODE</item>
            </argument>
        </arguments>
    </type>
</config>

creating source class Jeff\Stripe\Model\Source\Cctype


#app/code/Jeff/Stripe/Model/Source/Cctype.php

<?php
namespace Jeff\Stripe\Model\Source;

class Cctype extends \Magento\Payment\Model\Source\Cctype {
    public function getAllowedTypes() {
        return array('VI', 'MC', 'AE', 'DI', 'JCB', 'OT');
    }
}

At this point, we finished the backend creation. After clearing cache and reload, you will find the new Payment method at backend as following image

payment 1

The Model adapter for the frontend payment method

Stripe Payment class


#app/code/Jeff/Stripe/Model/Payment.php

<?php
namespace Jeff\Stripe\Model;

class Payment extends \Magento\Payment\Model\Method\Cc
{
    const METHOD_CODE                       = 'stripe';
 
    protected $_code                     = self::METHOD_CODE;
 
    protected $_stripe;
 
    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,
        \Stripe\Stripe $stripe,
        array $data = array()
    ) {
        parent::__construct(
            $context,
            $registry,
            $extensionFactory,
            $customAttributeFactory,
            $paymentData,
            $scopeConfig,
            $logger,
            $moduleList,
            $localeDate,
            null,
            null,
            $data
        );
 
        $this->_code = 'stripe';
        $this->_stripe = $stripe;
        $this->_stripe->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 = \Stripe\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 {
            \Stripe\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);
    }
}

Knockout view model file: stripepayments.js


#app/code/Jeff/Stripe/view/frontend/web/js/view/payment/stripepayments.js

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

Stripe knockout model: stripemethod.js


#app/code/Jeff/Stripe/view/frontend/web/js/view/payment/method-renderer/stripemethod.js

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: 'Jeff_Stripe/payment/stripe1'
            },

            getCode: function() {
                return 'stripe';
            },

            isActive: function() {
                return true;
            },

            validate: function() {
                var $form = $('#' + this.getCode() + '-form');
                return $form.validation() && $form.validation('isValid');
            }
        });
    }
);

The view file for the knockout view model


#app/code/Jeff/Stripe/view/frontend/web/template/payment/stripe1.html

<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>

layout update file for checkout_index_index handle


# app/code/Jeff/Stripe/view/frontend/layout/checkout_index_index

<?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="stripe-payments" xsi:type="array">
                                                                    <item name="component" xsi:type="string">Jeff_Stripe/js/view/payment/stripepayments</item>
                                                                    <item name="methods" xsi:type="array">
                                                                        <item name="stripe" 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>

Install Stripe-php module with composer and enable our new payment method with following CLI commands



composer require stripe/stripe-php
php bin/magento setup:upgrade
php bin/magento setup:static-content:deploy
php bin/magento cache:clean


Following picture demonstrates our new payment method is working

payment 2

I hope my tutorial will help you create your own payment method easily

You could download the source code from GitHub.


comments powered by Disqus