aramics / mpesa-sdk
Interact with mpesa api (daraja) with ease
0.0.2
2022-11-25 16:49 UTC
README
mpesa payment library for PHP
Installation
Project using composer.
$ composer require aramics/mpesa-sdk
Others:
Download and include the required file
require mpesa-sdk/src/Mpesa.php
Backend Usage
<?php use Aramics\MpesaSdk\Mpesa; class Payment{ public $settings; public $mpesa; function __construct(){ //settings $this->settings = [ 'mode' => 'sandbox', //or live 'consumer_key' => '', 'consumer_secret' => '', 'phone_number' =>'', //admin mpesa phone number 'short_code' => '', 'stk_pass_key' => '', //LIPA stk push password 'logger' => 'custom_log', //callback function for logging. Empty to disable error logging. ]; //Create the instance $this->mpesa = new Mpesa($this->settings); } //Backend endpoint: https://services.com/backend/checkout function checkout() { $customer_mpesa_phone_number = ; //get from post sanitize($_POST["phone_number"]); $amount = (float); //get from post input $_POST['amount']; //make validation to phone and amount // Make payment with STK Push (LIPA API) $timestamp = date("YmdHis", time()); //add hmac for unique url. This increase the security $hmac = hash_hmac("sha1", "$amount:$customer_mpesa_phone_number:$timestamp", $this->settings['consumer_secret']); //make the https secure url. Ensure tls , use ngrok or telebit to test on localserver $callback_url = 'https://services.com/webhook/mpesa/' .$hmac; //send stk push to user $payment = $this->mpesa->stkPush( $amount=500, //amount $ref_id="someOrderID", //reference id $description="Payment for", //description $customer_phone_number, $callback_url, //callback/ipn to process payment $timestamp, //timestamp in "YmdHis" ..optional ); if ($payment->success) { //push send successfully. $payment_ref = $payment->ref_id; $_SESSION['order_ref'] = $payment->ref_id; //make some write to the DB with the payment_ref,user_id and $hmac to be used later in callback } return $this->responseJson($payment); } //Backend endpoint status check: https://services.com/backend/status public function status($txn_id = '') { $payment = find_payment_by_order_id or find_payment_by_session_ref; //$_SESSION['order_ref'] $id = null; $success = false; if ($payment) { $status = $payment->status; if ($status != "pending") { $id = $payment->id; } $success = $payment->status == 'success'; } return $this->responseJson($id ? ['id' => $id, 'success' => $success, 'message' => $payment->description] : []); } //webhook callback/ipn for validating payment i.e //https://services.com/webhook/mpesa/<signature> function callbackNotification() { //find the signature from the db $order = ;//find signature (hmac) from db if (!$order) { //make some log return; } $payload = $this->input->raw_input_stream; $event = (object)json_decode($payload); if (isset($event->Body->stkCallback)) { try { $stk = $event->Body->stkCallback; $code = $stk->ResultCode; $description = $stk->ResultDesc; $transactionReference = $stk->CheckoutRequestID; $amount = 0; $phone = ''; //validate callback/notification is truly from mpesa if ($this->settings['mode'] != "sandbox" && !$this->mpesa->isValidCallback()) { $ip = $this->mpesa->getIPAdress(); throw new \Exception("Mpesa: Request source is unkown ($ip) for $transactionReference", 1); } //successful if ($code === 0) { $metas = (array)$stk->CallbackMetadata->Item; foreach ($metas as $meta) { if ($meta->Name == "Amount") { $amount = (float)$meta->Value; } if ($meta->Name == "PhoneNumber") { $phone = $meta->Value; } } $timestamp = $order->timestamp; //enesure matched amount and phone number $hmac = hash_hmac("sha1", "$amount:$phone:$timestamp", $this->settings['consumer_secret']); if ($hmac !== $signature) { throw new \Exception("Mpesa: Invalid signature for $transactionReference", 1); } //ensure reference id generated during request when signature (hmac) was generated matches with the one in db. if ($order->payment_ref_id !== $transactionReference) { throw new \Exception("Mpesa: ref id mismatched for $transactionReference", 1); } //ensure $transactionRefrence not yet used on the db //finally make fulfillment using $transactionRefrence //you can remove the order log } else { //failed $event->status = "failed"; //update order with the status "failed"; } } catch (\Exception $e) { set_status_header(500); return $this->responseJson(['error' => $e->getMessage()]); } } }
Frontend Usage
Getting phone number for the UI and showing the user payment flow
<?php //generate the modal html, JS and CSS echo Aramics\MpesaSdk\Mpesa::loadModal(); //this will inject an object mpesaPay into the current window //see below for use. ?> <script> let from = document.getElementById("checkout-form"); let amount = document.getElementById("amount-input").value; //mpesaPay.init() mpesaPay.init({ timeoutMinutes: 30, amount: amount * 126, amountInUSD: amount, notificationCallback: notify, submitCallback: (phoneNumber) => { //disable phone input and button click. mpesaPay.disableActions(); //insert phone number to form. form.append(`<input type="hidden" name="phone_number" value="${phoneNumber}" />`) //submit form through ajax. i. e the backend that send STK Push (https://services.com/backend) $.post( 'https://services.com/backend/checkout', form.serialize(), (json, statusText) => { //We want to show message only when error i.e smooth process. if (!json.success) alert(json.message); //STK Push sent to user, now we need to await user to pay if (json.success && json.ref_id) { //when user input pin and confirm, the notification will be sent to our callback ///we need to check the backend to detect if order is marked paid let checkInterval = checkMpesaStatus(json.ref_id); let timeoutCallback = () => { clearInterval(checkInterval); alert("Timed out. If you have confirmed the prompt, refresh to check you order status"); window.location.reload(); } //init the timer countdown for completeing the transaction... mpesaPay.initTimer(timeoutCallback); } else { //enable button and input mpesaPay.enableActions(); } }, (xhr, textStatus, errorThrown) => { notify(errorThrown); //enable button and input mpesaPay.enableActions(); }) } }); //show modal mpesaPay.openModal(); //callback functioin to check if other is fullfiled on backend //check payment status every 15 seconds const checkMpesaStatus = (txnId) => { let poolInterval = setInterval(async () => { let resp = await fetch("https://services.com/backend/status/" + txnId) if (resp.status) { resp = await resp.json(); if (resp.id) { alert(resp.message); setTimeout(() => { window.location.reload(); }, 5000); } } }, 15000); return poolInterval; }; </script>