You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Android端in_app_purchase调用购买时弹出空白底部弹窗问题

Flutter内购调用buyProduct()弹出空白弹窗问题

在Android平台开发Flutter应用时,使用in_app_purchase: ^3.1.8插件集成内购功能,采用测试SKU ID进行测试。调用buyProduct()方法触发支付时,仅弹出空白底部弹窗,无法正常显示Google支付详情页面(预期显示包含支付选项的标准Google支付界面)。

相关实现代码

import 'dart:async';
import 'dart:developer';
import 'dart:io';

import 'package:get/get.dart';
import 'package:in_app_purchase/in_app_purchase.dart';

import '../../../storage/sharde_preference.dart';
import '../../../utils/const.dart';

//import for GooglePlayPurchaseDetails
import 'package:in_app_purchase_android/in_app_purchase_android.dart';

class LockScreenController extends GetxController{

  final String categoryName = Get.arguments;

  final InAppPurchase inAppPurchase = InAppPurchase.instance;

  late StreamSubscription<dynamic> subscription;

  List<String> productId = <String>[
    ANDROID_PURCHASE_SKU,
  ];

  // Is the API available on the device
  RxBool isApiAvailable = true.obs;

  RxBool isProVersion = false.obs;

  // Products for sale
  RxList<ProductDetails> products = <ProductDetails>[].obs;

  // Past purchases
  RxList<PurchaseDetails> purchases = <PurchaseDetails>[].obs;

  @override
  void onInit() {

    final Stream<List<PurchaseDetails>> purchaseUpdated =
        InAppPurchase.instance.purchaseStream;

    subscription = purchaseUpdated.listen((List<PurchaseDetails> purchaseDetailsList) {
      listenToPurchaseUpdate(purchaseDetailsList);
    },onDone: (){
      subscription.cancel();
    },onError: (error){
      log(error.toString(), name: "ERROR");
    }
    );

    initialize();

    super.onInit();
  }

  //buy product
  void buyProduct(ProductDetails productDetails) async {

    // Log product details to verify they are correct.
    log("Product ID: ${productDetails.id}, Title: ${productDetails.title}, Price: ${productDetails.price}");

    final PurchaseParam param = PurchaseParam(productDetails: productDetails);
    InAppPurchase.instance.buyNonConsumable(purchaseParam: param,);

  }

  initialize() async {

    // Check availability of In App Purchases
    isApiAvailable.value = await inAppPurchase.isAvailable();

    if(isApiAvailable.value){
      await getProducts();
      if(Platform.isAndroid){
        // await getPastPurchases();
        // verifyPurchases();
      }
    }
  }

  //get all products
  Future<void> getProducts () async {

    // Set<String> ids = Set.from({ANDROID_PURCHASE_SKU});

    ProductDetailsResponse response = await inAppPurchase.queryProductDetails(productId.toSet());

    products.addAll(response.productDetails);

    log(" description:${products.first.description} id: ${products.first.id} title: ${products.first.title} price: ${products.first.price}", name: "PRODUCTS");
  }

  //get past purchases
  Future getPastPurchases() async {
    await inAppPurchase.restorePurchases(
      applicationUserName: Platform.packageConfig
    );
  }

  //product is purchased or not
  PurchaseDetails hasPurchased(String productID) {
    return purchases.firstWhere((purchase) => purchase.productID == productID);
  }

  //to verify purchases
  void verifyPurchases () {
    for (var product in products){
      PurchaseDetails purchaseDetails = hasPurchased(product.id);

      if(purchaseDetails != null && purchaseDetails.status == PurchaseStatus.purchased){
        log("_________PURCHASED", name: 'PRODUCT STATUS');
        SharedPreference().saveBoolean(PREFKEY_ISPURCHASE, true);
        isProVersion = true.obs;
        update();
      }else{
        log('_________NOT PURCHASED', name: "PRODUCT STATUS");
        SharedPreference().saveBoolean(PREFKEY_ISPURCHASE, false);
        isProVersion = false.obs;
        update();
      }
    }
  }


  //listen on purchases update
  Future<void> listenToPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) async {

    for(PurchaseDetails purchaseDetails in purchaseDetailsList) {
      if (purchaseDetails.status == PurchaseStatus.pending) {
        //print("Pending");
      } else {
        if (purchaseDetails.status == PurchaseStatus.purchased) {
          SharedPreference().saveBoolean(PREFKEY_ISPURCHASE, true);
          isProVersion = true.obs;
          verifyPurchases();
          update();
        } else if (purchaseDetails.status == PurchaseStatus.error) {
          if (purchaseDetails.error?.message ==
              PurchaseStatus.purchased.name) {
            SharedPreference().saveBoolean(PREFKEY_ISPURCHASE, true);
            isProVersion = true.obs;
            update();
          }
        }
        if (Platform.isAndroid) {

            final InAppPurchaseAndroidPlatformAddition androidAddition =
            inAppPurchase.getPlatformAddition<
                InAppPurchaseAndroidPlatformAddition>();
            await androidAddition.consumePurchase(purchaseDetails);

        }
        if(purchaseDetails.pendingCompletePurchase){
          await inAppPurchase.completePurchase(purchaseDetails);
        }

      }
    }
  }


  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();

    subscription.cancel();
  }

}

排查与解决方案

  • 检查测试SKU有效性
    确保使用Google官方测试SKU(如android.test.purchased),自定义测试SKU需在Google Play控制台创建并关联应用,且应用处于内部/封闭测试状态,测试账号已加入测试列表。

  • 验证商品查询结果
    从日志确认getProducts()返回的ProductDetails包含完整信息(价格、标题、描述)。若信息缺失,需排查SKU ID错误、应用签名不一致或控制台配置问题。

  • Android平台配置检查

    1. 在AndroidManifest.xml添加内购权限:
      <uses-permission android:name="com.android.vending.BILLING" />
      
    2. 确保应用签名证书与Google Play控制台配置一致(测试环境需用上传到控制台的签名,而非debug签名)。
  • 修正支付流程逻辑

    • 非消耗品不应调用consumePurchase,该方法仅适用于消耗品,非消耗品完成购买后只需调用completePurchase。
    • hasPurchased方法使用firstWhere会在无购买记录时抛出异常,改为firstWhereOrNull(需导入collection包)避免崩溃:
      PurchaseDetails? hasPurchased(String productID) {
        return purchases.firstWhereOrNull((purchase) => purchase.productID == productID);
      }
      
    • 修正listenToPurchaseUpdate中的错误处理逻辑,添加支付失败的用户提示。
  • 测试环境要求
    使用安装Google Play商店的真实Android设备,登录已加入测试列表的Google账号,避免使用模拟器测试内购。

内容的提问来源于stack exchange,提问作者Payal Patel _d

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 12:18:10