iOS——苹果内购工具类(掉单处理)

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SandBoxHelper : NSObject
// 程序主目录,可见子目录(3个):Documents、Library、tmp
+ (NSString *)homePath;

// 程序目录,不能存任何东西
+ (NSString *)appPath;

// 文档目录,需要ITUNES同步备份的数据存这里,可存放用户数据
+ (NSString *)docPath;

// 配置目录,配置文件存这里
+ (NSString *)libPrefPath;

// 缓存目录,系统永远不会删除这里的文件,ITUNES会删除
+ (NSString *)libCachePath;

// 临时缓存目录,APP退出后,系统可能会删除这里的内容
+ (NSString *)tmpPath;

//用于存储iap内购返回的购买凭证
+ (NSString *)iapReceiptPath;

//存储成功订单的方法
+(NSString *)SuccessIapPath;

//存储崩溃日志的方法;
+(NSString *)crashLogInfo;

//存储退出资源的路径
+(NSString *)exitResourePath;

//保存临时订单
+(NSString *)tempOrderPath;
@end

NS_ASSUME_NONNULL_END
#import "SandBoxHelper.h"

@implementation SandBoxHelper
+ (NSString *)homePath {
    return NSHomeDirectory();
}

+ (NSString *)appPath {
    NSArray * paths = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSUserDomainMask, YES);
    return [paths objectAtIndex:0];
}

+ (NSString *)docPath {
    NSArray * paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    return [paths objectAtIndex:0];
}

+ (NSString *)libPrefPath {
    NSArray * paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
    return [[paths objectAtIndex:0] stringByAppendingFormat:@"/Preferences"];
}

+ (NSString *)libCachePath {
    
    NSArray * paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
    return [[paths objectAtIndex:0] stringByAppendingFormat:@"/Caches"];
}

+ (NSString *)tmpPath {
    return [NSHomeDirectory() stringByAppendingFormat:@"/tmp"];
}

+ (NSString *)iapReceiptPath {
    
    NSString *path = [[self libPrefPath] stringByAppendingFormat:@"/EACEF35FE363A75A"];
    [self hasLive:path];
    return path;
}

+ (BOOL)hasLive:(NSString *)path
{
    if ( NO == [[NSFileManager defaultManager] fileExistsAtPath:path] )
    {
        return [[NSFileManager defaultManager] createDirectoryAtPath:path
                                         withIntermediateDirectories:YES
                                                          attributes:nil
                                                               error:NULL];
    }
    
    return YES;
}

+(NSString *)SuccessIapPath{
    
    NSString *path = [[self libPrefPath] stringByAppendingFormat:@"/SuccessReceiptPath"];
    
    [self hasLive:path];
    
    
    return path;
    
}

+(NSString *)exitResourePath{
    
    NSString *path = [[self libPrefPath] stringByAppendingFormat:@"/ExitResourePath"];
    
    [self hasLive:path];
    
    
    return path;
}

+(NSString *)tempOrderPath{
  
    NSString *path = [[self libPrefPath] stringByAppendingFormat:@"/tempOrderPath"];
    
    [self hasLive:path];
   
    return path;
    
}

+(NSString *)crashLogInfo{
    
    NSString * path = [[self libPrefPath]stringByAppendingFormat:@"/crashLogInfoPath"];
    [self hasLive:path];
    
    return path;
}
@end
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
/**
 block

 @param isSuccess 是否支付成功
 @param certificate 支付成功得到的凭证(用于在自己服务器验证)
 @param errorMsg 错误信息
 */
typedef void(^PayResult)(BOOL isSuccess, NSString *__nullable certificate,NSString * __nullable errorMsg);

@interface IPAPurchase : NSObject
/**
 *
 */
@property (nonatomic, copy) PayResult payResultBlock;

+(instancetype)manager;
/**
开启内购监听 在程序入口didFinishLaunchingWithOptions实现
*/
-(void)startManager;
/**
停止内购监听 在AppDelegate.m中的applicationWillTerminate方法实现
*/
-(void)stopManager;

/**
拉起内购支付
@param productID 内购商品ID
@param payResult 结果
*/
-(void)buyProductWithProductID:(NSString *)productID payResult:(PayResult)payResult;


/**
 *订单编号
 */
@property (nonatomic, copy) NSString * orderNumber;
/**
 *用户id
 */
