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

最近公司在做APP內購會員功能 遇到了很多問題 總結記錄一下 首先一定要區(qū)分Apple pay 和IAP內購的區(qū)別
可以先去看一下官方文檔地址 有每個步驟的詳細解釋
本篇文章分為:
1、 內購支付流程;
2、開發(fā)集成步驟;
3、問題(遇坑)記錄解決方式

之前沒看官方文檔走了很多彎路 網(wǎng)上博客并不系統(tǒng) 強烈建議先過一遍官方文檔

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

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

第一步:內購賬戶稅務協(xié)議、銀行卡綁定相關

一般都是運營或者產(chǎn)品經(jīng)理處理這步 這篇文章圖文步驟比較詳細 處理稅務銀行相關設置 IAP,In App Purchases-在APP內部支付

第二步:Xcode設置相關

打開In-App Purchase開關 對應在開發(fā)者證書中心的項目證書中顯示應該也是可用狀態(tài)

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

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

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

  1. 首頁上,點按“我的 App”,然后選擇與該 App 內購買項目相關聯(lián)的 App。
  2. 在工具欄中,點按“功能”,然后在左列中點按“App 內購買項目”。
  3. 若要添加 App 內購買項目,請前往“App 內購買項目”,并點按“添加”按鈕(+)。

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

選擇功能 添加內購項目商品
選擇功能Tab

內購商品對應四種類型 消耗型、非消耗型、自動續(xù)訂訂閱型、非續(xù)訂訂閱型
官方文檔

  1. 選擇“消耗型項目”、“非消耗型項目”或“非續(xù)訂訂閱”,并點按“創(chuàng)建”。有關自動續(xù)訂訂閱的信息,請參見創(chuàng)建自動續(xù)期訂閱。
  2. 添加參考名稱、產(chǎn)品 ID 和本地化顯示名稱。
  3. 點按“存儲”或“提交以供審核”。
您可以在創(chuàng)建您的 App 內購買項目時輸入所有的元數(shù)據(jù),或稍后輸入您的 App 內購買項目信息。
屏幕快照 2018-08-23 上午10.09.34.png

添加一個測試商品 其他屬性都可以隨意填寫 產(chǎn)品ID一定要認真填寫 項目中需要根據(jù)ID獲取商品信息 價格有不同的等級可以選 最低備用等級1 == 1元
填寫完成之后儲存 就完成了一個內購商品的添加

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

第四步:沙盒環(huán)境測試賬號

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

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

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

第五步:代碼實現(xiàn)(初步,未進行優(yōu)化 有什么問題可以在評論中跟我溝通)

.h文件

typedef void(^XSProductStatusBlock)(BOOL isStatus);

@interface XSApplePayManager : NSObject


+ (instancetype)shareManager;

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


