iOS 内购及掉单问题

在iOS开发中你是否遇到过购买虚拟物品的而无法使用第三方支付的问题,让不熟悉Apple内购的你不知所措,废话不多说,直接搞起。

第一部分:协议


第1步.png
第2步.jpg

第3步.jpg

第4步.png

第5步.png

第6步.jpg

第7步.png

第8步.jpg

第9步.jpg

第10步.png

CNAPS CODE 查询地址
https://e.czbank.com/CORPORBANK/query_unionBank_index.jsp

第11步.jpg

第12步.jpg
第13步.png

第14步.png

第15步.jpg

![第16步.jpg](http://upload-images.jianshu.io/upload_images/1975881-dd1ac4d3d8fdd555.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

第17步.png

第18步.png

协议Done,我们现在已经和Apple签订了协议,接下来该去上架商品了

第二部分:创建内购项目

第1步.png

第2步.png

第3步.png

第4步.png

第5步.png

第6步.png

第7步.png

Apple内购的价格是等级制的,无法自己随意定价,而且每比订单成交都要向苹果缴纳百分之30的抽成,坑爹吧!!

第三部分:App代码集成

介绍一下APP内购的步骤:

一般的内购分为两种,一种是我们app有自己的服务器,一种是本地的,像我们玩的闯关游戏需要购买关卡一般都是本地的,像那种联网手游,会员VIP的基本都是服务器的。

服务器模式:
1.调用服务器接口创建一个商品的订单
2.请求Apple的商品列表
3.选取商品调用苹果支付
4.支付成功(会返回凭证)
5.把支付成功的返回凭证上传到APP服务器(带上订单的ID,有利于后台判断是哪个订单支付成功)
6.APP服务器保存该凭证等数据并像苹果服务器发起凭证验证,验证成功则发送商品

本地模式:
1.请求Apple的商品列表
2.选取商品调用苹果支付
3.支付成功(会返回凭证)
4.把凭证与商品发送状态保存到一个本地的数据库
5.app调用apple服务器的验证API
6.验证成功发送商品并改变数据库的物品发送状态

最后一步了,是不是有些欣喜,最后在代码中实现

首先导入StoreKit.framework库

#import "ApplePayVC.h"
#import <StoreKit/StoreKit.h>

//在内购项目中创的商品单号,从itunesConnect里可以看到
#define ProductID_1 @"product1"
#define ProductID_2 @"product2" 
#define ProductID_3 @"product3" 
#define ProductID_4 @"product4"
#define ProductID_5 @"product5"

@interface ApplePayVC ()
{
    NSString *buyProductId;
}
@end

@implementation ApplePayVC

- (void)viewDidLoad {
    
    [super viewDidLoad];
    
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    [self buyProduct:ProductID_1];
    
}

-(void)buyProduct:(NSString *)productId
{
    buyProductId = productId;
    if ([SKPaymentQueue canMakePayments]) {
        [self RequestProductData];
        NSLog(@"允许程序内付费购买");
    }
    else
    {
        NSLog(@"不允许程序内付费购买");
        UIAlertView *alerView =  [[UIAlertView alloc] initWithTitle:@"提示"
                                                            message:@"您的手机没有打开程序内付费购买"
                                                           delegate:nil cancelButtonTitle:NSLocalizedString(@"关闭",nil) otherButtonTitles:nil];
        
        [alerView show];
        
    }
}

-(void)RequestProductData
{
    NSLog(@"---------请求对应的产品信息------------");
    NSArray *product = [[NSArray alloc] initWithObjects:buyProductId,nil];
    NSSet *nsset = [NSSet setWithArray:product];
    SKProductsRequest *request=[[SKProductsRequest alloc] initWithProductIdentifiers: nsset];
    request.delegate=self;
    [request start];
    
}

//<SKProductsRequestDelegate> 请求协议
//收到的产品信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    
    NSLog(@"-----------收到产品反馈信息--------------");
    NSArray *myProduct = response.products;
    NSLog(@"产品Product ID:%@",response.invalidProductIdentifiers);
    NSLog(@"产品付费数量: %d", (int)[myProduct count]);
    // populate UI
    for(SKProduct *product in myProduct){
        NSLog(@"product info");
        NSLog(@"SKProduct 描述信息%@", [product description]);
        NSLog(@"产品标题 %@" , product.localizedTitle);
        NSLog(@"产品描述信息: %@" , product.localizedDescription);
        NSLog(@"价格: %@" , product.price);
        NSLog(@"Product id: %@" , product.productIdentifier);
    }
    SKPayment *payment = [SKPayment paymentWithProductIdentifier:buyProductId];
 
    NSLog(@"---------发送购买请求------------");
    [[SKPaymentQueue defaultQueue] addPayment:payment];
    
}
- (void)requestProUpgradeProductData
{
    NSLog(@"------请求升级数据---------");
    NSSet *productIdentifiers = [NSSet setWithObject:@"com.productid"];
    SKProductsRequest* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
    productsRequest.delegate = self;
    [productsRequest start];
    
}
//弹出错误信息
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    NSLog(@"-------弹出错误信息----------");
    UIAlertView *alerView =  [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Alert",NULL) message:[error localizedDescription]
                                                       delegate:nil cancelButtonTitle:NSLocalizedString(@"Close",nil) otherButtonTitles:nil];
    [alerView show];
    
}

