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. 验单成功后再完成订单
相关推荐
️ 邪神17 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】文本点击事件
flutter·ios·鸿蒙·reactnative·anroid
️ 邪神18 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】文本Text显示
flutter·ios·鸿蒙·reactnative·anroid
iFlyCai20 小时前
Flutter中有趣的级联语法
flutter
恋猫de小郭20 小时前
Flutter 小技巧之 Shader 实现酷炫的粒子动画
flutter
hello world smile1 天前
Dart中List API用法大全
flutter·list·dart
lqj_本人1 天前
Flutter&鸿蒙next 使用 BLoC 模式进行状态管理详解
flutter·华为·harmonyos
Miketutu1 天前
flutter 项目初建碰到的控制台报错无法启动问题
flutter
lqj_本人1 天前
flutter&鸿蒙next 使用 InheritedWidget 实现跨 Widget 传递状态
flutter·华为·harmonyos
氤氲息1 天前
flutter 发版的时候设置版本号
flutter
潘敬2 天前
flutter 语法糖库 flutter_magic 发布 1.0.1
开发语言·前端·javascript·flutter·typescript