iOS的应用内购买 iAP 的一些问题

iOS Jan 18, 2015

内购这块坑不算少,另外因为 sandbox 测试所需要特定的配置也很多,所以对于经验不太多的开发者来说很容易就遇到各种问题,并且测试时出错Apple给出的也只有“Can not connect iTunes Store”或者"Invalid Product IDs"之类毫无价值的错误提示,并没有详细的错误说明,因此调试起来往往没有方向。

再就是博主比较懒啦,就不扫盲了,部分步骤简略些。先申请 app 证书,必须支持推送,然后项目中打开推送。

  • iTC中,新建一个 app,注意包名一定对应,再添加内购项目。
  • 单纯测试的话,截图可以不传。
  • 添加沙盒测试者 注意必须是未注册 Apple ID 的邮件地址,然后收到邮件激活即可

第三方库实现

下面列出来的库,一个都不好用,最终我用了 IAPHelper,自行 github 吧。

if(![IAPShare sharedHelper].iap) {
        
        NSSet* dataSet = [[NSSet alloc] initWithObjects:@"eduadm_donate_3", @"eduadm_donate_6", @"eduadm_donate_12", @"eduadm_donate_25", nil];
        
        [IAPShare sharedHelper].iap = [[IAPHelper alloc] initWithProductIdentifiers:dataSet];
        
    }
    
    [IAPShare sharedHelper].iap.production = NO;
    
    [[IAPShare sharedHelper].iap requestProductsWithCompletion:^(SKProductsRequest *request, SKProductsResponse *response) {
        if(response > 0 ) {
        
            SKProduct* product =[[IAPShare sharedHelper].iap.products objectAtIndex:0];
            
            [[IAPShare sharedHelper].iap buyProduct:product onCompletion:^(SKPaymentTransaction *transcation) {
                
                
                if(transcation.error)
                {
                    NSLog(@"Fail %@",[transcation.error localizedDescription]);
                }
                else if(transcation.transactionState == SKPaymentTransactionStatePurchased) {
                    
                    [[IAPShare sharedHelper].iap checkReceipt:transcation.transactionReceipt AndSharedSecret:@"your sharesecret" onCompletion:^(NSString *response, NSError *error) {
                        
                        //Convert JSON String to NSDictionary
                        NSDictionary* rec = [IAPShare toJSON:response];
                        
                        if([rec[@"status"] integerValue]==0)
                        {
                            NSString *productIdentifier = transcation.payment.productIdentifier;
                            [[IAPShare sharedHelper].iap provideContent:productIdentifier];
                            NSLog(@"SUCCESS %@",response);
                            NSLog(@"Pruchases %@",[IAPShare sharedHelper].iap.purchasedProducts);
                        }
                        else {
                            NSLog(@"Fail");
                        }
                    }];
                }
                else if(transcation.transactionState == SKPaymentTransactionStateFailed) {
                    NSLog(@"Fail");
                }
                
            }];
        }
    }];

下面是结果:

2015-01-18 14:45:02.526 eduadmin[441:92076] SUCCESS {
"receipt":{"original_purchase_date_pst":"2015-01-17 22:44:51 America/Los_Angeles", "purchase_date_ms":"1421563491028", "unique_identifier":"25b170fda433c754e5ac3fc1a054a4b78560c716", "original_transaction_id":"1000000139233309", "bvrs":"62", "transaction_id":"1000000139233309", "quantity":"1", "unique_vendor_identifier":"9EE7D835-713A-4BDF-A2C0-50035D978ADE", "item_id":"959284801", "product_id":"eduadm_donate_12", "purchase_date":"2015-01-18 06:44:51 Etc/GMT", "original_purchase_date":"2015-01-18 06:44:51 Etc/GMT", "purchase_date_pst":"2015-01-17 22:44:51 America/Los_Angeles", "bid":"com.pupboss.eduadmin", "original_purchase_date_ms":"1421563491028"}, "status":0}
2015-01-18 14:45:02.527 eduadmin[441:92076] Pruchases {(
    "eduadm_donate_12"
)}

不过还有个问题

直接

[[IAPShare sharedHelper].iap.products objectAtIndex:0]

你会不知道他顺序

加上这么一句

