SDK接入(3)之iOS内支付(In-App Purchase)接入

SDK接入(3)之iOS内支付(In-App Purchase)接入

继整理了Android平台的SDK接入过程。再来分享下iOS平台的内支付(In-App Purchase)接入,作为笔者在游戏开发中实际遇到的,觉得有必要分享下,同时也当作是对工作的总结,就放在该SDK接入系列文章中了。

作为SDK接入系列,同时也是Android平台的SDK接入有:
SDK接入(2)之Android Google Play内支付(in-app Billing)接入
SDK接入(1)之Android Facebook SDK接入

这里提一点,SDK的接入,官方文档肯定最详细,最准确,而且有时效性,接入流程变化,API修改更新,肯定最终都以官方的为准。那么,苹果官方内支付(IAP)接入文档地址为:

In-App Purchase Programming Guide

iOS内支付流程

1.商品种类

在了解苹果IAP内支付之前,有必要先了解下苹果的商品种类。在苹果In-App Purchase Programming Guide文档上写明了,商品种类分为如下几种。接过GooglePlay支付的会发现,这点还是很相似的。
(1)消耗类商品
每次使用都须从新购买。
(2)非消耗类商品
购买一次即可。系统会自己购买状态,且会同步所有用户设备都一直保持可用状态。
(3)自动再生订阅
例如:一本书的章节内容。
(4)非自动再生订阅
例如:一个航班表。
(5)免费订阅
例如:报刊杂志等。

消耗类与非消耗类商品的区别:

订阅类商品的区别:

2.支付流程

对于IAP整个下单到支付过程,下图很形象的说明了该步骤:


(1) 应用向服务器发送请求,获得一份产品列表。
(2) 服务器返回包含商品标识符的列表。
(3) 应用向App Store发送请求,得到商品的信息。
(4) App Store返回商品信息。
(5) 应用把返回的商品信息显示在UI界面上。
(6) 用户选择某个商品。
(7) 应用向App Store发送支付请求。
(8) App Store处理支付请求并返回交易完成信息。
(9) 应用从信息中获得数据,并发送至服务器。
(10) 服务器纪录数据,并进行校验。
(11) 服务器将数据发给App Store来验证该交易的有效性。
(12) App Store对收到的数据进行解析,返回该数据和说明其是否有效的标识。
(13) 服务器读取返回的数据,确定用户购买的内容。
(14) 服务器将购买的内容传递给程序。

3.配置商品

(1)打开iTunes Connect后台
用开发者帐号,登录iTunes Connect,企业级用户需用主开发者帐号。

(2)配置iTunes Connect
在iTunes Connect后台添加应用,并配置App内购买项目,由于我们游戏中的钻石、金币等都属于消耗型商品,因此,直接选的这个。需注意下配置的Bundle id须和项目plist中的Bundle id一致。并添加沙箱测试帐号。

注意:商品Id不可重复,如果删除某个商品,以后这个商品的ID也不可用,即使它已经被删除了;另外类型也不能改,选错了只能重新增加一个商品。

iOS内支付接入

1. 项目工程引入StoreKit.framework
2. 这里推荐一个叫IAPHelper的开源封装,有效的封装支付流程,进一步简化了接入的效率。所以,下面也是基于该项目进行的接入。IAPHelper可自行Github搜索。

(1)InAppRageIAPHelper.m。在init中初始化商品id列表。

#import "InAppRageIAPHelper.h"
#import "InAppRageIAPHelper.h"

@implementation InAppRageIAPHelper
@synthesize orderInfo = _orderInfo;

static InAppRageIAPHelper * _sharedHelper;

+ (InAppRageIAPHelper *) sharedHelper {
    if (_sharedHelper != nil) {
        return _sharedHelper;
    }

    _sharedHelper = [[InAppRageIAPHelper alloc] init];
    return _sharedHelper;
}

- (void)dealloc
{
    [_orderInfo release];
    _orderInfo = nil;
    [super dealloc];
}

