iOS內購(iap)總結

剛剛做了內購, 記錄一下
這里直接上代碼, 至于寫代碼之前的一些設置工作參考以下文章:
http://www.lxweimin.com/p/690a7c68664e
http://www.lxweimin.com/p/86ac7d3b593a

需要注意的是:

  1. 只要工程配置了對應的證書, 就能請求商品信息, 不需要任何其他處理
  2. 沙盒測試填寫的郵箱不能是已經(jīng)綁定appleID的郵箱, 也不能是AppleID的救援郵箱, 其他的無所謂, 其實, 哪怕你填寫的郵箱不存在也沒有關系
//
//  IAPManager.m
//  SpeakEnglish
//
//  Created by Daniel on 16/6/8.
//  Copyright ? 2016年 Daniel. All rights reserved.
//

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

@interface IAPManager ()<SKPaymentTransactionObserver, SKProductsRequestDelegate>
// 所有商品
@property (nonatomic, strong)NSArray *products;
@property (nonatomic, strong)SKProductsRequest *request;
@end

static IAPManager *manager = nil;

@implementation IAPManager

+ (instancetype)shareIAPManager {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [self new];
        [[SKPaymentQueue defaultQueue] addTransactionObserver:manager];
    });
    return manager;
}

- (void)dealloc {
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

// 請求可賣的商品
- (void)requestProducts
{
    if (![SKPaymentQueue canMakePayments]) {
        // 您的手機沒有打開程序內付費購買
        return;
    }
    
    // 1.請求所有的商品ID
    NSString *productFilePath = [[NSBundle mainBundle] pathForResource:@"iapdemo.plist" ofType:nil];
    NSArray *products = [NSArray arrayWithContentsOfFile:productFilePath];
    
    // 2.獲取所有的productid
     NSArray *productIds = [products valueForKeyPath:@"productId"];
    
    // 3.獲取productid的set(集合中)
    NSSet *set = [NSSet setWithArray:productIds];
    
    // 4.向蘋果發(fā)送請求,請求可賣商品
    _request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
    _request.delegate = self;
    [_request start];
}

/**
 *  當請求到可賣商品的結果會執(zhí)行該方法
 *
 *  @param response response中存儲了可賣商品的結果
 */
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
     for (SKProduct *product in response.products) {

    // 用來保存價格
    NSMutableDictionary *priceDic = @{}.mutableCopy;
    // 貨幣單位
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
    [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
    [numberFormatter setLocale:product.priceLocale];
    // 帶有貨幣單位的價格
    NSString *formattedPrice = [numberFormatter stringFromNumber:product.price];
        [priceDic setObject:formattedPrice forKey:product.productIdentifier];

     NSLog(@"價格:%@", product.price);
     NSLog(@"標題:%@", product.localizedTitle);
     NSLog(@"秒速:%@", product.localizedDescription);
     NSLog(@"productid:%@", product.productIdentifier);
     }
     
    // 保存價格列表
    [[NSUserDefaults standardUserDefaults] setObject:priceDic forKey:@"priceDic"];
    [[NSUserDefaults standardUserDefaults] synchronize];
    
    // 1.存儲所有的數(shù)據(jù)
    self.products = response.products;
    self.products = [self.products sortedArrayWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(SKProduct *obj1, SKProduct *obj2) {
        return [obj1.price compare:obj2.price];
    }];
}

#pragma mark - 購買商品
- (void)buyProduct:(SKProduct *)product
{
    // 1.創(chuàng)建票據(jù)
    SKPayment *payment = [SKPayment paymentWithProduct:product];
    WELog(@"productIdentifier----%@", payment.productIdentifier);
    
    // 2.將票據(jù)加入到交易隊列中
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

#pragma mark - 實現(xiàn)觀察者回調的方法
/**
 *  當交易隊列中的交易狀態(tài)發(fā)生改變的時候會執(zhí)行該方法
 *
 *  @param transactions 數(shù)組中存放了所有的交易
 */
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    /*
     SKPaymentTransactionStatePurchasing, 正在購買
     SKPaymentTransactionStatePurchased, 購買完成(銷毀交易)
     SKPaymentTransactionStateFailed, 購買失敗(銷毀交易)
     SKPaymentTransactionStateRestored, 恢復購買(銷毀交易)
     SKPaymentTransactionStateDeferred 最終狀態(tài)未確定
     */
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                WELog(@"用戶正在購買");
                break;
                
            case SKPaymentTransactionStatePurchased:
                WELog(@"productIdentifier----->%@", transaction.payment.productIdentifier);
                [self buySuccessWithPaymentQueue:queue Transaction:transaction];
                break;
                
            case SKPaymentTransactionStateFailed:
                NSLog(@"購買失敗");
                [queue finishTransaction:transaction];
                break;
                
            case SKPaymentTransactionStateRestored:
                NSLog(@"恢復購買");
                [queue finishTransaction:transaction];
                break;
                
            case SKPaymentTransactionStateDeferred:
                NSLog(@"最終狀態(tài)未確定");
                break;
                
            default:
                break;
        }
    }
}

