Commit 5cd0cbf7 by ethanlamzs

Merge branch 'feature-2604-menu-iosvip' into feature-2607-iosbuild

ios 待上架的业务方案
2 parents c0cfeaad b3cc3055
......@@ -5,7 +5,8 @@ if [ -z "$1" ]; then
fi
env=$1
_rebuild_=$2
ot=$2
_rebuild_=$3
_main_ver='1.0.'
_ver=`date +%y%m%d%H%M`
......@@ -47,12 +48,27 @@ if [ ! -z "$_rebuild_" ]; then
pod install
fi
_ver_tmp=`date '+%y%m%d%H%M'`
echo 'build '
flutter build ios --build-number=$_ver_tmp
#16
#flutter run --dart-define=env=$env --dart-define=version=$_main_ver$_ver -d 00008140-001068C93AB8801C
if [ ! -z "$ot" ]; then
if [ "$ot" == '16' ]; then
flutter run --dart-define=env=$env --dart-define=version=$_main_ver$_ver -d 00008140-001068C93AB8801C
fi
#11
#flutter run --dart-define=env=$env --dart-define=version=$_main_ver$_ver -d 00008030-001C75810E42402E
if [ "$ot" == '11' ]; then
flutter run --dart-define=env=$env --dart-define=version=$_main_ver$_ver -d 00008030-001C75810E42402E
fi
flutter build ios --build-number=$_ver
if [ "$ot" == 'sim' ]; then
flutter run --debug --dart-define=env=$env --dart-define=version=$_main_ver$_ver -d 89482737-FAA1-4145-BB2D-4B1B122B5B37
fi
else
echo 'build else '
fi
#11
#flutter run --dart-define=env=$env --dart-define=version=$_main_ver$_ver -d 00008030-001C75810E42402E
#flutter build ios --build-number=$_ver
......@@ -20,7 +20,7 @@ flutter pub get
#pod deintegrate
# 4. 清理 CocoaPods 本地缓存 (这一步很重要,ffmpeg 包很大,容易缓存损坏)
pod cache clean --all
#pod cache clean --all
# 5. 重新安装 Pods 并更新仓库索引
# 注意:这一步可能比较慢,因为 ffmpeg 库非常大(几百MB),请保持网络通畅
......
......@@ -26,7 +26,7 @@ end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
# pod 'TOCropViewController', :path => '/Users/ethanlam/works/base_env/flutter_envirs/extlibs/TOCropViewController.git/2.8.0' , :podspec => '/Users/ethanlam/works/base_env/flutter_envirs/extlibs/TOCropViewController.git/2.8.0/TOCropViewController.podspec'
#pod 'TOCropViewController', :path => '/Users/ethanlam/works/base_env/flutter_envirs/extlibs/TOCropViewController.git/2.8.0' , :podspec => '/Users/ethanlam/works/base_env/flutter_envirs/extlibs/TOCropViewController.git/2.8.0/TOCropViewController.podspec'
pod 'SDWebImage', :git => 'https://gitee.com/mirrors/SDWebImage.git', :branch => '5.21.3', :modular_headers => true
......
......@@ -16,6 +16,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
DC7B80922FF3602D0006BCCE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC7B80912FF3602D0006BCCE /* StoreKit.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
......@@ -70,6 +71,8 @@
DC39D7882EFB987F00D795A8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/Info.plist; sourceTree = "<group>"; };
DC39D7892EFB988000D795A8 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "zh-Hans"; path = "zh-Hans.lproj/Info.plist"; sourceTree = "<group>"; };
DC4A6BC3E644F00288FAFDCD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DC7B80912FF3602D0006BCCE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
DCEFFEE63000FA7000F7B036 /* Testing.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; name = Testing.storekit; path = Runner/Testing.storekit; sourceTree = "<group>"; };
E94A65B4132ADF0336017236 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
......@@ -87,6 +90,7 @@
buildActionMask = 2147483647;
files = (
1600F8F364E182C3635C3546 /* Pods_Runner.framework in Frameworks */,
DC7B80922FF3602D0006BCCE /* StoreKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
......@@ -115,6 +119,7 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
DCEFFEE63000FA7000F7B036 /* Testing.storekit */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
......@@ -153,6 +158,7 @@
9F7FE946B3449150A78BC577 /* Frameworks */ = {
isa = PBXGroup;
children = (
DC7B80912FF3602D0006BCCE /* StoreKit.framework */,
B2450CB3B5E968BD7CC513E6 /* Pods_Runner.framework */,
DC4A6BC3E644F00288FAFDCD /* Pods_RunnerTests.framework */,
);
......
......@@ -84,7 +84,6 @@ import tencent_cloud_chat_push
return true
}
}
// 可选:防止冲突的手势识别设置
......@@ -93,4 +92,5 @@ extension AppDelegate: UIGestureRecognizerDelegate {
// 如果需要同时识别,比如与 WebView 的滚动手势共存
return true
}
}
\ No newline at end of file
}
{
"appPolicies" : {
"eula" : "",
"policies" : [
{
"locale" : "en_US",
"policyText" : "",
"policyURL" : ""
}
]
},
"identifier" : "6C86460D",
"nonRenewingSubscriptions" : [
{
"displayPrice" : "49.90",
"familyShareable" : false,
"internalID" : "6785393224",
"localizations" : [
{
"description" : "智学会员1个月有效期",
"displayName" : "智学VIP月卡",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.1m",
"referenceName" : "1个月时长会员",
"type" : "NonRenewingSubscription"
},
{
"displayPrice" : "129",
"familyShareable" : false,
"internalID" : "6788337604",
"localizations" : [
{
"description" : "智学会员服务1个季度的有效期",
"displayName" : "智学VIP季卡",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.3m",
"referenceName" : "1季度会员",
"type" : "NonRenewingSubscription"
},
{
"displayPrice" : "299",
"familyShareable" : false,
"internalID" : "6788342136",
"localizations" : [
{
"description" : "智学VIP会员服务1年有效期",
"displayName" : "智学VIP年卡",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.12m",
"referenceName" : "一年会员",
"type" : "NonRenewingSubscription"
},
{
"displayPrice" : "209",
"familyShareable" : false,
"internalID" : "6788341520",
"localizations" : [
{
"description" : "智学VIP会员服务半年有效期",
"displayName" : "智学VIP半年卡",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.6m",
"referenceName" : "半年会员",
"type" : "NonRenewingSubscription"
},
{
"displayPrice" : "1",
"familyShareable" : false,
"internalID" : "6789528182",
"localizations" : [
{
"description" : "智学会员服务1天有效",
"displayName" : "会员1天",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.0m",
"referenceName" : "测试会员",
"type" : "NonRenewingSubscription"
}
],
"products" : [
],
"settings" : {
"_applicationInternalID" : "6757346215",
"_askToBuyEnabled" : false,
"_billingGracePeriodEnabled" : false,
"_billingIssuesEnabled" : false,
"_developerTeamID" : "42794B2KBU",
"_disableDialogs" : false,
"_failTransactionsEnabled" : false,
"_lastSynchronizedDate" : 805370487.02650404,
"_locale" : "zh_Hans",
"_renewalBillingIssuesEnabled" : false,
"_storefront" : "CHN",
"_storeKitErrors" : [
],
"_timeRate" : 0
},
"subscriptionGroups" : [
],
"version" : {
"major" : 4,
"minor" : 0
}
}
......@@ -161,7 +161,8 @@ class AccountCubit extends Cubit<AccountState> {
Future<void> goLogoff() async {
await router.push(
'/account/logoff',
//'/account/logoff',
'/iosbuytest',
extra: {
'phone': state.phone,
},
......
......@@ -19,6 +19,7 @@ import 'package:appframe/ui/pages/setting_v2/account_apple_page_v2.dart';
import 'package:appframe/ui/pages/setting_v2/account_logoff_page_v2.dart';
import 'package:appframe/ui/pages/setting_v2/account_page_v2.dart';
import 'package:appframe/ui/pages/setting_v2/account_phone_page_v2.dart';
import 'package:appframe/ui/pages/subscription_page.dart';
import 'package:appframe/ui/pages/web_page.dart';
import 'package:appframe/ui/widgets/ios_edge_swipe_detector.dart';
import 'package:flutter/material.dart';
......@@ -130,6 +131,12 @@ final GoRouter router = GoRouter(
return const PreviewMediaPage();
},
),
GoRoute(
path: '/iosbuytest',
builder: (BuildContext context, GoRouterState state) {
return const SubscriptionPage();
},
),
],
);
......
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/data/repositories/user_auth_repository.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_localization/flutter_localization.dart';
import 'package:appframe/services/subscription_service_ios.dart';
import 'package:fluwx/fluwx.dart';
class XeCreateOrderHandler extends MessageHandler {
......@@ -19,6 +22,8 @@ class XeCreateOrderHandler extends MessageHandler {
// 超时定时器
Timer? _timeoutTimer;
final SubscriptionService _subService = SubscriptionService();
XeCreateOrderHandler() {
_fluwx = getIt.get<Fluwx>();
_userAuthRepository = getIt.get<UserAuthRepository>();
......@@ -26,6 +31,8 @@ class XeCreateOrderHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(params) async {
print("############## handleMessage #############");
// 从 params 中提取支付参数
final String chargeCode = params['chargeCode'] as String? ?? '';
final int durationType = params['durationType'] as int? ?? 0;
......@@ -50,13 +57,26 @@ class XeCreateOrderHandler extends MessageHandler {
'phone=$phone, userCode=$userCode, tbxStuId=$tbxStuId, stuId=$stuId, '
'ext=$ext');
// 1. 检查微信是否安装
// 1. 请求后端获取预支付订单信息
final deviceType = Platform.isIOS?"ios":"andorid";
print("[debug]. handleMessage 1 "+deviceType);
if (deviceType == 'andorid'){
return await _wx_pay_proc_(userCode, tbxStuId, chargeCode, deviceType, phone, stuId, ext, duration, durationType, totalFee, renew, useCoin);
}else if (deviceType == 'ios'){
// 暂时不支持使用学币扣减
return await _ios_pay_proc_(userCode, tbxStuId, chargeCode, deviceType, phone, stuId, ext, duration, durationType, totalFee, renew, false);
}
}
// 执行微信支付
Future<dynamic> _wx_pay_proc_ (String userCode, tbxStuId, chargeCode,deviceType,phone,stuId,ext,int duration, durationType, totalFee,bool renew, useCoin) async{
// 2. 检查微信是否安装
if (!await _fluwx.isWeChatInstalled) {
throw Exception('设备上未安装微信App,不支持微信支付');
}
// 2. 请求后端获取预支付订单信息
final deviceType = 'android';
var result = await _userAuthRepository.createOrder(userCode, tbxStuId, chargeCode, duration, durationType, totalFee,
renew, phone, stuId, ext, useCoin ? 1 : 0, deviceType);
if (result == null) {
......@@ -83,14 +103,14 @@ class XeCreateOrderHandler extends MessageHandler {
final paySign = data['paySign'] as String;
final wxTradeNo = data['wxTradeNo'] as String;
debugPrint('prepayId: $prepayId');
debugPrint('nonceStr: $nonceStr');
debugPrint('timestamp: $timestamp');
debugPrint('paySign: $paySign');
debugPrint('appId: $appId');
debugPrint('partnerId: $partnerId');
debugPrint('packageValue: $packageValue');
debugPrint('wxTradeNo: $wxTradeNo');
debugPrint('_wx_pay_proc_ => prepayId: $prepayId');
debugPrint('_wx_pay_proc_ => nonceStr: $nonceStr');
debugPrint('_wx_pay_proc_ => timestamp: $timestamp');
debugPrint('_wx_pay_proc_ => paySign: $paySign');
debugPrint('_wx_pay_proc_ => appId: $appId');
debugPrint('_wx_pay_proc_ => partnerId: $partnerId');
debugPrint('_wx_pay_proc_ => packageValue: $packageValue');
debugPrint('_wx_pay_proc_ => wxTradeNo: $wxTradeNo');
if (nonceStr.isEmpty || paySign.isEmpty || wxTradeNo.isEmpty) {
throw Exception('支付参数获取失败');
......@@ -155,6 +175,8 @@ class XeCreateOrderHandler extends MessageHandler {
} finally {
_cleanup();
}
}
/// Fluwx 响应监听器
......@@ -174,4 +196,69 @@ class XeCreateOrderHandler extends MessageHandler {
_timeoutTimer = null;
_payCompleter = null;
}
// 执行ios的微信支付
Future<dynamic> _ios_pay_proc_ (String userCode, tbxStuId, chargeCode,deviceType,phone,stuId,ext,int duration, durationType, totalFee,bool renew, useCoin) async{
// 暂时用固定的价格
var result = await _userAuthRepository.createOrder(userCode, tbxStuId, chargeCode, duration, durationType, totalFee,
renew, phone, stuId, ext, useCoin ? 1 : 0, deviceType);
print(result);
// map[nonceStr:qNfHK6a95jjJkwzDkh0h3fhfUVuS0jZ9 paySign: timeStamp:1783497438 wxTradeNo:to1281644408384671744]
var data = result['data'] as Map<String, dynamic>?;
if (data == null) {
throw Exception('订单生成失败');
}
// 3. 从后端响应中提取预支付ID
final wxTradeNo = data['wxTradeNo'] as String?;
if (wxTradeNo == null || wxTradeNo.isEmpty) {
throw Exception('订单生成失败');
}
// 4. 从后端响应中提取支付参数(nonceStr、timestamp、paySign 由后端生成)
final timestamp = data['timeStamp'] as int;
//debugPrint('_ios_pay_proc_ => timestamp: $timestamp');
debugPrint('_ios_pay_proc_ => wxTradeNo: $wxTradeNo');
if (wxTradeNo.isEmpty) {
throw Exception('支付参数获取失败');
}
// 执行ios支付
await _subService.initialize();
// 为了调试,先模拟
var product;
for (var p in _subService.products) {
// 因为带了一个¥符号
//debugPrint('checking ==> 产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}');
if (p.price.toString().indexOf(totalFee.toString().substring(0,2))==1){
product = p;
//debugPrint('match =====> 产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}');
break;
}
}
if(product==null){
throw Exception('订单生成失败');
}
print("match product is "+product.price);
var isOk = await _subService.buySubscription(product, userCode, tbxStuId,wxTradeNo);
print("[info] _subService.buySubscription res is "+isOk.toString());
if (isOk==true){
return {"orderUnique": wxTradeNo, "orderState": 1};
}else{
return {"orderUnique": wxTradeNo, "orderState": 0};
}
// throw Exception('已取消支付');
}
}
......@@ -162,4 +162,23 @@ class UserAuthRepository {
}
}
// 检查ios的订单是否已经订阅
Future<dynamic> orderCheck(String userId,String receipt,String orderid, String platform,int isSandbox ) async {
try {
Response resp = await _appService.post(
'/api/v1/comm/user/ordercheck',
{
"userId": userId,
"receipt": receipt,
"orderid":orderid,
"platform": platform,
"isSandbox":isSandbox,
},
);
return resp.statusCode == HttpStatus.ok ? resp.data : null;
} on DioException {
return null;
}
}
}
......@@ -6,6 +6,7 @@ import 'package:appframe/data/models/message/h5_message.dart';
import 'package:appframe/data/models/message/h5_resp.dart';
import 'package:appframe/data/repositories/message/upload_cancel_handler.dart';
import 'package:appframe/data/repositories/message/upload_start_handler.dart';
import 'package:flutter/widgets.dart';
// 消息处理器抽象类
abstract class MessageHandler {
......@@ -35,7 +36,6 @@ class MessageDispatcher {
Future<void> dispatch(String message, Function callback, {WebCubit? webCubit}) async {
final Map<String, dynamic> data = json.decode(message);
H5Message h5Message = H5Message.fromJson(data);
var handler = _handlers[h5Message.cmd];
if (handler == null) {
try {
......
// subscription_service.dart
import 'dart:async';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:flutter/widgets.dart';
import 'package:appframe/data/repositories/user_auth_repository.dart';
import 'package:appframe/config/locator.dart';
class SubscriptionService {
// App Store Connect 中配置的订阅产品 ID(组内唯一)
// 例如月度、年度两种方案
static const String monthlySubId_1 = 'cn.banxe.appframe.membership.1m';
static const String monthlySubId_3 = 'cn.banxe.appframe.membership.3m';
static const String monthlySubId_6 = 'cn.banxe.appframe.membership.6m';
static const String monthlySubId_12 = 'cn.banxe.appframe.membership.12m';
// 纯粹的数据容器,不继承任何类
// ---------- 业务上下文映射(productID → 自定义数据) ----------
final Map<String, Map<String, dynamic>> _pendingContexts = {};
final Map<String, String> _orderIdMap = {}; // key: productID, value: orderId
final InAppPurchase _iap = InAppPurchase.instance;
late StreamSubscription<List<PurchaseDetails>> _purchaseUpdatedSubscription;
final UserAuthRepository _userAuthRepository = getIt.get<UserAuthRepository>();
// 可用产品列表
List<ProductDetails> _products = [];
List<ProductDetails> get products => _products;
// 当前有效订阅状态(简化版)
bool _hasActiveSubscription = false;
bool get hasActiveSubscription => _hasActiveSubscription;
// ---------- 🔁 去重措施:记录已处理过的交易 ID ----------
final Set<String> _processedTransactionIds = {};
// ---------- 回调函数,用于通知 UI 状态变化 ----------
void Function(String? errorMessage)? onError;
void Function(String message)? onSuccess;
void Function()? onStateChanged;
// 初始化
Future<void> initialize() async {
// 1. 检查是否可用
final bool available = await _iap.isAvailable();
if (!available) {
throw Exception('In-App Purchase 不可用');
}
// 2. 监听交易更新(最重要!务必在调用购买之前设置)
_purchaseUpdatedSubscription =
_iap.purchaseStream.listen(
_onPurchaseUpdate,
onError: (error) => print('purchaseStream error: $error'),
);
// 3. 从 App Store 获取产品信息
await _fetchProducts();
// 4. 处理待处理的交易(应用被杀掉时未完成交易)
await _iap.restorePurchases(); // 订阅恢复也会触发 purchaseStream
}
// ---------- 获取产品信息 ----------
Future<void> _fetchProducts() async {
const Set<String> ids = {monthlySubId_1,monthlySubId_3,monthlySubId_6,monthlySubId_12};
final ProductDetailsResponse response = await _iap.queryProductDetails(ids);
if (response.notFoundIDs.isNotEmpty) {
// 有产品未在 App Store Connect 配置或配置错误
debugPrint('未找到产品: ${response.notFoundIDs}');
}
_products = response.productDetails;
// debugPrint('获取到 ${_products.length} 个产品');
// for (var p in _products) {
// debugPrint('x1=====> 产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}');
// }
}
// ---------- 购买订阅 ----------
Future<bool> buySubscription(ProductDetails product, String userId , String tbxStuId, String yWOrderId) async {
_pendingContexts[product.id] = {'userId': userId,'tbxStuId': tbxStuId,"orderId":yWOrderId};
_orderIdMap[product.id] = yWOrderId; // 按产品 ID 存储
// 订阅必须使用 PurchaseParam 并设置购买类型为订阅
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: product,
);
try {
// 发起购买(App Store 会弹出系统支付界面)
await _iap.buyNonConsumable(purchaseParam: purchaseParam);
return true; // 实际结果在 _onPurchaseUpdate 处理
}catch (e){
_pendingContexts.remove(product.id); // 发起失败,清除上下文
onError?.call('购买请求失败: $e');
return false;
}
// 注意:成功或失败会通过 purchaseStream 回调返回
}
// ---------- 交易更新回调 ----------
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
for (final purchaseDetails in purchaseDetailsList) {
print('Purchase update: ${purchaseDetails.productID} - ${purchaseDetails.status}');
// 只处理订阅类型产品
if (!_isSubscription(purchaseDetails.productID)){
print("Purchase update !_isSubscription ");
continue;
}
// 🔁 去重措施 1:根据原始交易 ID 去重,防止同一笔交易被重复处理
final transactionId = purchaseDetails.verificationData.serverVerificationData.hashCode.toString();
// 如果能获取到 purchaseID 更可靠,这里用 purchaseID 作为唯一标识
final uniqueId = purchaseDetails.purchaseID;
if (_processedTransactionIds.contains(uniqueId)) {
// 已经处理过,直接结束交易并跳过
if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
_iap.completePurchase(purchaseDetails);
}
continue;
}
// 标记为已处理
if (uniqueId != null) {
_processedTransactionIds.add(uniqueId);
}
//var orderId = _orderIdMap.remove(purchaseDetails.productID); // 取出并删除
final ctx = _pendingContexts[purchaseDetails.productID] ;
print(ctx);
if (ctx == null) {
// 打印错误日志,防止崩溃
print('错误:未找到 productID 对应的上下文缓存');
return;
}
final orderId = ctx['orderId'] as String?;
final userId = ctx['userId'] as String?;
final tbxStuId = ctx['tbxStuId'] as String?;
print("orderId ==> "+orderId.toString()+" "+ userId.toString() +". "+ tbxStuId.toString());
// 根据交易状态处理
switch (purchaseDetails.status) {
case PurchaseStatus.purchased:
// 购买成功或续期成功
// 1. 验证收据(必须!)
//_verifyReceipt(purchaseDetails,orderId??'',userId??'',"purchased");
_handlePurchaseSuccess(purchaseDetails,orderId??'',userId??'');
break;
case PurchaseStatus.error:
// 处理错误
_handlePurchaseError(purchaseDetails.error!);
break;
case PurchaseStatus.restored:
// 恢复购买成功
// 🔁 去重措施 2:恢复事件只处理最后一个有效的,或者收集后统一处理
// 这里演示:仍然逐个验证,但后端会基于 original_transaction_id 去重
//_verifyReceipt(purchaseDetails,orderId??'',userId??'',"restored");
_handleRestoreSuccess(purchaseDetails,orderId??'',userId??'');
break;
case PurchaseStatus.canceled:
// 用户取消支付
//_iap.completePurchase(purchaseDetails);
break;
default:
break;
}
// // ✅ 关键:只要不是 pending,都立刻完成交易,清出队列
// if (purchaseDetails.status != PurchaseStatus.pending) {
// _iap.completePurchase(purchaseDetails);
// print("[debug] _iap.completePurchase execute ");
// }
// 🔁 去重措施 3:所有恢复交易处理完后,统一刷新一次状态(防多次通知)
final hasRestored = purchaseDetailsList.any((d) => d.status == PurchaseStatus.restored);
if (hasRestored) {
// 统一通知 UI 更新,避免多次刷新
onSuccess?.call('购买已恢复');
onStateChanged?.call();
}
}
}
// ---------- 服务端验证收据 ----------
Future<void> _verifyReceipt(PurchaseDetails purchaseDetails,String orderid,String userid,String scence) async {
debugPrint("[info] _verifyReceipt is checking 3 ["+scence+"] "+purchaseDetails.productID+" orderid:"+orderid+" userid:"+userid);
if (orderid != null && orderid.length > 6) {
debugPrint("[info] _verifyReceipt is start scence ["+scence+"] "+purchaseDetails.productID+" orderid:"+orderid);
// 将收据发送到你的后端进行验证
final String receipt = purchaseDetails.verificationData.serverVerificationData;
// 把 base64Receipt 作为 receipt-data 发给你的服务端验证
final bool isValid = await _sendReceiptToBackend(userid,receipt,orderid);
if (isValid) {
// 更新本地订阅状态
_hasActiveSubscription = true;
// 保存到期时间等信息
}
}
}
// 示例:发送收据到自己的服务器进行二次验证
Future<bool> _sendReceiptToBackend(String userid,String receipt,String orderid) async {
var result = _userAuthRepository.orderCheck(userid,receipt,orderid,"ios",1);
//var data = result['data'] as Map<String, dynamic>?;
//debugPrint(result);
debugPrint("[info] _sendReceiptToBackend is done");
return true;
}
// ---------- 辅助方法 ----------
bool _isSubscription(String productId) {
return productId == monthlySubId_1 || productId == monthlySubId_3|| productId == monthlySubId_6|| productId == monthlySubId_12;
}
void _handlePurchaseError(IAPError error) {
// 根据 error.code 显示不同提示
debugPrint('购买失败: ${error.message}');
}
// ---------- 购买成功处理 ----------
void _handlePurchaseSuccess(PurchaseDetails detail,String orderId,String userId) {
// 发送收据到后端验证,携带 userId
_verifyReceipt(detail,orderId??'',userId??'',"purchased");
// 完成交易
_iap.completePurchase(detail);
// 乐观更新本地状态(等后端最终确认再真正解锁功能)
_hasActiveSubscription = true;
onSuccess?.call('订阅成功');
onStateChanged?.call();
}
// ---------- 恢复成功处理 ----------
void _handleRestoreSuccess(PurchaseDetails detail,String orderId,String userId) {
// 注意:这里只是处理单个 restored,但最终 UI 刷新在 _onPurchaseUpdate 统一完成
_verifyReceipt(detail,orderId??'',userId??'',"purchased");
_iap.completePurchase(detail);
}
// ---------- 错误处理 ----------
void _handleError(PurchaseDetails detail) {
_pendingContexts.remove(detail.productID);
_iap.completePurchase(detail);
onError?.call('支付失败: ${detail.error?.message}');
}
// ---------- 取消处理 ----------
void _handleCancel(PurchaseDetails detail) {
_pendingContexts.remove(detail.productID);
_iap.completePurchase(detail);
onError?.call('支付已取消');
}
// 释放资源
void dispose() {
_purchaseUpdatedSubscription.cancel();
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:appframe/services/subscription_service_ios.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
// subscription_page.dart
class SubscriptionPage extends StatefulWidget {
const SubscriptionPage({super.key});
@override
State<SubscriptionPage> createState() => _SubscriptionPageState();
}
class _SubscriptionPageState extends State<SubscriptionPage> {
final SubscriptionService _subService = SubscriptionService();
bool _isInitialized = false;
@override
void initState() {
super.initState();
_initService();
}
Future<void> _initService() async {
await _subService.initialize();
setState(() => _isInitialized = true);
}
Future<void> _buy(ProductDetails product) async {
if (!_isInitialized) return;
try {
await _subService.buySubscription(product,"","","");
} catch (e) {
// 显示出错信息
debugPrint(e.toString());
}
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) return CircularProgressIndicator();
// return ListView.builder(
// itemCount: _subService.products.length,
// itemBuilder: (context, index) {
// final product = _subService.products[index];
// return ListTile(
// title: Text(product.title),
// subtitle: Text(product.id),
// trailing: ElevatedButton(
// onPressed: () => _buy(product),
// child: Text(product.price),
// ),
// );
// },
// );
return Scaffold(
body: Center(
child: ListView.builder(
itemCount: _subService.products.length,
itemBuilder: (context, index) {
final product = _subService.products[index];
return ListTile(
title: Text(product.title),
subtitle: Text(product.description),
trailing: ElevatedButton(
onPressed: () => _buy(product),
child: Text(product.price),
),
);
},
),
),
);
}
@override
void dispose() {
// _subService.dispose();
super.dispose();
}
}
\ No newline at end of file
......@@ -15,6 +15,7 @@ import flutter_image_compress_macos
import flutter_localization
import flutter_sound
import geolocator_apple
import in_app_purchase_storekit
import mobile_scanner
import network_info_plus
import open_file_mac
......@@ -23,10 +24,12 @@ import path_provider_foundation
import photo_manager
import shared_preferences_foundation
import sign_in_with_apple
import sqflite_darwin
import tencent_cloud_chat_sdk
import url_launcher_macos
import video_player_avfoundation
import volume_controller
import wakelock_plus
import webview_flutter_wkwebview
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
......@@ -40,6 +43,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
FlutterSoundPlugin.register(with: registry.registrar(forPlugin: "FlutterSoundPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
......@@ -48,9 +52,11 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
TencentCloudChatSdkPlugin.register(with: registry.registrar(forPlugin: "TencentCloudChatSdkPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
}
......@@ -62,6 +62,8 @@ dependencies:
# --- Apple 登录 ---
sign_in_with_apple: ^7.0.1
in_app_purchase: ^3.2.0 # 使用最新稳定版
http: ^1.2.0 # 用于服务端验证
# --- 音视频与直播 (重灾区) ---
# 确保 ffmpeg_kit 版本与你的架构兼容。
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!