当前位置:首页 > 技术 > 正文内容

Flutter应用内购功能实践:in_app_purchase插件集成详解

访客 技术 2026年9月29日 8

在Flutter应用中实现应用内购买(In-App Purchase, IAP)功能是常见的商业化需求。本文将详细介绍如何使用官方推荐的in_app_purchase插件,从环境配置到支付流程实现,全面指导开发者集成IAP功能。

一、环境准备与依赖配置

Flutter应用要集成应用内购,首先需要添加in_app_purchase插件的依赖。在项目的pubspec.yaml文件中,找到dependencies部分并添加如下内容:

dependencies:
  flutter:
    sdk: flutter
  in_app_purchase: ^3.1.10 # 使用最新稳定版本
  # in_app_purchase_android: ^0.3.0+2 # 如果只针对Android平台,可以单独引入
  # in_app_purchase_storekit: ^0.3.0+2 # 如果只针对iOS/macOS平台,可以单独引入

添加依赖后,在项目根目录执行flutter pub get命令以下载并安装插件。

二、核心支付流程实现

2.1 应用商店服务连接与状态确认

所有应用内购操作的前提是与应用商店(Apple App Store或Google Play)建立连接,并确认服务可用。这通常在应用启动时执行。

import 'package:in_app_purchase/in_app_purchase.dart';
import 'dart:async'; // 用于StreamSubscription

class AppPurchaseService {
  final InAppPurchase _iap = InAppPurchase.instance;
  StreamSubscription<List<PurchaseDetails>>? _purchaseUpdatesSubscription;
  bool _isStoreAvailable = false;

  bool get storeAvailable => _isStoreAvailable;

  Future<void> initializeService() async {
    _isStoreAvailable = await _iap.isAvailable();
    if (!_isStoreAvailable) {
      print('应用商店服务不可用。');
      // 处理商店不可用的情况,例如禁用购买UI
      return;
    }
    _listenToPurchaseStream();
    print('应用内购服务已初始化,商店可用。');
  }

  void _listenToPurchaseStream() {
    _purchaseUpdatesSubscription = _iap.purchaseStream.listen(
      (List<PurchaseDetails> purchaseList) {
        _handleIncomingPurchases(purchaseList);
      },
      onDone: () {
        _purchaseUpdatesSubscription?.cancel();
      },
      onError: (error) {
        print('购买流监听错误: $error');
        // 处理流错误
      },
    );
  }

  void dispose() {
    _purchaseUpdatesSubscription?.cancel();
  }
  
  // ... 其他方法将在此处添加
}

建议在应用的initState方法中调用initializeService,并在dispose方法中取消订阅,以妥善管理资源。

2.2 商品信息查询与展示

在用户可以购买商品之前,应用需要从应用商店获取已配置的商品详情,包括价格、名称和描述等。商品ID需要在各平台(App Store Connect / Google Play Console)预先配置。

class AppPurchaseService {
  // ... (上文代码)

  List<ProductDetails> _availableProducts = [];
  Set<String> _productIdentifiers = {'product_premium_unlock', 'product_remove_ads'}; // 在商店后台配置的商品ID

  List<ProductDetails> get products => _availableProducts;

  Future<void> retrieveProductDetails() async {
    if (!_isStoreAvailable) {
      print('商店服务不可用,无法查询商品。');
      return;
    }

    final ProductDetailsResponse productResponse =
        await _iap.queryProductDetails(_productIdentifiers);

    if (productResponse.error != null) {
      print('查询商品详情失败: ${productResponse.error?.message}');
      // 处理查询错误
      return;
    }

    if (productResponse.productDetails.isEmpty) {
      print('未找到任何商品。请检查商品ID或商店配置。');
      return;
    }

    _availableProducts = productResponse.productDetails;
    print('已成功获取 ${_availableProducts.length} 个商品详情。');
    // 此时可以更新UI以展示这些商品
  }

  // ... (下文代码)
}

2.3 发起购买与订单处理

用户选择商品后,应用发起购买请求。所有购买结果(成功、失败、待处理、恢复)都将通过purchaseStream监听器返回。

class AppPurchaseService {
  // ... (上文代码)

  Future<void> initiatePurchase(ProductDetails product) async {
    if (!_isStoreAvailable) {
      print('商店服务不可用,无法发起购买。');
      return;
    }

    final PurchaseParam purchaseParams = PurchaseParam(productDetails: product);
    
    // 根据商品类型选择 buyConsumable 或 buyNonConsumable
    // 消耗品:用户可以多次购买(如游戏币)
    // 非消耗品:用户只能购买一次(如移除广告)
    if (product.id == 'product_premium_unlock') { // 假设这是一个非消耗品
      await _iap.buyNonConsumable(purchaseParam: purchaseParams);
    } else { // 假设其他都是消耗品
      await _iap.buyConsumable(purchaseParam: purchaseParams);
    }
  }

  void _handleIncomingPurchases(List<PurchaseDetails> currentPurchases) {
    for (var purchaseDetail in currentPurchases) {
      switch (purchaseDetail.status) {
        case PurchaseStatus.pending:
          print('交易正在进行中: ${purchaseDetail.productID}');
          // 可以在UI上显示加载或等待指示器
          break;
        case PurchaseStatus.purchased:
        case PurchaseStatus.restored: // 恢复购买也视为成功
          print('交易成功或已恢复: ${purchaseDetail.productID}');
          _processSuccessfulPurchase(purchaseDetail);
          break;
        case PurchaseStatus.error:
          print('交易失败: ${purchaseDetail.error?.message}');
          _handlePurchaseError(purchaseDetail);
          break;
        case PurchaseStatus.canceled:
          print('交易已取消: ${purchaseDetail.productID}');
          // 用户取消了购买流程
          break;
      }
    }
  }

  Future<void> _processSuccessfulPurchase(PurchaseDetails detail) async {
    // 生产环境强烈建议在服务器端验证购买收据,以防止欺诈
    final bool isValid = await _verifyPurchaseOnBackend(detail.verificationData.serverVerificationData);

    if (isValid) {
      print('购买验证成功,授予商品: ${detail.productID}');
      // 授予用户对应的权限或物品
      
      if (detail.pendingCompletePurchase) {
        // 对于消耗品和某些非消耗品,需要调用 completePurchase 告知商店已处理此订单
        await _iap.completePurchase(detail);
        print('购买已标记为完成: ${detail.productID}');
      }
      // 更新UI或本地存储的用户权益
    } else {
      print('购买验证失败,不授予商品: ${detail.productID}');
      // 可能需要退款或提示用户联系客服
    }
  }

  Future<bool> _verifyPurchaseOnBackend(String receiptData) async {
    // 这是一个模拟后端验证的函数
    // 在实际应用中,您会向您的后端服务器发送 receiptData 进行验证
    print('模拟后端验证收据...');
    await Future.delayed(Duration(seconds: 1)); // 模拟网络延迟
    return true; // 假设验证成功
  }

  void _handlePurchaseError(PurchaseDetails detail) {
    // 根据错误代码进行更细致的处理
    if (detail.error?.code == '2') { // iOS用户取消购买的常见错误码
      print('用户取消了购买。');
    } else {
      print('其他购买错误: ${detail.error?.message}');
    }
    // 如果购买处于pendingCompletePurchase状态,即使失败也可能需要完成它以清理队列
    // 但通常对于错误状态,商店会自动处理,无需手动 completePurchase
  }

  // ... (其他方法)
}

三、平台特定配置与测试

3.1 Android平台配置

对于Android应用,需要在android/app/build.gradle文件中添加Google Billing Client的依赖:

android {
    // ...
}

dependencies {
    // ...
    implementation "com.android.billingclient:billing-ktx:6.0.1" // 使用当前最新稳定版
}

在Google Play Console中,需要配置测试人员和测试商品。使用测试账号进行测试,避免产生实际费用。

3.2 iOS/macOS平台配置

对于iOS/macOS应用,需要在ios/Runner/Info.plist文件中添加App Store连接权限的描述:

<key>SKPaymentNetworkUsageDescription</key>
<string>我们需要访问App Store来完成您的应用内购买。</string>

在App Store Connect中配置好沙盒测试用户和测试商品。使用沙盒账号在真机上进行测试,以模拟实际购买流程。

四、注意事项与最佳实践

  • 商品ID一致性: 确保代码中使用的商品ID与App Store Connect和Google Play Console中配置的完全一致,包括大小写。
  • 服务器端验证: 为了防止购买欺诈和保障数据安全,强烈建议将购买收据发送到您自己的后端服务器进行验证。客户端验证容易被篡改。
  • purchaseStream生命周期: 务必在Widget销毁时取消对purchaseStream的订阅,以避免内存泄漏。
  • 处理所有PurchaseStatus: 购买流会返回多种状态,包括purchased、error、pending、restored和canceled,应用应妥善处理每种状态。
  • completePurchase调用: 成功处理完一笔购买后,务必调用InAppPurchase.instance.completePurchase(purchaseDetails)来告知应用商店该笔交易已完成。否则,该交易可能会在每次应用启动时重新出现在purchaseStream中。
  • 恢复购买功能: 对于非消耗品(如高级功能解锁),应用应提供"恢复购买"功能,允许用户在新设备上或重新安装应用后恢复其已购买的权益。这通常通过调用_iap.restorePurchases()触发。
  • 错误日志: 集成良好的错误日志系统,以便在生产环境中追踪和排查支付相关问题。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。