九月份的時候到公司,開始制作一個游戲的sdk,包括了登錄注冊,初始化,信息收集等功能... 這些相對來說簡單些,最主要的是內購.為什么要做內購呢?
游戲里面包含了虛擬商品(金幣,藥水等等...)
我們要進行虛擬商品的購買,就必須要通過蘋果的內購簡稱IAP(In-App Purchase)
在內購之前我也查詢了一些資料:
iOS內購實現及測試Check List
iOS應用內支付(內購)的個人開發(fā)過程及坑!
iOS證書說明和發(fā)布內購流程整理
真·iOS內購的完整流程
當然還有官方文檔.
開始內購前需要做準備:
- 要有開發(fā)者賬號,創(chuàng)建應用添加內購產品(產品名字,產品ID,產品圖片)
- 協議、稅務和銀行信息處理
- 創(chuàng)建沙箱測試賬號,方便測試
開始前要有如下幾樣東西:
- itunes connect上注冊的商品id
- 創(chuàng)建的應用的bundle id
- 沙箱測試的賬號和密碼
接下來我們就開始開發(fā)了:
首先我們要認識這幾個類
SKProductSKProduct
對象提供有關您在iTunes Connect中注冊的產品的信息。官方文檔解釋如下
SKProduct objects are returned as part of an SKProductsResponse object. Each product object provides information about a product you previously registered in iTunes Connect.
SKPaymentSKPayment
類定義了一個支付請求, 支付封裝了標識特定產品的字符串以及用戶想要購買的那些項目的數量。官方文檔解釋如下
The SKPayment class defines a request to the Apple App Store to process payment for additional functionality offered by your application. A payment encapsulates a string that identifies a particular product and the quantity of those items the user would like to purchase.
SKProductsRequest
SKProductsRequest對象用于從Apple App Store獲得產品信息的。官方文檔解釋如下
An SKProductsRequest object is used to retrieve localized information about a list of products from the Apple App Store. Your application uses this request to present localized prices and other information to the user without having to maintain that list itself.
SKProductsResponse
SKProductsResponse對象存儲了從Apple App Store獲取得產品信息。官方文檔解釋如下
An SKProductsResponse object is returned by the Apple App Store in response to a request for information about a list of products.
SKPaymentTransaction
SKPaymentTransaction類定義駐留在支付隊列中的支付交易對象。 每當付款被添加到支付隊列時,創(chuàng)建這個支付交易對象。 當App Store處理完付款后,交易就會傳送到您的應用程式。 完成的交易提供收據和交易標識符,您的應用程序可以使用該標識來保存已處理付款的永久記錄。(谷歌翻譯,覺得這個解釋的挺好)
The SKPaymentTransaction class defines objects residing in the payment queue. A payment transaction is created whenever a payment is added to the payment queue. Transactions are delivered to your application when the App Store has finished processing the payment. Completed transactions provide a receipt and transaction identifier that your application can use to save a permanent record of the processed payment.
上代碼之前先說一下我的邏輯:
- 先添加一個遮罩層,防止連續(xù)多次點擊(因為內購的反應真的慢)
- 請求商品列表(我沒有將所有的商品請求下來,我是買哪個就請求哪個),如果失敗就提示用戶,并移除遮罩層,如果成功就進行下一步
- 得到商品后調用下單接口(我們公司服務器的下單接口,因為公司要訂單數據),若失敗取消購買,并移除遮罩層,如果成功,將部分訂單信息本地化(后面的驗簽用的,也是公司要求這樣做的),進行下一步
- 將訂單信息添加到支付隊列,添加支付監(jiān)聽,若失敗移除遮罩層,如果成功就更新本地存儲的數據,進行下一步
- 驗簽,無論成功失敗,更新本地數據,做響應的處理(根據各個公司的情況),移除遮罩層,移除支付監(jiān)聽
接下來我們就要上代碼了
封裝一個外部調用的方法,在接口類里面,這樣只要外部調用接口類就可以調用內購,不用實際調用內購類,viewController是調用這個接口的控制器,用來設置回調代理,和承載內購控制器,order是訂單信息,里面包括產品ID之類的屬性,寫一個模型利于擴展,后面添加參數的時候不需要修改接口,只要在模型里面添加屬性就好了
+ (void)showInAppPurchasedWithViewController:(UIViewController <DYStoreKitControllerDelegate>*)viewController
model:(DYOrderModel *)order {
//創(chuàng)建控制器傳值order
DYStoreKitController *paymentVC = [[DYStoreKitController alloc] initWithOrder:order];
//添加遮罩層,我直接把內購功能寫成了一個控制器,每次調用內購的時候我就將這個控制器設置為子控制器,并將它的view添加視圖上
[viewController.view addSubview:paymentVC.view];
[viewController addChildViewController:paymentVC];
//設置代理
paymentVC.delegate = viewController;
//通過這個方法開始內購的流程
[paymentVC startObserverAndProductRequest];
}
回調方法,將支付結果的報文回調給調用方,方便調試和后續(xù)操作,這個回調制作參考,實際的支付結果以服務端的結果為準
//這個是代理回調方法,用來回調給調用方
@protocol DYStoreKitControllerDelegate <NSObject>
//支付回調
- (void)paymentHandler:(NSString *)info;
@end
這個是用來處理支付結束的,每一種情況結束都要調用,寫一個方法
- (void)payFinishWithTrinsaction:(SKPaymentTransaction *)transaction state:(NSString *)stateString {
//如果傳了這個參數就完成這個訂單的支付
if (transaction) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
//如果有這個數據就執(zhí)行代理方法回調這個字符串,因為我做的是SDK,需要把結果回調個調用方,故有如下操作
if (stateString) {
if ([self.delegate respondsToSelector:@selector(paymentHandler:)]) {
[self.delegate paymentHandler:stateString];
}
}
//移除控制器和它的view
[self.view removeFromSuperview];
[self removeFromParentViewController];
}
開始內購監(jiān)聽和結束內購監(jiān)聽
#pragma mark - 產品監(jiān)聽
- (void)startObserver {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
- (void)stopObserver {
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
請求商品列表方法,也是DYStoreKitController留給外界調用的方法,
內購是可以進行批量購買的,是以支付隊列的方式,所以要將proID放到一個數組后轉換成NSSet類型進行支付請求的調用
//2-請求商品列表
- (void)startObserverAndProductRequest {
//商品id數組
NSMutableArray *proIDs = [[NSMutableArray alloc] initWithCapacity:1];
if (self.order.proID) {
//內購商品ID的數組
[proIDs addObject:self.order.proID];
if ([SKPaymentQueue canMakePayments]) {
//開始內購支付監(jiān)聽
[self startObserver];
NSSet *IDSet = [NSSet setWithArray:proIDs];
SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:IDSet];
//設置內購請求代理,以監(jiān)聽請求結果
productRequest.delegate = self;
//開始請求
[productRequest start];
}
else {
//請求失敗提示,DYHudTool是自己寫的提示工具
[DYHudTool showText:@"用戶未授權內購" hideDelayAfter:1.0];
//調用支付結束的方法
[self payFinishWithTrinsaction:nil state:@"notAuthorization"];
}
}
else {
//沒有上商品ID也結束內購
[DYHudTool showText:@"未獲取商品信息" hideDelayAfter:1.0];
[self payFinishWithTrinsaction:nil state:@"proID is nil"];
}
}
#pragma mark - SKProductsRequestDelegate控制器要遵守這個協議
//請求商品成功的返回結果
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
//NSLog(@"products = %@",response.products);
//得到產品
self.products = response.products;
//如果有產品就開始下單并添加到支付隊列
if (self.products.count > 0) {
[self addPaymentToPamentQueue];
}
//沒有產品就結束
else {
[self payFinishWithTrinsaction:nil state:nil];
}
//無效的商品id處理
for (NSString *invalidProductId in response.invalidProductIdentifiers)
{
//無效的invalidProductId
}
}
//請求失敗的時候
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
//結束
[self payFinishWithTrinsaction:nil state:@"requestFailed"];
}
下單接口,這個接口是我們要進行訂單統計才添加的接口
//3-請求成功后就調用下單接口
- (void)addPaymentToPamentQueue {
for (SKProduct *product in self.products) {
//調服務器的下單接口
[self orderWithProduct:product Handler:^(NSString *orderID) {
//下單成功回調,將需要存儲的數據存到本地,根據不同的需求處理,這里就不上代碼了
//根據產品創(chuàng)建支付并添加到支付隊列
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}];
}
}
- (void)orderWithProduct:(SKProduct *)product Handler:(void(^)(NSString *orderID))handler {
NSLog(@"開始下單");
NSDictionary *parameters = @{
//參數,下單參數,可以根據需求進行修改
};
[[DYNetworingManager sharedManager] requestWithType:RequestTypePost URLString:URL_CREATER_ORDER parameters:parameters progress:nil success:^(NSURLSessionDataTask *task, id response) {
int status = [[response objectForKey:@"status"] intValue];
//如果status等于1的時候請求成功
//若成功就回調
if (status == 1) {
NSLog(@"下單成功");
NSDictionary *dataDic = [response objectForKey:@"data"];
NSString *orderid = [dataDic objectForKey:@"orderid"];
//拿到orderid回調
handler(orderid);
}
//失敗就結束
else {
[self payFinishWithTrinsaction:nil state:@"orderFailed"];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
//網絡出錯也結束
[self payFinishWithTrinsaction:nil state:@"netFailed"];
}];
}
支付結果監(jiān)聽,遵守SKPaymentTransactionObserver協議
//4-監(jiān)聽支付結果
#pragma mark - 內購狀態(tài)回調 要遵守SKPaymentTransactionObserver協議
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
NSLog(@"transactions回調來了");
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchased: {
//這里可以寫更新本地數據的代碼
// 發(fā)送到蘋果服務器驗證憑證
[self verifyPurchaseWithPaymentTransaction:transaction];
//結束支付
[self payFinishWithTrinsaction:transaction state:@"Purchased"];
}
break;
case SKPaymentTransactionStateFailed: {
//失敗結束
[self payFinishWithTrinsaction:transaction state:@"支付失敗"];
}
break;
case SKPaymentTransactionStateRestored: {
[self payFinishWithTrinsaction:transaction state:@"恢復已經購買的商品"];
}
break;
case SKPaymentTransactionStatePurchasing: {
//商品添加進購買隊列
}
break;
default: {
}
break;
}
}
}
支付驗簽步驟
//5-驗證購買
-(void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction{
//從沙盒中獲取交易憑證并且拼接成請求體數據
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *transactionReceipt=[NSData dataWithContentsOfURL:receiptUrl];
//將數據進行base64編碼,這個方法是從別地方粘貼過來的
NSDictionary *requestContents = @{
@"receipt-data": [self encode:(uint8_t *)transactionReceipt.bytes length:transactionReceipt.length]};
//將數據轉換為json格式
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:nil];
//再轉換為字符串,來發(fā)送請求
NSString *dataString = [[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding];
NSString *transid = transaction.transactionIdentifier;
NSDictionary *parms = @{
@"dataorg":dataString ? dataString : @"",//驗簽數據,剛處理好的
@"transid":transid ? transid : @"",//transid從transaction中獲取的
};
[[DYNetworingManager sharedManager] requestParm:parms success:^(id response) {
NSLog(@"驗簽response = %@",response);
int status = [[response objectForKey:@"status"] intValue];
if (status == 1) {
//更新本地數據
//結束交易
[self payFinishWithTrinsaction:transaction state:@"支付成功"];
}
else {
//更新本地數據
//結束交易
[self payFinishWithTrinsaction:transaction state:@"驗簽失敗,可能是非法的途徑,可能是越獄的手機"];
}
} failed:^(NSError *error) {
//網絡問題也要結束
[self payFinishWithTrinsaction:nil state:nil];
}];
}
//Base64編碼
- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length {
static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
NSMutableData *data = [NSMutableData dataWithLength:((length + 2) / 3) * 4];
uint8_t *output = (uint8_t *)data.mutableBytes;
for (NSInteger i = 0; i < length; i += 3) {
NSInteger value = 0;
for (NSInteger j = i; j < (i + 3); j++) {
value <<= 8;
if (j < length) {
value |= (0xFF & input[j]);
}
}
NSInteger index = (i / 3) * 4;
output[index + 0] = table[(value >> 18) & 0x3F];
output[index + 1] = table[(value >> 12) & 0x3F];
output[index + 2] = (i + 1) < length ? table[(value >> 6) & 0x3F] : '=';
output[index + 3] = (i + 2) < length ? table[(value >> 0) & 0x3F] : '=';
}
return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
}
- (void)dealloc {
//控制器銷毀的時候要移除監(jiān)聽
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
代碼基本如上,本地數據部分沒有寫,這個需要根據不用的需求來做,就沒有附上去
遇到的問題說一下吧
- 驗簽地址問題驗簽有兩個地址
//沙盒測試環(huán)境驗證
#define SANDBOX @"https://sandbox.itunes.apple.com/verifyReceipt"
//正式環(huán)境驗證
#define AppStore @"https://buy.itunes.apple.com/verifyReceipt"
無論如何都先到正式的環(huán)境驗證,如果返回的是21007,那么說明這是一個沙盒測試,再去沙盒測試環(huán)境去驗證,避免了代碼的修改,方便,上線后也是這樣.
- 驗簽數據的問題
//從沙盒中獲取交易憑證并且拼接成請求體數據
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *transactionReceipt=[NSData dataWithContentsOfURL:receiptUrl];
//將數據進行base64編碼,這個方法是從別地方粘貼過來的
NSDictionary *requestContents = @{
@"receipt-data": [self encode:(uint8_t *)transactionReceipt.bytes length:transactionReceipt.length]};
//將數據轉換為json格式
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:nil];
//再轉換為字符串,來發(fā)送請求
NSString *dataString = [[NSString alloc] initWithData:requestData encoding:NSUTF8StringEncoding];
從沙盒中獲取數據后,一定要將其先進行base64編碼后,再轉換為json格式字符串再來驗證,延簽的流程需要放到服務端進行,所以我只要把處理好的數據發(fā)給服務端就好了。
- 付過錢后沒有驗簽成功怎么辦
這種情況是一個比較少見的情況,但是還是要考慮,這種情況會導致用戶付過錢了但是沒有收到商品。
我們主要有三種解決方式:
- 不完成訂單方式
- 補驗簽方式
- 聯系客服的方式
先說一下不完成訂單方式:
這種方式要先介紹一下內購的一個機制:
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
每一個支付,我只要不執(zhí)行上面這句代碼,這個支付就不會被完成,也就是說系統會一直給你發(fā)消息通過下面這個方法:
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
當然前提是你添加了監(jiān)聽:
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
而且并沒有被移除這樣就算我們這次交易成功付過款了,但是沒有驗簽,這個時候我們是沒有執(zhí)行下面這句代碼的,
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
也就是說,當我們下次再開啟支付的時候,添加了監(jiān)聽的話,這個訂單還會去驗簽,這樣我們就可以確保沒問題了。
但是,這樣有缺點,你這個支付在隊列里,什么時候執(zhí)行來驗簽就是一個問題了,而且當我們支付完成后一般會移除支付隊列的監(jiān)聽,這樣就沒有辦法進行監(jiān)聽,只有進行下一次支付的時候,添加監(jiān)聽到對列里面才可以進行又一次的驗簽,如果我一直不去交易怎么辦,或者玩家想,我給錢了,又不給我東西,再也不買了怎么辦。
補驗簽方法:
可以通過手動驗簽就要進行數據的持久化,將訂單信息進行本地存儲,在本地形成一個支付記錄的表格,每一筆訂單都有他的支付狀態(tài),在客戶端上做一個支付記錄的展示界面,將客戶已支付的訂單顯示出來,訂單狀態(tài)可以分為已支付且驗簽和已支付未驗簽兩種,當狀態(tài)是已支付未驗簽的時候,就可以點擊進行手動驗簽,手動驗簽需要前面驗簽的數據存到本地,手動驗簽的時候,還是調用原來的驗簽接口,但是數據都是從本地來拿,這樣就解決了這個問題。
這種方式也有一個漏洞,如果用戶卸載了那不是本地的數據也沒有了嗎。所以還有一種更好的存儲方式,Keychain可以滿足這一需求,Keychain數據并不存放在App的Sanbox中,即使刪除了App,資料依然保存在Keychain中。如果重新安裝了app,還可以從Keychain獲取數據,這樣就解決了這一個問題。
聯系客服的方式:
這種方式是最簡單最直接的,也不用我們客戶端對這個問題做什么特殊處理,當玩家充值沒有了但是沒有得到相應的商品的時候,肯定會打電話給客服,玩家提供相應的數據,我們客服到查一下數據,將沒有給玩家的商品直接給補上就好了,畢竟這也只是少數的情況,而且我們有自己的訂單系統,這個問題一查就查到了。
- 充錯用戶的問題
看別人的博客,說當付過錢后沒有網絡導致沒有驗簽,這樣的話,我切換用戶后,下次支付時,系統會把前面的支付結果在來一次,這樣就又去調用驗簽方法,這個時候可能會充錯用戶。
如果有自己的訂單系統的話就可以完全不用考慮這個問題,每一筆訂單都是和用戶的id綁定的,不會出現充值錯的問題。
當人如果沒有訂單系統的話,可以通過進行數據本地存儲來解決這個問題,具體就不說了。
總結
經過一段時間的運行發(fā)現自己的思路有些地方還是不是很好,比如說當我調起支付的時候,我會先添加一個遮罩層,在支付完成的時候在去掉,這樣其實是一個不是很好的操作,每一個支付都是添加都隊列里面的,不是你添進去后就會馬上執(zhí)行,彈出支付框,這樣的體驗就會比較差,而且在去掉遮罩層的時候有很多種情況,一不留神有一種情況沒有去掉,那么這個沒辦法進行下去了,遮罩還在上面,點都點不了,后來我發(fā)現可以在點擊購買后就把商品列表辭退掉也是一種處理方法,這樣也可以避免用戶多次點擊,每一種方式都有優(yōu)點缺點吧,這個可以根據不同情況來進行選擇處理方式。
還有就是將訂單信息存儲為一個屬性也是一個不是很好的選擇,初步想法,可以將內購控制器設置為一個單例,這樣統一管理內購訂單,也可以將原來的訂單屬性變?yōu)橐粋€訂單數組,這樣當前訂單沒有完成的時候也可以進行下一個訂單的處理,這個還有一些考慮的不夠周全,還有待完善。
內購我也屬于邊學邊做,也會有地方不是很好,后面發(fā)現再進行修改吧,如果有讀者看到了不是很合適的地方,希望留言指出,好繼續(xù)改進。