Files
arcx-app/lib/features/iap/application/iap_controller.dart
T
2026-08-08 10:57:54 +08:00

227 lines
6.2 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import '../../../core/providers/app_providers.dart';
import '../../config/application/app_config_controller.dart';
/// Observable state for the in-app purchase flow.
class IapState {
const IapState({
this.product,
this.isLoading = false,
this.isPurchasing = false,
this.errorMessage,
this.productsAvailable = false,
this.lastEvent,
});
final ProductDetails? product;
final bool isLoading;
final bool isPurchasing;
final String? errorMessage;
final bool productsAvailable;
/// Last human-readable event (e.g. "解锁成功").
final String? lastEvent;
IapState copyWith({
ProductDetails? product,
bool? isLoading,
bool? isPurchasing,
String? errorMessage,
bool? productsAvailable,
String? lastEvent,
}) {
return IapState(
product: product ?? this.product,
isLoading: isLoading ?? this.isLoading,
isPurchasing: isPurchasing ?? this.isPurchasing,
errorMessage: errorMessage,
productsAvailable: productsAvailable ?? this.productsAvailable,
lastEvent: lastEvent,
);
}
}
final iapControllerProvider = NotifierProvider<IapController, IapState>(
IapController.new,
);
class IapController extends Notifier<IapState> {
late final InAppPurchase _store = InAppPurchase.instance;
StreamSubscription<List<PurchaseDetails>>? _sub;
@override
IapState build() {
ref.onDispose(() {
_sub?.cancel();
_sub = null;
});
_init();
return const IapState(isLoading: true);
}
Future<void> _init() async {
final available = await _store.isAvailable();
if (!available) {
state = state.copyWith(
isLoading: false,
productsAvailable: false,
errorMessage: '无法连接到 App Store,请检查网络设置',
);
return;
}
_sub = _store.purchaseStream.listen(
_onPurchaseUpdated,
onError: (Object e) {
state = state.copyWith(isPurchasing: false, errorMessage: '支付失败:$e');
},
);
await _loadProduct();
}
Future<void> _loadProduct() async {
final response = await _store.queryProductDetails({kUnlockProductId});
final error = response.error;
if (error != null) {
state = state.copyWith(
isLoading: false,
productsAvailable: false,
errorMessage: '商品信息加载失败:${error.message}',
);
return;
}
final products = response.productDetails;
if (products.isEmpty) {
// No product configured in App Store Connect yet.
state = state.copyWith(
isLoading: false,
productsAvailable: false,
errorMessage: null,
);
return;
}
final product = products.first;
state = state.copyWith(
isLoading: false,
productsAvailable: true,
product: product,
errorMessage: null,
);
}
Future<void> buy() async {
final product = state.product;
if (product == null) {
state = state.copyWith(errorMessage: '商品尚未就绪,请稍后再试');
return;
}
state = state.copyWith(
isPurchasing: true,
errorMessage: null,
lastEvent: null,
);
final param = PurchaseParam(productDetails: product);
final ok = await _store.buyNonConsumable(purchaseParam: param);
if (!ok) {
state = state.copyWith(isPurchasing: false, errorMessage: '支付已取消');
}
}
Future<void> restore() async {
state = state.copyWith(
isLoading: true,
errorMessage: null,
lastEvent: null,
);
await _store.restorePurchases();
// The purchase stream will deliver restored purchases; clear loading after
// a short delay in case nothing arrives.
Future<void>.delayed(const Duration(seconds: 2), () {
if (state.isLoading) {
state = state.copyWith(
isLoading: false,
lastEvent: state.lastEvent ?? '未找到历史购买记录',
);
}
});
}
void _onPurchaseUpdated(List<PurchaseDetails> purchases) {
for (final purchase in purchases) {
_handle(purchase);
}
}
Future<void> _handle(PurchaseDetails purchase) async {
switch (purchase.status) {
case PurchaseStatus.purchased:
case PurchaseStatus.restored:
if (purchase.pendingCompletePurchase) {
await _store.completePurchase(purchase);
}
await ref
.read(appConfigProvider.notifier)
.markUnlocked(
fromRestore: purchase.status == PurchaseStatus.restored,
);
state = IapState(
product: state.product,
productsAvailable: state.productsAvailable,
lastEvent: purchase.status == PurchaseStatus.restored
? '权益已恢复'
: '解锁成功!已获得无限记分权益',
);
break;
case PurchaseStatus.error:
state = state.copyWith(
isPurchasing: false,
errorMessage: '支付失败:${purchase.error?.message ?? '未知错误'}',
);
break;
case PurchaseStatus.canceled:
state = state.copyWith(isPurchasing: false, lastEvent: '支付已取消');
break;
case PurchaseStatus.pending:
// Purchase awaiting approval/processing; keep the loading state.
state = state.copyWith(isPurchasing: true);
break;
}
}
/// Debug-only unlock used so the app remains usable without App Store
/// Connect configuration during development.
Future<void> debugUnlock() async {
await ref.read(appConfigProvider.notifier).debugUnlock();
state = state.copyWith(lastEvent: '已通过开发模式解锁');
}
void clearEvent() {
state = state.copyWith(lastEvent: null, errorMessage: null);
}
}
@visibleForTesting
IapState iapStateForTest({
ProductDetails? product,
bool isLoading = false,
bool isPurchasing = false,
String? errorMessage,
bool productsAvailable = false,
String? lastEvent,
}) {
return IapState(
product: product,
isLoading: isLoading,
isPurchasing: isPurchasing,
errorMessage: errorMessage,
productsAvailable: productsAvailable,
lastEvent: lastEvent,
);
}