Commit 64002d44 by ethanlamzs

ios 1.1.0 线上版本 1.1.4 版本封版

1 parent 0f7f20cd
...@@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; ...@@ -10,7 +10,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:appframe/services/subscription_service_ios.dart';
import 'app.dart'; import 'app.dart';
void main() async { void main() async {
...@@ -21,6 +21,11 @@ void main() async { ...@@ -21,6 +21,11 @@ void main() async {
await _initH5Version(); await _initH5Version();
await _initIOSGesture(); await _initIOSGesture();
if (Platform.isIOS){
SubscriptionService _subService = SubscriptionService();
_subService.initialize();
}
runApp(const App()); runApp(const App());
} }
......
// subscription_service.dart // subscription_service.dart
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:in_app_purchase/in_app_purchase.dart'; import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:appframe/data/repositories/user_auth_repository.dart'; import 'package:appframe/data/repositories/user_auth_repository.dart';
import 'package:appframe/config/locator.dart'; import 'package:appframe/config/locator.dart';
...@@ -15,9 +17,16 @@ class SubscriptionService { ...@@ -15,9 +17,16 @@ class SubscriptionService {
static const String monthlySubId_6 = 'cn.banxe.appframe.membership.6m'; static const String monthlySubId_6 = 'cn.banxe.appframe.membership.6m';
static const String monthlySubId_12 = 'cn.banxe.appframe.membership.12m'; static const String monthlySubId_12 = 'cn.banxe.appframe.membership.12m';
// 纯粹的数据容器,不继承任何类 // 纯粹的数据容器,不继承任何类
// ---------- 业务上下文映射(productID → 自定义数据) ---------- // ---------- 业务上下文映射(productID → 自定义数据) ----------
static const String _cacheKey = 'iap_products_cache';
static const String _timestampKey = 'iap_cache_timestamp';
static const Duration _cacheValidDuration = Duration(hours: 24); // 可调整
// 标记是否已从 StoreKit 获取到最新产品
bool _hasRealProducts = false;
final Map<String, Map<String, dynamic>> _pendingContexts = {}; final Map<String, Map<String, dynamic>> _pendingContexts = {};
final InAppPurchase _iap = InAppPurchase.instance; final InAppPurchase _iap = InAppPurchase.instance;
...@@ -56,8 +65,14 @@ class SubscriptionService { ...@@ -56,8 +65,14 @@ class SubscriptionService {
onError: (error) => print('purchaseStream error: $error'), onError: (error) => print('purchaseStream error: $error'),
); );
// 3. 从 App Store 获取产品信息 // // 3. 从 App Store 获取产品信息
await _fetchProducts(); // await _fetchProducts();
// 1. 立即从缓存加载,先展示给用户
await _loadCacheAndShow();
// 2. 后台异步拉取最新产品,不阻塞 UI
_fetchAndUpdateProducts(); // 注意:非 await,fire-and-forget
} }
...@@ -80,7 +95,103 @@ class SubscriptionService { ...@@ -80,7 +95,103 @@ class SubscriptionService {
debugPrint('x1=====> 产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}'); debugPrint('x1=====> 产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}');
} }
}
// ---------- 从缓存加载产品(仅用于展示) ----------
Future<void> _loadCacheAndShow() async {
final cached = await _loadCachedProducts();
if (cached != null && cached.isNotEmpty) {
_products = cached;
_hasRealProducts = false; // 缓存不是真实产品,仅用于展示
onStateChanged?.call(); // 通知 UI 更新
} }
print("[info] ################# _loadCacheAndShow is ok ############# ");
}
// ---------- 后台获取真实产品并更新缓存 ----------
Future<void> _fetchAndUpdateProducts() async {
try {
const Set<String> ids = {monthlySubId_1,monthlySubId_3,monthlySubId_6,monthlySubId_12};
final response = await _iap.queryProductDetails(ids);
if (response.productDetails.isNotEmpty) {
_products = response.productDetails;
_hasRealProducts = true; // 已拿到真实产品
await _cacheProducts(_products);
onStateChanged?.call();
}
} catch (e) {
debugPrint('获取产品失败: $e');
// 如果获取失败且缓存为空,UI 可能空白,可在此处理
}
}
// ---------- 缓存读写(ProductDetails ↔ JSON) ----------
Future<void> _cacheProducts(List<ProductDetails> products) async {
try {
final prefs = await SharedPreferences.getInstance();
final jsonList = products.map((p) => _productToJson(p)).toList();
await prefs.setString(_cacheKey, jsonEncode(jsonList));
await prefs.setString(_timestampKey, DateTime.now().toIso8601String());
print("[debug] ######### _cacheProducts is done ##############");
} catch (e) {
debugPrint('缓存产品失败: $e');
}
}
Future<List<ProductDetails>?> _loadCachedProducts() async {
try {
final prefs = await SharedPreferences.getInstance();
final timestampStr = prefs.getString(_timestampKey);
if (timestampStr == null) return null;
final cacheTime = DateTime.parse(timestampStr);
if (DateTime.now().difference(cacheTime) > _cacheValidDuration) {
await _clearCache();
return null;
}
final jsonStr = prefs.getString(_cacheKey);
if (jsonStr == null) return null;
final List<dynamic> list = jsonDecode(jsonStr);
return list.map((item) => _productFromJson(item)).toList();
} catch (e) {
return null;
}
}
Future<void> _clearCache() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_cacheKey);
await prefs.remove(_timestampKey);
}
// ProductDetails 序列化(请确保字段名称与你的插件版本一致)
Map<String, dynamic> _productToJson(ProductDetails p) => {
'id': p.id,
'title': p.title,
'description': p.description,
'price': p.price,
'rawPrice': p.rawPrice,
'currencyCode': p.currencyCode,
'currencySymbol': p.currencySymbol,
// 'subscriptionPeriod': p,
// 'introductoryPrice': p.introductoryPrice,
};
ProductDetails _productFromJson(Map<String, dynamic> json) => ProductDetails(
id: json['id'] ?? '',
title: json['title'] ?? '',
description: json['description'] ?? '',
price: json['price'] ?? '',
rawPrice: (json['rawPrice'] as num?)?.toDouble() ?? 0.0,
currencyCode: json['currencyCode'] ?? '',
currencySymbol: json['currencySymbol'] ?? '',
// subscriptionPeriod: json['subscriptionPeriod'] ?? '',
// introductoryPrice: json['introductoryPrice'] ?? '',
);
Future<void> restoreSubscription() async { Future<void> restoreSubscription() async {
try{ try{
...@@ -99,14 +210,18 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS ...@@ -99,14 +210,18 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
// 订阅必须使用 PurchaseParam 并设置购买类型为订阅 // 订阅必须使用 PurchaseParam 并设置购买类型为订阅
final PurchaseParam purchaseParam = PurchaseParam( final PurchaseParam purchaseParam = PurchaseParam(
productDetails: product, productDetails: product,
applicationUserName: yWOrderId,
); );
try { try {
// 发起购买(App Store 会弹出系统支付界面) // 发起购买(App Store 会弹出系统支付界面)
print("[debug] ############## buySubscription is try to run ##############");
await _iap.buyNonConsumable(purchaseParam: purchaseParam); await _iap.buyNonConsumable(purchaseParam: purchaseParam);
print("[debug] ############## buySubscription is run end ##############");
return true; // 实际结果在 _onPurchaseUpdate 处理 return true; // 实际结果在 _onPurchaseUpdate 处理
}catch (e){ }catch (e){
_pendingContexts.remove(product.id); // 发起失败,清除上下文 _pendingContexts.remove(product.id); // 发起失败,清除上下文
debugPrint(e.toString());
onError?.call('购买请求失败: $e'); onError?.call('购买请求失败: $e');
return false; return false;
} }
...@@ -118,10 +233,9 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS ...@@ -118,10 +233,9 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) { void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
for (final purchaseDetails in purchaseDetailsList) { for (final purchaseDetails in purchaseDetailsList) {
print('Purchase update: ${purchaseDetails.productID} - ${purchaseDetails.status}'); print('[debug] ############## Purchase update: ${purchaseDetails.productID} - ${purchaseDetails.status} - ${purchaseDetails.purchaseID}- ${purchaseDetails.toString()}');
// 只处理订阅类型产品 // 只处理订阅类型产品
if (!_isSubscription(purchaseDetails.productID)){ if (!_isSubscription(purchaseDetails.productID)){
print("Purchase update !_isSubscription ");
continue; continue;
} }
...@@ -154,7 +268,7 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS ...@@ -154,7 +268,7 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
final orderId = ctx['orderId'] as String?; final orderId = ctx['orderId'] as String?;
final userId = ctx['userId'] as String?; final userId = ctx['userId'] as String?;
final tbxStuId = ctx['tbxStuId'] as String?; final tbxStuId = ctx['tbxStuId'] as String?;
print("orderId ==> "+orderId.toString()+" "+ userId.toString() +". "+ tbxStuId.toString()); print("[debug] ############## orderId ==> "+orderId.toString()+" "+ userId.toString() +". "+ tbxStuId.toString()+" "+purchaseDetails.status.toString());
// 根据交易状态处理 // 根据交易状态处理
...@@ -187,8 +301,7 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS ...@@ -187,8 +301,7 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
case PurchaseStatus.pending: case PurchaseStatus.pending:
onStateChanged?.call(); onStateChanged?.call();
break; break;
default:
break;
} }
// // ✅ 关键:只要不是 pending,都立刻完成交易,清出队列 // // ✅ 关键:只要不是 pending,都立刻完成交易,清出队列
...@@ -209,9 +322,9 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS ...@@ -209,9 +322,9 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
// ---------- 服务端验证收据 ---------- // ---------- 服务端验证收据 ----------
Future<bool> _verifyReceipt(PurchaseDetails purchaseDetails,String orderid,String userid,String scence) async { Future<bool> _verifyReceipt(PurchaseDetails purchaseDetails,String orderid,String userid,String scence) async {
debugPrint("[info] _verifyReceipt is checking 3 ["+scence+"] "+purchaseDetails.productID+" orderid:"+orderid+" userid:"+userid); debugPrint("[debug] ############## _verifyReceipt is checking 3 ["+scence+"] "+purchaseDetails.productID+" orderid:"+orderid+" userid:"+userid);
if (orderid != null && orderid.length > 6) { if (orderid != null && orderid.length > 6) {
debugPrint("[info] _verifyReceipt is start scence ["+scence+"] "+purchaseDetails.productID+" orderid:"+orderid); debugPrint("[debug] ############## _verifyReceipt is start scence ["+scence+"] "+purchaseDetails.productID+" orderid:"+orderid);
// 将收据发送到你的后端进行验证 // 将收据发送到你的后端进行验证
final String receipt = purchaseDetails.verificationData.serverVerificationData; final String receipt = purchaseDetails.verificationData.serverVerificationData;
// 把 base64Receipt 作为 receipt-data 发给你的服务端验证 // 把 base64Receipt 作为 receipt-data 发给你的服务端验证
...@@ -230,7 +343,7 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS ...@@ -230,7 +343,7 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
// 示例:发送收据到自己的服务器进行二次验证 // 示例:发送收据到自己的服务器进行二次验证
Future<bool> _sendReceiptToBackend(String userid,String receipt,String orderid) async { Future<bool> _sendReceiptToBackend(String userid,String receipt,String orderid) async {
var result = await _userAuthRepository.orderCheck(userid,receipt,orderid,"ios",1); var result = await _userAuthRepository.orderCheck(userid,receipt,orderid,"ios",1);
debugPrint("[info] _sendReceiptToBackend is done"); debugPrint("[info] ############## _sendReceiptToBackend is done");
return result!=null; return result!=null;
} }
...@@ -265,7 +378,7 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS ...@@ -265,7 +378,7 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
// ---------- 恢复成功处理 ---------- // ---------- 恢复成功处理 ----------
Future<void> _handleRestoreSuccess(PurchaseDetails detail,String orderId,String userId) async{ Future<void> _handleRestoreSuccess(PurchaseDetails detail,String orderId,String userId) async{
// 注意:这里只是处理单个 restored,但最终 UI 刷新在 _onPurchaseUpdate 统一完成 // 注意:这里只是处理单个 restored,但最终 UI 刷新在 _onPurchaseUpdate 统一完成
final ok = await _verifyReceipt(detail,orderId??'',userId??'',"purchased"); final ok = await _verifyReceipt(detail,orderId??'',userId??'',"restored");
if(ok){ if(ok){
if(detail.pendingCompletePurchase){ if(detail.pendingCompletePurchase){
await _iap.completePurchase(detail); await _iap.completePurchase(detail);
......
name: appframe name: appframe
description: "app frame project." description: "app frame project."
publish_to: 'none' publish_to: 'none'
version: 1.1.3 version: 1.1.4
environment: environment:
sdk: ">=3.5.0 <4.0.0" sdk: ">=3.5.0 <4.0.0"
...@@ -62,7 +62,7 @@ dependencies: ...@@ -62,7 +62,7 @@ dependencies:
# --- Apple 登录 --- # --- Apple 登录 ---
sign_in_with_apple: ^7.0.1 sign_in_with_apple: ^7.0.1
in_app_purchase: ^3.2.0 # 使用最新稳定版 in_app_purchase: ^3.2.3 # 使用最新稳定版
http: ^1.2.0 # 用于服务端验证 http: ^1.2.0 # 用于服务端验证
# --- 音视频与直播 (重灾区) --- # --- 音视频与直播 (重灾区) ---
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!