iOS應用內支付(IAP)詳解

在iOS開發中若是涉及到虛擬物品的購買,就須要使用IAP服務,咱們今天來看看如何實現。html

在實現代碼以前咱們先作一些準備工做,一步步來看。ios


一、IAP流程

IAP流程分爲兩種,一種是直接使用Apple的服務器進行購買和驗證,另外一種就是本身假設服務器進行驗證。因爲國內網絡鏈接Apple服務器驗證很是慢,並且也爲了防止黑客僞造購買憑證,通用作法是本身架設服務器進行驗證。git

下面咱們經過圖來看看兩種方式的差異:github

1.一、使用Apple服務器

image

1.二、本身架設服務器

image

簡單說下第二中狀況的流程:服務器

  1. 用戶進入購買虛擬物品頁面,App從後臺服務器獲取產品列表而後顯示給用戶
  2. 用戶點擊購買購買某一個虛擬物品,APP就發送該虛擬物品的productionIdentifier到Apple服務器
  3. Apple服務器根據APP發送過來的productionIdentifier返回相應的物品的信息(描述,價格等)
  4. 用戶點擊確認鍵購買該物品,購買請求發送到Apple服務器
  5. Apple服務器完成購買後,返回用戶一個完成購買的憑證
  6. APP發送這個憑證到後臺服務器驗證
  7. 後臺服務器把這個憑證發送到Apple驗證,Apple返回一個字段給後臺服務器代表該憑證是否有效
  8. 後臺服務器把驗證結果在發送到APP,APP根據驗證結果作相應的處理

二、iTunes Connet操做

搞清楚了本身架設服務器是如何完成IAP購買的流程了以後,咱們下一步就是登陸到iTunes Connet建立應用和指定虛擬物品價格表網絡

2.一、建立本身的App

以下圖所示,咱們須要建立一個本身的APP,要注意的是這裏的Bundle ID必定要跟你的項目中的info.plist中的Bundle ID保證一致。也就是圖中紅框部分。app

image

2.二、建立虛擬物品價格表
2.2.一、虛擬物品分爲以下幾種:
  1. 消耗品(Consumable products):好比遊戲內金幣等。dom

  2. 不可消耗品(Non-consumable products):簡單來講就是一次購買,終身可用(用戶可隨時從App Store restore)。iphone

  3. 自動更新訂閱品(Auto-renewable subscriptions):和不可消耗品的不一樣點是有失效時間。好比一全年的付費週刊。在這種模式下,開發者按期投遞內容,用戶在訂閱期內隨時能夠訪問這些內容。訂閱快要過時時,系統將自動更新訂閱(若是用戶贊成)。ide

  4. 非自動更新訂閱品(Non-renewable subscriptions):通常使用場景是從用戶從IAP購買後,購買信息存放在本身的開發者服務器上。失效日期/可用是由開發者服務器自行控制的,而非由App Store控制,這一點與自動更新訂閱品有差別。

  5. 免費訂閱品(Free subscriptions):在Newsstand中放置免費訂閱的一種方式。免費訂閱永不過時。只能用於Newsstand-enabled apps。

類型二、三、5都是以Apple ID爲粒度的。好比小張有三個iPad,有一個Apple ID購買了不可消耗品,則三個iPad上均可以使用。

類型一、4通常來講則是現買現用。若是開發者本身想作更多控制,通常選4

2.2.二、建立成功後以下所示:

image

其中產品id是字母或者數字,或者二者的組合,用於惟一表示該虛擬物品,app也是經過請求產品id來從apple服務器獲取虛擬物品信息的。

2.三、設置稅務和銀行卡信息

這一步必須設置,否則是沒法從apple獲取虛擬產品信息。

設置成功後以下所示:

image

更多關於iTunes Connet的操做請纔看這篇博文http://openfibers.github.io/blog/2015/02/28/in-app-purchase-walk-through/


三、iOS端具體代碼實現

完成了上面的準備工做,咱們就能夠開始着手IAP的代碼實現了。

