This commit is contained in:
2025-11-13 19:20:57 +08:00
parent 50163d02a7
commit ae79d1b1ba
17 changed files with 846 additions and 4 deletions

View File

@@ -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 */

View File

@@ -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 = "<group>"; };
04122F602EC5F41D00EF7AB3 /* KBUser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBUser.h; sourceTree = "<group>"; };
04122F612EC5F41D00EF7AB3 /* KBUser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBUser.m; sourceTree = "<group>"; };
04122F622EC5F40800EF7AB3 /* FGIAPManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FGIAPManager.h; sourceTree = "<group>"; };
04122F632EC5F40800EF7AB3 /* FGIAPManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FGIAPManager.m; sourceTree = "<group>"; };
04122F642EC5F40800EF7AB3 /* FGIAPProductsFilter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FGIAPProductsFilter.h; sourceTree = "<group>"; };
04122F652EC5F40800EF7AB3 /* FGIAPProductsFilter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FGIAPProductsFilter.m; sourceTree = "<group>"; };
04122F662EC5F40800EF7AB3 /* FGIAPService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FGIAPService.h; sourceTree = "<group>"; };
04122F672EC5F40800EF7AB3 /* FGIAPService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FGIAPService.m; sourceTree = "<group>"; };
04122F682EC5F40800EF7AB3 /* FGIAPServiceUtility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FGIAPServiceUtility.h; sourceTree = "<group>"; };
04122F692EC5F40800EF7AB3 /* FGIAPVerifyTransaction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FGIAPVerifyTransaction.h; sourceTree = "<group>"; };
04122F6A2EC5F40800EF7AB3 /* NSObject+FGIsNullOrEmpty.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSObject+FGIsNullOrEmpty.h"; sourceTree = "<group>"; };
04122F6B2EC5F40800EF7AB3 /* NSObject+FGIsNullOrEmpty.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSObject+FGIsNullOrEmpty.m"; sourceTree = "<group>"; };
0459D1B22EBA284C00F2D189 /* KBSkinCenterVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinCenterVC.h; sourceTree = "<group>"; };
0459D1B32EBA284C00F2D189 /* KBSkinCenterVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCenterVC.m; sourceTree = "<group>"; };
0459D1B52EBA287900F2D189 /* KBSkinManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinManager.h; sourceTree = "<group>"; };
@@ -468,6 +482,54 @@
path = VM;
sourceTree = "<group>";
};
04122F5E2EC5F3DF00EF7AB3 /* M */ = {
isa = PBXGroup;
children = (
);
path = M;
sourceTree = "<group>";
};
04122F5F2EC5F3DF00EF7AB3 /* V */ = {
isa = PBXGroup;
children = (
);
path = V;
sourceTree = "<group>";
};
04122F602EC5F3DF00EF7AB3 /* VC */ = {
isa = PBXGroup;
children = (
);
path = VC;
sourceTree = "<group>";
};
04122F612EC5F3DF00EF7AB3 /* Pay */ = {
isa = PBXGroup;
children = (
04122F5E2EC5F3DF00EF7AB3 /* M */,
04122F5F2EC5F3DF00EF7AB3 /* V */,
04122F602EC5F3DF00EF7AB3 /* VC */,
);
path = Pay;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
@@ -1151,6 +1215,33 @@
path = Login;
sourceTree = "<group>";
};
04122F612EC5F3DF00EF7AB3 /* Pay */ = {
isa = PBXGroup;
children = (
04122F642EC5F40600EF7AB3 /* M */,
04122F652EC5F40600EF7AB3 /* VM */,
);
path = Pay;
sourceTree = "<group>";
};
04122F642EC5F40600EF7AB3 /* M */ = {
isa = PBXGroup;
children = (
04122F7B2EC6123500EF7AB3 /* IAPVerifyTransactionObj.h */,
04122F7C2EC6123500EF7AB3 /* IAPVerifyTransactionObj.m */,
);
path = M;
sourceTree = "<group>";
};
04122F652EC5F40600EF7AB3 /* VM */ = {
isa = PBXGroup;
children = (
04122F782EC610C500EF7AB3 /* PayVM.h */,
04122F792EC610C500EF7AB3 /* PayVM.m */,
);
path = VM;
sourceTree = "<group>";
};
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 */,

View File

