iOS 內購集成

最近在做內購項目SDK,現(xiàn)將集成過程和集成內購過程中遇到的問題記載下來:

項目中使用到了中間貨幣(金幣)的形式來進行功能使用,模式是使用RMB換成-金幣比如:(1RMB = 10金幣),所以會集成第三方的支付平臺,使用了微信和支付寶的第三方平臺過后,發(fā)現(xiàn)審核失敗,被蘋果拒絕,查了一查原因,才是因為蘋果對app內的中間幣的購買必須走蘋果內購(比如沖點券,比如買鉆石....)。所以無奈只有使用蘋果內購,由于蘋果內購的步驟很多,設置的東西太多,所以將這步驟記錄下來。

首先設置協(xié)議

1.打開itunes Connect,選擇協(xié)議,稅務和銀行業(yè)務

協(xié)議稅務.png

2.點擊Request Contracts(申請合同)下面的,request谆棺,點了幾個確定和下一步后回到主界面。

協(xié)議稅務和銀行.png

Contact info:聯(lián)系人信息
Bank info:銀行信息
Tax info:稅務信息


協(xié)議稅務和銀行.png

3.首先設置聯(lián)系人信息,點擊Contact info下面的 Set up(設置),點擊Add New Contract(增加先的聯(lián)系方式)

填寫完成.png

4.填寫詳情
填寫完成后點擊save(保存)

保存信息.png

5.在下面的所有項目中都選擇剛剛填寫的信息,選擇后點擊右下角的done(完成),你可以創(chuàng)建很多聯(lián)系人,在不同的職務選擇不同的聯(lián)系人悉罕。因為我是獨立開發(fā),所以我全部填寫的我自己。
Senior Management:高管
Financial:財務
Technical:技術支持
Legal:法務
Marketing:市場推廣

顯示信息.png

6.設置銀行信息,點擊Back info下面的Set up,彈出頁面

點擊Add Bank Account(添加銀行賬號)

添加銀行卡.png

選擇china,后點擊next。

添加銀行卡選擇國家.png

填寫了CNAPS Code后點擊Next

輸入銀行卡號.png

會彈出你的銀行卡開戶地的信息,確認一下點擊next

確認銀行卡信息.png

填寫銀行卡信息壁袄,注意:戶主名只能寫拼音,比如:李三(Li San)类早。填完后點擊Next

添加銀行卡信息.png

彈出確定信息頁面,在下面打鉤后點擊Save

確認保存信息.png

點擊了save后就可以在彈出的頁面中選擇剛剛填寫的卡了然想。選擇后點擊Save

選擇填寫的卡保存.png

7.設置稅務信息莺奔,點擊Tax info下面的Set up,此時聯(lián)系人信息已經(jīng)變成可以編輯狀態(tài),銀行信息為瀏覽狀態(tài)。

設置稅務信息.png

彈出的界面中,稅務分為三種
U.S Tax Forms: 美國稅務
Australia Tax Forms:澳大利亞稅務
Canada Tax Forms: 加拿大稅務
這里我選擇的美國稅務,就是第一個

選擇美國稅務.png

彈出第一個選擇,點擊submit(提交)后,彈出第二個選擇

提交稅務信息.png

彈出第二個選擇,選擇后點擊submit

提交稅務信息2.png

彈出第三個頁面令哟,填寫的資料后點擊提交,記得勾選頁面上的幾個復選框

提交稅務信息3.png

在提交成功后,狀態(tài)就變成processing成功

提交稅務信息4.png

到這里設置的協(xié)議就已經(jīng)設置完了。

創(chuàng)建項目的內購

1.進入到項目的APP信息頁面,點擊功能屏富,在彈出的頁面點擊App內購買項目后面的?神年。

創(chuàng)建內購項目.png

2.在彈出的新對話框中選擇你需要哪一種服務护奈,由于我的項目需要兌換成消耗的金幣,所以我選擇第一個。選擇后點擊創(chuàng)建岛马。

選擇內購項目類型.png

