在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中,这样我们就可以使用云功能了。

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

添加所有必要的依赖项。
// 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中打开终端,键入以下命令。
npm install -g firebase-tools- 来安装Firebase。firebase login- 登录到你的Firebase功能。firebase init functions- 来初始化你的项目。- 选择,
Use an Existing project。 - 选择你要链接函数的项目,在本教程中,我将选择
LNMCallback。 - 对于语言,选择
Javascript。 - 对于ESLint,就选择
N。 - 要用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 来部署该函数。
部署完成后,你应该看到类似这样的东西。

当你打开你的Firebase控制台时,你就可以看到部署的函数了。
第4步 - 在Safaricom开发者门户网站上创建一个应用程序
在这一步,我们将把我们的应用程序与Safaricom Daraja API - Lipa Na Mpesa连接起来。
进入[Safaricom开发者门户网站]并登录。如果你还没有一个账户,请创建一个。
一旦准备就绪,在菜单栏中点击我的应用程序,并选择,创建一个新的应用程序,并确保你已经勾选了Lipa na M-Pesa沙箱。

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

第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_KEY 和CONSUMER_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。

在这个例子中,我的是。
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()
}
}
}
应用程序演示
运行该应用程序后,它应该是这样的,你应该能够启动一个交易。


Firebase功能日志

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