目录
序
本篇文章目的主要是纠正我之前写过的一篇内购文章的错误说法,以及稍微展示一下目前的flutter内购流程.
问题解决
applicationUsername为空问题
flutter在这里确实是有这个问题,但是applicationUsername
本来就不应该存放一些敏感数据. 有些项目为了自己的验单安全,用户安全等等原因,将json数据都存到了这个字段中其实是不恰当的行为.
我们仔细想想,内购是什么在支付,内购使用的是appstore的登录账户,这个权限级别是苹果商店登录账户级别
- 生产环境: 设置->最顶部账户->媒体与购买项目
- 沙盒环境: 设置->App Store->最底部沙盒账户
也就是说,在你没有更改appstore登录账户时,他使用的都是用一个,这东西基本可以说跟keychain是同一级别的,其实我们可以理解为苹果登录账户大于app登录账户. 所以,基于这一思考点,我们其实验单没有必要去区分验单成功时充值的是什么app账户
- 苹果内购支付跟app是完全独立的,支付成功,那他愿意去哪个app账户验单就去哪个app验单,没必要做什么预订单,校验是否发起订单和验单是否是同一用户.
- 对于我们程序开发而言,只要确保不要掉单即可
- 对于财务而言,他们要确定只要确定对账没有问题就行.
苹果验单需要什么东西?
- productID(details.productID)
- transactionId(details.purchaseID)
- receiptKey(details.verificationData.serverVerificationData)
账户登录退出与苹果内购队列监听问题
之前我说过
in_app_purchase希望我们在app启动时就开启队列监听
这个其实是有问题的,仔细查看源码,在调用InAppPurchase.instance
这个单例时,它做了什么事情呢?
dart
// in_app_purchase.dart line:28
static InAppPurchase get instance => _getOrCreateInstance();
ini
// 获取单例
static InAppPurchase _getOrCreateInstance() {
// 如果有直接返回
if (_instance != null) {
return _instance!;
}
// 如果为空,则进行平台初始化
if (defaultTargetPlatform == TargetPlatform.android) {
InAppPurchaseAndroidPlatform.registerPlatform();
} else if (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS) {
// 注意这一行,iOS平台的初始化
InAppPurchaseStoreKitPlatform.registerPlatform();
}
_instance = InAppPurchase._();
return _instance!;
}
scss
static void registerPlatform() {
*******省略其他内容********
_skPaymentQueueWrapper = SKPaymentQueueWrapper();
// Create a purchaseUpdatedController and notify the native side when to
// start of stop sending updates.
// 这里创建了一个streamcontroller
final StreamController<List<PurchaseDetails>> updateController =
StreamController<List<PurchaseDetails>>.broadcast(
// 开启队列监听
onListen: () => _skPaymentQueueWrapper.startObservingTransactionQueue(),
// 取消队列监听
onCancel: () => _skPaymentQueueWrapper.stopObservingTransactionQueue(),
);
_observer = _TransactionObserver(updateController);
_skPaymentQueueWrapper.setTransactionObserver(observer);
}
startObservingTransactionQueue与stopObservingTransactionQueue在iOS端实现:
oc
// startObservingTransactionQueue
[_paymentQueueHandler startObservingPaymentQueue];
// stopObservingTransactionQueue
[_paymentQueueHandler stopObservingPaymentQueue];
那么onListen和onCancel什么时候触发呢?
stream_controller.dart
line:167
The [onListen] callback is called when the first listener is subscribed, and the [onCancel] is called when there are no longer any active listeners. If a listener is added again later, after the [onCancel] was called, the [onListen] will be called again.
当第一个侦听器被订阅时,调用[onListen]回调, 并且当不再有任何活动侦听器时调用[onCancel]。如果稍后在调用[onCancel]之后再次添加侦听器, 将再次调用[onListen]。
结论:
只要我们在账号登录后进行第一次监听,并且在账号退出后取消所有的交易监听,底层还是能正常调用到startObservingPaymentQueue与stopObservingPaymentQueue,并且账号重新登录后startObservingPaymentQueue也能够正常执行.
- 退出登录时:
- 重新登录:
总结就是,完全没有我之前所说的那个账户登录退出与苹果内购队列监听问题
,只要你做好队列退出的取消监听即可
简单代码逻辑示例
做了一层相对比较抽象的内购逻辑实现层,不涉及app业务验单等,不过该代码并不是最终代码,安卓端的谷歌支付我目前并未测试过,因为安卓不仅需要翻墙,还需要goole play环境,还需要谷歌账号配置,有些麻烦,不过想来大差不差,等安卓童鞋测试完后,看看需要抛出的类是否需要封装一下.
dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
enum InAppPurchaseStep {
// 等待中
pending,
// 购买成功
purchased,
// 验单成功
verified,
// 错误
error,
// 恢复购买
restored,
// 已取消
canceled;
}
abstract class InAppPurchaseServiceInterface {
/*
* 购买状态监听回调
* */
void listenToPurchaseUpdated(InAppPurchaseStep status);
/*
* 验单接口
* */
Future<bool> verifyPurchase(PurchaseDetails details);
}
class InAppPurchaseService {
InAppPurchaseService(this.iapServiceImpl);
InAppPurchaseServiceInterface iapServiceImpl;
StreamSubscription<List<PurchaseDetails>>? _subscription;
List<ProductDetails> _products = [];
Completer<bool>? _buySignal;
InAppPurchaseStep? _lastStatus;
/*
* 2.登录后调用监听
* */
void listen() {
_subscription ??= InAppPurchase.instance.purchaseStream.listen(
_listenToPurchaseUpdated,
);
}
/*
* 需要取消监听,退出登录时调用
* */
void cancel() {
_subscription?.cancel();
_subscription = null;
}
/*
* 确定苹果购买服务可不可用
* */
Future<bool> checkAvailable() async {
final bool available = await InAppPurchase.instance.isAvailable();
return available;
}
/*
* 开启购买
* */
Future<bool> buy(String priceId) async {
try {
handlerLoadingCallBack(InAppPurchaseStep.pending);
final isAvailable = await checkAvailable();
if (!isAvailable) {
throw Exception('服务不可用');
}
_iapLog('内购服务可用');
// 1.商品详情
final detail = await _queryProductDetails(priceId);
if (detail == null) {
throw Exception('商品不存在');
}
_iapLog('获取到商品详情信息');
// 2.购买参
final purchaseParam = PurchaseParam(productDetails: detail);
// 3.购买
final res = await InAppPurchase.instance
.buyConsumable(purchaseParam: purchaseParam);
_iapLog('购买结果: $res');
final fu = Completer<bool>();
_buySignal = fu;
return fu.future;
} catch (e) {
_iapLog(e);
handlerLoadingCallBack(InAppPurchaseStep.error);
return false;
}
}
/*
* 查询可用商品
* */
Future<ProductDetails?> _queryProductDetails(String priceId) async {
// query local
for (final pro in _products) {
if (pro.id == priceId) {
return pro;
}
}
// query remote
ProductDetailsResponse res =
await InAppPurchase.instance.queryProductDetails({priceId}).timeout(const Duration(seconds: 15,));
for (final pro in res.productDetails) {
if (pro.id == priceId) {
_products.add(pro);
return pro;
}
}
// find null
return null;
}
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
switch (purchaseDetails.status) {
case PurchaseStatus.pending:
handlerLoadingCallBack(InAppPurchaseStep.pending);
break;
case PurchaseStatus.purchased:
try {
handlerLoadingCallBack(InAppPurchaseStep.purchased);
// 验单结束
final verifyResult =
await iapServiceImpl.verifyPurchase(purchaseDetails);
if (verifyResult) {
_buySignal?.complete(true);
handlerLoadingCallBack(InAppPurchaseStep.verified);
// 完成订单
await _finishDetails(purchaseDetails, true);
} else {
// 发送失败
_buySignal?.complete(false);
// 状态更新
handlerLoadingCallBack(InAppPurchaseStep.error);
// 完成订单
await _finishDetails(purchaseDetails, true);
}
} catch (e) {
_buySignal?.complete(false);
handlerLoadingCallBack(InAppPurchaseStep.error);
// 完成订单
await _finishDetails(purchaseDetails, true);
}
break;
case PurchaseStatus.error:
await _finishDetails(purchaseDetails, true);
handlerLoadingCallBack(InAppPurchaseStep.error);
break;
case PurchaseStatus.restored:
await _finishDetails(purchaseDetails, true);
handlerLoadingCallBack(InAppPurchaseStep.restored);
break;
case PurchaseStatus.canceled:
await _finishDetails(purchaseDetails, true);
handlerLoadingCallBack(InAppPurchaseStep.canceled);
break;
}
_iapLog(
'订单状态: ${purchaseDetails.status.name} 是否需要完成订单: ${purchaseDetails.pendingCompletePurchase}',
);
});
}
/*
* 购买各状态回调
* */
void handlerLoadingCallBack(InAppPurchaseStep status) {
if (status != _lastStatus) {
iapServiceImpl.listenToPurchaseUpdated(status);
_lastStatus = status;
}
}
Future<void> _finishDetails(PurchaseDetails details, [bool force = false]) async {
// 完成订单
if (details.pendingCompletePurchase || force) {
await InAppPurchase.instance.completePurchase(details);
}
}
}
void _iapLog(dynamic m) {
debugPrint('内购🥚🥚🥚 $m');
}
总结
内购说来也是比较简单的,需要注意的点也是简单,不管你技术能力强还是弱,顺嘴最后说几个点:
- if else判断完全,要保证loading能够移除掉
- 验单成功后再完成订单