|
|
@@ -1,45 +1,76 @@
|
|
|
+import 'dart:async';
|
|
|
+
|
|
|
import 'package:flutter/material.dart';
|
|
|
import 'package:get/get.dart';
|
|
|
+import 'package:in_app_purchase/in_app_purchase.dart';
|
|
|
import 'package:video_player/video_player.dart';
|
|
|
|
|
|
import '../../../../config/theme/dark_theme_colors.dart';
|
|
|
import '../../../../config/translations/strings_enum.dart';
|
|
|
+import '../../../../utils/in_app_purchase_util.dart';
|
|
|
import '../../../components/ix_snackbar.dart';
|
|
|
import '../../../constants/assets.dart';
|
|
|
|
|
|
class SubscriptionController extends GetxController {
|
|
|
+ // 内购工具实例
|
|
|
+ final InAppPurchaseUtil _iapUtil = InAppPurchaseUtil.instance;
|
|
|
+
|
|
|
// 视频播放器控制器
|
|
|
late VideoPlayerController videoController;
|
|
|
final isVideoInitialized = false.obs;
|
|
|
|
|
|
+ // 产品加载状态
|
|
|
+ final isLoadingProducts = false.obs;
|
|
|
+
|
|
|
+ // 购买处理中状态
|
|
|
+ final isPurchasing = false.obs;
|
|
|
+
|
|
|
+ // 产品列表
|
|
|
+ final productDetails = <ProductDetails>[].obs;
|
|
|
+
|
|
|
// 当前选中的订阅计划索引 (0: 年度, 1: 终身, 2: 月度, 3: 周度)
|
|
|
final selectedPlanIndex = 0.obs;
|
|
|
|
|
|
+ // 产品ID配置
|
|
|
+ // iOS 测试: 使用 StoreKit Configuration 文件中配置的 ID
|
|
|
+ // Android 测试: 需要在 Google Play Console 中创建对应的测试产品
|
|
|
+ // 生产环境: 替换为你在 App Store Connect 和 Google Play Console 中创建的真实产品 ID
|
|
|
+ final Map<String, String> productIds = {
|
|
|
+ 'yearly': 'com.test.yearly', // 年度订阅
|
|
|
+ 'lifetime': 'com.test.lifetime', // 终身会员
|
|
|
+ 'monthly': 'com.test.monthly', // 月度订阅
|
|
|
+ 'weekly': 'com.test.weekly', // 周度订阅
|
|
|
+ };
|
|
|
+
|
|
|
// 订阅计划列表
|
|
|
List<Map<String, dynamic>> get plans => [
|
|
|
{
|
|
|
- 'price': '\$40.00',
|
|
|
+ 'productId': productIds['yearly'],
|
|
|
+ 'price': _getProductPrice(productIds['yearly']!) ?? '\$40.00',
|
|
|
'period': Strings.perYear.tr,
|
|
|
'title': Strings.yearlyPlan.tr,
|
|
|
'badge': Strings.mostlyChoose.tr,
|
|
|
'badgeBgColor': DarkThemeColors.bg1,
|
|
|
'badgeTextColor': DarkThemeColors.subscriptionColor,
|
|
|
- 'badgeBorderColor': DarkThemeColors.dividerColor, // null 表示没有边框
|
|
|
+ 'badgeBorderColor': DarkThemeColors.dividerColor,
|
|
|
},
|
|
|
{
|
|
|
- 'price': '\$58.00',
|
|
|
+ 'productId': productIds['lifetime'],
|
|
|
+ 'price': _getProductPrice(productIds['lifetime']!) ?? '\$58.00',
|
|
|
'period': Strings.once.tr,
|
|
|
'title': Strings.lifeTime.tr,
|
|
|
'badge': null,
|
|
|
},
|
|
|
{
|
|
|
- 'price': '\$58.00',
|
|
|
+ 'productId': productIds['monthly'],
|
|
|
+ 'price': _getProductPrice(productIds['monthly']!) ?? '\$58.00',
|
|
|
'period': Strings.perYear.tr,
|
|
|
'title': Strings.monthPlan.tr,
|
|
|
'badge': null,
|
|
|
},
|
|
|
{
|
|
|
- 'price': '\$1.00',
|
|
|
+ 'productId': productIds['weekly'],
|
|
|
+ 'price': _getProductPrice(productIds['weekly']!) ?? '\$1.00',
|
|
|
'period': Strings.perWeek.tr,
|
|
|
'title': Strings.weekPlan.tr,
|
|
|
'badge': Strings.limitedTime.tr,
|
|
|
@@ -49,18 +80,161 @@ class SubscriptionController extends GetxController {
|
|
|
},
|
|
|
];
|
|
|
|
|
|
+ // 获取产品价格
|
|
|
+ String? _getProductPrice(String productId) {
|
|
|
+ final product = _iapUtil.getProductById(productId);
|
|
|
+ return product?.price;
|
|
|
+ }
|
|
|
+
|
|
|
@override
|
|
|
void onInit() {
|
|
|
super.onInit();
|
|
|
_initializeVideoPlayer();
|
|
|
+ _initializeInAppPurchase();
|
|
|
}
|
|
|
|
|
|
@override
|
|
|
void onClose() {
|
|
|
videoController.dispose();
|
|
|
+ // 注意: 不要在这里调用 _iapUtil.dispose()
|
|
|
+ // 因为它是单例,可能在其他地方还在使用
|
|
|
super.onClose();
|
|
|
}
|
|
|
|
|
|
+ /// 初始化内购
|
|
|
+ Future<void> _initializeInAppPurchase() async {
|
|
|
+ try {
|
|
|
+ // 初始化内购
|
|
|
+ final success = await _iapUtil.initialize(
|
|
|
+ onSuccess: _handlePurchaseSuccess,
|
|
|
+ onError: _handlePurchaseError,
|
|
|
+ onCancelled: _handlePurchaseCancelled,
|
|
|
+ onPending: _handlePurchasePending,
|
|
|
+ onRestore: _handleRestoreSuccess,
|
|
|
+ );
|
|
|
+
|
|
|
+ if (success) {
|
|
|
+ // 加载产品信息
|
|
|
+ await _loadProducts();
|
|
|
+ } else {
|
|
|
+ IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '内购功能不可用');
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ debugPrint('初始化内购失败: $e');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 加载产品信息
|
|
|
+ Future<void> _loadProducts() async {
|
|
|
+ isLoadingProducts.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ final productIdSet = productIds.values.toSet();
|
|
|
+ final success = await _iapUtil.loadProducts(productIdSet);
|
|
|
+
|
|
|
+ if (success) {
|
|
|
+ productDetails.value = _iapUtil.products;
|
|
|
+ debugPrint('加载了 ${productDetails.length} 个产品');
|
|
|
+ } else {
|
|
|
+ IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '加载产品信息失败');
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ debugPrint('加载产品失败: $e');
|
|
|
+ } finally {
|
|
|
+ isLoadingProducts.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理购买成功
|
|
|
+ void _handlePurchaseSuccess(PurchaseDetails purchaseDetails) {
|
|
|
+ isPurchasing.value = false;
|
|
|
+ IXSnackBar.showIXSnackBar(
|
|
|
+ title: Strings.success.tr,
|
|
|
+ message: '购买成功: ${purchaseDetails.productID}',
|
|
|
+ );
|
|
|
+
|
|
|
+ // TODO: 这里添加你的业务逻辑
|
|
|
+ // 1. 更新用户订阅状态到服务器
|
|
|
+ // 2. 更新本地存储
|
|
|
+ // 3. 刷新 UI 显示
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理购买失败
|
|
|
+ void _handlePurchaseError(PurchaseDetails purchaseDetails) {
|
|
|
+ isPurchasing.value = false;
|
|
|
+ IXSnackBar.showIXSnackBar(
|
|
|
+ title: Strings.error.tr,
|
|
|
+ message: '购买失败: ${purchaseDetails.error?.message ?? "未知错误"}',
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理购买取消
|
|
|
+ void _handlePurchaseCancelled(PurchaseDetails purchaseDetails) {
|
|
|
+ isPurchasing.value = false;
|
|
|
+ IXSnackBar.showIXSnackBar(title: Strings.info.tr, message: '购买已取消');
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理购买等待中
|
|
|
+ void _handlePurchasePending(PurchaseDetails purchaseDetails) {
|
|
|
+ isPurchasing.value = true;
|
|
|
+ IXSnackBar.showIXSnackBar(title: Strings.info.tr, message: '购买处理中...');
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 处理恢复购买成功
|
|
|
+ void _handleRestoreSuccess(PurchaseDetails purchaseDetails) {
|
|
|
+ IXSnackBar.showIXSnackBar(
|
|
|
+ title: Strings.success.tr,
|
|
|
+ message: '恢复购买成功: ${purchaseDetails.productID}',
|
|
|
+ );
|
|
|
+
|
|
|
+ // TODO: 更新用户订阅状态
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 发起购买
|
|
|
+ Future<void> _requestPurchase(String productId) async {
|
|
|
+ if (isPurchasing.value) {
|
|
|
+ debugPrint('已有购买正在进行中');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ isPurchasing.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ final success = await _iapUtil.purchaseProductById(productId);
|
|
|
+
|
|
|
+ if (!success) {
|
|
|
+ isPurchasing.value = false;
|
|
|
+ IXSnackBar.showIXSnackBar(
|
|
|
+ title: Strings.error.tr,
|
|
|
+ message: '发起购买失败,请重试',
|
|
|
+ );
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ isPurchasing.value = false;
|
|
|
+ debugPrint('购买异常: $e');
|
|
|
+ IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '购买异常: $e');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 恢复购买
|
|
|
+ Future<void> _restorePurchases() async {
|
|
|
+ try {
|
|
|
+ final success = await _iapUtil.restorePurchases();
|
|
|
+
|
|
|
+ if (success) {
|
|
|
+ IXSnackBar.showIXSnackBar(
|
|
|
+ title: Strings.info.tr,
|
|
|
+ message: Strings.restoringPurchases.tr,
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '恢复购买失败');
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ debugPrint('恢复购买异常: $e');
|
|
|
+ IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '恢复购买异常: $e');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 初始化视频播放器
|
|
|
void _initializeVideoPlayer() {
|
|
|
videoController = VideoPlayerController.asset(Assets.subscriptionBg)
|
|
|
@@ -81,22 +255,22 @@ class SubscriptionController extends GetxController {
|
|
|
selectedPlanIndex.value = index;
|
|
|
}
|
|
|
|
|
|
- // 确认变更
|
|
|
+ // 确认变更/购买
|
|
|
void confirmChange() {
|
|
|
- // TODO: 实现确认订阅变更逻辑
|
|
|
- IXSnackBar.showIXSnackBar(
|
|
|
- title: Strings.success.tr,
|
|
|
- message: Strings.subscriptionChanged.tr,
|
|
|
- );
|
|
|
+ final plan = plans[selectedPlanIndex.value];
|
|
|
+ final productId = plan['productId'] as String?;
|
|
|
+
|
|
|
+ if (productId == null) {
|
|
|
+ IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '产品ID未配置');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _requestPurchase(productId);
|
|
|
}
|
|
|
|
|
|
// 恢复购买
|
|
|
void restorePurchases() {
|
|
|
- // TODO: 实现恢复购买逻辑
|
|
|
- IXSnackBar.showIXSnackBar(
|
|
|
- title: Strings.info.tr,
|
|
|
- message: Strings.restoringPurchases.tr,
|
|
|
- );
|
|
|
+ _restorePurchases();
|
|
|
}
|
|
|
|
|
|
// 支付问题
|