iOS開發 IAP支付總結
一、IAP介紹
1.1、簡介
這里先把官方文檔給大家
內購:只要在iPhone App上購買的不是實物產品(也就是虛擬產品如qq幣、魚翅、電子書......) 都需要走內購流程,蘋果這里面抽走30%
蘋果規定,凡是在App內提供的服務需要付費時,必須使用IAP,比如說游戲的金幣,道具等;而在App外提供的服務需要付費時,可以使用其他的支付方式,比如支付寶SDK、微信SDK等。說的更通俗一點,如果付費購買的商品是虛擬商品,比如游戲中的道具,并不是現實中存在的,那么必須使用IAP;如果付費購買的商品是真實產品,比如在淘寶中買了件衣服,是實實在在存在的,那么沒有必要使用IAP。因此,在使用IAP之前,首先要確認是否一定要使用IAP,如果不使用IAP也可以,那么盡量不要用IAP,因為IAP流程、使用復雜度相比支付寶SDK、微信SDK來說,要復雜很多。
1.2、內購流程
1.1.1 填寫協議,稅務和銀行業務
1、登錄https://appstoreconnect.apple.com,選擇進入App Store Connect。
2、進入“協議、稅務和銀行業務”
3、內購用的是付費應用程序,先簽署《付費應用程序協議》,同意后狀態變更為“用戶信息待處理”,等待審核。
4、狀態更改完畢后,點擊“開始設置稅務、銀行業務和聯系信息”。
a.添加銀行賬戶,按照要求填寫相關內容即可。
b.選擇報稅表,并填寫。(我是可愛的中國公民,在美國有沒有商業活動,所以我填的是否。)
然后繼續填寫報稅表,按照填寫要求填寫就行了(要是英文閱讀有點困難,那就雙擊網頁,應該會有翻譯成中文的功能;沒有的話,那就詞典。。。你懂得,哈哈哈), 我是個人開發者賬戶相對公司開發者賬戶填的會少一點,不過沒關系。都是一些基本信息。
c.填寫聯系信息,一共5個。高級管理、財務、技術、法務、營銷。
5、上面的稅務表填完了之后,點擊“我的APP”,進入到項目APP的信息頁,點擊功能,在彈出的頁面點擊App內購買項目后面的+。
創建完成之后 填寫內購買項目信息
信息填寫完成了點擊右上角的 “存儲”,然后點擊左邊 “App 內購買項目”。出現“元數據丟失”說明里面信息沒填寫完整,在點進去填寫。直到顯示“準備提交”。
6、添加沙箱測試人員
7、我們需要在工程里配置好證書,測試證書是必須的因為我們內購需要連接到蘋果的App Store的,需要正式的測試證書才能測試,同時把下圖工程中的這一配置打開
二、IAP代碼部分
我這里就直接上代碼記錄了
2.1、大體代碼流程
typedefenum: NSUInteger {
EPaymentTransactionStateNoPaymentPermission,//沒有Payment權限
EPaymentTransactionStateAddPaymentFailed,//addPayment失敗
EPaymentTransactionStatePurchasing,//正在購買
EPaymentTransactionStatePurchased,//購買完成(銷毀交易)
EPaymentTransactionStateFailed,//購買失敗(銷毀交易)
EPaymentTransactionStateCancel,//用戶取消
EPaymentTransactionStateRestored,//恢復購買(銷毀交易)
EPaymentTransactionStateDeferred,//最終狀態未確定
} EPaymentTransactionState;
// 這個大家要熟悉哦~
步驟一:App Store請求內購項
注意:此步驟建議在開始創建購買訂單前完成,這樣可以減少購買時查詢訂單的時間
1、判斷用戶是否具備支付權限
//是否允許內購
if ([SKPaymentQueue canMakePayments]) {
[self getRequestAppleProduct];
}else{
[self removeLoadingHandle];
[self removeIAPObserverHandle];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUDManager showFailedHUD:TZKeyWindow text:@"請打開Apple支付"];
});
}
2、創建一個商品查詢的請求,productIdentifiers指需要查詢的“產品ID”的數組
- (void)getRequestAppleProduct
{
NSLog(@"---------請求對應的產品信息------------");
[MBProgressHUDManager showHUD:TZKeyWindow text:@"等待響應..."];
NSArray *product = [[NSArray alloc] initWithObjects:self.productID, nil];
NSSet *nsset = [NSSet setWithArray:product];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}
查詢的結果將通過SKProductsRequestDelegate得到查詢的結果
獲取商品的查詢結果
#pragma mark - SKProductsRequestDelegate
//接收到產品的返回信息,然后用返回的商品信息進行發起購買請求
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response NS_AVAILABLE_IOS(3_0){
NSArray *product = response.products;
//沒有產品
if([product count] == 0){
[self removeLoadingHandle];
[self removeIAPObserverHandle];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUDManager showFailedHUD:TZKeyWindow text:@"網絡開小差了,請稍后重試"];
});
return;
}
SKProduct *requestProduct = nil;
for (SKProduct *pro in product) {
CYLOG(@"描述信息-%@", [pro description]);
CYLOG(@"產品標題-%@", [pro localizedTitle]);
CYLOG(@"產品描述信息-%@", [pro localizedDescription]);
CYLOG(@"價格-%@", [pro price]);
CYLOG(@"Product id-%@", [pro productIdentifier]);
CYLOG(@"位置-%@", pro.priceLocale.localeIdentifier);
// 確保訂單的正確性
if([pro.productIdentifier isEqualToString:self.productID]){
requestProduct = pro;
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:requestProduct];
payment.applicationUsername = self.orderId;
[[SKPaymentQueue defaultQueue] addPayment:payment];
break;
}
}
}``
步驟二:開始構建購買請求
SKPayment * payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
步驟三:添加支付交易的Observer
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
注意在適當的時候移除Observer
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
可以通過遵循SKPaymentTransactionObserver協議來監聽整個交易的過程
交易狀態發生改變時,包括狀態的改變,交易的結束
//監聽購買結果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
NSLog(@"==監聽購買結果==");
[self addLoadingHandle];
[self addIAPObserverHandle];
for(SKPaymentTransaction *tran in transactions){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
{
NSLog(@"交易完成");
[self didPurchaseTransaction:tran queue:queue];
}
break;
case SKPaymentTransactionStatePurchasing:{
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUDManager showHUD:TZKeyWindow text:@"正在購買..."];
});
}
break;
case SKPaymentTransactionStateRestored:{
CYLOG(@"已經購買過商品");
//消耗型不用寫
// [self removeLoadingHandle];
// [self removeIAPObserverHandle];
// [[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStateFailed:{
NSLog(@"交易失敗");
[self removeLoadingHandle];
[self removeIAPObserverHandle];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUDManager showFailedHUD:TZKeyWindow text:@"交易失敗"];
});
}
break;
case SKPaymentTransactionStateDeferred:{
NSLog(@"還在隊列里");
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUDManager showHUD:TZKeyWindow text:@"正在購買..."];
});
}
break;
default:
break;
}
}
}
步驟四:校驗憑證
我這里直接把我這邊相關的思路以及代碼提供大家參考了:
有問題可以隨時聯系我、我后面會講一下我所遇到過的坑以及解決方案。
#pragma mark Transaction State
我這里使用后臺校驗憑證、更加安全
- (void)didPurchaseTransaction:(SKPaymentTransaction *)transaction queue:(SKPaymentQueue*)queue
{
CYLOG(@"transaction purchased with product ---%@", transaction.payment.productIdentifier);
CYLOG(@"transaction ID ---%@", transaction.transactionIdentifier);
if(transaction.payment.productIdentifier != nil){
if([self.orderId length] && !self.ischecking){
//如果這個參數存在,則肯定是通過主動發起購買請求引起的
//在支付成功后,將parameters中的預訂單號存起來,并與蘋果的訂單號綁定起來,并存儲到keychain中
if([self.orderId length] && transaction.transactionIdentifier){
[SAMKeychain setPassword:self.orderId forService:TZServiceKey account:transaction.transactionIdentifier];
}
}
}
WS(weakSelf);
// appStoreReceiptURL iOS7.0增加的,購買交易完成后,會將憑據存放在該地址
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 從沙盒中獲取到購買憑據
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
NSString *encodeStr = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
NSString *payload = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", encodeStr];
NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding];
NSString *applicationUsername = transaction.payment.applicationUsername;
NSString *productId = transaction.payment.productIdentifier;
NSString *transactionId = transaction.transactionIdentifier;
//發送POST請求,對購買憑據進行驗證
//測試驗證地址:https://sandbox.itunes.apple.com/verifyReceipt
//正式驗證地址:https://buy.itunes.apple.com/verifyReceipt
if(applicationUsername.length == 0){
NSString *savedOrderNumber = [SAMKeychain passwordForService:TZServiceKey account:transactionId];
if ([savedOrderNumber length]) {
applicationUsername = savedOrderNumber;//獲取到訂單號
}
}
if ([applicationUsername length] && [encodeStr length] && [productId length] && [transactionId length]) {
[[HTTPAPIManager manager] reqeustPayAppleReceiptWithOutTradeNo:applicationUsername receiptData:encodeStr useSandbox:TZPAYSandbox productId:productId transactionId:transactionId success:^(NSURLSessionDataTask * _Nullable task, id _Nullable responseObject, NSDictionary * _Nullable inforDict) {
_errTimes = 0;
weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:(2.0)
target:weakSelf
selector:@selector(handleTimer:)
userInfo:@{@"transaction":transaction}
repeats:YES];
[[NSRunLoop mainRunLoop]addTimer:weakSelf.timer forMode:NSDefaultRunLoopMode];
[weakSelf.timer setFireDate:[NSDate date]];
} failure:^(NSURLSessionDataTask * _Nullable task, YWHTTPError * _Nullable error, NSDictionary * _Nullable responseDict) {
_errTimes = 0;
weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:(2.0)
target:weakSelf
selector:@selector(handleTimer:)
userInfo:@{@"transaction":transaction}
repeats:YES];
[[NSRunLoop mainRunLoop]addTimer:weakSelf.timer forMode:NSDefaultRunLoopMode];
[weakSelf.timer setFireDate:[NSDate date]];
}];
if (!_ischecking) {
[MBProgressHUDManager showHUD:TZKeyWindow text:@"正在確認支付..."];
}
} else {
[[HTTPAPIManager manager] reqeustAppleFailRecordWithOutTradeNo:applicationUsername receiptData:encodeStr productId:productId transactionId:transactionId success:^(NSURLSessionDataTask * _Nullable task, id _Nullable responseObject, NSDictionary * _Nullable inforDict) {
_ischecking = NO;
[MBProgressHUDManager hiddenHUD:TZKeyWindow];
[weakSelf destroyTimer];
[weakSelf removeLoadingHandle];
[weakSelf removeIAPObserverHandle];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[[NSNotificationCenter defaultCenter] postNotificationName:TZBuyVipSuccessNotification object:nil userInfo:nil];
} failure:^(NSURLSessionDataTask * _Nullable task, YWHTTPError * _Nullable error, NSDictionary * _Nullable responseDict) {
[MBProgressHUDManager hiddenHUD:TZKeyWindow];
}];
}
}
三、重點總結
1.獲取內購列表(從App內讀取或從自己服務器讀取)
2.App Store請求可用的內購列表
3.向用戶展示內購列表
4.用戶選擇了內購列表,再發個購買請求,收到購買完成的回調(購買完成
后會把錢打給申請內購的銀行卡內)
5.購買流程結束后, 向服務器發起驗證憑證以及支付結果的請求
6.自己的服務器將支付結果信息返回給前端并發放虛擬產品
7.服務端的工作比較簡單,分4步:
7.1.接收ios端發過來的購買憑證。
7.2.判斷憑證是否已經存在或驗證過,然后存儲該憑證。
7.3.將該憑證發送到蘋果的服務器驗證,并將驗證結果返回給客戶端。
7.4.如果需要,修改用戶相應的會員權限。
7.5.考慮到網絡異常情況,服務器的驗證應該是一個可恢復的隊列,如果網絡失敗了,應該進行重試。
簡單來說就是將該購買憑證用Base64編碼,然后POST給蘋果的驗證服務
器,蘋果將驗證結果以JSON形式返回。
四、總結坑
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:requestProduct];
payment.applicationUsername = self.orderId;
[[SKPaymentQueue defaultQueue] addPayment:payment];
//我這里剛才只是對訂單號的存儲、并且把訂單號存在了applicationUsername
上面的處理中、上線后我這里第一個測出了真實支付中的漏單情況、訂單號返回的nil,由于對payment.applicationUsername的極度信任、造成自己不得不緊急解決下這個問題發版、不過我事先作的還是有一些功課的、做了埋點、及時的定位到了問題、并且讓后臺進行了手動補單。相信大家看到這里的時候就不會只是簡單的這樣做了。
五、解決方案
1、針對掉單的問題、網上的資料討論的太多了我這里簡單說下我的方案吧
我這里進行了對訂單號、transactionId、進行對應的鑰匙串存儲。這樣可以解決大部分的異常場景、基本沒什么漏單了、并且我對掉單每次啟動進行了掉單查詢、還有就是再次購買頁面也會有相應的提示、讓用戶自己繼續處理、便可以重新提交訂單了。方便太多啦
/// 掉單處理
- (void)checkIAPHandle{
_ischecking = YES;
_isShowErrorM = NO;
[self addIAPObserverHandle];
}
- (void)checkTransactionHandle
{
NSArray *transactions = [SKPaymentQueue defaultQueue].transactions;
if (transactions && [transactions isKindOfClass:[NSArray class]] && [transactions count]) {
for (SKPaymentTransaction *transaction in transactions) {
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
NSString *title = @"您有一筆會員訂單未完成,請繼續處理";
NYAlertView *alertView = [[NYAlertView alloc] initWithTitle:@"發現未完成訂單"
message:title
cancelButtonTitle:nil
otherButtonTitles:@"繼續處理", nil];
[alertView setClickButtonBlock:^(NYAlertView * _Nonnull alert, NSInteger index) {
[[ApplePayManager sharedManager] handleCheckPurchaseTransaction:transaction];
}];
alertView.titleTextAlignment = NSTextAlignmentLeft;
alertView.messageTextAlignment = NSTextAlignmentLeft;
[alertView.otherButton setTitleColor:[UIColor wb_colorWithHexString:@"F44A4A"] forState:UIControlStateNormal];
[alertView show];
}
}
}
}
六、附言
大家做內購過程中遇到問題可以隨時溝通哈!!! QQ:304517331
有好的建議也記得及時分享哦??
祝大家工作順利!!!!~~~~