2016年最新版iOS App内购买详细指南

##摘要:

  • 本文讲述了如何创建App内购买项目?
  • 如何添加沙盒App内购买测试帐号?
  • 如何封装一个内购买的管理类?
  • 如何在完成付款之后,保证有效的提供增值服务?
    ###如何创建内购买项目?
    创建内购买之前,请确保已完成“协议、税务和银行业务”。打开iTunes Connect,进入app,选择”功能”-“App内购买项目”,点击”+”创建一个内购买项目


选择所属类型:

按照提示填写名称,定价,产品id,以及描述信息,内购商品列表截图。

需要注意产品id的规范,它必须由:"bundle identifier"+编号组成 如xxx01,其中xxx表示bundle identifier
每个内购项目需要提供一个内购买商品列表截图,如:

创建完内购项目之后,我们需要一个沙盒测试帐号。

###如何创建沙盒测试帐号?
进入“用户和职能”,选择“沙箱测试技术员”,点击“+”添加一个测试帐号:

按照提示,输入信息,创建测试帐号,用于内购买测试。接下来是代码部分。

###如何封装一个内购买管理类?
思路

  • 这个管理类需要有一个支付的调起接口,至少要一个产品id参数;
  • 若是非消耗型商品,需要提供一个恢复已购买商品的接口;
  • 要保证内购付完款之后,要告诉我们自己的服务器,我买了什么东西,而要做到这一点,我们又要考虑:
    搞一个管理类:@interface FCInAppPurchaseManager : NSObject
    搞个单例类方法:
    1
    2
    3
    4
    5
    6
    /**
    * 单例类
    *
    * @return 单例对象
    */
    +(instancetype)sharedManager;

搞一个购买商品的接口:

1
2
3
4
5
6
7
8
/**
* 购买商品
*
* @param identifier iTunes Connect商品ID
* @param tradeId 交易id(自己服务器创建订单的交易id)
* @param key 交易标识(自己服务器生成的key,防治杜撰交易)
*/
-(void)purchaseProductWithIdentifier:(NSString *)identifier tradeId:(NSString *)tradeId key:(NSString *)key;

在商品列表界面购买商品时,调用上面这个接口,从自己服务器创建交易,获取交易id和key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
//创建交易
FCInnserPurchaseProductModel *model=_dataArray[indexPath.row];
FCFamousUser *currentUser=[[FCAccountSettings getCurrentAccountSetting] getCurrentUser];
NSString *token=[FCMainSettings getToken];
NSDictionary *params=@{kAfterLoginToken:token,
@"userid":currentUser.userId,
@"no":@(currentUser.no),
@"productid":model.productId,
@"price":model.price,
@"coin":model.coin,
@"quantity":@1,
@"signature":[[NSUUID UUID] UUIDString],
@"channel":@"ios",
@"device_type":@"ios"};
HUDShowLoading
[[FCWebServiceController sharedWebServiceController] sendRequestWithParameters:params action:@"trade/create" success:^(id json) {

if(![json isKindOfClass:[NSDictionary class]]){
HUDError(kServerInnerError)
return;
}
HUDDismiss
NSString *tradeId=[[json objectForKey:@"trade"] objectForKey:@"_id"];
NSString *key=[[json objectForKey:@"extras"] objectForKey:@"key"];
//调起购买管理类的购买
[[FCInAppPurchaseManager sharedManager] purchaseProductWithIdentifier:model.identifier tradeId:tradeId key:key];

} failure:^(NSError *error) {
HUDError(error.localizedDescription);
}];
}

###内购买管理类的实现 FCInAppPurchaseManager.m
首先要导入StoreKit框架:

1
#import <StoreKit/StoreKit.h>

单例类方法:

1
2
3
4
5
6
7
8
9
static FCInAppPurchaseManager *purchaseManager=nil;
+(instancetype)sharedManager{

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
purchaseManager=[[FCInAppPurchaseManager alloc] init];
});
return purchaseManager;
}

搞一个拓展,加上一些私有属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface FCInAppPurchaseManager()<SKProductsRequestDelegate,SKPaymentTransactionObserver>
/**
* 服务器生成的校验码
*/
@property(nonatomic,copy)NSString *key;
/**
* iTunes Connect上商品的标识符
*/
@property(nonatomic,copy)NSString *identifier;
/**
* 服务器的交易编号
*/
@property(nonatomic,copy)NSString *tradeId;

@end

初始化方法中,添加监听:

