2018iOS内购详细教程&iOS内购我踩过的坑(出现的问题以及优化方案&一些细节处理)

首先,在我上一次发的这篇文章里https://www.jianshu.com/p/d2b3f297ba9e 存在几个容易出现问题的点,在实际的运营中确实也出现过,所以针对出现的问题也做了一下优化,希望对大家有帮助。

问题一

在前一篇内购的代码中,在实际运营中,出现过获取不到平台订单号,也就是applicationUsername为nil的情况 ,这种情况是因为苹果返回参数为nil导致的漏单,所以说这个参数并不是完全的可靠。所以,有必要在这个参数返回为nil 的时候做一下处理。首先有两种方案:

一、可以在本地进行存储,先去判断苹果返回的这个applicationUsername是否为nil 如果为nil,去判断传入的这个平台订单号是否有值,如果有,直接使用这个值(一般走购买流程,这个值都是有的即可,如果是走的漏单流程,applicationUsername 为nil 且此时是不传入平台订单号的,这时候就应该在发起购买的之后存储这个订单号,具体存储可以将拉起购买传进来的平台order作为值,然后transaction.transactionIdentifier作为key(注:实测当凭证为key 存储的话,在有订阅类型商品的收会出现同一个商品不同时段凭证不一样的情况,导致取不到存储的order,因此通过transaction.transactionIdentifier(唯一事务id)来进行存储,这个实测确保是唯一的)存储到某个目录下,文件名可以用:平台订单号.plist的格式保存。所以在下次走漏单的流程时候苹果返回applicatonUsername为nil 的 ,可以去遍历存储的plist,然后将键为该凭证的值取出来,这就是与购买凭证匹配的平台订单号了。

二、第二种方法需要跟服务端交互 ,也就是在拉起内购的时候,在获取到凭证的时候,将凭证和传进来的平台订单号一起post到你们家后台,当在获取苹果返回的applicationUsername为nil 的时候 ,通过凭证去后台将这个平台订单号取回来就可以了。第二种方法需要进行两部交互有点繁琐。

问题二

在此前的文章中的这一段代码,在实际运营中出现过因为订单号为nil 而无法删除已经成功的凭证,不停向服务器发起访问的问题,部分代码如下:

  -(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 {
    
    [RRHUD hide];
    
 }
}



  #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]) {
    
    //移除与游戏cp订单号一样的plist 文件
    BOOL ifRemove =  [fileManager removeItemAtPath:plistPath error:&error];
    
    if (ifRemove) {
        
        NSLog(@"成功订单移除成功");
        
    }else{
        
        NSLog(@"成功订单移除失败");
    }
    
}else{
    
    NSLog(@"本地无与之匹配的订单");
 }
}

以上代码的话会在获取不到订单号的时候删除不了本地的已经成功的订单。会导致 -(void)checkIAPFiles:(SKPaymentTransaction *)transaction这个方法始终判断这个目录下有文件。在购买的时候不停地向服务端发起访问,加剧服务器压力。

针对以上的问题,我简化了一些流程,考虑到苹果内购有独有的漏单操作。其实不必要存储凭证也可以实现补单流程 。优化代码我贴出来,具体关于applicationUsername为nil 的处理,大家可以根据需求从中选择一个来进行处理 。至于文件我也会更新到github ,链接请看评论,有需要大家可以下载使用。至于该文件中的一些block 或者你们看不懂的回调其实跟我的业务有关,因为我是写SDK给别人用的所以要回调购买的结果给研发做判断进行发货操作。不关乎你们的内购逻辑。一下是优化之后的内购代码 :
IPAPurchase.h

IPAPurchase.h
#import <Foundation/Foundation.h>