3.開始填寫內購項目信息湿诊。填完后點擊右上角的存儲(所有信息必須填寫完整)簿晓。

填寫內購項目信息.png

4.點擊存儲后,內購列表就會有剛剛創(chuàng)建的內購條目。

內購條目.png

你app有幾個內購級別就需要依次創(chuàng)建幾個條目臀脏。

添加測試賬號,用來測試支付功能

1.點擊圖中用戶和職能

添加測試賬號.png

2.點擊沙盒測試員,然后點擊左邊的?按鈕延塑。

添加沙盒測試員.png

3.設置好信息點擊右上角存儲就可以,記住里面的郵箱和密碼用于支付的時候登陸Apple id


添加測試員信息.png

代碼集成

打開自己的項目,創(chuàng)建一個測試類关带。代碼都有注釋和步驟,直接上代碼。

注意:

1.必須用真機測試沼撕。
2.測試的時候必須退出自己的apple ID宋雏。彈出頁面后登陸沙盒的測試apple id。

使用的時候首先要導入        #import <StoreKit/StoreKit.h>

先上代碼再細分析

實現(xiàn)觀察者監(jiān)聽付錢的代理方法,只要交易發(fā)生變化就會走下面的方法

// 監(jiān)聽交易操作與結果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
    for(SKPaymentTransaction *tran in transaction){
        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchased:
            {
                NSLog(@"交易完成");
                [self completeTransaction:tran];
                //// 去驗證是否真正的支付成功了
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            }break;
            case SKPaymentTransactionStatePurchasing:
            {
                NSLog(@"商品添加進列表");
            }break;
            case SKPaymentTransactionStateRestored:
            {
                NSLog(@"已經(jīng)購買過商品");
                [self restoreTransaction:tran];
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            }break;
            case SKPaymentTransactionStateFailed:
            {
                NSLog(@"交易失敗%@",tran.error);
                [self failedTransaction:tran];
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            }break;
            default:
            {
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            }break;
        }
    }
}

注意:在購買成功后需要釋放
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];

請求驗證
獲取到票據(jù)以后我們通過App Store來驗證票據(jù)是否真實
沙盒狀態(tài)下使用:https://sandbox.itunes.apple.com/verifyReceipt來驗證
生產環(huán)境下使用:https://buy.itunes.apple.com/verifyReceipt
常見的驗證狀態(tài)代碼:

InAppPurchaseValidate.h

#import <Foundation/Foundation.h>

typedef void (^SuccessBlock)(id response);

typedef void (^FailBlock)(NSError *error);


#define KK_RECEIPT_VALIDATAURL @"http://10.0.0.110:8001/api/pay/callback_iap"

@interface InAppPurchaseValidate : NSObject

/**
 獲取收據(jù)信息

 @param successBlock 成功回調
 @param failBlock 失嵊回調
 */
+(void)loadReceiptWithSuccessBlock:(SuccessBlock)successBlock failBlock:(FailBlock)failBlock;

/**
 驗證收據(jù)信息
 
 配置KK_RECEIPT_VALIDATAURL 為提交receipt到服務端地址
 @param recepiptString AppStore返回的收據(jù)信息
 @param successBlock 成功回調
 @param failBlock 失嵊回調
 */
+(void)validateWithReceipt:(NSString *)recepiptString successBlock:(SuccessBlock)successBlock failBlock:(FailBlock)failBlock;


/**
 合并loadReceiptWithSuccessBlock:與validateWithReceipt:獲取recpipt信息并向服務器提交驗證

 配置KK_RECEIPT_VALIDATAURL 為提交receipt到服務端地址
 @param successBlock 成功回調
 @param failBlock 失嵊回調
 */
+(void)ValidatReceipteWithSuccessBlock:(SuccessBlock)successBlock failBlock:(FailBlock)failBlock;


@end

InAppPurchaseValidate.m 文件

#import "InAppPurchaseManager.h"


static NSMutableArray* productIdentifiers = nil;
static InAppPurchaseManager* m_pInstance = nil;