-(void) requestDidFinish:(SKRequest *)request
{
    NSLog(@"----------反馈信息结束--------------");
    
}

-(void) PurchasedTransaction: (SKPaymentTransaction *)transaction{
    NSLog(@"-----PurchasedTransaction----");
    NSArray *transactions =[[NSArray alloc] initWithObjects:transaction, nil];
    [self paymentQueue:[SKPaymentQueue defaultQueue] updatedTransactions:transactions];
}

//<SKPaymentTransactionObserver> 千万不要忘记绑定,代码如下:
//----监听购买结果
//[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions//交易结果
{
    NSLog(@"-----paymentQueue--------");
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased:{//交易完成
                [self completeTransaction:transaction];
                NSLog(@"-----交易完成 --------");
                
                UIAlertView *alerView =  [[UIAlertView alloc] initWithTitle:@""
                                                                    message:@"购买成功"
                                                                   delegate:nil cancelButtonTitle:NSLocalizedString(@"关闭",nil) otherButtonTitles:nil];
                
                [alerView show];
                
            } break;
            case SKPaymentTransactionStateFailed://交易失败
            { [self failedTransaction:transaction];
                NSLog(@"-----交易失败 --------");
                UIAlertView *alerView2 =  [[UIAlertView alloc] initWithTitle:@"提示"
                                                                     message:@"购买失败,请重新尝试购买"
                                                                    delegate:nil cancelButtonTitle:NSLocalizedString(@"关闭",nil) otherButtonTitles:nil];
                
                [alerView2 show];
                
            }break;
            case SKPaymentTransactionStateRestored://已经购买过该商品
                [self restoreTransaction:transaction];
                NSLog(@"-----已经购买过该商品 --------");
            case SKPaymentTransactionStatePurchasing:      //商品添加进列表
                NSLog(@"-----商品添加进列表 --------");
                break;
            default:
                break;
        }
    }
}
- (void) completeTransaction: (SKPaymentTransaction *)transaction

{
    NSLog(@"-----completeTransaction--------");
    // Your application should implement these two methods.
    NSString *product = transaction.payment.productIdentifier;
    if ([product length] > 0) {
        
        NSArray *tt = [product componentsSeparatedByString:@"."];
        NSString *bookid = [tt lastObject];
        if ([bookid length] > 0) {
            [self recordTransaction:bookid];
            [self provideContent:bookid];
        }
    }
    
    // Remove the transaction from the payment queue.
    
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    
}

