340 lines
10 KiB
Dart
340 lines
10 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||
import 'package:yumi/app_localizations.dart';
|
||
import 'package:yumi/ui_kit/components/dialog/dialog_base.dart';
|
||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||
import 'package:yumi/modules/wallet/recharge/recharge_page.dart';
|
||
|
||
import '../../shared/data_sources/models/enum/sc_erro_code.dart';
|
||
|
||
class IOSPaymentProcessor extends ChangeNotifier {
|
||
StreamSubscription<List<PurchaseDetails>>? _subscription;
|
||
final InAppPurchase iap = InAppPurchase.instance;
|
||
|
||
// 只复制必要的状态变量
|
||
bool _isAvailable = false;
|
||
String _errorMessage = '';
|
||
List<SelecteProductConfig> _products = [];
|
||
|
||
late BuildContext context;
|
||
Map<String, SCProductConfigRes> productMap = {};
|
||
|
||
// 公开访问器
|
||
bool get isAvailable => _isAvailable;
|
||
|
||
String get errorMessage => _errorMessage;
|
||
|
||
List<SelecteProductConfig> get products => _products;
|
||
|
||
// 初始化 - 只处理iOS逻辑
|
||
Future<void> initializePaymentProcessor(BuildContext context) async {
|
||
this.context = context;
|
||
|
||
// 如果不是iOS,直接返回
|
||
if (!Platform.isIOS) {
|
||
_errorMessage = 'This provider is for iOS only';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
SCLoadingManager.show(context: context);
|
||
// 检查支付是否可用
|
||
_isAvailable = await iap.isAvailable();
|
||
if (!_isAvailable) {
|
||
_errorMessage = 'App Store payment service is unavailable';
|
||
SCTts.show(_errorMessage);
|
||
return;
|
||
}
|
||
_subscription?.cancel();
|
||
// 设置购买流监听
|
||
_subscription = iap.purchaseStream.listen(
|
||
_handlePurchase,
|
||
onError: (error, stackTrace) => _handleError(error, stackTrace),
|
||
);
|
||
|
||
// 获取iOS商品配置
|
||
await _loadIOSProductConfig();
|
||
|
||
// 获取苹果商品信息
|
||
await _fetchIOSProducts();
|
||
} catch (e) {
|
||
SCTts.show("Apple Pay init fail: $e");
|
||
_errorMessage = 'Apple Pay init fail: ${e.toString()}';
|
||
} finally {
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
/// 加载iOS商品配置
|
||
Future _loadIOSProductConfig() async {
|
||
productMap.clear();
|
||
// 假设你的接口支持按平台获取配置
|
||
var productConfigs = await SCConfigRepositoryImp().productConfig();
|
||
|
||
for (var value in productConfigs) {
|
||
productMap[value.productPackage!] = value;
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
// 获取iOS商品信息
|
||
Future<void> _fetchIOSProducts() async {
|
||
try {
|
||
SCLoadingManager.show(context: context);
|
||
notifyListeners();
|
||
|
||
Set<String> productIds = productMap.keys.toSet();
|
||
|
||
ProductDetailsResponse response = await iap.queryProductDetails(
|
||
productIds,
|
||
);
|
||
|
||
if (response.notFoundIDs.isNotEmpty) {
|
||
_errorMessage = '未找到商品: ${response.notFoundIDs.join(', ')}';
|
||
debugPrint(_errorMessage);
|
||
}
|
||
|
||
var details = response.productDetails;
|
||
// 保持你的排序逻辑
|
||
if (details.isNotEmpty) {
|
||
// 这里可以添加iOS特定的排序逻辑
|
||
}
|
||
|
||
_products.clear();
|
||
for (var v in details) {
|
||
_products.add(SelecteProductConfig(v, _products.isEmpty)); // 第一个选中
|
||
}
|
||
} catch (e) {
|
||
SCTts.show("Failed to retrieve iOS products: $e");
|
||
_errorMessage = 'Failed to get iOS goods: ${e.toString()}';
|
||
debugPrint(_errorMessage);
|
||
} finally {
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
// 发起iOS购买
|
||
Future<void> processPurchase() async {
|
||
ProductDetails? product;
|
||
_products.forEach((d) {
|
||
if (d.isSelecte) {
|
||
product = d.produc;
|
||
}
|
||
});
|
||
|
||
if (product == null) {
|
||
SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectaItem);
|
||
return;
|
||
}
|
||
|
||
SmartDialog.show(
|
||
tag: "showConfirmDialog",
|
||
alignment: Alignment.center,
|
||
debounce: true,
|
||
animationType: SmartAnimationType.fade,
|
||
builder: (_) {
|
||
return MsgDialog(
|
||
title: SCAppLocalizations.of(context)!.tips,
|
||
msg: SCAppLocalizations.of(context)!.areYouRureRoRecharge,
|
||
btnText: SCAppLocalizations.of(context)!.confirm,
|
||
onEnsure: () {
|
||
_goIOSBuy(product!);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
// iOS购买流程
|
||
void _goIOSBuy(ProductDetails product) async {
|
||
try {
|
||
SCLoadingManager.show();
|
||
notifyListeners();
|
||
|
||
final PurchaseParam purchaseParam = PurchaseParam(
|
||
productDetails: product,
|
||
applicationUserName: null,
|
||
);
|
||
|
||
// iOS统一使用buyNonConsumable,但实际类型由App Store Connect配置决定
|
||
bool success = await iap.buyConsumable(purchaseParam: purchaseParam);
|
||
|
||
if (!success) {
|
||
_errorMessage =
|
||
'If the purchase fails to start, check your network connection';
|
||
SCTts.show(_errorMessage);
|
||
}
|
||
} catch (e) {
|
||
SCTts.show("iOS Purchase failed: $e");
|
||
_errorMessage = 'iOS purchase failed: ${e.toString()}';
|
||
debugPrint(_errorMessage);
|
||
} finally {
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
// 处理iOS购买结果
|
||
void _handlePurchase(List<PurchaseDetails> purchases) {
|
||
for (var purchase in purchases) {
|
||
switch (purchase.status) {
|
||
case PurchaseStatus.purchased:
|
||
_verifyIOSPayment(purchase);
|
||
break;
|
||
|
||
case PurchaseStatus.error:
|
||
if (purchase.error != null) {
|
||
_handleIOSError(purchase.error!);
|
||
}
|
||
break;
|
||
|
||
case PurchaseStatus.pending:
|
||
debugPrint('iOS支付处理中: ${purchase.productID}');
|
||
SCTts.show('Payment is being processed, please wait...');
|
||
break;
|
||
case PurchaseStatus.restored:
|
||
// SmartDialog.show(
|
||
// tag: "showConfirmDialog",
|
||
// alignment: Alignment.center,
|
||
// animationType: SmartAnimationType.fade,
|
||
// builder: (_) {
|
||
// return MsgDialog(
|
||
// title: SCAppLocalizations.of(context)!.tips,
|
||
// msg: SCAppLocalizations.of(context)!.restorePurchasesTips,
|
||
// btnText: SCAppLocalizations.of(context)!.confirm,
|
||
// onEnsure: () {
|
||
//
|
||
// },
|
||
// );
|
||
// },
|
||
// );
|
||
_verifyIOSPayment(purchase); // 恢复的购买也需要验证
|
||
break;
|
||
case PurchaseStatus.canceled:
|
||
break;
|
||
}
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
// 验证iOS支付
|
||
Future<void> _verifyIOSPayment(PurchaseDetails purchase) async {
|
||
SCLoadingManager.show();
|
||
try {
|
||
String receiptData = purchase.verificationData.serverVerificationData;
|
||
if (receiptData.isEmpty) {
|
||
receiptData = purchase.verificationData.localVerificationData;
|
||
}
|
||
// iOS验证 - 使用服务器收据验证
|
||
try {
|
||
await SCConfigRepositoryImp().applePay(
|
||
purchase.productID,
|
||
receiptData,
|
||
purchase.purchaseID ?? "",
|
||
);
|
||
// 🔥 iOS关键:必须完成交易
|
||
await iap.completePurchase(purchase);
|
||
// 更新用户信息
|
||
Provider.of<SocialChatUserProfileManager>(context, listen: false).fetchUserProfileData();
|
||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||
SCTts.show('Purchase is successful!');
|
||
debugPrint('iOS购买成功: ${purchase.productID}');
|
||
} catch (e) {
|
||
if (e.toString().endsWith("${SCErroCode.orderExistsCreated.code}")) {
|
||
SCTts.show('The order already exists!');
|
||
await iap.completePurchase(purchase);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
SCTts.show("ios verification fails $e");
|
||
_errorMessage = 'iOS verification failed: ${e.toString()}';
|
||
debugPrint(_errorMessage);
|
||
// 即使验证失败,也要完成交易,否则会卡住
|
||
if (purchase.pendingCompletePurchase) {
|
||
await iap.completePurchase(purchase);
|
||
}
|
||
} finally {
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
// 交付商品(与安卓相同)
|
||
Future<void> _deliverProduct(String productId) async {
|
||
debugPrint('交付iOS商品: $productId');
|
||
// 使用相同的业务逻辑
|
||
}
|
||
|
||
// iOS错误处理
|
||
void _handleIOSError(IAPError error) {
|
||
_errorMessage = 'iOS payment error: ${error.message} (${error.code})';
|
||
|
||
switch (error.code) {
|
||
case 'SKErrorPaymentCancelled':
|
||
SCTts.show('the purchase has been canceled');
|
||
break;
|
||
case 'SKErrorPaymentNotAllowed':
|
||
SCTts.show('the device does not support in app purchases');
|
||
break;
|
||
default:
|
||
SCTts.show('ios payment failed: ${error.message}');
|
||
break;
|
||
}
|
||
|
||
debugPrint(_errorMessage);
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
|
||
void _handleError(Object error, StackTrace stackTrace) {
|
||
if (error is IAPError) {
|
||
_handleIOSError(error);
|
||
} else {
|
||
_errorMessage = 'iOS Unknown Error: ${error.toString()}';
|
||
SCTts.show(_errorMessage);
|
||
}
|
||
}
|
||
|
||
// iOS恢复购买
|
||
Future<void> recoverTransactions() async {
|
||
try {
|
||
SCLoadingManager.show(context: context);
|
||
notifyListeners();
|
||
|
||
SCTts.show('recovering ios purchases...');
|
||
await iap.restorePurchases();
|
||
} catch (e) {
|
||
_errorMessage = 'Failed to restore iOS purchases: ${e.toString()}';
|
||
SCTts.show(_errorMessage);
|
||
} finally {
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
void chooseProductConfig(int index) {
|
||
for (var v in _products) {
|
||
v.isSelecte = false;
|
||
}
|
||
_products[index].isSelecte = true;
|
||
notifyListeners();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_subscription?.cancel();
|
||
super.dispose();
|
||
}
|
||
}
|