flutter in_app_purchase使用Apple pay和google pay内购订阅详解以及遇到的问题

三年前我写的时候使用的是flutter_inapp_purchase,现在已经更新到了5.4.0,因为那时候官方还没有出插件,是一位民间大神写出来的,而且使用广泛.本是打算使用这个插件的,但是看插件的时候,发现已经9个月没更新了,而且官方也出了插件,就转而去研究官方插件了...

本文详细介绍的也是官方插件,有空了我也会把flutter_inapp_purchase写个demo出来,发个文章.!!!! flutter_inapp_purchase使用以写完,点击跳转,但是遇到的问题两个插件都是一样的,这篇文章里都写的很清楚了.

我打算写的详细一点,一点点写吧,争取让大家写苹果支付和谷歌支付的时候,看我这一片文章就行. 我会把接入插件之前的坑,遇到的问题,如何接入插件订阅大概能用到的方法以及上线的时候被拒绝回来的原因和解决办法都写上.

路漫漫其修远兮,吾将上下而求索~~

先说前期问题

问题:
使用过程中遇到了好多的问题,下面会一一列举出来.

1.安卓手机莫名其妙的googlepay拽不到订单.

解决: 初步发现是华为手机和小米手机以及googleplay账号的问题,第一步,一定要翻墙,然后使用手机的时候目前vivo手机是可以的,为此还特意买了个pixel3做测试机,华为手机和小米手机我目前手里的几个都拽不到订单,然后是账号问题googleplay点击到底部按钮游戏栏,再选中付费栏目,如果出现可购买的游戏,这证明你的账号是可用的,如果无可购买的内容,说明账号不可用. 剩下的就是配置了

2.配置Apple pay沙盒账号

解决: 进入https://appstoreconnect.apple.com/access/users/sandbox,点击沙盒测试员右侧添加按钮,需要注意的是,添加的沙盒账号必须是没有注册过App Store的和没有注册过沙盒账号的邮箱,实话说,这个随便填,记住账号密码就行,然后打开手机,进入设置,往下滑,点击App Store,滑到最下面,有一个沙盒账户,登陆,然后Apple ID安全,点击其他选项,选择不升级,OK,这样就可以用了.
如果你的App Store下面没有沙盒账户,说明你的手机之前没有做过沙盒测试,那就连接你的电脑,运行一下,点击你的测试购买的商品,会弹出提示框,让你登陆沙盒账户,以后就会有这个登陆选项了.

image.png

3.配置Apple pay订阅信息

解决:进入appstoreconnect,进入你的app里,左下角功能 -> 订阅,在iOS里是有订阅组的,同组的订阅是可以进行时间升级和降级,比如一个月的升级到一年,一年降级到一个月等等,看你有多少类型可供用户选择,而且价钱也只能选择苹果提供的价钱.App Store 本地化版本这个就是按照正常填写就行,这个订阅组叫什么. 创建订阅,你的产品id一定要想好在填写,填写之后如果不对是删除了,想再次使用这个产品id是不能重复创建的,即使是删除了也不行,然后填写参考名称,订阅时限,销售范围,订阅价格,本地化和审核信息,这个一定要认真填写,能够清楚的表明你的订阅是干什么的,多钱,时效,让用户使用的功能!
审核信息的截屏也是一定要能够清晰的表达出你的订阅是干什么的,多钱,时效,让用户使用的功能,否则上线的时候会被拒.推广图标一定要让UI好好设计出来一个,不能使用logo,不能几个订阅使用同一个icon,推广图标也要标记出这个订阅的时长之类的,有特点的.


image.png
4.配置google pay账号

解决:翻墙,进入https://play.google.com/console/u/1/developers/页面,左下角设置 -> 许可测试 ,添加测试人员, 许可响应: RESPOND_NORMALLY,然后进入需要测试的app,所有应用 -> 测试app -> 内部测试/封闭测试/开放测试/ -> 测试用户数量 -> 选择测试列表 -> 添加电子邮件地址 -> 添加完回车,下面会多出你添加的邮箱(一定要回车,要不添加不上),保存更改!然后点击下面的复制链接,发送给要测试的人员,然后让他接受邀请,这部是必须的,否则添加不上,一样拽不到订单.接受完邀请,就可以安装你打出来的apk包,注意安装的包一定要和上传的包的版本号和build一致,还有你的秘钥一致 !

