Unity客户端接入原生Google支付
1. Google后台配置
找到内部测试(这个测试轨道过审最快),打包上传,这个包不需要接入支付,如果已经有上传过包了那就跳过这一步
在许可测试里添加测试人员,勾选测试人员列表,并且设置许可相应为LICENSED,这样才可以使用测试卡测试支付
确认已经添加了付款方式,以及开放地区有香港,否则可能需要挂VPN才能进行支付流程测试
流程图
2. 开始接入
确保Unity Plugins/Android里有com.android.billingclient.billing,并且是v3版本以上,这里用的5.0.0版本
Java部分
java文件,也放到Plugins/Android下,开头package需要根据项目而定
GoogleBillingManager.java
package com.xxx.xxx;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.unity3d.player.UnityPlayer;
import java.util.List;
public class GoogleBillingManager {
private static GoogleBillingManager instance;
private static BillingClient billingClient;
private static GoogleBillingListener billingListener;
public static boolean isConnection = false;
private GoogleBillingManager() {
instance = this;
createClient(UnityPlayer.currentActivity);
}
public static GoogleBillingManager getInstance() {
if (instance == null) {
synchronized (GoogleBillingManager.class) {
if (instance == null) {
instance = new GoogleBillingManager();
}
}
}
return instance;
}
/**
* 创建支付客户端
*/
public static void createClient(Activity activity) {
if (isReady()) {
return;
}
if (null == activity) {
Log.e("TAG","谷歌支付CreateClient, activity = null");
return;
}
billingClient = BillingClient.newBuilder(activity)
.enablePendingPurchases()
.setListener(new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
if (null != billingListener) {
billingListener.onPurchasesUpdated(billingResult, purchases);
}
}
})
.build();
//启动支付连接
startConn();
}
public BillingClient getBillingClient() {
return billingClient;
}
/**
* 添加监听事件
*/
public void setBillingListener(GoogleBillingListener listener) {
billingListener = listener;
}
/**
* 是否准备好了
*
* @return
*/
public static boolean isReady() {
return !(null == billingClient || !billingClient.isReady());
}
/**
* 启动连接
*/
private static void startConn() {
if (isReady()) {
return;
}
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
isConnection = true;
Log.e("TAG", "连接成功,可以开始操作了~~~");
}
}
@Override
public void onBillingServiceDisconnected() {
isConnection = false;
//连接失败。 可以尝试调用 startConnection 重新建立连接
Log.e("TAG", "连接失败");
}
});
}
/**
* 结束连接
*/
public void endConn() {
if (null != billingClient) {
billingClient.endConnection();
isConnection = false;
}
}
}
GoogleBillHelper.java
package com.xxx.xxx;
import android.app.Activity;
import android.util.Log;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesResponseListener;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.QueryPurchasesParams;
import com.unity3d.player.UnityPlayer;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.annotations.NonNull;
/**
* Desc:支付的具体操作
* 1.查询
* 2.购买
* 3.消费
*/
public class GoogleBillHelper {
public static final String TAG = GoogleBillHelper.class.getSimpleName();
/**
* 查询商品详情
*
* @param billingListener : 接口监听
* @param productIds :商品id 。对应Google 后台的
* @param productType :取值
* BillingClient.ProductType.INAPP(一次性商品)
* BillingClient.ProductType.SUBS(订阅)
*/
public static void onQuerySkuDetailsAsync(GoogleBillingListener billingListener, String productType, String productIds, String orderId) {
if (null == productIds){
return;
}
String[] productList = productIds.split(",");
Log.e("TAG", "onQuerySkuDetailsAsync: " + productIds + " ----->" + productList[0]);
if (productList.length == 0 || !GoogleBillingManager.getInstance().isReady()) {
Log.e("TAG", "productList.length:" + productList.length + ",client:" + GoogleBillingManager.getInstance().isReady());
return;
}
List<QueryProductDetailsParams.Product> skuList = new ArrayList<>();
for (String productId : productList) {
QueryProductDetailsParams.Product product = QueryProductDetailsParams
.Product.newBuilder()
.setProductId(productId)
.setProductType(productType)
.build();
//添加对应的 产品id 去查询详情
skuList.add(product);
}
QueryProductDetailsParams params = QueryProductDetailsParams
.newBuilder()
.setProductList(skuList)
.build();
GoogleBillingManager.getInstance().getBillingClient().queryProductDetailsAsync(params, (billingResult, list) -> {
if (null != billingListener) {
billingListener.onProductDetailsSus(billingResult, list, orderId);
}
});
}
/**
* 打开支付面板
*
* @param billingListener
* @param activity
* @param details
*/
public static void onOpenGooglePlay(GoogleBillingListener billingListener, Activity activity, ProductDetails details, String orderId) {
if (null == details) {
return;
}
List<BillingFlowParams.ProductDetailsParams> params = new ArrayList<>();
//添加购买数据
BillingFlowParams.ProductDetailsParams productDetailsParams = BillingFlowParams.ProductDetailsParams
.newBuilder()
.setProductDetails(details)
.build();
params.add(productDetailsParams);
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(params)
.setObfuscatedAccountId(orderId)
.build();
//添加购买监听
GoogleBillingManager.getInstance().setBillingListener(billingListener);
//响应code 码
GoogleBillingManager.getInstance().getBillingClient().launchBillingFlow(activity, billingFlowParams).getResponseCode();
}
/**
* 消费商品
* 对于购买类型的商品需要手动调用一次消费方法 (目的:用户可以再次购买此商品)
*
* @param billingListener
* @param purchase
*/
public static void onConsumeAsync(GoogleBillingListener billingListener, Purchase purchase) {
if (!GoogleBillingManager.getInstance().isReady()) {
return;
}
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
ConsumeResponseListener listener = (billingResult, purchaseToken) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
String result = "消费code : " + billingResult.getResponseCode() + " message : " + billingResult.getDebugMessage();
if (null == billingListener) {
Log.e(TAG, result);
}
billingListener.onConsumeSus(billingResult.getResponseCode(), result, purchaseToken, purchase);
}
};
GoogleBillingManager.getInstance().getBillingClient().consumeAsync(consumeParams, listener);
}
/**
* 检查补单
*
* @param billingListener
* @param productType
*/
public static void queryPurchases(String productType, GoogleBillingListener billingListener){
PurchasesResponseListener mPurchasesResponseListener = new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> purchasesResult) {
if(billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK || purchasesResult == null){
return;
}
for (Purchase purchase : purchasesResult) {
if(purchase == null || purchase.getPurchaseState() != Purchase.PurchaseState.PURCHASED){
continue;
}
billingListener.onQueryPurchases(purchase.getAccountIdentifiers().getObfuscatedAccountId());
onConsumeAsync(billingListener, purchase);
// 这里处理已经支付过的订单,通知服务器去验证
}
}
};
QueryPurchasesParams params =
QueryPurchasesParams.newBuilder()
.setProductType(productType)
.build();
GoogleBillingManager.getInstance().getBillingClient().queryPurchasesAsync(params, mPurchasesResponseListener);
}
}
GoogleBillingListener.java
package com.xxx.xxx;
import android.util.Log;
import com.android.billingclient.api.AccountIdentifiers;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.unity3d.player.UnityPlayer;
import java.util.List;
public class GoogleBillingListener implements PurchasesUpdatedListener {
public final String objectName;
public final String paySuccessMethodName;
public final String detailsSusMethodName;
public final String payFailMethodName;
public final String payEndMethodName;
public final String queryPurchasesMethodName;
public final String detailsFailMethodName;
public ProductDetails.OneTimePurchaseOfferDetails productDetails;
public GoogleBillingListener(String objectName, String successMethodName, String processingMethodName,
String failMethodName, String payEndMethodName, String queryPurchasesMethodName, String detailsFailMethodName) {
this.objectName = objectName;
this.paySuccessMethodName = successMethodName;
this.detailsSusMethodName = processingMethodName;
this.payFailMethodName = failMethodName;
this.payEndMethodName = payEndMethodName;
this.queryPurchasesMethodName = queryPurchasesMethodName;
this.detailsFailMethodName = detailsFailMethodName;
}
/**
* 购买监听
*
* @param result
* @param purchases
*/
@Override
public void onPurchasesUpdated(BillingResult result, List<Purchase> purchases) {
Log.e("TAG", result.toString());
if (null == purchases || purchases.size() == 0) {
Log.e("TAG", "not purchases");
UnityPlayer.UnitySendMessage(this.objectName, this.payEndMethodName, "not purchases;BillingResult:" + result.toString());
return;
}
for (Purchase purchase : purchases) {
AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers();
String resultStr = accountIdentifiers.getObfuscatedAccountId() + "," + purchase.getPurchaseToken() + "," + purchase.getPurchaseState();
UnityPlayer.UnitySendMessage(this.objectName, this.payEndMethodName, resultStr);
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED){
GoogleBillHelper.onConsumeAsync(this, purchase);
}
}
}
/**
* 查询商品详情成功
*
* @param list
*/
public void onProductDetailsSus(BillingResult result, List<ProductDetails> list, String orderId) {
if (result.getResponseCode() != BillingClient.BillingResponseCode.OK){
String msg = "Get Details Fails, code:" + result.getResponseCode() + ",msg:" + result.getDebugMessage();
UnityPlayer.UnitySendMessage(this.objectName, this.detailsFailMethodName, msg);
return;
}
if (null == list || list.size() <= 0) {
Log.e("TAG", "没有查询到相关产品~~~~");
UnityPlayer.UnitySendMessage(this.objectName, this.detailsFailMethodName, "Not Search Product, Please check ProductID!");
return;
}
if (orderId != null && orderId.length() > 0){
GoogleBillHelper.onOpenGooglePlay(this, UnityPlayer.currentActivity, list.get(0), orderId);
productDetails = list.get(0).getOneTimePurchaseOfferDetails();
}
String infoList = "";
for (ProductDetails details: list) {
ProductDetails.OneTimePurchaseOfferDetails oneTimePurchaseOfferDetails = details.getOneTimePurchaseOfferDetails();
//注意:如果手机语言是法语,获取的商品价格是以 , 作为分隔符
String info = details.getProductId() + "|-|" + oneTimePurchaseOfferDetails.getFormattedPrice() + "|-|" +
oneTimePurchaseOfferDetails.getPriceCurrencyCode() + "|-|" + oneTimePurchaseOfferDetails.getPriceAmountMicros();
if (infoList.isEmpty()){
infoList = info;
}else{
infoList = infoList + ";" + info;
}
}
UnityPlayer.UnitySendMessage(this.objectName, this.detailsSusMethodName, infoList);
}
/**
* 商品消费成功
*
* @param code
* @param purchaseToken
*/
public void onConsumeSus(int code, String result, String purchaseToken, Purchase purchase) {
AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers();
String itemId = purchase.getProducts().get(0);
String msg = code + "," + result + "," + purchaseToken + "," + accountIdentifiers.getObfuscatedAccountId() + "," + itemId;
if (productDetails != null){
msg = msg + "," + productDetails.getPriceCurrencyCode() + "," + productDetails.getPriceAmountMicros();
}
if (code == BillingClient.BillingResponseCode.OK) {
UnityPlayer.UnitySendMessage(this.objectName, this.paySuccessMethodName, msg);
}else{
UnityPlayer.UnitySendMessage(this.objectName, this.payFailMethodName, msg);
}
}
public void onQueryPurchases(String txnid){
UnityPlayer.UnitySendMessage(this.objectName, this.queryPurchasesMethodName, txnid);
}
}
C#部分
IAPMangaer.cs
namespace根据自己项目决定要不要写
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace xxx.Sdk
{
public enum BillingResponseCode
{
SERVICE_TIMEOUT = -3,
FEATURE_NOT_SUPPORTED = -2,
SERVICE_DISCONNECTED = -1,
OK = 0,
USER_CANCELED = 1,
SERVICE_UNAVAILABLE = 2,
BILLING_UNAVAILABLE = 3,
ITEM_UNAVAILABLE = 4,
DEVELOPER_ERROR = 5,
ERROR = 6,
ITEM_ALREADY_OWNED = 7,
ITEM_NOT_OWNED = 8,
}
public class IAPManager
{
private bool initialize;
#if UNITY_ANDROID
private AndroidJavaClass billingManager;
private AndroidJavaClass billingHelper;
#endif
public event Action<bool, string> OnPayEndResult;
public event Action<bool, string> OnPayResult;
public event Action<bool, string> OnDetailsSus;
public event Action<bool, string> OnQueryPurchasesResult;
public void Initialize()
{
if (initialize)
{
return;
}
#if UNITY_ANDROID
if (billingManager == null)
{
billingManager = new AndroidJavaClass("com.dorocat.bombman.GoogleBillingManager");
}
if (billingHelper == null)
{
billingHelper = new AndroidJavaClass("com.dorocat.bombman.GoogleBillHelper");
}
if (SdkMgr.currentActivity == null) return;
SdkMgr.currentActivity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
billingManager.CallStatic("createClient", SdkMgr.currentActivity);
}));
#endif
initialize = true;
}
public void StartConnection()
{
#if UNITY_ANDROID
if (SdkMgr.currentActivity == null) return;
SdkMgr.currentActivity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
billingManager.CallStatic("startConn");
}));
#endif
}
public void endConnection()
{
#if UNITY_ANDROID
if (billingManager != null)
{
if (SdkMgr.currentActivity == null) return;
SdkMgr.currentActivity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
billingManager.CallStatic("endConn");
}));
}
#endif
}
public void pay(string itemId, string productType, string orderId)
{
#if UNITY_ANDROID
if (SdkMgr.currentActivity == null) return;
SdkMgr.currentActivity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
var listener = new AndroidJavaObject("com.dorocat.bombman.GoogleBillingListener",
SdkMgr.GameObjectName,
"OnPaySuccess",
"OnProductDetailsSus",
"OnPayFail",
"OnPayEnd",
"OnQueryPurchases",
"OnProductDetailsSusFail");
billingHelper.CallStatic("onQuerySkuDetailsAsync", listener, productType, itemId, orderId);
}));
#endif
}
public void getProductsDetail(string itemId, string productType)
{
#if UNITY_ANDROID
if (SdkMgr.currentActivity == null) return;
SdkMgr.currentActivity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
var listener = new AndroidJavaObject("com.dorocat.bombman.GoogleBillingListener",
SdkMgr.GameObjectName,
"OnPaySuccess",
"OnProductDetailsSus",
"OnPayFail",
"OnPayEnd",
"OnQueryPurchases",
"OnProductDetailsSusFail");
billingHelper.CallStatic("onQuerySkuDetailsAsync", listener, productType, itemId, "");
}));
#endif
}
public void queryPurchases(string productType)
{
#if UNITY_ANDROID
if (SdkMgr.currentActivity == null) return;
SdkMgr.currentActivity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
var listener = new AndroidJavaObject("com.dorocat.bombman.GoogleBillingListener",
SdkMgr.GameObjectName,
"OnPaySuccess",
"OnProductDetailsSus",
"OnPayFail",
"OnPayEnd",
"OnQueryPurchases",
"OnProductDetailsSusFail");
billingHelper.CallStatic("queryPurchases", productType, listener);
}));
#endif
}
public void onPaySuccess(string msg)
{
OnPayResult?.Invoke(true, msg);
}
public void onPayFail(string msg)
{
OnPayResult?.Invoke(false, msg);
}
public void onProductDetailsSus(string msg)
{
OnDetailsSus?.Invoke(true, msg);
}
public void onPayEnd(string msg)
{
OnPayEndResult?.Invoke(true, msg);
}
public void onQueryPurchases(string msg)
{
OnQueryPurchasesResult?.Invoke(true, msg);
}
public void onDeatilSusFail(string msg)
{
OnDetailsSus?.Invoke(false, msg);
}
public bool getConnectionState()
{
#if UNITY_ANDROID
return billingManager.GetStatic<bool>("isConnection");
#else
return false;
#endif
}
}
}
自行定义一个SdkManager.cs,在这里面初始化,包括在java层定义的回调函数名也要在这里实现
public static IAPManager billingManager = null;
public static IAPManager CreateBillingClient()
{
billingManager = new IAPManager();
billingManager.Initialize();
return billingManager;
}
public void OnPaySuccess(string result)
{
if (billingManager != null)
{
billingManager.onPaySuccess(result);
}
else
{
current?.auth.OnPaySuccess(result);
}
}
public void OnPayFail(string message)
{
if (billingManager != null)
{
billingManager.onPayFail(message);
}
else
{
current?.auth.OnPayFail(message);
}
}
public void OnPayEnd(string result)
{
if (billingManager != null)
{
billingManager.onPayEnd(result);
}
}
public void OnProductDetailsSus(string result)
{
if (billingManager != null)
{
billingManager.onProductDetailsSus(result);
}
}
public void OnProductDetailsSusFail(string result)
{
if (billingManager != null)
{
billingManager.onDeatilSusFail(result);
}
}
public void OnQueryPurchases(string result)
{
if (billingManager != null)
{
billingManager.onQueryPurchases(result);
}
}
Lua部分
初始化支付SDK
---@type xxx.Sdk.IAPManager
App.billingSdk = CS.BombMan.Sdk.SdkMgr.CreateBillingClient()
调用支付
local billingProductType =
{
INAPP = "inapp",
SUBS = "subs",
}
---sdk支付---
function ShopMgr:pay(itemId, payCallBack, addInfo)
if LuaClass.Application.isMobilePlatform then
local callback
callback = function(result,str)
App.billingSdk:OnPayResult("-", callback)
print("onPayResult",result,str)
if payCallBack then
payCallBack(result,str)
end
end
if not App.billingSdk:getConnectionState() then
--如果没有连上Google支付服务器,开始连接
App.billingSdk:StartConnection()
return
end
local payEnd
payEnd = function(result, msg)
App.billingSdk:OnPayEndResult("-", payEnd)
print("payEnd", msg)
self.starPay = false
local infoList = string.split(msg, ",")
self:requestPayEnd(infoList[1], infoList[2], tonumber(infoList[3]), self.priceStrGoogle and self.priceStrGoogle[itemId][2] or nil, self.priceStrGoogle and tonumber(self.priceStrGoogle[itemId][3]) or nil, infoList[4], itemId, payCallBack)
end
local detailFail
detailFail = function(result, msg)
print("detailFail", result, msg)
if not result then
App.billingSdk:OnPayEndResult("-", payEnd)
end
self.starPay = false
App.billingSdk:OnDetailsSus("-", detailFail)
end
self:requestPayStart(itemId, addInfo, function ()
App.billingSdk:OnPayEndResult("+", payEnd)
App.billingSdk:OnDetailsSus("+", detailFail)
App.billingSdk:pay(itemId, billingProductType.INAPP, StringUtil.obfuscate(App.playerMgr.data.id, "pay"))
self.starPay = false
end)
end
end
检查补单
function ShopMgr:queryPurchases()
if not self.isQuery then
local addCallBack = function(result, str)
print("onQueryPurchases", str)
local infoList = string.split(str, ",")
local price = self.priceStrGoogle and infoList[5] and self.priceStrGoogle[infoList[5]]
self:requestPayEnd(infoList[1], infoList[3], tonumber(infoList[4]),
price and price[2] or nil, price and tonumber(price[3]) or nil, infoList[2], infoList[5])
end
App.billingSdk:OnQueryPurchasesResult("+", addCallBack)
end
App.billingSdk:queryPurchases(billingProductType.INAPP)
self.isQuery = true
end
获取谷歌商店内价格
function ShopMgr:getProductsByGoogle()
if LuaClass.Application.platform == LuaClass.RuntimePlatform.IPhonePlayer or
not isValid(App.billingSdk) then
return
end
if self.priceStrGoogle == nil then
self.priceStrGoogle = {}
local templates = LuaClass.DirectpurchaseDatatable:getAll()
local idstr = ""
for i = 1,#templates do
idstr = idstr..templates[i].ID..","
end
if idstr then
local callback
callback = function(result,str)
print("getProductsByGoogle:",result,str)
App.billingSdk:OnDetailsSus("-", callback)
if result then
local strSP = string.split(str,";")
for i = 1, #strSP do
local productInfo = string.split(strSP[i], "|-|")
self.priceStrGoogle[productInfo[1]] = {
--格式化后的价格 如:HK$8.00
[1] = productInfo[2],
--货币代码,如HKD
[2] = productInfo[3],
--微单位价格,1,000,000 微单位等于 1 货币单位
[3] = productInfo[4],
}
end
print("productInfo", self.priceStrGoogle)
self:queryPurchases()
end
end
App.billingSdk:OnDetailsSus("+", callback)
App.billingSdk:pay(idstr, billingProductType.INAPP, "")
end
end
end
3. 导出工程打包测试
注意要导apk,并且要带有调试标签(直连手机Build即可),包名和版本号要和Google Play后台上传的包一致,确保测试机只登陆了一个谷歌测试账号
参考
https://blog.51cto.com/kenkao/5989952
https://www.cnblogs.com/fnlingnzb-learner/p/16385685.html
踩坑注意
1.手机语言是法语的话价格会用逗号代替小数点,注意自己使用的分隔符,例如 $1234,56
2.关闭订单这一步操作最好由后端处理,以防客户端因为网络等原因关闭订单后无法通知后端发货
3.在拉起支付时如果需要设置ObfuscatedAccountId的话,请确保每次传输的值都是一样的,否则会出现用户支付遭拒的情况