Android Google支付接入

在App的开发中,支付功能也是比较常见的,国内通常是集成支付宝和微信,但公司的App要上架GooglePlay,所以集成的是Google支付。本篇文章主要介绍一下如何接入Google支付。

之前在SDK项目中接入过4.1.0的Billing库,最近Billing库升级到了5.0.0,看到后我也是升级了。但是发现,如果手机上的GooglePlay商店版本太低,会导致无法正常使用新的API,例如无法正常获取商品信息。根据目前测试的情况来看,商店版本在30以下的都无法通过5.0.0的API获取商品信息。因此,本篇文章会同时介绍4.1.0和5.0.0两个版本的API。

官方文档传送门

集成Billing库

在项目app module的build.gradle中的dependencies中添加依赖:

dependencies {
    //4.1.0和5.0.0选择一种即可
    implementation("com.android.billingclient:billing:4.1.0")
    
    implementation("com.android.billingclient:billing:5.0.0")
}
复制代码

初始化与连接GooglePlay

这一部分两个版本之间的API没有变化,代码如下:

//购买交易更新监听,需要在初始化BillingClient时设置
//所有购买都会回调此监听
PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
    @Override
    public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
        switch (billingResult.getResponseCode()) {
            case BillingClient.BillingResponseCode.OK:
                //购买商品成功
                break;
            case BillingClient.BillingResponseCode.USER_CANCELED:
                //取消购买
                break;
            default:
                //购买失败,具体异常码可以到BillingClient.BillingResponseCode中查看
                break;
        }
    }
};

//初始化BillingClient
BillingClient billingClient = BillingClient.newBuilder(context)
        .setListener(purchasesUpdatedListener)
        //支持待处理的交易
        .enablePendingPurchases()
        .build();

//与GooglePlay连接状态监听
BillingClientStateListener stateListener = new BillingClientStateListener() {
    @Override
    public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
            //连接成功,可以进行查询商品等操作
        }
    }

    @Override
    public void onBillingServiceDisconnected() {
        //连接已经断开,重新连接
        billingClient.startConnection(this);
    }
};

//与GooglePlay建立连接
billingClient.startConnection(stateListener);
复制代码

获取可用的商品

Google支付的商品分为内购(INAPP)和订阅(SUBS)两种类型。获取商品的API在两个版本之间有所不同,下面分别介绍一下。

4.1.0 获取可用商品

代码如下:

//获取商品详情回调
SkuDetailsResponseListener skuDetailsResponseListener=new SkuDetailsResponseListener() {
    @Override
    public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> list) {
        //list为可用商品的集合
    }
};

//查询内购类型的商品
//productId为产品ID(从谷歌后台获取)
ArrayList<String> inAppSkuInfo = new ArrayList<>();
inAppSkuInfo.add("productId");
SkuDetailsParams skuParams = SkuDetailsParams.newBuilder()
        .setType(BillingClient.SkuType.INAPP)
        .setSkusList(inAppSkuInfo)
        .build();
billingClient.querySkuDetailsAsync(skuParams, skuDetailsResponseListener);

//查询订阅类型的商品
//productId为产品ID(从谷歌后台获取)
ArrayList<String> subscriptionSkuInfo = new ArrayList<>();
subscriptionSkuInfo.add("productId");
SkuDetailsParams skuParams = SkuDetailsParams.newBuilder()
        .setType(BillingClient.SkuType.SUBS)
        .setSkusList(subscriptionSkuInfo)
        .build();
billingClient.querySkuDetailsAsync(skuParams, skuDetailsResponseListener);
复制代码

5.0.0 获取可用商品

代码如下:

ProductDetailsResponseListener productDetailsResponseListener = new ProductDetailsResponseListener() {
    @Override
    public void onProductDetailsResponse(@NonNull BillingResult billingResult, @NonNull List<ProductDetails> productDetailsList) {
         //productDetailsList为可用商品的集合
    }
};

//查询内购类型的商品
//设置查询参数方式有所更改,productId为产品ID(从谷歌后台获取)
ArrayList<QueryProductDetailsParams.Product> inAppProductInfo = new ArrayList<>();
inAppProductInfo.add(QueryProductDetailsParams.Product.newBuilder()
        .setProductId("productId")
        .setProductType(BillingClient.ProductType.INAPP)
        .build());
QueryProductDetailsParams productDetailsParams = QueryProductDetailsParams.newBuilder()
        .setProductList(inAppProductInfo)
        .build();
billingClient.queryProductDetailsAsync(productDetailsParams, productDetailsResponseListener);

//查询订阅类型的商品
//设置查询参数方式有所更改,productId为产品ID(从谷歌后台获取)
ArrayList<QueryProductDetailsParams.Product> subscriptionProductInfo = new ArrayList<>();
subscriptionProductInfo.add(QueryProductDetailsParams.Product.newBuilder()
        .setProductId("productId")
        .setProductType(BillingClient.ProductType.SUBS)
        .build());
QueryProductDetailsParams productDetailsParams = QueryProductDetailsParams.newBuilder()
        .setProductList(subscriptionProductInfo)
        .build();
