Bladeren bron

feat: 虚拟支付

lilu 3 maanden geleden
bovenliggende
commit
5f634a9b59

+ 315 - 0
in_app_purchase_readme.md

@@ -0,0 +1,315 @@
+# In-App Purchase 工具使用说明
+
+## 概述
+
+`InAppPurchaseUtil` 是一个基于 `in_app_purchase: ^3.2.3` 封装的内购工具类,提供了简单易用的 API 来处理应用内购买功能。
+
+## 功能特性
+
+- ✅ 单例模式,全局统一管理
+- ✅ 支持 iOS 和 Android 双平台
+- ✅ 自动处理购买流程和状态更新
+- ✅ 支持订阅型和消耗型产品
+- ✅ 支持恢复购买
+- ✅ 完整的回调机制
+- ✅ 详细的日志输出
+
+## 快速开始
+
+### 1. 初始化内购
+
+在应用启动或需要使用内购的页面初始化:
+
+```dart
+final InAppPurchaseUtil _iapUtil = InAppPurchaseUtil.instance;
+
+// 初始化并设置回调
+await _iapUtil.initialize(
+  onSuccess: (purchaseDetails) {
+    print('购买成功: ${purchaseDetails.productID}');
+    // 处理购买成功逻辑
+  },
+  onError: (purchaseDetails) {
+    print('购买失败: ${purchaseDetails.error?.message}');
+  },
+  onCancelled: (purchaseDetails) {
+    print('购买取消');
+  },
+  onPending: (purchaseDetails) {
+    print('购买处理中');
+  },
+  onRestore: (purchaseDetails) {
+    print('恢复购买成功: ${purchaseDetails.productID}');
+  },
+);
+```
+
+### 2. 加载产品信息
+
+```dart
+// 定义产品 ID
+final productIds = {
+  'com.yourapp.weekly',     // 周订阅
+  'com.yourapp.monthly',    // 月订阅
+  'com.yourapp.yearly',     // 年订阅
+  'com.yourapp.lifetime',   // 终身会员
+};
+
+// 加载产品
+bool success = await _iapUtil.loadProducts(productIds);
+
+if (success) {
+  // 获取产品列表
+  List<ProductDetails> products = _iapUtil.products;
+  
+  // 显示产品信息
+  for (var product in products) {
+    print('${product.title}: ${product.price}');
+  }
+}
+```
+
+### 3. 购买产品
+
+```dart
+// 方法 1: 通过产品 ID 购买(推荐)
+await _iapUtil.purchaseProductById('com.yourapp.yearly');
+
+// 方法 2: 通过产品详情购买
+final product = _iapUtil.getProductById('com.yourapp.yearly');
+if (product != null) {
+  await _iapUtil.purchaseProduct(product);
+}
+
+// 购买消耗型产品
+await _iapUtil.purchaseProduct(product, isConsumable: true);
+```
+
+### 4. 恢复购买
+
+```dart
+await _iapUtil.restorePurchases();
+// 恢复结果会在初始化时设置的 onRestore 回调中返回
+```
+
+## 完整示例
+
+参考 `in_app_purchase_example.dart` 文件查看完整使用示例。
+
+### Controller 中使用示例
+
+```dart
+class MyController extends GetxController {
+  final InAppPurchaseUtil _iapUtil = InAppPurchaseUtil.instance;
+  final isLoading = false.obs;
+  final products = <ProductDetails>[].obs;
+
+  @override
+  void onInit() {
+    super.onInit();
+    _initIAP();
+  }
+
+  Future<void> _initIAP() async {
+    // 初始化
+    await _iapUtil.initialize(
+      onSuccess: _handleSuccess,
+      onError: _handleError,
+    );
+
+    // 加载产品
+    await _loadProducts();
+  }
+
+  Future<void> _loadProducts() async {
+    final productIds = {'com.yourapp.premium'};
+    await _iapUtil.loadProducts(productIds);
+    products.value = _iapUtil.products;
+  }
+
+  void _handleSuccess(PurchaseDetails details) {
+    // 更新用户状态
+    // 解锁高级功能
+    // 显示成功提示
+  }
+
+  void _handleError(PurchaseDetails details) {
+    // 显示错误提示
+  }
+
+  // 购买
+  Future<void> purchase(String productId) async {
+    await _iapUtil.purchaseProductById(productId);
+  }
+
+  // 恢复购买
+  Future<void> restore() async {
+    await _iapUtil.restorePurchases();
+  }
+}
+```
+
+## API 文档
+
+### 初始化方法
+
+#### `initialize()`
+
+初始化内购功能并设置回调。
+
+**参数:**
+- `onUpdate`: 购买状态更新回调(可选)
+- `onSuccess`: 购买成功回调(可选)
+- `onError`: 购买失败回调(可选)
+- `onCancelled`: 购买取消回调(可选)
+- `onPending`: 购买等待中回调(可选)
+- `onRestore`: 恢复购买成功回调(可选)
+
+**返回值:** `Future<bool>` - 是否初始化成功
+
+### 产品管理方法
+
+#### `loadProducts(Set<String> productIds)`
+
+加载产品信息。
+
+**参数:**
+- `productIds`: 产品 ID 集合
+
+**返回值:** `Future<bool>` - 是否加载成功
+
+#### `getProductById(String productId)`
+
+根据产品 ID 获取产品详情。
+
+**参数:**
+- `productId`: 产品 ID
+
+**返回值:** `ProductDetails?` - 产品详情,未找到返回 null
+
+### 购买方法
+
+#### `purchaseProduct(ProductDetails productDetails, {bool isConsumable = false})`
+
+购买产品。
+
+**参数:**
+- `productDetails`: 产品详情
+- `isConsumable`: 是否是消耗型产品,默认 false
+
+**返回值:** `Future<bool>` - 是否发起购买成功
+
+#### `purchaseProductById(String productId)`
+
+通过产品 ID 购买产品。
+
+**参数:**
+- `productId`: 产品 ID
+
+**返回值:** `Future<bool>` - 是否发起购买成功
+
+#### `restorePurchases()`
+
+恢复购买。
+
+**返回值:** `Future<bool>` - 是否发起恢复成功
+
+### 其他方法
+
+#### `completePurchase(PurchaseDetails purchaseDetails)`
+
+完成购买(通常由工具类自动调用)。
+
+#### `dispose()`
+
+清理资源(在应用退出时调用)。
+
+## 配置说明
+
+### iOS 配置
+
+1. 在 App Store Connect 中创建内购商品
+2. 配置 StoreKit Configuration 文件(用于本地测试)
+3. 在 `Runner/Info.plist` 中添加必要的配置
+
+### Android 配置
+
+1. 在 Google Play Console 中创建应用内商品
+2. 配置结算权限
+3. 在 `AndroidManifest.xml` 中添加必要的权限
+
+## 注意事项
+
+### 服务器验证
+
+⚠️ **重要**: 当前工具类中的 `_verifyPurchase()` 方法只是一个占位实现。在生产环境中,你必须:
+
+1. 将购买凭证发送到你的后端服务器
+2. 服务器验证凭证的真实性(调用 Apple/Google 验证 API)
+3. 根据验证结果处理业务逻辑
+
+示例验证流程:
+
+```dart
+Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
+  try {
+    // 发送到服务器验证
+    final response = await dio.post('/api/verify-purchase', data: {
+      'receipt': purchaseDetails.verificationData.serverVerificationData,
+      'productId': purchaseDetails.productID,
+      'platform': Platform.isIOS ? 'ios' : 'android',
+    });
+    
+    return response.data['valid'] == true;
+  } catch (e) {
+    debugPrint('验证购买失败: $e');
+    return false;
+  }
+}
+```
+
+### 测试建议
+
+1. **沙盒测试**: 在 iOS 和 Android 上使用测试账号进行沙盒测试
+2. **本地测试**: iOS 可以使用 StoreKit Configuration 文件进行本地测试
+3. **真实测试**: 在发布前进行真实的购买和恢复测试
+
+### 常见问题
+
+**Q: 为什么产品加载失败?**
+
+A: 检查以下几点:
+- 产品 ID 是否正确
+- 是否已在 App Store Connect/Google Play Console 中创建商品
+- 商品状态是否为"准备提交"或"批准销售"
+- 是否使用了正确的 Bundle ID/Package Name
+
+**Q: 购买后如何同步到服务器?**
+
+A: 在 `onSuccess` 回调中:
+1. 验证购买凭证
+2. 将购买信息同步到服务器
+3. 更新用户订阅状态
+
+**Q: 如何处理离线购买?**
+
+A: 工具会自动保存未完成的购买,在下次启动时自动处理。确保在应用启动时调用 `initialize()`。
+
+## 更新日志
+
+### v1.0.0 (2025-11-13)
+- ✨ 首次发布
+- ✅ 支持 iOS 和 Android
+- ✅ 完整的购买流程处理
+- ✅ 恢复购买功能
+
+## 相关资源
+
+- [in_app_purchase 官方文档](https://pub.dev/packages/in_app_purchase)
+- [Apple In-App Purchase 文档](https://developer.apple.com/in-app-purchase/)
+- [Google Play Billing 文档](https://developer.android.com/google/play/billing)
+
+## 支持
+
+如有问题,请查看示例代码或提交 Issue。
+

+ 20 - 0
ios/Podfile.lock

@@ -19,6 +19,11 @@ PODS:
     - Flutter
   - flutter_secure_storage (6.0.0):
     - Flutter
+  - image_gallery_saver_plus (0.0.1):
+    - Flutter
+  - in_app_purchase_storekit (0.0.1):
+    - Flutter
+    - FlutterMacOS
   - network_info_plus (0.0.1):
     - Flutter
   - OrderedSet (6.0.3)
@@ -39,6 +44,9 @@ PODS:
     - FlutterMacOS
   - url_launcher_ios (0.0.1):
     - Flutter
+  - video_player_avfoundation (0.0.1):
+    - Flutter
+    - FlutterMacOS
 
 DEPENDENCIES:
   - app_links (from `.symlinks/plugins/app_links/ios`)
@@ -49,6 +57,8 @@ DEPENDENCIES:
   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
+  - image_gallery_saver_plus (from `.symlinks/plugins/image_gallery_saver_plus/ios`)
+  - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`)
   - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@@ -57,6 +67,7 @@ DEPENDENCIES:
   - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
   - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
   - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+  - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
 
 SPEC REPOS:
   trunk:
@@ -79,6 +90,10 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/flutter_native_splash/ios"
   flutter_secure_storage:
     :path: ".symlinks/plugins/flutter_secure_storage/ios"
+  image_gallery_saver_plus:
+    :path: ".symlinks/plugins/image_gallery_saver_plus/ios"
+  in_app_purchase_storekit:
+    :path: ".symlinks/plugins/in_app_purchase_storekit/darwin"
   network_info_plus:
     :path: ".symlinks/plugins/network_info_plus/ios"
   package_info_plus:
@@ -95,6 +110,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/sqflite_darwin/darwin"
   url_launcher_ios:
     :path: ".symlinks/plugins/url_launcher_ios/ios"
+  video_player_avfoundation:
+    :path: ".symlinks/plugins/video_player_avfoundation/darwin"
 
 SPEC CHECKSUMS:
   app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
@@ -105,6 +122,8 @@ SPEC CHECKSUMS:
   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
+  image_gallery_saver_plus: e597bf65a7846979417a3eae0763b71b6dfec6c3
+  in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
   network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
   OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
   package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
@@ -114,6 +133,7 @@ SPEC CHECKSUMS:
   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
+  video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
 
 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
 

+ 11 - 0
ios/Runner/AppDelegate.swift

@@ -3,11 +3,22 @@ import UIKit
 
 @main
 @objc class AppDelegate: FlutterAppDelegate {
+  private var coreApi: CoreApiImpl?
+  
   override func application(
     _ application: UIApplication,
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
   ) -> Bool {
+    let controller = window?.rootViewController as! FlutterViewController
+    
+    // 创建 CoreApi 实例
+    coreApi = CoreApiImpl(controller: controller)
+    
+    // 注册 CoreApi
+    CoreApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: coreApi)
+    
     GeneratedPluginRegistrant.register(with: self)
+    
     return super.application(application, didFinishLaunchingWithOptions: launchOptions)
   }
 }

+ 37 - 13
ios/Runner/CoreApiImpl.swift

@@ -11,36 +11,60 @@ class CoreApiImpl: CoreApi {
         self.controller = controller
     }
 
-    func getApps() -> String? {
-        return "getApps"
+    // getApps 是异步方法,需要使用 completion handler
+    func getApps(completion: @escaping (Result<String?, Error>) -> Void) {
+        // TODO: 实现获取应用列表的逻辑
+        completion(.success("getApps"))
     }
 
-    func getSystemLocale() -> String? {
-        return "getSystemLocale"
+    func getSystemLocale() throws -> String? {
+        // TODO: 实现获取系统语言的逻辑
+        return Locale.current.identifier
     }
 
-    func connect() -> Bool? {
+    func connect(sessionId: String, socksPort: Int64, tunnelConfig: String, configJson: String) throws -> Bool? {
+        // TODO: 实现 VPN 连接逻辑
         return true
     }
 
-    func disconnect() -> Bool? {
+    func disconnect() throws -> Bool? {
+        // TODO: 实现 VPN 断开逻辑
         return true
     }
 
-    func getRemoteIp() -> String? {
-        return "getRemoteIp"
+    func getRemoteIp() throws -> String? {
+        // TODO: 实现获取远程 IP 的逻辑
+        return nil
     }
 
-    func getAdvertisingId() -> String? {
-        return "getAdvertisingId"
+    func getAdvertisingId() throws -> String? {
+        // TODO: 实现获取广告 ID 的逻辑
+        return nil
     }
 
-    func moveTaskToBack() -> Bool? {
-        return true
+    func moveTaskToBack() throws -> Bool? {
+        // TODO: iOS 不支持此操作,返回 false
+        return false
+    }
+
+    func isConnected() throws -> Bool? {
+        // TODO: 实现检查 VPN 连接状态的逻辑
+        return false
     }
 
-    func isConnected() -> Bool? {
+    func getSimInfo() throws -> String? {
+        // TODO: 实现获取 SIM 卡信息的逻辑
+        return nil
+    }
+
+    func reconnect() throws -> Bool? {
+        // TODO: 实现 VPN 重连逻辑
         return true
     }
+
+    func getChannel() throws -> String? {
+        // TODO: 实现获取渠道信息的逻辑
+        return nil
+    }
 }
 

+ 17 - 16
lib/app/controllers/api_controller.dart

@@ -62,16 +62,25 @@ class ApiController extends GetxService {
         fp.channel = channel ?? 'unknown';
       } catch (e) {
         log(TAG, 'read app channel error: $e');
-        fp.channel = 'unknown';
+        fp.channel = '';
+      }
+      try {
+        final advertisingId = await CoreApi().getAdvertisingId();
+        fp.googleId = advertisingId ?? '';
+      } catch (e) {
+        log(TAG, 'read app googleId error: $e');
+        fp.googleId = '';
+      }
+      try {
+        ReferrerDetails referrerDetails =
+            await PlayInstallReferrer.installReferrer;
+        fp.refer = referrerDetails.installReferrer ?? '';
+      } catch (e) {
+        log(TAG, 'get install referrer error: $e');
+        fp.refer = '';
       }
     }
-    try {
-      final advertisingId = await CoreApi().getAdvertisingId();
-      fp.googleId = advertisingId ?? '';
-    } catch (e) {
-      log(TAG, 'read app googleId error: $e');
-      fp.googleId = '';
-    }
+
     // 读取应用信息
     final info = await PackageInfo.fromPlatform();
     fp.appVersionCode = int.tryParse(info.buildNumber) ?? 0;
@@ -98,14 +107,6 @@ class ApiController extends GetxService {
 
     // 读取设备ID
     fp.deviceId = DeviceManager.getCacheDeviceId();
-
-    try {
-      ReferrerDetails referrerDetails =
-          await PlayInstallReferrer.installReferrer;
-      fp.refer = referrerDetails.installReferrer ?? '';
-    } catch (e) {
-      log(TAG, 'get install referrer error: $e');
-    }
     fp.isNewInstall = IXSP.getIsNewInstall();
     await updateFingerprintData();
     return fp;

+ 21 - 0
lib/app/dialog/all_dialog.dart

@@ -3,6 +3,7 @@ import 'package:get/get.dart';
 import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
 import 'package:nomo/config/translations/strings_enum.dart';
 import '../constants/iconfont/iconfont.dart';
+import '../routes/app_pages.dart';
 import 'custom_dialog.dart';
 
 /// 弹窗使用示例
@@ -78,6 +79,7 @@ class AllDialog {
         print('User confirmed logout');
         // 这里可以调用退出登录的API
         Navigator.of(Get.context!).pop();
+        Get.toNamed(Routes.LOGIN);
       },
       onCancel: () {
         // 处理取消退出逻辑
@@ -119,6 +121,25 @@ class AllDialog {
     );
   }
 
+  /// 显示无效授权码弹窗
+  static void showInvalidAuthorizationCode() {
+    CustomDialog.showError(
+      title: Strings.invalidAuthorizationCode.tr,
+      message: Strings.invalidAuthorizationCodeMessage.tr,
+      buttonText: Strings.invalidAuthorizationCodeButton.tr,
+      onPressed: () {
+        // 处理重试逻辑
+        print('Retry authorization code');
+        Navigator.of(Get.context!).pop();
+      },
+      onCancel: () {
+        // 处理取消逻辑
+        print('Cancel authorization code retry');
+        Navigator.of(Get.context!).pop();
+      },
+    );
+  }
+
   /// 显示自定义成功弹窗
   static void showCustomSuccess({
     required String title,

+ 6 - 6
lib/app/modules/deviceauth/views/deviceauth_view.dart

@@ -19,7 +19,8 @@ class DeviceauthView extends BaseView<DeviceauthController> {
   const DeviceauthView({super.key});
 
   @override
-  PreferredSizeWidget? get appBar => IXAppBar(title: Strings.deviceAuthorization.tr);
+  PreferredSizeWidget? get appBar =>
+      IXAppBar(title: Strings.deviceAuthorization.tr);
 
   @override
   Widget buildContent(BuildContext context) {
@@ -276,8 +277,8 @@ class DeviceauthView extends BaseView<DeviceauthController> {
   /// 设备管理区域
   Widget _buildDeviceManagementSection() {
     return Obx(() {
-      final authStatus = controller.authorizationStatus.value;
-      final isConfiguring = authStatus == AuthorizationStatus.configuring;
+      final inputCodeLength = controller.inputCode.value.length;
+      final shouldShowLoadingIcon = inputCodeLength == 6;
 
       return Container(
         margin: EdgeInsets.only(top: 20.w),
@@ -297,9 +298,8 @@ class DeviceauthView extends BaseView<DeviceauthController> {
               );
             }),
 
-            // 加载中状态或等待激活的设备
-            if (isConfiguring || controller.inputCode.value.isEmpty)
-              const AwaitingActivationCard(),
+            // 默认显示等待激活的设备,只有当输入代码长度为6时显示加载图标
+            AwaitingActivationCard(showLoadingIcon: shouldShowLoadingIcon),
           ],
         ),
       );

+ 7 - 3
lib/app/modules/deviceauth/widgets/device_card.dart

@@ -152,7 +152,9 @@ class DeviceCard extends StatelessWidget {
 
 /// 等待激活设备卡片
 class AwaitingActivationCard extends StatelessWidget {
-  const AwaitingActivationCard({super.key});
+  final bool showLoadingIcon;
+
+  const AwaitingActivationCard({super.key, this.showLoadingIcon = false});
 
   @override
   Widget build(BuildContext context) {
@@ -180,7 +182,9 @@ class AwaitingActivationCard extends StatelessWidget {
           // 等待激活文字
           Expanded(
             child: Text(
-              Strings.awaitingActivation.tr,
+              showLoadingIcon
+                  ? Strings.configureAuthorizedDevices.tr
+                  : Strings.awaitingActivation.tr,
               style: TextStyle(
                 fontSize: 13.sp,
                 color: Get.reactiveTheme.hintColor,
@@ -189,7 +193,7 @@ class AwaitingActivationCard extends StatelessWidget {
             ),
           ),
 
-          const InfiniteRotateIcon(),
+          if (showLoadingIcon) const InfiniteRotateIcon(),
         ],
       ),
     );

+ 2 - 0
lib/app/modules/home/controllers/home_controller.dart

@@ -1,5 +1,6 @@
 import 'package:get/get.dart';
 import '../../../../utils/log/logger.dart';
+import '../../../../utils/permission_manager.dart';
 import '../../../base/base_controller.dart';
 import '../../../controllers/core_controller.dart';
 import '../../../data/models/launch/groups.dart';
@@ -57,6 +58,7 @@ class HomeController extends BaseController {
   void onInit() {
     super.onInit();
     _initializeLocations();
+    PermissionManager.requestNotificationPermission();
   }
 
   /// 初始化位置数据

+ 114 - 30
lib/app/modules/home/widgets/connection_button.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart' hide ConnectionState;
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'dart:math' as math;
+import 'dart:async';
 import 'package:get/get.dart';
 import 'package:nomo/app/widgets/ix_image.dart';
 
@@ -24,6 +25,9 @@ class _ConnectionButtonState extends State<ConnectionButton>
   bool _isAtTop = false; // 控制按钮位置,false=底部,true=顶部
   List<Color>? _previousGradientColor; // 保存前一个渐变色用于动画过渡
   late AnimationController _rotationController; // 旋转动画控制器
+  Timer? _connectingTimer; // 连接中状态的计时器
+  int _connectingTextIndex = 0; // 当前显示的连接文本索引(0-4)
+  ConnectionState? _previousState; // 保存前一个连接状态
 
   @override
   void initState() {
@@ -38,6 +42,7 @@ class _ConnectionButtonState extends State<ConnectionButton>
   @override
   void dispose() {
     _rotationController.dispose();
+    _connectingTimer?.cancel();
     super.dispose();
   }
 
@@ -47,6 +52,45 @@ class _ConnectionButtonState extends State<ConnectionButton>
     }
   }
 
+  // 启动连接中状态的文本轮播计时器
+  void _startConnectingTimer() {
+    _connectingTimer?.cancel();
+    _connectingTextIndex = 0;
+    _connectingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
+      if (mounted) {
+        setState(() {
+          // 每秒切换到下一个文本,循环显示0-4
+          _connectingTextIndex = (_connectingTextIndex + 1) % 5;
+        });
+      }
+    });
+  }
+
+  // 停止连接中状态的文本轮播计时器
+  void _stopConnectingTimer() {
+    _connectingTimer?.cancel();
+    _connectingTimer = null;
+    _connectingTextIndex = 0;
+  }
+
+  // 根据索引获取对应的连接文本
+  String _getConnectingText() {
+    switch (_connectingTextIndex) {
+      case 0:
+        return Strings.securingData.tr;
+      case 1:
+        return Strings.encryptingTraffic.tr;
+      case 2:
+        return Strings.protectingPrivacy.tr;
+      case 3:
+        return Strings.safeConnection.tr;
+      case 4:
+        return Strings.yourDataIsSafe.tr;
+      default:
+        return Strings.connecting.tr;
+    }
+  }
+
   // 比较两个颜色列表是否相等
   bool _colorsEqual(List<Color> a, List<Color> b) {
     if (a.length != b.length) return false;
@@ -116,11 +160,17 @@ class _ConnectionButtonState extends State<ConnectionButton>
         statusImgPath = Assets.disconnected;
         text = Strings.disconnected.tr;
         textColor = Get.reactiveTheme.hintColor;
-        if (_isAtTop) {
-          setState(() {
-            _isAtTop = false;
+        // 只在状态改变时执行一次
+        if (_previousState != ConnectionState.disconnected && _isAtTop) {
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (mounted) {
+              setState(() {
+                _isAtTop = false;
+              });
+            }
           });
         }
+        _stopConnectingTimer(); // 停止计时器
         break;
       case ConnectionState.connecting:
         gradientColor = [
@@ -129,13 +179,22 @@ class _ConnectionButtonState extends State<ConnectionButton>
         ];
         imgPath = Assets.switchStatusConnecting;
         statusImgPath = Assets.connecting;
-        text = Strings.connecting.tr;
+        text = _getConnectingText(); // 使用轮播文本
         textColor = Get.reactiveTheme.hintColor;
-        // 切换位置
-        setState(() {
-          _isAtTop = !_isAtTop;
-        });
-
+        // 只在状态改变时执行一次切换位置
+        if (_previousState != ConnectionState.connecting) {
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (mounted) {
+              setState(() {
+                _isAtTop = !_isAtTop;
+              });
+            }
+          });
+        }
+        // 启动连接文本轮播计时器
+        if (_connectingTimer == null || !_connectingTimer!.isActive) {
+          _startConnectingTimer();
+        }
         break;
       case ConnectionState.connected:
         gradientColor = [
@@ -146,11 +205,17 @@ class _ConnectionButtonState extends State<ConnectionButton>
         statusImgPath = Assets.connected;
         text = Strings.connected.tr;
         textColor = Get.reactiveTheme.textTheme.bodyLarge!.color!;
-        if (!_isAtTop) {
-          setState(() {
-            _isAtTop = true;
+        // 只在状态改变时执行一次
+        if (_previousState != ConnectionState.connected && !_isAtTop) {
+          WidgetsBinding.instance.addPostFrameCallback((_) {
+            if (mounted) {
+              setState(() {
+                _isAtTop = true;
+              });
+            }
           });
         }
+        _stopConnectingTimer(); // 停止计时器
         break;
       case ConnectionState.error:
         gradientColor = [
@@ -161,6 +226,7 @@ class _ConnectionButtonState extends State<ConnectionButton>
         statusImgPath = Assets.error;
         text = Strings.error.tr;
         textColor = Get.reactiveTheme.hintColor;
+        _stopConnectingTimer(); // 停止计时器
         break;
     }
 
@@ -179,6 +245,15 @@ class _ConnectionButtonState extends State<ConnectionButton>
       });
     }
 
+    // 更新前一个状态
+    if (_previousState != widget.state) {
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        if (mounted) {
+          _previousState = widget.state;
+        }
+      });
+    }
+
     return Column(
       children: [
         TweenAnimationBuilder<List<Color>>(
@@ -233,25 +308,34 @@ class _ConnectionButtonState extends State<ConnectionButton>
           ),
         ),
         20.verticalSpaceFromWidth,
-        Row(
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: [
-            IXImage(
-              source: statusImgPath,
-              sourceType: ImageSourceType.asset,
-              width: 14.w,
-              height: 14.w,
-            ),
-            4.horizontalSpace,
-            Text(
-              text,
-              style: TextStyle(
-                fontSize: 14.sp,
-                fontWeight: FontWeight.w500,
-                color: textColor,
+        AnimatedSwitcher(
+          duration: const Duration(milliseconds: 400),
+          switchInCurve: Curves.easeIn,
+          switchOutCurve: Curves.easeOut,
+          transitionBuilder: (Widget child, Animation<double> animation) {
+            return FadeTransition(opacity: animation, child: child);
+          },
+          child: Row(
+            key: ValueKey(text), // 使用文本作为 key,确保文字改变时触发动画
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              IXImage(
+                source: statusImgPath,
+                sourceType: ImageSourceType.asset,
+                width: 14.w,
+                height: 14.w,
               ),
-            ),
-          ],
+              4.horizontalSpace,
+              Text(
+                text,
+                style: TextStyle(
+                  fontSize: 14.sp,
+                  fontWeight: FontWeight.w500,
+                  color: textColor,
+                ),
+              ),
+            ],
+          ),
         ),
       ],
     );

+ 190 - 16
lib/app/modules/subscription/controllers/subscription_controller.dart

@@ -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();
   }
 
   // 支付问题

+ 12 - 1
lib/config/translations/ar_AR/ar_ar_translation.dart

@@ -196,6 +196,10 @@ final Map<String, String> arAR = {
   Strings.currentDevice: 'الجهاز الحالي',
   Strings.androidDevices: 'أجهزة Android',
   Strings.authCodeCopied: 'تم نسخ رمز التفويض إلى الحافظة',
+  Strings.invalidAuthorizationCode: 'رمز التفويض غير صالح',
+  Strings.invalidAuthorizationCodeMessage:
+      'الرمز الذي أدخلته غير صحيح أو منتهي الصلاحية.\nيرجى التحقق من الرمز المكون من 6 أرقام على الجهاز الآخر والمحاولة مرة أخرى.',
+  Strings.invalidAuthorizationCodeButton: 'حاول مرة أخرى',
 
   // Pre Code Email
   Strings.codeBackedUpMessage:
@@ -248,6 +252,13 @@ final Map<String, String> arAR = {
   Strings.activeTime: 'الوقت النشط',
   Strings.yearPlanPrice: 'الخطة السنوية @price سنوياً',
 
+  // Connecting status carousel texts
+  Strings.securingData: 'تأمين البيانات…',
+  Strings.encryptingTraffic: 'تشفير حركة المرور…',
+  Strings.protectingPrivacy: 'حماية الخصوصية…',
+  Strings.safeConnection: 'اتصال آمن…',
+  Strings.yourDataIsSafe: 'بياناتك آمنة…',
+
   // Login & Signup
   Strings.login: 'تسجيل الدخول',
   Strings.loginButton: 'تسجيل الدخول',
@@ -266,7 +277,7 @@ final Map<String, String> arAR = {
   Strings.loginNow: ' تسجيل الدخول الآن',
 
   // Feedback
-  Strings.feedbackPlaceholder: 'نسخة إنجليزية للمرجع: صف\nمشكلتك أو اقتراحك...',
+  Strings.feedbackPlaceholder: 'نسخة إنجليزية للمرجع: صف مشكلتك أو اقتراحك...',
   Strings.emailAddressForReply: '• عنوان بريدك الإلكتروني (لردنا)',
   Strings.send: 'إرسال',
 

+ 12 - 1
lib/config/translations/de_DE/de_de_translation.dart

@@ -198,6 +198,10 @@ const Map<String, String> deDE = {
   Strings.currentDevice: 'Aktuelles Gerät',
   Strings.androidDevices: 'Android-Geräte',
   Strings.authCodeCopied: 'Autorisierungscode in die Zwischenablage kopiert',
+  Strings.invalidAuthorizationCode: 'Ungültiger Autorisierungscode',
+  Strings.invalidAuthorizationCodeMessage:
+      'Der eingegebene Code ist falsch oder abgelaufen.\nBitte überprüfen Sie den 6-stelligen Code auf dem anderen Gerät und versuchen Sie es erneut.',
+  Strings.invalidAuthorizationCodeButton: 'Erneut versuchen',
 
   // Pre Code Email
   Strings.codeBackedUpMessage: 'Ihr Code wird in dieser E-Mail gesichert.',
@@ -251,6 +255,13 @@ const Map<String, String> deDE = {
   Strings.activeTime: 'Aktive Zeit',
   Strings.yearPlanPrice: 'Jahresplan @price pro Jahr',
 
+  // Connecting status carousel texts
+  Strings.securingData: 'Daten werden gesichert…',
+  Strings.encryptingTraffic: 'Datenverkehr wird verschlüsselt…',
+  Strings.protectingPrivacy: 'Privatsphäre wird geschützt…',
+  Strings.safeConnection: 'Sichere Verbindung…',
+  Strings.yourDataIsSafe: 'Ihre Daten sind sicher…',
+
   // Login & Signup
   Strings.login: 'Anmelden',
   Strings.loginButton: 'Anmelden',
@@ -270,7 +281,7 @@ const Map<String, String> deDE = {
 
   // Feedback
   Strings.feedbackPlaceholder:
-      'Eine englische Version als Referenz: Beschreiben Sie\nIhr Problem oder Ihren Vorschlag...',
+      'Eine englische Version als Referenz: Beschreiben Sie Ihr Problem oder Ihren Vorschlag...',
   Strings.emailAddressForReply: '• Ihre E-Mail-Adresse (für unsere Antwort)',
   Strings.send: 'Senden',
 

+ 12 - 1
lib/config/translations/en_US/en_us_translation.dart

@@ -202,6 +202,10 @@ Map<String, String> enUs = {
   Strings.currentDevice: 'Current Device',
   Strings.androidDevices: 'Android devices',
   Strings.authCodeCopied: 'Authorization code copied to clipboard',
+  Strings.invalidAuthorizationCode: 'Invalid Authorization Code',
+  Strings.invalidAuthorizationCodeMessage:
+      'The code you entered is incorrect or has expired.\nPlease check the 6-digit code on the other device and try again.',
+  Strings.invalidAuthorizationCodeButton: 'Try again',
 
   // Pre Code Email
   Strings.codeBackedUpMessage: 'Your code will be backed up to this email.',
@@ -253,6 +257,13 @@ Map<String, String> enUs = {
   Strings.activeTime: 'Active time',
   Strings.yearPlanPrice: 'Year Plan @price per year',
 
+  // Connecting status carousel texts
+  Strings.securingData: 'Securing data…',
+  Strings.encryptingTraffic: 'Encrypting traffic…',
+  Strings.protectingPrivacy: 'Protecting privacy…',
+  Strings.safeConnection: 'Safe connection…',
+  Strings.yourDataIsSafe: 'Your data is safe…',
+
   // Login & Signup
   Strings.login: 'Log in',
   Strings.loginButton: 'Log In',
@@ -272,7 +283,7 @@ Map<String, String> enUs = {
 
   // Feedback
   Strings.feedbackPlaceholder:
-      'An English version for reference: Describe\nyour issue or suggestion...',
+      'An English version for reference: Describe your issue or suggestion...',
   Strings.emailAddressForReply: '• Your email address (for our reply)',
   Strings.send: 'Send',
 

+ 12 - 1
lib/config/translations/es_ES/es_es_translation.dart

@@ -203,6 +203,10 @@ const Map<String, String> esEs = {
   Strings.currentDevice: 'Dispositivo actual',
   Strings.androidDevices: 'Dispositivos Android',
   Strings.authCodeCopied: 'Código de autorización copiado al portapapeles',
+  Strings.invalidAuthorizationCode: 'Código de autorización no válido',
+  Strings.invalidAuthorizationCodeMessage:
+      'El código que ingresaste es incorrecto o ha caducado.\nPor favor, verifica el código de 6 dígitos en el otro dispositivo e intenta nuevamente.',
+  Strings.invalidAuthorizationCodeButton: 'Intentar de nuevo',
 
   // Pre Code Email
   Strings.codeBackedUpMessage:
@@ -256,6 +260,13 @@ const Map<String, String> esEs = {
   Strings.activeTime: 'Tiempo activo',
   Strings.yearPlanPrice: 'Plan anual @price por año',
 
+  // Connecting status carousel texts
+  Strings.securingData: 'Protegiendo datos…',
+  Strings.encryptingTraffic: 'Cifrando tráfico…',
+  Strings.protectingPrivacy: 'Protegiendo privacidad…',
+  Strings.safeConnection: 'Conexión segura…',
+  Strings.yourDataIsSafe: 'Tus datos están seguros…',
+
   // Login & Signup
   Strings.login: 'Iniciar sesión',
   Strings.loginButton: 'Iniciar sesión',
@@ -275,7 +286,7 @@ const Map<String, String> esEs = {
 
   // Feedback
   Strings.feedbackPlaceholder:
-      'Una versión en inglés como referencia: Describe\ntu problema o sugerencia...',
+      'Una versión en inglés como referencia: Describe tu problema o sugerencia...',
   Strings.emailAddressForReply:
       '• Tu dirección de correo (para nuestra respuesta)',
   Strings.send: 'Enviar',

+ 12 - 1
lib/config/translations/fa_IR/fa_ir_translation.dart

@@ -200,6 +200,10 @@ const Map<String, String> faIR = {
   Strings.currentDevice: 'دستگاه فعلی',
   Strings.androidDevices: 'دستگاه‌های اندروید',
   Strings.authCodeCopied: 'کد مجوز در کلیپ‌بورد کپی شد',
+  Strings.invalidAuthorizationCode: 'کد مجوز نامعتبر',
+  Strings.invalidAuthorizationCodeMessage:
+      'کدی که وارد کرده‌اید نادرست است یا منقضی شده است.\nلطفاً کد ۶ رقمی را در دستگاه دیگر بررسی کنید و دوباره امتحان کنید.',
+  Strings.invalidAuthorizationCodeButton: 'دوباره تلاش کنید',
 
   // Pre Code Email
   Strings.codeBackedUpMessage: 'کد شما در این ایمیل پشتیبان‌گیری خواهد شد.',
@@ -251,6 +255,13 @@ const Map<String, String> faIR = {
   Strings.activeTime: 'زمان فعال',
   Strings.yearPlanPrice: 'طرح سالانه @price در سال',
 
+  // Connecting status carousel texts
+  Strings.securingData: 'در حال ایمن‌سازی داده‌ها…',
+  Strings.encryptingTraffic: 'در حال رمزگذاری ترافیک…',
+  Strings.protectingPrivacy: 'در حال حفاظت از حریم خصوصی…',
+  Strings.safeConnection: 'اتصال امن…',
+  Strings.yourDataIsSafe: 'داده‌های شما امن هستند…',
+
   // Login & Signup
   Strings.login: 'ورود',
   Strings.loginButton: 'ورود',
@@ -270,7 +281,7 @@ const Map<String, String> faIR = {
 
   // Feedback
   Strings.feedbackPlaceholder:
-      'نسخه انگلیسی برای مرجع: مشکل\nیا پیشنهاد خود را توضیح دهید...',
+      'نسخه انگلیسی برای مرجع: مشکل یا پیشنهاد خود را توضیح دهید...',
   Strings.emailAddressForReply: '• آدرس ایمیل شما (برای پاسخ ما)',
   Strings.send: 'ارسال',
 

+ 12 - 1
lib/config/translations/fr_FR/fr_fr_translation.dart

@@ -203,6 +203,10 @@ const Map<String, String> frFR = {
   Strings.currentDevice: 'Appareil actuel',
   Strings.androidDevices: 'Appareils Android',
   Strings.authCodeCopied: 'Code d\'autorisation copié dans le presse-papiers',
+  Strings.invalidAuthorizationCode: 'Code d\'autorisation non valide',
+  Strings.invalidAuthorizationCodeMessage:
+      'Le code que vous avez saisi est incorrect ou a expiré.\nVeuillez vérifier le code à 6 chiffres sur l\'autre appareil et réessayer.',
+  Strings.invalidAuthorizationCodeButton: 'Réessayer',
 
   // Pre Code Email
   Strings.codeBackedUpMessage: 'Votre code sera sauvegardé dans cet e-mail.',
@@ -257,6 +261,13 @@ const Map<String, String> frFR = {
   Strings.activeTime: 'Temps actif',
   Strings.yearPlanPrice: 'Forfait annuel @price par an',
 
+  // Connecting status carousel texts
+  Strings.securingData: 'Sécurisation des données…',
+  Strings.encryptingTraffic: 'Chiffrement du trafic…',
+  Strings.protectingPrivacy: 'Protection de la vie privée…',
+  Strings.safeConnection: 'Connexion sécurisée…',
+  Strings.yourDataIsSafe: 'Vos données sont en sécurité…',
+
   // Login & Signup
   Strings.login: 'Se connecter',
   Strings.loginButton: 'Se connecter',
@@ -276,7 +287,7 @@ const Map<String, String> frFR = {
 
   // Feedback
   Strings.feedbackPlaceholder:
-      'Une version anglaise pour référence : Décrivez\nvotre problème ou suggestion...',
+      'Une version anglaise pour référence : Décrivez votre problème ou suggestion...',
   Strings.emailAddressForReply: '• Votre adresse e-mail (pour notre réponse)',
   Strings.send: 'Envoyer',
 

+ 12 - 1
lib/config/translations/ja_JP/ja_jp_translation.dart

@@ -189,6 +189,10 @@ const Map<String, String> jaJP = {
   Strings.currentDevice: '現在のデバイス',
   Strings.androidDevices: 'Androidデバイス',
   Strings.authCodeCopied: '認証コードがクリップボードにコピーされました',
+  Strings.invalidAuthorizationCode: '無効な認証コード',
+  Strings.invalidAuthorizationCodeMessage:
+      '入力されたコードが正しくないか、有効期限が切れています。\n他のデバイスの6桁のコードを確認して、もう一度お試しください。',
+  Strings.invalidAuthorizationCodeButton: 'もう一度試す',
 
   // Pre Code Email
   Strings.codeBackedUpMessage: 'コードはこのメールにバックアップされます。',
@@ -236,6 +240,13 @@ const Map<String, String> jaJP = {
   Strings.activeTime: 'アクティブ時間',
   Strings.yearPlanPrice: '年間プラン @price 年間',
 
+  // Connecting status carousel texts
+  Strings.securingData: 'データを保護中…',
+  Strings.encryptingTraffic: 'トラフィックを暗号化中…',
+  Strings.protectingPrivacy: 'プライバシーを保護中…',
+  Strings.safeConnection: '安全な接続中…',
+  Strings.yourDataIsSafe: 'データは安全です…',
+
   // Login & Signup
   Strings.login: 'ログイン',
   Strings.loginButton: 'ログイン',
@@ -254,7 +265,7 @@ const Map<String, String> jaJP = {
   Strings.loginNow: ' 今すぐログイン',
 
   // Feedback
-  Strings.feedbackPlaceholder: '参考用の英語版:問題や提案を\n説明してください...',
+  Strings.feedbackPlaceholder: '参考用の英語版:問題や提案を説明してください...',
   Strings.emailAddressForReply: '• メールアドレス(返信用)',
   Strings.send: '送信',
 

+ 12 - 1
lib/config/translations/ko_KR/ko_kr_translation.dart

@@ -184,6 +184,10 @@ const Map<String, String> koKR = {
   Strings.currentDevice: '현재 장치',
   Strings.androidDevices: 'Android 장치',
   Strings.authCodeCopied: '인증 코드가 클립보드에 복사되었습니다',
+  Strings.invalidAuthorizationCode: '유효하지 않은 인증 코드',
+  Strings.invalidAuthorizationCodeMessage:
+      '입력하신 코드가 올바르지 않거나 만료되었습니다.\n다른 장치의 6자리 코드를 확인하고 다시 시도해 주세요.',
+  Strings.invalidAuthorizationCodeButton: '다시 시도',
 
   // Pre Code Email
   Strings.codeBackedUpMessage: '코드가 이 이메일에 백업됩니다.',
@@ -230,6 +234,13 @@ const Map<String, String> koKR = {
   Strings.activeTime: '활성 시간',
   Strings.yearPlanPrice: '연간 플랜 @price 연간',
 
+  // Connecting status carousel texts
+  Strings.securingData: '데이터 보호 중…',
+  Strings.encryptingTraffic: '트래픽 암호화 중…',
+  Strings.protectingPrivacy: '개인정보 보호 중…',
+  Strings.safeConnection: '안전한 연결 중…',
+  Strings.yourDataIsSafe: '데이터가 안전합니다…',
+
   // Login & Signup
   Strings.login: '로그인',
   Strings.loginButton: '로그인',
@@ -248,7 +259,7 @@ const Map<String, String> koKR = {
   Strings.loginNow: ' 지금 로그인',
 
   // Feedback
-  Strings.feedbackPlaceholder: '참고용 영어 버전: 문제나 제안을\n설명해주세요...',
+  Strings.feedbackPlaceholder: '참고용 영어 버전: 문제나 제안을 설명해주세요...',
   Strings.emailAddressForReply: '• 이메일 주소 (답변용)',
   Strings.send: '보내기',
 

+ 12 - 1
lib/config/translations/my_MM/my_mm_translation.dart

@@ -205,6 +205,10 @@ const Map<String, String> myMM = {
   Strings.currentDevice: 'လက်ရှိစက်ပစ္စည်း',
   Strings.androidDevices: 'Android စက်ပစ္စည်းများ',
   Strings.authCodeCopied: 'ခွင့်ပြုကုဒ်ကို clipboard သို့ ကူးယူပြီးပါပြီ',
+  Strings.invalidAuthorizationCode: 'မမှန်ကန်သော ခွင့်ပြုကုဒ်',
+  Strings.invalidAuthorizationCodeMessage:
+      'သင်ထည့်သွင်းသော ကုဒ်သည် မမှန်ကန်ပါ သို့မဟုတ် သက်တမ်းကုန်ဆုံးသွားပါပြီ။\nအခြားစက်ပစ္စည်းရှိ ဂဏန်း ၆ လုံးကုဒ်ကို စစ်ဆေးပြီး ထပ်မံကြိုးစားကြည့်ပါ။',
+  Strings.invalidAuthorizationCodeButton: 'ထပ်မံကြိုးစားမည်',
 
   // Pre Code Email
   Strings.codeBackedUpMessage: 'သင့်ကုဒ်ကို ဤအီးမေးလ်တွင် အရန်သိမ်းဆည်းမည်။',
@@ -258,6 +262,13 @@ const Map<String, String> myMM = {
   Strings.activeTime: 'တက်ကြွသောအချိန်',
   Strings.yearPlanPrice: 'နှစ်စဉ်အစီအစဉ် @price နှစ်စဉ်',
 
+  // Connecting status carousel texts
+  Strings.securingData: 'ဒေတာကို လုံခြုံအောင် လုပ်နေသည်…',
+  Strings.encryptingTraffic: 'အသွားအလာကို စာဝှက်နေသည်…',
+  Strings.protectingPrivacy: 'ကိုယ်ရေးကိုယ်တာကို ကာကွယ်နေသည်…',
+  Strings.safeConnection: 'လုံခြုံသော ချိတ်ဆက်မှု…',
+  Strings.yourDataIsSafe: 'သင့်ဒေတာ လုံခြုံပါသည်…',
+
   // Login & Signup
   Strings.login: 'အကောင့်ဝင်ရန်',
   Strings.loginButton: 'အကောင့်ဝင်ရန်',
@@ -278,7 +289,7 @@ const Map<String, String> myMM = {
 
   // Feedback
   Strings.feedbackPlaceholder:
-      'ရည်ညွှန်းအတွက် အင်္ဂလိပ်ဗားရှင်း: သင့်ပြဿနာ သို့မဟုတ်\nအကြံပြုချက်ကို ဖော်ပြပါ...',
+      'ရည်ညွှန်းအတွက် အင်္ဂလိပ်ဗားရှင်း: သင့်ပြဿနာ သို့မဟုတ် အကြံပြုချက်ကို ဖော်ပြပါ...',
   Strings.emailAddressForReply:
       '• သင့်အီးမေးလ်လိပ်စာ (ကျွန်ုပ်တို့၏ပြန်လည်ဖြေကြားမှုအတွက်)',
   Strings.send: 'ပို့ပါ',

+ 12 - 1
lib/config/translations/ru_RU/ru_ru_translation.dart

@@ -202,6 +202,10 @@ const Map<String, String> ruRU = {
   Strings.currentDevice: 'Текущее устройство',
   Strings.androidDevices: 'Android-устройства',
   Strings.authCodeCopied: 'Код авторизации скопирован в буфер обмена',
+  Strings.invalidAuthorizationCode: 'Неверный код авторизации',
+  Strings.invalidAuthorizationCodeMessage:
+      'Введённый код неверен или истёк срок его действия.\nПожалуйста, проверьте 6-значный код на другом устройстве и попробуйте снова.',
+  Strings.invalidAuthorizationCodeButton: 'Попробовать снова',
 
   // Pre Code Email
   Strings.codeBackedUpMessage:
@@ -254,6 +258,13 @@ const Map<String, String> ruRU = {
   Strings.activeTime: 'Активное время',
   Strings.yearPlanPrice: 'Годовой план @price в год',
 
+  // Connecting status carousel texts
+  Strings.securingData: 'Защита данных…',
+  Strings.encryptingTraffic: 'Шифрование трафика…',
+  Strings.protectingPrivacy: 'Защита конфиденциальности…',
+  Strings.safeConnection: 'Безопасное соединение…',
+  Strings.yourDataIsSafe: 'Ваши данные в безопасности…',
+
   // Login & Signup
   Strings.login: 'Войти',
   Strings.loginButton: 'Войти',
@@ -273,7 +284,7 @@ const Map<String, String> ruRU = {
 
   // Feedback
   Strings.feedbackPlaceholder:
-      'Английская версия для справки: Опишите\nвашу проблему или предложение...',
+      'Английская версия для справки: Опишите вашу проблему или предложение...',
   Strings.emailAddressForReply: '• Ваш адрес email (для нашего ответа)',
   Strings.send: 'Отправить',
 

+ 12 - 1
lib/config/translations/strings_enum.dart

@@ -218,6 +218,10 @@ class Strings {
       'Configure authorized devices...';
   static const String authorizeUpTo4DevicesAsPremium =
       'Authorize up to @max devices as Premium (@current/@max)';
+  static const String invalidAuthorizationCode = 'Invalid Authorization Code';
+  static const String invalidAuthorizationCodeMessage =
+      'The code you entered is incorrect or has expired.\nPlease check the 6-digit code on the other device and try again.';
+  static const String invalidAuthorizationCodeButton = 'Try again';
 
   // Pre Code Email
   static const String codeBackedUpMessage =
@@ -273,6 +277,13 @@ class Strings {
   static const String activeTime = 'Active time';
   static const String yearPlanPrice = 'Year Plan @price per year';
 
+  // Connecting status carousel texts
+  static const String securingData = 'Securing data…';
+  static const String encryptingTraffic = 'Encrypting traffic…';
+  static const String protectingPrivacy = 'Protecting privacy…';
+  static const String safeConnection = 'Safe connection…';
+  static const String yourDataIsSafe = 'Your data is safe…';
+
   // Login & Signup
   static const String login = 'Log in';
   static const String loginButton = 'Log In';
@@ -293,7 +304,7 @@ class Strings {
 
   // Feedback
   static const String feedbackPlaceholder =
-      'An English version for reference: Describe\nyour issue or suggestion...';
+      'An English version for reference: Describe your issue or suggestion...';
   static const String emailAddressForReply =
       '• Your email address (for our reply)';
   static const String send = 'Send';

+ 420 - 0
lib/utils/in_app_purchase_util.dart

@@ -0,0 +1,420 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:in_app_purchase/in_app_purchase.dart';
+import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
+import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';
+
+/// 内购工具类
+/// 封装了 in_app_purchase 的常用功能
+class InAppPurchaseUtil {
+  InAppPurchaseUtil._();
+
+  static final InAppPurchaseUtil _instance = InAppPurchaseUtil._();
+  static InAppPurchaseUtil get instance => _instance;
+
+  /// 内购实例
+  final InAppPurchase _inAppPurchase = InAppPurchase.instance;
+
+  /// 购买流订阅
+  StreamSubscription<List<PurchaseDetails>>? _subscription;
+
+  /// 是否可用
+  bool _available = false;
+
+  /// 产品列表
+  List<ProductDetails> _products = [];
+
+  /// 购买状态回调
+  Function(List<PurchaseDetails>)? onPurchaseUpdate;
+
+  /// 购买成功回调
+  Function(PurchaseDetails)? onPurchaseSuccess;
+
+  /// 购买失败回调
+  Function(PurchaseDetails)? onPurchaseError;
+
+  /// 购买取消回调
+  Function(PurchaseDetails)? onPurchaseCancelled;
+
+  /// 购买等待中回调
+  Function(PurchaseDetails)? onPurchasePending;
+
+  /// 恢复购买成功回调
+  Function(PurchaseDetails)? onRestoreSuccess;
+
+  /// 获取内购是否可用
+  bool get isAvailable => _available;
+
+  /// 获取产品列表
+  List<ProductDetails> get products => _products;
+
+  /// 初始化内购
+  ///
+  /// [productIds] 产品ID列表
+  /// [onUpdate] 购买状态更新回调
+  /// [onSuccess] 购买成功回调
+  /// [onError] 购买失败回调
+  /// [onCancelled] 购买取消回调
+  /// [onPending] 购买等待中回调
+  /// [onRestore] 恢复购买成功回调
+  Future<bool> initialize({
+    Function(List<PurchaseDetails>)? onUpdate,
+    Function(PurchaseDetails)? onSuccess,
+    Function(PurchaseDetails)? onError,
+    Function(PurchaseDetails)? onCancelled,
+    Function(PurchaseDetails)? onPending,
+    Function(PurchaseDetails)? onRestore,
+  }) async {
+    // 设置回调
+    onPurchaseUpdate = onUpdate;
+    onPurchaseSuccess = onSuccess;
+    onPurchaseError = onError;
+    onPurchaseCancelled = onCancelled;
+    onPurchasePending = onPending;
+    onRestoreSuccess = onRestore;
+
+    // 检查内购是否可用
+    _available = await _inAppPurchase.isAvailable();
+    if (!_available) {
+      debugPrint('内购不可用');
+      return false;
+    }
+
+    // 初始化平台特定的配置
+    if (Platform.isIOS) {
+      final iosPlatformAddition = _inAppPurchase
+          .getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
+      await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
+    }
+
+    // 监听购买流
+    _subscription = _inAppPurchase.purchaseStream.listen(
+      _handlePurchaseUpdate,
+      onDone: () {
+        _subscription?.cancel();
+      },
+      onError: (error) {
+        debugPrint('购买流错误: $error');
+      },
+    );
+
+    debugPrint('内购初始化成功');
+    return true;
+  }
+
+  /// 加载产品信息
+  ///
+  /// [productIds] 产品ID列表
+  /// 返回是否加载成功
+  Future<bool> loadProducts(Set<String> productIds) async {
+    if (!_available) {
+      debugPrint('内购不可用,无法加载产品');
+      return false;
+    }
+
+    if (productIds.isEmpty) {
+      debugPrint('产品ID列表为空');
+      return false;
+    }
+
+    try {
+      final ProductDetailsResponse response = await _inAppPurchase
+          .queryProductDetails(productIds);
+
+      if (response.error != null) {
+        debugPrint('加载产品失败: ${response.error}');
+        return false;
+      }
+
+      if (response.productDetails.isEmpty) {
+        debugPrint('未找到任何产品');
+        return false;
+      }
+
+      _products = response.productDetails;
+
+      // 打印产品信息
+      for (var product in _products) {
+        debugPrint(
+          '产品: ${product.id}, 价格: ${product.price}, 标题: ${product.title}',
+        );
+      }
+
+      return true;
+    } catch (e) {
+      debugPrint('加载产品异常: $e');
+      return false;
+    }
+  }
+
+  /// 根据产品ID获取产品详情
+  ///
+  /// [productId] 产品ID
+  /// 返回产品详情,如果未找到返回 null
+  ProductDetails? getProductById(String productId) {
+    try {
+      return _products.firstWhere((product) => product.id == productId);
+    } catch (e) {
+      debugPrint('未找到产品: $productId');
+      return null;
+    }
+  }
+
+  /// 购买产品
+  ///
+  /// [productDetails] 产品详情
+  /// [isConsumable] 是否是消耗型产品,默认为 false(非消耗型产品,如订阅)
+  /// 返回是否发起购买成功
+  Future<bool> purchaseProduct(
+    ProductDetails productDetails, {
+    bool isConsumable = false,
+  }) async {
+    if (!_available) {
+      debugPrint('内购不可用');
+      return false;
+    }
+
+    try {
+      final PurchaseParam purchaseParam = PurchaseParam(
+        productDetails: productDetails,
+      );
+
+      bool success;
+      if (isConsumable) {
+        success = await _inAppPurchase.buyConsumable(
+          purchaseParam: purchaseParam,
+        );
+      } else {
+        success = await _inAppPurchase.buyNonConsumable(
+          purchaseParam: purchaseParam,
+        );
+      }
+
+      if (success) {
+        debugPrint('发起购买成功: ${productDetails.id}');
+      } else {
+        debugPrint('发起购买失败: ${productDetails.id}');
+      }
+
+      return success;
+    } catch (e) {
+      debugPrint('购买异常: $e');
+      return false;
+    }
+  }
+
+  /// 通过产品ID购买产品
+  ///
+  /// [productId] 产品ID
+  /// 返回是否发起购买成功
+  Future<bool> purchaseProductById(String productId) async {
+    final product = getProductById(productId);
+    if (product == null) {
+      debugPrint('未找到产品: $productId,请先调用 loadProducts');
+      return false;
+    }
+
+    return await purchaseProduct(product);
+  }
+
+  /// 恢复购买
+  ///
+  /// 返回是否发起恢复成功
+  Future<bool> restorePurchases() async {
+    if (!_available) {
+      debugPrint('内购不可用');
+      return false;
+    }
+
+    try {
+      await _inAppPurchase.restorePurchases();
+      debugPrint('发起恢复购买');
+      return true;
+    } catch (e) {
+      debugPrint('恢复购买异常: $e');
+      return false;
+    }
+  }
+
+  /// 完成购买
+  ///
+  /// [purchaseDetails] 购买详情
+  Future<void> completePurchase(PurchaseDetails purchaseDetails) async {
+    await _inAppPurchase.completePurchase(purchaseDetails);
+    debugPrint('完成购买: ${purchaseDetails.productID}');
+  }
+
+  /// 处理购买更新
+  void _handlePurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
+    // 触发统一回调
+    onPurchaseUpdate?.call(purchaseDetailsList);
+
+    // 处理每个购买详情
+    for (var purchaseDetails in purchaseDetailsList) {
+      debugPrint(
+        '购买状态: ${purchaseDetails.status}, 产品: ${purchaseDetails.productID}',
+      );
+
+      switch (purchaseDetails.status) {
+        case PurchaseStatus.pending:
+          // 购买等待中
+          onPurchasePending?.call(purchaseDetails);
+          break;
+
+        case PurchaseStatus.purchased:
+          // 购买成功
+          _handlePurchaseSuccess(purchaseDetails);
+          break;
+
+        case PurchaseStatus.restored:
+          // 恢复购买成功
+          _handleRestoreSuccess(purchaseDetails);
+          break;
+
+        case PurchaseStatus.error:
+          // 购买失败
+          _handlePurchaseError(purchaseDetails);
+          break;
+
+        case PurchaseStatus.canceled:
+          // 购买取消
+          onPurchaseCancelled?.call(purchaseDetails);
+          if (purchaseDetails.pendingCompletePurchase) {
+            completePurchase(purchaseDetails);
+          }
+          break;
+      }
+    }
+  }
+
+  /// 处理购买成功
+  Future<void> _handlePurchaseSuccess(PurchaseDetails purchaseDetails) async {
+    // 这里可以添加服务器验证逻辑
+    bool valid = await _verifyPurchase(purchaseDetails);
+
+    if (valid) {
+      debugPrint('购买验证成功: ${purchaseDetails.productID}');
+      onPurchaseSuccess?.call(purchaseDetails);
+    } else {
+      debugPrint('购买验证失败: ${purchaseDetails.productID}');
+      onPurchaseError?.call(purchaseDetails);
+    }
+
+    // 完成购买
+    if (purchaseDetails.pendingCompletePurchase) {
+      await completePurchase(purchaseDetails);
+    }
+  }
+
+  /// 处理恢复购买成功
+  Future<void> _handleRestoreSuccess(PurchaseDetails purchaseDetails) async {
+    // 验证恢复的购买
+    bool valid = await _verifyPurchase(purchaseDetails);
+
+    if (valid) {
+      debugPrint('恢复购买验证成功: ${purchaseDetails.productID}');
+      onRestoreSuccess?.call(purchaseDetails);
+    }
+
+    // 完成购买
+    if (purchaseDetails.pendingCompletePurchase) {
+      await completePurchase(purchaseDetails);
+    }
+  }
+
+  /// 处理购买失败
+  Future<void> _handlePurchaseError(PurchaseDetails purchaseDetails) async {
+    debugPrint('购买失败: ${purchaseDetails.error}');
+    onPurchaseError?.call(purchaseDetails);
+
+    // 完成购买
+    if (purchaseDetails.pendingCompletePurchase) {
+      await completePurchase(purchaseDetails);
+    }
+  }
+
+  /// 验证购买(需要根据实际业务实现)
+  ///
+  /// [purchaseDetails] 购买详情
+  /// 返回是否验证通过
+  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
+    // TODO: 在这里实现服务器验证逻辑
+    // 1. 将购买凭证发送到你的服务器
+    // 2. 服务器向 Apple/Google 验证购买
+    // 3. 返回验证结果
+
+    // 临时返回 true,实际使用时需要实现服务器验证
+    return true;
+  }
+
+  /// 获取过去的购买记录
+  ///
+  /// 注意:iOS 和 Android 都建议使用 restorePurchases() 方法来恢复购买
+  /// 本方法已弃用,建议使用 restorePurchases() 替代
+  @Deprecated('使用 restorePurchases() 方法替代')
+  Future<List<PurchaseDetails>> getPastPurchases() async {
+    if (!_available) {
+      debugPrint('内购不可用');
+      return [];
+    }
+
+    try {
+      if (Platform.isIOS) {
+        debugPrint('iOS 平台请使用 restorePurchases 来恢复购买');
+        return [];
+      } else if (Platform.isAndroid) {
+        debugPrint('Android 平台请使用 restorePurchases 来恢复购买');
+        return [];
+      }
+    } catch (e) {
+      debugPrint('获取过去购买记录异常: $e');
+    }
+
+    return [];
+  }
+
+  /// 检查用户是否有有效订阅
+  Future<bool> hasActiveSubscription(Set<String> subscriptionIds) async {
+    if (!_available) {
+      return false;
+    }
+
+    try {
+      // 恢复购买
+      await _inAppPurchase.restorePurchases();
+
+      // 这里需要根据实际情况检查订阅状态
+      // 通常需要结合服务器验证来判断
+
+      return false;
+    } catch (e) {
+      debugPrint('检查订阅状态异常: $e');
+      return false;
+    }
+  }
+
+  /// 清理资源
+  void dispose() {
+    _subscription?.cancel();
+    _subscription = null;
+    debugPrint('内购资源已清理');
+  }
+}
+
+/// iOS 支付队列代理示例
+class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
+  @override
+  bool shouldContinueTransaction(
+    SKPaymentTransactionWrapper transaction,
+    SKStorefrontWrapper storefront,
+  ) {
+    return true;
+  }
+
+  @override
+  bool shouldShowPriceConsent() {
+    return false;
+  }
+}

+ 155 - 1
lib/utils/permission_manager.dart

@@ -34,7 +34,7 @@ class PermissionManager {
       }
 
       if (status.isPermanentlyDenied) {
-        await openAppSettings();
+        // await openAppSettings();
         return false;
       }
 
@@ -61,4 +61,158 @@ class PermissionManager {
       return false;
     }
   }
+
+  // ============== 通知权限管理 ==============
+
+  /// 检查通知权限状态
+  static Future<PermissionStatus> checkNotificationPermission() async {
+    try {
+      return await Permission.notification.status;
+    } catch (e) {
+      log('PermissionManager', 'Error checking notification permission: $e');
+      return PermissionStatus.denied;
+    }
+  }
+
+  /// 检查通知权限是否已授予
+  static Future<bool> isNotificationPermissionGranted() async {
+    try {
+      final status = await Permission.notification.status;
+      return status.isGranted;
+    } catch (e) {
+      log('PermissionManager', 'Error checking notification permission: $e');
+      return false;
+    }
+  }
+
+  /// 请求通知权限
+  /// 返回 true 表示已授予权限,false 表示被拒绝
+  static Future<bool> requestNotificationPermission() async {
+    try {
+      var status = await Permission.notification.status;
+
+      // 如果已经授予,直接返回
+      if (status.isGranted) {
+        log('PermissionManager', 'Notification permission already granted');
+        return true;
+      }
+
+      // 如果被永久拒绝,引导用户到设置页面
+      if (status.isPermanentlyDenied) {
+        log(
+          'PermissionManager',
+          'Notification permission permanently denied, opening settings',
+        );
+        await openAppSettings();
+        return false;
+      }
+
+      // 请求权限
+      log('PermissionManager', 'Requesting notification permission');
+      status = await Permission.notification.request();
+
+      final granted = status.isGranted;
+      log(
+        'PermissionManager',
+        'Notification permission request result: $granted',
+      );
+
+      return granted;
+    } catch (e) {
+      log('PermissionManager', 'Error requesting notification permission: $e');
+      return false;
+    }
+  }
+
+  /// 检查通知权限是否被永久拒绝
+  static Future<bool> isNotificationPermissionPermanentlyDenied() async {
+    try {
+      final status = await Permission.notification.status;
+      return status.isPermanentlyDenied;
+    } catch (e) {
+      log('PermissionManager', 'Error checking notification permission: $e');
+      return false;
+    }
+  }
+
+  /// 打开应用设置页面
+  static Future<bool> openNotificationSettings() async {
+    try {
+      log(
+        'PermissionManager',
+        'Opening app settings for notification permission',
+      );
+      return await openAppSettings();
+    } catch (e) {
+      log('PermissionManager', 'Error opening app settings: $e');
+      return false;
+    }
+  }
+
+  /// 获取通知权限的详细状态
+  static Future<NotificationPermissionInfo>
+  getNotificationPermissionInfo() async {
+    try {
+      final status = await Permission.notification.status;
+
+      return NotificationPermissionInfo(
+        isGranted: status.isGranted,
+        isDenied: status.isDenied,
+        isPermanentlyDenied: status.isPermanentlyDenied,
+        isRestricted: status.isRestricted,
+        isLimited: status.isLimited,
+        canRequest: !status.isPermanentlyDenied && !status.isGranted,
+      );
+    } catch (e) {
+      log(
+        'PermissionManager',
+        'Error getting notification permission info: $e',
+      );
+      return NotificationPermissionInfo(
+        isGranted: false,
+        isDenied: true,
+        isPermanentlyDenied: false,
+        isRestricted: false,
+        isLimited: false,
+        canRequest: true,
+      );
+    }
+  }
+}
+
+/// 通知权限信息类
+class NotificationPermissionInfo {
+  /// 是否已授予
+  final bool isGranted;
+
+  /// 是否被拒绝
+  final bool isDenied;
+
+  /// 是否被永久拒绝
+  final bool isPermanentlyDenied;
+
+  /// 是否受限制(iOS)
+  final bool isRestricted;
+
+  /// 是否为有限权限(iOS)
+  final bool isLimited;
+
+  /// 是否可以请求
+  final bool canRequest;
+
+  NotificationPermissionInfo({
+    required this.isGranted,
+    required this.isDenied,
+    required this.isPermanentlyDenied,
+    required this.isRestricted,
+    required this.isLimited,
+    required this.canRequest,
+  });
+
+  @override
+  String toString() {
+    return 'NotificationPermissionInfo(isGranted: $isGranted, isDenied: $isDenied, '
+        'isPermanentlyDenied: $isPermanentlyDenied, isRestricted: $isRestricted, '
+        'isLimited: $isLimited, canRequest: $canRequest)';
+  }
 }