iOS應用內支付(IAP)詳解

来源:http://www.cnblogs.com/XimuYouzi/archive/2016/04/17/5401749.html
-Advertisement-
Play Games

在iOS開發中如果涉及到虛擬物品的購買,就需要使用IAP服務,我們今天來看看如何實現。 在實現代碼之前我們先做一些準備工作,一步步來看。 1、IAP流程 IAP流程分為兩種,一種是直接使用Apple的伺服器進行購買和驗證,另一種就是自己假設伺服器進行驗證。由於國內網路連接Apple伺服器驗證非常慢, ...


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

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


1、IAP流程

IAP流程分為兩種,一種是直接使用Apple的伺服器進行購買和驗證,另一種就是自己假設伺服器進行驗證。由於國內網路連接Apple伺服器驗證非常慢,而且也為了防止黑客偽造購買憑證,通用做法是自己架設伺服器進行驗證。

下麵我們通過圖來看看兩種方式的差別:

1.1、使用Apple伺服器

image

1.2、自己架設伺服器

image

簡單說下第二中情況的流程:

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

2、iTunes Connet操作

搞清楚了自己架設伺服器是如何完成IAP購買的流程了之後,我們下一步就是登錄到iTunes Connet創建應用和指定虛擬物品價格表

2.1、創建自己的App

如下圖所示,我們需要創建一個自己的APP,要註意的是這裡的Bundle ID一定要跟你的項目中的info.plist中的Bundle ID保證一致。也就是圖中紅框部分。

image

2.2、創建虛擬物品價格表
2.2.1、虛擬物品分為如下幾種:
  1. 消耗品(Consumable products):比如游戲內金幣等。

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

  3. 自動更新訂閱品(Auto-renewable subscriptions):和不可消耗品的不同點是有失效時間。比如一整年的付費周刊。在這種模式下,開發者定期投遞內容,用戶在訂閱期內隨時可以訪問這些內容。訂閱快要過期時,系統將自動更新訂閱(如果用戶同意)。

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

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

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

類型1、4一般來說則是現買現用。如果開發者自己想做更多控制,一般選4

2.2.2、創建成功後如下所示:

image

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

2.3、設置稅務和銀行卡信息

這一步必須設置,不然是無法從apple獲取虛擬產品信息。

設置成功後如下所示:

image

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


3、iOS端具體代碼實現

完成了上面的準備工作,我們就可以開始著手IAP的代碼實現了。

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

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

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

3.1、確認用戶是否允許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.2、發起購買操作

如果用戶允許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.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.4、向伺服器端驗證購買憑證的有效性

在這一步我們需要向伺服器驗證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.5、發送憑證失敗的處理

如果出現網路問題,導致無法驗證。我們需要持久化保存購買憑證,在用戶下次啟動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.6、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



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、volley 項目地址 https://github.com/smanikandan14/Volley-demo (1) JSON,圖像等的非同步下載;(2) 網路請求的排序(scheduling)(3) 網路請求的優先順序處理(4) 緩存(5) 多級別取消請求(6) 和Activity和生命周期的 ...
  • 字典是一種存儲相同類型多重數據的存儲器。每個值(value)都關聯獨特的鍵(key),鍵作為字典中的這個值數據的標識符。和數組中的數據項不同,字典中的數據項並沒有具體順序。 字典寫作Dictionary<Key, Value>。也可以寫作[Key: Value] 創建空字典 類型推斷寫作[:] 創建 ...
  • 預覽: 需要許可權: 1 <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> 配置文件:AndroidManifest.xml 在應用的閃屏頁面Activity的 oncreate方法調 ...
  • 一個Activity 對應 多個Fragment; 每一個類 extends Fragment , 一個Activity 可以同時顯示多個 Fragment; ...
  • 1、關於代理對象的設計小技巧 在設計一個類,需要通過代理和協議來從外部獲取需要的動態的數據。那麼在這裡設計使用代理會有兩種方法。 <第一種方法> 也是比較常見的: 在你設計的類中,聲明一個代理屬性 然後外部使用的時候 最後根據那個<...Protocol>協議,去遵循這個協議並實現協議的方法。 <第 ...
  • 本文轉載自345大神。。。。 "查看原文" 先上個圖形化界面GIT工具 Git 常用命令 git clone git remote git fetch git pull git push 1. git clone 遠程操作的第一步,通常是從遠程主機克隆一個版本庫,這時就要用到git clone命令。 ...
  • 每建一個Activity都要註冊許可權Manifest.xml但是有時候自動註冊好了,註意!不然的話是不能調用的!!!!!<activity android:name=".MainView"></activity>安卓Fragment的調用不用註冊! ...
  • 實際上,我們要做的工作是根據內核的Program header table的信息進行類似下麵這個C語言語句的記憶體複製: memcpy(p_vaddr, BaseOfLoaderPhyAddr+p_offset, p_filesz); 複製可能不止一次,如果Program header有n個,複製就進 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...