/**
 block

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

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

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

@end

IPAPurchase.m

//  IPAPurchase.m
//  iOS_Purchase
//  Created by zhanfeng on 2017/6/6.
//  Copyright © 2017年 unlock_liujia. All rights reserved.

#import "IPAPurchase.h"
#import "ULSDKConfig.h"
#import <StoreKit/StoreKit.h>
#import <StoreKit/SKPaymentTransaction.h>

static NSString * const receiptKey = @"receipt_key";

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()<SKPaymentTransactionObserver,
SKProductsRequestDelegate>
{
SKProductsRequest *request;
}
//购买凭证
@property (nonatomic,copy)NSString *receipt;//存储base64编码的交易凭证

//产品ID
@property (nonnull,copy)NSString * profductId;

@end

static IPAPurchase * manager = nil;

@implementation IPAPurchase
#pragma mark -- 单例方法
+ (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 removeAllUncompleteTransactionsBeforeNewPurchase];

self.payResultBlock = payResult;

[RRHUD showWithContainerView:RR_keyWindow status:NSLocalizedString(@"Buying...", @"")];

self.profductId = productID;

if (!self.profductId.length) {
    
    UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"Warm prompt" message:@"There is no corresponding product." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
    
    [alertView show];
}

if ([SKPaymentQueue canMakePayments]) {
    
    [self requestProductInfo:self.profductId];
    
}else{
    
UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"Warm prompt" message:@"Please turn on the in-app paid purchase function first." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
    
[alertView show];
    
  }

}

 #pragma mark -- 结束上次未完成的交易
-(void)removeAllUncompleteTransactionsBeforeNewPurchase{

NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;

if (transactions.count >= 1) {

    for (NSInteger count = transactions.count; count > 0; count--) {
        
        SKPaymentTransaction* transaction = [transactions objectAtIndex:count-1];
        
        if (transaction.transactionState == SKPaymentTransactionStatePurchased||transaction.transactionState == SKPaymentTransactionStateRestored) {
            
            [[SKPaymentQueue defaultQueue]finishTransaction:transaction];
        }
    } 
}else{ 
     NSLog(@"没有历史未消耗订单");
  }
}


#pragma mark -- 发起购买请求
-(void)requestProductInfo:(NSString *)productID{

NSArray * productArray = [[NSArray alloc]initWithObjects:productID,nil];

NSSet * IDSet = [NSSet setWithArray:productArray];

request = [[SKProductsRequest alloc]initWithProductIdentifiers:IDSet];

request.delegate = self;

[request start];

}

#pragma mark -- SKProductsRequestDelegate 查询成功后的回调
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *myProduct = response.products;

if (myProduct.count == 0) {
    
    [RRHUD hide];
    [RRHUD showErrorWithContainerView:UL_rootVC.view status:NSLocalizedString(@"No Product Info", @"")];
    
    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.profductId]) {
        
        product = pro;
        
        break;
    }
}

if (product) {
    
    SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
    //使用苹果提供的属性,将平台订单号复制给这个属性作为透传参数
    payment.applicationUsername = self.order;
    
    [[SKPaymentQueue defaultQueue] addPayment:payment];
    
}else{
    
    NSLog(@"没有此商品信息");
  }
}

//查询失败后的回调
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {

if (self.payResultBlock) {
    self.payResultBlock(NO, nil, [error localizedDescription]);
  }
}

//如果没有设置监听购买结果将直接跳至反馈结束;
-(void)requestDidFinish:(SKRequest *)request{

}

#pragma mark -- 监听结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{

//当用户购买的操作有结果时,就会触发下面的回调函数,
for (SKPaymentTransaction * transaction in transactions) {
    
    switch (transaction.transactionState) {
            
        case SKPaymentTransactionStatePurchased:{
            
            [self completeTransaction:transaction];
            
        }break;
            
        case SKPaymentTransactionStateFailed:{
            
            [self failedTransaction:transaction];
            
        }break;
            
        case SKPaymentTransactionStateRestored:{//已经购买过该商品
            
            [self restoreTransaction:transaction];
            
        }break;
            
        case SKPaymentTransactionStatePurchasing:{
            
            NSLog(@"正在购买中...");
            
        }break;
            
        case SKPaymentTransactionStateDeferred:{
            
            NSLog(@"最终状态未确定");
            
        }break;
            
        default:
            break;
    }
  }
}

//完成交易
#pragma mark -- 交易完成的回调
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{    
[self getAndSaveReceipt:transaction]; //获取交易成功后的购买凭证
}

#pragma mark -- 处理交易失败回调
- (void)failedTransaction:(SKPaymentTransaction *)transaction{

[RRHUD hide];

NSString * error = nil;

if(transaction.error.code != SKErrorPaymentCancelled) {
    
    [RRHUD showInfoWithContainerView:UL_rootVC.view status:NSLocalizedString(@"Buy Failed", @"")];
    error = [NSString stringWithFormat:@"%ld",transaction.error.code];
    
} else {
    
    [RRHUD showInfoWithContainerView:UL_rootVC.view status:NSLocalizedString(@"Buy Canceled", @"")];
    error = [NSString stringWithFormat:@"%ld",transaction.error.code];
    
}

if (self.payResultBlock) {
    self.payResultBlock(NO, nil, error);
}

[[SKPaymentQueue defaultQueue]finishTransaction:transaction];

}

- (void)restoreTransaction:(SKPaymentTransaction *)transaction{

[RRHUD hide];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}


#pragma mark -- 获取购买凭证
-(void)getAndSaveReceipt:(SKPaymentTransaction *)transaction{

//获取交易凭证
NSURL * receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData * receiptData = [NSData dataWithContentsOfURL:receiptUrl];
NSString * base64String = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
//初始化字典
NSMutableDictionary * dic = [[NSMutableDictionary alloc]init];

NSString * order = transaction.payment.applicationUsername;

//如果这个返回为nil

NSLog(@"后台订单号为订单号为%@",order);

[dic setValue: base64String forKey:receiptKey];
[dic setValue: order forKey:@"order"];
[dic setValue:[self getCurrentZoneTime] forKey:@"time"];
NSString * userId;

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

if (userId == nil||[userId length] == 0) {
    
    userId = @"走漏单流程未传入userId";
}

if (order == nil||[order length] == 0) {
    
    order = @"苹果返回透传参数为nil";
}

[[ULSDKAPI shareAPI]sendLineWithPayOrder:order UserId:userId Receipt:base64String LineNumber:@"IPAPurchase.m 337"];

NSString *fileName = [NSString UUID];

NSString *savedPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper iapReceiptPath], fileName];
[dic setValue: userId forKey:@"user_id"];

//这个存储成功与否其实无关紧要
BOOL ifWriteSuccess = [dic writeToFile:savedPath atomically:YES];

if (ifWriteSuccess){

    NSLog(@"购买凭据存储成功!");

}else{
    
    NSLog(@"购买凭据存储失败");
}

[self sendAppStoreRequestBuyWithReceipt:base64String userId:userId paltFormOrder:order trans:transaction];

}

-(void)getPlatformAmountInfoWithOrder:(NSString *)transOrcer{

[[ULSDKAPI shareAPI]getPlatformAmountWithOrder:transOrcer success:^(id responseObject) {
    
    if (RequestSuccess) {
        
    _platformAmount = [[responseObject objectForKey:@"data"]objectForKey:@"amount"];
    _amount_type = [[responseObject objectForKey:@"data"]objectForKey:@"amount_type"];
    _third_goods_id = [[responseObject objectForKey:@"data"]objectForKey:@"third_goods_id"];
    
    [FBSDKAppEvents logEvent:@"pay_in_sdk" valueToSum:[_platformAmount doubleValue] parameters:@{@"fb_currency":@"USD",@"amount":_platformAmount,@"amount_type":_amount_type,@"third_goods_id":_third_goods_id}];
    
    }else{
        
        NSLog(@"%@",[responseObject objectForKey:@"message"]);
    }
    
} failure:^(NSString *failure) {

}];

}

#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];
//存储购买成功的凭证
[self insertReceiptWithReceiptByReceipt:receipt withDic:mdic  inReceiptPath:successReceiptPath];
}



-(void)insertReceiptWithReceiptByReceipt:(NSString *)receipt withDic:(NSDictionary *)dic inReceiptPath:(NSString *)receiptfilePath{

BOOL isContain = NO;

NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
NSArray * cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper SuccessIapPath] error:&error];

if (cacheFileNameArray.count == 0) {
    
    [dic writeToFile:receiptfilePath atomically:YES];
    
    if ([dic writeToFile:receiptfilePath atomically:YES]) {
        
        NSLog(@"写入购买凭据成功");
        
    }
    
}else{
   
    if (error == nil) {
     
        for (NSString * name in cacheFileNameArray) {

            NSString * filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper SuccessIapPath], name];
            NSMutableDictionary *localdic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
            
            if ([localdic.allValues containsObject:receipt]) {
                
                isContain = YES;
                
            }else{
                
                continue;
            }
        }
        
    }else{
        
        NSLog(@"读取本文存储凭据失败");
    }
    
}

if (isContain == NO) {
    
BOOL  results = [dic writeToFile:receiptfilePath atomically:YES];
    
if (results) {
    
    NSLog(@"写入凭证成功");
}else{
    
    NSLog(@"写入凭证失败");
}
    
}else{
    
    NSLog(@"已经存在凭证请勿重复写入");
  }
}

#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)sendAppStoreRequestBuyWithReceipt:(NSString *)receipt userId:(NSString *)userId paltFormOrder:(NSString * )order trans:(SKPaymentTransaction *)transaction{

[[ULSDKAPI shareAPI]sendLineWithPayOrder:order UserId:userId Receipt:receipt LineNumber:@"IPAPurchase.m 474"];
#pragma mark -- 发送信息去验证是否成功
[[ULSDKAPI shareAPI] sendVertifyWithReceipt:receipt order:order userId:userId success:^(ULSDKAPI *api, id responseObject) {
    
    if (RequestSuccess) {
        
        [RRHUD hide];
        [RRHUD showSuccessWithContainerView:UL_rootVC.view status:NSLocalizedString(@"Buy Success", @"")];
        //结束交易方法
        [[SKPaymentQueue defaultQueue]finishTransaction:transaction];
        
        [self getPlatformAmountInfoWithOrder:order];
        
        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 successConsumptionOfGoodsWithReceipt:receipt];
       
    }else{
#pragma mark -- callBack 回调
        [api sendVertifyWithReceipt:receipt order:order userId:userId success:^(ULSDKAPI *api, id responseObject) {
            
            if (RequestSuccess) {
                
                [RRHUD hide];
                
                [RRHUD showSuccessWithContainerView:UL_rootVC.view status:NSLocalizedString(@"Buy Success", @"")];
                
                [[SKPaymentQueue defaultQueue]finishTransaction:transaction];
                
                [self getPlatformAmountInfoWithOrder:order];
                
                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 successConsumptionOfGoodsWithReceipt:receipt];
                    }
#pragma 校验发货失败 1
                } failure:^(ULSDKAPI *api, NSString *failure) {
                    
                    [RRHUD hide];
                    [RRHUD showErrorWithContainerView:UL_rootVC.view status:NSLocalizedString(@"Request Error", @"")];
                    
                }];
                
            }else{
                
                [RRHUD hide];
#pragma mark --发送错误报告
                [api sendFailureReoprtWithReceipt:receipt order:order success:^(ULSDKAPI *api, id responseObject) {
                    
                } failure:^(ULSDKAPI *api, NSString *failure) {
                    
                    [RRHUD hide];
                }];
                
            }
            
        } failure:^(ULSDKAPI *api, NSString *failure) {
            
            [RRHUD hide];
        }];
        
    }
    
} failure:^(ULSDKAPI *api, NSString *failure) {
    
    [RRHUD hide];
    
    [api VertfyFailedRePostWithUserId:userId Order:order jsonStr:failure];
    
}];
}

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

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 ByReceipt:receipt];
            
        }
    }
}
}

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

NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
NSString * localReceipt = [dic objectForKey:@"receipt_key"];
//通过凭证进行对比
if ([receipt isEqualToString:localReceipt]) {
  
    BOOL ifRemove = [fileManager removeItemAtPath:plistPath error:&error];
    
    if (ifRemove) {
        
        NSLog(@"成功订单移除成功");
        
    }else{
        
        NSLog(@"成功订单移除失败");
    }
    
}else{
    
    NSLog(@"本地无与之匹配的订单");
}
}

@end

代码可能粘贴不齐,细节调整之后和最新的单例类内购文件已经同步到GitHub 了,如有需要可以去gitHud 直接下载源文件,建议自己揣摩清楚之后再参考或者引入自己的项目,之所谓知其所以也要知其所以然。另外在-(void)getAndSaveReceipt:(SKPaymentTransaction *)transaction;方法里面没有对applicationUsername为nil 做处理,大家可以跟我说的哪两种思路进行解决,我在这里就不代码演示了。大家多动脑~ github链接如下:https://github.com/jiajiaailaras/ULIPAPurchase

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

推荐阅读更多精彩内容

  • 1、收款协议以及账户等的创建 内购收款协议等的创建,这里一般由运营负责,这里不做介绍,但是如果想要了解,请参考这位...
    佳佳爱宥嘉阅读 8,885评论 44 49
  • 1、收款协议以及账户等的创建 内购收款协议等的创建,这里一般由运营负责,这里不做介绍,但是如果想要了解,请参考这位...
    Flash_qing阅读 10,902评论 7 43
  • 更新:经过这几天的用户反馈及自己的查找,发现了一些问题。首先,在添加观察者之前是获取不到未完成订单的,只有在观察者...
    皮乐皮儿阅读 8,697评论 15 24
  • 一.总说内购的内容 协议、税务和银行业务 信息填写 内购商品的添加 添加沙盒测试账号 内购代码的具体实现 内购的注...
    九洲仙人阅读 2,954评论 2 3
  • 高中的时候流行一句口头禅“三观尽毁” 那个时候对于三观没有什么定义 只是碰到了自己感到惊奇的事情就会这样吐槽一句 ...
    姁姁旭旭阅读 168评论 0 0