在Flutter中快速进行苹果商店内购(in-app purchase, IAP)测试

567 阅读8分钟

提示

  • 本文主要面向需要在应用内提供付费内容或增值服务的开发者,即基于 StoreKitIn-App Purchase 测试流程。如果你想要测试 Apple Pay(使用信用卡、借记卡等直接支付),流程会有所不同,但核心测试思路类似:都需在苹果沙盒环境进行测试。
  • 在 Flutter 中进行内购,一般会使用类似于 in_app_purchase 插件,它在 iOS 端底层依赖 StoreKit 完成支付流程。

一、前置准备

1. 注册苹果开发者账号并加入开发者计划

  1. 前往 Apple Developer 注册或登陆你的 Apple ID。
  2. 如果你尚未加入 Apple Developer Program,需要加入付费的开发者计划,才能创建并配置 In-App Purchase 产品。

2. 配置 Xcode 项目

  1. 打开你的 Flutter 项目的 iOS 子项目:在 Flutter 项目根目录下执行 open ios/Runner.xcworkspace,使用 Xcode 打开。

  2. 选择你的项目 target,进入 Signing & Capabilities:

    • Team 一栏选择你的开发者团队 (Team)。
    • 确保 Bundle Identifier 与苹果开发者后台应用的 Bundle ID 一致。
  3. Capabilities: 确保你的应用中已经启用了 In-App Purchases

3. App Store Connect 配置

  1. 登录 App Store Connect

  2. 在 “我的 App” 或 “My Apps” 中,创建一个 App(如果已经有,直接编辑即可)。

    • Bundle ID 与 Xcode 项目一致。
    • 其他信息可以先随意填写,后续上架再补充完善。
  3. 进入你新建/已有的 App,切换到 “功能” (Features) 或 “App 内购买项目” (In-App Purchases) 标签页,创建新的 In-App Purchase 项目:

    • Consumable (消耗型): 用户每次使用都需要再次购买(如游戏内道具、点券)。
    • Non-Consumable (非消耗型): 只需购买一次,永久有效(如解锁高级功能)。
    • Auto-Renewable Subscription (自动续期订阅): 按月或年等周期自动续期。
    • Non-Renewing Subscription (非续期订阅): 手动续订的订阅类型。
  4. 在创建时,填写:

    • Reference Name:仅管理用,你可随意填写。
    • Product ID:与应用中硬编码引用的 productId 一致,通常类似 com.yourcompany.app.iap_item_001
    • Pricing:可以选择适当的价格档位。
    • 语言描述:提供展示给用户看的名称、描述等。
  5. 状态:创建完成后,这些商品最初会是 “Ready to Submit” 或 “Missing Metadata” 状态。当所有必须的描述、截图(如果有)都填写完后,商品会变成 “Ready to Submit”。只要 App 处于 “Ready for Sale” 或者在测试中就可以在沙盒环境购买。

二、沙盒测试账号配置

为了在开发调试阶段测试支付流程,需要使用苹果沙盒 (Sandbox) 环境下的测试账号,而不是你的真实 Apple ID。

  1. 前往 App Store Connect -> Users and Access -> Sandbox

  2. 点击右上角的 “+” 号创建 Sandbox Tester

    • Email:随意写一个未被苹果注册过的邮箱(可以是子邮箱或临时邮箱)。
    • Password:设置一个符合苹果要求的密码。
    • Country/Region:选择与测试市场一致的地区。一般与创建商品时的定价区域对应。
  3. 创建完成后,不要在真实设备的系统设置中直接登录这个沙盒账号到 iCloud!正确流程是:

    • 在 iOS 设备或模拟器上,打开 Settings -> App Store,下拉到 “沙盒账户”,点击登入(一般只有在安装了测试包后或者 debug 包后,实际操作才能看到)。
    • 如果在模拟器中,则需要手动执行购买流程后,iOS 会在弹窗中提示你输入沙盒账号。

三、在 Xcode 中使用 StoreKit Configuration 文件进行本地测试(可选)

