For this example we will integrate Flutterwave payment processor into LatePoint. First of all you have to download our latepoint-addon-starter plugin from GitHub. Unzip it and rename the folder to latepoint-payments-flutterwave and replace all instances of -addon-starter with –payments-flutterwave. Open the main file, which is now called latepoint-payments-flutterwave.php and renamed all variables and strings from addon-starter to payments-flutterwave
!IMPORTANT: don’t forget to rename the main add-on object $LATEPOINT_ADDON_ADDON_STARTER, at the very bottom of the main add-on plugin file, which you just renamed to latepoint-payments-flutterwave.php:
Implementing Payments
In this tutorial we will be implementing “inline callback” type of Flutterwave payment processing. Basically what it does is the following: when your customer tries to pay for their booking, it opens a flutterwave payment modal using javascript, which on success or error will call our own javascript function to handle a response.
Flutterwave integration requires us to have public and secret API keys to launch their payment module. Business owner will need to generate it in Flutterwave settings→api page. We need a way to capture these API keys in LatePoint settings. Let’s register flutterwave as a new payment method with fields for capturing API keys in Latepoint Settings → Payments page.
Set a unique code name for your processor:
<?php
class LatePointPaymentsFlutterwave {
public $version = '1.0.0';
public $db_version = '1.0.0';
public $addon_name = 'latepoint-payments-flutterwave';
public $processor_code = 'flutterwave';
...
// latepoint-payments-flutterwave.php
To register this processor and it’s payment methods in LatePoint, we need to hook into payment filters. You should call your hooks from the init_hooks() method of a starter add-on, so it looks like this:
<?php
class LatePointPaymentsFlutterwave {
...
public function init_hooks(){
...
// Register flutterwave as a payment processor
add_filter('latepoint_payment_processors', [$this, 'register_payment_processor'], 10, 2);
// Register flutterwave available payment methods
add_filter('latepoint_all_payment_methods', [$this, 'register_payment_methods']);
// Add payment methods to a list of enabled methods for the front-end, if processor is turned on in settings
add_filter('latepoint_enabled_payment_methods', [$this, 'register_enabled_payment_methods']);
...
}
...
}
// latepoint-payments-flutterwave.php
Now let’s define functions for these hooks. First of all let’s create a function that will add payment methods for this processor:
<?php
class LatePointPaymentsFlutterwave {
...
// Payment method for the processor
public function get_supported_payment_methods(){
return ['inline_checkout' => [
'name' => __('Inline Checkout', 'latepoint-payments-flutterwave'),
'label' => __('Inline Checkout', 'latepoint-payments-flutterwave'),
'image_url' => LATEPOINT_IMAGES_URL.'payment_cards.png',
'code' => 'inline_checkout',
'time_type' => 'now'
]
];
}
...
}
// latepoint-payments-flutterwave.php
This function will register inline_checkout payment method, because that’s what we are implementing in this tutorial. Setting time_type attribute to ‘now’ means that the payment needs to be made at a time of booking, for example “Pay Later” payment method uses ‘later’.
Now let’s implement hooks processing functions that we defined above:
<?php
class LatePointPaymentsFlutterwave {
...
// register payment processor
public function register_payment_processor($payment_processors, $enabled_only){
$payment_processors[$this->processor_code] = ['code' => $this->processor_code,
'name' => __('Paystack', 'latepoint-payments-paystack'),
'image_url' => $this->images_url().'processor-logo.png'];
return $payment_processors;
}
// adds payment method to payment settings
public function register_payment_methods($payment_methods){
$payment_methods = array_merge($payment_methods, $this->get_supported_payment_methods());
return $payment_methods;
}
// enables payment methods if the processor is turned on
public function register_enabled_payment_methods($enabled_payment_methods){
// check if payment processor is enabled in settings
if(OsPaymentsHelper::is_payment_processor_enabled($this->processor_code)){
$enabled_payment_methods = array_merge($enabled_payment_methods, $this->get_supported_payment_methods());
}
return $enabled_payment_methods;
}
...
}
// latepoint-payments-flutterwave.php
When registering your processor, you should also set it’s logo, just upload an image called processor-logo.png to the YOUR_ADDON/public/images/ folder, and it will be used in a list of payment processors. Once you’ve done that and activate your newly created add-on you should see a new payment processor appear on your settings page:
Now let’s create a form, that will collect API keys and other settings from the business owner. We want to collect public key, secret key, country code, currency code and a logo. Secret key should be added to a list of encrypted values in LatePoint settings. We are going to use LatePoint’s form helpers to build fields which will capture user’s input. We have to use latepoint_payment_processor_settings action hook to add settings form in a payment processor box. But before we do that, let’s add country and currency list in our helper file payments_flutterwave_helper.php:
<?php
class OsPaymentsFlutterwaveHelper {
...
public static function load_countries_list(){
return ["GH" => "Ghana",
"KE" => "Kenya",
"ZA" => "South Africa"];
}
public static function load_currencies_list(){
return ["GHS" => "Ghanian Cedi",
"KES" => "Kenyan shilling",
"ZAR" => "South African rand"];
}
...
}
// /lib/helpers/payments_flutterwave_helper.php
Now let’s build setting form using these methods to generate list of options for country and currency select boxes:
<?php
class LatePointPaymentsFlutterwave {
...
public function init_hooks(){
...
// add settings fields for the payment processor
add_action('latepoint_payment_processor_settings',[$this, 'add_settings_fields'], 10);
...
}
public function add_settings_fields($processor_code){
if($processor_code != $this->processor_code) return false; ?>
<h3 class="os-sub-header"><?php _e('API Keys', 'latepoint-payments-flutterwave'); ?></h3>
<div class="os-row">
<div class="os-col-6">
<?php echo OsFormHelper::text_field('settings[flutterwave_publishable_key]', __('Public Key', 'latepoint-payments-flutterwave'), OsSettingsHelper::get_settings_value('flutterwave_publishable_key')); ?>
</div>
<div class="os-col-6">
<?php echo OsFormHelper::password_field('settings[flutterwave_secret_key]', __('Secret Key', 'latepoint-payments-flutterwave'), OsSettingsHelper::get_settings_value('flutterwave_secret_key')); ?>
</div>
</div>
<h3 class="os-sub-header"><?php _e('Other Settings', 'latepoint-payments-flutterwave'); ?></h3>
<div class="os-row">
<div class="os-col-6">
<?php echo OsFormHelper::select_field('settings[flutterwave_country_code]', __('Country', 'latepoint-payments-flutterwave'), OsPaymentsFlutterwaveHelper::load_countries_list(), OsSettingsHelper::get_settings_value('flutterwave_country_code', 'NG')); ?>
</div>
<div class="os-col-6">
<?php echo OsFormHelper::select_field('settings[flutterwave_currency_iso_code]', __('Currency Code', 'latepoint-payments-flutterwave'), OsPaymentsFlutterwaveHelper::load_currencies_list(), OsSettingsHelper::get_settings_value('flutterwave_currency_iso_code', 'NGN')); ?>
</div>
</div>
<div class="os-row">
<div class="os-col-12">
<?php echo OsFormHelper::media_uploader_field('settings[flutterwave_logo_image_id]', 0, __('Logo for Payment Modal', 'latepoint-payments-flutterwave'), __('Remove Logo', 'latepoint-payments-flutterwave'), OsSettingsHelper::get_settings_value('flutterwave_logo_image_id')); ?>
</div>
</div>
<?php
}
...
}
// latepoint-payments-flutterwave.php
OsFormHelper class helps build form fields for LatePoint. We are using it’s select_field, text_field, password_field and media_uploader_field methods to help us build fields to capture user data.
OsSettingsHelper::get_settings_value(KEY, DEFAULT) method retrieves settings by key, if it’s not set yet it will return a DEFAULT value.
Sometimes you want to have certain sensitive fields to be encrypted when they are stored in LatePoint settings table. In this example we want flutterwave_secret_key setting value to be encrypted. To do that use latepoint_encrypted_settings hook:
<?php
class LatePointPaymentsFlutterwave {
...
public function init_hooks(){
...
// encrypt sensitive fields
add_filter('latepoint_encrypted_settings', [$this, 'add_encrypted_settings']);
...
}
public function add_encrypted_settings($encrypted_settings){
$encrypted_settings[] = 'flutterwave_secret_key';
return $encrypted_settings;
}
...
}
// latepoint-payments-flutterwave.php
Now if you open your payment settings page, you should see our newly created form:
Now that we have these settings captured in the backend, we need a way to pass them to our Javascript front, so that the booking form on the front can use them to initialize FlutterWave’s inline checkout. There is an object called latepoint_helper on a front-end, which holds all the variables passed from the backend, as object properties using latepoint_localized_vars_front filter. To add more properties to that object we should use that hook:
<?php
class LatePointPaymentsFlutterwave {
...
public function init_hooks(){
...
// pass variables to JS frontend
add_filter('latepoint_localized_vars_front', [$this, 'localized_vars_for_front']);
...
}
public function localized_vars_for_front($localized_vars){
// check if flutterwave is enabled
if(OsPaymentsHelper::is_payment_processor_enabled($this->processor_code)){
$localized_vars['is_flutterwave_active'] = true;
// pass variables from settings to frontend
$localized_vars['flutterwave_key'] = OsSettingsHelper::get_settings_value('flutterwave_publishable_key', '');
$localized_vars['flutterwave_payment_options_route'] = OsRouterHelper::build_route_name('payments_flutterwave', 'get_payment_options');
}else{
$localized_vars['is_flutterwave_active'] = false;
}
return $localized_vars;
}
...
}
// latepoint-payments-flutterwave.php
Let’s connect FlutterWave javascript library and also create our javascript and stylesheet files to hook into LatePoint JS actions, as well as customizing css styles, if we need.
Your JS and CSS files should be in ADDON/public/javascripts/ and ADDON/public/stylesheets/ folder respectively. We can load them, along with a Flutterwave library, using latepoint_wp_enqueue_scripts action hook:
<?php
class LatePointPaymentsFlutterwave {
...
public function init_hooks(){
...
// hooks into the action to enqueue our styles and scripts
add_action('latepoint_wp_enqueue_scripts', [$this, 'load_front_scripts_and_styles']);
...
}
// Loads addon specific javascript and stylesheets for frontend site
public function load_front_scripts_and_styles(){
// Stylesheets
wp_enqueue_style( 'latepoint-payments-flutterwave-front', $this->public_stylesheets() . 'latepoint-payments-flutterwave-front.css', false, $this->version );
// Javascripts
// add flutterwave library
wp_enqueue_script( 'flutterwave-checkout', 'https://checkout.flutterwave.com/v3.js', false, null );
// include our custom js file with payment init methods
wp_enqueue_script( 'latepoint-payments-flutterwave-front', $this->public_javascripts() . 'latepoint-payments-flutterwave-front.js', array('jquery', 'flutterwave-checkout', 'latepoint-main-front'), $this->version );
}
...
}
// latepoint-payments-flutterwave.php
Let’s create a javascript file public/javascripts/latepoint-payments-flutterwave-front.js and add this code to process payment events for Flutterwave:
class LatepointPaymentsFlutterwaveAddon {
// Init
constructor(){
this.ready();
}
ready(){
jQuery(document).ready(() => {
jQuery('body').on('latepoint:submitBookingForm', '.latepoint-booking-form-element', (e, data) => {
if(!latepoint_helper.demo_mode && data.is_final_submit && data.direction == 'next'){
let payment_method = jQuery(e.currentTarget).find('input[name="booking[payment_method]"]').val();
switch(payment_method){
case 'inline_checkout':
latepoint_add_action(data.callbacks_list, () => {
return this.initPaymentModal(jQuery(e.currentTarget), payment_method);
});
break;
}
}
});
jQuery('body').on('latepoint:nextStepClicked', '.latepoint-booking-form-element', (e, data) => {
if(!latepoint_helper.demo_mode && (data.current_step == 'payment')){
let payment_method = jQuery(e.currentTarget).find('input[name="booking[payment_method]"]').val();
switch(payment_method){
case 'inline_checkout':
latepoint_add_action(data.callbacks_list, () => {
});
break;
}
}
});
jQuery('body').on('latepoint:initPaymentMethod', '.latepoint-booking-form-element', (e, data) => {
if(data.payment_method == 'inline_checkout'){
let $booking_form_element = jQuery(e.currentTarget);
let $latepoint_form = $booking_form_element.find('.latepoint-form');
latepoint_add_action(data.callbacks_list, () => {
latepoint_show_next_btn($booking_form_element);
});
}
});
jQuery('body').on('latepoint:initStep:payment', '.latepoint-booking-form-element', (e, data) => {
});
});
}
initPaymentModal($booking_form_element, payment_method) {
let deferred = jQuery.Deferred();
let $latepoint_form = $booking_form_element.find('.latepoint-form');
var data = {
action: 'latepoint_route_call',
route_name: latepoint_helper.flutterwave_payment_options_route,
params: $booking_form_element.find('.latepoint-form').serialize(),
layout: 'none',
return_format: 'json'
}
jQuery.ajax({
type : "post",
dataType : "json",
url : latepoint_helper.ajaxurl,
data : data,
success: (data) => {
if(data.status === "success"){
if(data.amount > 0){
$booking_form_element.find('input[name="booking[intent_key]"]').val(data.booking_intent_key);
data.options.callback = (response) => {
if(response.transaction_id){
let $payment_token_field = $booking_form_element.find('input[name="booking[payment_token]"]');
if($payment_token_field.length){
$booking_form_element.find('input[name="booking[payment_token]"]').val(response.transaction_id);
}else{
// create payment token field if it doesn ot exist (when payment step is skipped)
$booking_form_element.find('.latepoint-booking-params-w').append('<input type="hidden" value="' + response.transaction_id +'" name="booking[payment_token]" class="latepoint_payment_token"/>');
}
// remove flutterwave iframe
jQuery('iframe[name="checkout"]').remove();
jQuery('body').css('overflow', '');
deferred.resolve();
}else{
deferred.reject({message: 'Processor Payment Error'});
}
};
data.options.onclose = () => {
deferred.reject({message: 'Checkout form closed'});
}
FlutterwaveCheckout(data.options);
}else{
// free booking
deferred.resolve();
}
}else{
deferred.reject({message: data.message});
}
},
error: function(request, status, error){
deferred.reject({message: result.error.message});
}
});
return deferred;
}
}
let latepointPaymentsFlutterwaveAddon = new LatepointPaymentsFlutterwaveAddon();
To generate options hash for Flutterwave popup from the backend, we need to create a new controller file in addon’s /lib/controllers/ folder, called payments_flutterwave_controller.php. You can create a controller in LatePoint by extending OsController class. We need to add get_payment_options method, which will handle an action generating necessary data for our ajax call:
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
if ( ! class_exists( 'OsPaymentsFlutterwaveController' ) ) :
class OsPaymentsFlutterwaveController extends OsController {
function __construct(){
parent::__construct();
$this->views_folder = plugin_dir_path( __FILE__ ) . '../views/flutterwave/';
}
/* Generates payment options for Flutterwave inline checkout */
public function get_payment_options(){
// set booking object from passed params
OsStepsHelper::set_booking_object($this->params['booking']);
// set restrictions passed from a form shortcode
OsStepsHelper::set_restrictions($this->params['restrictions']);
$customer = OsAuthHelper::get_logged_in_customer();
// calculate amount to be charged
$amount = OsStepsHelper::$booking_object->specs_calculate_price_to_charge();
try{
if($amount > 0){
// create booking intent in the database
$booking_intent = OsBookingIntentHelper::create_or_update_booking_intent($this->params['booking'], $this->params['restrictions'], ['payment_method' => $this->params['booking']['payment_method']], '');
// create options array, which will be passed to the front-end JS
$options = [
"public_key" => OsSettingsHelper::get_settings_value('flutterwave_publishable_key'),
"tx_ref" => $booking_intent->intent_key,
"amount" => $amount,
"currency" => OsSettingsHelper::get_settings_value('flutterwave_currency_iso_code', 'NGN'),
"country" => OsSettingsHelper::get_settings_value('flutterwave_country_code', 'NG'),
"customer" => [
"email" => $customer->email,
"phone_number" => $customer->phone,
"name" => $customer->full_name
]
,
"customizations" => [
"name" => OsSettingsHelper::get_settings_value('flutterwave_company_name', 'Company'),
"description" => $booking->service->name,
"logo" => OsImageHelper::get_image_url_by_id(OsSettingsHelper::get_settings_value('flutterwave_logo_image_id', false))
]
];
$this->send_json(array('status' => LATEPOINT_STATUS_SUCCESS, 'options' => $options, 'amount' => $amount, 'booking_intent_key' => $booking_intent->intent_key));
}else{
// free booking, nothing to pay (probably coupon was applied)
$this->send_json(array('status' => LATEPOINT_STATUS_SUCCESS, 'message' => __('Nothing to pay', 'latepoint-payments-flutterwave'), 'amount' => $amount));
}
}catch(Exception $e){
error_log($e->getMessage());
$this->send_json(array('status' => LATEPOINT_STATUS_ERROR, 'message' => $e->getMessage()));
}
}
}
endif;
// /lib/controllers/payments_flutterwave_controller.php
Last step is to process the booking submission and create transaction record in the backend. We will need to hook into latepoint_process_payment_for_booking filter:
<?php
class LatePointPaymentsFlutterwave {
...
public function init_hooks(){
...
// hook into payment processing
add_filter('latepoint_process_payment_for_booking', [$this, 'process_payment'], 10, 3);
...
}
public function process_payment($result, $booking, $customer){
if(OsPaymentsHelper::is_payment_processor_enabled($this->processor_code)){
switch($booking->payment_method){
// check if payment method is flutterwave inline checkout
case 'inline_checkout':
if($booking->payment_token){
// call flutterwave api endpoint to verify that transaction exists
$remote = wp_remote_get( "https://api.flutterwave.com/v3/transactions/".$booking->payment_token."/verify", [
'timeout' => 10,
'headers' => [
'Accept' => 'application/json',
'Authorization' => 'Bearer '. self::get_secret_key()
]
]);
// process the response
if ( ! is_wp_error( $remote ) && isset( $remote['response']['code'] ) && $remote['response']['code'] == 200 && ! empty( $remote['body'] ) ) {
$response_body = json_decode($remote['body']);
// check if transaction is found and it has "successful" status
if($response_body->status == 'success' && $response_body->data->status == 'successful'){
$result['status'] = LATEPOINT_STATUS_SUCCESS;
$result['charge_id'] = $response_body->data->id;
$result['processor'] = $this->processor_code;
$result['funds_status'] = LATEPOINT_TRANSACTION_FUNDS_STATUS_CAPTURED;
}else{
// transaction not found or is not successfull
$result['status'] = LATEPOINT_STATUS_ERROR;
$result['message'] = __('Payment Error', 'latepoint-payments-flutterwave');
$booking->add_error('payment_error', $result['message']);
$booking->add_error('send_to_step', $result['message'], 'payment');
}
}else{
// api connection error
$result['status'] = LATEPOINT_STATUS_ERROR;
$result['message'] = __('Connection error', 'latepoint-payments-flutterwave');
$booking->add_error('payment_error', $result['message']);
$booking->add_error('send_to_step', $result['message'], 'payment');
}
}else{
// payment token is not set
$result['status'] = LATEPOINT_STATUS_ERROR;
$result['message'] = __('Payment Error KSF9834', 'latepoint-payments-flutterwave');
$booking->add_error('payment_error', $result['message']);
}
break;
}
}
return $result;
}
...
}
// latepoint-payments-flutterwave.php
latepoint_process_payment_for_booking filter is called if total booking charge amount is greater than 0, it will pass 3 variables: processing result (false by default), booking object and customer object, you can hook into that filter and check if the payment method of a booking object [$booking→payment_method] is one of the payment methods of the enabled payment processor and process the payment. Booking object also holds payment_token property, which is being passed from the front-end form on submission. It’s being set by javascript, when FlutterWave processes the payment.
We then call Flutterwave API and check if token matches the transaction, and if transaction is indeed successful. Once everything is verified, we update $result (processing result) variable with a success status, set charge_id, processor_code and funds_status keys.