flutter flutter_inapp_purchase使用Apple pay和google pay内购

因为之前写了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'],
    );
  }
}

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,859评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,772评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,956评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,729评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,611评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,441评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,850评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,485评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,770评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,803评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,598评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,434评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,856评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,065评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,365评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,852评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,065评论 2 338

推荐阅读更多精彩内容