Xcode 从 12 版本开始,支持通过 StoreKit Configuration File 进行本地测试,无需创建沙盒账号也能模拟购买流程。这对于快速测试非常有用。

  1. 在 Xcode 中,右击项目根目录,选择 New File...

  2. 选择 StoreKit Configuration File,命名为 StoreKitTest.storekit(可自定义)。

  3. 打开新建的 StoreKitTest.storekit 文件,点击 “+” 创建一个新的产品。

    • Product Identifier 必须与 App Store Connect 上配置的 In-App Purchase Product ID 一致。
    • 设置价格、类型等信息。
  4. Scheme 设置中,选择 Edit Scheme -> Run -> Options,将 StoreKit Configuration 选择为你刚刚创建的 StoreKitTest.storekit

  5. 运行应用后,进行购买操作时,将使用本地 StoreKit 文件进行模拟购买,并可在 Debug 控制台查看详细的测试日志。

注意:本地 StoreKit Configuration 测试并不需要沙盒账号,它与沙盒环境是两种不同的测试方式。

四、Flutter 端的插件与代码编写

以下以官方维护的 in_app_purchase 插件为例,演示在 Flutter 中如何集成并调用 iOS 端的 StoreKit 进行支付测试。你也可以使用其他插件,但思路基本一致。

1. 安装依赖

pubspec.yaml 中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  in_app_purchase: ^5.0.0  # 版本号仅举例,请查看最新版本

然后执行 flutter pub get

2. iOS 端配置

ios/Runner/Info.plist 中,添加 In-App Purchase 权限描述(一般来说不需要额外添加,但有时需要声明用途):

<key>SKAdNetworkItems</key>
<array/>

通常只需要在 Xcode “Signing & Capabilities” 中勾选 In-App Purchases 即可,Info.plist 中并不一定需要额外的权限描述,不过如果项目有使用到其他权限或网络请求,可能在 Info.plist 中统一配置。

3. 初始化与获取商品信息

在你的 Flutter 代码中(比如 main.dart),进行初始化与商品信息请求。

import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 检查平台是否支持内购
  final bool isAvailable = await InAppPurchase.instance.isAvailable();
  if (!isAvailable) {
    // 如果不支持,可能是模拟器不支持购买,或网络问题
    print('In-app purchases are NOT available');
  } else {
    print('In-app purchases are available');
  }

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // 假设我们要测试的商品 ID
  static const String _testProductId = 'com.yourcompany.app.iap_item_001';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'StoreKit Test Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('StoreKit Test Demo'),
        ),
        body: FutureBuilder<ProductDetailsResponse>(
          future: InAppPurchase.instance.queryProductDetails({_testProductId}),
          builder: (context, snapshot) {
            if (!snapshot.hasData) {
              return const Center(child: CircularProgressIndicator());
            }
            final response = snapshot.data!;
            if (response.error != null) {
              return Center(child: Text('Error: ${response.error}'));
            }
            if (response.productDetails.isEmpty) {
              return const Center(child: Text('No products found.'));
            }
            final product = response.productDetails.first;
            return Center(
              child: ElevatedButton(
                child: Text('购买 ${product.title} - ${product.price}'),
                onPressed: () {
                  // 创建购买请求
                  final purchaseParam = PurchaseParam(
                    productDetails: product,
                  );
                  InAppPurchase.instance.buyConsumable(
                    purchaseParam: purchaseParam,
                  );
                },
              ),
            );
          },
        ),
      ),
    );
  }
}

4. 监听购买状态并处理结果

为了正确地处理购买成功、失败等情况,我们需要监听 InAppPurchase.instance.purchaseStream。这通常在一个全局或独立的 Provider/Bloc 中实现,这里简化示例:

import 'package:in_app_purchase/in_app_purchase.dart';

class PurchaseHandler {
  // 单例
  static final PurchaseHandler _instance = PurchaseHandler._internal();
  factory PurchaseHandler() => _instance;
  PurchaseHandler._internal();

  void init() {
    InAppPurchase.instance.purchaseStream.listen((purchases) {
      _handlePurchaseUpdate(purchases);
    }, onDone: () {
      // 流结束了
    }, onError: (error) {
      // 购买出错
      print('Purchase Stream error: $error');
    });
  }