@interface InAppPurchaseManager()
{
    SKProductsRequest *productsRequest;
    SKProduct *startedPaymentProduct;
}
@property (nonatomic, copy, readwrite) LoadStoreDidBlock loadStoreDidBlock;
@property (nonatomic, copy, readwrite) PurchaseStatusBlock purchaseStatusBlock;

@end
@implementation InAppPurchaseManager

#pragma mark- init
+ (InAppPurchaseManager*) getInstance
{
    if (m_pInstance == nil){
        m_pInstance = [[InAppPurchaseManager alloc] init];
    }
    return m_pInstance;
}

+ (void) releaseInstance
{
    if (m_pInstance){
        m_pInstance = nil;
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    }
}

#pragma mark- ProductId
- (void)addProductIdentifiers:(NSArray*)identifiers
{
    if (productIdentifiers == nil)
    {
        productIdentifiers = [[NSMutableArray alloc] init];
    }
    
    [productIdentifiers addObjectsFromArray:identifiers];
    
}

- (void) clearProductIdentifiers
{
    if (productIdentifiers)
    {
        [productIdentifiers removeAllObjects];
    }
}


#pragma mark- Public methods

- (void)loadStore:(LoadStoreDidBlock)loadStoreDidBlock
{
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    
    self.loadStoreDidBlock = loadStoreDidBlock;

    [self requestProductData];
    
}

- (void)requestProductData
{
    if(productIdentifiers.count==0) {
        NSLog(@"error: no productId");
        return;
    }
    
    NSSet *productIdentifiersSet = [NSSet setWithArray:productIdentifiers];
    productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiersSet];
    productsRequest.delegate = self;
    [productsRequest start];
}

- (BOOL)canMakePurchases
{
    return [SKPaymentQueue canMakePayments];
}

-(void)purchaseWithProductId:(NSString *)identifier purchaseStatusBlock:(PurchaseStatusBlock)purchaseStatusBlock
{
    self.purchaseStatusBlock = purchaseStatusBlock;
    startedPaymentProduct = nil;
    [self addProductIdentifiers:@[identifier]];
    if(self.productList == nil) {
        __weak typeof(self) weakSelf = self;
        [self loadStore:^{
            if(weakSelf.productList == nil)
                weakSelf.productList = [[NSArray alloc]init];
            [weakSelf purchaseWithProductId:identifier purchaseStatusBlock:purchaseStatusBlock];
        }];
        return;
    }
    
    for (int i = 0; i < self.productList.count; ++i) {
        SKProduct* p = [self.productList objectAtIndex:i];
        if ([[p productIdentifier] isEqualToString:identifier]) {
            startedPaymentProduct = p;
            break;
        }
    }
    
    if(startedPaymentProduct == nil) {
        NSLog(@"沒有找到該商品");
        if(purchaseStatusBlock) purchaseStatusBlock(nil,InAppPurchaseFailure);
        return;
    }
    
    [self paymentWithProduct:startedPaymentProduct];
}
-(void)paymentWithProduct:(SKProduct *)product
{
    if (product == nil) {
        NSLog(@"err: startedPaymentProduct is nil");
        return;
    }
    
    SKPayment *payment = [SKPayment paymentWithProduct:product];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}


#pragma mark-  SKProductsRequestDelegate
/// 接收商品信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    if (self.productList) {
        self.productList = nil;
    }
    self.productList = response.products;
    
    NSMutableArray* productListArray = [[NSMutableArray alloc] init];
    for (int i = 0; i < self.productList.count; ++i) {
        NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
        SKProduct* p = [self.productList objectAtIndex:i];
        [dict setObject:(p.localizedTitle != nil ? p.localizedTitle : @"") forKey:@"localizedTitle"];
        [dict setObject:(p.localizedDescription != nil ? p.localizedDescription : @"") forKey:@"localizedDescription"];
        [dict setObject:p.price forKey:@"price"];
        [dict setObject:p.productIdentifier forKey:@"productIdentifier"];
        [productListArray addObject:dict];
    }
    
    NSMutableArray* invalidProductArray = [[NSMutableArray alloc] init];
    for (NSString *invalidProductId in response.invalidProductIdentifiers)
    {
        [invalidProductArray addObject:invalidProductId];
    }
    
    if(self.loadStoreDidBlock) self.loadStoreDidBlock();
    
}

#pragma mark - SKPaymentTransactionObserver methods

/// 監(jiān)聽交易操作與結果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
    for(SKPaymentTransaction *tran in transaction){
        switch (tran.transactionState) {
            case SKPaymentTransactionStatePurchased:
            {
                NSLog(@"交易完成");
                [self completeTransaction:tran];
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            }break;
            case SKPaymentTransactionStatePurchasing:
            {
                NSLog(@"商品添加進列表");
            }break;
            case SKPaymentTransactionStateRestored:
            {
                NSLog(@"已經(jīng)購買過商品");
                [self restoreTransaction:tran];
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            }break;
            case SKPaymentTransactionStateFailed:
            {
                NSLog(@"交易失敗%@",tran.error);
                [self failedTransaction:tran];
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            }break;
            default:
            {
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
            }break;
        }
    }
}

//交易結束
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
    [self recordTransaction:transaction];
    [self provideContent:transaction.payment.productIdentifier];
    [self finishTransaction:transaction status:0];
}

//交易失敗
- (void)restoreTransaction:(SKPaymentTransaction *)transaction
{
    [self recordTransaction:transaction.originalTransaction];
    [self provideContent:transaction.originalTransaction.payment.productIdentifier];
    [self finishTransaction:transaction status:1];
}

- (void)failedTransaction:(SKPaymentTransaction *)transaction
{
        [self finishTransaction:transaction status:-1];
}

