最近公司在做APP內購會員功能 遇到了很多問題 總結記錄一下 首先一定要區(qū)分Apple pay 和IAP內購的區(qū)別
可以先去看一下官方文檔地址 有每個步驟的詳細解釋
本篇文章分為:
1、 內購支付流程;
2、開發(fā)集成步驟;
3、問題(遇坑)記錄解決方式
之前沒看官方文檔走了很多彎路 網(wǎng)上博客并不系統(tǒng) 強烈建議先過一遍官方文檔
先看一下IAP內購支付流程(官方)

- 程序向服務器發(fā)送請求,獲得一份產(chǎn)品列表。
- 服務器返回包含產(chǎn)品標識符的列表。
- 程序向App Store發(fā)送請求,得到產(chǎn)品的信息。
- App Store返回產(chǎn)品信息。
- 程序把返回的產(chǎn)品信息顯示給用戶(App的store界面)
- 用戶選擇某個產(chǎn)品
- 程序向App Store發(fā)送支付請求
- App Store處理支付請求并返回交易完成信息。
- 程序從信息中獲得數(shù)據(jù),并發(fā)送至服務器。
- 服務器紀錄數(shù)據(jù),并進行審(我們的)查。
- 服務器將數(shù)據(jù)發(fā)給App Store來驗證該交易的有效性。
- App Store對收到的數(shù)據(jù)進行解析,返回該數(shù)據(jù)和說明其是否有效的標識。
- 服務器讀取返回的數(shù)據(jù),確定用戶購買的內容。
- 服務器將購買的內容傳遞給程序。
第一步:內購賬戶稅務協(xié)議、銀行卡綁定相關
一般都是運營或者產(chǎn)品經(jīng)理處理這步 這篇文章圖文步驟比較詳細 處理稅務銀行相關設置 IAP,In App Purchases-在APP內部支付
第二步:Xcode設置相關
打開In-App Purchase開關 對應在開發(fā)者證書中心的項目證書中顯示應該也是可用狀態(tài)


第三步:在App Store Content -> 我的APP 添加內購項目商品
- 在首頁上,點按“我的 App”,然后選擇與該 App 內購買項目相關聯(lián)的 App。
- 在工具欄中,點按“功能”,然后在左列中點按“App 內購買項目”。
- 若要添加 App 內購買項目,請前往“App 內購買項目”,并點按“添加”按鈕(+)。

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

內購商品對應四種類型 消耗型、非消耗型、自動續(xù)訂訂閱型、非續(xù)訂訂閱型
官方文檔
- 選擇“消耗型項目”、“非消耗型項目”或“非續(xù)訂訂閱”,并點按“創(chuàng)建”。有關自動續(xù)訂訂閱的信息,請參見創(chuàng)建自動續(xù)期訂閱。
- 添加參考名稱、產(chǎn)品 ID 和本地化顯示名稱。
- 點按“存儲”或“提交以供審核”。
您可以在創(chuàng)建您的 App 內購買項目時輸入所有的元數(shù)據(jù),或稍后輸入您的 App 內購買項目信息。

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

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


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之你一定要看的內購破解-越獄篇 他遇到的實際問題比較多 按需借鑒