如何在Android中用Firebase云函数为Safaricom Daraja API创建一个回调URL

110 阅读7分钟

在Android中用Firebase云功能为Safaricom Daraja API创建回调URL

随着Safaricom开发的Daraja API的引入,在Android中实现Lipa-Na-Mpesa (用MPesa支付)功能变得更加容易。

许多开发者希望收到Safaricom在用户进行交易时发送的所有信息。有些交易可能会通过,有些则会失败。这些信息在更新你的应用程序中的记录时很有用。

作为一个Android开发者,拥有一个回调URL意味着你需要有一个REST后端来接收响应。这可能很昂贵。我们可以用Firebase云函数创建一个简单的API,它将帮助我们接收回调URL的数据。

前提条件

要完成本教程,你必须具备以下条件。

  • 在你的电脑上安装了[Android Studio]。
  • 对如何创建和运行Android应用程序有扎实的了解。
  • [Kotlin]和Coroutines的基本知识。
  • 了解云函数。

Daraja API回调URL

回调是一个异步的API请求,它来自API服务器,并被发送到客户端,作为对客户端之前某个请求的回应。当使用Daraja API时,Safaricom要求你传递一个URL,他们将从你的应用程序返回已处理的交易信息。

第1步 - 在Firebase控制台创建一个项目

首先,在Firebase上创建一个项目,并将其链接到你的Android应用程序。在成功链接后,确保你的项目在Blaze Plan中,这样我们就可以使用云功能了。

Change To Blaze

第2步 - 在Android Studio上创建一个项目

创建一个空的Android项目。

New Project

添加所有必要的依赖项。

// Daraja API Library
implementation 'com.androidstudy:daraja:1.0.2'

// Firebase Functions
implementation 'com.google.firebase:firebase-functions:20.0.1'

// Firebase Messaging
implementation 'com.google.firebase:firebase-messaging:20.2.1'

// Gson
implementation 'com.google.code.gson:gson:2.8.6'

// Android Kotlin Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'

第3步 - 创建一个Firebase云函数

我们将从创建一个Firebase函数开始。

让我们快速创建一个我们将使用的云函数。

一旦你的项目准备好了,并且你已经添加了所有必要的依赖项,在你的Android Studio中打开终端,键入以下命令。

  1. npm install -g firebase-tools - 来安装Firebase。
  2. firebase login - 登录到你的Firebase功能。
  3. firebase init functions - 来初始化你的项目。
  4. 选择,Use an Existing project
  5. 选择你要链接函数的项目,在本教程中,我将选择LNMCallback
  6. 对于语言,选择Javascript
  7. 对于ESLint,就选择N
  8. 要用npm安装依赖项?- 选择Y

一旦你看到Firebase的初始化已经完成了我们就可以进入下一个步骤了。我们需要安装express body-parser ,这是一个中间件,以便我们能够读取传入的JSON对象的body

在你的Android Studio中,切换到项目视图,在终端中打开functions 文件夹,粘贴以下命令。

npm install express body-parser -S

一旦你安装了body-parser,你就可以开始了。

在'index.js'中,我们需要编写云功能代码,擦除一切,粘贴以下代码。

let functions = require('firebase-functions');
let admin = require('firebase-admin');

admin.initializeApp(functions.config().firebase);
const express = require('express');
const body_parser = require('body-parser');

const app = express();
app.use(body_parser.json());
app.disable('x-powered-by');

app.post('/CallbackUrl', (request, result) => {
    let response = { "ResultCode": 0, "ResultDesc": "Success" }

    result.status(200).json(response);

    let requestBody = request.body;
    let myPayload = JSON.stringify(requestBody)

    // Logs successful function calls
    console.log(myPayload)

    let topicId =  body.Body.stkCallback.CheckoutRequestID

      const sentPayload = {
          data: {
            myPayload,
            },
            topicId : id
        };

         return admin.messaging().send(sentPayload).catch(error=>{
         
            // Logs Failed function calls    
         console.error(error)
         })
})

