For this example we will integrate Flutterwave payment processor into LatePoint. First of all you have to download our latepoint-addon-starter plugin. 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

latepoint-payments-flutterwave.php

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

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.