谷歌支付
1.流程
2.谷歌后台API配置
启用API
网址:https://play.google.com/console/u/0/developers/6637358084117674478/api-access
进入谷歌管理后台,当未启动时需要启动对应API,随后会提示去创建服务账号
创建服务账号
网址:https://console.developers.google.com/iam-admin/serviceaccounts/create?project=wewatch-98a41
选中应用(例如选择wewatch)>> 输入服务账号名称 >> 输入服务账号ID(这时候会有提示 应用名-应用ID.@appspot.gserviceaccount.com,注意核实应用是否一致) >>当创建完毕之后会得到一个p12文件,需要提供给后台,同时会需要记录下对应p12文件的密码
查看对应用户以及权限
网址:https://console.cloud.google.com/iam-admin/iam?project=wewatch-98a41
当创建成功时,可以到IAM中查看对应的账号权限,为Editor权限
2.开启消息通知(Cloud Pub/Sub)
定义:Cloud Pub/Sub是一种托管式的消息服务,当应用购买对于商品成功时,就会去找到对应的谷歌服务用户,去通知其状态的改变。
开启Cloud Pub/Sub网址:https://cloud.google.com/pubsub/
点击转到控制台,会到达该网址:https://console.cloud.google.com/cloudpubsub/topic/list?folder=&organizationId=&project=wewatch-98a41
创建主题
当点击创建主题时,会要求输入对应的主题名称
创建成功后,会出现对应的主题ID和主题名称,名称是用于测试的,选择最右边的编辑按钮,或者直接点击左侧的订阅专栏,都能为主题创建订阅
创建订阅
进入之后输入对应的订阅名称,选择传送类型为拉取,用于测试主题消息的连通性,其余选项选择默认,当创建成功时会进入订阅界面,这时候复制对应的主题名称到测试页面。正式订阅时,需要改为推送,这里拉取只是测试连通性。
权限授予和校验流程
原理图:
首先进入Google play console >> 选择应用 >> 选择获利设置
当进入该页面以后,输入刚刚复制的主题名称
当权限失败时就会出现该情况,这时候需要检查主题和订阅的权限。
1.给谷歌云消息账号添加推送的权限,例如:签名是应用的ID,后面格式一致91804594755@cloudservices.gserviceaccount.com
点击添加成员后,输入账号,并给予推送权限
2.给订阅者添加权限
与主题添加权限一致,这里的账号添加的是谷歌API后台账号,即一开始我们创建谷歌后台账号,到此之后,便能确保当谷歌支付有状态改变时,谷歌能及时通知到后台,去对订单做对应的处理。
3.谷歌应用测试人员配置
谷歌控制台的测试人员配置
在选项中输入对顶的gmail邮箱
进入应用配置测试人员
点击应用>> 测试 >> 内部测试
点击图上的右键头,输入对应的邮箱地址,随后将底部的复制链接,发送给对应的谷歌账号,同意授权之后,需要等待一段时间,一般在24小时之内,该账号就有权限。校验测试购买的权限之一,就是进入到Play store商店中,查找对应的APP名字,如果能查到就有对应权限
测试补充说明
1.手机或平板需要有对应的谷歌服务、谷歌商店、稳定的vpn,才能顺利购买,通常可以使用Google Go三方软件进行配置检测,华为magic系统新手机除外
2.对于测试前提,需要上架对应的应用,当前上架应用需要审核,审核时间为7天,只有应用审核通过之后,才能进行以下测试
3.对于测试周期,订阅的周期测试为5分钟,即自动续订是每5分钟一次,会续订6次,对于消费包,没有限制。对于订阅的升降级,谷歌内部有提供多种结算方式:
BillingFlowParams.IMMEDIATE_WITH_TIME_PRORATION(立即生效,购买的新套餐首月价格会减去生效包剩余价格)
BillingFlowParams.DEFERRED(下一个结算周期生效)
其余结算方式请查看该网址: https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode
4.订阅或消费商品的创建
点击应用>> 商品 >> 应用内商品
消耗商品配置
该页面为消费商品,即一次性商品,通过最右侧的创建商品,可以创建对应的商品信息
输入产品ID和对应名称,点击最底下的创建模板
网址: https://play.google.com/console/u/0/developers/6637358084117674478/pricing-templates
该模板可以选择不同国家,并且根据实际汇率进行价格转换
选择完价格之后,对商品进行保存
订阅商品配置
点击创建订阅内容
订阅相比消耗,多了结算周期,和首订等功能添加,最后需要点击右下角的保存按钮,便可以生效。
5.安卓终端支付对接
a.逻辑图
订阅流程:
1.判断是否为预付费用户
2.判断是否为订阅,默认未订阅
3.判断当前用户是否存在订阅套餐,若不是Google方式购买则不允许购买,若是则继续
4.判断已购买的套餐,对应的谷歌订单中的用户是否为同一用户,如果不是则不允许购买,若是则继续
5.每次购买前会查询谷歌订单,用于补单处理
6.访问后台API创建对应的订单
7.调用谷歌的订阅购买
8.调用谷歌的消费接口
9.将订单信息保存到本地中,并进行加密处理,防止漏单情况
10.将对应订单信息回调后台,通知后台去开通订单,当返回成功则,删除订单信息,若失败或存储,在应用启动时进行补单操作
11.当升级订阅时,会立马提示刷新订阅界面,当降级时,会有通知弹窗告知用户下个结算周期生效,最终完成订单处理
消费流程:
1.判断是否为预付费用户
2.判断是否为消费包
3.提示支付方式的弹窗
4.支付弹窗选择谷歌支付
5.访问后台API创建对应的订单
6.调用谷歌消费包购买
7.调用谷歌的消费接口
8.将订单信息保存到本地中,并进行加密处理,防止漏单情况
9.将对应订单信息回调后台,通知后台去开通订单,当返回成功则,删除订单信息,若失败或存储,在应用启动时进行补单操作
10.当购买成功时,会立马提示刷新界面,并将消费包按钮置与不可点击状态
b.谷歌API对应的提示码
c.代码
官方文档网址:https://developer.android.google.cn/google/play/billing/billing_library_overview
1.GoogleUtils.java和PayUtils.kt
public class GooglePayUtils implements PurchasesUpdatedListener {
//持弱引用仿内存泄露
private WeakReference<Context> weakContext;
private BillingClient billingClient;
private static final String TAG = "GooglePayUtils";
public GooglePayUtils(Context context) {
weakContext = new WeakReference<>(context);
}
public void init() {
//为Utils添加产品包更新回调监听 PurchasesUpdatedListener
billingClient = BillingClient.newBuilder(getContext()).setListener(this).enablePendingPurchases().build();
//开始连接谷歌服务
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
LogUtil.e(TAG, "init Success");
if (payStateListener != null) {
payStateListener.onConnectionSuccess();
}
} else {
XMToastUtil.showShortToastCenter(billingResult.getDebugMessage(), getContext());
LogUtil.e(TAG, "init fail");
if (payStateListener != null && getContext() instanceof Activity) {
Intent intent = new Intent();
intent.putExtra("data", "fail");
((Activity) getContext()).setResult(Activity.RESULT_CANCELED, intent);
((Activity) getContext()).finish();
}
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
});
}
@Override
public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchaseList) {
if (payStateListener != null)
payStateListener.onPurchasesUpdated(billingResult, purchaseList);
}
/**
* 查询已购买的产品
*/
public void queryHavePurchased() {
Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
if (areSubscriptionsSupported()) {
Purchase.PurchasesResult subscriptionResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
if (subscriptionResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
purchasesResult.getPurchasesList().addAll(subscriptionResult.getPurchasesList());
} else {
LogUtil.e(TAG, "Got an error response trying to query subscription purchases");
}
}
LogUtil.e(TAG, "queryHavePurchased: " + purchasesResult.getPurchasesList().toString());
//payStateListener.onQueryPurchasedSuccess(purchasesResult);
}
/**
* @return 是否有可订阅的商品
*/
private boolean areSubscriptionsSupported() {
BillingResult result = billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS);
int responseCode = result.getResponseCode();
if (responseCode != BillingClient.BillingResponseCode.OK) {
LogUtil.e(TAG, "areSubscriptionsSupported() got an error response: " + responseCode);
}
return responseCode == BillingClient.BillingResponseCode.OK;
}
/**
* 下单购买消耗性商品的sku
*
* @param sku 产品的sku数组
*/
public void purchase(String sku) {
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getContext()) != 0) {
XMToastUtil.showShortToastCenter("your phone can't support google service", getContext());
return;
}
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
List<String> skuList = new ArrayList<>();
skuList.add(sku);
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> list) {
if (list == null || list.size() == 0) {
Toast.makeText(getContext(), "not match" + billingResult.getDebugMessage() + billingResult.getResponseCode(), Toast.LENGTH_SHORT).show();
if (payStateListener != null)
payStateListener.onPurchasesFail(sku, billingResult.getResponseCode());
return;
}
if (list == null) return;
for (int i = 0; i < list.size(); i++) {
SkuDetails skuDetails = list.get(i);
BillingFlowParams flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
int code = billingClient.launchBillingFlow(getActivity(getContext()), flowParams).getResponseCode();
if (code != BillingClient.BillingResponseCode.OK && payStateListener != null) {
Toast.makeText(getContext(), "purchase fail:" + code, Toast.LENGTH_SHORT).show();
payStateListener.onPurchasesFail(skuDetails.getTitle(), code);
}
/*if (code == BillingClient.BillingResponseCode.OK && payStateListener != null) {
payStateListener.onSubscribeSuccess(ProductType.NORMAL);
}*/
}
}
});
}
/**
* @description 消耗所有可查询的包
* @param
*/
public void consumePurchase() {
Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
List<Purchase> purchasesList = purchasesResult.getPurchasesList();
if (purchasesList == null) return;
for (int i = 0; i < purchasesList.size(); i++) {
Purchase purchase = purchasesList.get(i);
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
LogUtil.e(">>>>onConsumeResponse:", "message:" + billingResult.getDebugMessage() + billingResult.getResponseCode());
payStateListener.onConsumeFinished(purchase);
}
});
}
}
private Purchase currentSusProduct;
/**
* @description 订阅包的购买
* @param newSku 对应的商品id
* @param productType 具体升降级类型
*/
public void subscribe(String newSku, ProductType productType) {
LogUtil.e(TAG, "newSku:" + newSku);
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getContext()) != 0) {
XMToastUtil.showShortToastCenter("your phone can't support google service", getContext());
return;
}
ArrayList<String> skuList = new ArrayList<>();
skuList.add(newSku);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> list) {
Purchase.PurchasesResult subscriptionResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
//LogUtil.e(TAG, "当前订阅的套餐:" + subscriptionResult.getPurchasesList().toString());
if (subscriptionResult.getPurchasesList() != null && !subscriptionResult.getPurchasesList().isEmpty()) {
currentSusProduct = subscriptionResult.getPurchasesList().get(0);
}
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) {
Toast.makeText(getContext(), "please check you network", Toast.LENGTH_SHORT).show();
return;
}
if (list == null || list.size() == 0) {
Toast.makeText(getContext(), "not match", Toast.LENGTH_SHORT).show();
return;
}
for (int i = 0; i < list.size(); i++) {
SkuDetails skuDetails = list.get(i);
BillingFlowParams.Builder builder;
//没有订阅记录
if (currentSusProduct == null) {
//没有有效期内的订阅记录
builder = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails);
} else {
//有订阅记录,根据type判断是升级还是降级
builder = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.setReplaceSkusProrationMode(productType == ProductType.UPGRADE ? IMMEDIATE_WITH_TIME_PRORATION : DEFERRED)
.setOldSku(currentSusProduct.getSku(), currentSusProduct.getPurchaseToken());
}
int code = billingClient.launchBillingFlow(getActivity(getContext()), builder.build()).getResponseCode();
if (code != BillingClient.BillingResponseCode.OK && payStateListener != null) {
payStateListener.onPurchasesFail(skuDetails.getTitle(), code);
}
/*if (code == BillingClient.BillingResponseCode.OK && payStateListener != null) {
payStateListener.onSubscribeSuccess(productType);
}*/
}
}
});
}
private Context getContext() {
return weakContext.get();
}
private static Activity getActivity(Context context) {
return context == null ? null : (context instanceof Activity ? (Activity) context : (context instanceof ContextWrapper ? getActivity(((ContextWrapper) context).getBaseContext()) : null));
}
/**
* 消耗购买的内购产品
*
* @param purchase 要消费掉的产品类
* @param purchaseType 产品类型
*/
public void consumePurchase(Purchase purchase, String purchaseType) {
LogUtil.e(TAG, "consumeAsync");
if (purchaseType.equals(BillingClient.SkuType.INAPP)) {
ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.setDeveloperPayload(purchase.getDeveloperPayload())
.build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
payStateListener.onConsumeFinished(purchase);
}
});
} else if (purchaseType.equals(BillingClient.SkuType.SUBS)) {
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged()) {
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
@Override
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
payStateListener.onConsumeFinished(purchase);
}
}
});
} else {
payStateListener.onConsumeFinished(purchase);
}
}
}
}
private GooglePayStateListener payStateListener;
/**
* @param payStateListener google pay支付相关回调额数据
*/
public void setGooglePayStateListener(GooglePayStateListener payStateListener) {
this.payStateListener = payStateListener;
}
/**
* 跳转到google play订阅界面
*
* @param context
*/
public static void jump2GooglePlaySub(Context context) {
Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
Uri content_url = Uri.parse("https://play.google.com/store/account/subscriptions");
intent.setData(content_url);
context.startActivity(intent);
}
/**
* 跳转到google play订阅指定产品id界面
*
* @param context
*/
public static void jump2GooglePlaySubById(Context context, String productId) {
Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
Uri content_url = Uri.parse("https://play.google.com/store/account/subscriptions?sku=" + productId + "&package=" + context.getPackageName());
intent.setData(content_url);
context.startActivity(intent);
}
public void endConnection() {
if (billingClient != null && billingClient.isReady()) {
billingClient.endConnection();
}
}
public void dismissConnect() {
if (billingClient != null) billingClient.endConnection();
}
}
class PayUtils private constructor(context: Context) : PurchasesUpdatedListener {
private val context: Context? = WeakReference(context).get()
private var currentSusProduct: Purchase? = null
/**
* @description 使用by lazy方式,在第一次使用的时候就会进行初始化
*/
private val billingClient by lazy {
BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
}
fun init() {
//进行谷歌服务器连接监听
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
LogUtil.e(TAG, "init Success")
payStateListener?.onConnectionSuccess()
} else {
LogUtil.e(TAG, "init Fail")
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
}
/**
* @description 监听谷歌包发生改变,在by lazy时就做了监听处理
* @param billingResult 购买包购买结果
* @param purchaseList 为空代表降级,不为空则为正常购买或升级
*/
override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: List<Purchase>?) {
payStateListener?.onPurchasesUpdated(billingResult, purchaseList)
}
/**
* 查询已购买的产品,通常用于补单或购买前校验
*/
fun queryHavePurchased() {
val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
if (areSubscriptionsSupported()) {
val subscriptionResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS)
if (subscriptionResult.responseCode == BillingClient.BillingResponseCode.OK) {
purchasesResult.purchasesList.addAll(subscriptionResult.purchasesList)
} else {
LogUtil.e(TAG, "Got an error response trying to query subscription purchases")
}
payStateListener?.onQueryPurchasedSuccess(subscriptionResult)
}
LogUtil.e(TAG, "queryHavePurchased: " + purchasesResult.purchasesList.toString())
}
/**
* @return 是否有可订阅的商品
*/
private fun areSubscriptionsSupported(): Boolean {
val result = billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS)
val responseCode = result.responseCode
if (responseCode != BillingClient.BillingResponseCode.OK) {
LogUtil.e(TAG, "areSubscriptionsSupported() got an error response: $responseCode")
}
return responseCode == BillingClient.BillingResponseCode.OK
}
/**
* 下单购买消耗性商品的sku
*
* @param sku 产品的sku数组
*/
fun purchase(sku: String) {
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != 0) {
XMToastUtil.showShortToastCenter(context!!.getString(R.string.pay_google_device_not_support),
context)
return
}
val params = SkuDetailsParams.newBuilder()
val skuList: MutableList<String> = ArrayList()
skuList.add(sku)
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP)
billingClient.querySkuDetailsAsync(params.build(),
SkuDetailsResponseListener { billingResult, list ->
if (list == null || list.size == 0) {
LogUtil.e(TAG,
"not match" + billingResult.debugMessage + billingResult.responseCode)
XMToastUtil.showShortToastCenter(context!!.getString(R.string.pay_google_no_package),
context)
payStateListener?.onPurchasesFail(sku,
BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED)
return@SkuDetailsResponseListener
}
if (list == null) return@SkuDetailsResponseListener
for (i in list.indices) {
val skuDetails = list[i]
val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build()
val code =
billingClient.launchBillingFlow(getActivity(context),
flowParams).responseCode
if (code != BillingClient.BillingResponseCode.OK) {
payStateListener?.onPurchasesFail(skuDetails.title, code)
}
/* if (code == BillingClient.BillingResponseCode.OK) {
payStateListener?.onSubscribeSuccess(ProductType.NORMAL)
}*/
}
})
}
/**
* @description 消费所有未消费的产品包,含订阅消费包
*/
fun consumePurchase() {
val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
val purchasesList = purchasesResult.purchasesList ?: return
for (i in purchasesList.indices) {
val purchase = purchasesList[i]
val consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.setDeveloperPayload(purchase.developerPayload)
.build()
billingClient.consumeAsync(consumeParams) { billingResult, purchaseToken ->
LogUtil.e(">>>>onConsumeResponse:",
"message:" + billingResult.debugMessage + billingResult.responseCode)
payStateListener?.onConsumeFinished(purchase)
}
}
}
/**
* @description 订阅包的购买
* @param newSku 对应的商品id
* @param productType 具体升降级类型
*/
fun subscribe(newSku: String, productType: ProductType) {
LogUtil.e(TAG, "newSku:$newSku")
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != 0) {
XMToastUtil.showShortToastCenter(context!!.getString(R.string.pay_google_device_not_support),
context)
return
}
val skuList = ArrayList<String>()
skuList.add(newSku)
val params = SkuDetailsParams.newBuilder()
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS)
billingClient.querySkuDetailsAsync(params.build(),
SkuDetailsResponseListener { billingResult, list ->
val subscriptionResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS)
LogUtil.e(TAG, "当前订阅的套餐:" + subscriptionResult.purchasesList.toString());
if (subscriptionResult.purchasesList != null && subscriptionResult.purchasesList.isNotEmpty()) {
currentSusProduct = subscriptionResult.purchasesList[0]
}
if (billingResult.responseCode == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) {
XMToastUtil.showShortToastCenter(context!!.getString(R.string.pay_check_network),
context)
return@SkuDetailsResponseListener
}
if (list.isNullOrEmpty()) {
XMToastUtil.showShortToastCenter(context!!.getString(R.string.pay_google_no_package),
context)
return@SkuDetailsResponseListener
}
for (i in list.indices) {
val skuDetails = list[i]
var builder: BillingFlowParams.Builder
//没有订阅记录
builder = if (currentSusProduct == null) {
//没有有效期内的订阅记录
BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
} else {
//有订阅记录,根据type判断是升级还是降级
BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.setReplaceSkusProrationMode(if (productType == ProductType.UPGRADE) BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION else BillingFlowParams.ProrationMode.DEFERRED)
.setOldSku(currentSusProduct!!.sku, currentSusProduct!!.purchaseToken)
}
val code =
billingClient.launchBillingFlow(getActivity(context),
builder.build()).responseCode
if (code != BillingClient.BillingResponseCode.OK) {
payStateListener?.onPurchasesFail(skuDetails.title, code)
}
}
})
}
/**
* 消耗包购买
* @param purchase 要消费掉的产品类
* @param purchaseType 产品类型
*/
fun consumePurchase(purchase: Purchase, purchaseType: String) {
LogUtil.e(TAG, "consumeAsync")
if (purchaseType == BillingClient.SkuType.INAPP) {
val consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.setDeveloperPayload(purchase.developerPayload)
.build()
billingClient.consumeAsync(consumeParams) { billingResult, purchaseToken ->
payStateListener?.onConsumeFinished(purchase)
}
} else if (purchaseType == BillingClient.SkuType.SUBS) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
payStateListener?.onConsumeFinished(purchase)
}
}
} else {
payStateListener?.onConsumeFinished(purchase)
}
}
}
}
//设置播放状态回调监听
private var payStateListener: GooglePayStateListener? = null
fun setGooglePayStateListener(payStateListener: GooglePayStateListener?) {
this.payStateListener = payStateListener
}
/**
* @description 连接状态关闭
*/
fun endConnection() {
if (billingClient?.isReady) {
billingClient?.endConnection()
}
}
/**
* @description 强制关闭
*/
fun dismissConnect() {
billingClient?.endConnection()
}
companion object : SingletonHolder<PayUtils, Context>(::PayUtils) {
private const val TAG = "GooglePayUtils"
/**
* @description 获取当前context的实际依附Activity
*/
private fun getActivity(context: Context?): Activity? {
return when (context) {
null -> null
is Activity -> context
is ContextWrapper -> getActivity(
context.baseContext)
else -> null
}
}
/**
* 跳转到google play订阅界面
*/
fun jump2GooglePlaySub(context: Context) {
val intent = Intent()
intent.action = "android.intent.action.VIEW"
val contentUrl = Uri.parse("https://play.google.com/store/account/subscriptions")
intent.data = contentUrl
context.startActivity(intent)
}
/**
* 跳转到google play订阅指定产品id界面
*/
fun jump2GooglePlaySubById(context: Context, productId: String) {
val intent = Intent()
intent.action = "android.intent.action.VIEW"
val contentUrl =
Uri.parse("https://play.google.com/store/account/subscriptions?sku=" + productId + "&package=" + context.packageName)
intent.data = contentUrl
context.startActivity(intent)
}
}
}
2.PayUtils的调用方式
代码逻辑图
//a.初始化
googlePayUtils = PayUtils.getInstance(activity)
googlePayUtils?.setGooglePayStateListener(this)
googlePayUtils?.init()
//b.setGooglePayStateListener的监听回调
/**
* 谷歌服务连接成功,查询谷歌订单做无感补单
*/
override fun onConnectionSuccess() {
googlePayUtils?.queryHavePurchased()
isReady = true
}
/**
* 查询谷歌订单做无感补单回调
*/
override fun onQueryPurchasedSuccess(purchasesResult: Purchase.PurchasesResult) {
this.purchasesResult = purchasesResult
for (purchase in purchasesResult.purchasesList) {
if (!purchase.isAcknowledged) {
productType = ProductType.UPGRADE
closeNotification = true
googlePayUtils?.consumePurchase(purchase, BillingClient.SkuType.SUBS)
}
}
}
/**
* 购买成功做消费处理,或降级成功弹窗处理
*/
override fun onPurchasesUpdated(billingResult: BillingResult,
purchaseList: MutableList<Purchase>?) {
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
if (purchaseList != null && purchaseList.size > 0) {
for (purchase in purchaseList) {
val type =
if (productType == ProductType.NORMAL) BillingClient.SkuType.INAPP else BillingClient.SkuType.SUBS
googlePayUtils?.consumePurchase(purchase, type)
}
} else {
callback?.showSubscriptionDown()
}
}
else -> {
LogUtil.e(TAG, "message:" + billingResult.debugMessage)
}
}
}
/**
* 谷歌购买失败提示
*/
override fun onPurchasesFail(sku: String, code: Int) {
XMToastUtil.showShortToastCenter(activity.getString(R.string.pay_google_purchase_fail), activity)
}
/**
* @description 谷歌产品消费完毕,进行本地存储,防止漏单
*/
override fun onConsumeFinished(purchase: Purchase) {
LogUtil.e(TAG, "onConsumeFinished: google消耗商品" + purchase.packageName + "成功,开始服务器消耗")
if (!closeNotification)
XMToastUtil.showShortToastCenter(activity.getString(R.string.pay_google_wait_a_while), activity)
//存储谷歌订单信息
localData = GooglePlayLocalData(orderId, purchase, productType)
GooglePayInfoUtils.saveGooglePayInfo(localData)
//谷歌订单信息上报后台
viewModel?.postGooglePayCallback(orderId,
purchase.signature,
purchase.originalJson,
if (productType == ProductType.DOWNGRADE) "2" else "1")
}
//c.开始支付入口
/**
* @description 开始支付
* @param
* @time 2020/11/11 11:41
*/
fun startPay(product: PurchaseData.Product,
productType: ProductType,
memberId: String,
contentId: String = "") {
this.product = product
this.productType = productType
this.contentId = contentId
this.memberId = memberId
LogUtil.e(TAG,
"startPay product: $product>>>productType: $productType>>>contentId: $contentId>>>memberId: $memberId")
if (!isReady) {
callback?.showSubscriptionServiceError()
return
}
closeNotification = false
//是否为单点
if (productType == ProductType.NORMAL) {
callback?.showChooseDialog()
} else {
// 1.支付为订阅,检测是否存在其他平台购买的订阅套餐
checkDeviceSupportPay(purchasesResult)
}
}
/**
* @description 检测是否存在其他平台购买的订阅套餐
* @param
* @time 2020/11/11 11:42
*/
private fun checkDeviceSupportPay(purchasesResult: Purchase.PurchasesResult?) {
if (subscriptionResultData?.resultCode == XMediaErrorCode.CODE_SUCCESS) {
// 2.当前订阅是否存在谷歌支付方式或没有订阅历史
if (isSubscriptionContainerGooglePay()) {
// 3.检测当前谷歌账号是否绑定过其他账号
queryGooglePayHistory(purchasesResult)
} else {
callback?.showOtherDevicePay()
}
} else {
callback?.showSubscriptionServiceError()
}
}
/**
* @description 当前订阅是否存在谷歌支付方式或没有订阅历史
* @param
* @time 2020/11/11 14:14
*/
private fun isSubscriptionContainerGooglePay(): Boolean {
subscriptionResultData?.subscription?.productTime?.paymentMethod?.let {
return it == GOOGLE_PAY || it == OFFLINE_PAY
}
return true
}
/**
* @description 检测当前谷歌账号是否绑定过其他账号
* @param
* @time 2020/11/11 14:13
*/
private fun queryGooglePayHistory(purchasesResult: Purchase.PurchasesResult?) {
purchasesResult?.let {
val requestValue = StringBuffer()
for (index in it.purchasesList.indices) {
requestValue.append(it.purchasesList[index].orderId)
if (index != it.purchasesList.size - 1) requestValue.append(",")
}
if (requestValue.isNotEmpty()) {
// 4.检测当前谷歌账号是否绑定过其他账号
viewModel?.requestOrderCheck(requestValue.toString())
} else {
callback?.showChooseDialog()
}
}
if (purchasesResult == null) {
//无购买历史,可以准备下预订单
callback?.showChooseDialog()
}
}
/**
* @description 5.处理下单前的检测结果
*/
private fun handleOrderCheckData(orderCheckData: OrderCheckData) {
if (orderCheckData.resultCode == XMediaErrorCode.CODE_SUCCESS) {
if (orderCheckData.memberId == null) {
callback?.showChooseDialog()
return
} else {
//6.同一用户,可以准备下预订单
if (orderCheckData.memberId == memberId) {
viewModel?.requestOrder(product?.productId.toString(), contentId)
} else {
callback?.showGoogleAccountConflict()
}
}
} else {
callback?.showOtherDevicePay()
}
}
//d.与后台的接口回调
viewModel?.run {
//接口,当前已订阅产品
subscriptionData.observe(activity) {
subscriptionResultData = it
}
//接口,5.下单前检测回调通知,防止同一账号用不同方式购买,导致续订混乱
orderCheckData.observe(activity) {
handleOrderCheckData(it)
}
//接口,7.下单回调通知,提交一个预订单给后台
orderData.observe(activity) {
if (it.resultCode == XMediaErrorCode.CODE_SUCCESS) {
orderId = it.orderId
when (payType) {
GOOGLE_PAY -> {
closeNotification = false
if (productType == ProductType.NORMAL) {
//8.下单消费包
startGoogleConsume()
} else {
//9.下单订阅包
startGoogleSubscription()
}
}
else -> {
}
}
} else {
callback?.showOrderServiceError()
}
}
//接口,10.支付回调通知,谷歌购买已成功,需将数据上报给后台开套餐
googlePayResultData.observe(activity) {
if (it.resultCode == XMediaErrorCode.CODE_SUCCESS) {
localData?.let { localData -> GooglePayInfoUtils.removeGooglePayInfo(localData) }
if (!closeNotification){
// 11.回调刷新订阅界面
callback?.showSubscriptionSuccessful()
closeNotification = false
}
}
}
}
//e.开始使用PayUtils进行谷歌的订阅和消费包购买
/**
* @description 开始谷歌单次购买
* @param
* @time 2020/11/13 10:34
*/
private fun startGoogleConsume() {
product?.let {
var sku = ""
for (payGateway in it.payGateways) {
if (payGateway.gatewayCode == GOOGLE_PAY) {
sku = payGateway.externalCode
}
}
googlePayUtils?.purchase(sku)
}
}
/**
* @description 开始谷歌订阅
* @param
* @time 2020/11/11 15:19
*/
private fun startGoogleSubscription() {
//订阅下单回调
product?.let { product ->
var sku = ""
for (payGateway in product.payGateways) {
if (payGateway.gatewayCode == GOOGLE_PAY) {
sku = payGateway.externalCode
}
}
subscriptionResultData?.subscription?.product?.grade?.let {
if (it.isNotEmpty() && it.toInt() > product.grade) {
productType = ProductType.DOWNGRADE
}
}
googlePayUtils?.subscribe(sku, productType)
}
}
//f.UI相关的回调接口
interface PayControllerCallback {
fun showChooseDialog()
fun showOtherDevicePay()
fun showSubscriptionServiceError()
fun showGoogleAccountConflict()
fun showOrderServiceError()
fun showSubscriptionSuccessful()
fun showExitNoActiveOrder()
fun showSubscriptionDown()
}