Commit decaf318 by ethanlamzs

Merge branch 'feature-2607-iosbuild-ext' into feature-2607-iosbuild

线上版本
2 parents a646ed94 e1c4d9b1
...@@ -47,3 +47,5 @@ assets/static ...@@ -47,3 +47,5 @@ assets/static
pubspec.lock pubspec.lock
assets/dist.zip assets/dist.zip
macos/Podfile.lock macos/Podfile.lock
macos/Flutter/GeneratedPluginRegistrant.swift
windows/flutter/generated_plugins.cmake
...@@ -21,6 +21,8 @@ android { ...@@ -21,6 +21,8 @@ android {
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
compileOptions { compileOptions {
// flutter_local_notifications 插件要求启用核心库脱糖
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
...@@ -91,6 +93,9 @@ flutter { ...@@ -91,6 +93,9 @@ flutter {
} }
dependencies { dependencies {
// flutter_local_notifications 插件要求启用核心库脱糖
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
// 添加 AppCompat 支持库(PrivacyActivity需要) // 添加 AppCompat 支持库(PrivacyActivity需要)
implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.appcompat:appcompat:1.6.1")
...@@ -104,7 +109,7 @@ dependencies { ...@@ -104,7 +109,7 @@ dependencies {
// OPPO // OPPO
//implementation("com.tencent.timpush:oppo:8.8.7357") //implementation("com.tencent.timpush:oppo:8.8.7357")
// vivo // vivo
//implementation("com.tencent.timpush:vivo:8.8.7357") implementation("com.tencent.timpush:vivo:8.8.7357")
// Honor // Honor
//implementation("com.tencent.timpush:honor:8.7.7201") //implementation("com.tencent.timpush:honor:8.7.7201")
// Meizu // Meizu
......
{"version":"1.0.1","cn.banxe.bxe":{"manifestPlaceholders":{"VIVO_APPKEY":"f7e37b20f48234c425d9e7f82fdd16c0","VIVO_APPID":"105993977","HONOR_APPID":""},"vivoPushBussinessId":"44580","oppoPushBussinessId":"46190","oppoPushAppKey":"922926ff488b4c2cb76bb4103ca8a93d","oppoPushAppSecret":"30636ddc19d94571aa1378db15d61ac0"}}
\ No newline at end of file \ No newline at end of file
{
"version": "1.0.1",
"cn.banxe.bxe": {
"manifestPlaceholders": {
"VIVO_APPKEY": "f7e37b20f48234c425d9e7f82fdd16c0",
"VIVO_APPID": "105993977",
"HONOR_APPID": ""
},
"vivoPushBussinessId": "44580"
}
}
\ No newline at end of file \ No newline at end of file
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
</Testables> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Profile"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
......
{
"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
}
}
...@@ -57,6 +57,9 @@ class WebState extends Equatable { ...@@ -57,6 +57,9 @@ class WebState extends Equatable {
final int? userType; final int? userType;
final String? stuId; final String? stuId;
/// 通知点击等场景需要直接打开的 URL
final String? targetUrl;
/// getOrientationCmd /// getOrientationCmd
final bool orientationCmdFlag; final bool orientationCmdFlag;
final String orientationCmdMessage; final String orientationCmdMessage;
...@@ -99,6 +102,7 @@ class WebState extends Equatable { ...@@ -99,6 +102,7 @@ class WebState extends Equatable {
this.classCode, this.classCode,
this.userType, this.userType,
this.stuId, this.stuId,
this.targetUrl,
this.orientationCmdFlag = false, this.orientationCmdFlag = false,
this.orientationCmdMessage = '', this.orientationCmdMessage = '',
this.windowInfoCmdFlag = false, this.windowInfoCmdFlag = false,
...@@ -131,6 +135,7 @@ class WebState extends Equatable { ...@@ -131,6 +135,7 @@ class WebState extends Equatable {
String? classCode, String? classCode,
int? userType, int? userType,
String? stuId, String? stuId,
String? targetUrl,
bool? orientationCmdFlag, bool? orientationCmdFlag,
String? orientationCmdMessage, String? orientationCmdMessage,
bool? windowInfoCmdFlag, bool? windowInfoCmdFlag,
...@@ -162,6 +167,7 @@ class WebState extends Equatable { ...@@ -162,6 +167,7 @@ class WebState extends Equatable {
classCode: classCode ?? this.classCode, classCode: classCode ?? this.classCode,
userType: userType ?? this.userType, userType: userType ?? this.userType,
stuId: stuId ?? this.stuId, stuId: stuId ?? this.stuId,
targetUrl: targetUrl ?? this.targetUrl,
orientationCmdFlag: orientationCmdFlag ?? this.orientationCmdFlag, orientationCmdFlag: orientationCmdFlag ?? this.orientationCmdFlag,
orientationCmdMessage: orientationCmdMessage ?? this.orientationCmdMessage, orientationCmdMessage: orientationCmdMessage ?? this.orientationCmdMessage,
windowInfoCmdFlag: windowInfoCmdFlag ?? this.windowInfoCmdFlag, windowInfoCmdFlag: windowInfoCmdFlag ?? this.windowInfoCmdFlag,
...@@ -195,6 +201,7 @@ class WebState extends Equatable { ...@@ -195,6 +201,7 @@ class WebState extends Equatable {
classCode, classCode,
userType, userType,
stuId, stuId,
targetUrl,
orientationCmdFlag, orientationCmdFlag,
orientationCmdMessage, orientationCmdMessage,
windowInfoCmdFlag, windowInfoCmdFlag,
...@@ -394,6 +401,15 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver { ...@@ -394,6 +401,15 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
} }
void _loadHtml() { void _loadHtml() {
// 如果通过路由透传了 targetUrl(如通知点击场景),
// 优先在 WebView 中打开该 URL。
final targetUrl = state.targetUrl;
if (targetUrl != null && targetUrl.isNotEmpty) {
final String resolvedUrl = '${Constant.localServerUrl}/index.html#$targetUrl';
_controller.loadRequest(Uri.parse(resolvedUrl));
return;
}
var sharedPreferences = getIt.get<SharedPreferences>(); var sharedPreferences = getIt.get<SharedPreferences>();
var debug = sharedPreferences.getInt('debug') ?? 0; var debug = sharedPreferences.getInt('debug') ?? 0;
// 构造函数中已拦截判断未登录的情况进行了处理,所以这里不再处理未登录的情况 // 构造函数中已拦截判断未登录的情况进行了处理,所以这里不再处理未登录的情况
...@@ -452,7 +468,7 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver { ...@@ -452,7 +468,7 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
imService.registerPush(); imService.registerPush();
// 处理加群和退群 // 处理加群和退群
if (state.loginOpFlag) { if (state.loginOpFlag) {
// imService.joinAndLeaveGroup(state.userCode!); imService.joinAndLeaveGroup(state.userCode!);
} }
} else { } else {
debugPrint("缓存自动登录处,IM 登录失败"); debugPrint("缓存自动登录处,IM 登录失败");
......
...@@ -125,6 +125,6 @@ class Constant { ...@@ -125,6 +125,6 @@ class Constant {
static const String hasShownPrivacyFirstTimeKey = 'has_shown_privacy_first_time'; static const String hasShownPrivacyFirstTimeKey = 'has_shown_privacy_first_time';
/// 测试阶段使用 /// 测试阶段使用
static const bool needIM = false; static const bool needIM = true;
static const bool needUpgrade = true; static const bool needUpgrade = true;
} }
...@@ -50,6 +50,7 @@ import 'package:appframe/services/api_service.dart'; ...@@ -50,6 +50,7 @@ import 'package:appframe/services/api_service.dart';
import 'package:appframe/services/dispatcher.dart'; import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/services/im_service.dart'; import 'package:appframe/services/im_service.dart';
import 'package:appframe/services/local_server_service.dart'; import 'package:appframe/services/local_server_service.dart';
import 'package:appframe/services/notification_service.dart';
import 'package:appframe/services/player_service.dart'; import 'package:appframe/services/player_service.dart';
import 'package:appframe/services/recorder_service.dart'; import 'package:appframe/services/recorder_service.dart';
import 'package:fluwx/fluwx.dart'; import 'package:fluwx/fluwx.dart';
...@@ -241,6 +242,9 @@ Future<void> setupLocator() async { ...@@ -241,6 +242,9 @@ Future<void> setupLocator() async {
instanceName: "appApiService", instanceName: "appApiService",
); );
/// 通知服务
getIt.registerSingleton<NotificationService>(NotificationService());
/// imService /// imService
getIt.registerSingleton<ImService>(ImService()); getIt.registerSingleton<ImService>(ImService());
......
...@@ -235,14 +235,26 @@ class XeCreateOrderHandler extends MessageHandler { ...@@ -235,14 +235,26 @@ class XeCreateOrderHandler extends MessageHandler {
// 为了调试,先模拟 // 为了调试,先模拟
var product; var product;
var packageName = "";
if (totalFee.toString()=="4990"){
packageName="月卡";
}else if (totalFee.toString()=="12900"){
packageName="季卡";
}else if (totalFee.toString()=="20900"){
packageName="半年卡";
}else if (totalFee.toString()=="29900"){
packageName="年卡";
}
for (var p in _subService.products) { for (var p in _subService.products) {
// 因为带了一个¥符号 // 因为带了一个¥符号
//debugPrint('checking ==> 产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}'); //debugPrint('checking ==> 产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}');
if (p.price.toString().indexOf(totalFee.toString().substring(0,2))==1){ if (p.title.toString().indexOf(packageName)>0){
product = p; product = p;
//debugPrint('match =====> 产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}'); debugPrint('match =====> 产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}');
break; break;
} }
//
} }
if(product==null){ if(product==null){
throw Exception('订单生成失败'); throw Exception('订单生成失败');
......
...@@ -18,7 +18,7 @@ class SubsRepository { ...@@ -18,7 +18,7 @@ class SubsRepository {
{ {
"type": type, "type": type,
"userid": userid, "userid": userid,
"classCode": classCode, "classCodes": classCode,
}, },
); );
return resp.statusCode == HttpStatus.ok ? resp.data : null; return resp.statusCode == HttpStatus.ok ? resp.data : null;
......
...@@ -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());
} }
......
...@@ -2,7 +2,9 @@ import 'package:appframe/config/constant.dart'; ...@@ -2,7 +2,9 @@ import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart'; import 'package:appframe/config/locator.dart';
import 'package:appframe/data/repositories/subs_repository.dart'; import 'package:appframe/data/repositories/subs_repository.dart';
import 'package:appframe/services/api_service.dart'; import 'package:appframe/services/api_service.dart';
import 'package:appframe/services/notification_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tencent_cloud_chat_push/common/tim_push_listener.dart'; import 'package:tencent_cloud_chat_push/common/tim_push_listener.dart';
import 'package:tencent_cloud_chat_push/common/tim_push_message.dart'; import 'package:tencent_cloud_chat_push/common/tim_push_message.dart';
...@@ -65,120 +67,140 @@ class ImService { ...@@ -65,120 +67,140 @@ class ImService {
}, },
onRecvMessageReadReceipts: (List<V2TimMessageReceipt> receiptList) { onRecvMessageReadReceipts: (List<V2TimMessageReceipt> receiptList) {
//群聊已读回调 //群聊已读回调
for (var element in receiptList) { // for (var element in receiptList) {
element.groupID; // 群id // element.groupID; // 群id
element.msgID; // 已读回执消息 ID // element.msgID; // 已读回执消息 ID
element.readCount; // 群消息最新已读数 // element.readCount; // 群消息最新已读数
element.unreadCount; // 群消息最新未读数 // element.unreadCount; // 群消息最新未读数
element.userID; // C2C 消息对方 ID // element.userID; // C2C 消息对方 ID
} // }
}, },
onRecvMessageRevoked: (String messageid) { onRecvMessageRevoked: (String messageid) {
// 在本地维护的消息中处理被对方撤回的消息 // 在本地维护的消息中处理被对方撤回的消息
}, },
onRecvNewMessage: (V2TimMessage message) async { onRecvNewMessage: (V2TimMessage message) async {
// 处理文本消息 // 标记消息为已读
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) { if (message.userID != null && message.userID!.isNotEmpty) {
// message.textElem?.text; // C2C消息标记已读
var res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().cleanConversationUnreadMessageCount(
// 时间戳转换 conversationID: 'c2c_${message.userID}',
DateTime dt = DateTime.fromMillisecondsSinceEpoch(message.timestamp! * 1000); cleanTimestamp: 0,
debugPrint( cleanSequence: 0,
"收到IM消息—— 时间:${dt.year}-${dt.month}-${dt.day} ${dt.hour}:${dt.minute}:${dt.second} 发送者:${message.sender} 内容:${message.textElem?.text}");
// 目前只会有文本消息,所以其他消息类型暂不处理,直接return
return;
}
// 使用自定义消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) {
message.customElem?.data;
message.customElem?.desc;
message.customElem?.extension;
}
// 使用图片消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_IMAGE) {
message.imageElem?.path; // 图片上传时的路径,消息发送者才会有这个字段,消息发送者可用这个字段将图片预先上屏,优化上屏体验。
message.imageElem?.imageList?.forEach((element) {
// 遍历大图、原图、缩略图
// 解析图片属性
element?.height;
element?.localUrl;
element?.size;
element?.type; // 大图 缩略图 原图
element?.url;
element?.uuid;
element?.width;
});
}
// 处理视频消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) {
// 解析视频消息属性,封面、播放地址、宽高、大小等。
message.videoElem?.UUID;
message.videoElem?.duration;
message.videoElem?.localSnapshotUrl;
message.videoElem?.localVideoUrl;
message.videoElem?.snapshotHeight;
message.videoElem?.snapshotPath;
// ...
}
// 处理音频消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND) {
// 解析语音消息 播放地址,本地地址,大小,时长等。
message.soundElem?.UUID;
message.soundElem?.dataSize;
message.soundElem?.duration;
message.soundElem?.localUrl;
message.soundElem?.url;
// ...
}
// 处理文件消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_FILE) {
// 解析文件消息 文件名、文件大小、url等
message.fileElem?.UUID;
message.fileElem?.fileName;
message.fileElem?.fileSize;
message.fileElem?.localUrl;
message.fileElem?.path;
message.fileElem?.url;
}
// 处理位置消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_LOCATION) {
// 解析地理位置消息,经纬度、描述等
message.locationElem?.desc;
message.locationElem?.latitude;
message.locationElem?.longitude;
}
// 处理表情消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_FACE) {
message.faceElem?.data;
message.faceElem?.index;
}
// 处理群组tips文本消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS) {
message.groupTipsElem?.groupID; // 所属群组
message.groupTipsElem?.type; // 群Tips类型
message.groupTipsElem?.opMember; // 操作人资料
message.groupTipsElem?.memberList; // 被操作人资料
message.groupTipsElem?.groupChangeInfoList; // 群信息变更详情
message.groupTipsElem?.memberChangeInfoList; // 群成员变更信息
message.groupTipsElem?.memberCount; // 当前群在线人数
}
// 处理合并消息消息
if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_MERGER) {
message.mergerElem?.abstractList;
message.mergerElem?.isLayersOverLimit;
message.mergerElem?.title;
V2TimValueCallback<List<V2TimMessage>> download =
await TencentImSDKPlugin.v2TIMManager.getMessageManager().downloadMergerMessage(
msgID: message.msgID!,
); );
if (download.code == 0) { debugPrint("[im_service]标记C2C消息已读: userID=${message.userID}, result=${res.code}");
//List<V2TimMessage>? messageList = download.data; } else if (message.groupID != null && message.groupID!.isNotEmpty) {
} // 群消息标记已读
} var res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().cleanConversationUnreadMessageCount(
if (message.textElem?.nextElem != null) { conversationID: 'group_${message.groupID}',
//通过第一个 Elem 对象的 nextElem 方法获取下一个 Elem 对象,如果下一个 Elem 对象存在,会返回 Elem 对象实例,如果不存在,会返回 null。 cleanTimestamp: 0,
cleanSequence: 0,
);
debugPrint("[im_service]标记群消息已读: groupID=${message.groupID}, result=${res.code}");
} }
// 处理文本消息
// if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) {
// // message.textElem?.text;
//
// // 时间戳转换
// DateTime dt = DateTime.fromMillisecondsSinceEpoch(message.timestamp! * 1000);
// debugPrint("[高级消息监听器] onRecvNewMessage 时间:${dt.year}-${dt.month}-${dt.day} ${dt.hour}:${dt.minute}:${dt.second} 发送者:${message.sender} 内容:${message.textElem?.text}");
//
// Fluttertoast.showToast(msg: '[高级消息监听器] onRecvNewMessage ${message.textElem?.text ?? ""}', gravity: ToastGravity.TOP);
//
// // 目前只会有文本消息,所以其他消息类型暂不处理,直接return
// return;
// }
// 使用自定义消息
// if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) {
// message.customElem?.data;
// message.customElem?.desc;
// message.customElem?.extension;
// }
// // 使用图片消息
// if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_IMAGE) {
// message.imageElem?.path; // 图片上传时的路径,消息发送者才会有这个字段,消息发送者可用这个字段将图片预先上屏,优化上屏体验。
// message.imageElem?.imageList?.forEach((element) {
// // 遍历大图、原图、缩略图
// // 解析图片属性
// element?.height;
// element?.localUrl;
// element?.size;
// element?.type; // 大图 缩略图 原图
// element?.url;
// element?.uuid;
// element?.width;
// });
// }
// // 处理视频消息
// if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) {
// // 解析视频消息属性,封面、播放地址、宽高、大小等。
// message.videoElem?.UUID;
// message.videoElem?.duration;
// message.videoElem?.localSnapshotUrl;
// message.videoElem?.localVideoUrl;
// message.videoElem?.snapshotHeight;
// message.videoElem?.snapshotPath;
// // ...
// }
// // 处理音频消息
// if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND) {
// // 解析语音消息 播放地址,本地地址,大小,时长等。
// message.soundElem?.UUID;
// message.soundElem?.dataSize;
// message.soundElem?.duration;
// message.soundElem?.localUrl;
// message.soundElem?.url;
// // ...
// }
// // 处理文件消息
// if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_FILE) {
// // 解析文件消息 文件名、文件大小、url等
// message.fileElem?.UUID;
// message.fileElem?.fileName;
// message.fileElem?.fileSize;
// message.fileElem?.localUrl;
// message.fileElem?.path;
// message.fileElem?.url;
// }
// // 处理位置消息
// if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_LOCATION) {
// // 解析地理位置消息,经纬度、描述等
// message.locationElem?.desc;
// message.locationElem?.latitude;
// message.locationElem?.longitude;
// }
// // 处理表情消息
// if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_FACE) {
// message.faceElem?.data;
// message.faceElem?.index;
// }
// // 处理群组tips文本消息
// if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS) {
// message.groupTipsElem?.groupID; // 所属群组
// message.groupTipsElem?.type; // 群Tips类型
// message.groupTipsElem?.opMember; // 操作人资料
// message.groupTipsElem?.memberList; // 被操作人资料
// message.groupTipsElem?.groupChangeInfoList; // 群信息变更详情
// message.groupTipsElem?.memberChangeInfoList; // 群成员变更信息
// message.groupTipsElem?.memberCount; // 当前群在线人数
// }
// // 处理合并消息消息
// if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_MERGER) {
// message.mergerElem?.abstractList;
// message.mergerElem?.isLayersOverLimit;
// message.mergerElem?.title;
// V2TimValueCallback<List<V2TimMessage>> download =
// await TencentImSDKPlugin.v2TIMManager.getMessageManager().downloadMergerMessage(
// msgID: message.msgID!,
// );
// if (download.code == 0) {
// //List<V2TimMessage>? messageList = download.data;
// }
// }
// if (message.textElem?.nextElem != null) {
// //通过第一个 Elem 对象的 nextElem 方法获取下一个 Elem 对象,如果下一个 Elem 对象存在,会返回 Elem 对象实例,如果不存在,会返回 null。
// }
}, },
onSendMessageProgress: (V2TimMessage message, int progress) { onSendMessageProgress: (V2TimMessage message, int progress) {
//文件上传进度回调 //文件上传进度回调
...@@ -349,33 +371,33 @@ class ImService { ...@@ -349,33 +371,33 @@ class ImService {
void _onNotificationClicked({required String ext, String? userID, String? groupID}) { void _onNotificationClicked({required String ext, String? userID, String? groupID}) {
debugPrint("_onNotificationClicked: $ext, userID: $userID, groupID: $groupID"); debugPrint("_onNotificationClicked: $ext, userID: $userID, groupID: $groupID");
if (userID != null || groupID != null) { getIt.get<NotificationService>().handleNotificationClick(ext);
// 根据 userID 或 groupID 跳转至对应 Message 页面.
} else {
// 根据 ext 字段, 自己写解析方式, 跳转至对应页面.
}
} }
TIMPushListener timPushListener = TIMPushListener( TIMPushListener timPushListener = TIMPushListener(
onRecvPushMessage: (TimPushMessage message) { onRecvPushMessage: (TimPushMessage message) {
debugPrint('推送监听器 onRecvPushMessage-------------'); // debugPrint('[推送监听器] onRecvPushMessage-------------');
String messageLog = message.toLogString(); // String messageLog = message.toLogString();
debugPrint("message: $messageLog"); // debugPrint("message: $messageLog");
//
// // 手机消息通知 // Fluttertoast.showToast(msg: "[推送监听器] onRecvPushMessage: ${message.toLogString()}", gravity: ToastGravity.TOP);
// getIt.get<NotificationService>().showNotification( debugPrint('[im_service][推送监听器] onRecvPushMessage------------- 调起手机通知');
// id: DateTime.now().millisecondsSinceEpoch % 1000000, // 手机消息通知
// title: message.title ?? '', getIt.get<NotificationService>().showNotification(
// body: message.desc ?? '', id: DateTime.now().millisecondsSinceEpoch % 1000000,
// ); title: message.title ?? '',
body: message.desc ?? '',
payload: message.ext ?? '',
);
}, },
onRevokePushMessage: (String messageId) { onRevokePushMessage: (String messageId) {
debugPrint('推送监听器 onRevokePushMessage-------------'); debugPrint('[im_service][推送监听器] onRevokePushMessage-------------');
debugPrint("message: $messageId"); debugPrint("message: $messageId");
}, },
onNotificationClicked: (String ext) { onNotificationClicked: (String ext) {
debugPrint('推送监听器 onNotificationClicked-------------'); debugPrint('[im_service][推送监听器] onNotificationClicked-------------');
debugPrint("ext: $ext"); debugPrint("ext: $ext");
//Fluttertoast.showToast(msg: "[im_service][推送监听器] Notification Clicked: $ext", toastLength: Toast.LENGTH_LONG);
}, },
); );
......
import 'dart:convert';
import 'dart:io';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NotificationService {
final FlutterLocalNotificationsPlugin _plugin = FlutterLocalNotificationsPlugin();
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
/// 初始化通知插件
Future<void> initialize() async {
if (_isInitialized) return;
const androidSettings = AndroidInitializationSettings('@mipmap/launcher_icon');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const settings = InitializationSettings(android: androidSettings, iOS: iosSettings);
final result = await _plugin.initialize(
settings,
onDidReceiveNotificationResponse: _onNotificationResponse,
);
_isInitialized = result ?? false;
if (_isInitialized) {
debugPrint('NotificationService 初始化成功');
// Android 13+ 需要请求通知权限
if (Platform.isAndroid) {
final androidPlugin = _plugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
await androidPlugin?.requestNotificationsPermission();
}
} else {
debugPrint('NotificationService 初始化失败');
}
}
/// 处理通知点击(供外部调用,如离线推送点击)
void handleNotificationClick(String? payload) {
debugPrint('[notification_service] handleNotificationClick payload: $payload');
// 通过 /reload 中转路由跳转,确保即使当前已在 /web,
// GoRouter 也会完整重建 WebPage 并加载新的 targetUrl。
final targetUrl = _buildTargetUrl(payload);
debugPrint('[notification_service] targetUrl: $targetUrl');
if (targetUrl != null && targetUrl.isNotEmpty) {
router.go(
'/reload',
extra: {'targetUrl': targetUrl},
);
} else {
router.go('/web');
}
}
/// 前台本地通知点击回调处理
void _onNotificationResponse(NotificationResponse response) {
debugPrint('[notification_service] Notification clicked, payload: ${response.payload}');
handleNotificationClick(response.payload);
}
/// 解析通知 payload,结合 auth_roles 缓存拼接目标 URL。
/// payload 格式:{"func": "homework", "uniqueCode": "...", "classCode": "..."}
/// 返回示例:/h5/login/pages/applogin?sessionCode=xxx&userCode=xxx&classCode=xxx&userType=2&stuId=xxx&page=homeworkdetail&uniqueCode=xxx
String? _buildTargetUrl(String? payload) {
if (payload == null || payload.isEmpty) return null;
try {
final data = jsonDecode(payload) as Map<String, dynamic>;
final func = data['func'] as String?;
final uniqueCode = data['uniqueCode'] as String?;
final classCode = data['classCode'] as String?;
if (uniqueCode == null || uniqueCode.isEmpty || classCode == null || classCode.isEmpty) {
debugPrint('[notification_service] payload 缺少 uniqueCode 或 classCode');
return null;
}
final prefs = getIt.get<SharedPreferences>();
final rolesJson = prefs.getString('auth_roles') ?? '';
if (rolesJson.isEmpty) {
debugPrint('[notification_service] auth_roles 缓存为空');
return null;
}
final roles = jsonDecode(rolesJson) as List<dynamic>;
Map<String, dynamic>? matchedRole;
for (final role in roles) {
if (role is Map<String, dynamic> && role['classCode']?.toString() == classCode) {
matchedRole = role;
break;
}
}
if (matchedRole == null) {
debugPrint('[notification_service] 未找到与 classCode=$classCode 匹配的 role');
return null;
}
final stuId = matchedRole['stuId']?.toString() ?? '';
final sessionCode = prefs.getString('auth_sessionCode') ?? '';
final userCode = prefs.getString('auth_userCode') ?? '';
var funcPage = '';
if (func == 'homework') {
funcPage = 'page=homeworkdetail&uniqueCode=$uniqueCode';
} else if (func == 'clockin') {
funcPage = 'page=clockdetail&clockUnique=$uniqueCode';
} else {
debugPrint('[notification_service] 不支持功能的类型: $func');
return null;
}
return '/h5/login/pages/applogin?'
'sessionCode=$sessionCode&'
'userCode=$userCode&'
'classCode=$classCode&'
'userType=2&'
'stuId=$stuId&'
'$funcPage';
} catch (e, stackTrace) {
debugPrint('[notification_service] 解析 payload 失败: $e\n$stackTrace');
return null;
}
}
/// 显示手机通知(懒初始化:首次调用自动触发初始化)
Future<void> showNotification({
required int id,
required String title,
required String body,
String? payload,
}) async {
// 懒初始化:首次调用时自动初始化
if (!_isInitialized) {
await initialize();
}
const androidDetails = AndroidNotificationDetails(
'homework_message_channel',
'作业消息通知',
channelDescription: '接收作业消息推送通知',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
const details = NotificationDetails(android: androidDetails, iOS: iosDetails);
await _plugin.show(id, title, body, details, payload: payload);
debugPrint('NotificationService showNotification: id=$id, title=$title, body=$body');
}
}
// 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,12 +17,17 @@ class SubscriptionService { ...@@ -15,12 +17,17 @@ 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 → 自定义数据) ----------
final Map<String, Map<String, dynamic>> _pendingContexts = {}; 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, String> _orderIdMap = {}; // key: productID, value: orderId final Map<String, Map<String, dynamic>> _pendingContexts = {};
final InAppPurchase _iap = InAppPurchase.instance; final InAppPurchase _iap = InAppPurchase.instance;
late StreamSubscription<List<PurchaseDetails>> _purchaseUpdatedSubscription; late StreamSubscription<List<PurchaseDetails>> _purchaseUpdatedSubscription;
...@@ -58,11 +65,15 @@ class SubscriptionService { ...@@ -58,11 +65,15 @@ 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
// 4. 处理待处理的交易(应用被杀掉时未完成交易)
//await _iap.restorePurchases(); // 订阅恢复也会触发 purchaseStream
} }
...@@ -79,30 +90,138 @@ class SubscriptionService { ...@@ -79,30 +90,138 @@ class SubscriptionService {
} }
_products = response.productDetails; _products = response.productDetails;
// debugPrint('获取到 ${_products.length} 个产品'); debugPrint('获取到 ${_products.length} 个产品');
// for (var p in _products) { for (var p in _products) {
// 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 序列化(请确保字段名称与你的插件版本一致)
Future<bool> buySubscription(ProductDetails product, String userId , String tbxStuId, String yWOrderId) async { 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 {
try{
await _iap.restorePurchases();
}
catch(e){
onError?.call(e.toString());
}
}
// ---------- 购买订阅 ----------
Future<bool> buySubscription(ProductDetails product, String userId , String tbxStuId, String yWOrderId) async {
_pendingContexts[product.id] = {'userId': userId,'tbxStuId': tbxStuId,"orderId":yWOrderId}; _pendingContexts[product.id] = {'userId': userId,'tbxStuId': tbxStuId,"orderId":yWOrderId};
_orderIdMap[product.id] = yWOrderId; // 按产品 ID 存储
// 订阅必须使用 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;
} }
...@@ -114,15 +233,14 @@ class SubscriptionService { ...@@ -114,15 +233,14 @@ class SubscriptionService {
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;
} }
// 🔁 去重措施 1:根据原始交易 ID 去重,防止同一笔交易被重复处理 // 🔁 去重措施 1:根据原始交易 ID 去重,防止同一笔交易被重复处理
final transactionId = purchaseDetails.verificationData.serverVerificationData.hashCode.toString(); //final transactionId = purchaseDetails.verificationData.serverVerificationData.hashCode.toString();
// 如果能获取到 purchaseID 更可靠,这里用 purchaseID 作为唯一标识 // 如果能获取到 purchaseID 更可靠,这里用 purchaseID 作为唯一标识
final uniqueId = purchaseDetails.purchaseID; final uniqueId = purchaseDetails.purchaseID;
if (_processedTransactionIds.contains(uniqueId)) { if (_processedTransactionIds.contains(uniqueId)) {
...@@ -138,19 +256,19 @@ class SubscriptionService { ...@@ -138,19 +256,19 @@ class SubscriptionService {
_processedTransactionIds.add(uniqueId); _processedTransactionIds.add(uniqueId);
} }
//var orderId = _orderIdMap.remove(purchaseDetails.productID); // 取出并删除
final ctx = _pendingContexts[purchaseDetails.productID] ; final ctx = _pendingContexts[purchaseDetails.productID] ;
print(ctx); print(ctx);
if (ctx == null) { if (ctx == null) {
// 打印错误日志,防止崩溃 // 打印错误日志,防止崩溃
print('错误:未找到 productID 对应的上下文缓存'); print('错误:未找到 productID 对应的上下文缓存');
return; debugPrint("No purchase context: ${purchaseDetails.productID}");
continue;
} }
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());
// 根据交易状态处理 // 根据交易状态处理
...@@ -180,8 +298,10 @@ class SubscriptionService { ...@@ -180,8 +298,10 @@ class SubscriptionService {
// 用户取消支付 // 用户取消支付
//_iap.completePurchase(purchaseDetails); //_iap.completePurchase(purchaseDetails);
break; break;
default: case PurchaseStatus.pending:
onStateChanged?.call();
break; break;
} }
// // ✅ 关键:只要不是 pending,都立刻完成交易,清出队列 // // ✅ 关键:只要不是 pending,都立刻完成交易,清出队列
...@@ -201,10 +321,10 @@ class SubscriptionService { ...@@ -201,10 +321,10 @@ class SubscriptionService {
} }
// ---------- 服务端验证收据 ---------- // ---------- 服务端验证收据 ----------
Future<void> _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 发给你的服务端验证
...@@ -215,22 +335,20 @@ class SubscriptionService { ...@@ -215,22 +335,20 @@ class SubscriptionService {
_hasActiveSubscription = true; _hasActiveSubscription = true;
// 保存到期时间等信息 // 保存到期时间等信息
} }
return isValid;
} }
return false;
} }
// 示例:发送收据到自己的服务器进行二次验证 // 示例:发送收据到自己的服务器进行二次验证
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 = _userAuthRepository.orderCheck(userid,receipt,orderid,"ios",1); debugPrint("[info] ############## _sendReceiptToBackend is done");
//var data = result['data'] as Map<String, dynamic>?; return result!=null;
//debugPrint(result);
debugPrint("[info] _sendReceiptToBackend is done");
return true;
} }
// ---------- 辅助方法 ---------- // ---------- 辅助方法 ----------
bool _isSubscription(String productId) { bool _isSubscription(String productId) {
return productId == monthlySubId_1 || productId == monthlySubId_3|| productId == monthlySubId_6|| productId == monthlySubId_12; return productId == monthlySubId_1 || productId == monthlySubId_3|| productId == monthlySubId_6|| productId == monthlySubId_12;
...@@ -242,25 +360,30 @@ class SubscriptionService { ...@@ -242,25 +360,30 @@ class SubscriptionService {
} }
// ---------- 购买成功处理 ---------- // ---------- 购买成功处理 ----------
void _handlePurchaseSuccess(PurchaseDetails detail,String orderId,String userId) { Future<void> _handlePurchaseSuccess(PurchaseDetails detail,String orderId,String userId) async {
// 发送收据到后端验证,携带 userId // 发送收据到后端验证,携带 userId
_verifyReceipt(detail,orderId??'',userId??'',"purchased"); final ok = await _verifyReceipt(detail,orderId??'',userId??'',"purchased");
if(ok){
// 完成交易
_iap.completePurchase(detail);
// 乐观更新本地状态(等后端最终确认再真正解锁功能)
_hasActiveSubscription = true; _hasActiveSubscription = true;
onSuccess?.call('订阅成功'); if(detail.pendingCompletePurchase){
onStateChanged?.call(); await _iap.completePurchase(detail);
}
}else{
// 不complete
// 等待下次重新验证
}
} }
// ---------- 恢复成功处理 ---------- // ---------- 恢复成功处理 ----------
void _handleRestoreSuccess(PurchaseDetails detail,String orderId,String userId) { Future<void> _handleRestoreSuccess(PurchaseDetails detail,String orderId,String userId) async{
// 注意:这里只是处理单个 restored,但最终 UI 刷新在 _onPurchaseUpdate 统一完成 // 注意:这里只是处理单个 restored,但最终 UI 刷新在 _onPurchaseUpdate 统一完成
_verifyReceipt(detail,orderId??'',userId??'',"purchased"); final ok = await _verifyReceipt(detail,orderId??'',userId??'',"restored");
_iap.completePurchase(detail); if(ok){
if(detail.pendingCompletePurchase){
await _iap.completePurchase(detail);
}
}
} }
// ---------- 错误处理 ---------- // ---------- 错误处理 ----------
...@@ -277,9 +400,9 @@ class SubscriptionService { ...@@ -277,9 +400,9 @@ class SubscriptionService {
onError?.call('支付已取消'); onError?.call('支付已取消');
} }
// 释放资源 // 释放资源
void dispose() { Future<void> dispose() async {
_purchaseUpdatedSubscription.cancel(); await _purchaseUpdatedSubscription.cancel();
} }
} }
import 'package:appframe/config/routes.dart'; import 'package:appframe/config/routes.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
/// ///
/// 用于重新加载的中间路由 /// 用于重新加载的中间路由
/// 通知点击等场景先跳转到此页,再转发 targetUrl 到 /web,
/// 确保 GoRouter 完整重建 WebPage 并加载目标 URL。
/// ///
class ReloadPage extends StatefulWidget { class ReloadPage extends StatefulWidget {
const ReloadPage({super.key}); const ReloadPage({super.key});
...@@ -23,8 +26,13 @@ class _ReloadPageState extends State<ReloadPage> { ...@@ -23,8 +26,13 @@ class _ReloadPageState extends State<ReloadPage> {
} }
void _performPostDisplayOperations() { void _performPostDisplayOperations() {
final extra = GoRouterState.of(context).extra;
if (extra is Map<String, dynamic> && extra['targetUrl'] != null) {
router.go('/web', extra: extra);
} else {
router.go('/web'); router.go('/web');
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
......
...@@ -25,6 +25,7 @@ class WebPage extends StatelessWidget { ...@@ -25,6 +25,7 @@ class WebPage extends StatelessWidget {
var classCode = extraData?['classCode']; var classCode = extraData?['classCode'];
var userType = extraData?['userType']; var userType = extraData?['userType'];
var stuId = extraData?['stuId']; var stuId = extraData?['stuId'];
var targetUrl = extraData?['targetUrl'];
if (sessionCode == null || sessionCode == '') { if (sessionCode == null || sessionCode == '') {
loginOpFlag = false; loginOpFlag = false;
...@@ -49,6 +50,7 @@ class WebPage extends StatelessWidget { ...@@ -49,6 +50,7 @@ class WebPage extends StatelessWidget {
classCode: classCode, classCode: classCode,
userType: userType, userType: userType,
stuId: stuId, stuId: stuId,
targetUrl: targetUrl,
), ),
); );
return webCubit; return webCubit;
......
import 'dart:convert';
import 'package:appframe/config/constant.dart'; import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart'; import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart'; import 'package:appframe/config/routes.dart';
...@@ -89,6 +91,8 @@ class LoginUtil { ...@@ -89,6 +91,8 @@ class LoginUtil {
sharedPreferences.setString('auth_className', className); sharedPreferences.setString('auth_className', className);
sharedPreferences.setString('auth_stuName', stuName); sharedPreferences.setString('auth_stuName', stuName);
sharedPreferences.setString('auth_relation', relation); sharedPreferences.setString('auth_relation', relation);
// 用于处理点击推送通知时,获取跳转到指定页面的参数
sharedPreferences.setString('auth_roles', jsonEncode(roles));
if (loginType == 'router') { if (loginType == 'router') {
router.go( router.go(
......
...@@ -12,6 +12,7 @@ import device_info_plus ...@@ -12,6 +12,7 @@ import device_info_plus
import ffmpeg_kit_flutter_new import ffmpeg_kit_flutter_new
import file_picker import file_picker
import flutter_image_compress_macos import flutter_image_compress_macos
import flutter_local_notifications
import flutter_localization import flutter_localization
import flutter_sound import flutter_sound
import geolocator_apple import geolocator_apple
...@@ -40,6 +41,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ...@@ -40,6 +41,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FFmpegKitFlutterPlugin.register(with: registry.registrar(forPlugin: "FFmpegKitFlutterPlugin")) FFmpegKitFlutterPlugin.register(with: registry.registrar(forPlugin: "FFmpegKitFlutterPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin")) FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
FlutterSoundPlugin.register(with: registry.registrar(forPlugin: "FlutterSoundPlugin")) FlutterSoundPlugin.register(with: registry.registrar(forPlugin: "FlutterSoundPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
......
name: appframe name: appframe
description: "app frame project." description: "app frame project."
publish_to: 'none' publish_to: 'none'
version: 1.1.0 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 # 用于服务端验证
# --- 音视频与直播 (重灾区) --- # --- 音视频与直播 (重灾区) ---
...@@ -97,6 +97,7 @@ dependencies: ...@@ -97,6 +97,7 @@ dependencies:
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
fluttertoast: ^9.0.0 fluttertoast: ^9.0.0
flutter_local_notifications: ^19.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
......
...@@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ...@@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!