image.png

5.配置google pay订阅信息

解决:翻墙, 进入你的开发者平台,选择app,滑到最下面,左侧的创收 -> 商品 -> 订阅, google pay的订阅吧,不像iOS那样有订阅组,即使你创建了订阅组,最后也是不好使,只能一个一个创建,然后用代码实现更改订阅选项.
创建订阅 -> 添加基础方案,剩下的就是填写信息,按照提示填写完成就好,谷歌的订阅还好审核没有那么严格,能够调用到就行.

5.配置Xcode

记得在Xcode里添加这个


image.png

剩下的有点想不起来了,还有别的问题,等我想起来了在添加上!

使用插件

#支付  https://pub.dev/packages/in_app_purchase
  in_app_purchase: ^3.1.5
  in_app_purchase_android: ^0.2.3
  in_app_purchase_storekit: ^0.3.4
初始化:
List<String> _kProductIds =  <String>['com.xxxx.week','com.xxxx.year']; 
// 产品
List<ProductDetails> products = <ProductDetails>[];

// 购买
List<PurchaseDetails> _purchases = <PurchaseDetails>[];

// 监听更新
late StreamSubscription<List<PurchaseDetails>> _subscription;
initInAppPurchase() {
    //创建监听:
    final Stream<List<PurchaseDetails>> purchaseUpdated =
        _inAppPurchase.purchaseStream;
    // 获得新订阅
    _subscription =
        purchaseUpdated.listen((List<PurchaseDetails> purchaseDetailsList) {
      _listenToPurchaseUpdated(purchaseDetailsList);
    }, onDone: () {
      _subscription.cancel();
    }, onError: (Object error) {
      // handle error here.
    });

    _kProductIds = <String>['com.xxxx.week','com.xxxx.year'];
    final bool isAvailable = await _inAppPurchase.isAvailable();

    if (!isAvailable) {
      products = <ProductDetails>[];
      _purchases = <PurchaseDetails>[];

      XXToast.toast(msg: 'Store unavailable'.tr);
      return;
    }

    if (Platform.isIOS) {
      final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
          _inAppPurchase
              .getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
      await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
    }
    // 加载待售产品
    final ProductDetailsResponse productDetailResponse =
        await _inAppPurchase.queryProductDetails(_kProductIds.toSet());

    if (productDetailResponse.error != null) {
      products = productDetailResponse.productDetails;
      _purchases = <PurchaseDetails>[];
      return;
    }

    if (productDetailResponse.productDetails.isEmpty) {
      products = productDetailResponse.productDetails;
      _purchases = <PurchaseDetails>[];
      return;
    }

    products = productDetailResponse.productDetails;
    await finishIAPTransaction();
}

在iOS里面,必须要拉取一下苹果端的数据,所以不论你是后台下单,还是手机下单,都需要拉取一下代售的数据.

