[紹棠] 應用內支付(IAP)詳解
1、IAP流程
IAP流程分為兩種,一種是直接使用Apple的伺服器進行購買和驗證,另一種就是自己假設伺服器進行驗證。由於國內網路連線Apple伺服器驗證非常慢,而且也為了防止黑客偽造購買憑證,通用做法是自己架設伺服器進行驗證。
下面我們通過圖來看看兩種方式的差別:
1.1、使用Apple伺服器
1.2、自己架設伺服器
簡單說下第二中情況的流程:
- 使用者進入購買虛擬物品頁面,App從後臺伺服器獲取產品列表然後顯示給使用者
- 使用者點選購買購買某一個虛擬物品,APP就傳送該虛擬物品的productionIdentifier到Apple伺服器
- Apple伺服器根據APP傳送過來的productionIdentifier返回相應的物品的資訊(描述,價格等)
- 使用者點選確認鍵購買該物品,購買請求傳送到Apple伺服器
- Apple伺服器完成購買後,返回使用者一個完成購買的憑證
- APP傳送這個憑證到後臺伺服器驗證
- 後臺伺服器把這個憑證傳送到Apple驗證,Apple返回一個欄位給後臺伺服器表明該憑證是否有效
- 後臺伺服器把驗證結果在傳送到APP,APP根據驗證結果做相應的處理
2、iTunes Connet操作
搞清楚了自己架設伺服器是如何完成IAP購買的流程了之後,我們下一步就是登入到iTunes Connet建立應用和指定虛擬物品價格表
2.1、建立自己的App
如下圖所示,我們需要建立一個自己的APP,要注意的是這裡的Bundle ID一定要跟你的專案中的info.plist中的Bundle ID保證一致。也就是圖中紅框部分。
2.2、建立虛擬物品價格表
2.2.1、虛擬物品分為如下幾種:
-
消耗品(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
2.2.2、建立成功後如下所示:
其中產品id是字母或者數字,或者兩者的組合,用於唯一表示該虛擬物品,app也是通過請求產品id來從apple伺服器獲取虛擬物品資訊的。
2.3、設定稅務和銀行卡資訊
這一步必須設定,不然是無法從apple獲取虛擬產品資訊。
設定成功後如下所示:
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];
}
}
至此,整個流程結束.