努力创造价值

iOS In-App Purchase

1.苹果iTunes Connect内购产品信息录入。

1)创建app内购买项目(Create New),选择类型:

1.消耗型项目

对于消耗型 App 内购买项目,用户每次下载时都必须进行购买。一次性服务通常属于消耗型项目,例如钓鱼App 中的鱼饵。

2.非消耗型项目

对于非消耗型 App 内购买项目,用户仅需要购买一次。不会过期或随使用而减少的服务通常为非消耗型项目,例如游戏App 的新跑道。

3.自动续订订阅

通过自动续订订阅,用户可以购买指定时间期限内的更新和动态内容。除非用户取消选择,否则订阅(例如杂志订阅等)会自动续订。

4.免费订阅

通过免费订阅,开发者可以将免费订阅内容放入“报刊杂志”。用户注册免费订阅后,该订阅内容将会出现在与该用户Apple ID 关联的所有设备上。请注意,免费订阅不会过期,并且仅在支持报刊杂志功能的 App 中提供。

5.非续订订阅

非续订订阅允许有时限性的营销服务。对于 App 内购买项目中的限时访问内容,就需使用非续订订阅。例如,导航App 中语音导航功能的一周订阅,或者年度订阅已存档的视频或音频的在线目录。

一定要根据自己应用的情况选择正确,不然会被App Store审核团队拒绝。应用内的虚拟币要采用消耗型的,有固定时限的会员选择自动续订订阅。也可以只选择虚拟币充值自己后台购买的情况解决会员问题。

2)生成共享密钥

共享密钥是在您联系我们的服务器获取 App 内购买项目收据时使用的唯一代码。没有共享密钥,您将无法在沙箱技术模式下测试自动续订 App 内购买项目。另外,共享密钥不能在 App Store 使用。
注:无论与哪个 App 相关联,您的所有自动续订订阅都将使用同一共享密钥。
此共享密钥用于后台服务器验证用户购买项目的凭证,生成新密要服务器也立即改变验证密钥。共享密钥在验证自动续订订阅类型项目的时候必须需要。

3)内购项目的状态

A) Pending Developer Approval – Your in apppurchase has been created but has not been tested in a sandbox environment andapproved by you.

B) Approved By Developer – Your in apppurchase has been tested in a sandbox environment and has been approved by you.

C) Waiting For Review – You have submittedyour in app purchase to be reviewed by Apple.

D) In Review – Your in app purchase iscurrently being reviewed by Apple no edits can be made.

E) Developer Action Required – In app purchasedetail changes that you submitted have been rejected. You are required to takeaction to edit the detail information or cancel the request to change thedetail information before this in app purchase can be reviewed again.(内购项目详情界面会提示那个地方出现了问题,稍微修改一下再次提交就行了)

F) Ready for Sale – Apple has approved your inapp purchase to go live on the App Store with its associated application. Thein app purchase must be cleared for sale in iTunes Connect to be Ready forSale.

G) Rejected – Apple has rejected your in apppurchase during the review process. If you have not already been contacted byApple with more information about your rejection, you may inquire through theContact Us section of iTunes Connect. A rejected in app purchase cannot bereinstated. You must create a new in app purchase if you still wish for it tobe sold.

H) Developer Removed from Sale – You havemarked your in app purchase as not cleared for sale in iTunes Connect.

2.app端程序代码编写(代码仅供参考)

#pragma mark - 支付以单利的形式展开
+(PurchasesObject*)SharePurchases
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    if (_purchase == nil) {

        _purchase = [[super alloc]init];
        [[SKPaymentQueue defaultQueue]addTransactionObserver:_purchase];
    }
});
return _purchase;
}
+(id)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    if (_purchase == nil) {

        _purchase = [[super allocWithZone:zone]init];
    }
});
return _purchase;
}
+(id)alloc
{
return _purchase;

}
#pragma mark -支付钻石会员
_alter = [[UIAlertView alloc]initWithTitle:@"游客模式购买仅限当前设备使用所购买的权限,推荐您登录购买" message:nil delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"登陆购买(推荐)",@"游客模式购买" ,nil];
//游客购买很重要,会被AppStore审核团队拒绝。
#pragma mark -开始支付,根据录入内购项目的产品id去AppStore请求产品信息。
if ([SKPaymentQueue canMakePayments]) {
    NSSet * set = [NSSet setWithArray:@[ProductID]];
    SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
    request.delegate = self;
    [request start];

}
else
{
    NSLog(@"无权限购买");
}