- (void)buySuccessWithPaymentQueue:(SKPaymentQueue *)queue Transaction:(SKPaymentTransaction *)transaction {
    
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    NSDictionary *params = @{@"user_id":@"user_id",
                             // 獲取商品
                             @"goods":[self goodsWithProductIdentifier:transaction.payment.productIdentifier]};
    
    [manager POST:@"url" parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
        
        if ([responseObject[@"code"] intValue] == 200) {
            
            // 防止丟單, 必須在服務器確定后從交易隊列刪除交易
            // 如果不從交易隊列上刪除交易, 下次調用addTransactionObserver:, 仍然會回調'updatedTransactions'方法, 以此處理丟單
            
            WELog(@"購買成功");
            [queue finishTransaction:transaction];
        }
        
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        
    }];
}

// 商品列表 也可以使用從蘋果請求的數(shù)據(jù), 具體細節(jié)自己視情況處理
// goods1 是商品的ID
- (NSString *)goodsWithProductIdentifier:(NSString *)productIdentifier {
    NSDictionary *goodsDic = [[NSUserDefaults standardUserDefaults] objectForKey:@"priceDic"];
    return goodsDic[productIdentifier];
}

// 恢復購買
- (void)restore
{
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
    // 恢復失敗
    WELog(@"恢復失敗");
}

// 取消請求商品信息
- (void)dealloc {
    [_request cancel];
}
@end

對于丟單的處理, 這里利用蘋果自帶的機制, 即如果不調用'finishTransaction'方法, 下次調用[[SKPaymentQueue defaultQueue] addTransactionObserver:self]后會再次回調'- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions'方法, 所以在向自己的服務器確認了交易成功后再調用'finishTransaction'方法.
這樣做一定程度上可以解決丟單的狀況, 但是好像還有問題, 很多人都覺得應該做本地化才能更好地防止丟單, 常見的方法是:

  1. 創(chuàng)建票據(jù)時使用SKMutablePayment并設置applicationUsername參數(shù), 方便后臺區(qū)分用戶, 給用戶發(fā)商品
  2. 在'- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions' 方法中, 當transactionState == SKPaymentTransactionStateRestored時保存票據(jù)到本地.
    獲取票據(jù)的方式有兩種。一種是直接獲取SKPaymentTransaction里面的屬性transactionReceipt。這種方式,在iOS7已經(jīng)廢棄了,到iOS9停用。但是為了兼容舊機型,最好還是加上這個方式。
  3. 每次用戶登錄時, 發(fā)送用戶未驗證的票據(jù)到服務器, 然后服務器再到AppStore驗證, 根據(jù)AppStore返回的結果處理交易, 客戶端視情況刪除保存的票據(jù)

思考:
(1)如果考慮到用戶更換手機的情況, 還是傳到服務器比較安全...但是如果能夠將數(shù)據(jù)上傳到服務器, 那么同樣也應該可以告訴服務器交易成功并請求發(fā)放商品...感覺整個人都不好了.
(2)我感覺在transactionState == SKPaymentTransactionStateRestored才上傳數(shù)據(jù)最好, 假如你使用你朋友的手機購買商品并且沒有馬上交易成功并且退出了應用, 交易成功的回調是在
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];之后才會回調, 你很難保證你朋友在什么時候才會再次打開這個應用, 如果他把這個應用刪了, 就會造成丟單. 但可惜的是, 只有在transactionState == SKPaymentTransactionStateRestored才會有transactionReceipt, 就當我什么都沒說好了...
(3)仔細思考了下, 發(fā)現(xiàn)除了第一步之外, 其他步驟并沒有起到太多的作用, 而且不本地化也可以使用. 這樣看來, 好像并沒有做本地化的必要, 突然感受到了來自這個世界的惡意. 我倒是希望是我邏輯上有漏洞, 如果有誰發(fā)現(xiàn)了請告訴我, 先行謝過.

總結:
內購有三個可能出現(xiàn)的問題

  1. 支付成功后, 沒來得及向服務器發(fā)送交易成功的數(shù)據(jù)就退出應用, 導致丟單. 這個問題貌似不需要本地化數(shù)據(jù)也已經(jīng)沒問題了, 除非再次回調updatedTransactions方法時已經(jīng)拿不到票據(jù)了, 這樣才有必要本地存儲票據(jù).
  2. 無法綁定交易和對應的用戶. 因為applicationUsername的存在這已經(jīng)不是問題了.
  3. 只用別人的手機進行購買, 沒來得及向服務器發(fā)送交易成功的數(shù)據(jù)就退出應用, 導致丟單. 如果別人再也不打開這個應用甚至刪掉了, 目前看來, 沒有辦法解決

參考資料:

  1. 蘋果內購二次驗證 PHP代碼
    http://my.oschina.net/qianglong/blog/503861

  2. In-App Purchase Programming Guide
    https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267

  3. iPhone In App Purchase購買完成時驗證transactionReceipt
    http://www.cnblogs.com/eagley/archive/2011/06/15/2081577.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容