1
2
3
4
5
6
7
8
-(instancetype)init{

if(self=[super init]){

[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}

实现购买商品的接口:保存交易id、key到本地的目的是,支付完成后,告诉服务器用户买了啥的这个过程是一个不确定的过程,有可能断网了,手机没电了、程序被终结了,程序bug崩溃了等不确定因素,一次要保留这一次的交易id、key,再次启动app时,我们的服务器没有拿着凭证去向苹果服务器验证交易信息苹果会自动调用监听购买结果的方法paymentQueue : updatedTransactions :,在这个方法中,完成上一次应该告诉我们服务器用户买了啥的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 购买商品
*
* @param identifier 产品在iTunes connect中的标识符(HZSX.FriendCircle01 HZSX.FriendCircle02 HZSX.FriendCircle03 分别对应60圈币、180圈币 300圈币)
* @param tradeId 交易编号(服务端生成的)
* @param key 验证用的key(服务端生成)
*/
-(void)purchaseProductWithIdentifier:(NSString *)identifier tradeId:(NSString *)tradeId key:(NSString *)key{

if(![SKPaymentQueue canMakePayments]){

HUDError(@"您的手机没有打开程序内付费购买");
return;
}
if(![identifier hasPrefix:@"HZSX.FriendCircle"]){

HUDError(@"商品类型错误");
return;
}
if(kFCEmptyCheck(tradeId)){
HUDError(@"交易编号不能为空");
return;
}
if(kFCEmptyCheck(key)){
HUDError(@"缺少必要参数");
return;
}
_identifier=identifier;
_tradeId=tradeId;
_key=key;
//存到本地
NSString *token=[FCMainSettings getToken];
NSString *tradeId_key=[NSString stringWithFormat:@"%@_tradeId",token];
NSString *tradeKey_key=[NSString stringWithFormat:@"%@_key",token];
[[NSUserDefaults standardUserDefaults] setObject:tradeId forKey:tradeId_key];
[[NSUserDefaults standardUserDefaults] setObject:key forKey:tradeKey_key];
[[NSUserDefaults standardUserDefaults] synchronize];

NSSet *set=[NSSet setWithObject:identifier];
//发起购买商品的请求
SKProductsRequest *request=[[SKProductsRequest alloc] initWithProductIdentifiers:set];
request.delegate=self;
[request start];
}

实现StoreKit的代理方法:
#pragma mark - SKProductsRequestDelegate

1
2
3
4
5
6
7
8
9
10
11
//收到的产品信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{

if(response.products.count==0){
HUDError(@"商品信息错误");
return;
}
SKProduct *product=response.products.lastObject;
SKPayment *payment=[SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}

#pragma mark - SKRequestDelegate

1
2
3
4
5
6
7
8
-(void)request:(SKRequest *)request didFailWithError:(NSError *)error{

HUDError(error.localizedDescription);
}
-(void)requestDidFinish:(SKRequest *)request{

HUDDismiss;
}

监听购买结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#pragma mark - 监听购买结果(若购买时,退出了app,苹果会在app启动时,自动调用这个方法)
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{

if(transactions.count==0!transactions){

HUDError(@"获取不到购买结果")
return;
}
SKPaymentTransaction *transaction=transactions.lastObject;
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased:{//交易完成

[self recordTransaction:transaction];
break;
}
case SKPaymentTransactionStateFailed:{//交易失败

[self failedTransaction:transaction];
break;
}
case SKPaymentTransactionStateRestored://已经购买过该商品
[self restoreTransactions];
break;
case SKPaymentTransactionStatePurchasing: //商品添加进列表
break;
default:
break;
}

那么问题来了?

###为什么不把交易完成后,告诉自己服务器,买了什么,这部分业务抽离出来呢?
考虑到从调起支付到支付结果(成功、失败等)需要几秒甚至十几秒,在这段时间内,用户可能离开了当前界面,
而苹果购买的结果回调时,用户不在当前界面就告诉不了我们的服务器,用户买了啥啥啥。
因此我把购买完成,告诉服务器的这部分逻辑也写进了这个管理类里面。这样,用户无论是否离开当前页面,只要支付成功了,就能调用告诉服务器用户买了啥的逻辑。

告诉服务器用户买了啥:同时将交易id、交易key、凭证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#pragma mark - 请求服务端,记录交易 告诉服务器我买了什么
-(void)recordTransaction:(SKPaymentTransaction *)transaction{

//结束交易
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];

NSString *token=[FCMainSettings getToken];
NSString *tradeId_key=[NSString stringWithFormat:@"%@_tradeId",token];
NSString *tradeKey_key=[NSString stringWithFormat:@"%@_key",token];
if(kFCEmptyCheck(_tradeId)){
_tradeId=[[NSUserDefaults standardUserDefaults] objectForKey:tradeId_key];
}
if(kFCEmptyCheck(_key)){
_key=[[NSUserDefaults standardUserDefaults] objectForKey:tradeKey_key];
}
//获取苹果返回的支付凭证
NSURL *receiptURL=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData=[NSData dataWithContentsOfURL:receiptURL];
if(!receiptData){
return;
}
NSString *base64_receipt=[[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]
stringByReplacingOccurrencesOfString:@" " withString:@""];
if(kFCEmptyCheck(base64_receipt)){
return;
}
//sign=MD5(tradeid=tradeid&salt=mrpyp-2016&key=key)
NSString *components=[[NSString stringWithFormat:@"tradeid=%@&salt=mrpyp-2016&key=%@",_tradeId,_key]
stringByReplacingOccurrencesOfString:@" " withString:@""];
//NSString *rsa_components=[[RSA encryptString:components publicKey:kInAppPurchaseRSAPublicKey]
// stringByReplacingOccurrencesOfString:@" " withString:@""];
NSString *md5_componets=[components MD5Hash];
if(kFCEmptyCheck(token)kFCEmptyCheck(_tradeId)kFCEmptyCheck(md5_componets)){
return;
}
NSDictionary *params=@{kAfterLoginToken:token,
@"sign":md5_componets,
@"tradeid":_tradeId,
@"receipt":base64_receipt,
@"key":_key};
[self tellServerBuySuccessWithdTradeInfo:params];
}

告诉服务器,我们买了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 告诉服务器购买成功了
*
* @param tradeInfo 交易信息
* @param destinationPath 存放交易信息的文件路径
*/
-(void)tellServerBuySuccessWithdTradeInfo:(NSDictionary *)tradeInfo/* destionationPath:(NSString *)destinationPath*/{

[[FCWebServiceController sharedWebServiceController] sendRequestWithParameters:tradeInfo action:@"pay/ios_receipt_verify" success:^(id json) {

if(![json isKindOfClass:[NSDictionary class]]){
HUDError(kServerInnerError)
return;
}
BOOL result=[[json objectForKey:@"result"] boolValue];
if(!result){
return;
}
NSLog(@"验证成功");
//显示购买成功的视图
//[self showBuySuccessViewWithCoin:60];

} failure:^(NSError *error) {
NSLog(@"%@",error.localizedDescription);
}];
}

###恢复已购买的商品
若你购买的商品类型是非消耗型的,在用户切换帐号,换手机等情景下,恢复已购买的内容:

1
2
3
4
5
//恢复购买的装备
-(void)restoreTransactions{

[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

并实现恢复购买的代理

1
2
3
4
5
6
7
8
9
10
11
12
//恢复购买的装备成功
-(void)paymentQueueRestoreCompletedTransactionsFinished: (SKPaymentTransaction *)transaction{

if(!transaction)
return;
[self paymentQueue:[SKPaymentQueue defaultQueue] updatedTransactions:@[transaction]];
}
//恢复购买的装备失败
-(void)paymentQueue:(SKPaymentQueue *) paymentQueue restoreCompletedTransactionsFailedWithError:(NSError *)error{

HUDError(error.localizedDescription);
}

最后在dealloc方法中注销监听:

1
2
3
4
-(void)dealloc{

[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];//解除监听
}

###测试内购买
内购买的代码已经写好了,内购项目也新建了,沙盒测试帐号也有了,现在可以动手测试内购买了。

  • 手机打开App Store,选择第一个tab精品推荐,滚动到最底部,点击自己的Apple ID,在弹出的框中,选择注销
  • 打开我们自己的app,进入内购买商品列表页面,点一个商品,发起购买。
  • 在登陆的弹框中选择使用现有的Apple ID,在登陆iTunes connect的弹框中,输入前面注册的沙盒测试帐号
  • 在购买信息的弹框中选择购买
  • 购买完成,前往我的帐户,查看充值记录,验证服务器是否纪录此次交易。
    ###提交审核
    若你的app时第一次加入内购买,则,app内购项目也要一并提交

    至此,内购买集成结束,欢迎关注我的个人博客:CGPointZero