Unity客户端接入原生Google支付

news2024/9/22 5:41:10

Unity客户端接入原生Google支付

  • 1. Google后台配置
  • 2. 开始接入
    • Java部分
    • C#部分
    • Lua部分
  • 3. 导出工程打包测试
  • 参考
  • 踩坑注意

1. Google后台配置

  1. 找到内部测试(这个测试轨道过审最快),打包上传,这个包不需要接入支付,如果已经有上传过包了那就跳过这一步
    在这里插入图片描述

  2. 在许可测试里添加测试人员,勾选测试人员列表,并且设置许可相应为LICENSED,这样才可以使用测试卡测试支付
    在这里插入图片描述

  3. 确认已经添加了付款方式,以及开放地区有香港,否则可能需要挂VPN才能进行支付流程测试
    在这里插入图片描述
    在这里插入图片描述

  4. 流程图
    在这里插入图片描述

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的话,请确保每次传输的值都是一样的,否则会出现用户支付遭拒的情况
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1935531.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器人开源调度系统OpenTcs6-架构运行分析

系统启动 启动 Kernel&#xff1a;加载核心应用&#xff0c;初始化系统配置和状态。 启动 Plant Overview&#xff1a;加载图形用户界面&#xff0c;初始化模型和用户界面。 模型导入和配置 在 Plant Overview 中导入或创建工厂布局模型。 配置路径、位置和车辆信息。 车辆连…

用DrissionPage过某里滑块分析

最近我又在找工作了&#xff0c;悲哀啊~&#xff0c;面试官给了一道题&#xff0c;要求如下&#xff1a; 爬虫机试&#xff1a;https://detail.1688.com/offer/643272204627.html 过该链接的滑动验证码&#xff0c;拿到正确的商品信息页html&#xff0c;提取出商品维度的信息&a…

排序一次讲清(从冒泡到基数)

文章目录 冒泡原理代码pythonc 选择原理代码pythonc 插入原理代码pythonc 希尔原理代码pythonc 快排原理代码pythonc 归并原理代码pythonc 堆原理代码pythonc 计数原理代码pythonc 桶原理代码pythonc 基数原理代码pythonc 【待更新】 冒泡 原理 如果我们想要让数组从左至右从…

海豚调度器(DolphinScheduler)集群搭建详细笔记

海豚调度器集群搭建笔记 1.DolphinScheduler Cluster部署1.1 集群部署规划1.2 集群准备工作1.3 初始化数据库1.4 修改安装环境配置1.5 安装DolphinScheduler1.6 启停命令1.7 登录 DolphinScheduler UI 1.DolphinScheduler Cluster部署 分布式去中心化易扩展的工作流任务调度系…

【最强八股文 -- 计算机网络】TCP 四次挥手的过程及原因

第一次挥手&#xff1a;FIN 报文第二次挥手&#xff1a;ACK 报文第三次挥手&#xff1a;FIN 报文第四次挥手&#xff1a;ACK 报文 为什么需要四次挥手&#xff1f; 为什么需要 TIME_WAIT 状态&#xff1f; TIME_WAIT 的时间为什么是 2MSL&#xff1f;

springboot服务如何执行sql脚本文件

当sql脚本文件包含不同数据库实例sql时&#xff0c;遍历读取sql文件再插入时&#xff0c;由于是不同的数据库实例这种方式就不行了&#xff0c;这时就需要程序直接执行sql脚本。 springboot执行sql脚本 /*** 执行sql脚本* throws SQLException*/ private void executeSqlScri…

go-zero框架入门

go-zero框架环境的安装 goctl 若想用go-zero框架&#xff0c;还需要一些前置条件&#xff1a; 安装goctl go install github.com/zeromicro/go-zero/tools/goctllatest可以使用 goctl 命令查看是否安装成功 成功后安装protoc goctl env check --install --verbose --force…

重生奇迹MU 三代翅膀行情

在重生奇迹MU游戏中&#xff0c;达到400级以上的玩家都知道&#xff0c;重生奇迹大陆拍卖行里最值钱的物品是翅膀。翅膀可以分为一代、二代和三代翅膀&#xff0c;而其中价格最高的则是三代翅膀。有时候&#xff0c;三代翅膀的售价非常之高&#xff0c;甚至有市无价。这是因为三…

[论文笔记] CT数据配比方法论——1、Motivation

