| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559 |
- import 'dart:async';
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:get/get.dart';
- import 'package:in_app_purchase/in_app_purchase.dart';
- import 'package:nomo/config/theme/theme_extensions/theme_extension.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 '../../../../utils/log/logger.dart';
- import '../../../../utils/system_helper.dart';
- import '../../../components/ix_snackbar.dart';
- import '../../../constants/assets.dart';
- import '../../../controllers/api_controller.dart';
- import '../../../data/models/channelplan/channel_plan_list.dart';
- import '../../../data/sp/ix_sp.dart';
- import '../../../dialog/all_dialog.dart';
- import '../../../dialog/loading/loading_dialog.dart';
- class SubscriptionController extends GetxController {
- static const String TAG = 'SubscriptionController';
- final ApiController _apiController = Get.find<ApiController>();
- // 内购工具实例
- final InAppPurchaseUtil _iapUtil = InAppPurchaseUtil.instance;
- // 视频播放器控制器
- late VideoPlayerController videoController;
- final isVideoInitialized = false.obs;
- // 套餐列表加载状态
- final isLoadingPlans = false.obs;
- // 产品加载状态
- final isLoadingProducts = false.obs;
- // 购买处理中状态
- final isPurchasing = false.obs;
- // 产品列表(内购产品)
- final productDetails = <ProductDetails>[].obs;
- // 套餐列表(API 返回)
- final channelPlans = <ChannelPlan>[].obs;
- // 当前选中的订阅计划索引
- final selectedPlanIndex = 0.obs;
- /// 是否显示套餐变更信息(仅 userLevel == 3 时显示)
- bool get showPlanChangeInfo {
- final user = IXSP.getUser();
- return user?.userLevel == 3;
- }
- // 获取产品价格(优先使用内购价格,否则使用 API 返回的价格)
- String _getDisplayPrice(ChannelPlan plan) {
- // 尝试从内购获取价格
- final productId = plan.payoutData;
- if (productId != null && productId.isNotEmpty) {
- final product = _iapUtil.getProductById(productId);
- if (product != null) {
- return product.price;
- }
- }
- // 使用 API 返回的价格
- return _formatPrice(plan.price, plan.currency);
- }
- // 格式化价格
- String _formatPrice(double? price, int? currency) {
- if (price == null) return '';
- // currency: 1=USD, 2=CNY, etc. 根据实际情况调整
- final symbol = currency == 2 ? '¥' : '\$';
- return '$symbol${price.toStringAsFixed(2)}';
- }
- // 获取订阅周期显示文本
- String _getPeriodText(ChannelPlan plan) {
- if (plan.isSubscribe != true) {
- return Strings.once.tr; // 一次性购买(终身)
- }
- // 根据 subscribeType 和 subscribePeriodValue 确定周期
- // subscribeType: 1=天, 2=周, 3=月, 4=年
- switch (plan.subscribeType) {
- case 1:
- final days = plan.subscribePeriodValue ?? 1;
- return '/$days day${days > 1 ? 's' : ''}';
- case 2:
- return Strings.perWeek.tr;
- case 3:
- return '/month';
- case 4:
- return Strings.perYear.tr;
- default:
- return '';
- }
- }
- // 获取标签背景色
- Color? _getBadgeBgColor(ChannelPlan plan) {
- if (plan.tagType == 1) {
- return DarkThemeColors.bg1;
- } else if (plan.tagType == 2) {
- return DarkThemeColors.primaryColor;
- }
- return null;
- }
- // 获取标签文字颜色
- Color? _getBadgeTextColor(ChannelPlan plan) {
- if (plan.tagType == 1) {
- return DarkThemeColors.subscriptionColor;
- } else if (plan.tagType == 2) {
- return Colors.white;
- }
- return null;
- }
- Color? _getBadgeBorderColor(ChannelPlan plan) {
- if (plan.tagType == 1) {
- return ReactiveTheme.isLightTheme
- ? DarkThemeColors.subscriptionColor
- : DarkThemeColors.dividerColor;
- } else if (plan.tagType == 2) {
- return null;
- }
- return null;
- }
- @override
- void onInit() {
- super.onInit();
- _initializeVideoPlayer();
- // _initializeInAppPurchase();
- _getChannelPlanList();
- refreshSubscriptionStatus();
- // 设置状态栏颜色
- _setSystemUIOverlayStyle(ReactiveTheme.isLightTheme, true);
- }
- void _setSystemUIOverlayStyle(bool isLight, bool changeDarkMode) {
- if (isLight) {
- if (changeDarkMode) {
- SystemChrome.setSystemUIOverlayStyle(
- SystemUiOverlayStyle(
- statusBarColor: Get.reactiveTheme.scaffoldBackgroundColor,
- statusBarIconBrightness: Brightness.light,
- statusBarBrightness: Brightness.light,
- ),
- );
- } else {
- SystemChrome.setSystemUIOverlayStyle(
- SystemUiOverlayStyle(
- statusBarColor: Get.reactiveTheme.scaffoldBackgroundColor,
- statusBarIconBrightness: Brightness.dark,
- statusBarBrightness: Brightness.dark,
- ),
- );
- }
- }
- }
- @override
- void onClose() {
- videoController.dispose();
- _setSystemUIOverlayStyle(ReactiveTheme.isLightTheme, false);
- // 注意: 不要在这里调用 _iapUtil.dispose()
- // 因为它是单例,可能在其他地方还在使用
- super.onClose();
- }
- /// 获取套餐列表
- Future<void> _getChannelPlanList() async {
- isLoadingPlans.value = true;
- try {
- final plans = await _apiController.getChannelPlanList();
- channelPlans.value = plans;
- // 设置默认选中项
- final defaultIndex = plans.indexWhere((p) => p.isDefault == true);
- if (defaultIndex >= 0) {
- selectedPlanIndex.value = defaultIndex;
- }
- // 加载对应的内购产品
- // await _loadProductsFromPlans();
- } catch (e) {
- log(TAG, '获取套餐列表失败: $e');
- IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '获取套餐列表失败');
- } finally {
- isLoadingPlans.value = false;
- }
- }
- /// 购买套餐
- Future<void> subscribe() async {
- if (selectedPlan?.payoutType == 'default') {
- await LoadingDialog.show(
- context: Get.context!,
- loadingText: 'Purchasing...',
- successText: 'Purchase successful',
- onRequest: () async {
- // 执行你的异步请求
- await _apiController.subscribe({
- 'channelItemId': selectedPlan?.channelItemId,
- });
- },
- onSuccess: () async {
- // 刷新用户信息
- refreshSubscriptionStatus();
- },
- );
- } else if (selectedPlan?.payoutType == 'web') {
- SystemHelper.openWebPage(selectedPlan?.payoutData ?? '');
- AllDialog.showSubscriptionForWeb(() {
- _apiController.refreshLaunch();
- });
- }
- }
- /// 初始化内购
- Future<void> _initializeInAppPurchase() async {
- try {
- // 初始化内购
- final success = await _iapUtil.initialize(
- onSuccess: _handlePurchaseSuccess,
- onError: _handlePurchaseError,
- onCancelled: _handlePurchaseCancelled,
- onPending: _handlePurchasePending,
- onRestore: _handleRestoreSuccess,
- );
- if (!success) {
- IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '内购功能不可用');
- }
- } catch (e) {
- debugPrint('初始化内购失败: $e');
- }
- }
- /// 根据套餐列表加载内购产品
- Future<void> _loadProductsFromPlans() async {
- if (channelPlans.isEmpty) return;
- isLoadingProducts.value = true;
- try {
- // 从套餐列表中提取产品ID
- final productIdSet = channelPlans
- .where((p) => p.payoutData != null && p.payoutData!.isNotEmpty)
- .map((p) => p.payoutData!)
- .toSet();
- if (productIdSet.isEmpty) {
- debugPrint('没有需要加载的内购产品');
- return;
- }
- final success = await _iapUtil.loadProducts(productIdSet);
- if (success) {
- productDetails.value = _iapUtil.products;
- debugPrint('加载了 ${productDetails.length} 个产品');
- // 刷新套餐列表以更新价格显示
- channelPlans.refresh();
- }
- } 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)
- ..initialize()
- .then((_) {
- isVideoInitialized.value = true;
- videoController.setLooping(true);
- videoController.setVolume(0); // 静音播放
- videoController.play();
- })
- .catchError((error) {
- print('视频初始化失败: $error');
- });
- }
- // 选择订阅计划
- void selectPlan(int index) {
- if (index >= 0 && index < channelPlans.length) {
- selectedPlanIndex.value = index;
- }
- }
- // 获取当前选中的套餐
- ChannelPlan? get selectedPlan {
- if (selectedPlanIndex.value < channelPlans.length) {
- return channelPlans[selectedPlanIndex.value];
- }
- return null;
- }
- /// 获取当前选中套餐的设备限制数量
- String get selectedPlanDeviceLimit {
- return (selectedPlan?.deviceLimit ?? 0).toString();
- }
- // 确认变更/购买
- void confirmChange() {
- final plan = selectedPlan;
- if (plan == null) {
- IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '请选择套餐');
- return;
- }
- final productId = plan.payoutData;
- if (productId == null || productId.isEmpty) {
- IXSnackBar.showIXSnackBar(title: Strings.error.tr, message: '产品ID未配置');
- return;
- }
- _requestPurchase(productId);
- }
- // 恢复购买
- void restorePurchases() {
- _restorePurchases();
- }
- // 支付问题
- void handlePaymentIssue() {
- // TODO: 实现支付问题处理逻辑
- IXSnackBar.showIXSnackBar(
- title: Strings.info.tr,
- message: Strings.openingPaymentSupport.tr,
- );
- }
- // ==================== 用于 UI 展示的便捷方法 ====================
- /// 获取套餐数量
- int get planCount => channelPlans.length;
- /// 获取指定索引的套餐标题
- String getPlanTitle(int index) {
- if (index < channelPlans.length) {
- return channelPlans[index].title ?? '';
- }
- return '';
- }
- /// 获取指定索引的套餐副标题
- String getPlanSubTitle(int index) {
- if (index < channelPlans.length) {
- return channelPlans[index].subTitle ?? '';
- }
- return '';
- }
- /// 获取指定索引的套餐介绍
- String getPlanIntroduce(int index) {
- if (index < channelPlans.length) {
- return channelPlans[index].introduce ?? '';
- }
- return '';
- }
- /// 获取指定索引的套餐价格显示
- String getPlanPrice(int index) {
- if (index < channelPlans.length) {
- return _getDisplayPrice(channelPlans[index]);
- }
- return '';
- }
- /// 获取指定索引的套餐周期显示
- String getPlanPeriod(int index) {
- if (index < channelPlans.length) {
- return _getPeriodText(channelPlans[index]);
- }
- return '';
- }
- /// 获取指定索引的套餐标签
- String getPlanBadge(int index) {
- if (index < channelPlans.length) {
- return channelPlans[index].tag ?? '';
- }
- return '';
- }
- /// 获取指定索引的标签背景色
- Color? getPlanBadgeBgColor(int index) {
- if (index < channelPlans.length) {
- return _getBadgeBgColor(channelPlans[index]);
- }
- return null;
- }
- /// 获取指定索引的标签文字颜色
- Color? getPlanBadgeTextColor(int index) {
- if (index < channelPlans.length) {
- return _getBadgeTextColor(channelPlans[index]);
- }
- return null;
- }
- /// 获取指定索引的标签边框颜色
- Color? getPlanBadgeBorderColor(int index) {
- if (index < channelPlans.length) {
- return _getBadgeBorderColor(channelPlans[index]);
- }
- return null;
- }
- /// 是否显示原价(有折扣时显示)
- bool showOriginalPrice(int index) {
- if (index < channelPlans.length) {
- final plan = channelPlans[index];
- return plan.orgPrice != null &&
- plan.price != null &&
- plan.orgPrice! > plan.price!;
- }
- return false;
- }
- /// 获取原价显示
- String getPlanOriginalPrice(int index) {
- if (index < channelPlans.length) {
- final plan = channelPlans[index];
- return _formatPrice(plan.orgPrice, plan.currency);
- }
- return '';
- }
- // ==================== 当前订阅相关方法 ====================
- /// 当前订阅状态(响应式)
- final _hasCurrentSubscription = false.obs;
- bool get hasCurrentSubscription => _hasCurrentSubscription.value;
- /// 当前订阅套餐信息(响应式)
- final Rxn<ChannelPlan> _currentPlanInfo = Rxn<ChannelPlan>();
- ChannelPlan? get currentPlanInfo => _currentPlanInfo.value;
- /// 获取当前订阅套餐标题
- String get currentPlanTitle {
- return currentPlanInfo?.title ?? '';
- }
- /// 获取当前订阅套餐价格显示
- String get currentPlanPriceDisplay {
- final plan = currentPlanInfo;
- if (plan == null) return '';
- return '${plan.title}-${plan.subTitle}';
- }
- /// 刷新当前订阅状态
- void refreshSubscriptionStatus() {
- final user = IXSP.getUser();
- _hasCurrentSubscription.value =
- user?.userLevel == 3 &&
- user?.planInfo != null &&
- user?.planInfo?.channelItemId?.isNotEmpty == true;
- _currentPlanInfo.value = user?.userLevel == 3 ? user?.planInfo : null;
- }
- }
|