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 bfe36a4a
authored
2026-07-10 19:05:56 +0800
by
ethanlamzs
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
debug支付
1 parent
45d44847
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
256 additions
and
33 deletions
ios/Podfile
ios/Runner.xcodeproj/project.pbxproj
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
ios/Runner.xcworkspace/contents.xcworkspacedata
ios/Runner/AppDelegate.swift
ios/Runner/Testing.storekit
lib/data/repositories/message/xe_create_order_handler.dart
lib/services/subscription_service_ios.dart
lib/utils/ios_receiptHelper.dart
ios/Podfile
View file @
bfe36a4
...
...
@@ -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
...
...
ios/Runner.xcodeproj/project.pbxproj
View file @
bfe36a4
...
...
@@ -72,7 +72,7 @@
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; };
DC
CFA01F2FF3F74300E9FFD3 /* LocalIAP.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; name = LocalIAP.storekit; path = Runner/LocalIAP
.storekit; sourceTree = "<group>"; };
DC
EFFEE63000FA7000F7B036 /* 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 */
...
...
@@ -119,7 +119,7 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
DC
CFA01F2FF3F74300E9FFD3 /* LocalIAP
.storekit */,
DC
EFFEE63000FA7000F7B036 /* Testing
.storekit */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
...
...
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
View file @
bfe36a4
...
...
@@ -73,6 +73,9 @@
ReferencedContainer =
"container:Runner.xcodeproj"
>
</BuildableReference>
</BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier =
"../Runner/Testing.storekit"
>
</StoreKitConfigurationFileReference>
</LaunchAction>
<ProfileAction
buildConfiguration =
"Profile"
...
...
ios/Runner.xcworkspace/contents.xcworkspacedata
View file @
bfe36a4
...
...
@@ -2,9 +2,6 @@
<Workspace
version =
"1.0"
>
<FileRef
location =
"group:Runner/LocalIAP.storekit"
>
</FileRef>
<FileRef
location =
"group:Runner.xcodeproj"
>
</FileRef>
<FileRef
...
...
ios/Runner/AppDelegate.swift
View file @
bfe36a4
import
UIKit
import
Flutter
import
StoreKit
// Add these two import lines
import
TIMPush
...
...
@@ -21,6 +22,22 @@ import tencent_cloud_chat_push
fatalError
(
"rootViewController is not a FlutterViewController"
)
}
let
channel
=
FlutterMethodChannel
(
name
:
"cn.banxe.appframe/receipt"
,
binaryMessenger
:
controller
.
binaryMessenger
)
channel
.
setMethodCallHandler
{
(
call
,
result
)
in
switch
call
.
method
{
case
"getReceipt"
:
self
.
getReceiptBase64
(
result
:
result
)
case
"refreshReceipt"
:
self
.
refreshReceipt
(
result
:
result
)
default
:
result
(
FlutterMethodNotImplemented
)
}
}
// 注册插件
GeneratedPluginRegistrant
.
register
(
with
:
self
)
...
...
@@ -85,6 +102,24 @@ import tencent_cloud_chat_push
}
private
func
getReceiptBase64
(
result
:
@escaping
FlutterResult
)
{
guard
let
receiptURL
=
Bundle
.
main
.
appStoreReceiptURL
,
FileManager
.
default
.
fileExists
(
atPath
:
receiptURL
.
path
),
let
receiptData
=
try
?
Data
(
contentsOf
:
receiptURL
)
else
{
result
(
FlutterError
(
code
:
"NO_RECEIPT"
,
message
:
"收据不存在,请先刷新"
,
details
:
nil
))
return
}
result
(
receiptData
.
base64EncodedString
())
}
private
func
refreshReceipt
(
result
:
@escaping
FlutterResult
)
{
let
request
=
SKReceiptRefreshRequest
()
request
.
delegate
=
RefreshDelegate
(
result
:
result
)
request
.
start
()
}
}
// 可选:防止冲突的手势识别设置
...
...
@@ -94,3 +129,18 @@ extension AppDelegate: UIGestureRecognizerDelegate {
return
true
}
}
class
RefreshDelegate
:
NSObject
,
SKRequestDelegate
{
let
result
:
FlutterResult
init
(
result
:
@escaping
FlutterResult
)
{
self
.
result
=
result
}
func
requestDidFinish
(
_
request
:
SKRequest
)
{
result
(
true
)
}
func
request
(
_
request
:
SKRequest
,
didFailWithError
error
:
Error
)
{
result
(
FlutterError
(
code
:
"REFRESH_FAILED"
,
message
:
error
.
localizedDescription
,
details
:
nil
))
}
}
\ No newline at end of file
ios/Runner/Testing.storekit
0 → 100644
View file @
bfe36a4
{
"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
}
}
lib/data/repositories/message/xe_create_order_handler.dart
View file @
bfe36a4
import
'dart:async'
;
import
'dart:convert'
;
import
'dart:io'
;
import
'package:appframe/config/constant.dart'
;
import
'package:appframe/config/locator.dart'
;
...
...
@@ -57,9 +58,8 @@ class XeCreateOrderHandler extends MessageHandler {
'ext=
$ext
'
);
// 1. 请求后端获取预支付订单信息
final
deviceType
=
'ios'
;
print
(
"[debug]. handleMessage 1 "
);
final
deviceType
=
Platform
.
isIOS
?
"ios"
:
"andorid"
;
print
(
"[debug]. handleMessage 1 "
+
deviceType
);
if
(
deviceType
==
'andorid'
){
await
_wx_pay_proc_
(
userCode
,
tbxStuId
,
chargeCode
,
deviceType
,
phone
,
stuId
,
ext
,
duration
,
durationType
,
totalFee
,
renew
,
useCoin
);
}
else
if
(
deviceType
==
'ios'
){
...
...
@@ -212,7 +212,7 @@ class XeCreateOrderHandler extends MessageHandler {
// map[nonceStr:qNfHK6a95jjJkwzDkh0h3fhfUVuS0jZ9 paySign: timeStamp:1783497438 wxTradeNo:to1281644408384671744]
var
data
=
result
[
'data'
]
as
Map
<
String
,
dynamic
>?;
if
(
data
==
null
)
{
throw
Exception
(
'
支付参数获取
失败'
);
throw
Exception
(
'
订单生成
失败'
);
}
print
(
"[debug]. _ios_pay_proc_ 13 "
);
...
...
@@ -238,13 +238,27 @@ class XeCreateOrderHandler extends MessageHandler {
// 为了调试,先模拟
var
product
;
for
(
var
p
in
_subService
.
products
)
{
debugPrint
(
'x1=====> 产品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
){
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
=
_subService
.
buySubscription
(
product
,
userCode
,
tbxStuId
,
wxTradeNo
);
if
(
isOk
==
true
){
return
{
"orderUnique"
:
wxTradeNo
,
"orderState"
:
1
};
}
else
{
return
{
"orderUnique"
:
wxTradeNo
,
"orderState"
:
0
};
}
// throw Exception('已取消支付');
product
=
_subService
.
products
[
0
];
_subService
.
buySubscription
(
product
,
userCode
,
tbxStuId
,
wxTradeNo
);
}
...
...
lib/services/subscription_service_ios.dart
View file @
bfe36a4
...
...
@@ -4,6 +4,7 @@ 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'
;
import
'package:appframe/utils/ios_receiptHelper.dart'
;
class
SubscriptionService
{
...
...
@@ -42,7 +43,10 @@ class SubscriptionService {
// 2. 监听交易更新(最重要!务必在调用购买之前设置)
_purchaseUpdatedSubscription
=
_iap
.
purchaseStream
.
listen
(
_onPurchaseUpdate
);
_iap
.
purchaseStream
.
listen
(
_onPurchaseUpdate
,
onError:
(
error
)
=>
print
(
'purchaseStream error:
$error
'
),
);
// 3. 从 App Store 获取产品信息
await
_fetchProducts
();
...
...
@@ -65,9 +69,9 @@ class SubscriptionService {
_products
=
response
.
productDetails
;
debugPrint
(
'获取到
${_products.length}
个产品'
);
for
(
var
p
in
_products
)
{
debugPrint
(
'x1=====> 产品ID:
${p.id}
, 标题:
${p.title}
, 价格:
${p.price}
'
);
}
//
for (var p in _products) {
//
debugPrint('x1=====> 产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}');
//
}
}
...
...
@@ -83,11 +87,6 @@ class SubscriptionService {
// 订阅必须使用 PurchaseParam 并设置购买类型为订阅
final
PurchaseParam
purchaseParam
=
PurchaseParam
(
productDetails:
product
,
// 对于订阅,可在此传入升级/降级信息,例如:
// changeSubscriptionParam: ChangeSubscriptionParam(
// oldPurchaseDetails: oldPurchase,
// prorationMode: ProrationMode.immediateWithTimeProration,
// ),
);
// 发起购买(App Store 会弹出系统支付界面)
...
...
@@ -98,13 +97,16 @@ class SubscriptionService {
// ---------- 交易更新回调 ----------
void
_onPurchaseUpdate
(
List
<
PurchaseDetails
>
purchaseDetailsList
)
{
for
(
final
purchaseDetails
in
purchaseDetailsList
)
{
print
(
'Purchase update:
${purchaseDetails.productID}
-
${purchaseDetails.status}
'
);
// 只处理订阅类型产品
if
(!
_isSubscription
(
purchaseDetails
.
productID
))
continue
;
// 根据交易状态处理
debugPrint
(
"[info] _onPurchaseUpdate status is "
+
purchaseDetails
.
status
.
toString
());
switch
(
purchaseDetails
.
status
)
{
case
PurchaseStatus
.
pending
:
// 等待支付确认(例如需要家长批准),不做处理
break
;
...
...
@@ -117,7 +119,7 @@ class SubscriptionService {
print
(
ctx
);
// 1. 验证收据(必须!)
_verifyReceipt
(
purchaseDetails
,
orderId
??
''
);
_verifyReceipt
(
purchaseDetails
,
orderId
??
''
,
"purchased"
);
// 2. 通知 App Store 交易完成
_iap
.
completePurchase
(
purchaseDetails
);
break
;
...
...
@@ -130,7 +132,7 @@ class SubscriptionService {
case
PurchaseStatus
.
restored
:
// 恢复购买成功
_verifyReceipt
(
purchaseDetails
,
""
);
_verifyReceipt
(
purchaseDetails
,
""
,
"restored"
);
_iap
.
completePurchase
(
purchaseDetails
);
break
;
...
...
@@ -138,21 +140,33 @@ class SubscriptionService {
// 用户取消支付
_iap
.
completePurchase
(
purchaseDetails
);
break
;
default
:
break
;
}
// ✅ 关键:只要不是 pending,都立刻完成交易,清出队列
if
(
purchaseDetails
.
status
!=
PurchaseStatus
.
pending
)
{
_iap
.
completePurchase
(
purchaseDetails
);
}
}
}
// ---------- 服务端验证收据 ----------
Future
<
void
>
_verifyReceipt
(
PurchaseDetails
purchaseDetails
,
String
orderid
)
async
{
if
(
purchaseDetails
.
verificationData
.
serverVerificationData
.
isEmpty
)
{
debugPrint
(
'无收据数据'
);
return
;
}
debugPrint
(
"[info] _verifyReceipt is start "
+
purchaseDetails
.
productID
);
Future
<
void
>
_verifyReceipt
(
PurchaseDetails
purchaseDetails
,
String
orderid
,
String
scence
)
async
{
debugPrint
(
"[info] _verifyReceipt is start scence ["
+
scence
+
"] "
+
purchaseDetails
.
productID
);
// 将收据发送到你的后端进行验证
final
String
receipt
=
purchaseDetails
.
verificationData
.
serverVerificationData
;
final
bool
isValid
=
await
_sendReceiptToBackend
(
receipt
,
orderid
);
//final String receipt = purchaseDetails.verificationData.serverVerificationData;
// 可选:沙盒环境经常需要先刷新一下
await
ReceiptHelper
.
refreshReceipt
();
final
base64Receipt
=
await
ReceiptHelper
.
getBase64Receipt
();
// 把 base64Receipt 作为 receipt-data 发给你的服务端验证
final
bool
isValid
=
await
_sendReceiptToBackend
(
base64Receipt
,
orderid
);
debugPrint
(
"[info] _verifyReceipt is step 1 "
);
if
(
isValid
)
{
// 更新本地订阅状态
...
...
lib/utils/ios_receiptHelper.dart
0 → 100644
View file @
bfe36a4
import
'dart:convert'
;
import
'package:flutter/services.dart'
;
class
ReceiptHelper
{
static
const
_channel
=
MethodChannel
(
'cn.banxe.appframe/receipt'
);
/// 获取 Base64 编码的收据数据,直接可用于 /verifyReceipt
static
Future
<
String
>
getBase64Receipt
()
async
{
try
{
final
String
?
base64
=
await
_channel
.
invokeMethod
(
'getReceipt'
);
if
(
base64
==
null
||
base64
.
isEmpty
)
{
throw
Exception
(
'收据为空,请先刷新收据'
);
}
return
base64
;
}
on
MissingPluginException
{
throw
Exception
(
'原生插件未注册,请检查 iOS 代码'
);
}
}
/// 强制刷新收据(沙盒环境常需要)
static
Future
<
void
>
refreshReceipt
()
async
{
try
{
await
_channel
.
invokeMethod
(
'refreshReceipt'
);
}
catch
(
e
)
{
throw
Exception
(
'刷新收据失败:
$e
'
);
}
}
}
\ No newline at end of file
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