iOS IAP應用內購詳細步驟和問題總結指南

最近公司在作APP內購會員功能 遇到了不少問題 總結記錄一下 首先必定要區分Apple pay 和IAP內購的區別 能夠先去看一下官方文檔地址 有每一個步驟的詳細解釋html

本篇文章分爲:一、 內購支付流程; 二、開發集成步驟; 三、問題(遇坑)記錄解決方式 以前沒看官方文檔走了不少彎路 網上博客並不系統 強烈建議先過一遍官方文檔安全

先看一下IAP內購支付流程(官方)

官方流程圖

  1. 程序向服務器發送請求,得到一份產品列表。
  2. 服務器返回包含產品標識符的列表。
  3. 程序向App Store發送請求,獲得產品的信息。
  4. App Store返回產品信息。
  5. 程序把返回的產品信息顯示給用戶(App的store界面)
  6. 用戶選擇某個產品
  7. 程序向App Store發送支付請求
  8. App Store處理支付請求並返回交易完成信息。
  9. 程序從信息中得到數據,併發送至服務器。
  10. 服務器紀錄數據,並進行審(咱們的)查。
  11. 服務器將數據發給App Store來驗證該交易的有效性。
  12. App Store對收到的數據進行解析,返回該數據和說明其是否有效的標識。
  13. 服務器讀取返回的數據,肯定用戶購買的內容。
  14. 服務器將購買的內容傳遞給程序。

第一步:內購帳戶稅務協議、銀行卡綁定相關

通常都是運營或者產品經理處理這步 這篇文章圖文步驟比較詳細 處理稅務銀行相關設置 IAP,In App Purchases-在APP內部支付bash

第二步:Xcode設置相關

打開In-App Purchase開關 對應在開發者證書中心的項目證書中顯示應該也是可用狀態服務器

屏幕快照 2018-08-22 下午6.00.11.png
屏幕快照 2018-08-22 下午6.01.35.png

第三步:在App Store Content -> 個人APP 添加內購項目商品

  1. 首頁上,點按「個人 App」,而後選擇與該 App 內購買項目相關聯的 App。
  2. 在工具欄中,點按「功能」,而後在左列中點按「App 內購買項目」。
  3. 若要添加 App 內購買項目,請前往「App 內購買項目」,並點按「添加」按鈕(+)。

屏幕快照 2018-08-23 上午10.06.23.png
選擇功能 添加內購項目商品
選擇功能Tab

內購商品對應四種類型 消耗型、非消耗型、自動續訂訂閱型、非續訂訂閱型 官方文檔網絡

  1. 選擇「消耗型項目」、「非消耗型項目」或「非續訂訂閱」,並點按「建立」。有關自動續訂訂閱的信息,請參見建立自動續期訂閱
  2. 添加參考名稱、產品 ID 和本地化顯示名稱。
  3. 點按「存儲」或「提交以供審覈」。
您能夠在建立您的 App 內購買項目時輸入全部的元數據,或稍後輸入您的 App 內購買項目信息。
複製代碼

屏幕快照 2018-08-23 上午10.09.34.png

添加一個測試商品 其餘屬性均可以隨意填寫 產品ID必定要認真填寫 項目中須要根據ID獲取商品信息 價格有不一樣的等級能夠選 最低備用等級1 == 1元 填寫完成以後儲存 就完成了一個內購商品的添加併發

屏幕快照 2018-08-23 上午10.16.31.png

第四步:沙盒環境測試帳號

由於涉及到錢相關 總不能直接用money去支付吧 因此須要你去添加一個沙盒技術測試人員的帳號 (這個帳號是虛擬的) 付款不會扣你 看第三步那張圖 在App Store Content 選擇用戶和職能 進入下面頁面 選擇沙箱技術測試員 添加測試帳號app

屏幕快照 2018-08-23 上午11.02.26.png

屏幕快照 2018-08-23 上午11.05.28.png

Tips:Q:爲何添加沙箱技術測試員 註冊不成功 Unknown Email xxxxxx 首先這裏有個坑 郵箱只要符合格式就能夠 虛假郵箱也能夠 但密碼必須符合正式的要求要有大小寫和字符 複雜就好 例如:Lh123456*async

第五步:代碼實現(有什麼問題能夠在評論中跟我溝通)

.h文件函數

typedef void(^XSProductStatusBlock)(BOOL isStatus);

@interface XSApplePayManager : NSObject


+ (instancetype)shareManager;

/** 檢測客戶端與服務器漏單狀況處理*/
+ (void)checkOrderStatus;


/**
  根據商品ID請求支付信息


 @param orderId 訂單號
 @param productId 商品號
 @param statusBlock 回掉block
 */
