一.內購了解
1.內購的概述
內購分成:在訂閱者使用付費服務的首年內,您的收益率為 70%。當訂閱者為同一訂閱群組中的訂閱產品累積一年的付費服務后,您的收益率將提高至 85%。同一群組中的升級訂閱、降級訂閱和跨級訂閱不會中斷付費服務的天數。轉換至不同群組的訂閱將重置付費服務的天數。賺取 85% 訂閱價格這一規則適用於2016年6月之后生效的訂閱續期。
消耗型項目
用戶可以購買各種消耗型項目 (例如游戲中的生命或寶石) 以繼續 app 內進程。消耗型項目只可使用一次,使用之后即失效,必須再次購買。
非消耗型項目
用戶可購買非消耗型項目以提升 app 內的功能。非消耗型項目只需購買一次,不會過期 (例如修圖 app 中的其他濾鏡)。
自動續期訂閱
用戶可購買固定時段內的服務或更新的內容 (例如雲存儲或每周更新的雜志)。除非用戶選擇取消,否則此類訂閱會自動續期。
非續期訂閱
用戶可購買有時限性的服務或內容 (例如線上播放內容的季度訂閱)。此類的訂閱不會自動續期,用戶需要逐次續訂。
3.內購流程圖
4.協議、稅務和銀行業務 信息填寫
這一塊內容主要是在你的開發者賬號上查看協議,輸入聯系人信息,輸入銀行信息和提交報稅表。具體可以查看一下鏈接
https://help.apple.com/app-store-connect/?lang=zh-cn#/devb6df5ee51
二.開發部分
1.創建內購商品
首先點管理,點擊+號創建你業務邏輯需要的產品類型,如上圖。
填寫完信息后,就是這樣子,我這邊都是消耗性產品類型,如上圖。這樣子產品就創建好了。
2.添加沙箱測試人員
首先選擇用戶與訪問,第二步找到測試員,第三部選擇添加+,填寫你的測試賬號信息,最后就生成了下圖4的測試賬號信息了。(注意:測試賬號信息是和開發者賬號相關聯的)
3.代碼實踐部分
首先說兩個點,為了防止丟單,一般采用單例(整個應用都存在)和把單據信息存儲本地策略(只要沒有驗證成功就不清除本地緩存)。好了上代碼
3.1.單例類
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef enum {
IAPPurchSuccess = 0,//購買成功
IAPPurchFailed = 1, //購買失敗el
IAPPurchCancel = 2, //取消購買
IAPPurchVerFailed = 3, //訂單校驗失敗
IAPPurchVerSuccess = 4, //訂單校驗成功
IAPPurchNotArrow = 5, //不允許內購
}IAPPurchType;
typedef void(^IAPCompletionHandleBlock)(IAPPurchType type, NSData *data);
@interface JLKJApplePay : NSObject
@property(nonatomic,copy)NSString*idNo;
+ (instancetype)shareIAPManager;
//添加內購產品
- (void)addPurchWithProductID:(NSString *)product_id completeHandle:(IAPCompletionHandleBlock)handle;
@end
NS_ASSUME_NONNULL_END
/*注意事項:
1.沙盒環境測試appStore內購流程的時候,請使用沒越獄的設備。
2.請務必使用真機來測試,一切以真機為准。
3.項目的Bundle identifier需要與您申請AppID時填寫的bundleID一致,不然會無法請求到商品信息。
4.如果是你自己的設備上已經綁定了自己的AppleID賬號請先注銷掉,否則你哭爹喊娘都不知道是怎么回事。
5.訂單校驗 蘋果審核app時,仍然在沙盒環境下測試,所以需要先進行正式環境驗證,如果發現是沙盒環境則轉到沙盒驗證。
識別沙盒環境訂單方法:
1.根據字段 environment = sandbox。
2.根據驗證接口返回的狀態碼,如果status=21007,則表示當前為沙盒環境。
蘋果反饋的狀態碼:
21000App Store無法讀取你提供的JSON數據
21002 訂單數據不符合格式
21003 訂單無法被驗證
21004 你提供的共享密鑰和賬戶的共享密鑰不一致
21005 訂單服務器當前不可用
21006 訂單是有效的,但訂閱服務已經過期。當收到這個信息時,解碼后的收據信息也包含在返回內容中
21007 訂單信息是測試用(sandbox),但卻被發送到產品環境中驗證
21008 訂單信息是產品環境中使用,但卻被發送到測試環境中驗證
*/
#import "JLKJApplePay.h"
@interface JLKJApplePay () <SKProductsRequestDelegate,SKPaymentTransactionObserver>
{
NSString *_purchID;
IAPCompletionHandleBlock _handle;
}
@end
@implementation JLKJApplePay
+ (instancetype)shareIAPManager {
static JLKJApplePay *IAPManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
IAPManager = [[JLKJApplePay alloc] init];
});
return IAPManager;
}
- (instancetype)init {
if ([super init]) {
// 購買監聽寫在程序入口,程序掛起時移除監聽,這樣如果有未完成的訂單將會自動執行並回調 paymentQueue:updatedTransactions:方法
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
- (void)dealloc{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
//添加內購產品
- (void)addPurchWithProductID:(NSString *)product_id completeHandle:(IAPCompletionHandleBlock)handle {
//移除上次未完成的交易訂單
[self removeAllUncompleteTransactionBeforeStartNewTransaction];
if (product_id) {
if ([SKPaymentQueue canMakePayments]) {
// 開始購買服務
_purchID = product_id;
_handle = handle;
NSSet *nsset = [NSSet setWithArray:@[product_id]];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}else{
[self handleActionWithType:IAPPurchNotArrow data:nil];
}
}
}
- (void)handleActionWithType:(IAPPurchType)type data:(NSData *)data{
switch (type) {
case IAPPurchSuccess:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"0"]}];
// [JKProgressHUD showMsgWithoutView:@"購買成功"];
break;
case IAPPurchFailed:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"1"]}];
[JKProgressHUD showMsgWithoutView:@"購買失敗"];
break;
case IAPPurchCancel:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"2"]}];
[JKProgressHUD showMsgWithoutView:@"支付取消"];
break;
case IAPPurchVerFailed:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"3"]}];
[JKProgressHUD showMsgWithoutView:@"訂單校驗失敗"];
break;
case IAPPurchVerSuccess:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"4"]}];
[JKProgressHUD showMsgWithoutView:@"訂單校驗成功"];
break;
case IAPPurchNotArrow:
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":[NSString stringWithFormat:@"%@",@"5"]}];
[JKProgressHUD showMsgWithoutView:@"不允許程序內付費"];
break;
default:
break;
}
}
#pragma mark - SKProductsRequestDelegate// 交易結束
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
// Your application should implement these two methods.
NSString * productIdentifier = transaction.payment.productIdentifier;
NSData *data = [productIdentifier dataUsingEncoding:NSUTF8StringEncoding];
NSString *receipt = [data base64EncodedStringWithOptions:0];
NSLog(@"%@",receipt);
if ([productIdentifier length] > 0) {
// 向自己的服務器驗證購買憑證
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
if (![[NSFileManager defaultManager] fileExistsAtPath:[receiptURL path]]) {
// 取 receipt 的時候要判空,如果文件不存在,就要從蘋果服務器重新刷新下載 receipt 了
// SKReceiptRefreshRequest 刷新的時候,需要用戶輸入 Apple ID,同時需要網絡狀態良好
SKReceiptRefreshRequest *receiptRefreshRequest = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:nil];
receiptRefreshRequest.delegate = self;
[receiptRefreshRequest start];
return;
}
NSData *data = [NSData dataWithContentsOfURL:receiptURL];
/** 交易憑證*/
NSString *receipt_data = [data base64EncodedStringWithOptions:0];
/** 事務標識符(交易編號) 交易編號(必傳:防止越獄下內購被破解,校驗 in_app 參數)*/
NSString *transaction_id = transaction.transactionIdentifier;
NSString *goodID = transaction.payment.productIdentifier;
//這里緩存receipt_data,transaction_id 因為后端做校驗的時候需要用到這兩個字段
[JLKJLocalCacheUserInfo savePurchasedInfoWithReceipt_data:receipt_data transaction_id:transaction_id orderId:self.idNo];
NSLog(@"%@",receipt_data);
NSLog(@"%@",transaction_id);
[self retquestApplePay:receipt_data transaction_id:transaction_id goodsID:goodID];
}
[self verifyPurchaseWithPaymentTransaction:transaction isTestServer:NO];
}
- (void)retquestApplePay:(NSString *)receipt_data transaction_id:(NSString *)transaction_id goodsID:(NSString *)goodsId {
NSMutableDictionary *param = [NSMutableDictionary new];
param[@"transactionId"] = transaction_id;
param[@"receiptData"] = receipt_data;
param[@"orderId"] = self.idNo;
NSLog(@"%@",param);
[HttpsRequest requestPOSTWithURLString:KConfirmCredentials params:param successful:^(NSDictionary * result) {
NSLog(@"%@",result);
[[NSNotificationCenter defaultCenter]postNotificationName:@"buyResult" object:nil userInfo:@{@"type":@"6"}];//驗證成功
//驗證成功,清除本地緩存
[JLKJLocalCacheUserInfo removePurchasedInfo];
} failure:^(NSError * error) {
}];
}
// 交易失敗
- (void)failedTransaction:(SKPaymentTransaction *)transaction{
if (transaction.error.code != SKErrorPaymentCancelled) {
[self handleActionWithType:IAPPurchFailed data:nil];
}else{
[self handleActionWithType:IAPPurchCancel data:nil];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
- (void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction isTestServer:(BOOL)flag{
//交易驗證
NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:recepitURL];
if(!receipt){
// 交易憑證為空驗證失敗
[self handleActionWithType:IAPPurchVerFailed data:nil];
return;
}
// 購買成功將交易憑證發送給服務端進行再次校驗
[self handleActionWithType:IAPPurchSuccess data:receipt];
NSError *error;
NSDictionary *requestContents = @{
@"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) { // 交易憑證為空驗證失敗
[self handleActionWithType:IAPPurchVerFailed data:nil];
return;
}
//In the test environment, use https://sandbox.itunes.apple.com/verifyReceipt
//In the real environment, use https://buy.itunes.apple.com/verifyReceipt
#ifdef DEBUG
#define serverString @"https://sandbox.itunes.apple.com/verifyReceipt"
#else
#define serverString @"https://buy.itunes.apple.com/verifyReceipt"
#endif
NSURL *storeURL = [NSURL URLWithString:serverString];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];
NSURLSession *session = [NSURLSession sharedSession];
[session dataTaskWithRequest:storeRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
// 無法連接服務器,購買校驗失敗
[self handleActionWithType:IAPPurchVerFailed data:nil];
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) {
// 蘋果服務器校驗數據返回為空校驗失敗
[self handleActionWithType:IAPPurchVerFailed data:nil];
}
// 先驗證正式服務器,如果正式服務器返回21007再去蘋果測試服務器驗證,沙盒測試環境蘋果用的是測試服務器
NSString *status = [NSString stringWithFormat:@"%@",jsonResponse[@"status"]];
if (status && [status isEqualToString:@"21007"]) {
[self verifyPurchaseWithPaymentTransaction:transaction isTestServer:YES];
}else if(status && [status isEqualToString:@"0"]){
[self handleActionWithType:IAPPurchVerSuccess data:nil];
}
NSLog(@"----驗證結果 %@",jsonResponse);
}
}];
// 驗證成功與否都注銷交易,否則會出現虛假憑證信息一直驗證不通過,每次進程序都得輸入蘋果賬號
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *product = response.products;
if([product count] <= 0){
NSLog(@"--------------沒有商品------------------");
return;
}
SKProduct *p = nil;
for(SKProduct *pro in product){
if([pro.productIdentifier isEqualToString:_purchID]){
p = pro;
break;
}
}
NSLog(@"productID:%@", response.invalidProductIdentifiers);
NSLog(@"產品付費數量:%lu",(unsigned long)[product count]);
NSLog(@"%@",[p description]);
NSLog(@"%@",[p localizedTitle]);
NSLog(@"%@",[p localizedDescription]);
NSLog(@"%@",[p price]);
NSLog(@"%@",[p productIdentifier]);
SKPayment *payment = [SKPayment paymentWithProduct:p];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
//請求失敗
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
NSLog(@"------------------錯誤-----------------:%@", error);
}
- (void)requestDidFinish:(SKRequest *)request{
NSLog(@"------------反饋信息結束-----------------");
}
#pragma mark - SKPaymentTransactionObserver 監聽購買結果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
for (SKPaymentTransaction *tran in transactions) {
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
[self completeTransaction:tran];
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加進列表");
break;
case SKPaymentTransactionStateRestored:
NSLog(@"已經購買過商品");
// 消耗型不支持恢復購買
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
[self failedTransaction:tran];
break;
default:
break;
}
}
}
#pragma mark -- 結束上次未完成的交易 防止串單
-(void)removeAllUncompleteTransactionBeforeStartNewTransaction{
NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
if (transactions.count > 0) {
//檢測是否有未完成的交易
SKPaymentTransaction* transaction = [transactions firstObject];
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
return;
}
}
}
3.2支付界面點擊支付按鈕
-(void)zhifuBtnClick:(UIButton *)sender
{
if ([JailbreakDetectTool detectCurrentDeviceIsJailbroken]) {
//越獄手機直接reture
[JKProgressHUD showMsgWithoutView:@"請使用未越獄的手機購買"];
return;
}
//添加通知告知購買結果
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(buyResult:) name:@"buyResult" object:nil];
[JKProgressHUD showProgress:@"支付中" inView:self.view];
NSDictionary *param = @{@"coinExchangeItemId":self.coinmodel.idNo,@"deviceType":@"2",@"payType":@"3"};
WEAKSELF;
[HttpsRequest requestPOSTWithURLString:KCreateCoinOrder params:param successful:^(NSDictionary * result) {
JLKJApplePay *idStr = [JLKJApplePay shareIAPManager];
idStr.idNo = [NSString stringWithFormat:@"%@",result[@"data"][@"id"]];
[[JLKJApplePay shareIAPManager]addPurchWithProductID:weakSelf.coinmodel.productId completeHandle:^(IAPPurchType type, NSData * _Nonnull data) {
//購買成功后的操作
NSLog(@"%u==%@",type,data);
}];
} failure:^(NSError * error) {
}];
}
//通知返回購買結果
-(void)buyResult:(NSNotification *)noti
{
NSDictionary *dictFromData = [noti userInfo];
if ([dictFromData[@"type"] isEqualToString:@"0"]) {
[JKProgressHUD showProgress:@"等待驗證" inView:self.view];
}else if ([dictFromData[@"type"] isEqualToString:@"6"]){
[JKProgressHUD showMsgWithoutView:@"充值成功"];
if ([self.whichType isEqualToString:@"2"]) {
[self.navigationController popViewControllerAnimated:NO];
}else if ([self.whichType isEqualToString:@"1"]){
if (self.PaySuccess) {
self.PaySuccess();
}
[self.navigationController popViewControllerAnimated:NO];
}else{
[self getPurchaseInfo];
}
}else{
[JKProgressHUD hide];
}
}
3.3在AppDelegate檢查是否有緩存,有緩存的話,去請求自己的后台服務器驗證單據,驗證成功后清除緩存
//如果有內購緩存,調用自己后台驗證訂單
if ([JLKJLocalCacheUserInfo isSelfVerification]) {
[JLKJLocalCacheUserInfo verificationWithSelfServer];//自己寫的類實現的
}