for (SKProduct *pro in [IAPShare sharedHelper].iap.products) {
                
                NSLog(@"%@-----%@-----%@-----%@-----%@",pro.localizedDescription,pro.localizedTitle,pro.price,pro.priceLocale,pro.productIdentifier);
            }

挨个打印出来,再做判断就好了

2015-01-18 17:43:30.568 eduadmin[521:120874] 大约是一个汉堡的价格-----自由捐赠-----12-----<__NSCFLocale: 0x1740e8f00>-----eduadm_donate_12
2015-01-18 17:43:30.571 eduadmin[521:120874] 大约是中杯星巴克的价格-----自由捐赠-----25-----<__NSCFLocale: 0x1740e8f00>-----eduadm_donate_25
2015-01-18 17:43:30.571 eduadmin[521:120874] 大约是一瓶可口可乐的价格-----自由捐赠-----3-----<__NSCFLocale: 0x1740e8f00>-----eduadm_donate_3
2015-01-18 17:43:30.572 eduadmin[521:120874] 大约是一碗麻辣烫的价格-----自由捐赠-----6-----<__NSCFLocale: 0x1740e8f00>-----eduadm_donate_6

下面介绍坑们

  • 你是否在 iOS Dev Center中打开了对应应用AppID的 In-App Purchases 功能?登陆 iOS Dev Centre 的 Certificates, Identifiers & Profiles 下,在 Identifiers 中找到正在开发的 App,In-App Purchase 一项应当显示 Enabled(如果使用 Xcode5,可以直接在 Xcode 的 Capabilities 页面中打开 In-App Purchases)。
  • 你是否在 iTunes Connect 中注册了你的 IAP 项目,并将其设为 Cleared for Sale?
  • 你的 plist 中的 Bundle identifier 的内容是否和你的 AppID 一致?
  • 你是否正确填写了 Version(CFBundleVersion)和 Build(CFBuildNumber)两个数字?两者缺一不可。
  • 你用代码向 Apple 申请售卖物品列表时是否使用了完整的在 iTC 注册的 Product ID?(使用在 IAP 管理中内购项目的 Product ID一栏中的字符串)
  • 你是否在打开 IAP 以后重新生成过包含 IAP 许可的 provisioning profile?
  • 你是否重新导入了新的包含 IAP 的 provisioning profile?建议在 Organiser 中先删掉原来设备上的老的 provisioning profile。
  • 你是否在用包含 IAP 的 provisioning profile 在部署测试程序?在 Xcode5 中,建议使用 General 中的 Team 选项来自动管理。
  • 你是否是在模拟器中测试 IAP?虽然理论上说模拟器在某些情况下可以测试 IAP,但是条件很多也不让人安心,因此你确实需要一台真机来做 IAP 测试。
  • 你是在企业版发布中测试 IAP 么?因为企业版没有 iTC 进行内购项目管理,也无法发布 AppStore 应用,所以你在企业版的 build 中不能使用 IAP。
  • 你是否将设备上原来的 app 删除了,并重新进行了安装?记得在安装前做一下 Clean 和 Clean Build Folder。
  • 你是否在运行应用前将设备上实际的 Apple ID 登出了?建议在设置->iTunes Store 和 App Stroe 中将使用中的 Apple ID 登出,以未登录状态进入应用进行测试。
  • 你是否使用的是 Test User?如果你还没有创建 Test User,你需要到 iTC 中创建。
  • 你使用的测试账号是否是美国区账号?虽然不是一定需要,但是鉴于其他地区的测试账号经常抽风,加上美国区账号一直很稳定,因此强烈建议使用美国区账号。正常情况下 IAP 不需要进行信用卡绑定和其他信息填写,如果你遇到了这种情况,可以试试删除这个测试账号再新建一个其他地区的。
  • 你是否有新建账户进行测试?可能的话,可以使用新建测试账户试试看,因为某些特定情况下测试账户会被 Apple 锁定。
  • 你的应用是否是被拒状态(Rejected)或自己拒绝(Developer Rejected)了?被拒绝状态的应用的话对应还未通过的内购项目也会一起被拒,因此你需要重新将IAP项目设为 Cleared for Sale。
  • 你的应用是否处于等待开发者发布(Pending Developer Release)状态?等待发布状态的 IAP 是无法测试的。
  • 你的内购项目是否是最近才新建的,或者进行了更改?内购项目需要一段时间才能反应到所有服务器上,这个过程一般是一两小时,也可能再长一些达到若干小时。
  • 你在 iTC 中 Contracts, Tax, and Banking Information 项目中是否有还没有设置或者过期了的项目?不完整的财务信息无法进行内购测试。
  • 你是在越狱设备上进行内购测试么?越狱设备不能用于正常内购,你需要重装或者寻找一台没有越狱的设备。
  • 你是否能正常连接到 Apple 的服务器,你可以访问 Apple 开发者论坛关于 IAP 的板块,如果苹果服务器正 down 掉,那里应该有热烈的讨论。