#pragma mark -SKProductsRequestDelegate 获取appstroe产品信息
- (void)productsRequest:(SKProductsRequest *)requestdidReceiveResponse:(SKProductsResponse *)response {
self.mySelfView.userInteractionEnabled = NO;
[AFTools showHUD:@"获取产品信息" atView:self.mySelfView];
NSLog(@"-----------收到产品反馈信息--------------");
NSArray *myProduct = response.products;
if (myProduct.count == 0) {
    [AFTools alertWithTitle:@"购买失败" message:@"无法获取产品信息"];
    NSLog(@"无法获取产品信息,购买失败。");
    return;
}
NSLog(@"产品products==%@",myProduct);
NSLog(@"产品id==%@",response.invalidProductIdentifiers);
NSLog(@"产品数量==========%lu",(unsigned long)myProduct.count);
for(SKProduct *product in myProduct){
    //        SKMutablePayment
     NSLog(@"SKProduct描述信息%@", [product description]);
    NSLog(@"产品标题 %@" , product.localizedTitle);
    NSLog(@"产品描述信息: %@" , product.localizedDescription);
    NSLog(@"价格: %@" , product.price);
    NSLog(@"Product id: %@" , product.productIdentifier);
    SKMutablePayment *MpayMent = [SKMutablePayment paymentWithProduct:product];
    NSLog(@"===%@",MpayMent.requestData);
    if ([[SKPaymentQueue defaultQueue]respondsToSelector:@selector(addPayment:)]){
              [[SKPaymentQueue defaultQueue] addPayment:MpayMent];
    }
    else
    {
    }
  }
}
  #pragmamark-SKPaymentTransactionObserver支付结果
- (void)paymentQueue:(SKPaymentQueue *)queueupdatedTransactions:(NSArray *)transactions

{

for (SKPaymentTransaction *transaction in transactions)
{
    switch (transaction.transactionState)
    {
        case SKPaymentTransactionStatePurchased://交易完成
            NSLog(@"交易完成transactionIdentifier= %@", transaction.transactionIdentifier);
            [self completeTransaction:transaction];
            break;
        case SKPaymentTransactionStateFailed://交易失败
            [self failedTransaction:transaction];
            NSLog(@"交易失败");
            break;
        case SKPaymentTransactionStateRestored://已经购买过该商品
            [self restoreTransaction:transaction];
            NSLog(@"已买过商品");
            break;
        case SKPaymentTransactionStatePurchasing://商品添加进列表
            NSLog(@"商品添加进列表");
            break;
        default:
            break;
    }
}

}
- (void)paymentQueue:(SKPaymentQueue *)queueremovedTransactions:(NSArray *)transactions
{
   NSLog(@"---------------移除-------------");
    }
- (void)paymentQueue:(SKPaymentQueue *)queuerestoreCompletedTransactionsFailedWithError:(NSError *)error
{
NSLog(@"---------------重复支付失败-------------");
}
- (void)completeTransaction:(SKPaymentTransaction *)transaction {
NSLog(@"-------------------支付完成--------------------");
[self commitSeversSucceeWithTransaction:transaction];

}

-(void)restoreTransaction: (SKPaymentTransaction *)transaction
{
NSLog(@"----------------重复支付-----------------");
[self commitSeversSucceeWithTransaction:transaction];
}
- (void)commitSeversSucceeWithTransaction:(SKPaymentTransaction *)transaction
{
NSString * productIdentifier = transaction.payment.productIdentifier;
NSString *transactionReceiptString= nil;
//系统IOS7.0以上获取支付验证凭证的方式应该改变,切验证返回的数据结构也不一样了。
if(IOSSystemVersion>=7.0)
{
    NSURLRequest*appstoreRequest = [NSURLRequest requestWithURL:[[NSBundle mainBundle]appStoreReceiptURL]];
    NSError *error = nil;
    NSData * receiptData = [NSURLConnection sendSynchronousRequest:appstoreRequestreturningResponse:nil error:&error];
    transactionReceiptString = [receiptDatabase64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
}
else
{
    NSData * receiptData = transaction.transactionReceipt;
    transactionReceiptString = [receiptDatabase64EncodedString];
}
    // 向自己的服务器验证购买凭证
   }
- (void)failedTransaction:(SKPaymentTransaction *)transaction {

[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

最好在客户端上键一个数据库,跟踪订单的状态,防止用户订单在某个环节出现问题时无法寻找到订单进行二次处理。
去AppStore请求数据时有时候会出现错误,你可以iTunes connect里的connect us去给他们写邮件反馈问题。但是大部分时间你等等就能解决了,对就是什么也不做等着。也许那一天他就好了。

3.后台服务器验证

IOS 内支付有两种模式:

1) 内置模式

2) 服务器模式

内置模式的流程可以简单的总结为以下几步:

1) app从app store 获取产品信息

