最近在做内购项目SDK,现将集成过程和集成内购过程中遇到的问题记载下来:
项目中使用到了中间货币(金币)的形式来进行功能使用,模式是使用RMB换成-金币比如:(1RMB = 10金币),所以会集成第三方的支付平台,使用了微信和支付宝的第三方平台过后,发现审核失败,被苹果拒绝,查了一查原因,才是因为苹果对app内的中间币的购买必须走苹果内购(比如冲点券,比如买钻石....)。所以无奈只有使用苹果内购,由于苹果内购的步骤很多,设置的东西太多,所以将这步骤记录下来。
首先设置协议
1.打开itunes Connect,选择协议,税务和银行业务
2.点击Request Contracts(申请合同)下面的,request,点了几个确定和下一步后回到主界面。
Contact info:联系人信息
Bank info:银行信息
Tax info:税务信息
3.首先设置联系人信息,点击Contact info下面的 Set up(设置),点击Add New Contract(增加先的联系方式)
4.填写详情
填写完成后点击save(保存)
5.在下面的所有项目中都选择刚刚填写的信息,选择后点击右下角的done(完成),你可以创建很多联系人,在不同的职务选择不同的联系人。因为我是独立开发,所以我全部填写的我自己。
Senior Management:高管
Financial:财务
Technical:技术支持
Legal:法务
Marketing:市场推广
6.设置银行信息,点击Back info下面的Set up,弹出页面
点击Add Bank Account(添加银行账号)
选择china,后点击next。
填写了CNAPS Code后点击Next
会弹出你的银行卡开户地的信息,确认一下点击next
填写银行卡信息,注意:户主名只能写拼音,比如:李三(Li San)。填完后点击Next
弹出确定信息页面,在下面打钩后点击Save
点击了save后就可以在弹出的页面中选择刚刚填写的卡了。选择后点击Save
7.设置税务信息,点击Tax info下面的Set up,此时联系人信息已经变成可以编辑状态,银行信息为浏览状态。
弹出的界面中,税务分为三种
U.S Tax Forms: 美国税务
Australia Tax Forms:澳大利亚税务
Canada Tax Forms: 加拿大税务
这里我选择的美国税务,就是第一个
弹出第一个选择,点击submit(提交)后,弹出第二个选择
弹出第二个选择,选择后点击submit
弹出第三个页面,填写的资料后点击提交,记得勾选页面上的几个复选框
在提交成功后,状态就变成processing成功
到这里设置的协议就已经设置完了。
创建项目的内购
1.进入到项目的APP信息页面,点击功能,在弹出的页面点击App内购买项目后面的➕。
2.在弹出的新对话框中选择你需要哪一种服务,由于我的项目需要兑换成消耗的金币,所以我选择第一个。选择后点击创建。
3.开始填写内购项目信息。填完后点击右上角的存储(所有信息必须填写完整)。
4.点击存储后,内购列表就会有刚刚创建的内购条目。
你app有几个内购级别就需要依次创建几个条目。
添加测试账号,用来测试支付功能
1.点击图中用户和职能
2.点击沙盒测试员,然后点击左边的➕按钮。
3.设置好信息点击右上角存储就可以,记住里面的邮箱和密码用于支付的时候登陆Apple id
代码集成
打开自己的项目,创建一个测试类。代码都有注释和步骤,直接上代码。
注意:
1.必须用真机测试。
2.测试的时候必须退出自己的apple ID。弹出页面后登陆沙盒的测试apple id。
使用的时候首先要导入 #import
先上代码再细分析
实现观察者监听付钱的代理方法,只要交易发生变化就会走下面的方法
// 监听交易操作与结果
- (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
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虽然过审核了,但是内购商品还没有正式添加到苹果的服务器里,耐心等待一段时间就可以啦~
代码中的_currentProId所填写的是你的购买项目的的ID,这个和第二步创建的内购的productID要一致;本例中是 123。
在监听购买结果后,一定要调用[[SKPaymentQueue defaultQueue] finishTransaction:tran];来允许你从支付队列中移除交易。
沙盒环境测试appStore内购流程的时候,请使用没越狱的设备。
请务必使用真机来测试,一切以真机为准。
项目Bundle identifier需要与您申请AppID时填写的bundleID一致,不然会无法请求到商品信息。
真机测试的时候,一定要退出原来的账号,才能用沙盒测试账号
二次验证,请注意区分宏, 测试用沙盒验证,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