- (void)requestProductWithOrderId:(NSString *)orderId
                        productId:(NSString *)productId
                      statusBlock:(XSProductStatusBlock)statusBlock;
複製代碼

.m文件工具

#import <StoreKit/StoreKit.h>
#import "APIManager.h"
#import "UIAlertView+AABlock.h"

@interface XSApplePayManager ()<SKProductsRequestDelegate,SKPaymentTransactionObserver>

@property (nonatomic, copy) NSString *orderId;
@property (nonatomic, copy) XSProductStatusBlock statusBlcok;

@end

@implementation XSApplePayManager

+ (instancetype)shareManager
{
    static dispatch_once_t onceToken;
    static XSApplePayManager *manager = nil;
    dispatch_once(&onceToken, ^{
        manager = [[XSApplePayManager alloc]init];
    });
    return manager;
}

/** 檢測客戶端與服務器漏單狀況處理*/
+ (void)checkOrderStatus
{
    NSDictionary *orderInfo = [XSApplePayManager getReceiptData];
    if (orderInfo != nil) {
        
        NSString *orderId = orderInfo[@"orderId"];
        NSString *receipt = orderInfo[@"receipt"];
        
        [[XSApplePayManager shareManager] verifyPurchaseForServiceWithOrderId:orderId receipt:receipt];
    }
}

#pragma mark -- 結束上次未完成的交易
-(void)removeAllUncompleteTransactionsBeforeNewPurchase{
    
    NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
    
    if (transactions.count >= 1) {
        
        for (SKPaymentTransaction* transaction in transactions) {
            if (transaction.transactionState == SKPaymentTransactionStatePurchased ||
                transaction.transactionState == SKPaymentTransactionStateRestored) {
                [[SKPaymentQueue defaultQueue]finishTransaction:transaction];
            }
        }
        
    }else{
        NSLog(@"沒有歷史未消耗訂單");
    }
}


/** 檢測權限 添加支付監測 開始支付流程*/
- (void)requestProductWithOrderId:(NSString *)orderId
                        productId:(NSString *)productId
                      statusBlock:(XSProductStatusBlock)statusBlock

{
    
    if (orderId == nil || productId == nil) {
        [AAProgressManager showFinishWithStatus:@"訂單號/商品號有誤"];
        return;
    }
    
    if ([[XZDeviceManager didRoot] isEqualToString:@"didRoot"]) {//寫本身的越獄判斷方法
        [AAProgressManager showFinishWithStatus:@"越獄手機不支持內購"];
        return;
    }
    
    
    if([SKPaymentQueue canMakePayments]){
        
        [self removeAllUncompleteTransactionsBeforeNewPurchase];
        
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];

        self.orderId = orderId;
        self.statusBlcok = statusBlock;
        [self requestProductData:productId];
        
    }else{
        [AAProgressManager showFinishWithStatus:L(@"請打開應用內支付功能")];
    }
}

/** 去Apple IAP Service 根據商品ID請求商品信息*/
- (void)requestProductData:(NSString *)type{
    
    [AAProgressManager showWithStatus:@"正在請求..."];
    NSArray *product = [[NSArray alloc] initWithObjects:type,nil];
    
    NSSet *nsset = [NSSet setWithArray:product];
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
    request.delegate = self;
    [request start];
}


#pragma mark -- SKProductsRequestDelegate
//收到產品返回信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    
    NSArray *product = response.products;
    if([product count] == 0){
        [AAProgressManager showFinishWithStatus:L(@"沒法獲取商品信息,請從新嘗試購買")];
        return;
    }
    
    NSLog(@"產品付費數量:%ld",product.count);
    
    SKProduct *p = product.firstObject;
    
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:p];
    payment.quantity = (NSInteger)p.price;//購買次數=價錢
    if (payment.quantity == 0) {
        payment.quantity = 1;
    }
    payment.applicationUsername = self.orderId;//[NSString stringWithFormat:@"%@",[[AAUserManager shareManager] getUID]];
    [[SKPaymentQueue defaultQueue] addPayment:payment];

}

//請求失敗
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    NSLog(@"------------------錯誤-----------------:%@", error);
    if (self.statusBlcok) {
        self.statusBlcok(NO);
    }
    [AAProgressManager showFinishWithStatus:L(@"從Apple獲取商品信息失敗")];

}

- (void)requestDidFinish:(SKRequest *)request{
    NSLog(@"------------反饋信息結束-----------------%@",request);
}

