美文网首页
flutter flutter_inapp_purchase使用

flutter flutter_inapp_purchase使用

作者: 司徒新新 | 来源:发表于2023-07-29 16:33 被阅读0次

因为之前写了in_app_purchase的使用与常遇到的问题,在里面说要把flutter_inapp_purchase的使用方法也写上没后来一直在忙,最近清闲下来了,现在补上之前所说的.

本文只介绍使用方法,支付集成常见问题请跳转查看另一篇帖子.
flutter in_app_purchase使用Apple pay和google pay内购订阅详解以及遇到的问题

开始

pubspec.yaml内导入

flutter_inapp_purchase: ^5.4.1

使用


class PayPageController extends GetxController {

  late PayPlatform payPlatform = Platform.isAndroid ? PayAndroid() : PayIOS();

  List<IAPItem> get products => payPlatform.list;

  @override
  void onClose() {

    payPlatform.dispose();
    super.onClose();
  }

  @override
  Future<void> onInit() async {
    super.onInit();

    payPlatform.init();
  }

  pay(int index) async {
    bool available = await Global.checkConnectivity();
    if (!available) {
      return;
    }
    if (!await FlutterInappPurchase.instance.isReady()) {
      XXToast.toast(msg: 'Store unavailable'.tr);
      return;
    }
    if (Platform.isAndroid && products.length != Purchase.productIds.length) {
      XXToast.toast(msg: 'Google账户无法获取到订阅列表');
      return;
    }

    payPlatform.pay(products[index]);
  }
}

/// GPT 内购订阅 - 付费类型
enum GPTpurchaseType {
  textWeek,

  textYear;

  bool get isTextVip =>
      GPTpurchaseType.textWeek == this || GPTpurchaseType.textYear == this;

  bool get isWeek => this == GPTpurchaseType.textWeek;
}

pay_platform.dart文件
有可能安卓和iOS的产品名不一样,所以写了两个产品数组并做了判断


const List<String> iosProductIds = ['com.xxxx.week', 'com.xxxx.year'];

const List<String> androidProductIds = ['com.xxxx.week', 'com.xxxx.year'];

abstract class PayPlatform {
  static final instance = FlutterInappPurchase.instance;

  final completer = Completer<List<IAPItem>>();
  List<IAPItem> list = [];

  List<Purchase> planList = [Purchase.week(), Purchase.year()];

  @mustCallSuper
  init() {
    debugPrint('[PayPlatform]: init()');
    instance.initialize().then((value) async {
      if (await instance.isReady()) {
        await getItems();
      }
    });
  }

  getItems() async {
    var value = await instance.getSubscriptions(Purchase.productIds);
    debugPrint('[Purchase]: items - ${{value.length}}');
    if (value.length == planList.length) {
      debugPrint(
          '[Purchase]: items - grand 1.${value.first} \n 2.${value.last}');
      list = [
        value.firstWhere((e) => e.productId == Purchase.productIds.first),
        value.firstWhere((e) => e.productId == Purchase.productIds.last),
      ];
      planList = [
        Purchase.week()..update(list[0]),
        Purchase.year()..update(list[1]),
      ];
      completer.complete(list);
    }
  }

  @mustCallSuper
  dispose() {
    debugPrint('[PayPlatform]: dispose()');
    // instance.finalize();
  }

  @mustCallSuper
  Future<void> pay(IAPItem item) {
    debugPrint('[PayPlatform]: pay()');
    return Future.value();
  }

  void reportTransaction(IAPItem iapItem, PurchasedItem purchasedItem) {
    debugPrint(
        '[PayPlatform]: paySuccess > productId: ${iapItem.productId}, transcationId: ${purchasedItem.transactionId})');
    TenJin.inappPurchase(iapItem, purchasedItem: purchasedItem);
  }
  