- (id)init {
    NSSet *productIdentifiers = [NSSet setWithObjects:
                                 @"com.game.test.10001",
                                 @"com.game.test.10002",
                                 @"com.game.test.10003",
                                 @"com.game.test.10004",
                                 @"com.game.test.10005",
                                 nil];
    
    if ((self = [super initWithProductIdentifiers:productIdentifiers])) {                
        
    }
    
    return self;
}

@end

(2)注册本地通知。一般在应用启动时,添加如下代码:(productsLoaded、productPurchased、productPurchaseFailed分别对应支付过程中三种加载中,支付完成,支付失败状态回调,可根据实际情况作对应的处理)

    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(productsLoaded:) name:kProductsLoadedNotification object:nil];
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(productPurchased:) name:kProductPurchasedNotification object:nil];
    [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(productPurchaseFailed:) name:kProductPurchaseFailedNotification object:nil];
    [[InAppRageIAPHelper sharedHelper]requestProducts];

(3)发起支付。

    if ([SKPaymentQueue canMakePayments]) {
        [[InAppRageIAPHelper sharedHelper]buyProductIdentifier:[self getItemId] game_order:[self getOrderId]];
    } else {
        // 不允许程序内付费购买
        
    }

(4)支付成功的回调。这里将AppStore返回的数据,进行Base64加密,然后再发送给游戏服务器进行校验。同时,本地也会存储返回的票据receipt,防止在发送给服务器过程中请求失败,造成的充值成功但不到账的漏单现象。

-(NSData*)receiptWithTransation:(SKPaymentTransaction*) transcation {
    NSData *receipt = nil;
    if ([[NSBundle mainBundle]respondsToSelector:@selector(appStoreReceiptURL)]) {
        NSURL *receiptUrl = [[NSBundle mainBundle]appStoreReceiptURL];
        receipt = [NSData dataWithContentsOfURL:receiptUrl];
    } else {
        if ([transcation respondsToSelector:@selector(transactionReceipt)]) {
            receipt = [transcation transactionReceipt];
        }
    }
    
    return receipt;
}

-(void)productPurchased:(NSNotification*) notification {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    SKPaymentTransaction *transaction = (SKPaymentTransaction*)notification.object;
    
    NSData *receipt = [self receiptWithTransation:transaction];
    NSString *base64Receipt = [receipt base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    [[InAppRageIAPHelper sharedHelper].orderInfo setObject:[NSString stringWithFormat:@"%@", base64Receipt] forKey:@"originReceipt"];
    
    NSString *m_params = [self makeHttpParams:base64Receipt];
    if (m_params != nil) {
        [self saveReceipt:m_params];
        [self postGameServer:[self getPayUrl] params:m_params];
    }
}

(5)漏单检测。下次,启动时,会进行漏单检测。若存在本地票据receipt,就向游戏服务器发起请求。直到游戏服务器返回成功,再删除本地的票据receipt。

    NSUserDefaults *userDefalut = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary *receiptDict = [NSMutableDictionary dictionaryWithDictionary:[userDefalut objectForKey:@"receipts"]];
    NSEnumerator *enumerator = [receiptDict objectEnumerator];
    for (NSObject *obj in enumerator) {
        [self postGameServer:[self getPayUrl] params:[NSString stringWithFormat:@"orderdata=%@",obj]];
    }

(6)由于,我们的校验是放在服务器进行的,所以,这里就不进行过多的介绍了。简单说下,App Store正式环境校验地址是https://buy.itunes.apple.com/verifyReceipt ,测试环境校验地址是:https://sandbox.itunes.apple.com/verifyReceipt

iOS支付安全问题

对于某些越狱设备来说,如果校验流程有漏洞的话,使用某些神器,就能绕过Appstore的付费流程,伪造订单,达到免支付体验各种付费功能。
其中列举如下神器:
(1)ap cracker:越狱软件可以截获付费请求,并直接返回付费成功。
(2)iap free: 截获付费请求的同时,还能截获客户端发起的验证请求 ,返回验证成功的数据 ,返回的数据和官方的数据并不是完全一样,可以识别出来是否作弊,但是不保证永久有效。

因此,首先在支付成功之后,要将支付成功返回的票据发送给服务器,在服务器端作验证,根据服务器的验证结果来做相应的处理。其次,本地对应伪造的票据进行过滤。

推荐阅读更多精彩内容