我正在写这方面的论文,感兴趣的可以和我一起讨论!!!!!! Motivation 1、探测原有模型的配比: 配比 与 ppl, loss, bpw, benchmark等指标 之间的关系。 2、效果稳定的配比:配比 与 模型效果 之间的规律。 Experiments 1、主语言(什么语言作为主语言,几种主语言?…

PyTorch 深度学习实践-逻辑斯蒂回归

视频指路 参考博客笔记 参考笔记二 用来分类的模型 说明&#xff1a;1、 逻辑斯蒂回归和线性模型的明显区别是在线性模型的后面&#xff0c;添加了激活函数(非线性变换) ​ 2、分布的差异&#xff1a;KL散度&#xff0c;cross-entropy交叉熵 现在损失函数衡量不是距离而是分布…

Jmeter性能测试(九)

一、Jmeter性能测试需要特别注意的地方 1、参数化 2、请求参数 3、BeanShell 预处理程序更新jmeter请求参数 4、接口中不可重复的随机数处理 5、线程组设置 6、总结 二、参数化 1、参数化配置,多个参数用英文逗号隔开 2、wallet参数化文件,不要写表头,多个参数用英文逗号…

【YOLOv8改进[Conv]】KAN系列 |使用KACNConv改进C2f + 含全部代码和详细修改方式 + 手撕结构图

本文将进行在YOLOv8中使用KACNConv改进C2f 的实践,助力YOLOv8目标检测效果,文中含全部代码、详细修改方式以及手撕结构图。助您轻松理解改进的方法。训练速度会慢一些,要有心理准备哈! 改进前和改进后的参数对比: 目录

《基于 Kafka + Quartz 实现时限质控方案》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

玳数科技集成 Flink CDC 3.0 的实践

摘要&#xff1a;本文投稿自玳数科技工程师杨槐老师&#xff0c;介绍了 Flink CDC 3.0 与 ChunJun 框架在玳数科技的集成实践。主要分为以下六个内容&#xff1a; 背景技术选型架构设计挑战与解决方案上线效果未来规划 1. 背景 玳数科技对内外部用户提供了一站式的数据开发治理…

【BUG】已解决:error: subprocess-exited-with-error

已解决&#xff1a;error: subprocess-exited-with-error 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷分享知识&#xff0c;武汉城市开发者社区主…

【Vue】深入解析 Vue 生命周期:从创建到销毁的完整流程

文章目录 一、Vue 生命周期概述二、创建阶段1. beforeCreate 钩子2. created 钩子 三、挂载阶段1. beforeMount 钩子2. mounted 钩子 四、更新阶段1. beforeUpdate 钩子2. updated 钩子 五、销毁阶段1. beforeDestroy 钩子2. destroyed 钩子 六、Vue 3 的生命周期钩子变化七、生…

PHP pwn 学习 (2)

文章目录 A. 逆向分析A.1 基本数据获取A.2 函数逆向zif_addHackerzif_removeHackerzif_displayHackerzif_editHacker A.3 PHP 内存分配 A.4 漏洞挖掘B. 漏洞利用B.1 PHP调试B.2 exp 上一篇blog中&#xff0c;我们学习了一些PHP extension for C的基本内容&#xff0c;下面结合一…

软件著作权申请教程(超详细)(2024新版)软著申请

目录 一、注册账号与实名登记 二、材料准备 三、申请步骤 1.办理身份 2.软件申请信息 3.软件开发信息 4.软件功能与特点 5.填报完成 一、注册账号与实名登记 首先我们需要在官网里面注册一个账号&#xff0c;并且完成实名认证&#xff0c;一般是注册【个人】的身份。中…

STM32智能城市交通管理系统教程

目录 引言环境准备智能城市交通管理系统基础代码实现&#xff1a;实现智能城市交通管理系统 4.1 数据采集模块 4.2 数据处理与控制模块 4.3 通信与网络系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;城市交通管理与优化问题解决方案与优化收尾与总结 1. 引言 智能城…

HTTP协议、Wireshark抓包工具、json解析、天气爬虫

HTTP超文本传输协议 HTTP&#xff08;Hyper Text Transfer Protocol&#xff09;&#xff1a; 全称超文本传输协议&#xff0c;是用于从万维网&#xff08;WWW:World Wide Web &#xff09;服务器传输超文本到本地浏览器的传送协议。 HTTP 协议的重要特点&#xff1a; 一发一收…