exports.api = functions.https.onRequest(app);

写完代码后,就该把函数部署到Firebase了。使用firebase deploy --only functions 来部署该函数。

部署完成后,你应该看到类似这样的东西。

Deployed

当你打开你的Firebase控制台时,你就可以看到部署的函数了。

第4步 - 在Safaricom开发者门户网站上创建一个应用程序

在这一步,我们将把我们的应用程序与Safaricom Daraja API - Lipa Na Mpesa连接起来。

进入[Safaricom开发者门户网站]并登录。如果你还没有一个账户,请创建一个。

一旦准备就绪,在菜单栏中点击我的应用程序,并选择,创建一个新的应用程序,并确保你已经勾选了Lipa na M-Pesa沙箱

New App

请注意CONSUMER_KEYCONSUMER_SECRET ,因为我们将在后面的应用程序中使用它们。

Mpesa App

第5步 - 设计布局

在这一步,我们将创建一个简单的XML布局,它将包含一个用于输入电话号码的EditText和一个用于启动交易的按钮。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/editTextPhone"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="32dp"
        android:layout_marginEnd="32dp"
        android:hint="@string/phone_number"
        android:importantForAutofill="no"
        android:inputType="phone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.31" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="@string/pay"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/editTextPhone" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/lipa_na_m_pesa"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        app:layout_constraintBottom_toTopOf="@+id/editTextPhone"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

第6步 - 创建数据类

在这一步,我们将创建一个数据类,它将映射我们来自API的响应。

data class Transaction(
    @SerializedName("Body")
    val body: Body
) {
    data class Body(
        @SerializedName("stkCallback")
        val stkCallback: StkCallback
    ) {
        data class StkCallback(
            @SerializedName("CallbackMetadata")
            val callbackMetadata: CallbackMetadata,
            @SerializedName("CheckoutRequestID")
            val checkoutRequestID: String,
            @SerializedName("MerchantRequestID")
            val merchantRequestID: String,
            @SerializedName("ResultCode")
            val resultCode: Int,
            @SerializedName("ResultDesc")
            val resultDesc: String
        ) {
            data class CallbackMetadata(
                @SerializedName("Item")
                val item: List<Item>
            ) {
                data class Item(
                    @SerializedName("Name")
                    val name: String,
                    @SerializedName("Value")
                    val value: String
                )
            }
        }
    }
}

第7步 - 创建M-Pesa接口

在这一步,我们将定义一个有两个方法的接口。第一个方法将在交易成功时被调用,而第二个方法将在交易因不同原因(如余额不足)而失败时被调用。

interface MpesaListener {
    fun sendingSuccessful(transactionAmount: String, phoneNumber: String, transactionDate: String, MPesaReceiptNo: String)

    fun sendingFailed(cause: String)
}

第8步 - Firebase消息服务

在这一步,我们将创建一个Firebase消息服务类,它将完成接收API响应的大部分应用逻辑。

class MessagingService : FirebaseMessagingService() {

