1、收款協(xié)議以及賬戶等的創(chuàng)建
內(nèi)購收款協(xié)議等的創(chuàng)建,這里一般由運營負責,這里不做介紹,但是如果想要了解,請參考這位博主的文章,里面的圖文都解釋得很清楚。http://www.itdecent.cn/p/86ac7d3b593a
2、開發(fā)者中心文件創(chuàng)建
要開啟iOS內(nèi)購功能,首先在apple develop 中心,先創(chuàng)建證書以及描述文件并包含內(nèi)購功能。在項目中打開 In - App-purchase 功能即可繼續(xù)下面的代碼實現(xiàn)。
3、代碼實現(xiàn)
我這邊的代碼實現(xiàn)自己實現(xiàn)了一個工具類。然后內(nèi)購的相關(guān)代碼以及邏輯在這個類實現(xiàn),這樣做的好處是不需要在控制器中寫過多的代碼,方便轉(zhuǎn)移使用,符合代碼高聚合性低耦合性的原則。
首先導入在項目的 Build Phases 下的Link Binary With libraires 中添加StoreKit.framework
在這個工具類里面 ,我寫了一個單例方法,包括添加內(nèi)購監(jiān)聽,停止內(nèi)購監(jiān)聽以及發(fā)起內(nèi)購購買的方法,話不多說直接上代碼 。
下面是IPAPurchase.h的代碼
#import <Foundation/Foundation.h>
/**
block
@param isSuccess 是否支付成功
@param certificate 支付成功得到的憑證(用于在自己服務(wù)器驗證)
@param errorMsg 錯誤信息
*/
typedef void(^PayResult)(BOOL isSuccess,NSString *certificate,NSString *errorMsg);
@interface IPAPurchase : NSObject
@property (nonatomic, copy)PayResult payResultBlock;
/**
單例方法
*/
+ (instancetype)manager;
/**
開啟內(nèi)購監(jiān)聽 在程序入口didFinishLaunchingWithOptions實現(xiàn)
*/
-(void)startManager;
/**
停止內(nèi)購監(jiān)聽 在AppDelegate.m中的applicationWillTerminate方法實現(xiàn)
*/
-(void)stopManager;
/**
拉起內(nèi)購支付
@param productID 內(nèi)購商品ID
@param payResult 結(jié)果
*/
-(void)buyProductWithProductID:(NSString *)productID payResult:(PayResult)payResult;
以下是IPAPurchase.m文件的代碼
#import "IPAPurchase.h"
#import <StoreKit/StoreKit.h>
static NSString * const receiptKey = @"receipt_key";
dispatch_queue_t iap_queue() {
static dispatch_queue_t as_iap_queue;
static dispatch_once_t onceToken_iap_queue;
dispatch_once(&onceToken_iap_queue, ^{
as_iap_queue = dispatch_queue_create("com.iap.queue", DISPATCH_QUEUE_CONCURRENT);
});
return as_iap_queue;
}
@interface IPAPurchase()<SKPaymentTransactionObserver,
SKProductsRequestDelegate>
{
SKProductsRequest *request;
}
//購買憑證
@property (nonatomic,copy)NSString *receipt;//存儲base64編碼的交易憑證
//產(chǎn)品ID
@property (nonnull,copy)NSString * profductId;
@end
static IPAPurchase * manager = nil;
@implementation IPAPurchase
#pragma mark -- 單例方法
+ (instancetype)manager{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!manager) {
manager = [[IPAPurchase alloc] init];
}
});
return manager;
}
#pragma mark -- 漏單處理
-(void)startManager{
dispatch_sync(iap_queue(), ^{
[[SKPaymentQueue defaultQueue] addTransactionObserver:manager];
});
}
#pragma mark -- 移除交易事件
-(void)stopManager{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
});
}
#pragma mark -- 發(fā)起購買的方法
-(void)buyProductWithProductID:(NSString *)productID payResult:(PayResult)payResult{
self.payResultBlock = payResult;
//移除上次未完成的交易訂單
[self removeAllUncompleteTransactionBeforeStartNewTransaction];
[RRHUD showWithContainerView:RR_keyWindow status:NSLocalizedString(@"購買中...", @"")];
self.profductId = productID;
if (!self.profductId.length) {
UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"溫馨提示" message:@"沒有對應(yīng)的商品" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles: nil];
[alertView show];
}
if ([SKPaymentQueue canMakePayments]) {
[self requestProductInfo:self.profductId];
}else{
UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"溫馨提示" message:@"請先開啟應(yīng)用內(nèi)付費購買功能。" delegate:nil cancelButtonTitle:@"確定" otherButtonTitles: nil];
[alertView show];
}
}
#pragma mark -- 結(jié)束上次未完成的交易 防止串單
-(void)removeAllUncompleteTransactionBeforeStartNewTransaction{
NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
if (transactions.count > 0) {
//檢測是否有未完成的交易
SKPaymentTransaction* transaction = [transactions firstObject];
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
return;
}
}
}
#pragma mark -- 發(fā)起購買請求
-(void)requestProductInfo:(NSString *)productID{
NSArray * productArray = [[NSArray alloc]initWithObjects:productID,nil];
NSSet * IDSet = [NSSet setWithArray:productArray];
request = [[SKProductsRequest alloc]initWithProductIdentifiers:IDSet];
request.delegate = self;
[request start];
}
#pragma mark -- SKProductsRequestDelegate 查詢成功后的回調(diào)
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *myProduct = response.products;
if (myProduct.count == 0) {
[RRHUD hide];
[RRHUD showErrorWithContainerView:UL_rootVC.view status:NSLocalizedString(@"沒有該商品信息", @"")];
if (self.payResultBlock) {
self.payResultBlock(NO, nil, @"無法獲取產(chǎn)品信息,購買失敗");
}
return;
}
SKProduct * product = nil;
for(SKProduct * pro in myProduct){
NSLog(@"SKProduct 描述信息%@", [pro description]);
NSLog(@"產(chǎn)品標題 %@" , pro.localizedTitle);
NSLog(@"產(chǎn)品描述信息: %@" , pro.localizedDescription);
NSLog(@"價格: %@" , pro.price);
NSLog(@"Product id: %@" , pro.productIdentifier);
if ([pro.productIdentifier isEqualToString:self.profductId]) {
product = pro;
break;
}
}
if (product) {
SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
//內(nèi)購透傳參數(shù)
payment.applicationUsername = self.order;
[[SKPaymentQueue defaultQueue] addPayment:payment];
}else{
NSLog(@"沒有此商品");
}
}
//查詢失敗后的回調(diào)
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
if (self.payResultBlock) {
self.payResultBlock(NO, nil, [error localizedDescription]);
}
}
//如果沒有設(shè)置監(jiān)聽購買結(jié)果將直接跳至反饋結(jié)束;
-(void)requestDidFinish:(SKRequest *)request{
}
#pragma mark -- 監(jiān)聽結(jié)果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
//當用戶購買的操作有結(jié)果時,就會觸發(fā)下面的回調(diào)函數(shù),
for (SKPaymentTransaction * transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased:{
[self completeTransaction:transaction];
}break;
case SKPaymentTransactionStateFailed:{
[self failedTransaction:transaction];
}break;
case SKPaymentTransactionStateRestored:{//已經(jīng)購買過該商品
[RRHUD hide];
[self restoreTransaction:transaction];
}break;
case SKPaymentTransactionStatePurchasing:{
NSLog(@"正在購買中...");
}break;
case SKPaymentTransactionStateDeferred:{
NSLog(@"最終狀態(tài)未確定");
}break;
default:
break;
}
}
}
//完成交易
#pragma mark -- 交易完成的回調(diào)
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
NSLog(@"購買成功,準備驗證發(fā)貨");
[self getReceipt]; //獲取交易成功后的購買憑證
[self saveReceipt:transaction]; //存儲交易憑證
[self checkIAPFiles:transaction];
}
#pragma mark -- 處理交易失敗回調(diào)
- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
[RRHUD hide];
NSString *error = nil;
if(transaction.error.code != SKErrorPaymentCancelled) {
[RRHUD showInfoWithContainerView:UL_rootVC.view status:NSLocalizedString(@"購買失敗", @"")];
} else {
[RRHUD showInfoWithContainerView:UL_rootVC.view status:NSLocalizedString(@"取消購買", @"")];
}
if (self.payResultBlock) {
self.payResultBlock(NO, nil, error);
}
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];
}
- (void)restoreTransaction:(SKPaymentTransaction *)transaction{
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
#pragma mark -- 獲取購買憑證
-(void)getReceipt{
NSURL * receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData * receiptData = [NSData dataWithContentsOfURL:receiptUrl];
NSString * base64String = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
self.receipt = base64String;
}
#pragma mark -- 存儲購買憑證
-(void)saveReceipt:(SKPaymentTransaction *)transaction{
NSString * userId;
NSString * order;
if (self.userid) {
userId = self.userid;
[[NSUserDefaults standardUserDefaults]setObject:userId forKey:@"unlock_iap_userId"];
}else{
userId = [[NSUserDefaults standardUserDefaults]objectForKey:@"unlock_iap_userId"];
}
order = transaction.payment.applicationUsername;
NSString *fileName = [NSString UUID];
NSString *savedPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper iapReceiptPath], fileName];
NSMutableDictionary * dic = [[NSMutableDictionary alloc]init];
[dic setValue: self.receipt forKey:receiptKey];
[dic setValue: userId forKey:@"user_id"];
[dic setValue: order forKey:@"order"];
BOOL ifWriteSuccess = [dic writeToFile:savedPath atomically:YES];
if (ifWriteSuccess) {
NSLog(@"購買憑據(jù)存儲成功!");
}
}
#pragma mark -- 驗證本地數(shù)據(jù)
-(void)checkIAPFiles:(SKPaymentTransaction *)transaction{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];
if (error == nil) {
for (NSString *name in cacheFileNameArray) {
if ([name hasSuffix:@".plist"]){ //如果有plist后綴的文件,說明就是存儲的購買憑證
NSString *filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];
[self sendAppStoreRequestBuyPlist:filePath trans:transaction];
}
}
} else {
}
}
#pragma mark -- 存儲成功訂單
-(void)SaveIapSuccessReceiptDataWithReceipt:(NSString *)receipt Order:(NSString *)order UserId:(NSString *)userId{
NSMutableDictionary * mdic = [[NSMutableDictionary alloc]init];
[mdic setValue:[self getCurrentZoneTime] forKey:@"time"];
[mdic setValue: order forKey:@"order"];
[mdic setValue: userId forKey:@"userid"];
[mdic setValue: receipt forKey:receiptKey];
NSString *fileName = [NSString UUID];
NSString * successReceiptPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper SuccessIapPath], fileName];
//存儲購買成功的憑證
[mdic writeToFile:successReceiptPath atomically:YES];
}
#pragma mark -- 獲取系統(tǒng)時間的方法
-(NSString *)getCurrentZoneTime{
NSDate * date = [NSDate date];
NSDateFormatter*formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSString*dateTime = [formatter stringFromDate:date];
return dateTime;
}
#pragma mark -- 去服務(wù)器驗證購買
-(void)sendAppStoreRequestBuyPlist:(NSString *)plistPath trans:(SKPaymentTransaction *)transaction{
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
NSString * receipt = [dic objectForKey:receiptKey];
NSString * order = [dic objectForKey:@"order"];
NSString * userId = [dic objectForKey:@"user_id"];
#pragma mark -- 發(fā)送信息去驗證是否成功
[[ULSDKAPI shareAPI] sendVertifyWithReceipt:receipt order:order success:^(ULSDKAPI *api, id responseObject) {
if (RequestSuccess) {
NSLog(@"服務(wù)器驗證成功!");
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];
[RRHUD hide];
[RRHUD showSuccessWithContainerView:UL_rootVC.view status:NSLocalizedString(@"購買成功", @"")];
[[NSUserDefaults standardUserDefaults]removeObjectForKey:@"unlock_iap_userId"];
NSData * data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];
NSString *result = [data base64EncodedStringWithOptions:0];
if (self.payResultBlock) {
self.payResultBlock(YES, result, nil);
}
//這里將成功但存儲起來
[self SaveIapSuccessReceiptDataWithReceipt:receipt Order:order UserId:userId];
[self successConsumptionOfGoodsWithOrder:order];
}else{
//在這里向服務(wù)器發(fā)送驗證失敗相關(guān)信息
} failure:^(ULSDKAPI *api, NSString *failure) {
}
#pragma mark -- 根據(jù)訂單號來移除本地憑證的方法
-(void)successConsumptionOfGoodsWithOrder:(NSString * )cpOrder{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
if ([fileManager fileExistsAtPath:[SandBoxHelper iapReceiptPath]]) {
NSArray * cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];
if (error == nil) {
for (NSString * name in cacheFileNameArray) {
NSString * filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];
[self removeReceiptWithPlistPath:filePath ByCpOrder:cpOrder];
}
}
}
}
#pragma mark -- 根據(jù)訂單號來刪除 存儲的憑證
-(void)removeReceiptWithPlistPath:(NSString *)plistPath ByCpOrder:(NSString *)cpOrder{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError * error;
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:plistPath];
NSString * order = [dic objectForKey:@"order"];
if ([cpOrder isEqualToString:order]) {
//移除與游戲cp訂單號一樣的plist 文件
BOOL ifRemove = [fileManager removeItemAtPath:plistPath error:&error];
if (ifRemove) {
NSLog(@"成功訂單移除成功");
}else{
NSLog(@"成功訂單移除失敗");
}
}else{
NSLog(@"本地無與之匹配的訂單");
}
}
接下來是遇到的坑與解決
坑 1
因為我們走的服務(wù)器驗證發(fā)貨的流程,因此服務(wù)器驗證這一步尤其重要。如果用戶付了款 ,但是沒有發(fā)貨的話那問題就大了,客戶是無法忍受這種情況的(你丫的吞老子的錢)。剛開始的時候我是把以下結(jié)束交易的代碼 寫到了購買回調(diào)-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions 方法里,導致一走到購買回調(diào)里就告訴蘋果這個交易結(jié)束了。但是如果此時我們服務(wù)器沒收到購買憑證或者中途出了問題的話,玩家是收不到購買的東西的。導致我們后臺沒有匹配的訂單號,蘋果又沒有提供與我們平臺訂單號的匹配的參數(shù),導致無法確定是用戶充值了沒收到貨,還是用戶裝可憐來訛我們,這一度讓我們很痛苦,無奈只能告訴玩家去蘋果申請退款。經(jīng)過各種百度和研究實驗,所以在這里重點注意的是?。。?!如果是后臺做驗證的話請把以下代碼寫到成功提交內(nèi)購憑證到服務(wù)器后臺之后再結(jié)束這次交易。這樣確保后臺收到了憑證驗證成功,因此每次用戶來問我們怎么沒收到貨或者什么的,我們都有據(jù)可循。漏單率也大大降低。
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];
坑2
因為我是在游戲公司寫SDK給研發(fā)用的,因此在給接口研發(fā)對接的時候就遇到一個問題,就是研發(fā)接入的時候沒有實現(xiàn)內(nèi)購監(jiān)聽的代碼,也就是IPAPurchase.h 中的以下方法,我們看看這個方法是干什么的?蘋果的注釋是 Observers are not retained. The transactions array will only be synchronized with the server while the queue has observers. This may require that the user authenticate.假如我們沒寫這個方法會怎樣?答案是:你購買之后他壓根就沒走購買的代理方法,也就是說,他不會走-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions回調(diào)方法,就算你能獲取到商品又怎樣?反正你不添加監(jiān)聽,我就是不走。這會導致什么問題呢?導致的問題是:你購買成功后的邏輯都不會走,你驗證不了,你更發(fā)不了貨。然而更加恐怖的是什么呢?假如不知道自己沒寫,不停地點擊購買,買了100+次,這下你就攤上大事兒了。當你知道自己沒寫之后,將[[SKPaymentQueue defaultQueue] addTransactionObserver:self]代碼添加進去之后 ,你會發(fā)現(xiàn)-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions這個方法被回調(diào)了100+次。OMG!所以,記住了,這個代碼最好在程序啟動入口就實現(xiàn),這樣的話會在程序一進來就去遍歷過往未完成的單。
-(void)startManager
坑3
我們假想一種情況,比如有兩個玩家A和B,首先A在應(yīng)用內(nèi)發(fā)起內(nèi)購購買,成功了但是在去服務(wù)器驗證的中間發(fā)生異常應(yīng)用退出了。也就是說漏單了,他買了東西沒有到賬。然后他找到B說,我曹!我剛才買了東西,但是沒收到貨。我懷疑是不是我手機有問題,我在你手機上登錄看看,會不會到貨了。于是,A在B的手機上登陸了自己的appid 并且進入應(yīng)用內(nèi)發(fā)現(xiàn)們依然沒有到賬。于是他倆都把應(yīng)用刪除了,然后從新下載安裝發(fā)現(xiàn),還是沒有到貨。他們倆很氣,找到你們公司客服說:你丫的,怎么我買了東西都沒到賬。我都收到蘋果發(fā)送的憑據(jù)了。你們信不信我去工商局告你?然后你們客服問后臺說,后臺,他們說他們已經(jīng)付款了,但是沒收到貨,你能查一下后臺有沒有對應(yīng)的訂單么?然后后臺趕緊去看一下,竟然沒有對應(yīng)的訂單。于是猜測說他們是來訛我們的,不用管。于是就這樣,用戶付了錢沒收到東西,服務(wù)器端也找都不到對應(yīng)的訂單。大家相互猜疑和指責。問題不了了之,對用戶而言,他們無辜的浪費了金錢得不到東西,體驗很差。對公司而言,無法確認問題,導致用戶的流失。這都是我們不想遇到的。那這個問題的出現(xiàn)原因在哪里呢?首先,如果按照漏單流程來走的話,獲取A用戶在下次進應(yīng)用的時候就會收到東西,但是他沒有這樣而是在B的手機上登陸了APPid,也沒到賬的情況下,他們把應(yīng)用都刪除了。重點就是這一步,刪除了。一般的補單流程是這樣的,如果沒告訴蘋果這筆訂單已經(jīng)完成,那么下次進來的時候,他會走一個補單的流程。也就是重新走 -(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions,成功后走服務(wù)器驗證->驗證成功之后發(fā)貨。但是如果刪除了應(yīng)用那問題就不一樣了。假如,你們后臺的驗證流程是需要購買憑證以及平臺訂單號的。那么如果刪除了應(yīng)用,那此時走補單的流程,這個訂單號該怎么獲???因為刪除了應(yīng)用也就是說,之前存儲的訂單號都沒了,那為什么B的手機也沒法補單成功呢?那是因為B本來就沒存儲平臺訂單號。所以去服務(wù)器驗證當然也驗證不過,因為缺少平臺訂單參數(shù),所以請求無法完成。在iOS7 之前,針對這種情況,沒法解決這種情況。但是在iOS7 之后。蘋果新增了一個applicationUsername的屬性,那這個屬性是干嘛的?這個屬性是在創(chuàng)建內(nèi)購支付的透傳參數(shù),在iOS 7 之后蘋果新增的。他的作用,在發(fā)起支付前把這個參數(shù)的值設(shè)置為平臺訂單號,是在購買成功之后,這個參數(shù)原樣一并返回到回調(diào)方法的transcation 中,通過transcation.payment.applicationUsername可以獲取到,而且是每筆訂單一一對應(yīng)的。這樣我們在創(chuàng)建交易的時候加上這個參數(shù),這個參數(shù)的值為我們的平臺訂單號,這樣我們的平臺訂單就能跟每筆內(nèi)購交易對應(yīng)上了。
SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
//內(nèi)購透傳參數(shù),與transaction一一對應(yīng)
payment.applicationUsername = self.order;
[[SKPaymentQueue defaultQueue] addPayment:payment];
因此,無論我們在那臺手機上登錄,都可以獲取到交易對應(yīng)的平臺訂單,也就可以向服務(wù)器驗證成功了。耶~~
坑4
當你因為某種原因購買了東西,但是沒告訴蘋果這個交易已經(jīng)完成的時候再次發(fā)起購買,會發(fā)生什么事呢?你會發(fā)現(xiàn)出現(xiàn)一個提示“您已購買此App內(nèi)購買項目,此項目將會免費恢復”。當然出現(xiàn)這種的可能性不高,但是還是會有遇到。如果是消耗性的商品,如果不處理會導致這個內(nèi)購項目一直無法購買的問題。那怎么處理呢?首先,在以下方法中存儲著未完成的單,
NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
在發(fā)起新的購買之前,我們先去檢查一下是否有已經(jīng)購買成功但是未結(jié)束交易的單,如果有的話,實現(xiàn)以下代碼將未結(jié)束交易的單結(jié)束掉再發(fā)起新的購買就OK 啦。
NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
if (transactions.count > 0) {
//檢測是否有未完成的交易
SKPaymentTransaction* transaction = [transactions firstObject];
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
return;
}
}
github 的直通鏈接 https://github.com/jiajiaailaras/ULIPAPurchase 覺得有幫助的。可以給個Star 支持一下。