billingClient.queryProductDetailsAsync(productDetailsParams, productDetailsResponseListener);
复制代码

商品价格

获取商品返回的集合中,商品的价格会根据所在的区域换算,但货币符号仍然为$。

1655518028.png

例如上图中的商品,后台配置的价格为1.99美元。在测试机获取商品时,Google支付SDK判定我所在的地区是台湾省,所以返回的金额根据当时美元与台币的汇率进行了计算,但是返回的货币符号仍然是美元符号。如果直接用返回的价格显示,可能会对用户造成困扰。

从上面的图片中可以看到,返回的信息中有货币码(CurrencyCode),我们可以根据货币码来对价格进行优化。

Android SDK中提供了Currency类,可以获取所有的货币码信息,代码如下:

//用map存储,便于匹配
ArrayMap<String, String> currencyArrayMap = new ArrayMap<>();

for (Currency availableCurrency : Currency.getAvailableCurrencies()) {
    //currentcyCode为key,货币符号为value
    //对于没有特定符号的货币,symbol与currencyCode相同。
    currencyArrayMap.put(availableCurrency.getCurrencyCode(), availableCurrency.getSymbol());
}
复制代码

获取商品价格的API两个版本有所差异,下面分别介绍一下。

4.1.0 获取商品价格

在4.1.0版本,内购商品和订阅商品获取商品价格使用统一的API,代码如下:

String replaceCurrencySymbol(String priceStr, String currencyCode, String currencySymbol) {
    if (priceStr.startsWith("$")) {
        if (currencySymbol != null) {
            if (currencySymbol.equals(currencyCode)) {
                //没有货币符号的情况,把货币码拼接到前面
                priceStr = currencySymbol + priceStr;
            } else {
                if (!priceStr.startsWith(currencySymbol)) {
                    priceStr = priceStr.replace("$", currencySymbol);
                }
            }
        }
    }
    return priceStr;
}

for (SkuDetails skuDetail : skuInfoList) {
    String googleProductPrice = skuDetail.getPrice();
    String googleCurrencyCode = skuDetail.getPriceCurrencyCode();
    String currencySymbol = currencyArrayMap.get(googleCurrencyCode) == null ? googleCurrencyCode : currencyArrayMap.get(googleCurrencyCode);
    
    String replacePrice = replaceCurrencySymbol(googleProductPrice, googleCurrencyCode, currencySymbol);
}
复制代码

5.0.0 获取商品价格

在5.0.0版本,内购商品和订阅商品获取商品价格使用不同的API, 代码如下:

String replaceCurrencySymbol(String priceStr, String currencyCode, String currencySymbol) {
    if (priceStr.startsWith("$")) {
        if (currencySymbol != null) {
            if (currencySymbol.equals(currencyCode)) {
                //没有货币符号的情况,把货币码拼接到前面
                priceStr = currencySymbol + priceStr;
            } else {
                if (!priceStr.startsWith(currencySymbol)) {
                    priceStr = priceStr.replace("$", currencySymbol);
                }
            }
        }
    }
    return priceStr;
}

for (ProductDetails productDetail : productDetails) {
    if (BillingClient.ProductType.INAPP.equals(productDetail.getProductType()) && productDetail.getOneTimePurchaseOfferDetails() != null) {
        String googleProductPrice = productDetail.getOneTimePurchaseOfferDetails().getFormattedPrice();
        String googleCurrencyCode = productDetail.getOneTimePurchaseOfferDetails().getPriceCurrencyCode();
        String currencySymbol = currencyArrayMap.get(googleCurrencyCode) == null ? googleCurrencyCode : currencyArrayMap.get(googleCurrencyCode);
        
        String replacePrice = replaceCurrencySymbol(googleProductPrice, googleCurrencyCode, currencySymbol);
    } else if (BillingClient.ProductType.SUBS.equals(productDetail.getProductType()) && productDetail.getSubscriptionOfferDetails() != null) {
        String googleProductPrice = productDetail.getSubscriptionOfferDetails().get(0).getPricingPhases().getPricingPhaseList().get(0).getFormattedPrice();
        String googleCurrencyCode = productDetail.getSubscriptionOfferDetails().get(0).getPricingPhases().getPricingPhaseList().get(0).getPriceCurrencyCode();
        String currencySymbol = currencyArrayMap.get(googleCurrencyCode) == null ? googleCurrencyCode : currencyArrayMap.get(googleCurrencyCode);
        
        String replacePrice = replaceCurrencySymbol(googleProductPrice, googleCurrencyCode, currencySymbol);
    }
}
复制代码

启动购买

需要在主线程中调用launchBillingFlow,启动购买的API在两个版本之间有一些不同,下面分别介绍一下。

4.1.0 启动购买

//将要购买商品的商品详情配置到参数中
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
        .setSkuDetails("SkuDetails")
        .setObfuscatedAccountId(availableGoods.getOrderNum())
        .build();

//启动购买,会BillingResult。
BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);
复制代码

5.0.0 启动购买