- (void)finishTransaction:(SKPaymentTransaction *)transaction status:(int)status
{
    InAppPurchaseStatus inAppPurchasestatus = InAppPurchaseSuccess;
    if(status == 1) inAppPurchasestatus = InAppPurchaseRestore;
    if(status == -1) inAppPurchasestatus = InAppPurchaseFailure;

    if(self.purchaseStatusBlock) self.purchaseStatusBlock(transaction, inAppPurchasestatus);
    
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

在你使用到的 地方直接調用

// 購買產品
    [[InAppPurchaseManager getInstance] purchaseWithProductId:@"com.test1.020.App009" purchaseStatusBlock:^(SKPaymentTransaction *paymentTransaction, InAppPurchaseStatus status) {
       
        if(status == InAppPurchaseFailure) {
            NSLog(@"未完成支付");
            return;
        }
        
        NSString *productIdentifier = paymentTransaction.payment.productIdentifier;
        
        // 方法一 獲取票據(jù)并向服務端提交票據(jù)信息
        // 需要KK_RECEIPT_VALIDATAURL 配置服務端地址
        {
            [InAppPurchaseValidate ValidatReceipteWithSuccessBlock:^(id responesData) {
                // 提交成功
                NSLog(@"服務端已返回驗證結果responesData");
            } failBlock:^(NSError *error) {
                NSLog(@"error:%@",error);
            }];
        }

內購的注意事項

1.一般發(fā)生于首次提交app或添加新商品务豺,當你的app通過審核以后磨总,你發(fā)現(xiàn)在生產環(huán)境下獲取不到商品,這是因為app雖然過審核了笼沥,但是內購商品還沒有正式添加到蘋果的服務器里蚪燕,耐心等待一段時間就可以啦~

  1. 代碼中的_currentProId所填寫的是你的購買項目的的ID,這個和第二步創(chuàng)建的內購的productID要一致奔浅;本例中是 123馆纳。

  2. 在監(jiān)聽購買結果后,一定要調用[[SKPaymentQueue defaultQueue] finishTransaction:tran];來允許你從支付隊列中移除交易汹桦。

  3. 沙盒環(huán)境測試appStore內購流程的時候鲁驶,請使用沒越獄的設備。

  4. 請務必使用真機來測試舞骆,一切以真機為準钥弯。

  5. 項目Bundle identifier需要與您申請AppID時填寫的bundleID一致壹罚,不然會無法請求到商品信息。

  6. 真機測試的時候寿羞,一定要退出原來的賬號猖凛,才能用沙盒測試賬號

  7. 二次驗證,請注意區(qū)分宏绪穆, 測試用沙盒驗證辨泳,App Store審核的時候也使用的是沙盒購買,所以驗證購買憑證的時候需要判斷返回Status
    Code決定是否去沙盒進行二次驗證玖院,為了線上用戶的使用菠红,驗證的順序肯定是先驗證正式環(huán)境,此時若返回值為21007难菌,就需要去沙盒二次驗證试溯,因為此購買的是在沙盒進行的。

9.您的應用是否處于等待開發(fā)者發(fā)布(Pending Developer Release)狀態(tài)郊酒?等待發(fā)布狀態(tài)的IAP是無法測試的遇绞。

10.您的內購項目是否是最近才新建的,或者進行了更改燎窘?內購項目需要一段時間才能反應到所有服務器上摹闽,這個過程一般是一兩小時,也可能再長一些達到若干小時褐健。

11.您在iTC中Contracts, Tax, and Banking Information項目中是否有還沒有設置或者過期了的項目付鹿?不完整的財務信息無法進行內購測試。

12.您是在越獄設備上進行內購測試么蚜迅?越獄設備不能用于正常內購舵匾,您需要重裝或者尋找一臺沒有越獄的設備。

13.您的應用是否是被拒狀態(tài)(Rejected)或自己拒絕(Developer Rejected)了谁不?被拒絕狀態(tài)的應用的話對應還未通過的內購項目也會一起被拒坐梯,因此您需要重新將IAP項目設為Cleared for Sale。

14.您使用的測試賬號是否是美國區(qū)賬號拍谐?雖然不是一定需要烛缔,但是鑒于其他地區(qū)的測試賬號經(jīng)常抽風馏段,加上美國區(qū)賬號一直很穩(wěn)定轩拨,因此強烈建議使用美國區(qū)賬號。正常情況下IAP不需要進行信用卡綁定和其他信息填寫院喜,如果你遇到了這種情況亡蓉,可以試試刪除這個測試賬號再新建一個其他地區(qū)的。

15.您是否將設備上原來的app刪除了喷舀,并重新進行了安裝砍濒?記得在安裝前做一下Clean和Clean Build Folder淋肾。

16.您的plist中的Bundle identifier的內容是否和您的AppID一致?

文章有點長~~~
最后附上小demo:
內購集成Demo

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末爸邢,一起剝皮案震驚了整個濱河市樊卓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杠河,老刑警劉巖碌尔,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異券敌,居然都是意外死亡唾戚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門待诅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叹坦,“玉大人,你說我怎么就攤上這事卑雁∧际椋” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵测蹲,是天一觀的道長锐膜。 經(jīng)常有香客問我,道長弛房,這世上最難降的妖魔是什么道盏? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮文捶,結果婚禮上荷逞,老公的妹妹穿的比我還像新娘。我一直安慰自己粹排,他們只是感情好种远,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著顽耳,像睡著了一般坠敷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上射富,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天膝迎,我揣著相機與錄音,去河邊找鬼胰耗。 笑死限次,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播卖漫,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼费尽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了羊始?” 一聲冷哼從身側響起旱幼,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎突委,沒想到半個月后速警,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡鸯两,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年闷旧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钧唐。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡忙灼,死狀恐怖,靈堂內的尸體忽然破棺而出钝侠,到底是詐尸還是另有隱情该园,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布帅韧,位于F島的核電站里初,受9級特大地震影響,放射性物質發(fā)生泄漏忽舟。R本人自食惡果不足惜双妨,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叮阅。 院中可真熱鬧刁品,春花似錦、人聲如沸浩姥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽勒叠。三九已至兜挨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間眯分,已是汗流浹背拌汇。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留颗搂,地道東北人担猛。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像丢氢,于是被迫代替她去往敵國和親傅联。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內容