diff --git a/Shared/KBAPI.h b/Shared/KBAPI.h index 65a3463..ffd5ccb 100644 --- a/Shared/KBAPI.h +++ b/Shared/KBAPI.h @@ -16,4 +16,8 @@ #define KB_API_APP_CONFIG @"app/config" // 获取 App 配置 #endif +#ifndef KB_API_IAP_VERIFY +#define KB_API_IAP_VERIFY @"/payment/apple/verify" // Apple IAP 验签(示例路径,请按后端实际修改) +#endif + #endif /* KBAPI_h */ diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 21058f7..034b009 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 04122F5D2EC5E5A900EF7AB3 /* KBLoginVM.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F5B2EC5E5A900EF7AB3 /* KBLoginVM.m */; }; 04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F612EC5F41D00EF7AB3 /* KBUser.m */; }; + 04122F6D2EC5F40800EF7AB3 /* NSObject+FGIsNullOrEmpty.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F6B2EC5F40800EF7AB3 /* NSObject+FGIsNullOrEmpty.m */; }; + 04122F6E2EC5F40800EF7AB3 /* FGIAPProductsFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F652EC5F40800EF7AB3 /* FGIAPProductsFilter.m */; }; + 04122F6F2EC5F40800EF7AB3 /* FGIAPManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F632EC5F40800EF7AB3 /* FGIAPManager.m */; }; + 04122F702EC5F40800EF7AB3 /* FGIAPService.m in Sources */ = {isa = PBXBuildFile; fileRef = 04122F672EC5F40800EF7AB3 /* FGIAPService.m */; }; 043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 04C6EAE12EAF940F0089C901 /* KBPermissionViewController.m */; }; 0459D1B42EBA284C00F2D189 /* KBSkinCenterVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */; }; 0459D1B72EBA287900F2D189 /* KBSkinManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0459D1B62EBA287900F2D189 /* KBSkinManager.m */; }; @@ -183,6 +187,16 @@ 04122F5B2EC5E5A900EF7AB3 /* KBLoginVM.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBLoginVM.m; sourceTree = ""; }; 04122F602EC5F41D00EF7AB3 /* KBUser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBUser.h; sourceTree = ""; }; 04122F612EC5F41D00EF7AB3 /* KBUser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBUser.m; sourceTree = ""; }; + 04122F622EC5F40800EF7AB3 /* FGIAPManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FGIAPManager.h; sourceTree = ""; }; + 04122F632EC5F40800EF7AB3 /* FGIAPManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FGIAPManager.m; sourceTree = ""; }; + 04122F642EC5F40800EF7AB3 /* FGIAPProductsFilter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FGIAPProductsFilter.h; sourceTree = ""; }; + 04122F652EC5F40800EF7AB3 /* FGIAPProductsFilter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FGIAPProductsFilter.m; sourceTree = ""; }; + 04122F662EC5F40800EF7AB3 /* FGIAPService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FGIAPService.h; sourceTree = ""; }; + 04122F672EC5F40800EF7AB3 /* FGIAPService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FGIAPService.m; sourceTree = ""; }; + 04122F682EC5F40800EF7AB3 /* FGIAPServiceUtility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FGIAPServiceUtility.h; sourceTree = ""; }; + 04122F692EC5F40800EF7AB3 /* FGIAPVerifyTransaction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FGIAPVerifyTransaction.h; sourceTree = ""; }; + 04122F6A2EC5F40800EF7AB3 /* NSObject+FGIsNullOrEmpty.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSObject+FGIsNullOrEmpty.h"; sourceTree = ""; }; + 04122F6B2EC5F40800EF7AB3 /* NSObject+FGIsNullOrEmpty.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSObject+FGIsNullOrEmpty.m"; 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 = ""; }; @@ -468,6 +482,54 @@ path = VM; sourceTree = ""; }; + 04122F5E2EC5F3DF00EF7AB3 /* M */ = { + isa = PBXGroup; + children = ( + ); + path = M; + sourceTree = ""; + }; + 04122F5F2EC5F3DF00EF7AB3 /* V */ = { + isa = PBXGroup; + children = ( + ); + path = V; + sourceTree = ""; + }; + 04122F602EC5F3DF00EF7AB3 /* VC */ = { + isa = PBXGroup; + children = ( + ); + path = VC; + sourceTree = ""; + }; + 04122F612EC5F3DF00EF7AB3 /* Pay */ = { + isa = PBXGroup; + children = ( + 04122F5E2EC5F3DF00EF7AB3 /* M */, + 04122F5F2EC5F3DF00EF7AB3 /* V */, + 04122F602EC5F3DF00EF7AB3 /* VC */, + ); + path = Pay; + sourceTree = ""; + }; + 04122F6C2EC5F40800EF7AB3 /* FGIAPService */ = { + isa = PBXGroup; + children = ( + 04122F622EC5F40800EF7AB3 /* FGIAPManager.h */, + 04122F632EC5F40800EF7AB3 /* FGIAPManager.m */, + 04122F642EC5F40800EF7AB3 /* FGIAPProductsFilter.h */, + 04122F652EC5F40800EF7AB3 /* FGIAPProductsFilter.m */, + 04122F662EC5F40800EF7AB3 /* FGIAPService.h */, + 04122F672EC5F40800EF7AB3 /* FGIAPService.m */, + 04122F682EC5F40800EF7AB3 /* FGIAPServiceUtility.h */, + 04122F692EC5F40800EF7AB3 /* FGIAPVerifyTransaction.h */, + 04122F6A2EC5F40800EF7AB3 /* NSObject+FGIsNullOrEmpty.h */, + 04122F6B2EC5F40800EF7AB3 /* NSObject+FGIsNullOrEmpty.m */, + ); + path = FGIAPService; + sourceTree = ""; + }; 0477BD942EBAFF4E0055D639 /* Utils */ = { isa = PBXGroup; children = ( @@ -683,6 +745,7 @@ 048908D32EBF618E00FABA60 /* Vender */ = { isa = PBXGroup; children = ( + 04122F6C2EC5F40800EF7AB3 /* FGIAPService */, 049FB2162EC20A6600FAB05D /* BMLongPressDragCellCollectionView */, 048908D92EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout */, ); @@ -981,6 +1044,7 @@ 04FC95BF2EB1E3B1007BD342 /* Class */ = { isa = PBXGroup; children = ( + 04122F612EC5F3DF00EF7AB3 /* Pay */, 7276DDA22EC1B22500804C36 /* WebView */, 048908D32EBF618E00FABA60 /* Vender */, 048908C02EBE329D00FABA60 /* Search */, @@ -1114,9 +1178,9 @@ 04FC95EA2EB33611007BD342 /* M */ = { isa = PBXGroup; children = ( - 04122F602EC5F41D00EF7AB3 /* KBUser.h */, - 04122F612EC5F41D00EF7AB3 /* KBUser.m */, - ); + 04122F602EC5F41D00EF7AB3 /* KBUser.h */, + 04122F612EC5F41D00EF7AB3 /* KBUser.m */, + ); path = M; sourceTree = ""; }; @@ -1151,6 +1215,33 @@ path = Login; sourceTree = ""; }; + 04122F612EC5F3DF00EF7AB3 /* Pay */ = { + isa = PBXGroup; + children = ( + 04122F642EC5F40600EF7AB3 /* M */, + 04122F652EC5F40600EF7AB3 /* VM */, + ); + path = Pay; + sourceTree = ""; + }; + 04122F642EC5F40600EF7AB3 /* M */ = { + isa = PBXGroup; + children = ( + 04122F7B2EC6123500EF7AB3 /* IAPVerifyTransactionObj.h */, + 04122F7C2EC6123500EF7AB3 /* IAPVerifyTransactionObj.m */, + ); + path = M; + sourceTree = ""; + }; + 04122F652EC5F40600EF7AB3 /* VM */ = { + isa = PBXGroup; + children = ( + 04122F782EC610C500EF7AB3 /* PayVM.h */, + 04122F792EC610C500EF7AB3 /* PayVM.m */, + ); + path = VM; + sourceTree = ""; + }; 04FC95EE2EB3399D007BD342 /* Manager */ = { isa = PBXGroup; children = ( @@ -1477,7 +1568,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */, + 04122F7D2EC6123500EF7AB3 /* IAPVerifyTransactionObj.m in Sources */, + 04122F7A2EC610C500EF7AB3 /* PayVM.m in Sources */, + 04122F622EC5F41D00EF7AB3 /* KBUser.m in Sources */, 049FB31D2EC21BCD00FAB05D /* KBMyKeyboardCell.m in Sources */, 048909F62EC0AAAA00FABA60 /* KBCategoryTitleCell.m in Sources */, 048909F72EC0AAAA00FABA60 /* KBCategoryTitleView.m in Sources */, @@ -1536,6 +1629,10 @@ 04FC95CF2EB1E7A1007BD342 /* HomeVC.m in Sources */, 049FB2112EC1F72F00FAB05D /* KBMyListCell.m in Sources */, A1B2D7022EB8C00100000001 /* KBLangTestVC.m in Sources */, + 04122F6D2EC5F40800EF7AB3 /* NSObject+FGIsNullOrEmpty.m in Sources */, + 04122F6E2EC5F40800EF7AB3 /* FGIAPProductsFilter.m in Sources */, + 04122F6F2EC5F40800EF7AB3 /* FGIAPManager.m in Sources */, + 04122F702EC5F40800EF7AB3 /* FGIAPService.m in Sources */, 048908DA2EBF61AF00FABA60 /* UICollectionViewLeftAlignedLayout.m in Sources */, 04C6EABF2EAF86530089C901 /* main.m in Sources */, 04FC95CC2EB1E780007BD342 /* BaseTabBarController.m in Sources */, diff --git a/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.h b/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.h new file mode 100644 index 0000000..0fa4327 --- /dev/null +++ b/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.h @@ -0,0 +1,17 @@ +// +// IAPVerifyTransactionObj.h +// 将 Swift 内购验签逻辑迁移到 Objective-C +// + +#import +#import +#import "FGIAPVerifyTransaction.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface IAPVerifyTransactionObj : NSObject + +@end + +NS_ASSUME_NONNULL_END + diff --git a/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m b/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m new file mode 100644 index 0000000..291252a --- /dev/null +++ b/keyBoard/Class/Pay/M/IAPVerifyTransactionObj.m @@ -0,0 +1,71 @@ +// +// IAPVerifyTransactionObj.m +// + +#import "IAPVerifyTransactionObj.h" +#import "PayVM.h" +#import "KBAuthManager.h" +#import "KBHUD.h" +#import "KBLoginSheetViewController.h" + +@interface IAPVerifyTransactionObj () +@property (nonatomic, strong) PayVM *payVM; +@end + +@implementation IAPVerifyTransactionObj + +- (instancetype)init { + if (self = [super init]) { + _payVM = [PayVM new]; + } + return self; +} + +#pragma mark - FGIAPVerifyTransaction + +- (void)pushSuccessTradeReultToServer:(NSString *)receipt + transaction:(SKPaymentTransaction *)transaction + complete:(FGIAPVerifyTransactionPushCallBack)handler { + if (![self checkLogin]) { return; } + + NSLog(@"receipt = %@", receipt); + + NSInteger type = 0; +#if DEBUG + type = 0; +#else + type = 1; +#endif + + NSDictionary *params = @{ @"payment": @{ @"receipt": receipt ?: @"", @"type": @(type) } }; + + __weak typeof(self) weakSelf = self; + [self.payVM applePayReqWithParams:params needShow:NO completion:^(NSInteger sta, NSString * _Nullable msg) { + [KBHUD dismiss]; + [KBHUD showInfo:(sta == KB_PAY_ERROR_CODE ? @"支付失败" : @"支付成功")]; + if (sta == KB_PAY_SUCC_CODE) { + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + if (handler) handler(@"成功", nil); + } else { + if (handler) handler(@"失败", nil); + } + (void)weakSelf; // keep self during block life if needed + }]; +} + +#pragma mark - Helpers + +- (BOOL)checkLogin { + BOOL loggedIn = [[KBAuthManager shared] isLoggedIn]; + if (!loggedIn) { + dispatch_async(dispatch_get_main_queue(), ^{ + UIViewController *top = [UIViewController kb_topMostViewController]; + if (top) { [KBLoginSheetViewController presentIfNeededFrom:top]; } + }); + return NO; + } + return YES; +} + +@end + diff --git a/keyBoard/Class/Pay/VM/PayVM.h b/keyBoard/Class/Pay/VM/PayVM.h new file mode 100644 index 0000000..2046b78 --- /dev/null +++ b/keyBoard/Class/Pay/VM/PayVM.h @@ -0,0 +1,33 @@ +// +// PayVM.h +// 支付相关 VM:封装 Apple IAP 验签请求 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 统一的支付回调:sta 为状态码(0 成功,非 0 失败),msg 为后端返回的消息 +typedef void(^KBPayCompletion)(NSInteger sta, NSString * _Nullable msg); + +/// 统一状态码(与原 Swift 代码的 succCode / errorCode 语义一致) +#ifndef KB_PAY_SUCC_CODE +#define KB_PAY_SUCC_CODE 0 +#endif +#ifndef KB_PAY_ERROR_CODE +#define KB_PAY_ERROR_CODE 1 +#endif + +@interface PayVM : NSObject + +/// Apple 内购验签 +/// params 形如:@{ @"payment": @{ @"receipt": receipt, @"type": @(type) } } +/// needShow:是否显示加载 HUD +- (void)applePayReqWithParams:(NSDictionary *)params + needShow:(BOOL)needShow + completion:(KBPayCompletion)completion; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/keyBoard/Class/Pay/VM/PayVM.m b/keyBoard/Class/Pay/VM/PayVM.m new file mode 100644 index 0000000..7e386d2 --- /dev/null +++ b/keyBoard/Class/Pay/VM/PayVM.m @@ -0,0 +1,65 @@ +// +// PayVM.m +// + +#import "PayVM.h" +#import "KBNetworkManager.h" +#import "KBAPI.h" +#import "KBHUD.h" + +@implementation PayVM + +- (void)applePayReqWithParams:(NSDictionary *)params + needShow:(BOOL)needShow + completion:(KBPayCompletion)completion { + if (needShow) { [KBHUD show]; } + + [[KBNetworkManager shared] POST:KB_API_IAP_VERIFY jsonBody:params headers:nil completion:^(id _Nullable jsonOrData, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if (needShow) { [KBHUD dismiss]; } + + if (error) { + if (completion) completion(KB_PAY_ERROR_CODE, error.localizedDescription ?: @"网络错误"); + return; + } + + NSInteger sta = [self.class extractStatusFromResponseObject:jsonOrData response:response]; + NSString *msg = [self.class extractMessageFromResponseObject:jsonOrData] ?: (sta == KB_PAY_SUCC_CODE ? @"OK" : @"失败"); + if (completion) completion(sta, msg); + }]; +} + +#pragma mark - Helpers + ++ (NSInteger)extractStatusFromResponseObject:(id)obj response:(NSURLResponse *)resp { + // 优先从 JSON 提取 code/status/success + if ([obj isKindOfClass:NSDictionary.class]) { + NSDictionary *d = (NSDictionary *)obj; + id code = d[@"code"] ?: d[@"status"] ?: d[@"retcode"]; + if ([code isKindOfClass:NSNumber.class]) { + return [((NSNumber *)code) integerValue] == 0 ? KB_PAY_SUCC_CODE : KB_PAY_ERROR_CODE; + } + if ([code isKindOfClass:NSString.class]) { + // 常见:"0" 视为成功 + return [((NSString *)code) integerValue] == 0 ? KB_PAY_SUCC_CODE : KB_PAY_ERROR_CODE; + } + id success = d[@"success"] ?: d[@"ok"]; + if ([success respondsToSelector:@selector(boolValue)]) { + return [success boolValue] ? KB_PAY_SUCC_CODE : KB_PAY_ERROR_CODE; + } + } + // 无明显字段,按 HTTP 2xx 视为成功 + NSInteger http = 0; + if ([resp isKindOfClass:NSHTTPURLResponse.class]) { http = ((NSHTTPURLResponse *)resp).statusCode; } + return (http >= 200 && http < 300) ? KB_PAY_SUCC_CODE : KB_PAY_ERROR_CODE; +} + ++ (NSString *)extractMessageFromResponseObject:(id)obj { + if (![obj isKindOfClass:NSDictionary.class]) return nil; + NSDictionary *d = (NSDictionary *)obj; + NSString *msg = d[@"message"] ?: d[@"msg"] ?: d[@"error"]; + if (![msg isKindOfClass:NSString.class]) msg = nil; + return msg; +} + +@end + diff --git a/keyBoard/Class/Shop/VC/KBShopVC.m b/keyBoard/Class/Shop/VC/KBShopVC.m index 5a722fc..ef96bdc 100644 --- a/keyBoard/Class/Shop/VC/KBShopVC.m +++ b/keyBoard/Class/Shop/VC/KBShopVC.m @@ -226,6 +226,7 @@ static const CGFloat JXheightForHeaderInSection = 50; // 不要渐变:小于阈值 alpha=0,达到阈值(吸顶)后 alpha=1 BOOL shouldWhite = (thresholdDistance > 0.0 && scrollView.contentOffset.y >= (thresholdDistance - 0.5)); self.naviBGView.alpha = shouldWhite ? 1.0 : 0.0; + self.userHeaderView.alpha = shouldWhite ? 0 : 1.0; // 分类条背景:未吸顶时透明,吸顶后白色 if (shouldWhite != self.categoryIsWhite) { @@ -234,6 +235,7 @@ static const CGFloat JXheightForHeaderInSection = 50; self.categoryView.backgroundColor = bg; // 内部 collectionView 也同步,避免出现条纹底色 self.categoryView.collectionView.backgroundColor = bg; + self.naviBGView.backgroundColor = bg; } } diff --git a/keyBoard/Class/Vender/FGIAPService/FGIAPManager.h b/keyBoard/Class/Vender/FGIAPService/FGIAPManager.h new file mode 100644 index 0000000..cbe74f2 --- /dev/null +++ b/keyBoard/Class/Vender/FGIAPService/FGIAPManager.h @@ -0,0 +1,27 @@ +// +// FGIAPManager.h +// MaltBaby +// +// Created by FoneG on 2020/5/11. +// + +#import +#import "FGIAPService.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FGIAPManager : NSObject + +/// FGIAPService +@property (nonatomic, strong, readonly) FGIAPService *iap; + +/// Initialize ++ (FGIAPManager *)shared; + +/// Configure a server validation object +/// @param verifyTransaction a server validation object +- (void)setConfigureWith:(id)verifyTransaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Vender/FGIAPService/FGIAPManager.m b/keyBoard/Class/Vender/FGIAPService/FGIAPManager.m new file mode 100644 index 0000000..cbdcada --- /dev/null +++ b/keyBoard/Class/Vender/FGIAPService/FGIAPManager.m @@ -0,0 +1,26 @@ +// +// FGIAPManager.m +// MaltBaby +// +// Created by FoneG on 2020/5/11. +// + +#import "FGIAPManager.h" + +@implementation FGIAPManager + ++ (FGIAPManager *)shared{ + static FGIAPManager *manager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + manager = [[FGIAPManager alloc] init]; + }); + return manager; +} + +- (void)setConfigureWith:(id)verifyTransaction{ + self->_iap = [[FGIAPService alloc] initWithTransaction:verifyTransaction]; +} + + +@end diff --git a/keyBoard/Class/Vender/FGIAPService/FGIAPProductsFilter.h b/keyBoard/Class/Vender/FGIAPService/FGIAPProductsFilter.h new file mode 100644 index 0000000..c45ca5b --- /dev/null +++ b/keyBoard/Class/Vender/FGIAPService/FGIAPProductsFilter.h @@ -0,0 +1,24 @@ +// +// FGIAPProductsFilter.h +// MaltBaby +// +// Created by FoneG on 2021/5/8. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void(^FGIAPManagerResponseBlock)(NSArray * products); + +@interface FGIAPProductsFilter : NSObject + +/// 获取苹果内购商品列表 +/// @param productIdentifiers 商品id列表 +/// @param completion completion +- (void)requestProductsWith:(NSSet *)productIdentifiers completion:(nonnull FGIAPManagerResponseBlock)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Vender/FGIAPService/FGIAPProductsFilter.m b/keyBoard/Class/Vender/FGIAPService/FGIAPProductsFilter.m new file mode 100644 index 0000000..a529e19 --- /dev/null +++ b/keyBoard/Class/Vender/FGIAPService/FGIAPProductsFilter.m @@ -0,0 +1,41 @@ +// +// FGIAPProductsFilter.m +// MaltBaby +// +// Created by FoneG on 2021/5/8. +// + +#import "FGIAPProductsFilter.h" +#import "FGIAPServiceUtility.h" + +@interface FGIAPProductsFilter () +@property (nonatomic,strong) SKProductsRequest *request; +@property (nonatomic, copy) FGIAPManagerResponseBlock requestProductsBlock; +@end + +@implementation FGIAPProductsFilter + +- (void)requestProductsWith:(NSSet *)productIdentifiers completion:(nonnull FGIAPManagerResponseBlock)completion{ + + if (productIdentifiers.count==0) { + if (completion) completion([NSArray array]); + return; + } + self.request = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers]; + self.request.delegate = self; + self.requestProductsBlock = completion; + [self.request start]; +} + +#pragma mark - SKProductsRequestDelegate + +- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { + NSArray *products = [response.products sortedArrayUsingComparator:^NSComparisonResult(SKProduct *obj1, SKProduct *obj2) { + return obj1.price.doubleValue < obj2.price.doubleValue ? NSOrderedAscending : NSOrderedDescending; + }]; + if(_requestProductsBlock) { + _requestProductsBlock(products); + } +} + +@end diff --git a/keyBoard/Class/Vender/FGIAPService/FGIAPService.h b/keyBoard/Class/Vender/FGIAPService/FGIAPService.h new file mode 100644 index 0000000..ce99cc1 --- /dev/null +++ b/keyBoard/Class/Vender/FGIAPService/FGIAPService.h @@ -0,0 +1,46 @@ +// +// FGIAPManager.h +// MaltBaby +// +// Created by FoneG on 2020/5/7. +// + +#import +#import +#import "FGIAPVerifyTransaction.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, FGIAPManagerPurchaseRusult) { + FGIAPManagerPurchaseRusultSuccess = 0, //内购成功 + FGIAPManagerPurchaseRusultHalfSuccess, //苹果扣款成功,但是验签接口失败了 + FGIAPManagerPurchaseRusultFail, //内购失败 + FGIAPManagerPurchaseRusultCancel //用户取消 +}; +typedef void(^FGIAPManagerBuyBlock)(NSString *message, FGIAPManagerPurchaseRusult result); + +@interface FGIAPService : NSObject + +/** + * 初始化支付对象 + * @param verifyTransaction 一般得到苹果服务器返回的支付结果后,需要通过再次向服务器进行二次确认,来保证整个支付链路闭环 + */ +- (instancetype)initWithTransaction:(id)verifyTransaction; + +/** + * iap支付 + * @param product 对应的商品 + * @param completion 支付回调 + */ +- (void)buyProduct:(SKProduct *)product onCompletion:(FGIAPManagerBuyBlock)completion; + + +/** + * 在合适的时间处理从App Store下载页面触发的内购行为 + */ +- (void)tryShouldAddStorePayments; + +@end + + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Vender/FGIAPService/FGIAPService.m b/keyBoard/Class/Vender/FGIAPService/FGIAPService.m new file mode 100644 index 0000000..fdefab5 --- /dev/null +++ b/keyBoard/Class/Vender/FGIAPService/FGIAPService.m @@ -0,0 +1,235 @@ +// +// FGIAPManager.m +// MaltBaby +// +// Created by FoneG on 2020/5/7. +// + +#import "FGIAPService.h" +#import "FGIAPServiceUtility.h" +#import "NSObject+FGIsNullOrEmpty.h" + +static NSMutableDictionary *FGIAPServiceErrorMapsFromTransaction (SKPaymentTransaction *transaction) { + NSMutableDictionary *errorMaps = [NSMutableDictionary dictionary]; + [errorMaps setValue:transaction.transactionIdentifier?:@"" forKey:@"transactionIdentifier"]; + [errorMaps setValue:transaction.originalTransaction.transactionIdentifier?:@"" forKey:@"originalTransaction.transactionIdentifier"]; + [errorMaps setValue:transaction.payment.applicationUsername?:@"" forKey:@"applicationUsername"]; + [errorMaps setValue:transaction.payment.productIdentifier?:@"" forKey:@"productIdentifier"]; + return errorMaps; +} + +@interface FGIAPService () +@property (nonatomic, strong) id verifyTransaction; +@property (nonatomic, copy) FGIAPManagerBuyBlock buyProductCompleteBlock; +@property (nonatomic, strong) NSString *productIdentifier; +@property (nonatomic, strong) SKPayment *APPStorePayment; +@end + +@implementation FGIAPService + +- (instancetype)initWithTransaction:(id)verifyTransaction{ + if (self = [super init]) { + + _verifyTransaction = verifyTransaction; + + if ([SKPaymentQueue defaultQueue]) { + [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; + } + } + return self; +} + + +- (void)buyProduct:(SKProduct *)product onCompletion:(nonnull FGIAPManagerBuyBlock)completion{ + + if (![SKPaymentQueue canMakePayments]) { + completion(@"Failed to obtain the internal purchase permission", FGIAPManagerPurchaseRusultFail); + return; + } + if ([product.productIdentifier isNSStringAndNotEmpty]) { + _productIdentifier = product.productIdentifier; + _buyProductCompleteBlock = completion; + SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; + if ([SKPaymentQueue defaultQueue]) { + [[SKPaymentQueue defaultQueue] addPayment:payment]; + } + }else{ + completion(@"The selected payment does not exist", FGIAPManagerPurchaseRusultFail); + } +} + +- (void)tryShouldAddStorePayments{ + if (_APPStorePayment && [SKPaymentQueue defaultQueue]) { + [[SKPaymentQueue defaultQueue] addPayment:self.APPStorePayment]; + } +} + +#pragma mark - SKPaymentTransactionObserver + +- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions{ + FGLog(@"%s %ld", __func__, [SKPaymentQueue defaultQueue].transactions.count); + if (![[SKPaymentQueue defaultQueue].transactions isNSArrayAndNotEmpty]) { + /// all transactions finished + } +} + +- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions +{ + for (SKPaymentTransaction *transaction in transactions) + { + switch (transaction.transactionState) + { + case SKPaymentTransactionStatePurchasing: + FGLog(@"Add an payment to the list"); + break; + case SKPaymentTransactionStatePurchased: + FGLog(@"Customer paid successfully"); + [self completeTransaction:transaction retryWhenreceiptURLisEmpty:YES]; + break; + case SKPaymentTransactionStateRestored: + FGLog(@"The transaction has been recovered from the user's purchase history"); + [self completeTransaction:transaction retryWhenreceiptURLisEmpty:YES]; + break; + case SKPaymentTransactionStateFailed: + FGLog(@"Failure of commodity transaction"); + [self failedTransaction:transaction withError:FGIAPServiceErrorTypeNone]; + break; + case SKPaymentTransactionStateDeferred: + FGLog(@"Merchandise is suspended"); + break; + default: + /// + break; + } + } +} + +- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product API_AVAILABLE(ios(11.0)){ + BOOL shouldAddStorePayment = NO; + if (_verifyTransaction && [_verifyTransaction respondsToSelector:@selector(paymentQueue:shouldAddStorePayment:forProduct:)]) { + shouldAddStorePayment = [_verifyTransaction paymentQueue:queue shouldAddStorePayment:payment forProduct:product]; + } + if (shouldAddStorePayment == NO) { + _APPStorePayment = payment; + } + return shouldAddStorePayment; +} + + +#pragma mark - SKRequestDelegate + +- (void)requestDidFinish:(SKRequest *)request{ + if ([request isKindOfClass:[SKReceiptRefreshRequest class]]) { + SKReceiptRefreshRequest *RefreshRequest = (SKReceiptRefreshRequest *)request; + SKPaymentTransaction *transaction = [RefreshRequest.receiptProperties valueForKey:@"transaction"]; + if (transaction) { + [self completeTransaction:transaction retryWhenreceiptURLisEmpty:NO]; + }else{ + [self failedTransaction:transaction withError:FGIAPServiceErrorTypeReceiptNotExist]; + } + } +} + +- (void)request:(SKRequest *)request didFailWithError:(nonnull NSError *)error{ + if ([request isKindOfClass:[SKReceiptRefreshRequest class]]) { + SKReceiptRefreshRequest *RefreshRequest = (SKReceiptRefreshRequest *)request; + SKPaymentTransaction *transaction = [RefreshRequest.receiptProperties valueForKey:@"transaction"]; + [self failedTransaction:transaction withError:FGIAPServiceErrorTypeReceiptNotExist]; + } +} + + +#pragma mark - private method + +- (void)completeTransaction:(SKPaymentTransaction *)transaction retryWhenreceiptURLisEmpty:(BOOL)retry{ + FGLog(@"%s %@ %@", __func__, transaction.transactionIdentifier, transaction.originalTransaction.transactionIdentifier); + + if (![transaction.transactionIdentifier isNSStringAndNotEmpty]) { + [self failedTransaction:transaction withError:FGIAPServiceErrorTypeTransactionIdentifierNotExist]; + return; + } + + NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + if ([[NSFileManager defaultManager] fileExistsAtPath:receiptURL.path]) { + + NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL]; + NSString *receiptDataText = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; + [self checkReceipt:receiptDataText withTransaction:transaction handler:nil]; + + }else if (retry){ + + SKReceiptRefreshRequest *receiptRefreshRequest = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:@{@"transaction":transaction}]; + receiptRefreshRequest.delegate = self; + [receiptRefreshRequest start]; + + }else{ + [self failedTransaction:transaction withError:FGIAPServiceErrorTypeReceiptNotExist]; + } +} + + +- (void)failedTransaction:(SKPaymentTransaction *)transaction withError:(FGIAPServiceErrorType)error{ + FGLog(@"%s Transaction error:%@ code:%ld", __func__, transaction.error.localizedDescription, transaction.error.code); + + BOOL finish = error == FGIAPServiceErrorTypeNone; + + if (finish && [SKPaymentQueue defaultQueue]) { + [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; + } + + NSMutableDictionary *logStatistics = [NSMutableDictionary dictionaryWithDictionary:FGIAPServiceErrorMapsFromTransaction(transaction)]; + if (self.verifyTransaction && [self.verifyTransaction respondsToSelector:@selector(pushServiceErrorLogStatistics:error:)]) { + [logStatistics setValue:@(error) forKey:@"error"]; + [self.verifyTransaction pushServiceErrorLogStatistics:logStatistics error:error]; + } + + if (_buyProductCompleteBlock) { + FGIAPManagerPurchaseRusult result = FGIAPManagerPurchaseRusultHalfSuccess; + if (error == FGIAPServiceErrorTypeNone) { + result = transaction.error.code == SKErrorPaymentCancelled ? FGIAPManagerPurchaseRusultCancel : FGIAPManagerPurchaseRusultFail; + } + _buyProductCompleteBlock(transaction.error.localizedDescription, result); + } +} + + + +- (void)checkReceipt:(NSString *)receipt withTransaction:(SKPaymentTransaction *)transaction handler:(FGIAPVerifyTransactionBlock)handler{ + + WS(wSelf); + if (_verifyTransaction && [_verifyTransaction respondsToSelector:@selector(pushSuccessTradeReultToServer:transaction:complete:)]) { + [_verifyTransaction pushSuccessTradeReultToServer:receipt transaction:transaction complete:^(NSString * _Nonnull message, NSError * _Nullable requestErr) { + + //polling verify transaction + if (requestErr && requestErr.code != FGIAPServerOverdueErrorCode) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [wSelf checkReceipt:receipt withTransaction:transaction handler:handler]; + }); + return; + } + + [wSelf finishTransaction:transaction result: FGIAPManagerPurchaseRusultSuccess message:message]; + }]; + }else{ + NSAssert(NO, @"You must configure the method: - pushSuccessTradeReultToServer:transaction:complete:"); + } +} + + +- (void)finishTransaction:(SKPaymentTransaction *)transaction result:(FGIAPManagerPurchaseRusult)result message:(NSString *)msg{ + + FGLog(@"%s finishTransaction:%@", __func__, transaction.transactionIdentifier); + + if ([SKPaymentQueue defaultQueue]) { + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + } + + if ([transaction.payment.productIdentifier isEqualToString:self.productIdentifier]) { + self.productIdentifier = nil; + if (_buyProductCompleteBlock) { + _buyProductCompleteBlock(msg, result); + } + } +} + +@end diff --git a/keyBoard/Class/Vender/FGIAPService/FGIAPServiceUtility.h b/keyBoard/Class/Vender/FGIAPService/FGIAPServiceUtility.h new file mode 100644 index 0000000..4e2b509 --- /dev/null +++ b/keyBoard/Class/Vender/FGIAPService/FGIAPServiceUtility.h @@ -0,0 +1,22 @@ +// +// FGIAPServiceUtility.h +// Pods +// +// Created by FoneG on 2021/5/10. +// + +#ifndef FGIAPServiceUtility_h +#define FGIAPServiceUtility_h + +#define FGDEBUG + +#if defined (FGDEBUG) && defined (DEBUG) + #define FGLog(...) NSLog(__VA_ARGS__) +#else + #define FGLog(...) +#endif + +#define WS(wSelf) __weak typeof(self) wSelf = self +#define FGIAPServerOverdueErrorCode 11000007 //预留code:订单提交无效,需要删除本地的订单 + +#endif /* FGIAPServiceUtility_h */ diff --git a/keyBoard/Class/Vender/FGIAPService/FGIAPVerifyTransaction.h b/keyBoard/Class/Vender/FGIAPService/FGIAPVerifyTransaction.h new file mode 100644 index 0000000..34d3e55 --- /dev/null +++ b/keyBoard/Class/Vender/FGIAPService/FGIAPVerifyTransaction.h @@ -0,0 +1,58 @@ +// +// FGIAPVerifyTransaction.h +// MaltBaby +// +// Created by FoneG on 2021/5/8. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, FGIAPVerifyTransactionRusult) { + FGIAPManagerVerifyRusultCREATED = 0, + FGIAPManagerVerifyRusultFail, + FGIAPManagerVerifyRusultSuccess, +}; + +typedef NS_ENUM(NSInteger, FGIAPServiceErrorType) { + FGIAPServiceErrorTypeNone, + ///Failed to validate receipt + FGIAPServiceErrorTypeTransactionIdentifierNotExist, + ///No matching receipt data was found + FGIAPServiceErrorTypeReceiptNotExist, + ///Failed to validate receipt + FGIAPServiceErrorTypeVerifyTradeFail, +}; + +typedef void(^FGIAPVerifyTransactionBlock)(NSString *message, FGIAPVerifyTransactionRusult result); +typedef void(^FGIAPVerifyTransactionPushCallBack)(NSString *message, NSError * _Nullable result); + +@protocol FGIAPVerifyTransaction + +/// 苹果支付流程结束后,需要根据返回的票据等数据去自己的服务器校验 +/// @param receipt 票据 +/// @param transaction 支付事务 +/// @param handler 检验回调 +- (void)pushSuccessTradeReultToServer:(NSString *)receipt transaction:(SKPaymentTransaction *)transaction complete:(FGIAPVerifyTransactionPushCallBack)handler; + + +@optional + +/// 推送失败日志 +/// @param logStatistics 日志 +- (void)pushServiceErrorLogStatistics:(NSDictionary *)logStatistics error:(FGIAPServiceErrorType)error; + +/** +Promoting In-App Purchases + + 用户如果在 App下载页面点击购买你推广的内购商品,如果用户已经安装过你的 App 则会直接跳转你的App并调用上述代理方法;如果用户还没有安装你的 App 那么就会去下载你的 App,下载完成之后系统会推送一个通知,如果用户点击该通知就会跳转到你的App并且调用上面的代理方法 + + 上面的代理方法返回 true 则表示跳转到你的 App,IAP 继续完成交易,如果返回 false 则表示推迟或者取消购买,实际开发中因为可能还需要用户登录自己的账号、生成订单等,一般都是返回 false,之后通过 FGIAPService 的. -tryShouldAddStorePayments 在合适的时机触发。 + */ +- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product API_AVAILABLE(ios(11.0)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Vender/FGIAPService/NSObject+FGIsNullOrEmpty.h b/keyBoard/Class/Vender/FGIAPService/NSObject+FGIsNullOrEmpty.h new file mode 100644 index 0000000..e29e9b5 --- /dev/null +++ b/keyBoard/Class/Vender/FGIAPService/NSObject+FGIsNullOrEmpty.h @@ -0,0 +1,19 @@ +// +// NSObject+FGIsNullOrEmpty.h +// FGIAPService +// +// Created by FoneG on 2021/5/10. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSObject (FGIsNullOrEmpty) + +- (BOOL)isNSStringAndNotEmpty; +- (BOOL)isNSArrayAndNotEmpty; + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Vender/FGIAPService/NSObject+FGIsNullOrEmpty.m b/keyBoard/Class/Vender/FGIAPService/NSObject+FGIsNullOrEmpty.m new file mode 100644 index 0000000..18bd24c --- /dev/null +++ b/keyBoard/Class/Vender/FGIAPService/NSObject+FGIsNullOrEmpty.m @@ -0,0 +1,55 @@ +// +// NSObject+FGIsNullOrEmpty.m +// FGIAPService +// +// Created by FoneG on 2021/5/10. +// + +#import "NSObject+FGIsNullOrEmpty.h" + +@implementation NSObject (FGIsNullOrEmpty) + +- (BOOL)isNSStringAndNotEmpty +{ + if (nil == self) { + return false; + } + if ([self isKindOfClass:[NSString class]]) { + NSString *str = (NSString*)self; + str = [str stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if ([str isEqualToString:@""]) { + return false; + } + if ([str isEqualToString:@"(null)"]) { + return false; + } + if (str.length == 0) { + return false; + } + return true; + } else if ([self isKindOfClass:[NSAttributedString class]]) { + NSString *str = ((NSAttributedString *)self).string; + str = [str stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if (str.length > 0) { + return true; + }else{ + return false; + } + } else{ + return false; + } +} + + +- (BOOL)isNSArrayAndNotEmpty +{ + if ([self isKindOfClass:[NSArray class]]) { + NSArray *temp = (NSArray *)self; + if (temp.count > 0) { + return true; + } + } + return false; +} + +@end