ArrayList<BillingFlowParams.ProductDetailsParams> params = new ArrayList<>();
//将要购买商品的商品详情配置到参数中,两种类型的商品有所区别
if (BillingClient.ProductType.SUBS.equals(productDetailInfo.getProductType()) && productDetailInfo.getSubscriptionOfferDetails() != null) {
    params.add(BillingFlowParams.ProductDetailsParams.newBuilder()
            .setProductDetails(productDetailInfo)
            .setOfferToken(productDetailInfo.getSubscriptionOfferDetails().get(0).getOfferToken())
            .build());
} else {
    params.add(BillingFlowParams.ProductDetailsParams.newBuilder()
            .setProductDetails(productDetailInfo)
            .build());
}

BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(params)
        .setObfuscatedAccountId(availableGoods.getOrderNum())
        .build();

//启动购买,会BillingResult。
BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);
复制代码

核销订单

完成购买之后,还需要核销订单。如果三天之内没有核销订单的话, GooglePlay会取消交易,退款给用户。并且商品如果不核销则无法再次购买。

在调用谷歌的核销API之前,还可以通过服务器验证一下交易。

核销订单根据商品类型有不同的API,可重复购买的内购型商品使用consumeAsync,不可重复购买的内购型商品和订阅型商品使用acknowledgePurchase

如果网络通畅,可以在初始化时配置的purchasesUpdatedListener中收到回调并进行核销订单的处理,具体使用代码如下:

//核销回调
ConsumeResponseListener consumeResponseListener=new ConsumeResponseListener() {
    @Override
    public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String purchaseToken) {
        //核销完成后回调
    }
};

//核销回调
AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener=new AcknowledgePurchaseResponseListener() {
    @Override
    public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) {
        //核销完成后回调
    }
};

PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
    @Override
    public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
        switch (billingResult.getResponseCode()) {
            case BillingClient.BillingResponseCode.OK:
                //购买商品成功
                for (Purchase purchase : purchases) {
                    if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
                        //通过服务器验证订单,此处省略
                        //可重复购买的内购商品核销
                        consumePurchase(purchase.getPurchaseToken());
                        //不可重复购买的内购商品、订阅商品核销
                        acknowledgedPurchase(purchase.getPurchaseToken());
                    }
                }
                break;
            case BillingClient.BillingResponseCode.USER_CANCELED:
                //取消购买
                break;
            default:
                //购买失败,具体异常码可以到BillingClient.BillingResponseCode中查看
                break;
        }
    }
};

void consumePurchase(String purchaseToken) {
    if (billingClient != null && billingClient.isReady()) {
        ConsumeParams consumeParams = ConsumeParams.newBuilder()
                .setPurchaseToken(purchaseToken)
                .build();
        billingClient.consumeAsync(consumeParams, consumeResponseListener);
    }
}

void acknowledgedPurchase(String purchaseToken) {
    if (billingClient != null && billingClient.isReady()) {
        AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                .setPurchaseToken(purchaseToken)
                .build();
        billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
    }
}
复制代码

查询购买交易

为了避免在异常情况下(例如网络出现问题),无法通过purchasesUpdatedListener核销订单,可以使用queryPurchasesAsync来查询购买交易。

查询购买交易的API在两个版本之间有一些不同,下面分别介绍一下。

4.1.0 查询购买交易

PurchasesResponseListener purchasesResponseListener =new PurchasesResponseListener() {
    @Override
    public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> list) {
        //list为购买交易的集合
    }
};

//内购商品交易查询
billingClient.queryPurchasesAsync(BillingClient.SkuType.INAPP, purchasesResponseListener);

//订阅商品交易查询
billingClient.queryPurchasesAsync(BillingClient.SkuType.SUBS, purchasesResponseListener);
复制代码

5.0.0 查询购买交易

PurchasesResponseListener purchasesResponseListener =new PurchasesResponseListener() {
    @Override
    public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, @NonNull List<Purchase> list) {
        //list为购买交易的集合
    }
};

//内购商品交易查询
QueryPurchasesParams inAppPurchasesQurey = QueryPurchasesParams.newBuilder()
    .setProductType(BillingClient.ProductType.INAPP)
    .build()
billingClient.queryPurchasesAsync(inAppPurchasesQurey, inAppPurchasesResponseListener);

//订阅商品交易查询
QueryPurchasesParams inAppPurchasesQurey = QueryPurchasesParams.newBuilder()
    .setProductType(BillingClient.ProductType.SUBS)
    .build()
billingClient.queryPurchasesAsync(inAppPurchasesQurey, inAppPurchasesResponseListener);
复制代码

测试支付

到GooglePlay后台创建商品,需要注意的是要创建商品必须先发布一个带有Billing库的aab到GooglePlay(测试渠道即可)。

1655525847226.png

获取并配置好商品id之后,将测试用的aab发布到内部测试,并通过连接分享给测试人员。

1655526170422.png

测试人员接受邀请后,就可以测试支付。

1655526274391.png

如果想要用测试卡支付,还需要在许可测试中添加测试人员的谷歌账号。

1655526424651.png

猜你喜欢

转载自juejin.im/post/7110434269155033118