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

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


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

選擇功能 添加內(nèi)購(gòu)項(xiàng)目商品

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

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

第四步:沙盒環(huán)境測(cè)試賬號(hào)
因?yàn)樯婕暗藉X相關(guān) 總不能直接用money去支付吧 所以需要你去添加一個(gè)沙盒技術(shù)測(cè)試人員的賬號(hào) (這個(gè)賬號(hào)是虛擬的) 付款不會(huì)扣你
看第三步那張圖 在App Store Content 選擇用戶和職能 進(jìn)入下面頁(yè)面 選擇沙箱技術(shù)測(cè)試員 添加測(cè)試賬號(hào)


Tips:Q:為什么添加沙箱技術(shù)測(cè)試員 注冊(cè)不成功 Unknown Email xxxxxx
首先這里有個(gè)坑 郵箱只要符合格式就可以 虛假郵箱也可以 但密碼必須符合正式的要求要有大小寫和字符 復(fù)雜就好 例如:Lh123456*
第五步:代碼實(shí)現(xiàn)(初步,未進(jìn)行優(yōu)化 有什么問(wèn)題可以在評(píng)論中跟我溝通)
.h文件
typedef void(^XSProductStatusBlock)(BOOL isStatus);
@interface XSApplePayManager : NSObject
+ (instancetype)shareManager;
/** 檢測(cè)客戶端與服務(wù)器漏單情況處理*/
+ (void)checkOrderStatus;
/**
根據(jù)商品ID請(qǐng)求支付信息
@param orderId 訂單號(hào)
@param productId 商品號(hào)
@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;
}
/** 檢測(cè)客戶端與服務(wù)器漏單情況處理*/
+ (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 -- 結(jié)束上次未完成的交易
-(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(@"沒(méi)有歷史未消耗訂單");
}
}
/** 檢測(cè)權(quán)限 添加支付監(jiān)測(cè) 開(kāi)始支付流程*/
- (void)requestProductWithOrderId:(NSString *)orderId
productId:(NSString *)productId
statusBlock:(XSProductStatusBlock)statusBlock
{
if (orderId == nil || productId == nil) {
[AAProgressManager showFinishWithStatus:@"訂單號(hào)/商品號(hào)有誤"];
return;
}
if ([[XZDeviceManager didRoot] isEqualToString:@"didRoot"]) {//寫自己的越獄判斷方法
[AAProgressManager showFinishWithStatus:@"越獄手機(jī)不支持內(nèi)購(gòu)"];
return;
}
if([SKPaymentQueue canMakePayments]){
[self removeAllUncompleteTransactionsBeforeNewPurchase];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
self.orderId = orderId;
self.statusBlcok = statusBlock;
[self requestProductData:productId];
}else{
[AAProgressManager showFinishWithStatus:L(@"請(qǐng)打開(kāi)應(yīng)用內(nèi)支付功能")];
}
}
/** 去Apple IAP Service 根據(jù)商品ID請(qǐng)求商品信息*/
- (void)requestProductData:(NSString *)type{
[AAProgressManager showWithStatus:@"正在請(qǐng)求..."];
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(@"無(wú)法獲取商品信息,請(qǐng)重新嘗試購(gòu)買")];
return;
}
NSLog(@"產(chǎn)品付費(fèi)數(shù)量:%ld",product.count);
SKProduct *p = product.firstObject;
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:p];
payment.quantity = (NSInteger)p.price;//購(gòu)買次數(shù)=價(jià)錢
if (payment.quantity == 0) {
payment.quantity = 1;
}
payment.applicationUsername = self.orderId;//[NSString stringWithFormat:@"%@",[[AAUserManager shareManager] getUID]];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
//請(qǐng)求失敗
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
NSLog(@"------------------錯(cuò)誤-----------------:%@", error);
if (self.statusBlcok) {
self.statusBlcok(NO);
}
[AAProgressManager showFinishWithStatus:L(@"從Apple獲取商品信息失敗")];
}
- (void)requestDidFinish:(SKRequest *)request{
NSLog(@"------------反饋信息結(jié)束-----------------%@",request);
}
#pragma mark -- 監(jiān)聽(tīng)AppStore支付狀態(tài)
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
NSLog(@"監(jiān)聽(tīng)AppStore支付狀態(tài)");
dispatch_async(dispatch_get_main_queue(), ^{
for(SKPaymentTransaction *tran in transaction){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
{
// 發(fā)送到蘋果服務(wù)器驗(yàn)證憑證
[self verifyPurchaseWithPaymentTransaction];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加進(jìn)列表");
break;
case SKPaymentTransactionStateRestored:
{
[AAProgressManager showFinishWithStatus:L(@"已經(jīng)購(gòu)買過(guò)商品")];
[[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 -- 驗(yàn)證
/**驗(yàn)證購(gòu)買,避免越獄軟件模擬蘋果請(qǐng)求達(dá)到非法購(gòu)買問(wèn)題*/
-(void)verifyPurchaseWithPaymentTransaction{
//從沙盒中獲取交易憑證并且拼接成請(qǐng)求體數(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:@"訂單號(hào)/憑證無(wú)效"];
return;
}
[self removeTransaction];
[AAProgressManager showWithStatus:@"正在驗(yàn)證服務(wù)器..."];
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:@"服務(wù)器驗(yàn)證失敗"
message:@"賬單在驗(yàn)證服務(wù)器過(guò)程中出現(xiàn)錯(cuò)誤,\n請(qǐng)檢查網(wǎng)絡(luò)環(huán)境是否可以再次驗(yàn)證\n如果取消可在網(wǎng)絡(luò)環(huán)境良好的情況下重新啟動(dòng)行者可再次繼續(xù)驗(yàn)證支付"
cancelButtonTitle:L(@"取消")
otherButtonTitles:@[L(@"再次驗(yàn)證")]
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)絡(luò)環(huán)境良好的情況下重新啟動(dòng)行者可再次繼續(xù)驗(yàn)證支付"];
});
}];
[altert show];
}
//交易結(jié)束
- (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支付流程 & 服務(wù)器驗(yàn)證流程
整個(gè)支付流程如下:
1.客戶端向Appstore請(qǐng)求購(gòu)買產(chǎn)品(假設(shè)產(chǎn)品信息已經(jīng)取得),Appstore驗(yàn)證產(chǎn)品成功后,從用戶的Apple賬戶余額中扣費(fèi)。
2.Appstore向客戶端返回一段receipt-data,里面記錄了本次交易的證書和簽名信息。
3.客戶端向我們可以信任的服務(wù)器提供receipt-data
4.服務(wù)器對(duì)receipt-data進(jìn)行一次base64編碼
5.服務(wù)器把編碼后的receipt-data發(fā)往itunes.appstore進(jìn)行驗(yàn)證
6.itunes.appstore返回驗(yàn)證結(jié)果給服務(wù)器
7.服務(wù)器對(duì)商品購(gòu)買狀態(tài)以及商品類型,向客戶端發(fā)放相應(yīng)的道具與推送數(shù)據(jù)更新通知
漏單處理 確保receipt-data的成功提交與異常處理
建立在IAP Server Model的基礎(chǔ)上,并且我們知道手機(jī)網(wǎng)絡(luò)是不穩(wěn)定的,在付款成功后不能確保把receipt-data一定提交到服務(wù)器。如果出現(xiàn)了這樣的情況,那就意味著玩家被appstore扣費(fèi)了,卻沒(méi)收到服務(wù)器發(fā)放的道具。
漏單處理:
解決這個(gè)問(wèn)題的方法是在客戶端提交receipt-data給我們的服務(wù)器,讓我們的服務(wù)器向蘋果服務(wù)器發(fā)送驗(yàn)證請(qǐng)求,驗(yàn)證這個(gè)receipt-data賬單的有效性. 在沒(méi)有收到回復(fù)之前,客戶端必須要把receipt-data保存好,并且定期或在合理的UI界面觸發(fā)向服務(wù)端發(fā)起請(qǐng)求,直至收到服務(wù)端的回復(fù)后刪除客戶端的receipt賬單記錄。
如果是客戶端沒(méi)成功提交receipt-data,那怎么辦?就是玩家被扣費(fèi)了,也收到appstore的消費(fèi)收據(jù)了,卻依然沒(méi)收到游戲道具,于是投訴到游戲客服處。
這種情況在以往的經(jīng)驗(yàn)中也會(huì)出現(xiàn),常見(jiàn)的玩家和游戲運(yùn)營(yíng)商發(fā)生的糾紛。游戲客服向玩家索要游戲賬號(hào)和appstore的收據(jù)單號(hào),通過(guò)查詢itunes-connect看是否確有這筆訂單。如果訂單存在,則要聯(lián)系研發(fā)方去查詢游戲服務(wù)器,看訂單號(hào)與玩家名是否對(duì)應(yīng),并且是否已經(jīng)被使用了,做這一點(diǎn)檢查的目的是 為了防止惡意玩家利用已經(jīng)使用過(guò)了的訂單號(hào)進(jìn)行欺騙(已驗(yàn)證的賬單是可以再次請(qǐng)求驗(yàn)證的,曾經(jīng)為了測(cè)試,將賬單手動(dòng)發(fā)給服務(wù)器處理并成功),謊稱自己沒(méi)收到商品。這就是上面一節(jié)IAP Server Model中紅字所提到的安全邏輯的目的。當(dāng)然了,如果查不到這個(gè)訂單號(hào),就意味著這個(gè)訂單確實(shí)還沒(méi)使用過(guò),手動(dòng)給玩家補(bǔ)發(fā)商品即可。
更多可以查看這篇博文蘋果IAP安全支付與防范 receipt收據(jù)驗(yàn)證
遇到的坑
Q:21004 你提供的共享密鑰和賬戶的共享密鑰不一致 什么是共享密鑰? 共享密鑰從哪里獲取?
A:先看一下官方文檔怎么說(shuō)生成收據(jù)驗(yàn)證代碼
為了在驗(yàn)證自動(dòng)續(xù)期訂閱時(shí)提高您的 App 與 Apple 服務(wù)器交易的安全性,您可以在收據(jù)中包含一個(gè) 32 位隨機(jī)生成的字母數(shù)字字符串,作為共享密鑰。
在 App Store Connect 中生成共享密鑰。您可以生成一個(gè)主共享密鑰,作為您所有 App 的單一代碼,或作為針對(duì)單個(gè) App 的 App 專用共享密鑰。您也可以針對(duì)您的部分 App 使用主共享密鑰,其他 App 使用 App 專用共享密鑰。
點(diǎn)擊下面展開(kāi)就可以看到共享密鑰生成的方式
Q:沙箱技術(shù)測(cè)試人員添加不成功 總是提示郵箱錯(cuò)誤
A: 沙箱技術(shù)測(cè)試賬號(hào)用于付款測(cè)試 任意未創(chuàng)建過(guò)Apple ID 的郵箱都可以 假的郵箱也可以 重要的是密碼格式一定要包含大小寫 跟正式賬號(hào)注冊(cè)規(guī)則一樣 (例如:Lh123456*)
Q:自己服務(wù)器向蘋果服務(wù)器驗(yàn)證收據(jù)/憑證參數(shù)是什么?向status code 驗(yàn)證apple iap sever的狀態(tài)碼代表什么意思?
A:21002、21003、21004、21005、21006、21007... 具體可以查看這篇文檔用App Store驗(yàn)證收據(jù)
Q:Apple 和IAP的區(qū)別
A:IAP是鏈接App store的內(nèi)購(gòu)服務(wù) 一般是虛擬商品需要走的通道(比如會(huì)員功能)
Apple Pay是蘋果跟各大銀行合作的卡包形式的類似于刷卡支付服務(wù) 一般用于現(xiàn)實(shí)場(chǎng)景
這兩個(gè)一定別搞混了
Q:怎么通過(guò)itunes-connect查看具體訂單,itunes-connect中無(wú)法直接看到訂單信息,可以用以下方法來(lái)查詢
1.可以通過(guò)賬單向蘋果發(fā)送賬單驗(yàn)證,有效可以手動(dòng)補(bǔ)發(fā)
2 .用自己的服務(wù)器的記錄賬單列表對(duì)比
3.利用第三方的TalkingData等交易函數(shù),會(huì)自動(dòng)記錄賬單數(shù)據(jù)
還有一些問(wèn)題可以借鑒一下這篇博文iOS之你一定要看的內(nèi)購(gòu)破解-越獄篇 他遇到的實(shí)際問(wèn)題比較多 按需借鑒