Flutter中的restorePurchases方法没有按预期工作
我在处理一个与应用内购买相关的用例时遇到了困难。我已实现了包含两个订阅项的应用内购买。
产品可以从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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。