@@ -0,0 +1,17 @@
//
// IAPVerifyTransactionObj.h
// 将 Swift 内购验签逻辑迁移到 Objective-C
//
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
#import "FGIAPVerifyTransaction.h"
NS_ASSUME_NONNULL_BEGIN
@interface IAPVerifyTransactionObj : NSObject <FGIAPVerifyTransaction>
@end
NS_ASSUME_NONNULL_END

View File

@@ -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

View File

@@ -0,0 +1,33 @@
//
// PayVM.h
// 支付相关 VM封装 Apple IAP 验签请求
//
#import <Foundation/Foundation.h>
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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -0,0 +1,27 @@
//
// FGIAPManager.h
// MaltBaby
//
// Created by FoneG on 2020/5/11.
//
#import <Foundation/Foundation.h>
#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<FGIAPVerifyTransaction>)verifyTransaction;
@end
NS_ASSUME_NONNULL_END

View File

@@ -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<FGIAPVerifyTransaction>)verifyTransaction{
self->_iap = [[FGIAPService alloc] initWithTransaction:verifyTransaction];
}
@end

View File

@@ -0,0 +1,24 @@
//
// FGIAPProductsFilter.h
// MaltBaby
//
// Created by FoneG on 2021/5/8.
//
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef void(^FGIAPManagerResponseBlock)(NSArray<SKProduct *> * products);
@interface FGIAPProductsFilter : NSObject
/// 获取苹果内购商品列表
/// @param productIdentifiers 商品id列表
/// @param completion completion
- (void)requestProductsWith:(NSSet *)productIdentifiers completion:(nonnull FGIAPManagerResponseBlock)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,41 @@
//
// FGIAPProductsFilter.m
// MaltBaby
//
// Created by FoneG on 2021/5/8.
//
#import "FGIAPProductsFilter.h"
#import "FGIAPServiceUtility.h"
@interface FGIAPProductsFilter ()<SKProductsRequestDelegate>
@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

View File

@@ -0,0 +1,46 @@
//
// FGIAPManager.h
// MaltBaby
//
// Created by FoneG on 2020/5/7.
//
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
#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 一般得到苹果服务器返回的支付结果后,需要通过<verifyTransaction>再次向服务器进行二次确认,来保证整个支付链路闭环
*/
- (instancetype)initWithTransaction:(id<FGIAPVerifyTransaction>)verifyTransaction;
/**
* iap支付
* @param product 对应的商品
* @param completion 支付回调
*/
- (void)buyProduct:(SKProduct *)product onCompletion:(FGIAPManagerBuyBlock)completion;
/**
* 在合适的时间处理从App Store下载页面触发的内购行为
*/
- (void)tryShouldAddStorePayments;
@end
NS_ASSUME_NONNULL_END

View File

@@ -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 () <SKPaymentTransactionObserver, SKRequestDelegate>
@property (nonatomic, strong) id<FGIAPVerifyTransaction> verifyTransaction;
@property (nonatomic, copy) FGIAPManagerBuyBlock buyProductCompleteBlock;
@property (nonatomic, strong) NSString *productIdentifier;
@property (nonatomic, strong) SKPayment *APPStorePayment;
@end
@implementation FGIAPService
- (instancetype)initWithTransaction:(id<FGIAPVerifyTransaction>)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<SKPaymentTransaction *> *)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

View File

@@ -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 */

View File

@@ -0,0 +1,58 @@
//
// FGIAPVerifyTransaction.h
// MaltBaby
//
// Created by FoneG on 2021/5/8.
//
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
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 <NSObject>
/// 苹果支付流程结束后,需要根据返回的票据等数据去自己的服务器校验
/// @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 则表示跳转到你的 AppIAP 继续完成交易,如果返回 false 则表示推迟或者取消购买,实际开发中因为可能还需要用户登录自己的账号、生成订单等,一般都是返回 false之后通过 FGIAPService 的. -tryShouldAddStorePayments 在合适的时机触发。
*/
- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product API_AVAILABLE(ios(11.0));
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,19 @@
//
// NSObject+FGIsNullOrEmpty.h
// FGIAPService
//
// Created by FoneG on 2021/5/10.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (FGIsNullOrEmpty)
- (BOOL)isNSStringAndNotEmpty;
- (BOOL)isNSArrayAndNotEmpty;
@end
NS_ASSUME_NONNULL_END

View File

@@ -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:@"<null>"]) {
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