第一次接触内购,网上搜了很多资料,大多数都是讲的基本的用法,不能用来处理一些特殊情况,但是我们需要解决的往往都是一些特殊情况。经过一段时间的努力和时间,终于大体了解了内购的基本流程,这里整理一下内购的代码,以便以后使用或他人借鉴。
准备部分
没有开发者帐号以及证书没有配置好的请自行百度配置。
创建一个 AppID 和 BoundleID
进入苹果的Apple Developer 创建一个 AppID 和 BoundleID, 进入开发者中心后,点击 Account
选项 ,在点击 Certificates, IDs & Profiles
,在点击 App IDs
如图
最后点击页面上的加号创建AppID(AppID 和 BoundleID 的命名按照苹果的要求来就好了,最后选择需要的服务,要勾选上内购)。
新建一个 App 应用
进入苹果的itunes Connect,登录后点击 我的 App
然后创建一个新的应用,如图
按照要求填写上面申请好的AppID 和 BoundleID,填写好以后,点击 创建
就有一个新应用了。
添加付费项目
点击添加的应用,进入下一个页面后点击 功能
,然后添加 App 内购项目如图
一般都是消耗型项目吧,这里我选择的是消耗型项目。
然后是填写具体的内购信息,不知道该填什么的可以随便填一些信息,看看效果,如图
测试的时候照片可以不上串,产品Id 是唯一的(比较重要)。
添加测试账户
总不能用正式帐号去测试吧,那样要花好多钱的,这里我们用测试帐号去测试内购流程。
进入苹果的itunes Connect,登录后点击 用户和职能
然后在点击 沙箱技术测试员
,最后添加测试帐号,如图
这里的邮箱可以随便填写,不用去验证,填完后准备工作就完成了。
最后,注意一下测试内购时,把 App Store 里的正式帐号注销掉,并且不要用测试帐号去登录 App Store。
代码部分
在正式编码前,我们先来了解一下内购的大体流程,请看图
这是从网上找的一张图,很清楚的描述了内购的流程,下面的代码我会按照这张图的流程来进行。
在工程里导入storekit.framework,在内购类导入#import \
客户端代码
.h文件
//
// StoreAPI.h
//
//内购支付类
#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
// 加入 SKPaymentTransactionObserver 和 SKProductsRequestDelegate 监听机制
@interface StoreAPI : NSObject<SKPaymentTransactionObserver,SKProductsRequestDelegate>
+ (instancetype)shareInstance;
/*
请求商品,购买页面点击购买时调用的方法
@param:type 商品id
*/
- (void)requestProductData:(NSString *)type;
//恢复购买
- (void)restoreProduct;
@end
.m文件
//
// StoreAPI.m
//
#import "StoreAPI.h"
//购买验证URL
#define kAppStoreVerifyURL @"https://buy.itunes.apple.com/verifyReceipt" //实际购买验证URL
#define kSandboxVerifyURL @"https://sandbox.itunes.apple.com/verifyReceipt" //开发阶段沙盒验证URL
static StoreAPI *storeAPI = nil;
@implementation StoreAPI
{
NSString *productId;
}
//单例
+ (instancetype)shareInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
storeAPI = [[TDStoreAPI alloc]init];
// 添加观察者,观察内购的状态
[[SKPaymentQueue defaultQueue] addTransactionObserver:storeAPI];
});
return storeAPI;
}
// 请求商品
// 这里的产品Id是服务端传过来的,也可以保存在手机上(不利于修改)
- (void)requestProductData:(NSString *)type
{
productId = type;
//判断设备是否与支持内购
if (![SKPaymentQueue canMakePayments]) {
NSLog(@"不允许程序内付费");
return;
}
// 创建请求
NSArray *product=[[NSArray alloc]initWithObjects:type, nil];
NSSet *nsset=[NSSet setWithArray:product];
SKProductsRequest *request=[[SKProductsRequest alloc]initWithProductIdentifiers:nsset];
request.delegate=self;
[request start];
}
#pragma mark - SKProductsRequestDelegate
//收到产品返回的信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
NSArray *product=response.products;
NSLog(@"ProductID:%@",response.invalidProductIdentifiers);
NSLog(@"产品付费数量:%lu",(unsigned long)[product count]);
if ([product count]==0) {
NSLog(@"没有商品");
return;
}
SKProduct *p=nil;
for (SKProduct *pro in product) {
NSLog(@"%@",[pro description]);
NSLog(@"%@",[pro localizedTitle]);
NSLog(@"%@",[pro localizedDescription]);
NSLog(@"%@",[pro price]);
NSLog(@"%@",[pro productIdentifier]);
if ([pro.productIdentifier isEqualToString:productId]) {
p=pro;
}
}
SKPayment *payment=[SKPayment paymentWithProduct:p];
// 将支付请求添加进支付队列
[[SKPaymentQueue defaultQueue]addPayment:payment];
}
//请求失败(代理方法)
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
// 在这里可以做一些购买失败的弹框之类的处理
NSLog(@"--错误--:%@",error);
}
//请求结束
- (void)requestDidFinish:(SKRequest *)request
{
NSLog(@"反馈信息结束");
}
#pragma mark SKPaymentTransactionObserver
//监听购买结果(嗯,这个方法很重要!!!)
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
//记录了所有的购买记录
for (SKPaymentTransaction *tran in transactions) {
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
{
NSLog(@"交易完成");
[self completeTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
{
NSLog(@"商品添加进列表");
}
break;
case SKPaymentTransactionStateRestored:
{
[self restoreTransaction:tran];
NSLog(@"已经购买过商品");
}
break;
case SKPaymentTransactionStateFailed:
{
NSLog(@"交易失败");
[self failedTransaction:tran];
}
break;
default:
break;
}
}
}
//恢复购买(自定义方法)
- (void)restoreProduct{
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
//交易成功(自定义方法)
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
//从沙盒中获取交易凭证并且拼接成请求体数据
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData;
// 获取凭证的方法在ios7.0以后和以前有点不一样
if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_7_0)
{
receiptData = [NSData dataWithContentsOfURL:receiptUrl];
}else
{
receiptData = transaction.transactionReceipt;
}
//BASE64 编码
NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
// 保存交易记录
[self recordTransaction:transaction];
[self provideContent:transaction.payment.productIdentifier];
NSString * productIdentifier = transaction.payment.productIdentifier;
if ([productIdentifier length] > 0) {
// 向自己的服务器验证购买凭证
[self verifyPurchaseWithPaymentTransaction:receiptString];
}
// Remove the transaction from the payment queue.
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
//交易失败(自定义方法)
- (void)failedTransaction:(SKPaymentTransaction *)transaction {
if(transaction.error.code != SKErrorPaymentCancelled) {
NSLog(@"购买失败");
} else {
NSLog(@"用户取消交易");
}
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
//恢复交易
- (void) restoreTransaction: (SKPaymentTransaction *)transaction
{
NSLog(@" 交易恢复处理");
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
// 恢复购买完成
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
{
NSLog(@"恢复成功");
// if (queue.transactions.count == 0) {
// }else{
// }
}
//记录交易
- (void)recordTransaction:(SKPaymentTransaction *)transaction {
// Optional: Record the transaction on the server side...
NSLog(@"Hello World");
}
//下载
- (void)provideContent:(NSString *)productIdentifier {
NSLog(@"Hello World");
}
//验证购买凭证
-(void)verifyPurchaseWithPaymentTransaction:(NSString *)receiptString{
// 这里的代码是客户端自己向苹果验证的步骤(服务端的和这里相似)
//发送网络POST请求,对购买凭据进行验证---测试阶段对沙盒进行验证
NSURL *url = [NSURL URLWithString:kSandboxVerifyURL];
//国内访问苹果服务器比较慢 timeoutInterval需要长一点
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0f];
request.HTTPMethod = @"POST";
if (receiptString.length < 20) {
return;
}
NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", receiptString];//拼接请求数据
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
request.HTTPBody = bodyData;
//提交验证请求,并获得官方的验证JSON结果
NSError *error;
NSData *result = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&error];
if (error) {
NSLog(@"验证失败");
return;
}
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:result options:NSJSONReadingAllowFragments error:&error];
// 如果status的值为0, 就说明该receipt为有效的。 否则就是无效的。
if ([dic[@"status"] intValue] == 0) {
NSLog(@"验证成功");
}else
{
NSLog(@"验证失败");
}
}
- (void)dealloc
{
[[SKPaymentQueue defaultQueue]removeTransactionObserver:self];
}
@end
服务器端处理
接收ios端发过来的购买凭证。
判断凭证是否已经存在或验证过,然后存储该凭证。
将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
如果需要,修改用户相应的会员权限。
考虑到网络异常情况,服务器的验证应该是一个可恢复的队列,如果网络失败了,应该进行重试。
注意事项
观察者应该在程序启动的时候就创建好,因为程序重新启动后,苹果会继续未完成的订单,这样就不会漏掉之前的交易信息。
苹果不会提供等待的提示弹框,这需要自己去实现。
由于攻击者可以伪造支付成功的凭证,所以需要增加服务器验证。客户端将支付凭证发给服务端,由服务端向苹果的服务器进行二次验证,确保凭证的有效性。
向苹果提交的是正式版的,但是苹果审核App时,会在sandbox 环境购买,所以产生的凭证需要连接苹果的测试服务器验证,但是提交的App连接的是线上的服务器,会造成验证失败的问题。解决方案是:如果返回的code 是 21007,那么再向苹果的测试服务器验证一次,如果code 是 0,表示验证成功。
考虑到断网的情况,客户端发送的购买凭证应该进行本地的持久化存储,如果程序退出,崩溃或断网,可以重新向服务器发送凭证,当服务器返回验证成功后可以删除相应的凭证。
由于苹果那边充值成功后,服务器还需要进行验证(连接的是国外的服务器,响应比较慢),所以充值的金额不能及时到帐,需要等几秒。(或许是我没有发现更好地解决方法)
如果向服务器发送购买凭证过程中断网或因其他原因失败,导致服务器没有收到购买凭证,或许需要监听网络状况,联网后再次发送也可以等程序下次启动时再次发送,看具体要求吧。