//记录交易
-(void)recordTransaction:(NSString *)product{
    NSLog(@"-----记录交易--------");
}

//处理下载内容
-(void)provideContent:(NSString *)product{
    NSLog(@"-----下载--------");
}

- (void) failedTransaction: (SKPaymentTransaction *)transaction{
    NSLog(@"失败");
    if (transaction.error.code != SKErrorPaymentCancelled)
    {
        
    }
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
    
}
-(void) paymentQueueRestoreCompletedTransactionsFinished: (SKPaymentTransaction *)transaction{
    
}

- (void) restoreTransaction: (SKPaymentTransaction *)transaction
{
    NSLog(@" 交易恢复处理");
    
}

-(void) paymentQueue:(SKPaymentQueue *) paymentQueue restoreCompletedTransactionsFailedWithError:(NSError *)error{
    NSLog(@"-------paymentQueue----");
}

#pragma mark connection delegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    NSLog(@"%@",  [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
    
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    switch([(NSHTTPURLResponse *)response statusCode]) {
        case 200:
        case 206:
            break;
        case 304:
            break;
        case 400:
            break;
        case 404:
            break;
        case 416:
            break;
        case 403:
            break;
        case 401:
        case 500:
            break;
        default:
            break;
    }
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"test");
}

-(void)dealloc
{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];//解除监听
    
}

@end
我们已经完成了内购的付款操作了,至于如何给到用户商品就在
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
这个代理方法里面就行操作

你以为这样就完成了么?那你就惨了,APP上线以后你就会发现各种掉单问题,那时你心中肯定有10000只草泥马在奔腾,接下来我们来看看如何避免掉单。

附:解决掉单篇

我们先来看看有哪些请况会发生掉单:

①. 在ApplePay付款成功后由于网络或各种原因没有返回Transaction(SKPaymentTransaction),从而不能得到凭证去Apple服务器验证订单的正确性。
②.苹果服务器成功返回了Transaction,但是在APP在上传凭证给服务器时发生了网络或各种原因,造成了凭证的丢失,产生了掉单(用户付了款却没有得到相应的商品)

[SKPaymentQueue defaultQueue]这个队列里面存着所有的已支付,未支付的订单,而且需要手动移除,而APP每次启动的时候都会去判断这个队列里面是否为空,如果不为空的话会调用<SKPaymentTransactionObserver>代理的

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions//交易结果

所以我们可以把AppDelegate设置成这个协议的代理并实现这个方法,当然我一般是会写一个遵循<SKPaymentTransactionObserver>的工具类单例,毕竟协议是一对一的,不管是哪里的支付回调,都只走这个类,统一处理。

上面我们说到每次APP启动时都会判断订单队列是否为空,而且队列需要手动移除,所以我们可以在确保商品已经成功发放到用户手中再做移除操作,这样就完美了。

移除代码如下:

[[SKPaymentQueue defaultQueue] finishTransaction: transaction];

iOS 7.0后 我们是用[[NSBundle mainBundle] appStoreReceiptURL]来获取凭证。

注:苹果官方内购验证文档
https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1

到这里可能有些童鞋会懵逼了,他会说我付款后怎么和订单关联上啊,完全没有区分的地方啊!
解决办法如下:当我们创建苹果订单初始化SKPayment时我们应该使用SKMutablePayment,这个类里面有一个参数叫applicationUsername的成员变量,我们可以把后台服务器的订单号写到这里,在付款成功后返回的SKPaymentTransaction里面能拿到这个参数,然后就带着它去请求本地服务器.

我们把内购搭建好直接进行测试,会提示你购买失败对吧?内购测试我们要到iTunes connection 里去添加沙盒测试员

1.png

2.png

3.png

然后我们测试的时候换上这个appleId就能进行测试了

perfect!!!!!!
哈哈,第一次写技术博客可能写得不好,猿媛们哪里不明白可以在下面提问!!!

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

推荐阅读更多精彩内容