  #验证订阅状态
  Future<GPTpurchaseType?> checkPurchase() async {
    var res = await MyRequest.post(Api.getMembersubScribeStatus,
        data: {
          'member_id': Profile.uid,
        },
        needToken: true);
    try {
      var data = json.decode(res.data);
      debugPrint('[ScibeStatus]: 订阅状态 - $data');

      bool isWeek = data['week'] as bool;
      bool isYear = data['year'] as bool;

      return isYear
          ? GPTpurchaseType.textYear
          : (isWeek ? GPTpurchaseType.textWeek : null);
    } catch (e) {
      return Future.error(e);
    }
  }
}

/// 用于页面显示的封装类 - 订阅id & 价钱
class Purchase {
  final String id;
  final GPTpurchaseType type;
  String price = '-';
  String currency = r'$';
  bool offer = false;

  static final List<String> productIds =
      Platform.isAndroid ? androidProductIds : iosProductIds;

  Purchase.week()
      : id = productIds.first,
        type = GPTpurchaseType.textWeek;

  Purchase.year()
      : id = productIds.first,
        type = GPTpurchaseType.textYear;

  String get perDay {
    // ignore: no_leading_underscores_for_local_identifiers
    double? _price = double.tryParse(price);
    if (_price == null) return '';
    switch (type) {
      case GPTpurchaseType.textWeek:
        return (_price / 7).toStringAsFixed(2);
      case GPTpurchaseType.textYear:
        return (_price / 365).toStringAsFixed(2);
    }
  }

  update(IAPItem item) {
    if (Platform.isIOS) {
      offer = item.discountsIOS?.isNotEmpty ?? false;
    } else {
      offer = (item.subscriptionOffersAndroid?.length ?? 0) > 1;
    }
    price = item.price ?? '-';
    currency =
        RegExp(r'([^a-zA-Z]{1})').firstMatch(item.localizedPrice!)?.group(0) ??
            item.currency!;
  }
}

pay_android.dart文件
单独写的安卓文件,如果安卓有单独的业务,在这里面进行使用.


class PayAndroid extends PayPlatform {
  static final _instance = FlutterInappPurchase.instance;

  StreamSubscription? updateSub;
  StreamSubscription? errorSub;

  @override
  dispose() {
    updateSub?.cancel();
    updateSub = null;
    errorSub?.cancel();
    errorSub = null;
    super.dispose();
  }

  @override
  Future<void> pay(IAPItem item) async {
    super.pay(item);
    if (list.length != Purchase.productIds.length) {
      XXToast.toast(msg: 'Store unavailable'.tr);
      return;
    }

    // 先获取之前的订阅
    updateSub?.cancel();
    errorSub?.cancel();

    // const String userId = '';

    final completer = Completer<void>();
    showLoadingDialog();
    updateSub =
        FlutterInappPurchase.purchaseUpdated.listen((PurchasedItem? event) {
      if (event == null) return;
      // -- --- ----
      _instance.finishTransaction(event);

      // 订阅更新
      debugPrint('$event');
      if (item.productId == event.productId) {
        switch (event.purchaseStateAndroid!) {
          case PurchaseState.pending:
            break;
          case PurchaseState.purchased:
            updateSub?.cancel();
            androidPayInfo(event, onReport: () {
              reportTransaction(item, event);
            });

            completer.complete();
            break;
          case PurchaseState.unspecified:
            break;
        }
      }
    });
    errorSub =
        FlutterInappPurchase.purchaseError.listen((PurchaseResult? event) {
      // 出错
      debugPrint('$event');
      completer.completeError(event!);
      hideLoadingDialog();
    });

    var oldSub = await _instance.getAvailablePurchases();

    bool oldSubIsActive = oldSub != null && oldSub.isNotEmpty;
    if (oldSubIsActive) {
      // if (oldSub.any((e) => e.productId == item.productId)) {

      // var match = oldSub.firstWhere((e) => e.productId == item.productId);
      androidPayInfo(oldSub.first, onReport: () {});
      // } else {
      //   // 旧订阅有效,但不是一个订阅,购买新的
      //   _instance.requestSubscription(
      //     item.productId!,
      //     prorationModeAndroid: 3,
      //     purchaseTokenAndroid: oldSub.first.purchaseToken,
      //   );
      // }
    } else {
      _instance.requestSubscription(item.productId!);
    }
  }

