Commit bfe36a4a by ethanlamzs

debug支付

1 parent 45d44847
...@@ -26,7 +26,7 @@ end ...@@ -26,7 +26,7 @@ end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 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 pod 'SDWebImage', :git => 'https://gitee.com/mirrors/SDWebImage.git', :branch => '5.21.3', :modular_headers => true
......
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
DC39D7892EFB988000D795A8 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "zh-Hans"; path = "zh-Hans.lproj/Info.plist"; sourceTree = "<group>"; }; DC39D7892EFB988000D795A8 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "zh-Hans"; path = "zh-Hans.lproj/Info.plist"; sourceTree = "<group>"; };
DC4A6BC3E644F00288FAFDCD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; DC7B80912FF3602D0006BCCE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
DCCFA01F2FF3F74300E9FFD3 /* LocalIAP.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; name = LocalIAP.storekit; path = Runner/LocalIAP.storekit; sourceTree = "<group>"; }; DCEFFEE63000FA7000F7B036 /* Testing.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; name = Testing.storekit; path = Runner/Testing.storekit; sourceTree = "<group>"; };
E94A65B4132ADF0336017236 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; 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 */ /* End PBXFileReference section */
...@@ -119,7 +119,7 @@ ...@@ -119,7 +119,7 @@
97C146E51CF9000F007C117D = { 97C146E51CF9000F007C117D = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DCCFA01F2FF3F74300E9FFD3 /* LocalIAP.storekit */, DCEFFEE63000FA7000F7B036 /* Testing.storekit */,
9740EEB11CF90186004384FC /* Flutter */, 9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
......
...@@ -73,6 +73,9 @@ ...@@ -73,6 +73,9 @@
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier = "../Runner/Testing.storekit">
</StoreKitConfigurationFileReference>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Profile" buildConfiguration = "Profile"
......
...@@ -2,9 +2,6 @@ ...@@ -2,9 +2,6 @@
<Workspace <Workspace
version = "1.0"> version = "1.0">
<FileRef <FileRef
location = "group:Runner/LocalIAP.storekit">
</FileRef>
<FileRef
location = "group:Runner.xcodeproj"> location = "group:Runner.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
......
import UIKit import UIKit
import Flutter import Flutter
import StoreKit
// Add these two import lines // Add these two import lines
import TIMPush import TIMPush
...@@ -21,6 +22,22 @@ import tencent_cloud_chat_push ...@@ -21,6 +22,22 @@ import tencent_cloud_chat_push
fatalError("rootViewController is not a FlutterViewController") 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) GeneratedPluginRegistrant.register(with: self)
...@@ -85,6 +102,24 @@ import tencent_cloud_chat_push ...@@ -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 { ...@@ -94,3 +129,18 @@ extension AppDelegate: UIGestureRecognizerDelegate {
return true 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 \ No newline at end of file
{
"appPolicies" : {
"eula" : "",
"policies" : [
{
"locale" : "en_US",
"policyText" : "",
"policyURL" : ""
}
]
},
"identifier" : "6C86460D",
"nonRenewingSubscriptions" : [
{
"displayPrice" : "49.90",
"familyShareable" : false,
"internalID" : "6785393224",
"localizations" : [
{
"description" : "智学会员1个月有效期",
"displayName" : "智学VIP月卡",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.1m",
"referenceName" : "1个月时长会员",
"type" : "NonRenewingSubscription"
},
{
"displayPrice" : "129",
"familyShareable" : false,
"internalID" : "6788337604",
"localizations" : [
{
"description" : "智学会员服务1个季度的有效期",
"displayName" : "智学VIP季卡",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.3m",
"referenceName" : "1季度会员",
"type" : "NonRenewingSubscription"
},
{
"displayPrice" : "299",
"familyShareable" : false,
"internalID" : "6788342136",
"localizations" : [
{
"description" : "智学VIP会员服务1年有效期",
"displayName" : "智学VIP年卡",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.12m",
"referenceName" : "一年会员",
"type" : "NonRenewingSubscription"
},
{
"displayPrice" : "209",
"familyShareable" : false,
"internalID" : "6788341520",
"localizations" : [
{
"description" : "智学VIP会员服务半年有效期",
"displayName" : "智学VIP半年卡",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.6m",
"referenceName" : "半年会员",
"type" : "NonRenewingSubscription"
},
{
"displayPrice" : "1",
"familyShareable" : false,
"internalID" : "6789528182",
"localizations" : [
{
"description" : "智学会员服务1天有效",
"displayName" : "会员1天",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.0m",
"referenceName" : "测试会员",
"type" : "NonRenewingSubscription"
}
],
"products" : [
],
"settings" : {
"_applicationInternalID" : "6757346215",
"_askToBuyEnabled" : false,
"_billingGracePeriodEnabled" : false,
"_billingIssuesEnabled" : false,
"_developerTeamID" : "42794B2KBU",
"_disableDialogs" : false,
"_failTransactionsEnabled" : false,
"_lastSynchronizedDate" : 805370487.02650404,
"_locale" : "zh_Hans",
"_renewalBillingIssuesEnabled" : false,
"_storefront" : "CHN",
"_storeKitErrors" : [
],
"_timeRate" : 0
},
"subscriptionGroups" : [
],
"version" : {
"major" : 4,
"minor" : 0
}
}
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:appframe/config/constant.dart'; import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart'; import 'package:appframe/config/locator.dart';
...@@ -57,9 +58,8 @@ class XeCreateOrderHandler extends MessageHandler { ...@@ -57,9 +58,8 @@ class XeCreateOrderHandler extends MessageHandler {
'ext=$ext'); 'ext=$ext');
// 1. 请求后端获取预支付订单信息 // 1. 请求后端获取预支付订单信息
final deviceType = 'ios'; final deviceType = Platform.isIOS?"ios":"andorid";
print("[debug]. handleMessage 1 "+deviceType);
print("[debug]. handleMessage 1 ");
if (deviceType == 'andorid'){ if (deviceType == 'andorid'){
await _wx_pay_proc_(userCode, tbxStuId, chargeCode, deviceType, phone, stuId, ext, duration, durationType, totalFee, renew, useCoin); await _wx_pay_proc_(userCode, tbxStuId, chargeCode, deviceType, phone, stuId, ext, duration, durationType, totalFee, renew, useCoin);
}else if (deviceType == 'ios'){ }else if (deviceType == 'ios'){
...@@ -212,7 +212,7 @@ class XeCreateOrderHandler extends MessageHandler { ...@@ -212,7 +212,7 @@ class XeCreateOrderHandler extends MessageHandler {
// map[nonceStr:qNfHK6a95jjJkwzDkh0h3fhfUVuS0jZ9 paySign: timeStamp:1783497438 wxTradeNo:to1281644408384671744] // map[nonceStr:qNfHK6a95jjJkwzDkh0h3fhfUVuS0jZ9 paySign: timeStamp:1783497438 wxTradeNo:to1281644408384671744]
var data = result['data'] as Map<String, dynamic>?; var data = result['data'] as Map<String, dynamic>?;
if (data == null) { if (data == null) {
throw Exception('支付参数获取失败'); throw Exception('订单生成失败');
} }
print("[debug]. _ios_pay_proc_ 13 "); print("[debug]. _ios_pay_proc_ 13 ");
...@@ -238,13 +238,27 @@ class XeCreateOrderHandler extends MessageHandler { ...@@ -238,13 +238,27 @@ class XeCreateOrderHandler extends MessageHandler {
// 为了调试,先模拟 // 为了调试,先模拟
var product; var product;
for (var p in _subService.products) { 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);
} }
......
...@@ -4,6 +4,7 @@ import 'package:in_app_purchase/in_app_purchase.dart'; ...@@ -4,6 +4,7 @@ import 'package:in_app_purchase/in_app_purchase.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';
import 'package:appframe/utils/ios_receiptHelper.dart';
class SubscriptionService { class SubscriptionService {
...@@ -42,7 +43,10 @@ class SubscriptionService { ...@@ -42,7 +43,10 @@ class SubscriptionService {
// 2. 监听交易更新(最重要!务必在调用购买之前设置) // 2. 监听交易更新(最重要!务必在调用购买之前设置)
_purchaseUpdatedSubscription = _purchaseUpdatedSubscription =
_iap.purchaseStream.listen(_onPurchaseUpdate); _iap.purchaseStream.listen(
_onPurchaseUpdate,
onError: (error) => print('purchaseStream error: $error'),
);
// 3. 从 App Store 获取产品信息 // 3. 从 App Store 获取产品信息
await _fetchProducts(); await _fetchProducts();
...@@ -65,9 +69,9 @@ class SubscriptionService { ...@@ -65,9 +69,9 @@ 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}');
} // }
} }
...@@ -83,11 +87,6 @@ class SubscriptionService { ...@@ -83,11 +87,6 @@ class SubscriptionService {
// 订阅必须使用 PurchaseParam 并设置购买类型为订阅 // 订阅必须使用 PurchaseParam 并设置购买类型为订阅
final PurchaseParam purchaseParam = PurchaseParam( final PurchaseParam purchaseParam = PurchaseParam(
productDetails: product, productDetails: product,
// 对于订阅,可在此传入升级/降级信息,例如:
// changeSubscriptionParam: ChangeSubscriptionParam(
// oldPurchaseDetails: oldPurchase,
// prorationMode: ProrationMode.immediateWithTimeProration,
// ),
); );
// 发起购买(App Store 会弹出系统支付界面) // 发起购买(App Store 会弹出系统支付界面)
...@@ -98,13 +97,16 @@ class SubscriptionService { ...@@ -98,13 +97,16 @@ 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}');
// 只处理订阅类型产品 // 只处理订阅类型产品
if (!_isSubscription(purchaseDetails.productID)) continue; if (!_isSubscription(purchaseDetails.productID)) continue;
// 根据交易状态处理 // 根据交易状态处理
debugPrint("[info] _onPurchaseUpdate status is "+ purchaseDetails.status.toString()); debugPrint("[info] _onPurchaseUpdate status is "+ purchaseDetails.status.toString());
switch (purchaseDetails.status) { switch (purchaseDetails.status) {
case PurchaseStatus.pending: case PurchaseStatus.pending:
// 等待支付确认(例如需要家长批准),不做处理 // 等待支付确认(例如需要家长批准),不做处理
break; break;
...@@ -117,7 +119,7 @@ class SubscriptionService { ...@@ -117,7 +119,7 @@ class SubscriptionService {
print(ctx); print(ctx);
// 1. 验证收据(必须!) // 1. 验证收据(必须!)
_verifyReceipt(purchaseDetails,orderId??''); _verifyReceipt(purchaseDetails,orderId??'',"purchased");
// 2. 通知 App Store 交易完成 // 2. 通知 App Store 交易完成
_iap.completePurchase(purchaseDetails); _iap.completePurchase(purchaseDetails);
break; break;
...@@ -130,7 +132,7 @@ class SubscriptionService { ...@@ -130,7 +132,7 @@ class SubscriptionService {
case PurchaseStatus.restored: case PurchaseStatus.restored:
// 恢复购买成功 // 恢复购买成功
_verifyReceipt(purchaseDetails,""); _verifyReceipt(purchaseDetails,"","restored");
_iap.completePurchase(purchaseDetails); _iap.completePurchase(purchaseDetails);
break; break;
...@@ -138,21 +140,33 @@ class SubscriptionService { ...@@ -138,21 +140,33 @@ class SubscriptionService {
// 用户取消支付 // 用户取消支付
_iap.completePurchase(purchaseDetails); _iap.completePurchase(purchaseDetails);
break; break;
default:
break;
}
// ✅ 关键:只要不是 pending,都立刻完成交易,清出队列
if (purchaseDetails.status != PurchaseStatus.pending) {
_iap.completePurchase(purchaseDetails);
} }
} }
} }
// ---------- 服务端验证收据 ---------- // ---------- 服务端验证收据 ----------
Future<void> _verifyReceipt(PurchaseDetails purchaseDetails,String orderid) async { Future<void> _verifyReceipt(PurchaseDetails purchaseDetails,String orderid,String scence) async {
if (purchaseDetails.verificationData.serverVerificationData.isEmpty) {
debugPrint('无收据数据'); debugPrint("[info] _verifyReceipt is start scence ["+scence+"] "+purchaseDetails.productID);
return;
}
debugPrint("[info] _verifyReceipt is start "+purchaseDetails.productID);
// 将收据发送到你的后端进行验证 // 将收据发送到你的后端进行验证
final String receipt = purchaseDetails.verificationData.serverVerificationData; //final String receipt = purchaseDetails.verificationData.serverVerificationData;
final bool isValid = await _sendReceiptToBackend(receipt,orderid);
// 可选:沙盒环境经常需要先刷新一下
await ReceiptHelper.refreshReceipt();
final base64Receipt = await ReceiptHelper.getBase64Receipt();
// 把 base64Receipt 作为 receipt-data 发给你的服务端验证
final bool isValid = await _sendReceiptToBackend(base64Receipt,orderid);
debugPrint("[info] _verifyReceipt is step 1 "); debugPrint("[info] _verifyReceipt is step 1 ");
if (isValid) { if (isValid) {
// 更新本地订阅状态 // 更新本地订阅状态
......
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 \ No newline at end of file
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!