我也是第一次接觸iOS內(nèi)購示罗,其實(shí)一直以來我接觸的公司都是不愿意接入內(nèi)購的妖啥,畢竟要給蘋果分成埠居,所以也就沒有學(xué)習(xí)IAP流程編寫诞丽。最近公司考慮到虛擬會(huì)員防審核期間可以把iOS內(nèi)購作為備用方案,所以也就把iOS內(nèi)購流程做個(gè)梳理并根據(jù)咋們公司的業(yè)務(wù)特點(diǎn)寫了非續(xù)期訂購類型的IAP代碼拐格。
剛開始接觸內(nèi)購,也是千頭萬緒刑赶,所以現(xiàn)在網(wǎng)上找了幾篇IAP文章看了下捏浊,結(jié)合自己的整理,先總結(jié)了IAP大致流程撞叨。
前期準(zhǔn)備工作:
1. 閱讀蘋果的《App內(nèi)購買項(xiàng)目》文檔
2.去App Store connect稅務(wù)里面簽署內(nèi)購協(xié)議
登陸Apple開發(fā)官網(wǎng)金踪,選擇connect:
3.配置內(nèi)購項(xiàng)目,“App -->功能-->APP內(nèi)購買項(xiàng)目”
具體配置可以參考創(chuàng)建及發(fā)布說明
牵敷,下面是我的演示實(shí)例:
選擇“我的APP”
然后選擇你要接入內(nèi)購的APP胡岔,點(diǎn)擊進(jìn)入,選擇功能-APP內(nèi)購買項(xiàng)目
然后點(diǎn)擊+新增一個(gè)內(nèi)購商品枷餐,彈出下面對話框:
這里說明一下靶瘸,內(nèi)購產(chǎn)品分為4種熄赡,分別消耗浮声、非消耗、續(xù)期孟抗、非續(xù)期润匙,具體解釋整理如下:
蘋果的內(nèi)購分以下四類商品:
1诗眨、消耗型項(xiàng)目
只可使用一次的產(chǎn)品,使用之后即失效孕讳,必須再次購買匠楚。
示例:釣魚 App 中的魚食巍膘。
2、非消耗型項(xiàng)目
只需購買一次芋簿,不會(huì)過期或隨著使用而減少的產(chǎn)品峡懈。
示例:游戲 App 的賽道。
3益咬、自動(dòng)續(xù)期訂閱
允許用戶在固定時(shí)間段內(nèi)購買動(dòng)態(tài)內(nèi)容的產(chǎn)品逮诲。除非用戶選擇取消,否則此類訂閱會(huì)自動(dòng)續(xù)期幽告。
示例:每月訂閱提供流媒體服務(wù)的 App梅鹦。
4、非續(xù)期訂閱
允許用戶購買有時(shí)限性服務(wù)的產(chǎn)品冗锁。此 App 內(nèi)購買項(xiàng)目的內(nèi)容可以是靜態(tài)的齐唆。此類訂閱不會(huì)自動(dòng)續(xù)期。
示例:為期一年的已歸檔文章目錄訂閱冻河。
4.Xcode capablities 打開IAP開關(guān)
IAP內(nèi)購流程圖:
代碼實(shí)現(xiàn):
1.判斷用戶是否具備支付權(quán)限
- (BOOL)canMakePurchase {
if ([SKPaymentQueue canMakePayments]) {
return YES;
}else {
//用戶未開啟內(nèi)購箍邮,彈框提示
UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"內(nèi)購未開啟" message:@"進(jìn)入“【設(shè)置】 - 開啟【屏幕使用時(shí)間】功能。然后在【屏幕使用時(shí)間】選項(xiàng)中選擇【內(nèi)容和隱私訪問限制】叨叙,選擇【iTunes Store 與 App store 購買】- 選擇【App內(nèi)購項(xiàng)目】- 選擇“允許”锭弊,將該功能開啟" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alertView show];
//內(nèi)購結(jié)束
if (self.IAPurchaseResult) {
self.IAPurchaseResult(IAPurchaseNotAllow);
}
return NO;
}
}
2.獲取Apple內(nèi)購商品列表
- (void)fetchIAPProducts:(void(^)(void))block {
if (!self.productResp) {
//如果沒有商品信息,異步接口獲取
Weakify(self);
[self queryByPuoductId:IAProductID productInfoReuslts:^(SKProductsResponse * _Nonnull resp) {
if (resp == nil) {
if (weakself.IAPurchaseResult) {
weakself.IAPurchaseResult(IAPurchaseFailed);
}
return ;
}
if (resp.products.count == 0) {
if (weakself.IAPurchaseResult) {
weakself.IAPurchaseResult(IAPurchaseNoProducts);
}
return ;
}
if (block) {
block();
}
}];
}else {
if (block) {
block();
}
}
}
//通過產(chǎn)品ID查詢商品信息
- (void)queryByPuoductId:(NSString *)productId
productInfoReuslts:(void(^)(SKProductsResponse *resp))block {
self.fetchProductBlock = block;
NSSet *set = [NSSet setWithObject:productId];
SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
request.delegate = self;
[request start];
}
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(nonnull SKProductsRequest *)request didReceiveResponse:(nonnull SKProductsResponse *)response {
self.productResp = response;
if (self.fetchProductBlock) {
self.fetchProductBlock(response);
}
}
- (void)requestDidFinish:(SKRequest *)request {
//do nothting
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
self.productResp = nil;
if (self.fetchProductBlock) {
self.fetchProductBlock(nil);
}
}
3.創(chuàng)建蘋果內(nèi)購支付
- (void)createInPurchasePay {
SKProduct *product = nil;
for (SKProduct *prod in self.productResp.products) {
if ([prod.productIdentifier isEqualToString:IAProductID]) {
product = prod;
break;
}
}
if (!product) {
if (self.IAPurchaseResult) {
self.IAPurchaseResult(IAPurchaseNoProducts);
}
return;
}
SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
//透傳業(yè)務(wù)訂單
payment.applicationUsername = self.orderId;
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
#pragma mark - 監(jiān)聽用戶支付交易變化
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased://交易完成
[self verifyReceiptByTransaction:transaction];
break;
case SKPaymentTransactionStateFailed://交易失敗
[self failTransation:transaction];
break;
case SKPaymentTransactionStateRestored://已經(jīng)購買過該商品
[self verifyReceiptByTransaction:transaction];
break;
case SKPaymentTransactionStatePurchasing: //商品添加進(jìn)列表
//解決applicationUsername支付一半kill進(jìn)程后為nil的問題
[self saveCurrTransationBindedOrderId];
break;
default:
break;
}
}
}
//交易失敗
- (void)failTransation:(SKPaymentTransaction *)transaction {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
IAPurchaseStatus status = IAPurchaseFailed;
if (transaction.error.code != SKErrorPaymentCancelled) {
status = IAPurchaseCancel;
}
if (self.IAPurchaseResult) {
self.IAPurchaseResult(status);
}
}
//持久化當(dāng)前正在交易綁定的業(yè)務(wù)訂單
- (void)saveCurrTransationBindedOrderId {
NSLog(@"商品添加進(jìn)列表");
if (self.orderId) {
NSDictionary *orderdic = @{@"productId":IAProductID,
@"orderId": self.orderId
};
[[NSUserDefaults standardUserDefaults] setObject:orderdic forKey:@"persient.IAP.order"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
- (NSString *)bindedOrderId {
NSDictionary *dic = [[NSUserDefaults standardUserDefaults] objectForKey:@"persient.IAP.order"];
if (dic) {
return dic[@"orderId"];
}else {
return nil;
}
}
4.驗(yàn)證票據(jù)
- (void)verifyReceiptByTransaction:(SKPaymentTransaction *)transaction {
NSString *receiptString = [self iapReceipt];
if (!receiptString) {
if (self.IAPurchaseResult) {
self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
}
return;
}
NSError *error;
NSDictionary *requestContents = @{@"receipt-data": receiptString};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) { // 交易憑證為空驗(yàn)證失敗
if (self.IAPurchaseResult) {
self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
}
return;
}
//向蘋果服務(wù)器驗(yàn)證支付憑據(jù)真實(shí)性
[self verifyRequestData:requestData testSandbox:NO transaction:transaction];
}
//獲取內(nèi)購票據(jù)
- (NSString *)iapReceipt {
NSString *receiptString = nil;
NSURL *rereceiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:rereceiptURL];
receiptString = [receipt base64EncodedStringWithOptions:0];
return receiptString;
}
- (void)verifyRequestData:(NSData *)postData
testSandbox:(BOOL)test
transaction:(SKPaymentTransaction *)transaction
{
NSString *url = @"https://buy.itunes.apple.com/verifyReceipt";
if (test) {
url = @"https://sandbox.itunes.apple.com/verifyReceipt";
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
request.HTTPBody = postData;
static NSString *requestMethod = @"POST";
request.HTTPMethod = requestMethod;
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
// 無法連接服務(wù)器,購買校驗(yàn)失敗
if (self.IAPurchaseResult) {
self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
}
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) {
// 蘋果服務(wù)器校驗(yàn)數(shù)據(jù)返回為空校驗(yàn)失敗
if (self.IAPurchaseResult) {
self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
return ;
}
//先驗(yàn)證正式服務(wù)器,如果正式服務(wù)器返回21007再去蘋果測試服務(wù)器驗(yàn)證,沙盒測試環(huán)境蘋果用的是測試服務(wù)器
NSString *status = [NSString stringWithFormat:@"%@", jsonResponse[@"status"]];
if (status && [status isEqualToString:@"21007"]) {
[self verifyRequestData:postData testSandbox:YES transaction:transaction];
} else if (status && [status isEqualToString:@"0"]) {
//訂單校驗(yàn)成功,給蝸蝸生活訂單會(huì)員充值
NSString *orderId = transaction.payment.applicationUsername;
if (!orderId) {
orderId = [self bindedOrderId];
}
[self chargeWowoVipOrderId:orderId];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}else {
// 蘋果服務(wù)器校驗(yàn)數(shù)據(jù)返回為空校驗(yàn)失敗
if (self.IAPurchaseResult) {
self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
}
}];
}
5.最后一步擂错,給會(huì)員訂單沖會(huì)員
- (void)chargeWowoVipOrderId:(NSString *)orderId {
if (!orderId.length) {
if (self.IAPurchaseResult) {
self.IAPurchaseResult(IAPurchaseVerifyReciptSuccess);
}
return;
}
//開始調(diào)用充值接口...
if (self.IAPurchaseResult) {
self.IAPurchaseResult(IAPurchaseSuccess);
}
}
關(guān)于漏單的問題
由于用戶可能在支付過程中中途網(wǎng)絡(luò)不佳味滞,或者程序突然crash的情況下,有可能用戶支付成功了钮呀,但是驗(yàn)證票據(jù)等后續(xù)操作沒有走完剑鞍,也就沒有給用戶實(shí)際充值的情況。
這種情況下爽醋,我們可以將payment監(jiān)聽放到APP啟動(dòng)里啟用全局監(jiān)聽蚁署,那么下次APP啟動(dòng)后,會(huì)重新走支付交易事務(wù)變化的監(jiān)聽蚂四,就可以繼續(xù)完成票據(jù)驗(yàn)證以及給用戶充值的操作光戈。
我們就可以將內(nèi)購類設(shè)計(jì)成單例模式,在init時(shí)候即添加全局通知遂赠,然后實(shí)現(xiàn)SKPaymentTransactionObserver委托:
//單例模式
+ (instancetype)shared {
dispatch_once(&onceToken, ^{
sharedInstance = [[InAppPurchaseManager alloc] init];
});
return sharedInstance;
}
- (instancetype)init {
if (self = [super init]) {
//添加支付交易的全局Observer
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
然后我們再appDlegate的啟動(dòng)方法里調(diào)用如下方法:
/**
內(nèi)購準(zhǔn)備環(huán)境(在appDelegateAPP每次啟動(dòng)時(shí)調(diào)用)
*/
- (void)parpareIAP {
[self queryByPuoductId:IAProductID productInfoReuslts:^(SKProductsResponse * _Nonnull resp) {
}];
}
這樣appDelegate會(huì)自動(dòng)添加了全局事務(wù)觀察了田度。
關(guān)于applicationUsername為nil的問題
我們將我們自己的業(yè)務(wù)訂單ID跟Apple的支付事務(wù)是通過applicationUsername這個(gè)屬性關(guān)聯(lián)的。但是蘋果并不幫我們將這個(gè)屬性做了持久化操作解愤,只在內(nèi)存中镇饺。
復(fù)現(xiàn)場景:當(dāng)用戶殺掉APP后,重新打開APP后送讲,上次的訂單ID透傳給applicationUsername=nil,也就是訂單ID丟失了奸笤,那么后續(xù)給用戶充值的重要入?yún)⒂唵蜪D沒有了惋啃,也就無法充值。
解決方案:
這個(gè)解決方案监右,我是參考了這篇作者提供的思路:貝聊 IAP 實(shí)戰(zhàn)之訂單綁定边灭,粗放性訂單持久化。
上述思路實(shí)現(xiàn):
//持久化當(dāng)前正在交易綁定的業(yè)務(wù)訂單
- (void)saveCurrTransationBindedOrderId {
NSLog(@"商品添加進(jìn)列表");
if (self.orderId) {
NSDictionary *orderdic = @{@"productId":IAProductID,
@"orderId": self.orderId
};
[[NSUserDefaults standardUserDefaults] setObject:orderdic forKey:@"persient.IAP.order"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
以上健盒,內(nèi)購IAP完結(jié)绒瘦!