咱們假設你已經完成了從後臺服務器獲取虛擬物品列表這一步操做了,這一步後臺服務器還會返回每一個虛擬物品所對應的productionIdentifier,假設你也獲取到了,並保存在屬性self.productIdent中。

須要在工程中引入 storekit.framework。

咱們來看看後續如何實現IAP

3.一、確認用戶是否容許IAP
//移除監聽
-(void)dealloc
{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

//添加監聽
- (void)viewDidLoad{
    [super viewDidLoad];
    [self.tableView.mj_header beginRefreshing];
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}

- (void)buyProdution:(UIButton *)sender{    
    if ([SKPaymentQueue canMakePayments]) {
        [self getProductInfo:self.productIdent];
    } else {
        [self showMessage:@"用戶禁止應用內付費購買"];
    }
}
3.二、發起購買操做

若是用戶容許IAP,那麼就能夠發起購買操做了

//從Apple查詢用戶點擊購買的產品的信息
- (void)getProductInfo:(NSString *)productIdentifier {
    NSArray *product = [[NSArray alloc] initWithObjects:productIdentifier, nil];
    NSSet *set = [NSSet setWithArray:product];
    SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
    request.delegate = self;
    [request start];
    [self showMessageManualHide:@"正在購買,請稍後"];
}

// 查詢成功後的回調
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    [self hideHUD];
    NSArray *myProduct = response.products;
    if (myProduct.count == 0) {
        [self showMessage:@"沒法獲取產品信息,請重試"];
        return;
    }
    SKPayment * payment = [SKPayment paymentWithProduct:myProduct[0]];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

//查詢失敗後的回調
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
    [self hideHUD];
    [self showMessage:[error localizedDescription]];
}
3.三、購買操做後的回調
//購買操做後的回調
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    [self hideHUD];
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased://交易完成
                self.receipt = [GTMBase64 stringByEncodingData:[NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]]];
                [self checkReceiptIsValid];//把self.receipt發送到服務器驗證是否有效
                [self completeTransaction:transaction];
                break;
                
            case SKPaymentTransactionStateFailed://交易失敗
                [self failedTransaction:transaction];
                break;
                
            case SKPaymentTransactionStateRestored://已經購買過該商品
                [self showMessage:@"恢復購買成功"];
                [self restoreTransaction:transaction];
                break;
                
            case SKPaymentTransactionStatePurchasing://商品添加進列表
                [self showMessage:@"正在請求付費信息,請稍後"];
                break;
                
            default:
                break;
        }
    }
    
}



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


- (void)failedTransaction:(SKPaymentTransaction *)transaction {
    if(transaction.error.code != SKErrorPaymentCancelled) {
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:@"購買失敗,請重試"delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"重試", nil];
        [alertView show];
    } else {
        [self showMessage:@"用戶取消交易"];
    }
    
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}


- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
3.四、向服務器端驗證購買憑證的有效性

在這一步咱們須要向服務器驗證Apple服務器返回的購買憑證的有效性,而後把驗證結果通知用戶

- (void)checkReceiptIsValid{

    AFHTTPSessionManager manager]GET:@"後臺服務器地址"  parameters::@"發送的參數(必須包括購買憑證)"
    success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
        if(憑證有效){
          你要作的事
        }else{//憑證無效
          你要作的事
        }
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:nil message:@"購買失敗,請重試"delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"重試", nil];
            [alertView show];
    }

}
3.五、發送憑證失敗的處理

若是出現網絡問題,致使沒法驗證。咱們須要持久化保存購買憑證,在用戶下次啓動APP的時候在後臺向服務器再一次發起驗證,直到成功而後移除該憑證。
保證以下define可在全局訪問:

#define AppStoreInfoLocalFilePath [NSString stringWithFormat:@"%@/%@/", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject],@"EACEF35FE363A75A"]
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == 0)
    {
        [self saveReceipt];
    }
    else
    {
        [self checkReceiptIsValid];
    }
}