#pragma mark -- 監聽AppStore支付狀態
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
    
    NSLog(@"監聽AppStore支付狀態");
    dispatch_async(dispatch_get_main_queue(), ^{
        for(SKPaymentTransaction *tran in transaction){
            switch (tran.transactionState) {
                case SKPaymentTransactionStatePurchased:
                {
                    // 發送到蘋果服務器驗證憑證
                    [self verifyPurchaseWithPaymentTransaction];
                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                }
                    break;
                case SKPaymentTransactionStatePurchasing:
                    NSLog(@"商品添加進列表");
                    break;
                case SKPaymentTransactionStateRestored:
                {
                    [AAProgressManager showFinishWithStatus:L(@"已經購買過商品")];
                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                }
                    break;
                case SKPaymentTransactionStateFailed:
                {
                    if (self.statusBlcok) {
                        self.statusBlcok(NO);
                    }
                    NSLog(@"交易失敗");

                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                }
                    break;
                case SKPaymentTransactionStateDeferred:
                {
                    [AAProgressManager showFinishWithStatus:L(@"最終狀態未肯定")];
                }
                    break;
                default:
                    break;
            }
        }
    });
    
}

#pragma mark -- 驗證
/**驗證購買,避免越獄軟件模擬蘋果請求達到非法購買問題*/
-(void)verifyPurchaseWithPaymentTransaction{
    
    //從沙盒中獲取交易憑證而且拼接成請求體數據
    NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
    NSString *receiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    [self saveReceiptData:@{@"receipt":receiptString,
                            @"orderId":self.orderId}];
    
  
    [self verifyPurchaseForServiceWithOrderId:self.orderId
                                      receipt:receiptString];
}

- (void)verifyPurchaseForServiceWithOrderId:(NSString *)orderId
                                    receipt:(NSString *)receiptString
{
    if (orderId == nil && receiptString == nil) {
        if (self.statusBlcok) {
            self.statusBlcok(NO);
        }
        [AAProgressManager showFinishWithStatus:@"訂單號/憑證無效"];
        return;
    }
    
    [self removeTransaction];

    [AAProgressManager showWithStatus:@"正在驗證服務器..."];
    
    WS(weakSelf);
    [[APIManager sharedInstance] verifyPurchaseWithOrderID:orderId
                                                    params:@{@"ceceipt-data":receiptString}
                                                   success:^(id response)
     {
         dispatch_async(dispatch_get_main_queue(), ^{
             [AAProgressManager dismiss];
             [AAProgressManager showFinishWithStatus:L(@"交易完成")];
             [weakSelf removeLocReceiptData];
             if (weakSelf.statusBlcok) {
                 weakSelf.statusBlcok(YES);
             }
         });
         
     } failure:^(NSError *error) {
         dispatch_async(dispatch_get_main_queue(), ^{
             
             [CommonFunction showError:error];
             [weakSelf verifyPurchaseFail];
         });
     }];
}

- (void)verifyPurchaseFail
{
    WS(weakSelf);
    UIAlertView *altert =[UIAlertView alertViewWithTitle:@"服務器驗證失敗"
                                                 message:@"帳單在驗證服務器過程當中出現錯誤,\n請檢查網絡環境是否能夠再次驗證\n若是取消可在網絡環境良好的狀況下從新啓動行者可再次繼續驗證支付"
                                       cancelButtonTitle:L(@"取消")
                                       otherButtonTitles:@[L(@"再次驗證")]
                                               onDismiss:^(NSInteger buttonIndex)
                          {
                              dispatch_async(dispatch_get_main_queue(), ^
                                             {
                                                 [XSApplePayManager checkOrderStatus];
                                             });           ;
                              
                          } onCancel:^{
                              dispatch_async(dispatch_get_main_queue(), ^{
                                  
                                  if (weakSelf.statusBlcok) {
                                      weakSelf.statusBlcok(NO);
                                  }
                                  [PromptInfo showWithText:@"可在網絡環境良好的狀況下從新啓動行者可再次繼續驗證支付"];
                                  
                              });
                          }];
    [altert show];
}

//交易結束
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

- (void)removeTransaction
{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

#pragma mark -- 本地保存一次支付憑證
static NSString *const kSaveReceiptData = @"kSaveReceiptData";

- (void)saveReceiptData:(NSDictionary *)receiptData
{
    [[NSUserDefaults standardUserDefaults] setValue:receiptData forKey:kSaveReceiptData];
    [[NSUserDefaults standardUserDefaults]synchronize];
}

+ (NSDictionary *)getReceiptData
{
    return [[NSUserDefaults standardUserDefaults] valueForKey:kSaveReceiptData];
}

- (void)removeLocReceiptData
{
    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kSaveReceiptData];
    [[NSUserDefaults standardUserDefaults] synchronize];
}
複製代碼
第六步:IAP支付流程 & 服務器驗證流程

