概述
iOS內(nèi)購是指蘋果 App Store 的應(yīng)用內(nèi)購買盒延,即In-App Purchase
,簡稱IAP(以下本文關(guān)于內(nèi)購都簡稱為IAP)鼠冕,是蘋果為 App 內(nèi)購買虛擬商品或服務(wù)提供的一套交易系統(tǒng)添寺。為什么我們需要掌握IAP這套流程呢,因?yàn)锳pp Store審核指南規(guī)定:
如果您想要在 app 內(nèi)解鎖特性或功能 (解鎖方式有:訂閱懈费、游戲內(nèi)貨幣计露、游戲關(guān)卡、優(yōu)質(zhì)內(nèi)容的訪問
限或解鎖完整版等)憎乙,則必須使用 App 內(nèi)購買項(xiàng)目票罐。App 不得使用自身機(jī)制來解鎖內(nèi)容或功能,
如許可證密鑰泞边、增強(qiáng)現(xiàn)實(shí)標(biāo)記该押、二維碼等。App 及其元數(shù)據(jù)不得包含按鈕阵谚、外部鏈接或其他行動(dòng)號(hào)
召用語蚕礼,以指引用戶使用非 App 內(nèi)購買項(xiàng)目機(jī)制進(jìn)行購買。
這段話的大概意思就是APP內(nèi)的虛擬商品或服務(wù)梢什,必須使用 IAP 進(jìn)行購買支付奠蹬,不允許使用支付寶、微信支付等其它第三方支付方式(包括Apple Pay)绳矩,也不允許以任何方式(包括跳出App罩润、提示文案等)引導(dǎo)用戶通過應(yīng)用外部渠道購買。如果違反此規(guī)定翼馆,apple審核人員不會(huì)讓你的APP上架!=鸲取应媚!
內(nèi)購前準(zhǔn)備
APP內(nèi)集成IAP代碼之前需要先去開發(fā)賬號(hào)的ITunes Connect進(jìn)行以下三步操作:
1,后臺(tái)填寫銀行賬戶信息
2猜极,配置商品信息中姜,包括產(chǎn)品ID,產(chǎn)品價(jià)格等
3跟伏,配置用于測(cè)試IAP支付功能的沙箱賬戶丢胚。
填寫銀行賬戶信息一般交由產(chǎn)品管理人員負(fù)責(zé),開發(fā)者不需要關(guān)注受扳,開發(fā)者需要關(guān)注的是第二步和第三步携龟。
配置內(nèi)購商品
IAP 是一套商品交易系統(tǒng),而非簡單的支付系統(tǒng)勘高,每一個(gè)購買項(xiàng)目都需要在開發(fā)者后臺(tái)的Itunes Connect后臺(tái)為 App 創(chuàng)建一個(gè)對(duì)應(yīng)的商品峡蟋,提交給蘋果審核通過后坟桅,購買項(xiàng)目才會(huì)生效。內(nèi)購商品有四種類型:
- 消耗型項(xiàng)目:只可使用一次的產(chǎn)品蕊蝗,使用之后即失效仅乓,必須再次購買,如:游戲幣蓬戚、一次性虛擬道具等
- 非消耗型項(xiàng)目:只需購買一次夸楣,不會(huì)過期或隨著使用而減少的產(chǎn)品。如:電子書
- 自動(dòng)續(xù)期訂閱:允許用戶在固定時(shí)間段內(nèi)購買動(dòng)態(tài)內(nèi)容的產(chǎn)品子漩。除非用戶選擇取消豫喧,否則此類訂閱會(huì)自動(dòng)續(xù)期,如:Apple Music這類按月訂閱的商品(有些雞賊的開發(fā)者以此收割對(duì)IAP商品不熟悉的用戶痛单,參考App Store“流氓”軟件)
- 非續(xù)期訂閱:允許用戶購買有時(shí)限性服務(wù)的產(chǎn)品嘿棘,此 App 內(nèi)購買項(xiàng)目的內(nèi)容可以是靜態(tài)的。此類訂閱不會(huì)自動(dòng)續(xù)期
配置商品信息需要注意產(chǎn)品ID和產(chǎn)品價(jià)格
1旭绒,產(chǎn)品 ID 具有唯一性鸟妙,建議使用項(xiàng)目的 Bundle Identidier
作為前綴后面拼接自定義的唯一的商品名或者 ID(字母、數(shù)字)挥吵,這里有個(gè)坑:一旦新建一個(gè)內(nèi)購商品重父,它的產(chǎn)品ID將永遠(yuǎn)被占用,即使該商品已經(jīng)被刪除忽匈,已創(chuàng)建的內(nèi)購商品除了產(chǎn)品 ID 之外的所有信息都可以修改房午,如果刪除了一個(gè)內(nèi)購商品,將無法再創(chuàng)建一個(gè)相同產(chǎn)品 ID 的商品丹允,也意味著該產(chǎn)品 ID 永久失效9帷!雕蔽!
2折柠,在創(chuàng)建IAP項(xiàng)目的時(shí)候,需要設(shè)定價(jià)格批狐,產(chǎn)品價(jià)格只能從蘋果提供的價(jià)格等級(jí)去選擇扇售,這個(gè)價(jià)格等級(jí)是固定的,同一價(jià)格等級(jí)會(huì)對(duì)應(yīng)各個(gè)國家的貨幣嚣艇,比如等級(jí)1對(duì)應(yīng)1美元承冰、6元人民幣,等級(jí)2對(duì)應(yīng)2美元食零、12元人民幣……最高等級(jí)87對(duì)應(yīng)999.99美元困乒、6498元人民幣。另外可能是為了照顧某些貨幣區(qū)的開發(fā)者和用戶慌洪,還有一些特殊的等級(jí)顶燕,比如備用等級(jí)A對(duì)應(yīng)1美元凑保、1元人民幣,備用等級(jí)B對(duì)應(yīng)1美元涌攻、3元人民幣這樣欧引。除此之外,IAP項(xiàng)目不能定一個(gè)9.9元人民幣這樣不符合任何等級(jí)的價(jià)格恳谎。詳細(xì)價(jià)格等級(jí)表可以看蘋果的官方價(jià)格等級(jí)文檔
蘋果的價(jià)格等級(jí)表通常是不會(huì)調(diào)整的芝此,但也不排除在某些貨幣匯率發(fā)生巨大變化的情況下,對(duì)該貨幣的定價(jià)進(jìn)行調(diào)整因痛,調(diào)整前蘋果會(huì)發(fā)郵件通知開發(fā)者婚苹。
3,商品分成
App Store上的付費(fèi)App和App內(nèi)購鸵膏,蘋果與開發(fā)者默認(rèn)是3/7分成膊升。但實(shí)際上,在某些地區(qū)蘋果與開發(fā)者分成之前需要先扣除交易稅谭企,開發(fā)者的實(shí)際分成不一定是70%廓译。從2015年10月開始,蘋果對(duì)中國地區(qū)的App Store購買扣除了2%的交易稅债查,對(duì)于中國區(qū)帳號(hào)購買的IAP非区,開發(fā)者的實(shí)際分成在68%~69%之間。而且中國以外不同地區(qū)的交易稅標(biāo)準(zhǔn)也存在差異盹廷,如蘋果的官方價(jià)格等級(jí)文檔
征绸,如果需要嚴(yán)格計(jì)算實(shí)際收入,可能需要把這個(gè)部分也考慮進(jìn)來俄占。
針對(duì)不同地區(qū)的內(nèi)購管怠,內(nèi)購價(jià)格和對(duì)應(yīng)的開發(fā)者實(shí)際收入在蘋果的價(jià)格等級(jí)表中有詳細(xì)列舉。
另外缸榄,根據(jù)蘋果在2016年6月的新規(guī)則排惨,針對(duì)Auto-Renewable Subscription類型的IAP,如果用戶購買的訂閱時(shí)間超過1年碰凶,那么從第二年開始,開發(fā)者可以獲得85%的分成鹿驼。詳情可查看蘋果的訂閱產(chǎn)品價(jià)格說明
沙箱賬戶
新的內(nèi)購產(chǎn)品上線之前欲低,測(cè)試人員一般需要對(duì)內(nèi)購產(chǎn)品進(jìn)行測(cè)試,但是內(nèi)購涉及到錢畜晰,所以蘋果為內(nèi)購測(cè)試提供了 沙箱測(cè)試賬號(hào) 的功能砾莱,Apple Pay 推出之后 沙箱測(cè)試賬號(hào)`也可以用于 Apple Pay 支付的測(cè)試,沙箱測(cè)試賬號(hào) 簡單理解就是:只能用于內(nèi)購和 Apple Pay 測(cè)試功能的 Apple ID凄鼻,它并不是真實(shí)的 Apple ID腊瑟。
填寫沙箱測(cè)試賬號(hào)信息需要注意以下幾點(diǎn):
- 電子郵件不能是別人已經(jīng)注冊(cè)過 AppleID 的郵箱
- 電子郵箱可以不是真實(shí)的郵箱聚假,但是必須符合郵箱格式
- App Store 地區(qū)的選擇,測(cè)試的時(shí)候彈出的提示框以及結(jié)算的價(jià)格會(huì)按照沙箱賬號(hào)選擇的地區(qū)來闰非,建議測(cè)試的時(shí)候新建幾個(gè)不同地區(qū)的賬號(hào)進(jìn)行測(cè)試1旄瘛!财松!
沙箱賬號(hào)測(cè)試的使用:
- 首先沙箱測(cè)試賬號(hào)必須在真機(jī)環(huán)境下進(jìn)行測(cè)試瘪贱,并且是 adhoc 證書或者 develop 證書簽名的安裝包,沙盒賬號(hào)不支持直接從 App Store 下載的安裝包
- 去真機(jī)的 App Store 退出真實(shí)的 Apple ID 賬號(hào)辆毡,退出之后并不需要在App Store 里面登錄沙箱測(cè)試賬號(hào)
- 然后去 App 里面測(cè)試購買商品菜秦,會(huì)彈出登錄框,選擇
使用現(xiàn)有的 Apple ID
舶掖,然后登錄沙箱測(cè)試賬號(hào)球昨,登錄成功之后會(huì)彈出購買提示框,點(diǎn)擊購買
眨攘,然后會(huì)彈出提示框完成購買主慰。
內(nèi)購流程
IAP的支付流程分為客戶端和服務(wù)端,客戶端的工作如下:
- 獲取內(nèi)購產(chǎn)品列表(從App內(nèi)讀取或從自己服務(wù)器讀绕谌)河哑,向用戶展示內(nèi)購列表
- 用戶選擇某個(gè)內(nèi)購產(chǎn)品后,先請(qǐng)求可用的內(nèi)購產(chǎn)品的本地化信息列表龟虎,此次調(diào)用Apple的StoreKit庫的代碼
- 得到內(nèi)購產(chǎn)品的本地化信息后璃谨,根據(jù)用戶選擇的內(nèi)購產(chǎn)品的ID得到內(nèi)購產(chǎn)品
- 根據(jù)內(nèi)購產(chǎn)品發(fā)起IAP購買請(qǐng)求,收到購買完成的回調(diào)
- 購買流程結(jié)束后, 向服務(wù)器發(fā)起驗(yàn)證憑證以及支付結(jié)果的請(qǐng)求
- 自己的服務(wù)器將支付結(jié)果信息返回給前端并發(fā)放虛擬產(chǎn)品
前端支付流程圖如下:
------------------------------ IAPManager.h -----------------------------
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef enum {
IAPPurchSuccess = 0, // 購買成功
IAPPurchFailed = 1, // 購買失敗
IAPPurchCancel = 2, // 取消購買
IAPPurchVerFailed = 3, // 訂單校驗(yàn)失敗
IAPPurchVerSuccess = 4, // 訂單校驗(yàn)成功
IAPPurchNotArrow = 5, // 不允許內(nèi)購
}IAPPurchType;
typedef void (^IAPCompletionHandle)(IAPPurchType type,NSData *data);
@interface IAPManager : NSObject
+ (instancetype)shareIAPManager;
- (void)startPurchaseWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle;
@end
NS_ASSUME_NONNULL_END
------------------------------ IAPManager.m -----------------------------
#import "IAPManager.h"
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
@interface IAPManager()<SKPaymentTransactionObserver,SKProductsRequestDelegate>{
NSString *_currentPurchasedID;
IAPCompletionHandle _iAPCompletionHandle;
}
@end
@implementation IAPManager
+ (instancetype)shareIAPManager{
static IAPManager *iAPManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
iAPManager = [[IAPManager alloc] init];
});
return iAPManager;
}
- (instancetype)init{
self = [super init];
if (self) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
- (void)dealloc{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
- (void)startPurchaseWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle{
if (purchID) {
if ([SKPaymentQueue canMakePayments]) {
_currentPurchasedID = purchID;
_iAPCompletionHandle = handle;
//從App Store中檢索關(guān)于指定產(chǎn)品列表的本地化信息
NSSet *nsset = [NSSet setWithArray:@[purchID]];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}else{
[self handleActionWithType:IAPPurchNotArrow data:nil];
}
}
}
- (void)handleActionWithType:(IAPPurchType)type data:(NSData *)data{
#if DEBUG
switch (type) {
case IAPPurchSuccess:
NSLog(@"購買成功");
break;
case IAPPurchFailed:
NSLog(@"購買失敗");
break;
case IAPPurchCancel:
NSLog(@"用戶取消購買");
break;
case IAPPurchVerFailed:
NSLog(@"訂單校驗(yàn)失敗");
break;
case IAPPurchVerSuccess:
NSLog(@"訂單校驗(yàn)成功");
break;
case IAPPurchNotArrow:
NSLog(@"不允許程序內(nèi)付費(fèi)");
break;
default:
break;
}
#endif
if(_iAPCompletionHandle){
_iAPCompletionHandle(type,data);
}
}
- (void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction{
//交易驗(yàn)證
NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:recepitURL];
if(!receipt){
// 交易憑證為空驗(yàn)證失敗
[self handleActionWithType:IAPPurchVerFailed data:nil];
return;
}
// 購買成功將交易憑證發(fā)送給服務(wù)端進(jìn)行再次校驗(yàn)
[self handleActionWithType:IAPPurchSuccess data:receipt];
NSError *error;
NSDictionary *requestContents = @{
@"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) { // 交易憑證為空驗(yàn)證失敗
[self handleActionWithType:IAPPurchVerFailed data:nil];
return;
}
NSString *serverString = @"https:xxxx";
NSURL *storeURL = [NSURL URLWithString:serverString];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];
[[NSURLSession sharedSession] dataTaskWithRequest:storeRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
// 無法連接服務(wù)器,購買校驗(yàn)失敗
[self handleActionWithType:IAPPurchVerFailed data:nil];
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) {
// 服務(wù)器校驗(yàn)數(shù)據(jù)返回為空校驗(yàn)失敗
[self handleActionWithType:IAPPurchVerFailed data:nil];
}
NSString *status = [NSString stringWithFormat:@"%@",jsonResponse[@"status"]];
if(status && [status isEqualToString:@"0"]){
[self handleActionWithType:IAPPurchVerSuccess data:nil];
} else {
[self handleActionWithType:IAPPurchVerFailed data:nil];
}
#if DEBUG
NSLog(@"----驗(yàn)證結(jié)果 %@",jsonResponse);
#endif
}
}];
// 驗(yàn)證成功與否都注銷交易,否則會(huì)出現(xiàn)虛假憑證信息一直驗(yàn)證不通過,每次進(jìn)程序都得輸入蘋果賬號(hào)
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *product = response.products;
if([product count] <= 0){
#if DEBUG
NSLog(@"--------------沒有商品------------------");
#endif
return;
}
SKProduct *p = nil;
for(SKProduct *pro in product){
if([pro.productIdentifier isEqualToString:_currentPurchasedID]){
p = pro;
break;
}
}
#if DEBUG
NSLog(@"productID:%@", response.invalidProductIdentifiers);
NSLog(@"產(chǎn)品付費(fèi)數(shù)量:%lu",(unsigned long)[product count]);
NSLog(@"產(chǎn)品描述:%@",[p description]);
NSLog(@"產(chǎn)品標(biāo)題%@",[p localizedTitle]);
NSLog(@"產(chǎn)品本地化描述%@",[p localizedDescription]);
NSLog(@"產(chǎn)品價(jià)格:%@",[p price]);
NSLog(@"產(chǎn)品productIdentifier:%@",[p productIdentifier]);
#endif
SKPayment *payment = [SKPayment paymentWithProduct:p];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
//請(qǐng)求失敗
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
#if DEBUG
NSLog(@"------------------從App Store中檢索關(guān)于指定產(chǎn)品列表的本地化信息錯(cuò)誤-----------------:%@", error);
#endif
}
- (void)requestDidFinish:(SKRequest *)request{
#if DEBUG
NSLog(@"------------requestDidFinish-----------------");
#endif
}
#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
for (SKPaymentTransaction *tran in transactions) {
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
[self verifyPurchaseWithPaymentTransaction:tran];
break;
case SKPaymentTransactionStatePurchasing:
#if DEBUG
NSLog(@"商品添加進(jìn)列表");
#endif
break;
case SKPaymentTransactionStateRestored:
#if DEBUG
NSLog(@"已經(jīng)購買過商品");
#endif
// 消耗型不支持恢復(fù)購買
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:tran];
break;
default:
break;
}
}
}
// 交易失敗
- (void)failedTransaction:(SKPaymentTransaction *)transaction{
if (transaction.error.code != SKErrorPaymentCancelled) {
[self handleActionWithType:IAPPurchFailed data:nil];
}else{
[self handleActionWithType:IAPPurchCancel data:nil];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
@end
/* 調(diào)用支付方法
- (void)purchaseWithProductID:(NSString *)productID{
[[IAPManager shareIAPManager] startPurchaseWithID:productID completeHandle:^(IAPPurchType type,NSData *data) {
}];
}
*/
服務(wù)端的工作:
- 接收iOS端發(fā)過來的購買憑證鲤妥,判斷憑證是否已經(jīng)存在或驗(yàn)證過佳吞,然后存儲(chǔ)該憑證。將該憑證發(fā)送到蘋果的服務(wù)器驗(yàn)證棉安,并將驗(yàn)證結(jié)果返回給客戶端底扳。
恢復(fù)購買
內(nèi)購有4種:消耗型項(xiàng)目,非消耗型贡耽,自動(dòng)續(xù)期訂閱衷模,非續(xù)期訂閱。 其中”非消耗型“和”自動(dòng)續(xù)期訂閱“需要提供恢復(fù)購買的功能蒲赂,例如創(chuàng)建一個(gè)恢復(fù)按鈕阱冶,不然審核很可能會(huì)被拒絕。
//調(diào)起蘋果內(nèi)購恢復(fù)接口
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
“消耗型項(xiàng)目”和“非續(xù)期訂閱”蘋果不會(huì)提供恢復(fù)的接口滥嘴,不要調(diào)用上述方法去恢復(fù)木蹬,否則有可能被拒!H糁濉镊叁!
“非續(xù)期訂閱”也是跨設(shè)備同步的尘颓,所以原則上來說也需要提供恢復(fù)購買的功能,但需要依靠app自建的賬戶體系恢復(fù)晦譬,不能用上述蘋果提供的接口疤苹。
內(nèi)購掉單
掉單是用戶付款買商品,錢扣了蛔添,商品卻沒到賬痰催。掉單一旦發(fā)生,用戶通常會(huì)很生氣地來找客服迎瞧。然后客服只能找開發(fā)人員把商品給用戶手動(dòng)加上夸溶。顯然,傷害用戶的體驗(yàn)凶硅,特別是傷害付費(fèi)用戶的體驗(yàn)缝裁,是一件相當(dāng)糟糕的事情。
掉單是如何產(chǎn)生的呢足绅?這需要從IAP支付的技術(shù)流程說起捷绑。
IAP的支付流程:
1,發(fā)起支付
2氢妈,扣費(fèi)成功
3粹污,得到receipt(支付憑據(jù))
4,去后臺(tái)驗(yàn)證憑據(jù)獲取商品交易狀態(tài)
5首量,返回?cái)?shù)據(jù)壮吩,驗(yàn)證成功前端刷新數(shù)據(jù)
-
漏單情況一:
2到3環(huán)節(jié)出問題屬于蘋果的問題,目前沒做處理加缘。
- 漏單情況二:
3到4的時(shí)候出問題鸭叙,比如斷網(wǎng)。此時(shí)前端會(huì)把支付憑據(jù)持久化存儲(chǔ)下來拣宏,如果期間用戶卸載APP此單在前端就真漏了沈贝,如果沒有協(xié)助,下次重新打開app進(jìn)入購買頁會(huì)先判斷有無未成功的支付勋乾,有就提示用戶宋下,用戶選擇找回,重走4辑莫,5流程杨凑。這一步看產(chǎn)品需求怎么做,可以讓用戶自主選擇是否恢復(fù)未成功的支付也可以前端默默恢復(fù)就行摆昧。
- 漏單情況三:
4到5的時(shí)候出問題。此時(shí)后臺(tái)其實(shí)已經(jīng)成功蜒程,只是前端沒獲取到數(shù)據(jù)绅你,當(dāng)漏單處理伺帘,下次進(jìn)入的時(shí)候先刷新數(shù)據(jù)即可。
內(nèi)購注意事項(xiàng)
- 交易憑據(jù)receipt判重
一般來說驗(yàn)證支付憑據(jù)(receipt)是否有效放后臺(tái)去做忌锯,如果后臺(tái)不做判重伪嫁,同一個(gè)憑據(jù)就可以無數(shù)次驗(yàn)證通過,因?yàn)樘O果也不判重偶垮,這就會(huì)導(dǎo)致前端可以憑此取到的一個(gè)支付憑據(jù)可以去后臺(tái)無數(shù)次做校驗(yàn)U趴取!K贫妗脚猾!,后臺(tái)就會(huì)給前端發(fā)放無數(shù)次商品砚哗,但是用戶只支付了一次錢龙助,所以安全的做法是后臺(tái)把驗(yàn)證通過的支付憑據(jù)做個(gè)記錄,每次來新的憑據(jù)先判斷是否已經(jīng)使用過蛛芥,防止多次發(fā)放商品提鸟。