subscription_controller.dart 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:get/get.dart';
  4. import 'package:in_app_purchase/in_app_purchase.dart';
  5. import 'package:video_player/video_player.dart';
  6. import '../../../../config/theme/dark_theme_colors.dart';
  7. import '../../../../config/translations/strings_enum.dart';
  8. import '../../../../utils/in_app_purchase_util.dart';
  9. import '../../../components/ix_snackbar.dart';
  10. import '../../../constants/assets.dart';
  11. class SubscriptionController extends GetxController {
  12. // 内购工具实例
  13. final InAppPurchaseUtil _iapUtil = InAppPurchaseUtil.instance;
  14. // 视频播放器控制器
  15. late VideoPlayerController videoController;
  16. final isVideoInitialized = false.obs;
  17. // 产品加载状态
  18. final isLoadingProducts = false.obs;
  19. // 购买处理中状态
  20. final isPurchasing = false.obs;
  21. // 产品列表
  22. final productDetails = <ProductDetails>[].obs;
  23. // 当前选中的订阅计划索引 (0: 年度, 1: 终身, 2: 月度, 3: 周度)
  24. final selectedPlanIndex = 0.obs;
  25. // 产品ID配置
  26. // iOS 测试: 使用 StoreKit Configuration 文件中配置的 ID
  27. // Android 测试: 需要在 Google Play Console 中创建对应的测试产品
  28. // 生产环境: 替换为你在 App Store Connect 和 Google Play Console 中创建的真实产品 ID
  29. final Map<String, String> productIds = {
  30. 'yearly': 'com.test.yearly', // 年度订阅
  31. 'lifetime': 'com.test.lifetime', // 终身会员
  32. 'monthly': 'com.test.monthly', // 月度订阅
  33. 'weekly': 'com.test.weekly', // 周度订阅
  34. };
  35. // 订阅计划列表
  36. List<Map<String, dynamic>> get plans => [
  37. {
  38. 'productId': productIds['yearly'],
  39. 'price': _getProductPrice(productIds['yearly']!) ?? '\$40.00',
  40. 'period': Strings.perYear.tr,
  41. 'title': Strings.yearlyPlan.tr,
  42. 'badge': Strings.mostlyChoose.tr,
  43. 'badgeBgColor': DarkThemeColors.bg1,
  44. 'badgeTextColor': DarkThemeColors.subscriptionColor,
  45. 'badgeBorderColor': DarkThemeColors.dividerColor,
  46. },
  47. {
  48. 'productId': productIds['lifetime'],
  49. 'price': _getProductPrice(productIds['lifetime']!) ?? '\$58.00',
  50. 'period': Strings.once.tr,
  51. 'title': Strings.lifeTime.tr,
  52. 'badge': null,
  53. },
  54. {
  55. 'productId': productIds['monthly'],
  56. 'price': _getProductPrice(productIds['monthly']!) ?? '\$58.00',
  57. 'period': Strings.perYear.tr,
  58. 'title': Strings.monthPlan.tr,
  59. 'badge': null,
  60. },
  61. {
  62. 'productId': productIds['weekly'],
  63. 'price': _getProductPrice(productIds['weekly']!) ?? '\$1.00',
  64. 'period': Strings.perWeek.tr,
  65. 'title': Strings.weekPlan.tr,
  66. 'badge': Strings.limitedTime.tr,
  67. 'badgeBgColor': DarkThemeColors.primaryColor,
  68. 'badgeTextColor': Colors.white,
  69. 'badgeBorderColor': null,
  70. },
  71. ];
  72. // 获取产品价格
  73. String? _getProductPrice(String productId) {
  74. final product = _iapUtil.getProductById(productId);
  75. return product?.price;
  76. }
  77. @override
  78. void onInit() {
  79. super.onInit();
  80. _initializeVideoPlayer();
  81. _initializeInAppPurchase();
  82. }
  83. @override
  84. void onClose() {
  85. videoController.dispose();
  86. // 注意: 不要在这里调用 _iapUtil.dispose()
  87. // 因为它是单例,可能在其他地方还在使用
  88. super.onClose();
  89. }
  90. /// 初始化内购
  91. Future<void> _initializeInAppPurchase() async {
  92. try {
  93. // 初始化内购
  94. final success = await _iapUtil.initialize(
  95. onSuccess: _handlePurchaseSuccess,
  96. onError: _handlePurchaseError,
  97. onCancelled: _handlePurchaseCancelled,
  98. onPending: _handlePurchasePending,
  99. onRestore: _handleRestoreSuccess,
  100. );
  101. if (success) {
  102. // 加载产品信息
  103. await _loadProducts();
  104. } else {
  105. IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '内购功能不可用');
  106. }
  107. } catch (e) {
  108. debugPrint('初始化内购失败: $e');
  109. }
  110. }
  111. /// 加载产品信息
  112. Future<void> _loadProducts() async {
  113. isLoadingProducts.value = true;
  114. try {
  115. final productIdSet = productIds.values.toSet();
  116. final success = await _iapUtil.loadProducts(productIdSet);
  117. if (success) {
  118. productDetails.value = _iapUtil.products;
  119. debugPrint('加载了 ${productDetails.length} 个产品');
  120. } else {
  121. IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '加载产品信息失败');
  122. }
  123. } catch (e) {
  124. debugPrint('加载产品失败: $e');
  125. } finally {
  126. isLoadingProducts.value = false;
  127. }
  128. }
  129. /// 处理购买成功
  130. void _handlePurchaseSuccess(PurchaseDetails purchaseDetails) {
  131. isPurchasing.value = false;
  132. IXSnackBar.showIXSnackBar(
  133. title: Strings.success.tr,
  134. message: '购买成功: ${purchaseDetails.productID}',
  135. );
  136. // TODO: 这里添加你的业务逻辑
  137. // 1. 更新用户订阅状态到服务器
  138. // 2. 更新本地存储
  139. // 3. 刷新 UI 显示
  140. }
  141. /// 处理购买失败
  142. void _handlePurchaseError(PurchaseDetails purchaseDetails) {
  143. isPurchasing.value = false;
  144. IXSnackBar.showIXSnackBar(
  145. title: Strings.error.tr,
  146. message: '购买失败: ${purchaseDetails.error?.message ?? "未知错误"}',
  147. );
  148. }
  149. /// 处理购买取消
  150. void _handlePurchaseCancelled(PurchaseDetails purchaseDetails) {
  151. isPurchasing.value = false;
  152. IXSnackBar.showIXSnackBar(title: Strings.info.tr, message: '购买已取消');
  153. }
  154. /// 处理购买等待中
  155. void _handlePurchasePending(PurchaseDetails purchaseDetails) {
  156. isPurchasing.value = true;
  157. IXSnackBar.showIXSnackBar(title: Strings.info.tr, message: '购买处理中...');
  158. }
  159. /// 处理恢复购买成功
  160. void _handleRestoreSuccess(PurchaseDetails purchaseDetails) {
  161. IXSnackBar.showIXSnackBar(
  162. title: Strings.success.tr,
  163. message: '恢复购买成功: ${purchaseDetails.productID}',
  164. );
  165. // TODO: 更新用户订阅状态
  166. }
  167. /// 发起购买
  168. Future<void> _requestPurchase(String productId) async {
  169. if (isPurchasing.value) {
  170. debugPrint('已有购买正在进行中');
  171. return;
  172. }
  173. isPurchasing.value = true;
  174. try {
  175. final success = await _iapUtil.purchaseProductById(productId);
  176. if (!success) {
  177. isPurchasing.value = false;
  178. IXSnackBar.showIXSnackBar(
  179. title: Strings.error.tr,
  180. message: '发起购买失败,请重试',
  181. );
  182. }
  183. } catch (e) {
  184. isPurchasing.value = false;
  185. debugPrint('购买异常: $e');
  186. IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '购买异常: $e');
  187. }
  188. }
  189. /// 恢复购买
  190. Future<void> _restorePurchases() async {
  191. try {
  192. final success = await _iapUtil.restorePurchases();
  193. if (success) {
  194. IXSnackBar.showIXSnackBar(
  195. title: Strings.info.tr,
  196. message: Strings.restoringPurchases.tr,
  197. );
  198. } else {
  199. IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '恢复购买失败');
  200. }
  201. } catch (e) {
  202. debugPrint('恢复购买异常: $e');
  203. IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '恢复购买异常: $e');
  204. }
  205. }
  206. // 初始化视频播放器
  207. void _initializeVideoPlayer() {
  208. videoController = VideoPlayerController.asset(Assets.subscriptionBg)
  209. ..initialize()
  210. .then((_) {
  211. isVideoInitialized.value = true;
  212. videoController.setLooping(true);
  213. videoController.setVolume(0); // 静音播放
  214. videoController.play();
  215. })
  216. .catchError((error) {
  217. print('视频初始化失败: $error');
  218. });
  219. }
  220. // 选择订阅计划
  221. void selectPlan(int index) {
  222. selectedPlanIndex.value = index;
  223. }
  224. // 确认变更/购买
  225. void confirmChange() {
  226. final plan = plans[selectedPlanIndex.value];
  227. final productId = plan['productId'] as String?;
  228. if (productId == null) {
  229. IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '产品ID未配置');
  230. return;
  231. }
  232. _requestPurchase(productId);
  233. }
  234. // 恢复购买
  235. void restorePurchases() {
  236. _restorePurchases();
  237. }
  238. // 支付问题
  239. void handlePaymentIssue() {
  240. // TODO: 实现支付问题处理逻辑
  241. IXSnackBar.showIXSnackBar(
  242. title: Strings.info.tr,
  243. message: Strings.openingPaymentSupport.tr,
  244. );
  245. }
  246. }