  Future<void> _handlePurchaseUpdate(List<PurchaseDetails> purchases) async {
    for (final purchase in purchases) {
      switch (purchase.status) {
        case PurchaseStatus.purchased:
          // 购买成功
          print('Purchase Success: ${purchase.productID}');
          // TODO: 向自己的服务器验证收据 或 直接完成交易
          InAppPurchase.instance.completePurchase(purchase);
          break;
        case PurchaseStatus.pending:
          // 用户还在支付确认中
          print('Purchase Pending...');
          break;
        case PurchaseStatus.error:
          // 购买失败
          print('Purchase Error: ${purchase.error}');
          break;
        case PurchaseStatus.restored:
          // 恢复购买
          print('Purchase Restored: ${purchase.productID}');
          // 完成交易
          InAppPurchase.instance.completePurchase(purchase);
          break;
        case PurchaseStatus.canceled:
          // 用户取消
          print('Purchase Canceled');
          break;
      }
    }
  }
}

main.dart 或应用入口处调用 PurchaseHandler().init() 初始化即可。

五、如何进行测试

1. 使用沙盒环境测试

  1. 真机 / 模拟器上运行

    • 建议使用真机,模拟器有时在支付相关的功能不一定完全可行,尤其是苹果支付弹窗。
    • 在 Xcode 的 Runner 目标中,选择真机或者 iOS 模拟器,点击 Run
  2. 应用启动后

    • 调用 InAppPurchase.instance.queryProductDetails 获取商品信息,看能否正确返回。
  3. 进行购买

    • 点击 “购买” 按钮时,会弹出苹果的支付弹窗。
    • 输入之前在 App Store Connect -> Sandbox 里配置的 沙盒测试账号
    • 如果弹出 “需要登录” 的窗口,就使用你创建的沙盒账号。
  4. 观察控制台输出,或在你的 UI 中检查购买回调是否成功。

  5. 如果购买成功,可以在沙盒环境中多次进行相同操作来模拟消耗型商品,或切换到恢复购买逻辑测试非消耗型商品。

2. 使用本地 StoreKit Configuration 测试

  1. 在 Xcode 中设置 Run -> Options -> StoreKit Configuration 为创建的 StoreKitTest.storekit 文件。
  2. 运行应用后,点击 “购买” 按钮。
  3. 由于是本地模拟,会直接回调购买成功或失败,不会出现真正的支付弹窗或 Apple ID 输入窗口。
  4. 你可以在 StoreKitTest.storekit 界面里对每个商品的状态(如订阅周期、价格等)进行自定义,并可快速进行测试。

六、常见问题与注意事项

  1. 沙盒账号登录

    • 千万不要把沙盒账号登录到系统 iCloud 中,而是让它在支付弹窗时弹出,然后登录。
    • 如果出现无法弹出沙盒登录窗口、提示账号不存在等,可以在 “设置 -> 退出 Apple ID” 并重新以真实 Apple ID 登录,再重试购买流程。
  2. 商品拉取不到

    • 确认 Product ID 与 App Store Connect 上的设置一致;
    • 确认在 App Store Connect 上的商品状态是 “Ready to Submit” 或以上,且 App 已经有过至少一次构建并提交测试;
    • 有时候需要等待苹果服务器同步,最多可达 24 小时。
  3. 收据验证

    • 沙盒环境下的收据验证 URL 与正式环境不同,需要在服务器端根据环境做区分;
    • 使用本地 StoreKit Configuration 测试时,没有真正的收据验证流程。
  4. 测试订阅

    • 如果你是测试订阅类型 (Auto-Renewable),沙盒环境中某些周期会被加速,如 1 个月订阅在沙盒中可能只持续几分钟,用于模拟续订。

七、完整流程回顾

  1. 苹果开发者账号:注册并加入付费开发者计划。

  2. Xcode 项目配置:设置 Bundle ID、启用 In-App Purchase Capabilities。

  3. App Store Connect:创建应用、添加内购商品,配置商品信息与价格,记录 Product ID

  4. 沙盒账号:创建沙盒测试账号,用于在真机/模拟器中测试购买。

  5. 可选 StoreKit Configuration:在 Xcode 中创建本地配置文件,无需沙盒账号即可模拟购买。

  6. Flutter 端

    1. 添加 in_app_purchase 插件;
    2. 初始化 InAppPurchase,查询 ProductDetails
    3. 发起购买请求 buyConsumablebuyNonConsumable
    4. 监听 purchaseStream,根据回调处理成功/失败等逻辑;
    5. 完成交易 completePurchase
  7. 测试

    • 在真机或模拟器上测试,使用沙盒账号进行真实沙盒购买;
    • 或在 Xcode 使用本地 StoreKit 配置文件快速模拟购买。

可参考: