iOS內購全面實戰

內購是啥

App 內購買項目允許顧客通過訪問 App Store 購買您 App 中的內容、功能或服務,并安全處理來自用戶的付款。

詳情傳送門https://help.apple.com/itunes-connect/developer/#/devb57be10e7

下面來說內購集成流程

1.協議

登錄蘋果開發者中心,進入iTunes Connect,再進入“協議、稅務和銀行業務”頁面,如圖

image

點擊進入可以看到,目前共有兩個分組,三種合同。(此處有坑,比如我們當前賬號不能申請合同!如下圖)

Request Contracts 可以申請的合同;

Contracts In Effect 已經生效的合同。

三種合同分別是

Free Applications 免費應用(默認已經生效);

Paid Applications 付費應用,需要申請;

iAd App Network 廣告應用,需要申請。

image

內購對應的是Paid Applications 付費應用,需要申請,如圖2.(如果Request按鈕不顯示,則說明當前賬號權限有問題)

點擊Request完善信息,提交就行.

2.內購集成

內購實現流程:

1.客戶端向Appstore請求購買產品(假設產品信息已經取得),Appstore驗證產品成功后,從用戶的Apple賬戶余額中扣費。

2.Appstore向客戶端返回一段receipt-data,里面記錄了本次交易的證書和簽名信息。

3.客戶端向我們可以信任的服務器提供receipt-data

4.服務器對receipt-data進行一次base64編碼

5.把編碼后的receipt-data發往itunes.appstore進行驗證

6.itunes.appstore返回驗證結果給服務器

7.服務器對商品購買狀態以及商品類型,向客戶端發放相應的道具與推送數據更新通知

注,下圖3步驟和上面流程不是一一對應

image

我項目里面的購買流程,加入了一點業務邏輯和后臺驗證流程,有什么問題歡迎大家指出.

image

3.去蘋果開發者中心創建內購商品

如下圖5,點擊+號去創建內購商品,產品id最好是當前應用+數字,價格區間蘋果提供了一張表,商品價格只能是表上的價格,蘋果會抽取30%,商家能收到的錢是用戶充值的70%.這就造成了部分平臺區分安卓和蘋果.兩端賬號不互通,也造就了代充行業,再次就不展開說了.

商品價格大于100$,提交審核的時候要說明這個金額是確認過的,不然可能會被拒

image

4.代碼集成

建議單獨建一個類來處理內購業務
.h類

//
//  EMAppStorePay.h
//  MobileFixCar
//
//  Created by Wcting on 2018/4/11.
//  Copyright ? 2018年 XXX有限公司. All rights reserved.
//

/*
 wct20180917 內購支付類,短視頻e豆購買用到。
 */

#import <Foundation/Foundation.h>

@class EMAppStorePay;

@protocol EMAppStorePayDelegate <NSObject>;

@optional

/**
 wct20180418 內購支付成功回調

 @param appStorePay 當前類
 @param dicValue 返回值
 @param error 錯誤信息
 */
- (void)EMAppStorePay:(EMAppStorePay *)appStorePay responseAppStorePaySuccess:(NSDictionary *)dicValue error:(NSError*)error;


/**
 wct20180423 內購支付結果回調提示
 
 @param appStorePay 當前類
 @param dicValue 返回值
 @param error 錯誤信息
 */
- (void)EMAppStorePay:(EMAppStorePay *)appStorePay responseAppStorePayStatusshow:(NSDictionary *)dicValue error:(NSError*)error;

@end

@interface EMAppStorePay : NSObject

@property (nonatomic, weak)id<EMAppStorePayDelegate> delegate;/**<wct20180418 delegate*/


/**
  wct20180411 點擊購買

 @param goodsID 商品id
 */
-(void)starBuyToAppStore:(NSString *)goodsID;

@end

.m類(里面有客戶端驗證receipt的代碼,解開注釋就可以,用于調試.驗證建議放后臺去做)

//
//  EMAppStorePay.m
//  MobileFixCar
//
//  Created by Wcting on 2018/4/11.
//  Copyright ? 2018年 XXX有限公司. All rights reserved.
//

#import "EMAppStorePay.h"
#import <StoreKit/StoreKit.h>

//#define goods1 @"net.ejiajx.MobileFixCar06"

@interface EMAppStorePay()<SKPaymentTransactionObserver,SKProductsRequestDelegate>

@property (nonatomic, strong)NSString *goodsId;/**<wct20180420  商品id*/

@end

@implementation EMAppStorePay

- (instancetype)init
{
    self = [super init];
    if (self) {
       
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];// 4.設置支付服務
    }
    return self;
}
//結束后一定要銷毀
- (void)dealloc
{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
#pragma mark - public
-(void)starBuyToAppStore:(NSString *)goodsID
{
    if ([SKPaymentQueue canMakePayments]) {//5.判斷app是否允許apple支付
      
        [self getRequestAppleProduct:goodsID];// 6.請求蘋果后臺商品
        
    } else {
//        NSLog(@"not");
    }
}

#pragma mark - private
#pragma mark ------ 請求蘋果商品
- (void)getRequestAppleProduct:(NSString *)goodsID
{
    self.goodsId = goodsID;//把前面傳過來的商品id記錄一下,下面要用
    // 7.這里的com.czchat.CZChat01就對應著蘋果后臺的商品ID,他們是通過這個ID進行聯系的。
    NSArray *product = [[NSArray alloc] initWithObjects:goodsID,nil];
    NSSet *nsset = [NSSet setWithArray:product];
    
    //SKProductsRequest參考鏈接:https://developer.apple.com/documentation/storekit/skproductsrequest
    //SKProductsRequest 一個對象,可以從App Store檢索有關指定產品列表的本地化信息。
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];// 8.初始化請求
    request.delegate = self;
    [request start];// 9.開始請求
}
#pragma mark ------ 支付完成
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
    //交易驗證 本地驗證方法
    /*NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
     NSData *receipt = [NSData dataWithContentsOfURL:recepitURL];
     
     if(!receipt){
     
     }
     
     NSError *error;
     NSDictionary *requestContents = @{
     @"receipt-data": [receipt base64EncodedStringWithOptions:0]
     };
     NSLog(@"requestContentstr:%@",[receipt base64EncodedStringWithOptions:0]);
     NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
     options:0
     error:&error];
     
     
     //In the test environment, use https://sandbox.itunes.apple.com/verifyReceipt
     //In the real environment, use https://buy.itunes.apple.com/verifyReceipt
     // Create a POST request with the receipt data.
     NSURL *storeURL = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
     NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
     [storeRequest setHTTPMethod:@"POST"];
     [storeRequest setHTTPBody:requestData];
     
     // Make a connection to the iTunes Store on a background queue.
     NSOperationQueue *queue = [[NSOperationQueue alloc] init];
     [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
     completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
     if (connectionError) {
     } else {
     NSError *error;
     NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
     if (!jsonResponse) {  }
     //Parse the Response
     NSLog(@"成功了:%@",jsonResponse);
     }
     }];*/
    
    //此時告訴后臺交易成功,并把receipt傳給后臺驗證
    NSString *transactionReceiptString= nil;
    //系統IOS7.0以上獲取支付驗證憑證的方式應該改變,切驗證返回的數據結構也不一樣了。
    // 驗證憑據,獲取到蘋果返回的交易憑據
    // appStoreReceiptURL iOS7.0增加的,購買交易完成后,會將憑據存放在該地址
    NSURLRequest *appstoreRequest = [NSURLRequest requestWithURL:[[NSBundle mainBundle] appStoreReceiptURL]];
    NSError *error = nil;
    // 從沙盒中獲取到購買憑據
    
    NSData * receiptData = [NSURLConnection sendSynchronousRequest:appstoreRequest returningResponse:nil error:&error];
    // 20 BASE64 常用的編碼方案,通常用于數據傳輸,以及加密算法的基礎算法,傳輸過程中能夠保證數據傳輸的穩定性 21 BASE64是可以編碼和解碼的 22
    transactionReceiptString = [receiptData base64EncodedStringWithOptions:0];//[receiptData base64EncodedStringWithOptions:0];
    //    NSLog(@"requestContentstr:%@",[receiptData base64EncodedStringWithOptions:0]);
    
    //    NSDictionary *dic = @{@"orderCode":self.dataOrder.orderCode,
    //                          @"receipt":transactionReceiptString,
    //                          @"category":@"1"
    //                          };
    //    NSLog(@"diczhi:%@",dic);
    //
    //    self.tran = transaction;
    //    [self.bizEBeanBuy requestAppStorePaySuccessCallBack:dic];//蘋果支付成功,傳receipt-data給后臺驗證
    
    if (self.delegate && [self.delegate respondsToSelector:@selector(EMAppStorePay:responseAppStorePaySuccess:error:)]) {
        [self.delegate EMAppStorePay:self responseAppStorePaySuccess:@{@"value":transactionReceiptString} error:nil];
    }
    
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    
}

#pragma mark - delegate
#pragma mark ------ SKProductsRequestDelegate
// 10.接收到產品的返回信息,然后用返回的商品信息進行發起購買請求
- (void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    NSArray *product = response.products;
    
    if([product count] == 0){//如果服務器沒有產品
        return;
    }
    
    SKProduct *requestProduct = nil;
    for (SKProduct *pro in product) {
//        NSLog(@"%@", [pro description]);
//        NSLog(@"%@", [pro localizedTitle]);
//        NSLog(@"%@", [pro localizedDescription]);
//        NSLog(@"%@", [pro price]);
//        NSLog(@"%@", [pro productIdentifier]);
        // 11.如果后臺消費條目的ID與我這里需要請求的一樣(用于確保訂單的正確性)
        if([pro.productIdentifier isEqualToString:self.goodsId]){
            requestProduct = pro;
        }
    }
    // 12.發送購買請求,創建票據  這個時候就會有彈框了
    SKPayment *payment = [SKPayment paymentWithProduct:requestProduct];
    [[SKPaymentQueue defaultQueue] addPayment:payment];//將票據加入到交易隊列
    
}
#pragma mark ------ SKRequestDelegate (@protocol SKProductsRequestDelegate <SKRequestDelegate>)
//請求失敗
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
//    NSLog(@"error:%@", error);
}
//反饋請求的產品信息結束后
- (void)requestDidFinish:(SKRequest *)request
{
//    NSLog(@"信息反饋結束");
}

    
#pragma mark ------ SKPaymentTransactionObserver 監聽購買結果
// 13.監聽購買結果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction
{

    if (self.delegate && [self.delegate respondsToSelector:@selector(EMAppStorePay:responseAppStorePayStatusshow:error:)]) {
        [self.delegate EMAppStorePay:self responseAppStorePayStatusshow:@{@"value":transaction} error:nil];
    }
    
//    if (transaction.count > 0) {
//        //檢測是否有未完成的交易
//        SKPaymentTransaction* tran = [transaction firstObject];
//        if (tran.transactionState == SKPaymentTransactionStatePurchased) {
//            [self completeTransaction:tran];
//            [[SKPaymentQueue defaultQueue] finishTransaction:tran];//未完成的交易在此給它結束
//            return;
//        }
//    }

    for(SKPaymentTransaction *tran in transaction){

//        NSLog(@"%@",tran.payment.applicationUsername);
        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchased:{
//                NSLog(@"交易完成");
                // 購買后告訴交易隊列,把這個成功的交易移除掉。
                //走到這就說明這單交易走完了,無論成功失敗,所以要給它移出。finishTransaction
                [self completeTransaction:tran];//這兒出了問題拋異常,導致下面一句代碼沒執行
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                
            }
                break;
                
            case SKPaymentTransactionStatePurchasing:
//                NSLog(@"商品添加進列表");
                break;
                
            case SKPaymentTransactionStateRestored:
//                NSLog(@"已經購買過商品");
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                break;
                
            case SKPaymentTransactionStateFailed:
//                NSLog(@"交易失敗");
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                break;
                
            case SKPaymentTransactionStateDeferred:
//                NSLog(@"交易還在隊列里面,但最終狀態還沒有決定");
                break;
                
            default:
                break;
        }
        
    }

    
}

@end

5.沙盒測試

如下圖6,點添加創建沙盒測試賬號,賬號未未注冊成AppleID的賬號,測試前先到設置里退出當前AppleID,登錄沙盒測試賬號,沙盒測試賬號只能用來測試沙盒支付,不具備正常AppleID的功能.

image
準備工作

1.第一次測試內購需要卸載之前APP,找開發人員安裝可測試內購的APP。防止App Store下載的app走sandbox環境走不通;
2.在iPhone設置里面,退出原有賬號。登錄開發人員提供的內購測試賬號(可找開發申請新測試賬號);

6.交易安全機制

1.雙重驗證

蘋果審核人員審核內購的時候走的是沙盒環境對應沙盒驗證接口https://sandbox.itunes.apple.com/verifyReceipt,如果驗證receipt只有正式環境https://buy.itunes.apple.com/verifyReceipt,蘋果審核員走內購會驗證失敗,交易走不通,后果就是審核被拒.所以驗證的時候先默認走正式環境,如果返回21007的錯誤碼就去沙盒環境驗證,保證審核通過.

2.交易憑據receipt判重

一般我們驗證支付憑據(receipt)是否有效放后臺去做,如果后臺不做判重,同一個憑據就可以無數次驗證通過(蘋果也不判重),后臺就會給前端發放無數次商品,但是用戶只支付了一次錢取到一個支付憑據.所以安全的做法是后臺把驗證通過的支付憑據做個記錄,每次來新的憑據先判斷是否已經使用過,防止多次發放商品.

3.本地交易流水

在測試過程中,由于蘋果不提供交易流水,所以會出現無法對賬的情況,會提出一些莫名bug,因為測試不知道某個單的支付狀態,這時前端需要做個交易流水記錄,方便對賬和避免不必要的bug及撕逼.

在支付成功回調里面把當前交易數據存在本地持久化,然后去后臺驗證,出問題就那本地存的交易數據和后臺對,找出問題.

#pragma mark - EMAppStorePayDelegate
-(void)EMAppStorePay:(EMAppStorePay *)appStorePay responseAppStorePaySuccess:(NSDictionary *)dicValue error:(NSError *)error
{
   
    NSString *transactionReceiptString = [ZSTools objectOrNilForKey:@"value" fromDictionary:dicValue];
    
    NSDictionary *dic = @{@"orderCode":self.strOrderCode,
                          @"receipt":transactionReceiptString,
                          @"category":@"1"
                          };
//    NSLog(@"222diczhi:%@",dic);
    
    /*
     //wct20180601 本地交易流水,不測試內購就給注釋吧,省手機內存
    NSMutableDictionary *dicRec = [NSMutableDictionary dictionaryWithDictionary:self.dicPay];
    [dicRec setValue:self.strOrderCode forKey:@"orderCode"];
    [dicRec setValue:transactionReceiptString forKey:@"receipt"];
    [dicRec setValue:@"1" forKey:@"category"];
    NSString *time = [self getCurrentTimes];
    [dicRec setValue:time forKey:@"creatTime"];

    [self.modelEBean addDicReconciliation:dicRec];//對應下面的實現方法
*/
    
    [self.bizEBeanBuy requestAppStorePaySuccessCallBack:dic];//蘋果支付成功,傳receipt-data給后臺驗證
    [ZSTools loadActivityIndicatorOn:self.view withCenterPoint:self.view.center withTitleString:@"正在購買..." sizeType:2];

}

存儲持久化實現

-(void)addDicReconciliation:(NSDictionary *)dicEBean
{
    if (![self.arrReconciliationModel containsObject:dicEBean]) {
        [self.arrReconciliationModel addObject:dicEBean];
    }
    [self saveReconciliation];
}

- (void)saveReconciliation
{
    NSString *path = [NSString stringWithFormat:@"%@/%@_reconciliation.plist", [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0], [EMVideoUserSingleton sharedInstance].ugsvId];
    [self.arrReconciliationModel writeToFile:path atomically:NO];
}

7.注意事項

1.對賬問題

通過textflight下載的app走內購也是在sandbox環境。這時走內購不需要支付相應金額,但是對應的咱們后臺是正式環境,內購走通后返回的e豆(商品,以下e豆都對應商品)是正式環境。這就會造成沒支付錢,但是正式環境得到e豆了,對賬的時候要作記錄。

2.漏單的情況:

先看看支付流程,如下:
app iTunes app 后臺 app
1發起支付--->2扣費成功--->3得到receipt(支付憑據)--->4去后臺驗證憑據獲取e豆--->5返回數據,前端刷新數據

漏單情況1

3到4的時候出問題,比如斷網。此時前端會把支付憑據持久化存儲下來(期間用戶卸載APP此單在前端就真漏了),下次進入購買頁會先判斷有無未成功的支付,有就提示用戶,用戶選擇找回,重走4,5流程。

漏單情況2

4到5的時候出問題。此時后臺其實已經成功,只是前端沒獲取到數據,當漏單處理,還是上面的邏輯,會把該單存儲。下次進入的時候會先刷新數據(此時未獲取到e的豆已經獲取到了),然后提示有未完成單,此時點找回會提示無效的憑據,這是正常的,因為豆已經給了,此單已結束。

漏單情況3

2到3環節出問題屬于蘋果的問題,目前沒做處理。

3.漏單處理

1.在后臺返回商品支付回調失敗里面把當前交易數據持久化存儲,成功狀態下移除當前單數據.并檢查是否有已扣款未返商品單,對應下面checkHaveDidNotPay

}else{
        if (dicPara) {
            [self.modelEBean addDicEBean:dicPara];//傳receipt失敗,
            [self checkHaveDidNotPay];
        }
- (void)checkHaveDidNotPay
{
    if (self.modelEBean.arrEBeanBuyModel.count) {
        [EMTextAlertView title:@"溫馨提示" message:@"網絡不給力,e豆數據可能更新不及時,請重新加載。" leftTitle:@"下次再說" rightTitle:@"重新加載" complete:^(NSInteger index, NSString *title) {

            if (index == 1){//重新獲取會重新調用購買驗證
                for (NSDictionary *dic in self.modelEBean.arrEBeanBuyModel) {
                    [self.bizEBeanBuy requestAppStorePaySuccessCallBack:dic];
                }
            }
            
        }];
    }
}

根據需求,每次購買前先檢查有無之前漏單,有先處理漏單.視需求定.
我們目前是每次到購買頁面先檢查有無漏單

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self.bizVideoMine requestVideoMineData:nil];
    [self checkHaveDidNotPay];

}

有問題下面留言,有不足的地方歡迎指正.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374