    override fun onNewToken(p0: String) {
        super.onNewToken(p0)
    }

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)

        if (remoteMessage.data.isNotEmpty()) {
            val myPayload = remoteMessage.data["payload"]
            val gson = Gson()

            val mpesaResponse: Transaction = gson.fromJson(myPayload, Transaction::class.java)

            val topicID = mpesaResponse.body.stkCallback.checkoutRequestID

            if (mpesaResponse.body.stkCallback.resultCode != 0) {
                val cause = mpesaResponse.body.stkCallback.resultDesc
                MainActivity.mpesaListener.sendingFailed(cause)

            } else {
                val infoList: List<Body.StkCallback.CallbackMetadata.Item> =
                    mpesaResponse.body.stkCallback.callbackMetadata.item

                var dateOfTransaction = ""
                var amountTransacted = ""
                var receiptNo = ""
                var phoneNumber = ""

                infoList.forEach { transaction ->
                    if (transaction.name == "MpesaReceiptNumber") {
                        receiptNo = transaction.value
                    }
                    if (transaction.name == "TransactionDate") {
                        dateOfTransaction = transaction.value
                    }
                    if (transaction.name == "PhoneNumber") {
                        phoneNumber = transaction.value
                    }
                    if (transaction.name == "Amount") {
                        amountTransacted = transaction.value
                    }
                }

                MainActivity.mpesaListener.sendingSuccessful(
                    amountTransacted,
                    phoneNumber,
                    extractDate(dateOfTransaction),
                    receiptNo
                )
            }

            FirebaseMessaging.getInstance().unsubscribeFromTopic(topicID)
        }
    }

    private fun extractDate(date: String): String {
        return "${date.subSequence(6, 8)}${date.subSequence(4, 6)} ${
            date.subSequence(0, 4)
        } at ${date.subSequence(8, 10)}:${date.subSequence(10, 12)}:${date.subSequence(12, 14)}"
    }
}

在你的清单文件中,确保你已经包含了我们创建的服务,进入你的清单并粘贴以下几行代码。

<application>
...
  <service
      android:name=".MessagingService"
      android:stopWithTask="false"
      android:exported="false">
      <intent-filter>
          <action android:name="com.google.firebase.MESSAGING_EVENT" />
      </intent-filter>
  </service>
</application>

第9步--编写MainActivity代码

在这一步,我们将编写代码,利用android-mpesa-api将支付整合到我们的应用程序。

初始化Daraja API

在这里,用你在Safaricom开发者门户网站上创建应用程序时给你的SECRET_KEYCONSUMER_SECRET_KEY

daraja = Daraja.with("CONSUME_KEY", "CONSUMER_SECRET_KEY", Env.SANDBOX,
    object : DarajaListener<AccessToken> {
        override fun onResult(result: AccessToken) {
            Toast.makeText(applicationContext, result.access_token, Toast.LENGTH_SHORT).show()
        }

        override fun onError(error: String?) {
            Toast.makeText(applicationContext, error.toString(), Toast.LENGTH_SHORT).show()
        }

    })

当用户点击按钮时,我们需要执行以下代码。