@property (nonatomic, copy) NSString * userid;
/**
 *
 */
@property (nonatomic, assign) CGFloat  tbCount;
@end

NS_ASSUME_NONNULL_END
#import "IPAPurchase.h"
#import <StoreKit/StoreKit.h>
#import "SandBoxHelper.h"
#import "NSString+Chinese.h"

static NSString *const receiptKey = @"receiptKey";

dispatch_queue_t iap_queue(){
    static dispatch_queue_t as_iap_queue;
    static dispatch_once_t onceToken_iap_queue;
    dispatch_once(&onceToken_iap_queue, ^{
        as_iap_queue = dispatch_queue_create("com.iap.queue", DISPATCH_QUEUE_CONCURRENT);
    });
    return as_iap_queue;
}

@interface IPAPurchase()
<SKPaymentQueueDelegate, SKProductsRequestDelegate>
/**
 *
 */
@property (nonatomic, strong) SKProductsRequest * request;

/**
 *购买凭证(存储base64编码的交易凭证)
 */
@property (nonatomic, copy) NSString * receipt;
/**
 *
 */
@property (nonatomic, copy) NSString * productId;

@end

static IPAPurchase *manager = nil;

@implementation IPAPurchase

+(instancetype)manager{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if(!manager){
            manager = [[IPAPurchase alloc] init];
        }
    });
    return manager;
}
#pragma mark -- 漏单处理
-(void)startManager{
    dispatch_sync(iap_queue(), ^{
        [[SKPaymentQueue defaultQueue] addTransactionObserver:manager];
    });
}
#pragma mark -- 移除交易事件
-(void)stopManager{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    });
}
#pragma mark -- 发起购买的方法
-(void)buyProductWithProductID:(NSString *)productID payResult:(PayResult)payResult{
    self.payResultBlock = payResult;
    [self removeAllUncompleteTransactionBeforeStartNewtransaction];
    //购买中HUD。。。
    [self showWhiteHUDWithText:@" 购买中... "];
    
    self.productId = productID;
    if(!self.productId.length){
        UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"温馨提示" message:@"没有对应的商品" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles: nil];

        [alertView show];
    }
    
    if([SKPaymentQueue canMakePayments]){
        [self requestProductInfo:self.productId];
    }
    else{
        UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"温馨提示" message:@"请先开启应用内付费购买功能。" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles: nil];
        [alertView show];
    }
}
#pragma mark -- 结束上次未完成的交易 防止串单
-(void)removeAllUncompleteTransactionBeforeStartNewtransaction{
    NSArray *transactionsArray = [SKPaymentQueue defaultQueue].transactions;
    if(transactionsArray.count >0){
        //检测是否有未完成的交易
        SKPaymentTransaction *transaction = [transactionsArray firstObject];
        if(transaction.transactionState == SKPaymentTransactionStatePurchased){
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            return;
        }
    }
}
#pragma mark --发起购买请求
-(void)requestProductInfo:(NSString *)productId{
    NSArray *productArray = [[NSArray alloc] initWithObjects:productId, nil];
    NSSet *IDSet = [NSSet setWithArray:productArray];
    self.request = [[SKProductsRequest alloc] initWithProductIdentifiers:IDSet];
    self.request.delegate = self;
    [self.request start];
}

