1. 前言
这段时间在做安卓的海外项目,需要用到谷歌的结算库。这里对整个集成流程、重要的点和踩过的坑做一个总结。
2. 介绍
谷歌支付是Google Paly 提供的对应用内商品结算的服务。可通过Gradle远程依赖谷歌结算库后使用。
3. 使用
3.1 前置条件准备
在集成结算库之前,需要准备好以下东西:
- 能够通过VPN等方式科学上网。
- 谷歌开发者账号
- 发布应用(正式版或者内测版)并创建好应用内商品。
发布应用和创建商品步骤较多, 这里详细说下。 我们最终的目的是能够将应用发布到测试版或者是正式版。只有当要发布的应用的所有信息填写完毕,所有对勾都变绿以后。才能发布版本。填写没有顺序,比如填写定价和分发范围的时候,要先上传一个apk才能继续填写, 那就先在应用版本传一个apk再去继续填写,耐心点按照提示来就可以了。 创建商品在应用内商品 里面添加。 内购型商品在受管理商品创建,订阅型产品在订阅里面创建。 所以信息填写好,就可以到 应用版本->管理->查看->发布到XX版来发布。
3.2 集成谷歌结算库
主要参考官方文档,如果可以科学上网尽量多看官方文档,耐心看下去会少踩很多坑。 集成很简单, 直接依赖就好了:
implementation 'com.android.billingclient:billing:2.0.3'
3.3 使用
整个支付过程在安卓客户端这里分为5步:
- 建立连接
- 查询商品
- 调起支付
- 支付结果回调
- 消费商品
整个过程走下来就是, 先与Google Play 建立连接, 通过商品id来查询本应用是否存在该商品,存在则去支付,如果支付成功了,就消费此次商品。 消费成功后客户端需要去和服务端校验此次支付。
支付工具类封装如下:
public class GooglePayUtils implements PurchasesUpdatedListener {
String TAG = ".GooglePayUtils";
private BillingClient mBillingClient = null;
//是否已经建立连接
private boolean isClientInit = false;
//支付结果回调接口
private MyListener mListener = null;
private Context mContext = null;
private String orderId = "";
private String purchaseId = "";
//购买类型:内购、 订阅, 默认为内购
private String purchaseType = BillingClient.SkuType.INAPP;
public GooglePayUtils(Context context, MyListener mListener, String orderId, String purchaseId, String purchaseType) {
this.mContext = context;
this.mListener = mListener;
this.orderId = orderId;
this.purchaseId = purchaseId;
this.purchaseType = purchaseType;
}
// 1. 建立连接
public GooglePayUtils pay() {
mBillingClient = BillingClient.newBuilder(mContext).enablePendingPurchases().setListener(this).build();
mBillingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
Log.e(TAG, "onBillingSetupFinished code = " + billingResult.getResponseCode() + " , msg = " + billingResult.getDebugMessage());
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
isClientInit = true;
queryAndPayPurchases(purchaseId);
}
}
@Override
public void onBillingServiceDisconnected() {
isClientInit = false;
}
});
return this;
}
// 2. 查询商品信息
private void queryAndPayPurchases(@NonNull final String purchaseId) {
if (!isClientInit) {
if (mListener != null) {
mListener.onError(mContext.getResources().getString(R.string.not_connect));
}
return;
}
List<String> skuList = new ArrayList<>();
skuList.add(purchaseId);
// skuList.add("xxx"); // 这个参数不能为空,值随便传
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(purchaseType);
mBillingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
Log.e(TAG, "onSkuDetailsResponse code = " + billingResult.getResponseCode() + " , msg = " + billingResult.getDebugMessage() + " , skuDetailsList = " + skuDetailsList);
// Process the result.
if (billingResult.getResponseCode()!= BillingClient.BillingResponseCode.OK) {
onFail(mContext.getResources().getString(R.string.no_goods));
return;
}
if (skuDetailsList == null || skuDetailsList.isEmpty()) {
if (mListener != null) {
mListener.onError(mContext.getResources().getString(R.string.no_goods));
}
return;
}
SkuDetails skuDetails = null;
for (SkuDetails details : skuDetailsList) {
Log.e(TAG, "onSkuDetailsResponse skuDetails = " + details.toString());
if (purchaseId.equals(details.getSku())) {
skuDetails = details;
}
}
if (skuDetails != null) {
pay(skuDetails);
} else {
if (mListener != null) {
mListener.onError(mContext.getResources().getString(R.string.no_goods));
}
}
}
});
}
//3. 调起支付
private void pay(SkuDetails skuDetails) {
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
int code = mBillingClient.launchBillingFlow((Activity) mContext, flowParams).getResponseCode();
if (BillingClient.BillingResponseCode.OK!=code) {
onFail(mContext.getResources().getString(R.string.dump_pay_faile));
}
}
//4. 支付回调
@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchases) {
Log.e(TAG, "onPurchasesUpdated code = " + billingResult.getResponseCode() + " , msg = " + billingResult.getDebugMessage());
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
if (mListener != null) {
mListener.onError(mContext.getResources().getString(R.string.pay_cancle));
}
} else {
// Handle any other error codes.
if (mListener != null) {
mListener.onError(mContext.getResources().getString(R.string.pay_faile));
}
}
}
//5. 支付成功, 去消费此次支付, 支付成功后 不消费会自动退款。
private void handlePurchase(Purchase purchase) {
// if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
// }
if (!purchase.isAcknowledged()) {
if(BillingClient.SkuType.INAPP.equals(purchaseType)){
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(orderId)
.build();
mBillingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
Log.e(TAG, "onConsumeResponse code = " + billingResult.getResponseCode() + " , msg = " + billingResult.getDebugMessage() + " , purchaseToken = " + purchase.getPurchaseToken()); // 消费成功 处理自己的流程,我选择先存入数据库
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (mListener != null) {
mListener.onSuccess(purchase.getPurchaseToken());
}
} else {
// 消费失败,后面查询消费记录后再次消费,否则,就只能等待退款
if (mListener != null) {
mListener.onError(mContext.getResources().getString(R.string.pay_success_comfirm_faile));
}
}
}
});
}else {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken()).setDeveloperPayload(orderId)
.build();
mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
Log.e(TAG, "onConsumeResponse code = " + billingResult.getResponseCode() + " , msg = " + billingResult.getDebugMessage() + " , purchaseToken = " + purchase.getPurchaseToken()); // 消费成功 处理自己的流程,我选择先存入数据库
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
if (mListener != null) {
mListener.onSuccess(purchase.getPurchaseToken());
}
} else {
// 消费失败,后面查询消费记录后再次消费,否则,就只能等待退款
if (mListener != null) {
mListener.onError(mContext.getResources().getString(R.string.pay_success_comfirm_faile));
}
}
}
});}
} else {
if (mListener != null) {
mListener.onError(mContext.getResources().getString(R.string.pay_success_comfirm_faile));
}
}
}
public interface MyListener {
void onSuccess(String purchaseToken);
void onError(String msg);
}
private void onFail(String str){
if (mListener != null) {
mListener.onError(str);
}
}
}
3.4 测试
代码编写完了,要做的就是测试。 测试购买分两种, 一种是消耗性的也就是购买类型是内购。一种是非消耗性型,购买类型是订阅。 要测试内购,直接传结算库里面几个预设好拿来测试的商品id就行了,在官方文档中查看。如果是订阅, 则需要应用发布到正式版才能够测试。第一次发布后,审核的时间会比较久,3-4天左右。 页面上显示正在处理更新就是还在审核中。
- 添加测试账号
只有在控制台添加谷歌测试账号才能够用这个账号来测试支付。在这里添加测试账号: 控制台->开发者账号->账号详情->可用于测试的 Gmail帐号
这些都准备好了, 就可以传商品id和商品类型到上面的支付工具类中测试支付了。
测试过程中有哪些坑:
-
没有科学上网, 导致查询不到商品。
-
手机登录的不是刚刚添加的测试账号,导致调起支付框,但提示无法支付之类的信息。在手机->设置->账号/同步 -> Google账号 里查看。
-
版本号verionCode 、应用标识applicationId、签名等要保持和控制台上传的一致。
-
订阅后是自动续订的, 要通过谷歌发送的续订邮件中找到退订入口, 退订后才能继续测试订阅支付。 不然提示,系统正在处理你的订单,稍后再试。
4.总结
- 在Google Play 控制台创建应用、应用内商品。
- 发布应用到内测版或者正式版(测试订阅需要)。
- 参照官方文档集成结算库、封装好支付工具类。
- 在Google Play 控制台添加测试账号,用这个账号来测试支付。