iOS 内购详解及遇到的坑

前言

本文主要集中于代码实现,关于建立商品网上已经有不少了,就不说了,比较简单。git

以前作过 消耗性 和 非续订型 内购,代码里一直都是这两种。最近有个新需求,须要续订型 VIP。如今项目里有三种内购类型的产品了,嗯...github

 

流程与代码

流程图服务器

 

下面这句代码应该在程序入口写,这样写的好处是,若是有未完成的payment,进入程序后会继续走下去。而若是是在特定页面写,只有进入到这个页面才会继续走内购流程。app

    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];ide

 

按照流程图优化

第一步:请求产品列表atom

是在进入内购页面后,去咱们服务器请求spa

   [self requestProductInfo];代理

 

第二步:服务器返回 产品ID 列表rest

  成功拿到一系列产品ID

 

第三步:去苹果后台请求详细的产品信息(也可使用咱们服务器的信息,不去请求苹果上的信息)

[[IAPManager getInstance] requestProductsInfo:array];

//请求商品信息的代码
- (void)requestProductsInfo:(NSArray*)prodIds
{
    SKProductsRequest* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:prodIds]];
    productsRequest.delegate = self;
    [productsRequest start];
}

 

第四步:苹果后台返回详细的产品信息

//在代理方法中,返回详细的商品信息

#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    if (self.delegate && [self.delegate respondsToSelector:@selector(didReceiveProductsResponse:)])
        [self.delegate didReceiveProductsResponse:response.products];
}

 

 

第五步:展现产品

- (void)didReceiveProductsResponse:(NSArray *)array {

    if (array != nil && array.count > 0)
    {

   //能够先按价格排个序
        NSSortDescriptor* sortDes = [[NSSortDescriptor alloc] initWithKey:@"price.doubleValue" ascending:YES];
        NSArray *sortArray = [array sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDes]];
        _dataArray = [sortArray mutableCopy];
        [tableview reloadData];
    }

}

 

这里有一个产品价格本地化

使用 NSNumberFormatter *_numberFormatter;

_numberFormatter = [[NSNumberFormatter alloc] init];
[_numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[_numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];


- (void)updatePrice {

  [_numberFormatter setLocale:product.priceLocale];

     NSString *priceStr = [_numberFormatter stringFromNumber:price];

  //priceStr 就是正确的当地价格(例如 Rs290,¥10 等)

}

 

第六步:用户点击购买

点击某一产品购买时,应先判断

  [SKPaymentQueue canMakePayments]   若是是 YES,继续

 

第六点五步:这里咱们的流程略有差别,咱们先去本身服务器下单,拿到一个订单号

 

第七步:发送 payment 请求

下单后,这里把订单号设置进来;苹果会透传过来,确认订单时,须要使用到订单号。

SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = count;
payment.applicationUsername = orderId;
[[SKPaymentQueue defaultQueue] addPayment:payment];

这时,会有弹框出现,须要用户输入帐户密码购买产品,将从这个帐户绑定的卡上扣钱,咱们无需作什么。

 

第八步:代理方法中返回结果

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions)
    {
        [self onTransactionCompleted:transaction];
    }
}

//根据状态作相应的操做

- (void)onTransactionCompleted:(SKPaymentTransaction *)transaction
{
    switch (transaction.transactionState)
    {
        case SKPaymentTransactionStatePurchased:
            [self completeTransaction:transaction];
            break;
        case SKPaymentTransactionStateFailed:
            [self failedTransaction:transaction];
            break;
        case SKPaymentTransactionStateRestored:
            [self restoreTransaction:transaction];     //订阅型和非消耗型的商品才有恢复状态
            break;
        default:
            break;
    }
}

 

第九步:成功支付的,会有收据,transaction 的状态是 SKPaymentTransactionStatePurchased

