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. 验单成功后再完成订单
相关推荐
ujainu10 小时前
护眼又美观:Flutter + OpenHarmony 鸿蒙记事本一键切换夜间模式(四)
android·flutter·harmonyos
ujainu10 小时前
让笔记触手可及:为 Flutter + OpenHarmony 鸿蒙记事本添加实时搜索(二)
笔记·flutter·openharmony
一只大侠的侠10 小时前
Flutter开源鸿蒙跨平台训练营 Day 13从零开发注册页面
flutter·华为·harmonyos
一只大侠的侠10 小时前
Flutter开源鸿蒙跨平台训练营 Day19自定义 useFormik 实现高性能表单处理
flutter·开源·harmonyos
恋猫de小郭11 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
一只大侠的侠15 小时前
Flutter开源鸿蒙跨平台训练营 Day 10特惠推荐数据的获取与渲染
flutter·开源·harmonyos
renke336419 小时前
Flutter for OpenHarmony:色彩捕手——基于HSL色轮与感知色差的交互式色觉训练系统
flutter
子春一21 小时前
Flutter for OpenHarmony:构建一个 Flutter 四色猜谜游戏,深入解析密码逻辑、反馈算法与经典益智游戏重构
算法·flutter·游戏
铅笔侠_小龙虾1 天前
Flutter 实战: 计算器
开发语言·javascript·flutter
微祎_1 天前
Flutter for OpenHarmony:构建一个 Flutter 重力弹球游戏,2D 物理引擎、手势交互与关卡设计的工程实现
flutter·游戏·交互