2) 用户选择需要购买的产品

3) app发送支付请求到app store

4) app store 处理支付请求,并返回transaction信息

5) app将购买的内容展示给用户

服务器模式的主要流程如下所示:

1) app从服务器获取产品标识列表

2) app从app store 获取产品信息

3) 用户选择需要购买的产品

4) app 发送 支付请求到app store

5) app store 处理支付请求,返回transaction信息

6) app 将transaction receipt 发送到服务器

7) 服务器收到收据后发送到app stroe验证收据的有效性

8) app store 返回收据的验证结果

9) 根据app store 返回的结果决定用户是否购买成功

上述两种模式的不同之处主要在于:交易的收据验证,内建模式没有专门去验证交易收据,而服务器模式会使用独立的服务器去验证交易收据。内建模式简单快捷,但容易被破解。服务器模式流程相对复杂,但相对安全。

开发之初,苹果方就很负责的告知:我们的服务器不稳定。真正开发之后,发现苹果方果然是很负责的,不仅是不稳定,而且足够慢。app store server验证一个收据需要3-6s时间。

1.用户能否忍受3-6s的等待时间

2.如果app store server 宕机,如何确保成功付费的用户能够得到正常服务。

对于第一个问题,我们有理由相信用户完全无法忍受,所以采用异步验证的方式,服务器收到客户端的请求后,就将请求放到MCQ中去处理。

对于第二个问题,由于苹果人员很负责人的告知:我们的服务器不稳定,所以不排除收据验证超时的情况。对于验证超时的收据,保存到数据库中并标记为验证超时,定时任务每隔一定的时间去app store验证,确保能够获取收据的验证结果。

在开发过程中,需要测试应用是否能够正常的进行支付,但是又不能进行实际的支付,因此需要使用苹果提供的sandbox Store测试。Store Kit不能在iOS模拟器中使用,测试Store必须在真机上进行。

在sandbox中验证receipt

https://sandbox.itunes.apple.com/verifyReceipt

在生产环境中验证receipt

https://buy.itunes.apple.com/verifyReceipt

在实际开发过程中,服务器端通过issandbox字段标识客户端传递的收据是沙盒环境中的收据还是生产环境中的收据。在提交苹果审核前,沙盒测试均无问题。提交苹果审核后,被告知购买失败,审核未通过。通过查询日志发现,客户端发送的交易收据为沙盒收据,但是issandbox字段却标识为生产环境。

结论:苹果审核app时,仍然在沙盒环境下测试。但是客户端同事在app提交苹果审核时,将issandbox字段写死,设置为生产环境。这样就导致沙盒收据发送到https://buy.itunes.apple.com/verifyReceipt去验证。

那么如何自动的识别收据是否是sandbox receipt呢?

识别沙盒环境下收据的方法有两种:

1.根据收据字段 “environment” = “sandbox”。

2.根据收据验证接口返回的状态码

如果status=21007,则表示当前的收据为沙盒环境下收据,需要调用https://sandbox.itunes.apple.com/verifyReceipt进行验证。

苹果反馈的状态码;

21000App Store无法读取你提供的JSON数据

21002 收据数据不符合格式

21003 收据无法被验证

21004 你提供的共享密钥和账户的共享密钥不一致

21005 收据服务器当前不可用

21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中

21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证

21008 收据信息是产品环境中使用,但却被发送到测试环境中验证

先生产验证后测试验证,可以避免来回切换接口的麻烦。测试验证只要用你自己申请的测试appid的时候才会用到,用户不会拥有测试appid,所以不会走到测试验证这一步。即使生产验证出错,应该也不回返回21007状态吗。测试验证通过的用户名,和充值金额最好用数据库记录下来,方便公司资金核对。