须要把必要信息发送给咱们服务器

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

     //这里就是第七步中设置进去的订单号,正常状况下,苹果会透传过来;偶尔的没有透传嘛,就是个坑了。填坑中会有解决方法。 

     NSString* orderId = transaction.payment.applicationUsername;  

     NSString* transactionId = transaction.transactionIdentifier;
     NSString* transactionReceipt = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];    //收据
     NSInteger count = transaction.payment.quantity;

     //把这些信息传给后台 
     ...
     //[self sendToServer:(id)data];

}

以后 APP端就等着服务器返回便可

 

第十四步:成功返回

    服务器成功返回后,要关掉此次交易,这一步很是重要。

    [self finishTransaction:transaction];

    没有成功返回的,不要关掉。有多是咱们的服务器在某个时刻,没有响应,或者返回了错误,这时候交易还在队列中,下次打开APP,

          [[SKPaymentQueue defaultQueue] addTransactionObserver:self];      //因此这句写在了程序入口处

    以后,会从新触发代理方法:即从第八步再走一遍

     - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions

   


 

下面开始填坑

坑一:肯定订单时,没有订单号

通过线上运营一段时间,发现会出现少许的掉单状况。对于这类状况,能提供收据的,基本都是手动补发产品了。以后针对这类状况,优化了代码,就不多有掉单状况了。

经研究发现,咱们的掉单,发生在第九步,去咱们服务器确认时,苹果没有把 订单号 发过来。订单号为空,天然找不到对应的订单了。

因而,在下单成功后,先把订单保存在本地。去确认订单时,若是没有订单号,就从本地拿一下,再去确认;确认成功后,删除对应订单号。

- (void)purchaseProduct:(SKProduct*)product count:(int)count order:(NSString*)orderId
{
    {
        // 暂存最后一次支付订单的数据
        [self saveDataWithProductIdentifier:product.productIdentifier orderId:orderId];
    }
    
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
    payment.quantity = count;
    payment.applicationUsername = orderId;
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
    NSString* orderId = transaction.payment.applicationUsername;
    
    if (orderId == nil) {
     //若是没有订单号,本地取一下订单号
        orderId = [self getOrderIdWithProductIdentifier:transaction.payment.productIdentifier];
    }
    
    BOOL bVIP = [self isVipTransaction:transaction];
    NSString* transactionId = transaction.transactionIdentifier;
    NSString* transactionReceipt = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];
    NSInteger count = transaction.payment.quantity;
...
}

- (void)finishTransaction:(SKPaymentTransaction *)transaction
{
//移除订单号
[self removeOrderIdWithProductIdentifier:transaction.payment.productIdentifier];
    // remove the transaction from the payment queue.
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

//对应产品ID,保存订单号
- (void)saveDataWithProductIdentifier:(NSString *)identifier orderId:(NSString *)orderId {
    if (!identifier || !orderId) {
        return;
    }
    
    NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *path = [pathArray objectAtIndex:0];
    NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path];
    
    NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
    if (!dic) {
        dic = [NSMutableDictionary dictionary];
    }
    
    [dic setValue:orderId forKey:identifier];
    
    BOOL flag = [dic writeToFile:filePath atomically:YES];
    if(!flag) {
        NSLog(@"orderId保存失败");
    }
}


//获取某一订单号
- (NSString *)getOrderIdWithProductIdentifier:(NSString *)productIdentifier {
    if (!productIdentifier) {
        return nil;
    }
    
    NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *path = [pathArray objectAtIndex:0];
    NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path];
    
    NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
    
    return [dic valueForKey:productIdentifier];
}


//成功后删除对应订单号
- (void)removeOrderIdWithProductIdentifier:(NSString *)productIdentifier {
    if (!productIdentifier) {
        return;
    }
    
    NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *path = [pathArray objectAtIndex:0];
    NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path];
    
    NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
    
    [dic removeObjectForKey:productIdentifier];
    
    BOOL flag = [dic writeToFile:filePath atomically:YES];
    if(!flag) {
        NSLog(@"orderId从新保存失败");
    }
}

 

其余多为业务逻辑,欢迎各位留言交流

代码地址:https://github.com/lionwhitcher/InAppPurchase

相关文章
相关标签/搜索