Flutter中的restorePurchases方法没有按预期工作

移动开发 2026-07-11

我在处理一个与应用内购买相关的用例时遇到了困难。我已实现了包含两个订阅项的应用内购买。

产品可以从Apple正确加载。我还实现了一个自动刷新逻辑:如果该订阅已购买,则应高亮显示;否则屏幕保持原样。

然而,我现在遇到的问题是 restorePurchases() 不会返回任何响应。之前它工作得非常好,但现在似乎已经不再工作。

我不确定是我不小心多加了什么错误,还是最近的变动引发了这个问题。

我在下方附上了代码。如果你发现有什么不对的地方,或是可能导致这个问题的因素,请告诉我。

const Set<String> iosProductIds = {
  'monthly_iOS_Seven_Free_Days',
  'yearly_iOS_Seven_Free_Days',
};

const Set<String> androidProductIds = {
  'premium',
  'premium_yearly',
};

class AppleCubit extends Cubit<AppleState> {
  final InAppPurchase _iap = InAppPurchase.instance;

  StreamSubscription<List<PurchaseDetails>>? _purchaseSubscription;

  bool globalWebRedirectDone = false;
  Timer? _debounceTimer;
  List<PurchaseDetails> _pendingPurchases = [];
  bool _notificationShown = false;
  bool _isRestoring = false;
  final Set<String> _processedPurchaseIds = {};

  AppleCubit({bool autoRestore = false})
    : super(AppleState()) {
    _initStore(autoRestore: autoRestore);
  }

  // ---------------- INIT ----------------
  Future<void> _initStore({bool autoRestore = false}) async {
    emit(state.copyWith(loading: state.products.isEmpty));
    if (!kIsWeb) {
      _notificationShown = false;

      final isAvailable = await _iap.isAvailable();
      emit(state.copyWith(storeAvailable: isAvailable));

      if (!isAvailable) {
        emit(state.copyWith(loading: false));
        return;
      }

      _purchaseSubscription = _iap.purchaseStream.listen(
        _onPurchaseUpdated,
        onError: (error) {
          emit(
            state.copyWith(
              loading: false,
              error: "Purchase stream error: $error",
            ),
          );
        },
      );

      await _loadProducts();

      if (autoRestore) {
        await restorePurchases();
      }
    }
  }

  Future<void> handleWebRedirectionIfNeeded() async {
    if (globalWebRedirectDone) return;
    globalWebRedirectDone = true;

    final webSessionToken = await AppleSubscriptionRepository()
        .getSubscriptionSessionToken();

    if (webSessionToken.session.isEmpty) return;

    final callback = Uri.encodeComponent(Uri.base.toString());

    final url ="choose/${webSessionToken.session}?callback=$callback";

    print(url);

    final uri = Uri.parse(url);
    if (kIsWeb) {
      await launchUrl(uri, webOnlyWindowName: '_self');
    } else {
      await launchUrl(uri, mode: LaunchMode.externalApplication);
    }
  }

  // ---------------- PRODUCTS ----------------

  Future<void> _loadProducts() async {
    try {
      final productIds = kIsWeb
          ? <String>{}
          : Platform.isIOS
          ? iosProductIds
          : androidProductIds;

      final response = await _iap.queryProductDetails(productIds);

      if (response.error != null) {
        emit(state.copyWith(loading: false, error: response.error!.message));
        return;
      }

      if (response.productDetails.isEmpty) {
        emit(state.copyWith(loading: false, error: "No subscriptions found"));
        return;
      }

      emit(state.copyWith(loading: false, products: response.productDetails));
    } catch (e) {
      emit(
        state.copyWith(
          loading: false,
          error: "Failed to load subscriptions: $e",
        ),
      );
    }
  }

  // ---------------- PLAN SELECTION ----------------

  void selectPlan(ProductDetails product) {
    emit(state.copyWith(selectedProduct: product));
  }

  // ---------------- BUY ----------------