下单之后 回调处理订单
//商品回调   处理购买更新
  Future<void> _listenToPurchaseUpdated(
      List<PurchaseDetails> purchaseDetailsList) async {
    //排序
    final sortedList = List.from(purchaseDetailsList);
    sortedList.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0)
        .compareTo(int.tryParse(a.transactionDate ?? '') ?? 0));
    // 商品列表
    for (final PurchaseDetails purchaseDetails in sortedList) {
      if (purchaseDetails.status == PurchaseStatus.pending) {
        // XXToast.toast(msg: '请等待支付结果');
        await writeCache();
        showLoadingDialog();
      } else {
        //支付错误
        if (purchaseDetails.status == PurchaseStatus.error ||
            purchaseDetails.status == PurchaseStatus.canceled) {
          // XXToast.toast(msg: '${purchaseDetails.error!}');
          await clearCache();
          await finishIAPTransaction();
          hideLoadingDialog();
        } else if (purchaseDetails.status == PurchaseStatus.purchased ||
            purchaseDetails.status == PurchaseStatus.restored) {
          //购买成功  到服务器验证
          // purchaseDetails.billingClientPurchase.originalJson
          if (Platform.isAndroid) {
            var googleDetail = purchaseDetails as GooglePlayPurchaseDetails;
            // print(purchaseDetails);
            loadAndroidGetPayInfo(
              {
                'deviceId': SpUtils.getString(ConfigConstant.deviceId),
                'originalJson': googleDetail.billingClientPurchase.originalJson,
                'signature': googleDetail.billingClientPurchase.signature,
              },
            );
          } else if (Platform.isIOS) {
            if (sortedList.length > 1) {
              if (_isSameTransaction(purchaseDetails.transactionDate ?? '',
                  await getDateCache())) {
                var appstoreDetail = purchaseDetails as AppStorePurchaseDetails;
                // print(purchaseDetails);
                loadAppleGetPayInfo(appstoreDetail);
              }
            } else {
              var appstoreDetail = purchaseDetails as AppStorePurchaseDetails;
              // print(purchaseDetails);
              loadAppleGetPayInfo(appstoreDetail);
            }
          }
        }
        if (Platform.isAndroid) {
          if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId) {
            final InAppPurchaseAndroidPlatformAddition androidAddition =
                _inAppPurchase.getPlatformAddition<
                    InAppPurchaseAndroidPlatformAddition>();
            await androidAddition.consumePurchase(purchaseDetails);
          }
        }
        if (purchaseDetails.pendingCompletePurchase) {
          hideLoadingDialog();
          await _inAppPurchase.completePurchase(purchaseDetails);
        }
      }
    }
  }
安卓进行订阅升级或者降级时,需要获取老订单进行替换,iOS不需要
//获取老订单
  Future<GooglePlayPurchaseDetails?> _getOldSubscription() async {
    GooglePlayPurchaseDetails? oldSubscription;
    if (Platform.isAndroid) {
      final InAppPurchaseAndroidPlatformAddition androidAddition =
          _inAppPurchase
              .getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
      QueryPurchaseDetailsResponse oldPurchaseDetailsQuery =
          await androidAddition.queryPastPurchases();

      oldPurchaseDetailsQuery.pastPurchases.forEach((element) {
        if (element.status == PurchaseStatus.purchased) {
          oldSubscription = element;
        }
      });
    }

    return oldSubscription;
  }
购买

购买之前先要查询一下是否有老订阅,如果只有一个组的订阅,按照我这么写就行,如果有多个组可以升级和降级的订阅,安卓需要将以前的订单获取到,然后判断当前订单属于哪个订阅组,然后降已有的订单升级或降级为当前订单,如果是新订单,就按照新订单走.iOS会自动识别当前已有的订阅,自动在订阅组切换.

  pay() async {
    if (products.isEmpty) {
      XXToast.toast(
          msg: 'No item to be paid was found, please try again later'.tr);
      return;
    }
    late PurchaseParam purchaseParam;
    ProductDetails productDetails = products[type];
    // 两个基础商店以不同方式处理消耗品和非消耗品。如果你使用的是InAppPurchase,需要在这里做区分,为每种类型调用正确的购买方式。
    if (Platform.isAndroid) {
      final GooglePlayPurchaseDetails? oldSubscription =
          await _getOldSubscription();

      purchaseParam = GooglePlayPurchaseParam(
          productDetails: productDetails,
          changeSubscriptionParam: (oldSubscription != null)
              ? ChangeSubscriptionParam(
                  oldPurchaseDetails: oldSubscription,
                  prorationMode: ProrationMode.immediateWithTimeProration,
                )
              : null);
    } else {
      purchaseParam = PurchaseParam(
        productDetails: productDetails,
      );
    }

    // if (productDetails.id == '1111') {
    //   // 购买消耗品
    //   _inAppPurchase.buyConsumable(
    //       purchaseParam: purchaseParam, autoConsume: _kAutoConsume);
    // } else {
      await clearCache();
      // 购买非消耗品
      _inAppPurchase.buyNonConsumable(purchaseParam: purchaseParam);
    // }
  }
iOS恢复已有商品,切换新设备的时候,登陆同一个appstore账号

