Payment Addons
Payment addons allow you to capture payment from your customers as payment for their orders. They are usually implementations of the API of third-party payment gateways such as PayPal, Stripe, Authorize.Net, etc.
Padloper ships with 3 payment addons, i.e. Invoice, PayPal and Stripe.
Develop Payment Addon
Developing a custom payment addon is not difficult. A payment addon must extend the Padloper Class PadloperPayment
. In addition, similar to all addons, it must implement the Interface
PadloperAddons
. The minimum code structure for a payment addon is outlined below.
Payment Addon Code
In the examples below, we will assume a fictitious payment addon called MyCoolPaymentAddon
.
As stated in the addons documentation, all addons must adhere to pre-defined file structure and file naming rules. Hence, your MyCoolPaymentAddon
will be located at /site/templates/padloper/addons/MyCoolPaymentAddon/*.*
.
As a bare minimum, you will need the file MyCoolPaymentAddon.php
. This file must be a PHP Class
. For example:
namespace ProcessWire;
// load PadloperPayment class if not yet loaded by $padloper
$padloperPaymentClassPath = $this->wire('config')->paths->siteModules . "Padloper/includes/payment/PadloperPayment/PadloperPayment.php";
require_once $padloperPaymentClassPath;
// use/require any other third-party libraries your payment class might need
/**
*
* MyCoolPaymentClass for Padloper.
*
*
*/
class MyCoolPaymentClass extends PadloperPayment implements PadloperAddons {
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Interface PadloperAddons
All payment addons need to implement the following public methods
defined by the Interface Class
PadloperAddons
.
interface PadloperAddons {}
2
The file for this class can be found at /site/modules/Padloper/includes/addons/PadloperAddons.php
.
getType()
This method returns the addon type. Payment addons must return the value payment
for this method.
/**
* Returns addon type.
*
* Must be one of Padloper pre-defined types.
*
* @access public
* @return string $type
*/
public function getType();
2
3
4
5
6
7
8
9
getClassName()
This method returns the name of the Class of the addon. In this example this method would return MyCoolPaymentClass
.
/**
* Returns addon class name.
*
* @access public
* @return string $className.
*/
public function getClassName();
2
3
4
5
6
7
getTitle()
This method returns the title of the addon. This will be displayed in the addons dashboard to identify the addon. In this example this method could return My Cool Payment
.
/**
* Returns a user-friendly title of this addon.
*
* @access public
* @return string $title.
*/
public function getTitle();
2
3
4
5
6
7
getDescription()
This method returns a brief description about the addon. This will be displayed in the addons dashboard to describe the addon. In this example this method could return:
My Cool Payment allows you to easily collect payment from your customers in over 20 major world currencies.
/**
* Returns short description of this addon.
*
* @access public
* @return string $description.
*/
public function getDescription();
2
3
4
5
6
7
Abstract Class PadloperPayment
All payment addons need to implement the following abstract methods
defined by the Abstract Class
PadloperPayment
.
abstract class PadloperPayment extends WireData implements Module{}
2
The file for this class can be found at /site/modules/Padloper/includes/payment/PadloperPayment/PadloperPayment.php
.
WARNING
It might be necessary to require
the PadloperPayment.php
file on top of your addon payment class as shown in the example class above.
render()
This method renders the markup for the payment addon. This could be a button, a form with credit card inputs, etc.
/**
* Render frontend markup for the payment.
*
* @access protected
* @return string
*/
abstract protected function render();
2
3
4
5
6
7
Padloper will call this method during checkout confirmation, i.e. /checkout/confirmation/
. It will call $yourPaymentClass->render()
. In our example, this would be similar to:
$out = $myCoolPaymentClass->render();
2
The above would be returned as the output of /checkout/confirmation/
.
createOrder()
The logic in this method varies depending on the payment provider whose API you are implementing. The method receives a WireData
object $createOrderValues
as the first argument. This contains two properties: $createOrderValues->amount
which contains the order amount (whole currency) and $createOrderValues->referenceID
which is the ID of the order.
For ajax responses, Padloper expects an object as a response with a property result
containing the response, i.e. $results = $response->result;
. This value will be processed via json_encode()
and sent back to the browser. Hence, it must be a type that json_encode
can accept.
/**
* Create an order using payment.
*
* Varies depending on payment gateway.
* Can be used to create payment intent for gateways such as Stripe or PayPal.
* Can be used to create order, authorize and capture for gateways such as Authorize.Net.
*
* @param WireData $createOrderValues
* @param boolean $debug
* @return void
*/
abstract protected function createOrder(WireData $createOrderValues, $debug = false);
2
3
4
5
6
7
8
9
10
11
12
Below are examples of createOrder()
for three payment providers.
Stripe
This method can be used to create a payment intent. For instance, Stripe Payment Intents requires that a payment intent be created immediately a customer has indicated they intend to make a purchase. This is the point at which the customer lands on the payment page. This means that for Stripe implementations, this method would have to be called before its render()
method is used to show the Stripe Elements form. This allows for the passing of the payment intent's client secret to the form returned by render().
PayPal
In the case of PayPal, it sends an ajax request to the endpoint Padloper expects for ajax create order requests, i.e. /payment/create/
. The implementation of the PayPal API will send a JavaScript fetch()
request to above endpoint during its order create stage, i.e.
createOrder: function (data, actions) {
return fetch("/payment/create/", {
method: "post",
headers: {
"X-Requested-With": "XMLHttpRequest",
},
})
.then(function (res) {
return res.json()
})
.then(function (orderData) {
return orderData.id
})
},
2
3
4
5
6
7
8
9
10
11
12
13
14
This is the checkout stage at which a customer has clicked on the PayPal pay button and logged into their PayPal account to approve the payment.
Authorize.Net
With Authorize.Net, we authorise and capture at the same time. Hence, createOrder()
is called in its captureOrder()
method instead!
captureOrder()
Similar to createOrder()
, the logic in this method varies depending on the payment provider whose API you are implementing. The method receives the $orderId
as the first argument.
/**
* Capture an order using payment.
*
* Varies depending on payment gateway.
* Can be used to retrieve payment intent for gateways such as Stripe.
* Can be used to execute payment for gateways such as PayPal.
* Can be used to create order, authorize and capture for gateways such as Authorize.Net.
*
* @param int $orderId
* @param boolean $debug
* @return void
*/
abstract protected function captureOrder($orderId, $debug = false);
2
3
4
5
6
7
8
9
10
11
12
13
For ajax responses, Padloper expects an object as a response with a property result
containing the response, i.e. $results = $response->result;
. The value of this is processed using json_encode()
and sent back to the browswer as the ajax response. Hence, it must be a type that json_encode
can accept.
Please note the whole response object from captureOrder()
is passed to your Class isSuccessfulPaymentCapture()
to determine if the capture was successful.
Below are examples of captureOrder()
for three payment providers.
Stripe
In Stripe we use this method to retrieve and return the existing payment intent. At this stage the payment (if the transaction was OK) should have been taken.
PayPal
In the case of PayPal, it sends an ajax request to the endpoint Padloper expects for ajax capture order requests, i.e. /payment/capture/
. The implementation of the PayPal API will send a JavaScript fetch()
request to this endpoint during its order approve stage, i.e.
onApprove: function (data, actions) {
return fetch("/payment/capture/?orderID=" + data.orderID, {
method: "post",
headers: {
"X-Requested-With": "XMLHttpRequest",
},
})
.then(function (res) {
return res.json()
})
.then(function (orderData) {
const errorDetail =
Array.isArray(orderData.details) && orderData.details[0]
if (errorDetail && errorDetail.issue === "INSTRUMENT_DECLINED") {
// handle declined errors
return actions.restart() // Recoverable state
}
if (errorDetail) {
// handle processing errors
}
// REDIRECT TO SUCCESS PAGE:
// /checkout/success/
actions.redirect(orderData.redirectURL)
})
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Authorize.Net
As stated earlier, with Authorize.Net, we authorise and capture at the same time. Hence, in captureOrder
we just call createOrder()
.
isSuccessfulPaymentCapture()
This method receives the $response
from captureOrder()
to determine if the payment was successful. $response
is an object.
This method should return a bool
indicating whether payment was captured successfully.
/**
* Confirm payment capture was successfull.
*
* Ideally check various aspects of payment to confirm.
* For instance amount, currency, order ID, etc.
*
* @param object $response
* @param array $options
* @return boolean
*/
abstract protected function isSuccessfulPaymentCapture($response, $options = []);
2
3
4
5
6
7
8
9
10
11
You can implement various checks on the response this method receives from Padloper. For instance, you can be as rigorous as you wish, checking everything from the payment provider's status for the request, to the order id, expected currency and amount, etc.
getFieldsSchema()
This method should return a nested array whereby each inner array is an associative array
that defines a single field that will be mapped to an input created on the fly in the GUI when editing the payment configurations.
/**
* Returns fields schema for building GUI for editing fields/inputs for the payment gateway.
*
* @see documentation.
* @return array $schema.
*/
abstract protected function getFieldsSchema();
2
3
4
5
6
7
All payment providers such as PayPal, Stripe, etc require some form of authentication to transact with their APIs. These include details of public and secret keys, details of live/production versus test/sandbox credentials, etc. The inputs created from the array returned by getFieldsSchema()
allow you to edit and store this information in your server.
Currently, supported fields/inputs are text
, number
, email
, textarea
, radio
and checkbox
.
At minimum, each field must have the keys name
, type
and label
in its schema. The purpose of each is outlined below:
name
This will be used as both the name of the HTML input
for the field and the key to retrieve the saved value for the associated configuration.
type
The value of this key must be one of the supported fields above, i.e. text
, checkbox
, etc.
label
The value of this key will be used as the label for the HTML input
generated for the associated field, i.e. the label for the email
, radio
, etc, fields.
Below is an example getFieldsSchema()
that returns a field for each of the allowed fields above. Note the other extra keys
in use, i.e. notes
, description
, collapsed
and radio_options
. These will be matched to their corresponding options in the ProcessWire Inputfield
that will be used to build the GUI.
protected function getFieldsSchema() {
$schema = [
[
'name' => 'sandbox_merchant_login_id',
'type' => 'text',
'label' => $this->_('Sandbox Merchant Login ID'),
'description' => $this->_('Specify the sandbox/testing merchant login ID.'),
'notes' => $this->_('This is the testing merchant login ID. This value is found in your app in your Merchant Interface.'),
],
[
'name' => 'sandbox_merchant_transaction_key',
'type' => 'text',
'label' => $this->_('Sandbox Merchant Transaction Key'),
'description' => $this->_('Specify the sandbox/testing merchant transaction key.'),
'notes' => $this->_('This is the testing merchant transaction key. This value is found in your app in your Merchant Interface.'),
'collapsed' => true,
],
[
'name' => 'is_live',
'type' => 'checkbox',
'label' => $this->_('Use Live Keys'),
'description' => $this->_('Specify if this shop is in live/production versus sandbox/testing mode.'),
'notes' => $this->_('Live mode should be used for real purchases for a shop in production. For testing purposes, please make sure that you uncheck this box.'),
],
[
'name' => 'live_merchant_login_id',
'type' => 'text',
'label' => $this->_('Live Merchant Login ID'),
'description' => $this->_('Specify the live/production merchant login ID for purchases.'),
'notes' => $this->_('This is the live merchant login ID. This value is found in your app in your developer dashboard.'),
'collapsed' => true,
],
[
'name' => 'live_merchant_transaction_key',
'type' => 'text',
'label' => $this->_('Live Merchant Transaction Key'),
'description' => $this->_('Specify the live/production merchant transaction key for purchases.'),
'notes' => $this->_('This is the live client merchant transaction key. This value is found in your app in your Merchant Interface.'),
'collapsed' => true,
],
[
'name' => 'short_order_details_for_merchant',
'type' => 'radio',
'label' => $this->_('Order Details Length'),
'description' => $this->_('Specify whether to save short or full order details with the merchant.'),
'notes' => $this->_('Full order details are useful if you will be carrying out order management using your Merchant Interface.'),
'radio_options' => [
1 => __('Yes'),
0 => __('No'),
],
],
];
return $schema;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
The above schema would produce a GUI like this:
Other Considerations
If you payment addon needs to carry out some post-payment tasks, post those to /checkout/post-process/
. This tells Padloper to call your captureOrder()
method. If payment is already taken, you can then retrieve the payment details from your payment provider and take actions accordingly. For instance, you could use this approach to process redirect URL, webhooks, etc.
The __construct()
method of your payment addon class will receive its saved configurations as an array as the first argument.
public function __construct(array $MyCoolPaymentAddonConfigs, array $options = []) {}
The configurations array will contain, for instance client keys, secret keys, etc. These configurations are the ones saved via the inputs created on the fly from getFieldsSchema()
. The array values have array keys that match those in the payment class getFieldsSchema()
.
WARNING
Your payment addon's __construct()
method should not define other required parameters since it is not possible for Padloper to know beforehand what those required parameters should be.