详细教程

第三方库推荐


StoreKit 实现

上面写的比较乱,主要是之前的准备太繁琐,这次主要说 StoreKit 的代码。

IAPHelper 用法自行 Github ,这次我主要讲 StoreKit的用法。

先打打预防针,我说下都有哪些坑

  • 第三方库,无法准确的获取 已经购买的项目 。也可能是我用的姿势不对。
  • 系统自带的,老特么闪退,概率在 1/5 ,点进去内购页,退出,点进去,退出,点进去,我擦,闪退了。

第三方库不说了, IAPHelper 用法自行 GithubCargoBay,文档写的那是个屁啊,鬼能看懂,RMStore 更蛋疼,导入项目,编译就报错,缺什么文件。

总之,我水平低,玩不了这么高级玩意儿。无奈来研究系统自带的了。

博主比较懒啦,能省的就省,至于怎么抽方法,你们自己看着来啦。

1.控制器中添加两个属性,一个代理协议一个观察者协议

@interface DonateTableViewController () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
{
    NSArray *_productArr;
    NSMutableArray *_purchasedArr;
}

2.添加观察者对象为自身

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

3.创建一个请求

if ([SKPaymentQueue canMakePayments]) {
        // 没有意外
        NSSet *set =[NSSet setWithArray:@[@"eduadm_donate_3", @"eduadm_donate_6", @"eduadm_donate_12", @"eduadm_donate_25"]];
        
        SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
        
        request.delegate = self;
        [request start];
        
    }else {
    
        UIAlertView *view = [[UIAlertView alloc] initWithTitle:@"访问限制" message:@"您已经禁止应用内购买" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles: nil];
        
        [view show];
    }

4.解析可购买产品数据

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
    _productArr = response.products;
    
    [MBProgressHUD hideHUD];
    [self.tableView reloadData];
}

5.把产品添加到队列

SKPayment *payment = [SKPayment paymentWithProduct:product];
    
// 添加到队列
[[SKPaymentQueue defaultQueue] addPayment:payment];

6.监听

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *trans in transactions) {
        
        switch (trans.transactionState) {
            case SKPaymentTransactionStatePurchased:
                
                [self completeTransaction:trans];
                break;
                
            case SKPaymentTransactionStateFailed:
                
                [self failedTransaction:trans];
                break;
                
            case SKPaymentTransactionStateRestored:
                
                [self restoreTransaction:trans];
                break;
                
                
            default:
                break;
        }
        
    }

}

7.监听的补充

- (void)completeTransaction:(SKPaymentTransaction *)transaction {

    // 处理 id
    [self provideContentForProductIdentifier:transaction.payment.productIdentifier];
    
    // 移除队列
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}


- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
    
    [self provideContentForProductIdentifier:transaction.originalTransaction.payment.productIdentifier];
    
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

- (void)failedTransaction:(SKPaymentTransaction *)transaction {
    
    if (transaction.error.code != SKErrorPaymentCancelled) {
        
    }
    
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}


- (void)provideContentForProductIdentifier:(NSString *)productIdentifier {

    [[NSUserDefaults standardUserDefaults] setBool:YES forKey:productIdentifier];
    [[NSUserDefaults standardUserDefaults] synchronize];
    
    [_purchasedArr addObject:productIdentifier];
    [self.tableView reloadData];
}

The End

运行是没问题的,就是偶尔崩了,别找我,我不会

Tags

Jie Li

🚘 On-road / 📉 US Stock / 💻 Full Stack Engineer / ®️ ENTJ