  /// 订阅成功以后,传递给后台 通过签名获取数据
  androidPayInfo(PurchasedItem item, {required VoidCallback onReport}) {
    var data = {
      'deviceId': Global.deviceId,
      'originalJson': item.transactionReceipt,
      'signature': item.signatureAndroid,
    };
    MyRequest.post(Api.getPayInfo, data: data, needToken: false).then((value) {
      // {status 1需要上报 0不需要上报
      // environment 1正式 0测试}
      var map = value.data['data'] as Map;
      if (map['status'] == 1 && map['environment'] == 1) {
        onReport();
      }
      _instance.finishTransaction(item);
      Global.homeCon.login();
      hideLoadingDialog();

      Get.offAndToNamed(AppRoutes.Home);
    }).catchError((p0) {
      XXToast.toast(msg: '$p0');
    });
  }
}

pay_ios.dart文件
单独写的iOS文件,如果安卓有单独的业务,在这里面进行使用.


class PayIOS extends PayPlatform {
  static final _instance = FlutterInappPurchase.instance;

  @override
  Future<void> init() async {
    super.init();
  }

  StreamSubscription? updateSub;
  StreamSubscription? errorSub;

  @override
  dispose() {
    updateSub?.cancel();
    updateSub = null;
    errorSub?.cancel();
    errorSub = null;
    _timer?.cancel();
    super.dispose();
  }

  Timer? _timer;

  @override
  Future<void> pay(IAPItem item) async {
    super.pay(item);
    updateSub?.cancel();
    errorSub?.cancel();

    showLoadingDialog();

    // const String userId = 'msmk';
    await _instance.clearTransactionIOS();
    // Completer _completer = Completer();

    updateSub = FlutterInappPurchase.purchaseUpdated
        .listen((PurchasedItem? event) async {
      if (event == null) return;
      // -- --- ----
      _instance.finishTransaction(event);
      // 订阅更新
      debugPrint(
          '[Purchase Update]:\n productId: ${event.productId} transactionId: ${event.transactionId} transactionState: ${event.transactionStateIOS} \n transactionDate: ${event.transactionDate}');

      // if (event.transactionDate!.difference(dateTime).inSeconds < 0) {
      //   //
      //   return;
      // }
      if (item.productId == event.productId) {
        _timer?.cancel();
        _timer =
            Timer.periodic(const Duration(milliseconds: 300), (timer) async {
          timer.cancel();

          switch (event.transactionStateIOS) {
            case TransactionState.purchased:
            case TransactionState.restored:
              updateSub?.cancel();
              hideLoadingDialog();
              await applyVerify(event, onReport: () {
                reportTransaction(item, event);
              });

              // _completer.complete();
              break;
            default:
              hideLoadingDialog();
              break;
          }
        });
      }
    });
    errorSub =
        FlutterInappPurchase.purchaseError.listen((PurchaseResult? event) {
      // 出错
      hideLoadingDialog();
      _instance.clearTransactionIOS();
      debugPrint('$event');
      // _completer.completeError(event!);
    });
    // 检查是否还有有效的订阅
    // GPTpurchaseType? purchase = await checkPurchase();

    // List<DiscountIOS>? discounts = item.discountsIOS;

    // if (discounts != null && discounts.isNotEmpty) {
    // final offerSignature = await _appleSign(
    //   productId: item.productId!,
    //   offerId: discounts.first.identifier!,
    //   username: userId,
    // );

    // _instance
    //     .requestProductWithOfferIOS(item.productId!, userId, <String, Object>{
    //   "identifier": discounts.first.identifier!,
    //   "keyIdentifier": offerSignature.keyIdentifier,
    //   "nonce": offerSignature.nonce,
    //   "signature": offerSignature.signature,
    //   "timestamp": offerSignature.timestamp,
    // });
    // } else {
    _instance.requestPurchase(item.productId!);
    // }
  }

