在iOS開發中若是涉及到虛擬物品的購買,就須要使用IAP服務,咱們今天來看看如何實現。html
在實現代碼以前咱們先作一些準備工做,一步步來看。ios
IAP流程分爲兩種,一種是直接使用Apple的服務器進行購買和驗證,另外一種就是本身假設服務器進行驗證。因爲國內網絡鏈接Apple服務器驗證很是慢,並且也爲了防止黑客僞造購買憑證,通用作法是本身架設服務器進行驗證。git
下面咱們經過圖來看看兩種方式的差異:github
簡單說下第二中狀況的流程:服務器
搞清楚了本身架設服務器是如何完成IAP購買的流程了以後,咱們下一步就是登陸到iTunes Connet建立應用和指定虛擬物品價格表網絡
以下圖所示,咱們須要建立一個本身的APP,要注意的是這裏的Bundle ID必定要跟你的項目中的info.plist中的Bundle ID保證一致。也就是圖中紅框部分。app
消耗品(Consumable products):好比遊戲內金幣等。dom
不可消耗品(Non-consumable products):簡單來講就是一次購買,終身可用(用戶可隨時從App Store restore)。iphone
自動更新訂閱品(Auto-renewable subscriptions):和不可消耗品的不一樣點是有失效時間。好比一全年的付費週刊。在這種模式下,開發者按期投遞內容,用戶在訂閱期內隨時能夠訪問這些內容。訂閱快要過時時,系統將自動更新訂閱(若是用戶贊成)。ide
非自動更新訂閱品(Non-renewable subscriptions):通常使用場景是從用戶從IAP購買後,購買信息存放在本身的開發者服務器上。失效日期/可用是由開發者服務器自行控制的,而非由App Store控制,這一點與自動更新訂閱品有差別。
免費訂閱品(Free subscriptions):在Newsstand中放置免費訂閱的一種方式。免費訂閱永不過時。只能用於Newsstand-enabled apps。
類型二、三、5都是以Apple ID爲粒度的。好比小張有三個iPad,有一個Apple ID購買了不可消耗品,則三個iPad上均可以使用。
類型一、4通常來講則是現買現用。若是開發者本身想作更多控制,通常選4
其中產品id是字母或者數字,或者二者的組合,用於惟一表示該虛擬物品,app也是經過請求產品id來從apple服務器獲取虛擬物品信息的。
這一步必須設置,否則是沒法從apple獲取虛擬產品信息。
設置成功後以下所示:
更多關於iTunes Connet的操做請纔看這篇博文http://openfibers.github.io/blog/2015/02/28/in-app-purchase-walk-through/
完成了上面的準備工做,咱們就能夠開始着手IAP的代碼實現了。
咱們假設你已經完成了從後臺服務器獲取虛擬物品列表這一步操做了,這一步後臺服務器還會返回每一個虛擬物品所對應的productionIdentifier,假設你也獲取到了,並保存在屬性self.productIdent中。
須要在工程中引入 storekit.framework。
咱們來看看後續如何實現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:@"用戶禁止應用內付費購買"]; } }
若是用戶容許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]]; }
//購買操做後的回調 - (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]; }
在這一步咱們須要向服務器驗證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]; } }
若是出現網絡問題,致使沒法驗證。咱們須要持久化保存購買憑證,在用戶下次啓動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]; }
- (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]; } }
至此,整個流程結束,有任何疑問歡迎你們留言