apple 内购流程梳理

第一次接触内购,网上搜了很多资料,大多数都是讲的基本的用法,不能用来处理一些特殊情况,但是我们需要解决的往往都是一些特殊情况。经过一段时间的努力和时间,终于大体了解了内购的基本流程,这里整理一下内购的代码,以便以后使用或他人借鉴。

准备部分

没有开发者帐号以及证书没有配置好的请自行百度配置。

创建一个 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端发过来的购买凭证。
判断凭证是否已经存在或验证过,然后存储该凭证。
将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
如果需要,修改用户相应的会员权限。
考虑到网络异常情况,服务器的验证应该是一个可恢复的队列,如果网络失败了,应该进行重试。

注意事项

  1. 观察者应该在程序启动的时候就创建好,因为程序重新启动后,苹果会继续未完成的订单,这样就不会漏掉之前的交易信息。

  2. 苹果不会提供等待的提示弹框,这需要自己去实现。

  3. 由于攻击者可以伪造支付成功的凭证,所以需要增加服务器验证。客户端将支付凭证发给服务端,由服务端向苹果的服务器进行二次验证,确保凭证的有效性。

  4. 向苹果提交的是正式版的,但是苹果审核App时,会在sandbox 环境购买,所以产生的凭证需要连接苹果的测试服务器验证,但是提交的App连接的是线上的服务器,会造成验证失败的问题。解决方案是:如果返回的code 是 21007,那么再向苹果的测试服务器验证一次,如果code 是 0,表示验证成功。

  5. 考虑到断网的情况,客户端发送的购买凭证应该进行本地的持久化存储,如果程序退出,崩溃或断网,可以重新向服务器发送凭证,当服务器返回验证成功后可以删除相应的凭证。

  6. 由于苹果那边充值成功后,服务器还需要进行验证(连接的是国外的服务器,响应比较慢),所以充值的金额不能及时到帐,需要等几秒。(或许是我没有发现更好地解决方法)

  7. 如果向服务器发送购买凭证过程中断网或因其他原因失败,导致服务器没有收到购买凭证,或许需要监听网络状况,联网后再次发送也可以等程序下次启动时再次发送,看具体要求吧。

参考资料

唐巧大神的博客

李华明Himi 的博客

iOS应用内置付费详尽攻略(全文)

iOS应用程序内购/内付费

iOS内购IAP(In App Purchases)入门

iOS开发内购教程In App Purchase 需要了解的

IAP相关资源收集及开发总结

iOS内置付费开发笔记

iOS证书说明和发布内购流程整理

iOS- 给App添加内购& 验证购买iOS7新特性

应用内购(In-App Purchase)常见问题解答

如果觉得写的不错,那就打赏一下吧!