#pragma mark -- SKProductsRequestDelegate 查询成功后的回调
-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
//    [self dismissCustom];
    
    NSArray *myProduct = response.products;
    if(myProduct.count == 0){
        //没有该商品信息HUD。。。
        [self showErrorText:@"无法获取产品信息,购买失败"];
        
        if (self.payResultBlock) {
            self.payResultBlock(NO, nil, @"无法获取产品信息,购买失败");
        }
        return;
    }
    
    SKProduct *product = nil;
    for (SKProduct *pro in myProduct) {
        NSLog(@"SKProduct 描述信息%@", [pro description]);
        NSLog(@"产品标题 %@" , pro.localizedTitle);
        NSLog(@"产品描述信息: %@" , pro.localizedDescription);
        NSLog(@"价格: %@" , pro.price);
        NSLog(@"Product id: %@" , pro.productIdentifier);
        
        if([pro.productIdentifier isEqualToString:self.productId]){
            product = pro;
            break;
        }
    }
    
    if(product){
        SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
        payment.applicationUsername = self.orderNumber;
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
    else{
         NSLog(@"没有此商品");
    }
}
//查询失败后的回调
-(void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    [self dismissCustom];
    if (self.payResultBlock) {
        self.payResultBlock(NO, nil, [error localizedDescription]);
    }
}
////如果没有设置监听购买结果将直接跳至反馈结束
-(void)requestDidFinish:(SKRequest *)request{
//    [self dismissCustom];
}
#pragma mark -- 监听结果
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
    
    //当用户购买的操作有结果时,就会触发下面的回调函数,
    for (SKPaymentTransaction * transaction in transactions) {
        
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchased:{
                [self completeTransaction:transaction];
//                [self dismissCustom];
            }break;

            case SKPaymentTransactionStateFailed:{
                [self failedTransaction:transaction];
                [self dismissCustom];
            }break;

            case SKPaymentTransactionStateRestored:{//已经购买过该商品
                [self restoreTransaction:transaction];
                [self dismissCustom];
            }break;

            case SKPaymentTransactionStatePurchasing:{
                NSLog(@"正在购买中...");
            }break;

            case SKPaymentTransactionStateDeferred:{
                NSLog(@"最终状态未确定");
                [self dismissCustom];
            }break;

            default:
                break;
          }
    }
}
//完成交易
#pragma mark -- 交易完成的回调
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
    NSLog(@"购买成功,准备验证发货");
    [self getReceipt]; //获取交易成功后的购买凭证
    [self saveReceipt:transaction]; //存储交易凭证
    [self checkIAPFiles:transaction];
}

#pragma mark -- 处理交易失败回调
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
    NSString *error = nil;
    if(transaction.error.code != SKErrorPaymentCancelled) {
        //购买失败HUD。。。
        [self showSuccessText:@"购买失败"];
    } else {
        //取消购买HUD。。。
        [self showSuccessText:@"取消购买"];
    }

    if (self.payResultBlock) {
        self.payResultBlock(NO, nil, error);
    }
    [[SKPaymentQueue defaultQueue]finishTransaction:transaction];
}

- (void)restoreTransaction:(SKPaymentTransaction *)transaction{
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

#pragma mark -- 获取购买凭证
-(void)getReceipt{
    NSURL * receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData * receiptData = [NSData dataWithContentsOfURL:receiptUrl];
    NSString * base64String = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    self.receipt = base64String;
}

#pragma mark -- 存储购买凭证
-(void)saveReceipt:(SKPaymentTransaction *)transaction{
    NSString * userId;
    NSString * order;

    if (self.userid) {
        userId = self.userid;
        [[NSUserDefaults standardUserDefaults]setObject:userId forKey:@"unlock_iap_userId"];
    }else{

        userId = [[NSUserDefaults standardUserDefaults]objectForKey:@"unlock_iap_userId"];
    }

    order = transaction.payment.applicationUsername;

    NSString *fileName = [NSString UUID];
    NSString *savedPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper iapReceiptPath], fileName];
    NSMutableDictionary * dic = [[NSMutableDictionary alloc]init];
    [dic setValue: self.receipt forKey:receiptKey];
    [dic setValue: userId forKey:@"user_id"];
    [dic setValue: order forKey:@"order"];
    BOOL ifWriteSuccess  = [dic writeToFile:savedPath atomically:YES];

    if (ifWriteSuccess) {
        NSLog(@"购买凭据存储成功!");
    }
}

#pragma mark -- 验证本地数据
-(void)checkIAPFiles:(SKPaymentTransaction *)transaction{

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;
    NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];
    if (error == nil) {
    for (NSString *name in cacheFileNameArray) {

        if ([name hasSuffix:@".plist"]){ //如果有plist后缀的文件,说明就是存储的购买凭证

            NSString *filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];

            [self sendAppStoreRequestBuyPlist:filePath trans:transaction];
        }
    }

    } else {

    }
}

#pragma mark -- 存储成功订单
-(void)SaveIapSuccessReceiptDataWithReceipt:(NSString *)receipt Order:(NSString *)order UserId:(NSString *)userId{
    NSMutableDictionary * mdic = [[NSMutableDictionary alloc]init];
    [mdic setValue:[self getCurrentZoneTime] forKey:@"time"];
    [mdic setValue: order forKey:@"order"];
    [mdic setValue: userId forKey:@"userid"];
    [mdic setValue: receipt forKey:receiptKey];
    NSString *fileName = [NSString UUID];
    NSString * successReceiptPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper SuccessIapPath], fileName];
    //存储购买成功的凭证
    [mdic writeToFile:successReceiptPath atomically:YES];
}

