flutter IAP苹果内购总结

目录

序

本篇文章目的主要是纠正我之前写过的一篇内购文章的错误说法,以及稍微展示一下目前的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这个单例时,它做了什么事情呢?

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

总结

内购说来也是比较简单的,需要注意的点也是简单,不管你技术能力强还是弱,顺嘴最后说几个点:

  1. if else判断完全,要保证loading能够移除掉
  2. 验单成功后再完成订单
相关推荐
2601_949970691 天前
HumanizeDuration 跨端三方库仓颉适配全流程
前端·javascript·flutter
传奇开心果编程1 天前
【Flutter踩坑排雷】第3课:const 误用坑|const 错误使用引发 UI 异常与性能损耗
flutter·移动开发·跨平台·dart·编程语言理论·编程框架·声明式ui
程序员老刘1 天前
你的Flutter项目要不要紧急升级3.47.8?
flutter·客户端
Talyra422 天前
不想开会员,我写了一个记账 App
flutter
2601_949575862 天前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
法的空间2 天前
AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发
android·flutter·ios
恋猫de小郭2 天前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
好的佩奇2 天前
Dart 之模式
android·flutter·dart
淡淡的香烟3 天前
Flutter实现多语言切换
android·flutter·1024程序员节
事圆则缓3 天前
从零搭建一套 Flutter 架构:先跑通数据流
flutter