  // 苹果校验
  Future<void> applyVerify(PurchasedItem item,
      {required VoidCallback onReport}) async {
    showLoadingDialog();

    MyRequest.post(Api.appleVerify,
            data: {
              'deviceId': Global.deviceId,
              //开发环境(沙盒:sandbox,正式:buy)
              'env': 'sandbox',
              'productID': item.productId,
              'purchaseID': item.productId,
              'transactionId': item.transactionId,
              'originalTransactionId': item.originalTransactionIdentifierIOS,
              'transactionDate': item.transactionDate?.millisecondsSinceEpoch,
              // 'localVerificationData':
              //     appstoreDetail.verificationData.localVerificationData,
              'serverVerificationData': item.transactionReceipt,
            },
            needToken: false)
        .then((value) {
      // {status 1需要上报 0不需要上报
      // environment 1正式 0测试}
      var map = value.data['data'] as Map;
      if (map['status'] == 1 && map['environment'] == 1) {
        onReport();
      }

      Global.homeCon.login();
      Get.offAllNamed(AppRoutes.Home);
    }, onError: (error) {
      XXToast.toast(msg: '验证出错: $error');
    }).whenComplete(() {
      hideLoadingDialog();
      _instance.finishTransaction(item);
    });

    hideLoadingDialog();
  }

  finishIAPTransaction() async {
    if (Platform.isIOS) {
      var transactions = await _instance.getPendingTransactionsIOS();
      if (transactions != null) {
        Get.log(
            "iap _finishIAPTransaction transaction.length:${transactions.length}");
        for (var transaction in transactions) {
          Get.log(
              "wztest  _finishIAPTransaction transaction==>${transaction.transactionId}");
          await _instance.finishTransaction(transaction);
          Get.log("wztest  _finishIAPTransaction transaction==>finished");
        }
      }
    }
  }

  // ignore: unused_element
  Future<_OfferSign> _appleSign(
      {required String productId,
      required String offerId,
      required String username}) async {
    // return Future.value(
    //   _OfferSign(
    //     keyIdentifier: 'R5DD89747P',
    //     nonce: '31eba0f6-ce40-48a4-bbab-fa5ddccaff42',
    //     signature:
    //         'MEUCIBcLzYob+FT4nOtdopv8Q+v1r0bDhgZXZpP3XZbX+nPFAiEAskoXRnlqHgOBRyk99ICsSc7j5+FQxn1yw8RLwDjAiYs=',
    //     timestamp: 1679732536830,
    //   ),
    // );
    // 执行网络请求获取 签名
    var res = await Dio().get('http://apple.linkwz.com/api/v1/getSign/msmk');
    debugPrint('[Apple Sign]: - ${res.data["data"]}');

    return Future.value(_OfferSign.fromJson(res.data as Map));
  }

  restore() async {
    showLoadingDialog();
    if (!await _instance.isReady()) {
      await _instance.initialize();
    }
    List<PurchasedItem>? subList = await _instance.getAvailablePurchases();
    if (subList != null && subList.isNotEmpty) {
      applyVerify(subList.first, onReport: () {});
    }
  }
}

class _OfferSign {
  final String keyIdentifier;
  final String nonce;
  final String signature;
  final dynamic timestamp;

  _OfferSign(
      {required this.keyIdentifier,
      required this.nonce,
      required this.signature,
      required this.timestamp});

  static _OfferSign fromJson(Map json) {
    var data = json['data'] as Map;
    return _OfferSign(
      keyIdentifier: data['keyIdentifier'],
      nonce: data['nonce'],
      signature: data['sign'],
      timestamp: data['timestamp'],
    );
  }
}

大致使用就这样,其实拿过去就能用.有什么不懂得问题可以留言一起讨论.
喜欢的朋友点赞加关注给点鼓励哦!

相关文章

网友评论

      本文标题:flutter flutter_inapp_purchase使用

      本文链接:https://www.haomeiwen.com/subject/hqljpdtx.html