  Future<void> buySelected() async {
    final product = state.selectedProduct;

    if (product == null) {
      emit(state.copyWith(error: "No subscription selected"));
      return;
    }

    emit(state.copyWith(loading: true, status: CommonApiStatus.initial));

    final purchaseParam = PurchaseParam(productDetails: product);

    try {
      await _iap.buyNonConsumable(purchaseParam: purchaseParam);
    } on PlatformException catch (e) {
      final cancelled = e.code == 'storekit2_purchase_cancelled';

      emit(
        state.copyWith(
          loading: false,
          error: cancelled
              ? "Purchase cancelled by user"
              : "Purchase failed: ${e.message}",
        ),
      );
    } catch (e) {
      emit(
        state.copyWith(loading: false, error: "Unexpected purchase error: $e"),
      );
    }
  }

  // ---------------- PURCHASE STREAM (DEBOUNCED) ----------------

  void _onPurchaseUpdated(List<PurchaseDetails> purchases) {
    if (_isRestoring && purchases.isEmpty) {
      _isRestoring = false;
      emit(state.copyWith(loading: false));
      return;
    }

    _pendingPurchases.addAll(purchases);

    _debounceTimer?.cancel();
    _debounceTimer = Timer(const Duration(milliseconds: 200), () async {
      final pending = List<PurchaseDetails>.from(_pendingPurchases);
      _pendingPurchases.clear();

      for (final purchase in pending) {
        final purchaseKey =
            "${purchase.productID}_${purchase.purchaseID ?? purchase.transactionDate}";

        if (_processedPurchaseIds.contains(purchaseKey)) {
          continue;
        }

        _processedPurchaseIds.add(purchaseKey);

        if (state.activeProductId == purchase.productID &&
            purchase.status == PurchaseStatus.purchased) {
          continue;
        }

        switch (purchase.status) {
          case PurchaseStatus.purchased:
          case PurchaseStatus.restored:
            await _handleSuccess(purchase);
            break;

          case PurchaseStatus.pending:
            emit(
              state.copyWith(loading: true, status: CommonApiStatus.initial),
            );
            break;

          case PurchaseStatus.error:
            emit(
              state.copyWith(
                loading: false,
                error: purchase.error?.message ?? "Purchase failed",
              ),
            );
            break;

          default:
            break;
        }
      }
    });
  }

  // ---------------- SUCCESS HANDLING ----------------

  Future<void> _handleSuccess(PurchaseDetails purchase) async {
    _isRestoring = false;

    try {
      if (purchase.pendingCompletePurchase) {
        await _iap.completePurchase(purchase);
      }

      // Prevent duplicate backend calls
      if (state.subscription?.status == "active" &&
          state.subscription?.productId == purchase.productID) {
        return;
      }

      await AppleSubscriptionRepository().postSubscriptionApi(
        token: purchase.verificationData.serverVerificationData,
        selectedSubscritionId: purchase.productID,
        platform: Platform.isIOS ? "ios" : "android",
        packageName: Platform.isAndroid ? "com.avioflai.aviation" : "",
      );

      final backendResponse = await AppleSubscriptionRepository()
          .getSubscriptionDetails();

      final resolvedProductId = _resolveActiveProductId(
        appleProductId: purchase.productID,
        backendSubscription: backendResponse.data,
      );

      //  if (!kIsWeb) {
      if (!_notificationShown &&
          purchase.status == PurchaseStatus.purchased &&
          !kIsWeb) {
        _notificationShown = true;
        LocalNotificationHelper.show(
          title: "Subscription Active",
          body: "All premium features are unlocked",
          screenName: "profileSS",
        );
      }

      final isActive = backendResponse.data?.status == "active";

      emit(
        state.copyWith(
          purchased: isActive,
          loading: false,
          status: CommonApiStatus.success,
          activeProductId: resolvedProductId,
          subscription: backendResponse.data,
        ),
      );
    } catch (e) {
      emit(
        state.copyWith(
          loading: false,
          error: "Subscription verification failed: $e",
        ),
      );
    }
  }

  String _resolveActiveProductId({
    String? appleProductId,
    SubscriptionData? backendSubscription,
  }) {
    if (backendSubscription?.productId != null &&
        backendSubscription!.productId.isNotEmpty) {
      return backendSubscription.productId;
    }
    return appleProductId ?? "";
  }