#pragma mark -- 获取系统时间的方法
-(NSString *)getCurrentZoneTime{
    NSDate * date = [NSDate date];
    NSDateFormatter*formatter = [[NSDateFormatter alloc]init];
    [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    NSString*dateTime = [formatter stringFromDate:date];
    return dateTime;
}

#pragma mark -- 去服务器验证购买
-(void)sendAppStoreRequestBuyPlist:(NSString *)plistPath trans:(SKPaymentTransaction *)transaction{
//    [self showWhiteHUDWithText:@"验证中..."];
    NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
    NSString * receipt = [dic objectForKey:receiptKey];
    NSString * order = [dic objectForKey:@"order"];
    NSString * userId = [dic objectForKey:@"user_id"];

#pragma mark -- 发送信息去验证是否成功
    WeakSelf
    [JHNetworkHelper requestPOST:@"iapPay/yz_iappay" parameters:@{@"orderNo":order,@"receipt":receipt} modelClass:nil success:^(id responseObject) {
        [weakSelf showSuccessText:@"购买成功"];
        weakSelf.tbCount = [responseObject[@"result"][@"money"] floatValue];
        
        [[NSUserDefaults standardUserDefaults]removeObjectForKey:@"unlock_iap_userId"];
        NSData * data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];

        NSString *result = [data base64EncodedStringWithOptions:0];

        if (weakSelf.payResultBlock) {
            weakSelf.payResultBlock(YES, result, nil);
        }
        //这里将成功但存储起来
        [weakSelf SaveIapSuccessReceiptDataWithReceipt:receipt Order:order UserId:userId];

        [weakSelf successConsumptionOfGoodsWithOrder:order];
        
    } failure:^(NSError *error) {
        [weakSelf dismissCustom];
    }];
//    [[ULSDKAPI shareAPI] sendVertifyWithReceipt:receipt order:order success:^(ULSDKAPI *api, id responseObject) {
//
//        if (RequestSuccess) {
//
//            NSLog(@"服务器验证成功!");
//
//            [[SKPaymentQueue defaultQueue]finishTransaction:transaction];
//
//            [RRHUD hide];
//
//            [RRHUD showSuccessWithContainerView:UL_rootVC.view status:NSLocalizedString(@"购买成功", @"")];
//
//            [[NSUserDefaults standardUserDefaults]removeObjectForKey:@"unlock_iap_userId"];
//            NSData * data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];
//
//            NSString *result = [data base64EncodedStringWithOptions:0];
//
//            if (self.payResultBlock) {
//                self.payResultBlock(YES, result, nil);
//            }
//            //这里将成功但存储起来
//            [self SaveIapSuccessReceiptDataWithReceipt:receipt Order:order UserId:userId];
//
//            [self successConsumptionOfGoodsWithOrder:order];
//
//        }else{
//          //在这里向服务器发送验证失败相关信息
//    } failure:^(ULSDKAPI *api, NSString *failure) {
//
//    }
}

#pragma mark -- 根据订单号来移除本地凭证的方法
-(void)successConsumptionOfGoodsWithOrder:(NSString * )cpOrder{

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError * error;
    if ([fileManager fileExistsAtPath:[SandBoxHelper iapReceiptPath]]) {

        NSArray * cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];

        if (error == nil) {

            for (NSString * name in cacheFileNameArray) {

                NSString * filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];

                [self removeReceiptWithPlistPath:filePath ByCpOrder:cpOrder];

            }
        }
    }
}

#pragma mark -- 根据订单号来删除 存储的凭证
-(void)removeReceiptWithPlistPath:(NSString *)plistPath   ByCpOrder:(NSString *)cpOrder{

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError * error;
    NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
    NSString * order = [dic objectForKey:@"order"];

    if ([cpOrder isEqualToString:order]) {
        //移除与用户id订单号一样的plist 文件
        BOOL ifRemove =  [fileManager removeItemAtPath:plistPath error:&error];
        if (ifRemove) {
            NSLog(@"成功订单移除成功");
        }else{
            NSLog(@"成功订单移除失败");
        }
    }else{
        NSLog(@"本地无与之匹配的订单");
    }
}
@end

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容