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

小怪獸飼養猿發表於2019-03-04

最近公司在做APP內購會員功能 遇到了很多問題 總結記錄一下 首先一定要區分Apple pay 和IAP內購的區別 可以先去看一下官方文件地址 有每個步驟的詳細解釋

本篇文章分為:1、 內購支付流程; 2、開發整合步驟; 3、問題(遇坑)記錄解決方式 之前沒看官方文件走了很多彎路 網上部落格並不系統 強烈建議先過一遍官方文件

先看一下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內部支付

第二步: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 選擇使用者和職能 進入下面頁面 選擇沙箱技術測試員 新增測試賬號

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

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

Tips:Q:為什麼新增沙箱技術測試員 註冊不成功 Unknown Email xxxxxx 首先這裡有個坑 郵箱只要符合格式就可以 虛假郵箱也可以 但密碼必須符合正式的要求要有大小寫和字元 複雜就好 例如:Lh123456*

第五步:程式碼實現(有什麼問題可以在評論中跟我溝通)

.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:**21002、21003、21004、21005、21006、21007... 具體可以檢視這篇文件用App Store驗證收據

Q:Apple 和IAP的區別

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

Q:怎麼通過itunes-connect檢視具體訂單,itunes-connect中無法直接看到訂單資訊,可以用以下方法來查詢

1.可以通過賬單向蘋果傳送賬單驗證,有效可以手動補發 2 .用自己的伺服器的記錄賬單列表對比  3.利用第三方的TalkingData等交易函式,會自動記錄賬單資料

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

覺得有幫助可以關注我 後續繼續補充....

相關文章