目录

  • 问题
    • applicationUsername为空问题
    • 苹果验单需求什么东西?
    • 账户登录退出与苹果内购行列监听问题
  • 简略代码逻辑示例
  • 总结

本篇文章意图主要是纠正我之前写过的一篇内购文章的过错说法,以及稍微展示一下现在的flutter内购流程.

之前的文章戳这儿

问题解决

applicationUsername为空问题

flutter在这儿确实是有这个问题,可是applicationUsername本来就不应该存放一些敏感数据. 有些项目为了自己的验单安全,用户安全等等原因,将json数据都存到了这个字段中其实是不恰当的行为.

咱们细心想想,内购是什么在付出,内购使用的是appstore的登录账户,这个权限等级是苹果商店登录账户等级

  • 生产环境: 设置->最顶部账户->媒体与购买项目
  • 沙盒环境: 设置->App Store->最底部沙盒账户

也就是说,在你没有更改appstore登录账户时,他使用的都是用一个,这东西根本能够说跟keychain是同一等级的,其实咱们能够理解为苹果登录账户大于app登录账户. 所以,根据这一思考点,咱们其试验单没有必要去区别验单成功时充值的是什么app账户

  • 苹果内购付出跟app是彻底独立的,付出成功,那他乐意去哪个app账户验单就去哪个app验单,没必要做什么预订单,校验是否发起订单和验单是否是同一用户.
  • 关于咱们程序开发而言,只需确保不要掉单即可
  • 关于财务而言,他们要确认只需确认对账没有问题就行.

苹果验单需求什么东西?

  1. productID(details.productID)
  2. transactionId(details.purchaseID)
  3. receiptKey(details.verificationData.serverVerificationData)

账户登录退出与苹果内购行列监听问题

之前我说过

in_app_purchase希望咱们在app启动时就敞开行列监听

这个其实是有问题的,细心查看源码,在调用InAppPurchase.instance这个单例时,它做了什么事情呢?

// in_app_purchase.dart line:28
static InAppPurchase get instance => _getOrCreateInstance();
// 获取单例
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!;
}
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端完结:

// 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也能够正常执行.

  • 退出登录时:
    flutter IAP苹果内购总结
  • 从头登录:
    flutter IAP苹果内购总结

总结就是,彻底没有我之前所说的那个账户登录退出与苹果内购行列监听问题,只需你做好行列退出的撤销监听即可

简略代码逻辑示例

做了一层相对比较抽象的内购逻辑完结层,不触及app业务验单等,不过该代码并不是最终代码,安卓端的谷歌付出我现在并未测试过,因为安卓不仅需求翻墙,还需求goole play环境,还需求谷歌账号配置,有些费事,不过想来大差不差,等安卓童鞋测试完后,看看需求抛出的类是否需求封装一下.

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');
}

总结

内购说来也是比较简略的,需求留意的点也是简略,不管你技术能力强仍是弱,顺嘴最终说几个点:

  1. if else判断彻底,要确保loading能够移除掉
  2. 验单成功后再完结订单