From 1651258eec4170c55b9bcb2c448e5b174c9b2774 Mon Sep 17 00:00:00 2001 From: CodeST <694468528@qq.com> Date: Tue, 16 Dec 2025 13:49:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86storkit2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyBoard.xcodeproj/project.pbxproj | 4 + keyBoard/AppDelegate.m | 2 +- keyBoard/Class/Login/VM/KBLoginVM.m | 2 +- .../Class/Pay/M/IAPVerifyTransactionObj.h | 9 +- .../Class/Pay/M/IAPVerifyTransactionObj.m | 52 +++--- .../Class/Pay/M/keyBoard-Bridging-Header.h | 1 + .../StoreKit2Manager/KBStoreKitBridge.swift | 154 ++++++++++++++++++ keyBoard/Class/Pay/VC/KBJfPay.m | 47 +++--- keyBoard/Class/Pay/VC/KBVipPay.m | 49 +++--- 9 files changed, 245 insertions(+), 75 deletions(-) create mode 100644 keyBoard/Class/Pay/StoreKit2Manager/KBStoreKitBridge.swift diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 3bb09eb..d213eaa 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ 0450AC152EF11E4400B6AF06 /* TransactionHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AC042EF11E4400B6AF06 /* TransactionHistory.swift */; }; 0450AC162EF11E4400B6AF06 /* StoreKitServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450ABFD2EF11E4400B6AF06 /* StoreKitServiceDelegate.swift */; }; 0450AC172EF11E4400B6AF06 /* StoreKitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AC032EF11E4400B6AF06 /* StoreKitState.swift */; }; + 0450AC1B2EF11E4400B6AF06 /* KBStoreKitBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450AC1A2EF11E4400B6AF06 /* KBStoreKitBridge.swift */; }; 0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; }; 0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; }; 0459D1B82EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; }; @@ -297,6 +298,7 @@ 0450AC042EF11E4400B6AF06 /* TransactionHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistory.swift; sourceTree = ""; }; 0450AC062EF11E4400B6AF06 /* StoreKitDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitDelegate.swift; sourceTree = ""; }; 0450AC082EF11E4400B6AF06 /* StoreKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitManager.swift; sourceTree = ""; }; + 0450AC1A2EF11E4400B6AF06 /* KBStoreKitBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KBStoreKitBridge.swift; sourceTree = ""; }; 0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinCenterVC.h; sourceTree = ""; }; 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = ""; }; 0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = ""; }; @@ -802,6 +804,7 @@ 0450AC052EF11E4400B6AF06 /* Models */, 0450AC072EF11E4400B6AF06 /* Protocols */, 0450AC082EF11E4400B6AF06 /* StoreKitManager.swift */, + 0450AC1A2EF11E4400B6AF06 /* KBStoreKitBridge.swift */, ); path = StoreKit2Manager; sourceTree = ""; @@ -1917,6 +1920,7 @@ 0450AC152EF11E4400B6AF06 /* TransactionHistory.swift in Sources */, 0450AC162EF11E4400B6AF06 /* StoreKitServiceDelegate.swift in Sources */, 0450AC172EF11E4400B6AF06 /* StoreKitState.swift in Sources */, + 0450AC1B2EF11E4400B6AF06 /* KBStoreKitBridge.swift in Sources */, 043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */, 049FB20E2EC1CD2800FAB05D /* KBAlert.m in Sources */, 04A9FE162EB873C80020DB6D /* UIViewController+Extension.m in Sources */, diff --git a/keyBoard/AppDelegate.m b/keyBoard/AppDelegate.m index ca1c582..7be9980 100644 --- a/keyBoard/AppDelegate.m +++ b/keyBoard/AppDelegate.m @@ -44,7 +44,7 @@ /// 3:如果当前已经是登录状态,则在启动时初始化一次内购服务。 /// 对于“首次登录”的场景,会在登录成功(见 KBLoginVM)后再进行一次配置。 if ([KBUserSessionManager shared].isLoggedIn) { - [[FGIAPManager shared] setConfigureWith:[IAPVerifyTransactionObj new]]; +// [[FGIAPManager shared] setConfigureWith:[IAPVerifyTransactionObj new]]; } diff --git a/keyBoard/Class/Login/VM/KBLoginVM.m b/keyBoard/Class/Login/VM/KBLoginVM.m index 9beceb3..2dfa45d 100644 --- a/keyBoard/Class/Login/VM/KBLoginVM.m +++ b/keyBoard/Class/Login/VM/KBLoginVM.m @@ -23,7 +23,7 @@ + (void)kb_configureIAPIfNeeded { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - [[FGIAPManager shared] setConfigureWith:[IAPVerifyTransactionObj new]]; +// [[FGIAPManager shared] setConfigureWith:[IAPVerifyTransactionObj new]]; }); } diff --git a/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.h b/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.h index 3ec8cae..81dfbda 100644 --- a/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.h +++ b/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.h @@ -9,10 +9,17 @@ NS_ASSUME_NONNULL_BEGIN -FOUNDATION_EXPORT NSString * const KBIAPDidCompletePurchaseNotification; +FOUNDATION_EXPORT NSNotificationName const KBIAPDidCompletePurchaseNotification; @interface IAPVerifyTransactionObj : NSObject +/// 校验票据(StoreKit 2 入口) +/// - Parameters: +/// - receipt: Base64 编码的票据 +/// - completion: 回调,success 表示验签成功,statusCode 为后端状态码 +- (void)verifyReceipt:(NSString *)receipt + completion:(void (^)(BOOL success, NSString * _Nullable message, NSInteger statusCode))completion; + @end NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m b/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m index 8a15104..20a456e 100644 --- a/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m +++ b/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m @@ -9,7 +9,7 @@ //#import "KBLoginSheetViewController.h" #import "KBBizCode.h" -NSString * const KBIAPDidCompletePurchaseNotification = @"KBIAPDidCompletePurchaseNotification"; +NSNotificationName const KBIAPDidCompletePurchaseNotification = @"KBIAPDidCompletePurchaseNotification"; @interface IAPVerifyTransactionObj () @property (nonatomic, strong) PayVM *payVM; @end @@ -23,6 +23,29 @@ NSString * const KBIAPDidCompletePurchaseNotification = @"KBIAPDidCompletePurcha return self; } +- (void)verifyReceipt:(NSString *)receipt + completion:(void (^)(BOOL success, NSString * _Nullable message, NSInteger statusCode))completion { + if (![receipt isKindOfClass:NSString.class] || receipt.length == 0) { + if (completion) { + completion(NO, KBLocalized(@"Receipt missing"), KBBizCodeReceiptError); + } + return; + } + NSDictionary *params = @{ @"receipt": receipt ?: @"" }; + __weak typeof(self) weakSelf = self; + [self.payVM applePayReqWithParams:params needShow:NO completion:^(NSInteger sta, NSString * _Nullable msg) { + dispatch_async(dispatch_get_main_queue(), ^{ + __strong typeof(weakSelf) self = weakSelf; + (void)self; + BOOL success = (sta == KBBizCodeSuccess); + NSString *tip = msg ?: (success ? KBLocalized(@"Success") : KBLocalized(@"Payment failed")); + if (completion) { + completion(success, tip, sta); + } + }); + }]; +} + #pragma mark - FGIAPVerifyTransaction - (void)pushSuccessTradeReultToServer:(NSString *)receipt @@ -40,34 +63,23 @@ NSString * const KBIAPDidCompletePurchaseNotification = @"KBIAPDidCompletePurcha #endif - - - NSInteger type = 0; -#if DEBUG - type = 0; -#else - type = 1; -#endif - // , @"type": @(type) - NSDictionary *params = @{ @"receipt": receipt ?: @""}; __weak typeof(self) weakSelf = self; [KBHUD showWithStatus:@"请稍等..."]; - [self.payVM applePayReqWithParams:params needShow:NO completion:^(NSInteger sta, NSString * _Nullable msg) { -// [KBHUD dismiss]; -// [KBHUD showInfo:(sta == !KBBizCodeSuccess ? KBLocalized(@"Payment failed") : KBLocalized(@"Payment successful"))]; - if (sta == KBBizCodeSuccess) { + [self verifyReceipt:receipt completion:^(BOOL success, NSString * _Nullable message, NSInteger statusCode) { + __strong typeof(weakSelf) self = weakSelf; + (void)self; + if (success) { [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; [KBHUD showInfo:@"Success"]; [[NSNotificationCenter defaultCenter] postNotificationName:KBIAPDidCompletePurchaseNotification object:nil]; if (handler) handler(KBLocalized(@"Success"), nil); - }else if(sta == KBBizCodeReceiptError){ + } else if (statusCode == KBBizCodeReceiptError) { [KBHUD dismiss]; [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; - }else { - [KBHUD showError:@"Failed"]; - if (handler) handler(KBLocalized(@"Failed"), nil); + } else { + [KBHUD showError:message ?: KBLocalized(@"Failed")]; + if (handler) handler(message ?: KBLocalized(@"Failed"), nil); } - (void)weakSelf; // keep self during block life if needed }]; } diff --git a/keyBoard/Class/Pay/M/keyBoard-Bridging-Header.h b/keyBoard/Class/Pay/M/keyBoard-Bridging-Header.h index 1b2cb5d..4ed9a0e 100644 --- a/keyBoard/Class/Pay/M/keyBoard-Bridging-Header.h +++ b/keyBoard/Class/Pay/M/keyBoard-Bridging-Header.h @@ -2,3 +2,4 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +#import "IAPVerifyTransactionObj.h" diff --git a/keyBoard/Class/Pay/StoreKit2Manager/KBStoreKitBridge.swift b/keyBoard/Class/Pay/StoreKit2Manager/KBStoreKitBridge.swift new file mode 100644 index 0000000..f4379fd --- /dev/null +++ b/keyBoard/Class/Pay/StoreKit2Manager/KBStoreKitBridge.swift @@ -0,0 +1,154 @@ +// +// KBStoreKitBridge.swift +// keyBoard +// +// Created to expose StoreKit2Manager to Objective-C callers. +// + +import Foundation +import StoreKit + +@objcMembers +final class KBStoreKitBridge: NSObject, StoreKitDelegate { + static let shared = KBStoreKitBridge() + + private let manager = StoreKit2Manager.shared + private let receiptVerifier = IAPVerifyTransactionObj() + private var configuredProductIds: Set = [] + private var configuredLifetimeIds: Set = [] + private var hasConfigured = false + + private override init() { + super.init() + } + + // MARK: - Preparation + + func prepare(withProductIds productIds: [String], completion: ((Bool, String?) -> Void)? = nil) { + prepare(withProductIds: productIds, lifetimeIds: [], completion: completion) + } + + func prepare(withProductIds productIds: [String], lifetimeIds: [String], completion: ((Bool, String?) -> Void)? = nil) { + Task { + let success = await self.configureIfNeeded(productIds: productIds, lifetimeIds: lifetimeIds) + await MainActor.run { + completion?(success, success ? nil : "Unable to load products.") + } + } + } + + // MARK: - Purchase + + func purchase(productId: String, completion: @escaping (Bool, String?) -> Void) { + Task { + let ready = await self.configureIfNeeded(productIds: [productId], lifetimeIds: []) + guard ready else { + await MainActor.run { + completion(false, "Unable to load product.") + } + return + } + + do { + try await self.manager.purchase(productId: productId) + let state = self.manager.currentState + switch state { + case .purchaseSuccess(let id) where id == productId: + let receipt = try await Self.fetchReceiptData() + self.verifyReceipt(receipt, completion: completion) + case .purchasePending(let id) where id == productId: + await MainActor.run { + completion(false, "Purchase pending approval.") + } + case .purchaseCancelled(let id) where id == productId: + await MainActor.run { + completion(false, "Purchase cancelled.") + } + case .purchaseFailed(let id, let error) where id == productId: + await MainActor.run { + completion(false, error.localizedDescription) + } + case .error(let error): + await MainActor.run { + completion(false, error.localizedDescription) + } + default: + await MainActor.run { + completion(false, "Purchase failed.") + } + } + } catch { + await MainActor.run { + completion(false, error.localizedDescription) + } + } + } + } + + // MARK: - Private Helpers + + @MainActor + private func configureIfNeeded(productIds: [String], lifetimeIds: [String]) async -> Bool { + let ids = productIds.filter { !$0.isEmpty } + guard !ids.isEmpty else { return false } + + let newProducts = Set(ids) + let newLifetime = Set(lifetimeIds.filter { !$0.isEmpty }) + var needsConfigure = !hasConfigured + + if !newProducts.isSubset(of: configuredProductIds) { + configuredProductIds.formUnion(newProducts) + needsConfigure = true + } + + if !newLifetime.isSubset(of: configuredLifetimeIds) { + configuredLifetimeIds.formUnion(newLifetime) + needsConfigure = true + } + + if needsConfigure { + let config = StoreKitConfig( + productIds: Array(configuredProductIds), + lifetimeIds: Array(configuredLifetimeIds) + ) + manager.configure(with: config, delegate: self) + hasConfigured = true + } + + await manager.refreshProducts() + return true + } + + private func verifyReceipt(_ receipt: String, completion: @escaping (Bool, String?) -> Void) { + receiptVerifier.verifyReceipt(receipt) { success, message, _ in + DispatchQueue.main.async { + if success { + NotificationCenter.default.post( + name: NSNotification.Name(rawValue: NSNotification.Name.KBIAPDidCompletePurchase.rawValue), + object: nil + ) + } + completion(success, message) + } + } + } + + private static func fetchReceiptData() async throws -> String { + if let receipt = try loadReceiptFromBundle() { + return receipt + } + try await AppStore.sync() + if let receipt = try loadReceiptFromBundle() { + return receipt + } + throw StoreKitError.verificationFailed + } + + private static func loadReceiptFromBundle() throws -> String? { + guard let url = Bundle.main.appStoreReceiptURL else { return nil } + guard FileManager.default.fileExists(atPath: url.path) else { return nil } + let data = try Data(contentsOf: url, options: .alwaysMapped) + guard !data.isEmpty else { return nil } + return data.base64EncodedString() + } +} diff --git a/keyBoard/Class/Pay/VC/KBJfPay.m b/keyBoard/Class/Pay/VC/KBJfPay.m index ad923bb..4fcf3c2 100644 --- a/keyBoard/Class/Pay/VC/KBJfPay.m +++ b/keyBoard/Class/Pay/VC/KBJfPay.m @@ -4,13 +4,12 @@ #import "KBJfPay.h" #import "KBJfPayCell.h" -#import "FGIAPProductsFilter.h" -#import "FGIAPManager.h" #import "PayVM.h" #import "KBPayProductModel.h" #import "KBBizCode.h" #import "KBShopVM.h" #import "IAPVerifyTransactionObj.h" +#import "keyBoard-Swift.h" static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; @interface KBJfPay () @@ -40,7 +39,6 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; @property (nonatomic, strong) NSArray *data; // In-App 商品展示数据 @property (nonatomic, assign) NSInteger selectedIndex; // 当前选中项 -@property (nonatomic, strong) FGIAPProductsFilter *filter; @property (nonatomic, strong) PayVM *payVM; @property (nonatomic, strong) KBShopVM *shopVM; @@ -50,7 +48,6 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; - (void)viewDidLoad { [super viewDidLoad]; - self.filter = [[FGIAPProductsFilter alloc] init]; self.payVM = [PayVM new]; self.shopVM = [KBShopVM new]; [[NSNotificationCenter defaultCenter] addObserver:self @@ -272,11 +269,25 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; self.data = products ?: @[]; self.selectedIndex = self.data.count > 0 ? 0 : NSNotFound; [self.collectionView reloadData]; + [self prepareStoreKitWithProducts:self.data]; [self selectItemAtCurrentIndexAnimated:NO]; }); }]; } +- (void)prepareStoreKitWithProducts:(NSArray *)products { + if (![products isKindOfClass:NSArray.class] || products.count == 0) { return; } + NSMutableArray *ids = [NSMutableArray array]; + for (KBPayProductModel *item in products) { + if (![item isKindOfClass:KBPayProductModel.class]) { continue; } + if (item.productId.length) { + [ids addObject:item.productId]; + } + } + if (ids.count == 0) { return; } + [[KBStoreKitBridge shared] prepareWithProductIds:ids completion:nil]; +} + - (KBPayProductModel *)currentSelectedProductItem { if (self.selectedIndex == NSNotFound) { return nil; } if (self.selectedIndex < 0 || self.selectedIndex >= self.data.count) { return nil; } @@ -315,28 +326,12 @@ static NSString * const kKBJfPayCellId = @"kKBJfPayCellId"; } [KBHUD show]; __weak typeof(self) weakSelf = self; - [self.filter requestProductsWith:[NSSet setWithObject:productId] completion:^(NSArray * _Nonnull products) { - dispatch_async(dispatch_get_main_queue(), ^{ - __strong typeof(weakSelf) self = weakSelf; - if (!self) { return; } - SKProduct *match = nil; - for (SKProduct *product in products) { - if ([product.productIdentifier isEqualToString:productId]) { - match = product; - break; - } - } - if (!match) { - [KBHUD dismiss]; - [KBHUD showInfo:KBLocalized(@"Unable to load product information")]; - return; - } - [[FGIAPManager shared].iap buyProduct:match onCompletion:^(NSString * _Nonnull message, FGIAPManagerPurchaseRusult result) { - dispatch_async(dispatch_get_main_queue(), ^{ -// [KBHUD dismiss]; - }); - }]; - }); + [[KBStoreKitBridge shared] purchaseWithProductId:productId completion:^(BOOL success, NSString * _Nullable message) { + __strong typeof(weakSelf) self = weakSelf; + NSString *tip = message.length ? message : (success ? KBLocalized(@"Payment successful") : KBLocalized(@"Payment failed")); + [KBHUD dismiss]; + [KBHUD showInfo:tip]; + (void)self; }]; } diff --git a/keyBoard/Class/Pay/VC/KBVipPay.m b/keyBoard/Class/Pay/VC/KBVipPay.m index 86e69a7..f824daf 100644 --- a/keyBoard/Class/Pay/VC/KBVipPay.m +++ b/keyBoard/Class/Pay/VC/KBVipPay.m @@ -11,9 +11,8 @@ #import "KBVipReviewListCell.h" #import "PayVM.h" #import "KBPayProductModel.h" -#import "FGIAPProductsFilter.h" -#import "FGIAPManager.h" #import "KBBizCode.h" +#import "keyBoard-Swift.h" static NSString * const kKBVipHeaderId = @"kKBVipHeaderId"; static NSString * const kKBVipSubscribeCellId = @"kKBVipSubscribeCellId"; @@ -33,7 +32,6 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; @property (nonatomic, strong) UILabel *agreementLabel; // 协议提示 @property (nonatomic, strong) UIButton *agreementButton; // 《Embership Agreement》 @property (nonatomic, strong) PayVM *payVM; -@property (nonatomic, strong) FGIAPProductsFilter *filter; @end @@ -54,7 +52,6 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; make.height.mas_equalTo(224); }]; self.payVM = [PayVM new]; - self.filter = [[FGIAPProductsFilter alloc] init]; self.plans = @[]; self.selectedIndex = NSNotFound; @@ -120,11 +117,25 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; self.plans = products ?: @[]; self.selectedIndex = self.plans.count > 0 ? 0 : NSNotFound; [self.collectionView reloadData]; + [self prepareStoreKitWithPlans:self.plans]; [self selectCurrentPlanAnimated:NO]; }); }]; } +- (void)prepareStoreKitWithPlans:(NSArray *)plans { + if (![plans isKindOfClass:NSArray.class] || plans.count == 0) { return; } + NSMutableArray *ids = [NSMutableArray array]; + for (KBPayProductModel *plan in plans) { + if (![plan isKindOfClass:KBPayProductModel.class]) { continue; } + if (plan.productId.length) { + [ids addObject:plan.productId]; + } + } + if (ids.count == 0) { return; } + [[KBStoreKitBridge shared] prepareWithProductIds:ids completion:nil]; +} + - (void)selectCurrentPlanAnimated:(BOOL)animated { if (self.selectedIndex == NSNotFound) { return; } if (self.selectedIndex < 0 || self.selectedIndex >= self.plans.count) { return; } @@ -191,28 +202,14 @@ static NSString * const kKBVipReviewListCellId = @"kKBVipReviewListCellId"; } [KBHUD show]; __weak typeof(self) weakSelf = self; - [self.filter requestProductsWith:[NSSet setWithObject:productId] completion:^(NSArray * _Nonnull products) { - dispatch_async(dispatch_get_main_queue(), ^{ - __strong typeof(weakSelf) self = weakSelf; - if (!self) { return; } - SKProduct *match = nil; - for (SKProduct *product in products) { - if ([product.productIdentifier isEqualToString:productId]) { - match = product; - break; - } - } - if (!match) { - [KBHUD dismiss]; - [KBHUD showInfo:KBLocalized(@"Unable to load product information")]; - return; - } - [[FGIAPManager shared].iap buyProduct:match onCompletion:^(NSString * _Nonnull message, FGIAPManagerPurchaseRusult result) { - dispatch_async(dispatch_get_main_queue(), ^{ -// [KBHUD dismiss]; - }); - }]; - }); + [[KBStoreKitBridge shared] purchaseWithProductId:productId completion:^(BOOL success, NSString * _Nullable message) { + __strong typeof(weakSelf) self = weakSelf; + NSString *tip = message.length ? message : (success ? KBLocalized(@"Payment successful") : KBLocalized(@"Payment failed")); + [KBHUD dismiss]; + [KBHUD showInfo:tip]; + if (!self || !success) { return; } + // 刷新 UI 或数据 + [self selectCurrentPlanAnimated:NO]; }]; } - (void)agreementButtonAction{