/**
  根據(jù)商品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(@"沒有歷史未消耗訂單");
    }
}


/** 檢測權限 添加支付監(jiān)測 開始支付流程*/
- (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 根據(jù)商品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
//收到產(chǎn)品返回信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    
    NSArray *product = response.products;
    if([product count] == 0){
        [AAProgressManager showFinishWithStatus:L(@"無法獲取商品信息,請重新嘗試購買")];
        return;
    }
    
    NSLog(@"產(chǎn)品付費數(shù)量:%ld",product.count);
    
    SKProduct *p = product.firstObject;
    
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:p];
    payment.quantity = (NSInteger)p.price;//購買次數(shù)=價錢
    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 -- 監(jiān)聽AppStore支付狀態(tài)
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
    
    NSLog(@"監(jiān)聽AppStore支付狀態(tài)");
    dispatch_async(dispatch_get_main_queue(), ^{
        for(SKPaymentTransaction *tran in transaction){
            switch (tran.transactionState) {
                case SKPaymentTransactionStatePurchased:
                {
                    // 發(fā)送到蘋果服務器驗證憑證
                    [self verifyPurchaseWithPaymentTransaction];
                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                }
                    break;
                case SKPaymentTransactionStatePurchasing:
                    NSLog(@"商品添加進列表");
                    break;
                case SKPaymentTransactionStateRestored:
                {
                    [AAProgressManager showFinishWithStatus:L(@"已經(jīng)購買過商品")];
                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                }
                    break;
                case SKPaymentTransactionStateFailed:
                {
                    if (self.statusBlcok) {
                        self.statusBlcok(NO);
                    }
                    NSLog(@"交易失敗");

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

#pragma mark -- 驗證
/**驗證購買,避免越獄軟件模擬蘋果請求達到非法購買問題*/
-(void)verifyPurchaseWithPaymentTransaction{
    
    //從沙盒中獲取交易憑證并且拼接成請求體數(shù)據(jù)
    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:@"賬單在驗證服務器過程中出現(xiàn)錯誤,\n請檢查網(wǎng)絡環(huán)境是否可以再次驗證\n如果取消可在網(wǎng)絡環(huán)境良好的情況下重新啟動行者可再次繼續(xù)驗證支付"
                                       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:@"可在網(wǎng)絡環(huán)境良好的情況下重新啟動行者可再次繼續(xù)驗證支付"];
                                  
                              });
                          }];
    [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請求購買產(chǎn)品(假設產(chǎn)品信息已經(jīng)取得),Appstore驗證產(chǎn)品成功后,從用戶的Apple賬戶余額中扣費。
2.Appstore向客戶端返回一段receipt-data,里面記錄了本次交易的證書和簽名信息。
3.客戶端向我們可以信任的服務器提供receipt-data
4.服務器對receipt-data進行一次base64編碼
5.服務器把編碼后的receipt-data發(fā)往itunes.appstore進行驗證
6.itunes.appstore返回驗證結果給服務器
7.服務器對商品購買狀態(tài)以及商品類型,向客戶端發(fā)放相應的道具與推送數(shù)據(jù)更新通知

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

建立在IAP Server Model的基礎上,并且我們知道手機網(wǎng)絡是不穩(wěn)定的,在付款成功后不能確保把receipt-data一定提交到服務器。如果出現(xiàn)了這樣的情況,那就意味著玩家被appstore扣費了,卻沒收到服務器發(fā)放的道具。
漏單處理:
解決這個問題的方法是在客戶端提交receipt-data給我們的服務器,讓我們的服務器向蘋果服務器發(fā)送驗證請求,驗證這個receipt-data賬單的有效性. 在沒有收到回復之前,客戶端必須要把receipt-data保存好,并且定期或在合理的UI界面觸發(fā)向服務端發(fā)起請求,直至收到服務端的回復后刪除客戶端的receipt賬單記錄。
如果是客戶端沒成功提交receipt-data,那怎么辦?就是玩家被扣費了,也收到appstore的消費收據(jù)了,卻依然沒收到游戲道具,于是投訴到游戲客服處。

這種情況在以往的經(jīng)驗中也會出現(xiàn),常見的玩家和游戲運營商發(fā)生的糾紛。游戲客服向玩家索要游戲賬號和appstore的收據(jù)單號,通過查詢itunes-connect看是否確有這筆訂單。如果訂單存在,則要聯(lián)系研發(fā)方去查詢游戲服務器,看訂單號與玩家名是否對應,并且是否已經(jīng)被使用了,做這一點檢查的目的是 為了防止惡意玩家利用已經(jīng)使用過了的訂單號進行欺騙(已驗證的賬單是可以再次請求驗證的,曾經(jīng)為了測試,將賬單手動發(fā)給服務器處理并成功),謊稱自己沒收到商品。這就是上面一節(jié)IAP Server Model中紅字所提到的安全邏輯的目的。當然了,如果查不到這個訂單號,就意味著這個訂單確實還沒使用過,手動給玩家補發(fā)商品即可。

更多可以查看這篇博文蘋果IAP安全支付與防范 receipt收據(jù)驗證

遇到的坑

Q:21004 你提供的共享密鑰和賬戶的共享密鑰不一致 什么是共享密鑰? 共享密鑰從哪里獲???

A:先看一下官方文檔怎么說生成收據(jù)驗證代碼
為了在驗證自動續(xù)期訂閱時提高您的 App 與 Apple 服務器交易的安全性,您可以在收據(jù)中包含一個 32 位隨機生成的字母數(shù)字字符串,作為共享密鑰。
在 App Store Connect 中生成共享密鑰。您可以生成一個主共享密鑰,作為您所有 App 的單一代碼,或作為針對單個 App 的 App 專用共享密鑰。您也可以針對您的部分 App 使用主共享密鑰,其他 App 使用 App 專用共享密鑰。
點擊下面展開就可以看到共享密鑰生成的方式

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

A: 沙箱技術測試賬號用于付款測試 任意未創(chuàng)建過Apple ID 的郵箱都可以 假的郵箱也可以 重要的是密碼格式一定要包含大小寫 跟正式賬號注冊規(guī)則一樣 (例如:Lh123456*)

Q:自己服務器向蘋果服務器驗證收據(jù)/憑證參數(shù)是什么?向status code 驗證apple iap sever的狀態(tài)碼代表什么意思?

A:21002、21003、21004、21005、21006、21007... 具體可以查看這篇文檔用App Store驗證收據(jù)

Q:Apple 和IAP的區(qū)別

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

Q:怎么通過itunes-connect查看具體訂單,itunes-connect中無法直接看到訂單信息,可以用以下方法來查詢

1.可以通過賬單向蘋果發(fā)送賬單驗證,有效可以手動補發(fā)
2 .用自己的服務器的記錄賬單列表對比
3.利用第三方的TalkingData等交易函數(shù),會自動記錄賬單數(shù)據(jù)

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

覺得有幫助可以關注我 后續(xù)繼續(xù)補充....

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容