//持久化存儲用戶購買憑證(這裏最好還要存儲當前日期,用戶id等信息,用於區分不一樣的憑證)
-(void)saveReceipt{
    NSString *fileName = [AppUtils getUUIDString];
    NSString *savedPath = [NSString stringWithFormat:@"%@%@.plist", AppStoreInfoLocalFilePath, fileName];
    
    NSDictionary *dic =[ NSDictionary dictionaryWithObjectsAndKeys:
                        self.receipt,                           Request_transactionReceipt,
                        self.date                               DATE                        
                        self.userId                             USERID
                        nil];
    
    [dic writeToFile:savedPath atomically:YES];
}
3.六、APP啓動後再次發送持久化存儲的購買憑證到後臺服務器
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    //從服務器驗證receipt失敗以後,在程序再次啓動的時候,使用保存的receipt再次到服務器驗證
    if (![fileManager fileExistsAtPath:AppStoreInfoLocalFilePath]) {//若是在改路下不存在文件,說明就沒有保存驗證失敗後的購買憑證,也就是說發送憑證成功。
        [fileManager createDirectoryAtPath:AppStoreInfoLocalFilePath//建立目錄
               withIntermediateDirectories:YES
                                attributes:nil
                                     error:nil];
    }
    else//存在購買憑證,說明發送憑證失敗,再次發起驗證
    {
        [self sendFailedIapFiles];
    }
}

//驗證receipt失敗,App啓動後再次驗證
- (void)sendFailedIapFiles{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error = nil;
    
    //搜索該目錄下的全部文件和目錄
    NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:AppStoreInfoLocalFilePath error:&error];
    
    if (error == nil)
    {
        for (NSString *name in cacheFileNameArray)
        {
            if ([name hasSuffix:@".plist"])//若是有plist後綴的文件,說明就是存儲的購買憑證
            {
                NSString *filePath = [NSString stringWithFormat:@"%@/%@", AppStoreInfoLocalFilePath, name];
                [self sendAppStoreRequestBuyPlist:filePath];
                
            }
        }
    }
    else
    {
        DebugLog(@"AppStoreInfoLocalFilePath error:%@", [error domain]);
    }
}

-(void)sendAppStoreRequestBuyPlist:(NSString *)plistPath
{
    NSString *path = [NSString stringWithFormat:@"%@%@", AppStoreInfoLocalFilePath, plistPath];
    NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:path];
    
    //這裏的參數請根據本身公司後臺服務器接口定製,可是必須發送的是持久化保存購買憑證
    NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys:
              [dic objectForKey:USERID],                           USERID,                    
              [dic objectForKey:DATE],                             DATE,                                                                                                         [dic objectForKey:Request_transactionReceipt],      Request_transactionReceipt,
                                                                       nil];
                            
                                                                       
        AFHTTPSessionManager manager]GET:@"後臺服務器地址"  parameters:params  success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
        if(憑證有效){
         [self removeReceipt]
        }else{//憑證無效
          你要作的事
        }
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                
    }
    
 }

//驗證成功就從plist中移除憑證
-(void)removeReceipt{
    [AppUtils removeIapFailedPath:AppStoreInfoLocalFilePath];
}

//AppUtils類方法,驗證成功,移除存儲的receipt
+ (void)removeIapFailedPath:(NSString *)plistPath{
    NSString *path = [NSString stringWithFormat:@"%@/%@", AppStoreInfoLocalFilePath, plistPath];
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:AppStoreInfoLocalFilePath])
    {
        [fileManager removeItemAtPath:AppStoreInfoLocalFilePath error:nil];
    }
    
    if ([fileManager fileExistsAtPath:path])
    {
        [fileManager removeItemAtPath:path error:nil];
    }
}

至此,整個流程結束,有任何疑問歡迎你們留言


參考:

  1. http://openfibers.github.io/blog/2015/02/28/in-app-purchase-walk-through/

  2. http://www.himigame.com/iphone-cocos2d/550.html

  3. http://blog.devtang.com/2012/12/09/in-app-purchase-check-list/

  4. http://yarin.blog.51cto.com/1130898/549141

  5. 更多技術文章,歡迎你們訪問個人技術博客:http://blog.ximu.site

相關文章
相關標籤/搜索