  // ---------------- RESTORE ----------------

  Future<void> restorePurchases() async {
    _isRestoring = true;

    emit(state.copyWith(loading: true, status: CommonApiStatus.initial));

    try {
      await _iap.restorePurchases();
    } catch (e) {
      _isRestoring = false;

      emit(
        state.copyWith(
          loading: false,
          error: "Restore failed: $e",
          status: CommonApiStatus.failure,
        ),
      );
    }
  }

  // ---------------- CANCEL SUBSCRIPTION ----------------
  Future<void> guideUserToCancelSubscription() async {
    if (state.activeProductId == null) {
      emit(state.copyWith(error: "No active subscription to cancel"));
      return;
    }

    try {
      if (Platform.isIOS) {
        // Open Apple subscriptions page
        const url = 'https://apps.apple.com/account/subscriptions';
        if (await canLaunchUrl(Uri.parse(url))) {
          await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
        } else {
          emit(
            state.copyWith(error: "Cannot open App Store subscriptions page"),
          );
        }
      } else if (Platform.isAndroid) {
        // Open Google Play subscriptions page
        const url = 'https://play.google.com/store/account/subscriptions';
        if (await canLaunchUrl(Uri.parse(url))) {
          await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
        } else {
          emit(
            state.copyWith(error: "Cannot open Play Store subscriptions page"),
          );
        }
      }
    } catch (e) {
      emit(state.copyWith(error: "Failed to open subscription page: $e"));
    }
  }

  Future<void> cancelSubscription() async {
    if (state.activeProductId == null || state.activeProductId!.isEmpty) {
      emit(state.copyWith(error: "No active subscription to cancel"));
      return;
    }

    emit(state.copyWith(loading: true, status: CommonApiStatus.initial));

    try {
      // await AppleSubscriptionRepository().cancelSubscriptionApi(
      //   productId: state.activeProductId,
      //   platform: Platform.isIOS ? "ios" : "android",
      // );
      // Update Cubit state after cancellation
      emit(
        state.copyWith(
          purchased: false,
          activeProductId: "",
          subscription: null,
          loading: false,
          status: CommonApiStatus.success,
        ),
      );

      if (!kIsWeb) {
        LocalNotificationHelper.show(
          title: "Subscription Cancelled",
          body: "Your subscription has been cancelled successfully.",
          screenName: "profileSS",
        );
      }
    } catch (e) {
      emit(
        state.copyWith(
          loading: false,
          status: CommonApiStatus.failure,
          error: "Failed to cancel subscription: $e",
        ),
      );
    }
  }

  // ---------------- BACKEND SYNC ----------------

  Future<void> getSubscriptionsFromBackendServer(String activeProductId) async {
    emit(state.copyWith(loading: true, status: CommonApiStatus.initial));

    try {
      final response = await AppleSubscriptionRepository()
          .getSubscriptionDetails();

      emit(
        state.copyWith(
          subscription: response.data,
          purchased: response.data?.status == "active",
          activeProductId: activeProductId,
          loading: false,
          status: CommonApiStatus.success,
        ),
      );
    } catch (e) {
      emit(
        state.copyWith(
          loading: false,
          status: CommonApiStatus.failure,
          error: "Failed to fetch subscription: $e",
        ),
      );
    }
  }

  // ---------------- CLEANUP ----------------

  @override
  Future<void> close() {
    _purchaseSubscription?.cancel();
    _debounceTimer?.cancel();
    return super.close();
  }
}

解决方案

乍一看(前提是你分享的代码段中没有缺失的部分),你的autoRestore变量始终为false

 AppleCubit({bool autoRestore = false})
    : super(AppleState()) {
    _initStore(autoRestore: autoRestore);
  }
----------------------------------------------------------
_initStore({bool autoRestore = false})

我也看不出在你的代码的某处是否再次调用它以使其变为true,但即使你这么做了,由于它是一个局部变量,不是一个流也不是一个事件,if (autoRestore) { await restorePurchases(); } 将永远不会再次被调用。

所以请检查应该把autoRestore设置为true的那个元素,等它出现后,如果为true就调用restorePurchases方法。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章