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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...