整個支付流程以下: 1.客戶端向Appstore請求購買產品(假設產品信息已經取得),Appstore驗證產品成功後,從用戶的Apple帳戶餘額中扣費。 2.Appstore向客戶端返回一段receipt-data,裏面記錄了本次交易的證書和簽名信息。 3.客戶端向咱們能夠信任的服務器提供receipt-data 4.服務器對receipt-data進行一次base64編碼 5.服務器把編碼後的receipt-data發往itunes.appstore進行驗證 6.itunes.appstore返回驗證結果給服務器 7.服務器對商品購買狀態以及商品類型,向客戶端發放相應的道具與推送數據更新通知

漏單處理 確保receipt-data的成功提交與異常處理

創建在IAP Server Model的基礎上,而且咱們知道手機網絡是不穩定的,在付款成功後不能確保把receipt-data必定提交到服務器。若是出現了這樣的狀況,那就意味着玩家被appstore扣費了,卻沒收到服務器發放的道具。 漏單處理: 解決這個問題的方法是在客戶端提交receipt-data給咱們的服務器,讓咱們的服務器向蘋果服務器發送驗證請求,驗證這個receipt-data帳單的有效性. 在沒有收到回覆以前,客戶端必需要把receipt-data保存好,而且按期或在合理的UI界面觸發向服務端發起請求,直至收到服務端的回覆後刪除客戶端的receipt帳單記錄。 若是是客戶端沒成功提交receipt-data,那怎麼辦?就是玩家被扣費了,也收到appstore的消費收據了,卻依然沒收到遊戲道具,因而投訴到遊戲客服處。

這種狀況在以往的經驗中也會出現,常見的玩家和遊戲運營商發生的糾紛。遊戲客服向玩家索要遊戲帳號和appstore的收據單號,經過查詢itunes-connect看是否確有這筆訂單。若是訂單存在,則要聯繫研發方去查詢遊戲服務器,看訂單號與玩家名是否對應,而且是否已經被使用了,作這一點檢查的目的是 爲了防止惡意玩家利用已經使用過了的訂單號進行欺騙(已驗證的帳單是能夠再次請求驗證的,曾經爲了測試,將帳單手動發給服務器處理併成功),謊稱本身沒收到商品。這就是上面一節IAP Server Model中紅字所提到的安全邏輯的目的。固然了,若是查不到這個訂單號,就意味着這個訂單確實還沒使用過,手動給玩家補發商品便可。

更多能夠查看這篇博文蘋果IAP安全支付與防範 receipt收據驗證

遇到的坑

Q:21004 你提供的共享密鑰和帳戶的共享密鑰不一致 什麼是共享密鑰? 共享密鑰從哪裏獲取?

**A:**先看一下官方文檔怎麼說生成收據驗證代碼 爲了在驗證自動續期訂閱時提升您的 App 與 Apple 服務器交易的安全性,您能夠在收據中包含一個 32 位隨機生成的字母數字字符串,做爲共享密鑰。 在 App Store Connect 中生成共享密鑰。您能夠生成一個主共享密鑰,做爲您全部 App 的單一代碼,或做爲針對單個 App 的 App 專用共享密鑰。您也能夠針對您的部分 App 使用主共享密鑰,其餘 App 使用 App 專用共享密鑰。 點擊下面展開就能夠看到共享密鑰生成的方式

Q:沙箱技術測試人員添加不成功 老是提示郵箱錯誤

A: 沙箱技術測試帳號用於付款測試 任意未建立過Apple ID 的郵箱均可以 假的郵箱也能夠 重要的是密碼格式必定要包含大小寫 跟正式帳號註冊規則同樣 (例如:Lh123456*)

####Q:本身服務器向蘋果服務器驗證收據/憑證參數是什麼?向status code 驗證apple iap sever的狀態碼錶明什麼意思?

**A:**2100二、2100三、2100四、2100五、2100六、21007... 具體能夠查看這篇文檔用App Store驗證收據

Q:Apple 和IAP的區別

**A:**IAP是連接App store的內購服務 通常是虛擬商品須要走的通道(好比會員功能) Apple Pay是蘋果跟各大銀行合做的卡包形式的相似於刷卡支付服務 通常用於現實場景 這兩個必定別搞混了

Q:怎麼經過itunes-connect查看具體訂單,itunes-connect中沒法直接看到訂單信息,能夠用如下方法來查詢

1.能夠經過帳單向蘋果發送帳單驗證,有效能夠手動補發 2 .用本身的服務器的記錄帳單列表對比  3.利用第三方的TalkingData等交易函數,會自動記錄帳單數據

還有一些問題能夠借鑑一下這篇博文iOS之你必定要看的內購破解-越獄篇 他遇到的實際問題比較多 按需借鑑

以爲有幫助能夠關注我 後續繼續補充....

相關文章
相關標籤/搜索