iOS 内购集成

最近在做内购项目SDK,现将集成过程和集成内购过程中遇到的问题记载下来:

项目中使用到了中间货币(金币)的形式来进行功能使用,模式是使用RMB换成-金币比如:(1RMB = 10金币),所以会集成第三方的支付平台,使用了微信和支付宝的第三方平台过后,发现审核失败,被苹果拒绝,查了一查原因,才是因为苹果对app内的中间币的购买必须走苹果内购(比如冲点券,比如买钻石....)。所以无奈只有使用苹果内购,由于苹果内购的步骤很多,设置的东西太多,所以将这步骤记录下来。

首先设置协议

1.打开itunes Connect,选择协议,税务和银行业务

协议税务.png

2.点击Request Contracts(申请合同)下面的,request,点了几个确定和下一步后回到主界面。

协议税务和银行.png

Contact info:联系人信息
Bank info:银行信息
Tax info:税务信息


协议税务和银行.png

3.首先设置联系人信息,点击Contact info下面的 Set up(设置),点击Add New Contract(增加先的联系方式)

填写完成.png

4.填写详情
填写完成后点击save(保存)

保存信息.png

5.在下面的所有项目中都选择刚刚填写的信息,选择后点击右下角的done(完成),你可以创建很多联系人,在不同的职务选择不同的联系人。因为我是独立开发,所以我全部填写的我自己。
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,此时联系人信息已经变成可以编辑状态,银行信息为浏览状态。

设置税务信息.png

弹出的界面中,税务分为三种
U.S Tax Forms: 美国税务
Australia Tax Forms:澳大利亚税务
Canada Tax Forms: 加拿大税务
这里我选择的美国税务,就是第一个

选择美国税务.png

弹出第一个选择,点击submit(提交)后,弹出第二个选择

提交税务信息.png

弹出第二个选择,选择后点击submit

提交税务信息2.png

弹出第三个页面,填写的资料后点击提交,记得勾选页面上的几个复选框

提交税务信息3.png

在提交成功后,状态就变成processing成功

提交税务信息4.png

到这里设置的协议就已经设置完了。

创建项目的内购

1.进入到项目的APP信息页面,点击功能,在弹出的页面点击App内购买项目后面的➕。

创建内购项目.png

2.在弹出的新对话框中选择你需要哪一种服务,由于我的项目需要兑换成消耗的金币,所以我选择第一个。选择后点击创建。

选择内购项目类型.png

3.开始填写内购项目信息。填完后点击右上角的存储(所有信息必须填写完整)。

填写内购项目信息.png

4.点击存储后,内购列表就会有刚刚创建的内购条目。

内购条目.png

你app有几个内购级别就需要依次创建几个条目。

添加测试账号,用来测试支付功能

1.点击图中用户和职能

添加测试账号.png

2.点击沙盒测试员,然后点击左边的➕按钮。

添加沙盒测试员.png

3.设置好信息点击右上角存储就可以,记住里面的邮箱和密码用于支付的时候登陆Apple id


添加测试员信息.png

代码集成

打开自己的项目,创建一个测试类。代码都有注释和步骤,直接上代码。

注意:

1.必须用真机测试。
2.测试的时候必须退出自己的apple ID。弹出页面后登陆沙盒的测试apple id。

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

先上代码再细分析

实现观察者监听付钱的代理方法,只要交易发生变化就会走下面的方法

// 监听交易操作与结果
- (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(@"已经购买过商品");
                [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];

请求验证
获取到票据以后我们通过App Store来验证票据是否真实
沙盒状态下使用:https://sandbox.itunes.apple.com/verifyReceipt来验证
生产环境下使用:https://buy.itunes.apple.com/verifyReceipt
常见的验证状态代码:

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

/**
 获取收据信息

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

/**
 验证收据信息
 
 配置KK_RECEIPT_VALIDATAURL 为提交receipt到服务端地址
 @param recepiptString AppStore返回的收据信息
 @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

/// 监听交易操作与结果
- (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(@"已经购买过商品");
                [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;
        
        // 方法一 获取票据并向服务端提交票据信息
        // 需要KK_RECEIPT_VALIDATAURL 配置服务端地址
        {
            [InAppPurchaseValidate ValidatReceipteWithSuccessBlock:^(id responesData) {
                // 提交成功
                NSLog(@"服务端已返回验证结果responesData");
            } failBlock:^(NSError *error) {
                NSLog(@"error:%@",error);
            }];
        }

内购的注意事项

1.一般发生于首次提交app或添加新商品,当你的app通过审核以后,你发现在生产环境下获取不到商品,这是因为app虽然过审核了,但是内购商品还没有正式添加到苹果的服务器里,耐心等待一段时间就可以啦~

  1. 代码中的_currentProId所填写的是你的购买项目的的ID,这个和第二步创建的内购的productID要一致;本例中是 123。

  2. 在监听购买结果后,一定要调用[[SKPaymentQueue defaultQueue] finishTransaction:tran];来允许你从支付队列中移除交易。

  3. 沙盒环境测试appStore内购流程的时候,请使用没越狱的设备。

  4. 请务必使用真机来测试,一切以真机为准。

  5. 项目Bundle identifier需要与您申请AppID时填写的bundleID一致,不然会无法请求到商品信息。

  6. 真机测试的时候,一定要退出原来的账号,才能用沙盒测试账号

  7. 二次验证,请注意区分宏, 测试用沙盒验证,App Store审核的时候也使用的是沙盒购买,所以验证购买凭证的时候需要判断返回Status
    Code决定是否去沙盒进行二次验证,为了线上用户的使用,验证的顺序肯定是先验证正式环境,此时若返回值为21007,就需要去沙盒二次验证,因为此购买的是在沙盒进行的。

9.您的应用是否处于等待开发者发布(Pending Developer Release)状态?等待发布状态的IAP是无法测试的。

10.您的内购项目是否是最近才新建的,或者进行了更改?内购项目需要一段时间才能反应到所有服务器上,这个过程一般是一两小时,也可能再长一些达到若干小时。

11.您在iTC中Contracts, Tax, and Banking Information项目中是否有还没有设置或者过期了的项目?不完整的财务信息无法进行内购测试。

12.您是在越狱设备上进行内购测试么?越狱设备不能用于正常内购,您需要重装或者寻找一台没有越狱的设备。

13.您的应用是否是被拒状态(Rejected)或自己拒绝(Developer Rejected)了?被拒绝状态的应用的话对应还未通过的内购项目也会一起被拒,因此您需要重新将IAP项目设为Cleared for Sale。

14.您使用的测试账号是否是美国区账号?虽然不是一定需要,但是鉴于其他地区的测试账号经常抽风,加上美国区账号一直很稳定,因此强烈建议使用美国区账号。正常情况下IAP不需要进行信用卡绑定和其他信息填写,如果你遇到了这种情况,可以试试删除这个测试账号再新建一个其他地区的。

15.您是否将设备上原来的app删除了,并重新进行了安装?记得在安装前做一下Clean和Clean Build Folder。

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

文章有点长~~~
最后附上小demo:
内购集成Demo

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容