在iOS開發中如果涉及到虛擬物品的購買,就需要使用IAP服務,我們今天來看看如何實現。
在實現代碼之前我們先做一些准備工作,一步步來看。
IAP流程分為兩種,一種是直接使用Apple的服務器進行購買和驗證,另一種就是自己假設服務器進行驗證。由於國內網絡連接Apple服務器驗證非常慢,而且也為了防止黑客偽造購買憑證,通用做法是自己架設服務器進行驗證。
下面我們通過圖來看看兩種方式的差別:
簡單說下第二中情況的流程:
用戶進入購買虛擬物品頁面,App從後台服務器獲取產品列表然後顯示給用戶 用戶點擊購買購買某一個虛擬物品,APP就發送該虛擬物品的productionIdentifier到Apple服務器 Apple服務器根據APP發送過來的productionIdentifier返回相應的物品的信息(描述,價格等) 用戶點擊確認鍵購買該物品,購買請求發送到Apple服務器 Apple服務器完成購買後,返回用戶一個完成購買的憑證 APP發送這個憑證到後台服務器驗證 後台服務器把這個憑證發送到Apple驗證,Apple返回一個字段給後台服務器表明該憑證是否有效 後台服務器把驗證結果在發送到APP,APP根據驗證結果做相應的處理
搞清楚了自己架設服務器是如何完成IAP購買的流程了之後,我們下一步就是登錄到iTunes Connet創建應用和指定虛擬物品價格表
如下圖所示,我們需要創建一個自己的APP,要注意的是這裡的Bundle ID一定要跟你的項目中的info.plist中的Bundle ID保證一致。也就是圖中紅框部分。
消耗品(Consumable products):比如游戲內金幣等。
不可消耗品(Non-consumable products):簡單來說就是一次購買,終身可用(用戶可隨時從App Store restore)。
自動更新訂閱品(Auto-renewable subscriptions):和不可消耗品的不同點是有失效時間。比如一整年的付費周刊。在這種模式下,開發者定期投遞內容,用戶在訂閱期內隨時可以訪問這些內容。訂閱快要過期時,系統將自動更新訂閱(如果用戶同意)。
非自動更新訂閱品(Non-renewable subscriptions):一般使用場景是從用戶從IAP購買後,購買信息存放在自己的開發者服務器上。失效日期/可用是由開發者服務器自行控制的,而非由App Store控制,這一點與自動更新訂閱品有差異。
免費訂閱品(Free subscriptions):在Newsstand中放置免費訂閱的一種方式。免費訂閱永不過期。只能用於Newsstand-enabled apps。
類型2、3、5都是以Apple ID為粒度的。比如小張有三個iPad,有一個Apple ID購買了不可消耗品,則三個iPad上都可以使用。
類型1、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];
}
}
至此,整個流程結束,有任何疑問歡迎大家留言
http://openfibers.github.io/blog/2015/02/28/in-app-purchase-walk-through/
http://www.himigame.com/iphone-cocos2d/550.html
http://blog.devtang.com/2012/12/09/in-app-purchase-check-list/
http://yarin.blog.51cto.com/1130898/549141
更多技術文章,歡迎大家訪問我的技術博客:http://blog.ximu.site