Commit a53ff2fc by ethanlamzs

ios 购买测试

1 parent b41e0803
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
DC7B80922FF3602D0006BCCE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC7B80912FF3602D0006BCCE /* StoreKit.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
...@@ -70,6 +71,8 @@ ...@@ -70,6 +71,8 @@
DC39D7882EFB987F00D795A8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/Info.plist; sourceTree = "<group>"; }; DC39D7882EFB987F00D795A8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.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>"; }; 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; };
DCCFA01F2FF3F74300E9FFD3 /* LocalIAP.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; name = LocalIAP.storekit; path = Runner/LocalIAP.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 */
...@@ -87,6 +90,7 @@ ...@@ -87,6 +90,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
1600F8F364E182C3635C3546 /* Pods_Runner.framework in Frameworks */, 1600F8F364E182C3635C3546 /* Pods_Runner.framework in Frameworks */,
DC7B80922FF3602D0006BCCE /* StoreKit.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
...@@ -115,6 +119,7 @@ ...@@ -115,6 +119,7 @@
97C146E51CF9000F007C117D = { 97C146E51CF9000F007C117D = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DCCFA01F2FF3F74300E9FFD3 /* LocalIAP.storekit */,
9740EEB11CF90186004384FC /* Flutter */, 9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
...@@ -153,6 +158,7 @@ ...@@ -153,6 +158,7 @@
9F7FE946B3449150A78BC577 /* Frameworks */ = { 9F7FE946B3449150A78BC577 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
DC7B80912FF3602D0006BCCE /* StoreKit.framework */,
B2450CB3B5E968BD7CC513E6 /* Pods_Runner.framework */, B2450CB3B5E968BD7CC513E6 /* Pods_Runner.framework */,
DC4A6BC3E644F00288FAFDCD /* Pods_RunnerTests.framework */, DC4A6BC3E644F00288FAFDCD /* Pods_RunnerTests.framework */,
); );
......
...@@ -73,6 +73,9 @@ ...@@ -73,6 +73,9 @@
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<StoreKitConfigurationFileReference
identifier = "../Runner/LocalIAP.storekit">
</StoreKitConfigurationFileReference>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Profile" buildConfiguration = "Profile"
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
<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
......
{
"appPolicies" : {
"eula" : "",
"policies" : [
{
"locale" : "en_US",
"policyText" : "",
"policyURL" : ""
}
]
},
"identifier" : "8ADF1697",
"nonRenewingSubscriptions" : [
{
"displayPrice" : "59.0",
"familyShareable" : false,
"internalID" : "9F78CD7A",
"localizations" : [
{
"description" : "vipm1",
"displayName" : "vipm1",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.1m",
"referenceName" : "month1",
"type" : "NonRenewingSubscription"
},
{
"displayPrice" : "120",
"familyShareable" : false,
"internalID" : "119703EA",
"localizations" : [
{
"description" : "vipm2",
"displayName" : "vipm2",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.2m",
"referenceName" : "month2",
"type" : "NonRenewingSubscription"
},
{
"displayPrice" : "180",
"familyShareable" : false,
"internalID" : "4768E67B",
"localizations" : [
{
"description" : "vipm3",
"displayName" : "vipm3",
"locale" : "zh_Hans"
}
],
"productID" : "cn.banxe.appframe.membership.3m",
"referenceName" : "month3",
"type" : "NonRenewingSubscription"
}
],
"products" : [
],
"settings" : {
"_askToBuyEnabled" : false,
"_billingGracePeriodEnabled" : false,
"_billingIssuesEnabled" : false,
"_disableDialogs" : false,
"_failTransactionsEnabled" : false,
"_locale" : "en_US",
"_renewalBillingIssuesEnabled" : false,
"_storefront" : "USA",
"_storeKitErrors" : [
],
"_timeRate" : 0
},
"subscriptionGroups" : [
],
"version" : {
"major" : 4,
"minor" : 0
}
}
...@@ -19,6 +19,7 @@ import 'package:appframe/ui/pages/setting_v2/account_apple_page_v2.dart'; ...@@ -19,6 +19,7 @@ import 'package:appframe/ui/pages/setting_v2/account_apple_page_v2.dart';
import 'package:appframe/ui/pages/setting_v2/account_logoff_page_v2.dart'; import 'package:appframe/ui/pages/setting_v2/account_logoff_page_v2.dart';
import 'package:appframe/ui/pages/setting_v2/account_page_v2.dart'; import 'package:appframe/ui/pages/setting_v2/account_page_v2.dart';
import 'package:appframe/ui/pages/setting_v2/account_phone_page_v2.dart'; import 'package:appframe/ui/pages/setting_v2/account_phone_page_v2.dart';
import 'package:appframe/ui/pages/subscription_page.dart';
import 'package:appframe/ui/pages/web_page.dart'; import 'package:appframe/ui/pages/web_page.dart';
import 'package:appframe/ui/widgets/ios_edge_swipe_detector.dart'; import 'package:appframe/ui/widgets/ios_edge_swipe_detector.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
...@@ -130,6 +131,12 @@ final GoRouter router = GoRouter( ...@@ -130,6 +131,12 @@ final GoRouter router = GoRouter(
return const PreviewMediaPage(); return const PreviewMediaPage();
}, },
), ),
GoRoute(
path: '/iosbuytest',
builder: (BuildContext context, GoRouterState state) {
return const SubscriptionPage();
},
),
], ],
); );
......
// subscription_service.dart
import 'dart:async';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/widgets.dart';
class SubscriptionService {
// App Store Connect 中配置的订阅产品 ID(组内唯一)
// 例如月度、年度两种方案
static const String monthlySubId_1 = 'cn.banxe.appframe.membership.1m';
// static const String monthlySubId_2 = 'cn.banxe.appframe.membership.2m';
// static const String monthlySubId_3 = 'cn.banxe.appframe.membership.3m';
final InAppPurchase _iap = InAppPurchase.instance;
late StreamSubscription<List<PurchaseDetails>> _purchaseUpdatedSubscription;
// 可用产品列表
List<ProductDetails> _products = [];
List<ProductDetails> get products => _products;
// 当前有效订阅状态(简化版)
bool _hasActiveSubscription = false;
bool get hasActiveSubscription => _hasActiveSubscription;
// 初始化
Future<void> initialize() async {
// 1. 检查是否可用
final bool available = await _iap.isAvailable();
if (!available) {
throw Exception('In-App Purchase 不可用');
}
// 2. 监听交易更新(最重要!务必在调用购买之前设置)
_purchaseUpdatedSubscription =
_iap.purchaseStream.listen(_onPurchaseUpdate);
// 3. 从 App Store 获取产品信息
await _fetchProducts();
// 4. 处理待处理的交易(应用被杀掉时未完成交易)
await _iap.restorePurchases(); // 订阅恢复也会触发 purchaseStream
}
// ---------- 获取产品信息 ----------
Future<void> _fetchProducts() async {
const Set<String> ids = {monthlySubId_1};
final ProductDetailsResponse response = await _iap.queryProductDetails(ids);
if (response.notFoundIDs.isNotEmpty) {
// 有产品未在 App Store Connect 配置或配置错误
debugPrint('未找到产品: ${response.notFoundIDs}');
}
_products = response.productDetails;
debugPrint('获取到 ${_products.length} 个产品');
for (var p in _products) {
debugPrint('产品ID: ${p.id}, 标题: ${p.title}, 价格: ${p.price}');
}
}
// ---------- 购买订阅 ----------
Future<bool> buySubscription(ProductDetails product) async {
// 订阅必须使用 PurchaseParam 并设置购买类型为订阅
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: product,
// 对于订阅,可在此传入升级/降级信息,例如:
// changeSubscriptionParam: ChangeSubscriptionParam(
// oldPurchaseDetails: oldPurchase,
// prorationMode: ProrationMode.immediateWithTimeProration,
// ),
);
// 发起购买(App Store 会弹出系统支付界面)
await _iap.buyNonConsumable(purchaseParam: purchaseParam);
// 注意:成功或失败会通过 purchaseStream 回调返回
return true; // 实际结果在 _onPurchaseUpdate 处理
}
// ---------- 交易更新回调 ----------
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
for (final purchaseDetails in purchaseDetailsList) {
// 只处理订阅类型产品
if (!_isSubscription(purchaseDetails.productID)) continue;
// 根据交易状态处理
switch (purchaseDetails.status) {
case PurchaseStatus.pending:
// 等待支付确认(例如需要家长批准),不做处理
break;
case PurchaseStatus.purchased:
// 购买成功或续期成功
// 1. 验证收据(必须!)
_verifyReceipt(purchaseDetails);
// 2. 通知 App Store 交易完成
_iap.completePurchase(purchaseDetails);
break;
case PurchaseStatus.error:
// 处理错误
_handlePurchaseError(purchaseDetails.error!);
_iap.completePurchase(purchaseDetails); // 同样需要结束交易
break;
case PurchaseStatus.restored:
// 恢复购买成功
_verifyReceipt(purchaseDetails);
_iap.completePurchase(purchaseDetails);
break;
case PurchaseStatus.canceled:
// 用户取消支付
_iap.completePurchase(purchaseDetails);
break;
}
}
}
// ---------- 服务端验证收据 ----------
Future<void> _verifyReceipt(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.verificationData.serverVerificationData.isEmpty) {
debugPrint('无收据数据');
return;
}
// 将收据发送到你的后端进行验证
final String receipt = purchaseDetails.verificationData.serverVerificationData;
final bool isValid = await _sendReceiptToBackend(receipt);
if (isValid) {
// 更新本地订阅状态
_hasActiveSubscription = true;
// 保存到期时间等信息
}
}
// 示例:发送收据到自己的服务器进行二次验证
Future<bool> _sendReceiptToBackend(String receipt) async {
try {
final response = await http.post(
Uri.parse('https://your-server.com/api/verify_receipt'),
headers: {'Content-Type': 'application/json'},
body: {
'receipt': receipt,
'platform': 'ios',
},
);
if (response.statusCode == 200) {
// 解析你的服务器返回的订阅状态
return true;
}
return false;
} catch (e) {
debugPrint('验证收据失败: $e');
return false;
}
}
// ---------- 恢复购买 ----------
Future<void> restorePurchases() async {
await _iap.restorePurchases();
// 恢复的订阅信息会通过 purchaseStream 以 restored 状态返回
}
// ---------- 辅助方法 ----------
bool _isSubscription(String productId) {
return productId == monthlySubId_1 || productId == monthlySubId_1;
}
void _handlePurchaseError(IAPError error) {
// 根据 error.code 显示不同提示
debugPrint('购买失败: ${error.message}');
}
// 释放资源
void dispose() {
_purchaseUpdatedSubscription.cancel();
}
}
\ No newline at end of file \ No newline at end of file
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:appframe/services/subscription_service_ios.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
// subscription_page.dart
class SubscriptionPage extends StatefulWidget {
const SubscriptionPage({super.key});
@override
State<SubscriptionPage> createState() => _SubscriptionPageState();
}
class _SubscriptionPageState extends State<SubscriptionPage> {
final SubscriptionService _subService = SubscriptionService();
bool _isInitialized = false;
@override
void initState() {
super.initState();
_initService();
}
Future<void> _initService() async {
await _subService.initialize();
setState(() => _isInitialized = true);
}
Future<void> _buy(ProductDetails product) async {
if (!_isInitialized) return;
try {
await _subService.buySubscription(product);
} catch (e) {
// 显示出错信息
}
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) return CircularProgressIndicator();
return ListView.builder(
itemCount: _subService.products.length,
itemBuilder: (context, index) {
final product = _subService.products[index];
return ListTile(
title: Text(product.title),
subtitle: Text(product.description),
trailing: ElevatedButton(
onPressed: () => _buy(product),
child: Text(product.price),
),
);
},
);
}
@override
void dispose() {
_subService.dispose();
super.dispose();
}
}
\ No newline at end of file \ No newline at end of file
...@@ -61,6 +61,8 @@ dependencies: ...@@ -61,6 +61,8 @@ dependencies:
# --- Apple 登录 --- # --- Apple 登录 ---
sign_in_with_apple: ^7.0.1 sign_in_with_apple: ^7.0.1
in_app_purchase: ^3.2.0 # 使用最新稳定版
http: ^1.2.0 # 用于服务端验证
# --- 音视频与直播 (重灾区) --- # --- 音视频与直播 (重灾区) ---
# 确保 ffmpeg_kit 版本与你的架构兼容。 # 确保 ffmpeg_kit 版本与你的架构兼容。
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!