先说问题,在测试的时候沙盒测试的原因,如果是一周的订阅,在沙盒测试的时候是5分钟,然后会走自动续订,每一个续订都是一个新的订单,然后你恢复订单的时候你就会发现你的订单好多啊...每个订单都是新的,当时对我造成了很大的困扰,最后发现,在返回的数据里,有一个原始数据,
appstoreDetail.skPaymentTransaction.originalTransaction?.transactionIdentifier
这个订单id是最初下单的订单id,这一堆数据里,所有的purchaseID都是新的,但是原始id都是一样的,而且顺序是越新的数据在返回的订单列表数组里的最后,所以给后台上传的时候需要做一个排序去重操作,我是把最新的一条数据传给后台了.

重点:使用该方法,苹果给了建议,如果换手机的情况下,不要启动app我们就主动获取订单传给后台,而是让用户有一个恢复订单的按钮,让用户手动点击恢复,苹果希望这些数据都存在后台,而不是每次启动项目每次都把目前有效的订单都传给后台,所以在你的app里,要有一个按钮,给用户来恢复商品,我就因此被拒回来一次!

上代码

// 监听更新
  late StreamSubscription<List<PurchaseDetails>> _subscription;

  restoringPurchase() async {
    showLoadingDialog();
    final bool isAvailable = await InAppPurchase.instance.isAvailable();

    if (!isAvailable) {
      XXToast.toast(msg: 'Store unavailable'.tr);
      return;
    }

    _subscription = InAppPurchase.instance.purchaseStream.listen(
        (List<PurchaseDetails> purchaseDetailsList) {
      hideLoadingDialog();
        if (purchaseDetailsList.isEmpty) {
          XXToast.toast(msg: 'There is nothing to buy'.tr);
          return;
        }
        workData(purchaseDetailsList);
    }, onDone: () {
      _subscription.cancel();
    }, onError: (Object error) {
      // handle error here.
    });

    await InAppPurchase.instance.restorePurchases();
  }

处理数据:

workData(List<PurchaseDetails> purchaseDetailsList) {
    if (purchaseDetailsList.isNotEmpty) {
      final ids = purchaseDetailsList.map(
        (e) {
          var detail = e as AppStorePurchaseDetails;
          return detail
              .skPaymentTransaction.originalTransaction?.transactionIdentifier;
        },
      ).toSet();

      //排序
      List<PurchaseDetails> sortedList = List.from(purchaseDetailsList);
      sortedList.sort((a, b) => (int.tryParse(b.transactionDate ?? '') ?? 0)
          .compareTo(int.tryParse(a.transactionDate ?? '') ?? 0));

      sortedList.retainWhere((x) {
        var detail = x as AppStorePurchaseDetails;

        return ids.remove(detail
            .skPaymentTransaction.originalTransaction?.transactionIdentifier);
      });

      setUplistenToPurchaseUpdated(sortedList);
    }
  }

//处理数据

//商品回调   处理购买更新 
  Future<void> setUplistenToPurchaseUpdated(
      List<PurchaseDetails> purchaseDetailsList) async {
    List<AppStorePurchaseDetails> detailsList = [];
    // 商品列表
    for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
      var appstoreDetail = purchaseDetails as AppStorePurchaseDetails;
      // print(purchaseDetails);
       detailsList.add(appstoreDetail);
    }
    //提交服务器
    loadAppleGetPayInfo(detailsList);
  }

Appstore上架被拒相关问题

Guideline 3.1.1 - Business - Payments - In-App Purchase
We found that your app offers in-app purchases that can be restored but does not include a "Restore Purchases" feature to allow users to restore the previously purchased in-app purchases, as specified in the "Restoring Purchase Products" section of the In-App Purchase Programming Guide:
"Users restore transactions to maintain access to content they've already purchased. For example, when they upgrade to a new phone, they don't lose all of the items they purchased on the old phone. Include some mechanism in your app to let the user restore their purchases, such as a Restore Purchases button."
Next Steps
To restore previously purchased in-app purchase products, it would be appropriate to provide a "Restore" button and initiate the restore process when the "Restore" button is tapped by the user. Note that automatically restoring purchases on launch will not resolve this issue.

这个问题就很好解决了,像我上面说的,可以在你的设置里有一个可以恢复商品的按钮就可以了,然后处理你的逻辑.

