內(nèi)購是啥
App 內(nèi)購買項目允許顧客通過訪問 App Store 購買您 App 中的內(nèi)容、功能或服務(wù),并安全處理來自用戶的付款癌淮。
詳情傳送門https://help.apple.com/itunes-connect/developer/#/devb57be10e7
下面來說內(nèi)購集成流程
1.協(xié)議
登錄蘋果開發(fā)者中心翰撑,進入iTunes Connect,再進入“協(xié)議无虚、稅務(wù)和銀行業(yè)務(wù)”頁面,如圖
點擊進入可以看到,目前共有兩個分組衍锚,三種合同友题。(此處有坑,比如我們當前賬號不能申請合同戴质!如下圖)
Request Contracts 可以申請的合同度宦;
Contracts In Effect 已經(jīng)生效的合同。
三種合同分別是
Free Applications 免費應(yīng)用(默認已經(jīng)生效)告匠;
Paid Applications 付費應(yīng)用戈抄,需要申請;
iAd App Network 廣告應(yīng)用后专,需要申請划鸽。
內(nèi)購對應(yīng)的是Paid Applications 付費應(yīng)用,需要申請,如圖2.(如果Request按鈕不顯示,則說明當前賬號權(quán)限有問題)
點擊Request完善信息,提交就行.
2.內(nèi)購集成
內(nèi)購實現(xiàn)流程:
1.客戶端向Appstore請求購買產(chǎn)品(假設(shè)產(chǎn)品信息已經(jīng)取得)戚哎,Appstore驗證產(chǎn)品成功后裸诽,從用戶的Apple賬戶余額中扣費。
2.Appstore向客戶端返回一段receipt-data型凳,里面記錄了本次交易的證書和簽名信息丈冬。
3.客戶端向我們可以信任的服務(wù)器提供receipt-data
4.服務(wù)器對receipt-data進行一次base64編碼
5.把編碼后的receipt-data發(fā)往itunes.appstore進行驗證
6.itunes.appstore返回驗證結(jié)果給服務(wù)器
7.服務(wù)器對商品購買狀態(tài)以及商品類型,向客戶端發(fā)放相應(yīng)的道具與推送數(shù)據(jù)更新通知
注,下圖3步驟和上面流程不是一一對應(yīng)
我項目里面的購買流程,加入了一點業(yè)務(wù)邏輯和后臺驗證流程,有什么問題歡迎大家指出.
3.去蘋果開發(fā)者中心創(chuàng)建內(nèi)購商品
如下圖5,點擊+號去創(chuàng)建內(nèi)購商品,產(chǎn)品id最好是當前應(yīng)用+數(shù)字,價格區(qū)間蘋果提供了一張表,商品價格只能是表上的價格,蘋果會抽取30%,商家能收到的錢是用戶充值的70%.這就造成了部分平臺區(qū)分安卓和蘋果.兩端賬號不互通,也造就了代充行業(yè),再次就不展開說了.
商品價格大于100$,提交審核的時候要說明這個金額是確認過的,不然可能會被拒
4.代碼集成
建議單獨建一個類來處理內(nèi)購業(yè)務(wù)
.h類
//
// EMAppStorePay.h
// MobileFixCar
//
// Created by Wcting on 2018/4/11.
// Copyright ? 2018年 XXX有限公司. All rights reserved.
//
/*
wct20180917 內(nèi)購支付類甘畅,短視頻e豆購買用到埂蕊。
*/
#import <Foundation/Foundation.h>
@class EMAppStorePay;
@protocol EMAppStorePayDelegate <NSObject>;
@optional
/**
wct20180418 內(nèi)購支付成功回調(diào)
@param appStorePay 當前類
@param dicValue 返回值
@param error 錯誤信息
*/
- (void)EMAppStorePay:(EMAppStorePay *)appStorePay responseAppStorePaySuccess:(NSDictionary *)dicValue error:(NSError*)error;
/**
wct20180423 內(nèi)購支付結(jié)果回調(diào)提示
@param appStorePay 當前類
@param dicValue 返回值
@param error 錯誤信息
*/
- (void)EMAppStorePay:(EMAppStorePay *)appStorePay responseAppStorePayStatusshow:(NSDictionary *)dicValue error:(NSError*)error;
@end
@interface EMAppStorePay : NSObject
@property (nonatomic, weak)id<EMAppStorePayDelegate> delegate;/**<wct20180418 delegate*/
/**
wct20180411 點擊購買
@param goodsID 商品id
*/
-(void)starBuyToAppStore:(NSString *)goodsID;
@end
.m類(里面有客戶端驗證receipt的代碼,解開注釋就可以,用于調(diào)試.驗證建議放后臺去做)
//
// EMAppStorePay.m
// MobileFixCar
//
// Created by Wcting on 2018/4/11.
// Copyright ? 2018年 XXX有限公司. All rights reserved.
//
#import "EMAppStorePay.h"
#import <StoreKit/StoreKit.h>
//#define goods1 @"net.ejiajx.MobileFixCar06"
@interface EMAppStorePay()<SKPaymentTransactionObserver,SKProductsRequestDelegate>
@property (nonatomic, strong)NSString *goodsId;/**<wct20180420 商品id*/
@end
@implementation EMAppStorePay
- (instancetype)init
{
self = [super init];
if (self) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];// 4.設(shè)置支付服務(wù)
}
return self;
}
//結(jié)束后一定要銷毀
- (void)dealloc
{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
#pragma mark - public
-(void)starBuyToAppStore:(NSString *)goodsID
{
if ([SKPaymentQueue canMakePayments]) {//5.判斷app是否允許apple支付
[self getRequestAppleProduct:goodsID];// 6.請求蘋果后臺商品
} else {
// NSLog(@"not");
}
}
#pragma mark - private
#pragma mark ------ 請求蘋果商品
- (void)getRequestAppleProduct:(NSString *)goodsID
{
self.goodsId = goodsID;//把前面?zhèn)鬟^來的商品id記錄一下往弓,下面要用
// 7.這里的com.czchat.CZChat01就對應(yīng)著蘋果后臺的商品ID,他們是通過這個ID進行聯(lián)系的。
NSArray *product = [[NSArray alloc] initWithObjects:goodsID,nil];
NSSet *nsset = [NSSet setWithArray:product];
//SKProductsRequest參考鏈接:https://developer.apple.com/documentation/storekit/skproductsrequest
//SKProductsRequest 一個對象蓄氧,可以從App Store檢索有關(guān)指定產(chǎn)品列表的本地化信息函似。
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];// 8.初始化請求
request.delegate = self;
[request start];// 9.開始請求
}
#pragma mark ------ 支付完成
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
//交易驗證 本地驗證方法
/*NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:recepitURL];
if(!receipt){
}
NSError *error;
NSDictionary *requestContents = @{
@"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSLog(@"requestContentstr:%@",[receipt base64EncodedStringWithOptions:0]);
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
//In the test environment, use https://sandbox.itunes.apple.com/verifyReceipt
//In the real environment, use https://buy.itunes.apple.com/verifyReceipt
// Create a POST request with the receipt data.
NSURL *storeURL = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];
// Make a connection to the iTunes Store on a background queue.
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) { }
//Parse the Response
NSLog(@"成功了:%@",jsonResponse);
}
}];*/
//此時告訴后臺交易成功,并把receipt傳給后臺驗證
NSString *transactionReceiptString= nil;
//系統(tǒng)IOS7.0以上獲取支付驗證憑證的方式應(yīng)該改變匀们,切驗證返回的數(shù)據(jù)結(jié)構(gòu)也不一樣了缴淋。
// 驗證憑據(jù)准给,獲取到蘋果返回的交易憑據(jù)
// appStoreReceiptURL iOS7.0增加的泄朴,購買交易完成后,會將憑據(jù)存放在該地址
NSURLRequest *appstoreRequest = [NSURLRequest requestWithURL:[[NSBundle mainBundle] appStoreReceiptURL]];
NSError *error = nil;
// 從沙盒中獲取到購買憑據(jù)
NSData * receiptData = [NSURLConnection sendSynchronousRequest:appstoreRequest returningResponse:nil error:&error];
// 20 BASE64 常用的編碼方案露氮,通常用于數(shù)據(jù)傳輸祖灰,以及加密算法的基礎(chǔ)算法,傳輸過程中能夠保證數(shù)據(jù)傳輸?shù)姆€(wěn)定性 21 BASE64是可以編碼和解碼的 22
transactionReceiptString = [receiptData base64EncodedStringWithOptions:0];//[receiptData base64EncodedStringWithOptions:0];
// NSLog(@"requestContentstr:%@",[receiptData base64EncodedStringWithOptions:0]);
// NSDictionary *dic = @{@"orderCode":self.dataOrder.orderCode,
// @"receipt":transactionReceiptString,
// @"category":@"1"
// };
// NSLog(@"diczhi:%@",dic);
//
// self.tran = transaction;
// [self.bizEBeanBuy requestAppStorePaySuccessCallBack:dic];//蘋果支付成功畔规,傳receipt-data給后臺驗證
if (self.delegate && [self.delegate respondsToSelector:@selector(EMAppStorePay:responseAppStorePaySuccess:error:)]) {
[self.delegate EMAppStorePay:self responseAppStorePaySuccess:@{@"value":transactionReceiptString} error:nil];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
#pragma mark - delegate
#pragma mark ------ SKProductsRequestDelegate
// 10.接收到產(chǎn)品的返回信息,然后用返回的商品信息進行發(fā)起購買請求
- (void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *product = response.products;
if([product count] == 0){//如果服務(wù)器沒有產(chǎn)品
return;
}
SKProduct *requestProduct = nil;
for (SKProduct *pro in product) {
// NSLog(@"%@", [pro description]);
// NSLog(@"%@", [pro localizedTitle]);
// NSLog(@"%@", [pro localizedDescription]);
// NSLog(@"%@", [pro price]);
// NSLog(@"%@", [pro productIdentifier]);
// 11.如果后臺消費條目的ID與我這里需要請求的一樣(用于確保訂單的正確性)
if([pro.productIdentifier isEqualToString:self.goodsId]){
requestProduct = pro;
}
}
// 12.發(fā)送購買請求局扶,創(chuàng)建票據(jù) 這個時候就會有彈框了
SKPayment *payment = [SKPayment paymentWithProduct:requestProduct];
[[SKPaymentQueue defaultQueue] addPayment:payment];//將票據(jù)加入到交易隊列
}
#pragma mark ------ SKRequestDelegate (@protocol SKProductsRequestDelegate <SKRequestDelegate>)
//請求失敗
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
// NSLog(@"error:%@", error);
}
//反饋請求的產(chǎn)品信息結(jié)束后
- (void)requestDidFinish:(SKRequest *)request
{
// NSLog(@"信息反饋結(jié)束");
}
#pragma mark ------ SKPaymentTransactionObserver 監(jiān)聽購買結(jié)果
// 13.監(jiān)聽購買結(jié)果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction
{
if (self.delegate && [self.delegate respondsToSelector:@selector(EMAppStorePay:responseAppStorePayStatusshow:error:)]) {
[self.delegate EMAppStorePay:self responseAppStorePayStatusshow:@{@"value":transaction} error:nil];
}
// if (transaction.count > 0) {
// //檢測是否有未完成的交易
// SKPaymentTransaction* tran = [transaction firstObject];
// if (tran.transactionState == SKPaymentTransactionStatePurchased) {
// [self completeTransaction:tran];
// [[SKPaymentQueue defaultQueue] finishTransaction:tran];//未完成的交易在此給它結(jié)束
// return;
// }
// }
for(SKPaymentTransaction *tran in transaction){
// NSLog(@"%@",tran.payment.applicationUsername);
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:{
// NSLog(@"交易完成");
// 購買后告訴交易隊列,把這個成功的交易移除掉叁扫。
//走到這就說明這單交易走完了三妈,無論成功失敗,所以要給它移出莫绣。finishTransaction
[self completeTransaction:tran];//這兒出了問題拋異常畴蒲,導(dǎo)致下面一句代碼沒執(zhí)行
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
// NSLog(@"商品添加進列表");
break;
case SKPaymentTransactionStateRestored:
// NSLog(@"已經(jīng)購買過商品");
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
// NSLog(@"交易失敗");
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateDeferred:
// NSLog(@"交易還在隊列里面,但最終狀態(tài)還沒有決定");
break;
default:
break;
}
}
}
@end
5.沙盒測試
如下圖6,點添加創(chuàng)建沙盒測試賬號,賬號未未注冊成AppleID的賬號,測試前先到設(shè)置里退出當前AppleID,登錄沙盒測試賬號,沙盒測試賬號只能用來測試沙盒支付,不具備正常AppleID的功能.
準備工作
1.第一次測試內(nèi)購需要卸載之前APP对室,找開發(fā)人員安裝可測試內(nèi)購的APP模燥。防止App Store下載的app走sandbox環(huán)境走不通;
2.在iPhone設(shè)置里面掩宜,退出原有賬號蔫骂。登錄開發(fā)人員提供的內(nèi)購測試賬號(可找開發(fā)申請新測試賬號);
6.交易安全機制
1.雙重驗證
蘋果審核人員審核內(nèi)購的時候走的是沙盒環(huán)境對應(yīng)沙盒驗證接口https://sandbox.itunes.apple.com/verifyReceipt,如果驗證receipt只有正式環(huán)境https://buy.itunes.apple.com/verifyReceipt,蘋果審核員走內(nèi)購會驗證失敗,交易走不通,后果就是審核被拒.所以驗證的時候先默認走正式環(huán)境,如果返回21007的錯誤碼就去沙盒環(huán)境驗證,保證審核通過.
2.交易憑據(jù)receipt判重
一般我們驗證支付憑據(jù)(receipt)是否有效放后臺去做,如果后臺不做判重,同一個憑據(jù)就可以無數(shù)次驗證通過(蘋果也不判重),后臺就會給前端發(fā)放無數(shù)次商品,但是用戶只支付了一次錢取到一個支付憑據(jù).所以安全的做法是后臺把驗證通過的支付憑據(jù)做個記錄,每次來新的憑據(jù)先判斷是否已經(jīng)使用過,防止多次發(fā)放商品.
3.本地交易流水
在測試過程中,由于蘋果不提供交易流水,所以會出現(xiàn)無法對賬的情況,會提出一些莫名bug,因為測試不知道某個單的支付狀態(tài),這時前端需要做個交易流水記錄,方便對賬和避免不必要的bug及撕逼.
在支付成功回調(diào)里面把當前交易數(shù)據(jù)存在本地持久化,然后去后臺驗證,出問題就那本地存的交易數(shù)據(jù)和后臺對,找出問題.
#pragma mark - EMAppStorePayDelegate
-(void)EMAppStorePay:(EMAppStorePay *)appStorePay responseAppStorePaySuccess:(NSDictionary *)dicValue error:(NSError *)error
{
NSString *transactionReceiptString = [ZSTools objectOrNilForKey:@"value" fromDictionary:dicValue];
NSDictionary *dic = @{@"orderCode":self.strOrderCode,
@"receipt":transactionReceiptString,
@"category":@"1"
};
// NSLog(@"222diczhi:%@",dic);
/*
//wct20180601 本地交易流水牺汤,不測試內(nèi)購就給注釋吧辽旋,省手機內(nèi)存
NSMutableDictionary *dicRec = [NSMutableDictionary dictionaryWithDictionary:self.dicPay];
[dicRec setValue:self.strOrderCode forKey:@"orderCode"];
[dicRec setValue:transactionReceiptString forKey:@"receipt"];
[dicRec setValue:@"1" forKey:@"category"];
NSString *time = [self getCurrentTimes];
[dicRec setValue:time forKey:@"creatTime"];
[self.modelEBean addDicReconciliation:dicRec];//對應(yīng)下面的實現(xiàn)方法
*/
[self.bizEBeanBuy requestAppStorePaySuccessCallBack:dic];//蘋果支付成功,傳receipt-data給后臺驗證
[ZSTools loadActivityIndicatorOn:self.view withCenterPoint:self.view.center withTitleString:@"正在購買..." sizeType:2];
}
存儲持久化實現(xiàn)
-(void)addDicReconciliation:(NSDictionary *)dicEBean
{
if (![self.arrReconciliationModel containsObject:dicEBean]) {
[self.arrReconciliationModel addObject:dicEBean];
}
[self saveReconciliation];
}
- (void)saveReconciliation
{
NSString *path = [NSString stringWithFormat:@"%@/%@_reconciliation.plist", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0], [EMVideoUserSingleton sharedInstance].ugsvId];
[self.arrReconciliationModel writeToFile:path atomically:NO];
}
7.注意事項
1.對賬問題
通過textflight下載的app走內(nèi)購也是在sandbox環(huán)境檐迟。這時走內(nèi)購不需要支付相應(yīng)金額补胚,但是對應(yīng)的咱們后臺是正式環(huán)境,內(nèi)購走通后返回的e豆(商品,以下e豆都對應(yīng)商品)是正式環(huán)境锅减。這就會造成沒支付錢糖儡,但是正式環(huán)境得到e豆了,對賬的時候要作記錄怔匣。
2.漏單的情況:
先看看支付流程握联,如下:
app iTunes app 后臺 app
1發(fā)起支付--->2扣費成功--->3得到receipt(支付憑據(jù))--->4去后臺驗證憑據(jù)獲取e豆--->5返回數(shù)據(jù)桦沉,前端刷新數(shù)據(jù)
漏單情況1
3到4的時候出問題,比如斷網(wǎng)金闽。此時前端會把支付憑據(jù)持久化存儲下來(期間用戶卸載APP此單在前端就真漏了)纯露,下次進入購買頁會先判斷有無未成功的支付,有就提示用戶代芜,用戶選擇找回埠褪,重走4,5流程挤庇。
漏單情況2
4到5的時候出問題钞速。此時后臺其實已經(jīng)成功,只是前端沒獲取到數(shù)據(jù)嫡秕,當漏單處理渴语,還是上面的邏輯,會把該單存儲昆咽。下次進入的時候會先刷新數(shù)據(jù)(此時未獲取到e的豆已經(jīng)獲取到了)驾凶,然后提示有未完成單,此時點找回會提示無效的憑據(jù)掷酗,這是正常的调违,因為豆已經(jīng)給了,此單已結(jié)束泻轰。
漏單情況3
2到3環(huán)節(jié)出問題屬于蘋果的問題技肩,目前沒做處理。
3.漏單處理
1.在后臺返回商品支付回調(diào)失敗里面把當前交易數(shù)據(jù)持久化存儲,成功狀態(tài)下移除當前單數(shù)據(jù).并檢查是否有已扣款未返商品單,對應(yīng)下面checkHaveDidNotPay
}else{
if (dicPara) {
[self.modelEBean addDicEBean:dicPara];//傳receipt失敗糕殉,
[self checkHaveDidNotPay];
}
- (void)checkHaveDidNotPay
{
if (self.modelEBean.arrEBeanBuyModel.count) {
[EMTextAlertView title:@"溫馨提示" message:@"網(wǎng)絡(luò)不給力亩鬼,e豆數(shù)據(jù)可能更新不及時,請重新加載阿蝶。" leftTitle:@"下次再說" rightTitle:@"重新加載" complete:^(NSInteger index, NSString *title) {
if (index == 1){//重新獲取會重新調(diào)用購買驗證
for (NSDictionary *dic in self.modelEBean.arrEBeanBuyModel) {
[self.bizEBeanBuy requestAppStorePaySuccessCallBack:dic];
}
}
}];
}
}
根據(jù)需求,每次購買前先檢查有無之前漏單,有先處理漏單.視需求定.
我們目前是每次到購買頁面先檢查有無漏單
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.bizVideoMine requestVideoMineData:nil];
[self checkHaveDidNotPay];
}
有問題下面留言,有不足的地方歡迎指正.