findViewById<Button>(R.id.button).setOnClickListener {
    val phoneNumber = phoneNum.text.toString()
    val lnmExpress = LNMExpress(
        "174379",
        "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919",
        TransactionType.CustomerPayBillOnline,
        "1",
        phoneNumber,
        "174379",
        phoneNumber,
        "https://us-central1-lnmcallback-c79b4.cloudfunctions.net/api/CallbackUrl",
        "001ABC",
        "Goods Payment"
    )
    ....

在这里,我们传递电话号码,同时使用1作为交易的默认金额。

到你的Firebase控制台的功能部分,复制我们生成的API的URL。

Function

在这个例子中,我的是。

https://us-central1-lnmcallback-c79b4.cloudfunctions.net/api

在URL的末尾加上/CallbackUrl ,这样最后的URL就会变成这样。

https://us-central1-lnmcallback-c79b4.cloudfunctions.net/api/CallbackUrl

这将是我们的回调URL,这样Safaricom就可以向我们发送已经启动的交易的响应。去用新的URL替换MY_CALLBACK_URL

附加FirebaseMessaging服务

在这一步,在onResult 方法中,我们实例化FirebaseMessaging ,并且用已经启动的交易的CheckoutRequestID 来订阅主题。

daraja.requestMPESAExpress(lnmExpress, object : DarajaListener<LNMResult> {
    override fun onResult(result: LNMResult) {
        FirebaseMessaging.getInstance().subscribeToTopic(result.CheckoutRequestID.toString())
    }

    override fun onError(error: String?) {
        Toast.makeText(applicationContext, "An Error Occurred: $error", Toast.LENGTH_SHORT).show()
    }
})

确保你的MainActivity实现了我们所创建的接口,并重写了我们在其中创建的方法。

在我们重载的两个方法里面,我们将定义一个CoroutineScope ,并添加一个Toast ,以表示事务的成功或失败。

override fun sendingSuccessful(transactionAmount: String, phoneNumber: String, transactionDate: String, MPesaReceiptNo: String) {
    CoroutineScope(Dispatchers.Main).launch {
        Toast.makeText(
            applicationContext,
            "Transaction Successful\nM-Pesa Receipt No: $MPesaReceiptNo\nTransaction Date: $transactionDate\nTransacting Phone Number: $phoneNumber\nAmount Transacted: $transactionAmount", Toast.LENGTH_LONG).show()
    }
}

override fun sendingFailed(cause: String) {
    CoroutineScope(Dispatchers.Main).launch {
        Toast.makeText(
            applicationContext, "Transaction Failed\nReason: $cause", Toast.LENGTH_LONG
        ).show()
    }
}

MainActivity的整个实现

class MainActivity : AppCompatActivity(), MpesaListener {

    companion object {
        lateinit var mpesaListener: MpesaListener
    }

    private lateinit var daraja: Daraja

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mpesaListener = this

        val phoneNum = findViewById<EditText>(R.id.editTextPhone)

        daraja = Daraja.with("CONSUME_KEY", "CONSUMER_SECRET_KEY", Env.SANDBOX,
            object : DarajaListener<AccessToken> {
                override fun onResult(result: AccessToken) {
                    Toast.makeText(applicationContext, result.access_token, Toast.LENGTH_SHORT).show()
                }

                override fun onError(error: String?) {
                    Toast.makeText(applicationContext, error.toString(), Toast.LENGTH_SHORT).show()
                }
            })

        findViewById<Button>(R.id.button).setOnClickListener {
            val phoneNumber = phoneNum.text.toString()
            val lnmExpress = LNMExpress(
                "174379",
                "bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919",
                TransactionType.CustomerPayBillOnline,
                "1",
                phoneNumber,
                "174379",
                phoneNumber,
                "https://us-central1-lnmcallback-c79b4.cloudfunctions.net/api/CallbackUrl",
                "001ABC",
                "Goods Payment"
            )

            daraja.requestMPESAExpress(lnmExpress, object : DarajaListener<LNMResult> {
                override fun onResult(result: LNMResult) {
                    FirebaseMessaging.getInstance().subscribeToTopic(result.CheckoutRequestID.toString())
                }

                override fun onError(error: String?) {
                    Toast.makeText(applicationContext, "An Error Occurred: $error", Toast.LENGTH_SHORT).show()
                }
            })
        }
    }

    override fun sendingSuccessful(transactionAmount: String, phoneNumber: String, transactionDate: String, MPesaReceiptNo: String) {
        CoroutineScope(Dispatchers.Main).launch {
            Toast.makeText(
                applicationContext,
                "Transaction Successful\nM-Pesa Receipt No: $MPesaReceiptNo\nTransaction Date: $transactionDate\nTransacting Phone Number: $phoneNumber\nAmount Transacted: $transactionAmount", Toast.LENGTH_LONG).show()

        }
    }

    override fun sendingFailed(cause: String) {
        CoroutineScope(Dispatchers.Main).launch {
            Toast.makeText(
                applicationContext, "Transaction Failed\nReason: $cause", Toast.LENGTH_LONG
            ).show()
        }
    }
}

应用程序演示

运行该应用程序后,它应该是这样的,你应该能够启动一个交易。

Send Success

Send Failed

Firebase功能日志

Functions Logs

总结

在本教程中,我们学习了如何创建一个回调URL,我们还看到了如何在Safaricom开发者门户网站上创建一个应用程序。最后,我们使用我们创建的URL来接收交易的回调。