Guideline 3.1.2 - Business - Payments - Subscriptions
We continued to notice that your app did not meet all the terms and conditions for auto-renewing subscriptions, as specified in Schedule 2, section 3.8(b) of the Paid Applications agreement.
We were unable to find the following required information in your app's binary:
– Title of publication or service
– Price of subscription, and price per unit if appropriate
Next Steps
To resolve this issue, please add this missing information. If the above information is present, please reply to this message in App Store Connect to provide details on where to locate it.
Please see attached screenshot for details.

Guideline 2.3.2 - Performance - Accurate Metadata
We noticed your app's metadata refers to paid content or features, but they are not clearly identified as requiring additional purchase.
Paid digital content referenced in your metadata must be clearly labelled to ensure users understand what is and isn't included in your app.
Next Steps
To resolve this issue, please remove these references or clearly mark paid content or features as requiring separate purchases.

Guideline 2.3.2 - Performance - Accurate Metadata
We noticed that the display names and descriptions for your promoted in-app purchase products, week and year, are the same, which makes it hard for users to identify what they are purchasing from the App Store.
Next Steps
To resolve this issue, please revise the display names or descriptions for your promoted in-app purchase products to ensure each individual metadata item is unique.
Please note that display names for promoted in-app purchases can be up to 30 characters long, while descriptions can be up to 45 characters long.
If you have no future plans on promoting this in-app purchase product, you can delete the associated promotional image in App Store Connect.

Guideline 2.3.2 - Performance - Accurate Metadata
We noticed that your promotional image to be displayed on the App Store does not sufficiently represent the associated promoted in-app purchase. Specifically, we found the following issue with your promotional image:
– Your promotional image is the same as your app’s icon.
– You submitted duplicate or identical promotional images for different promoted in-app purchase products.
Next Steps
To resolve this issue, please revise your promotional image to ensure it is unique and accurately represents the associated promoted in-app purchase.
If you have no future plans on promoting this in-app purchase product, you can delete the associated promotional image in App Store Connect.

这些问题就是你的商品详情,还有你的审核数据填的不够完善,一定要把你的订阅的名称,价格,干什么的写清楚,审核图片里也要显示明白.推广图标不要用logo,不要用logo,不要用logo! 不同的商品推广图也不要一样!!!

Guideline 3.1.2 - Business - Payments - Subscriptions
We noticed that your app did not meet all the terms and conditions for auto-renewing subscriptions, as specified in Schedule 2, section 3.8(b) of the Paid Applications agreement.
We were unable to find the following required information in your app's binary:
– Title of publication or service
– Price of subscription, and price per unit if appropriate
– A functional link to the Terms of Use (EULA)
– A functional link to the privacy policy
We were unable to find the following required item(s) in your app's metadata:
– A functional link to the Terms of Use (EULA)
Next Steps
To resolve this issue, please add this missing information. If the above information is present, please reply to this message in App Store Connect to provide details on where to locate it.
If you are using the standard Apple Terms of Use (EULA), you will need to include a link to the Terms of Use in your App Description. If you are using a custom EULA, add it in App Store Connect.

这个问题吧,跟上面类似,但是多出来一条就是需要使用条款,你的连续订阅的使用条款,也就是说许可协议,你可以在 App信息 -> 许可协议,编辑里添加,你的订阅许可协议,他是说这样可以,但是我在网上并没有看到有人这么干,大部分都是让服务器给一个链接,点击链接跳转网页,网页里写的是你们的订阅协议,将这个链接写在你的app描述里面的最下面,我是将我们的用户协议,使用说明,订阅协议都卸载了app描述的最下面.

↑ 以上就是我目前写订阅的时候遇到的一些问题.同时给出一个我看了之后,对于我使用插件帮助比较大的一篇介绍in_app_purchase使用的文章
剩下的就是看苹果和谷歌的官方文档了.

如果对你有帮助的话,希望帮忙点赞,转发! 如果有没看懂,或者想问的,可以留言,看到的第一时间我会回复~~!欢迎留言哦!

也可有偿帮忙写支付业务! 😄😄😄😄

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

推荐阅读更多精彩内容