Skip to content
Toggle navigation
Toggle navigation
This project
Loading...
Sign in
ethan
/
appframe
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Commit e1c4d9b1
authored
2026-08-11 12:27:42 +0800
by
ethanlamzs
Browse Files
Options
Browse Files
Tag
Download
Plain Diff
Merge branch 'feature-2607-iosbuild-ext-new' into feature-2607-iosbuild-ext
2 parents
1d8c2b03
64002d44
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
155 additions
and
26 deletions
lib/main.dart
lib/services/subscription_service_ios.dart
pubspec.yaml
lib/main.dart
View file @
e1c4d9b
...
@@ -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
());
}
}
...
...
lib/services/subscription_service_ios.dart
View file @
e1c4d9b
// 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
}
}
...
@@ -84,25 +95,133 @@ class SubscriptionService {
...
@@ -84,25 +95,133 @@ 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
{
try
{
await
_iap
.
restorePurchases
();
}
catch
(
e
){
onError
?.
call
(
e
.
toString
());
}
}
}
// ---------- 购买订阅 ----------
// ---------- 购买订阅 ----------
Future
<
bool
>
buySubscription
(
ProductDetails
product
,
String
userId
,
String
tbxStuId
,
String
yWOrderId
)
async
{
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 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
...
@@ -114,15 +233,14 @@ 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
;
}
}
// 🔁 去重措施 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 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
...
@@ -138,19 +256,19 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
_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 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
...
@@ -180,8 +298,10 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
// 用户取消支付
// 用户取消支付
//_iap.completePurchase(purchaseDetails);
//_iap.completePurchase(purchaseDetails);
break
;
break
;
default
:
case
PurchaseStatus
.
pending
:
onStateChanged
?.
call
();
break
;
break
;
}
}
// // ✅ 关键:只要不是 pending,都立刻完成交易,清出队列
// // ✅ 关键:只要不是 pending,都立刻完成交易,清出队列
...
@@ -202,9 +322,9 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
...
@@ -202,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 发给你的服务端验证
...
@@ -223,7 +343,7 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
...
@@ -223,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
;
}
}
...
@@ -256,10 +376,14 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
...
@@ -256,10 +376,14 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
}
}
// ---------- 恢复成功处理 ----------
// ---------- 恢复成功处理 ----------
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
);
}
}
}
}
// ---------- 错误处理 ----------
// ---------- 错误处理 ----------
...
@@ -276,9 +400,9 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
...
@@ -276,9 +400,9 @@ Future<bool> buySubscription(ProductDetails product, String userId , String tbxS
onError
?.
call
(
'支付已取消'
);
onError
?.
call
(
'支付已取消'
);
}
}
// 释放资源
// 释放资源
void
dispose
()
{
Future
<
void
>
dispose
()
async
{
_purchaseUpdatedSubscription
.
cancel
();
await
_purchaseUpdatedSubscription
.
cancel
();
}
}
}
}
pubspec.yaml
View file @
e1c4d9b
name
:
appframe
name
:
appframe
description
:
"
app
frame
project."
description
:
"
app
frame
project."
publish_to
:
'
none'
publish_to
:
'
none'
version
:
1.1.
2
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
# 用于服务端验证
# --- 音视频与直播 (重灾区) ---
# --- 音视频与直播 (重灾区) ---
...
...
Write
Preview
Styling with
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment