iOS內購


一.內購了解

1.內購的概述

內購就是指,在APP內購買某些產品(還有另外的支付方式,比如微信,支付寶,Apple Pay等,這些一般通過集成第三方的SDK實現)。為什么要使用內購,蘋果審核指南(https://developer.apple.com/cn/app-store/review/guidelines/#business)3.1.1規定如果您想要在 app 內解鎖特性或功能 (解鎖方式有:訂閱、游戲內貨幣、游戲關卡、優質內容的訪問權限或解鎖完整版等),則必須使用 App 內購買項目。App 不得使用自身機制來解鎖內容或功能,如許可證密鑰、增強現實標記、二維碼等。App 及對應元數據不得包含指引用戶使用非 App 內購買項目機制進行購買的按鈕、外部鏈接或其他行動號召用語。App 可以提供 App 內購買貨幣,供用戶在 app 內“打賞”數字內容提供商。通過 App 內購買項目購買的所有點數和游戲貨幣不得過期,並且您應確保為所有可恢復的 App 內購買項目設計一套恢復機制。請務必指定正確的可購買類型,否則您的 app 將被拒絕。
內購分成:在訂閱者使用付費服務的首年內,您的收益率為 70%。當訂閱者為同一訂閱群組中的訂閱產品累積一年的付費服務后,您的收益率將提高至 85%。同一群組中的升級訂閱、降級訂閱和跨級訂閱不會中斷付費服務的天數。轉換至不同群組的訂閱將重置付費服務的天數。賺取 85% 訂閱價格這一規則適用於2016年6月之后生效的訂閱續期。
2.內購的產品類型

消耗型項目

用戶可以購買各種消耗型項目 (例如游戲中的生命或寶石) 以繼續 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];//自己寫的類實現的
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM