In-App Purchase 实战

版权声明:本文为博主原创文章,未经博主授权不得转载。

最近公司的APP需要新增苹果内购产品,需要重构一下苹果内购功能。顺便写篇文章总结一下遇到的所有内购的坑。

一、嵌入流程介绍

  • 1.1 简介
  • 1.2 如何开放内购功能
  • 1.3 商品的创建
  • 1.4 商品类型
  • 1.5 商品定价
  • 1.6 产品ID

二、编程指南

  • 2.1 常用类说明
  • 2.2 流程代码
  • 2.2.1 获取产品信息列表
  • 2.2.2 购买商品
  • 2.2.3 沙盒测试账号
  • 2.2.4 校验支付凭证
  • 2.3 漏单处理

一、嵌入流程介绍

1.1 简介

IAP(In-App Purchase) 苹果应用内购买。通过在应用程序内部的购买为用户提供额外的内容和服务。属于StoreKit下的功能。

这里就不直译官方文档的内容了,简单总结一下就是购买应用程序内的虚拟产品,例如游戏金币、软件服务、订阅等,凡是苹果App内售卖的虚拟产品都可以走苹果内购买渠道。如果使用苹果内购购买的商品,苹果公司是会分成的(会抽取商品总价的30%)。

本文只介绍在选择使用苹果内购的情况下如果去嵌入内购功能,其他方式本文暂不讨论。

详细原理见官方文档,这里就不过多阐述了。

1.2 如何开放内购功能

一个APP如果想要加入苹果内购,是需要在创建 AppId 的时候勾选 In-App Purchase 功能的(后期也可以修改)。

1.3 商品的创建

需要购买的商品需要在App Store Connect后台注册后方可被程序获取。

流程如下:

使用具有App管理功能的开发者账号登录App Store Connect --> 我的App --> 选择需要添加内购功能的App --> 功能 --> App内购项目 --> 点击右侧“加号“ 即可添加app内购项目了。

首先需要选择商品类型,然后参考名称、产品ID、价格、本地描述 、截图和审核备注等信息。具体每一步都有说明。

1.4 商品类型

对于苹果内购的来说,用户每次购买的都是一个商品,商品和商品之间是有区别的。苹果提供了4中不同种类的商品模式,供开发者选择,也已经足够应付大部分应用的需求了。

  • 消耗型项目

只可使用一次的产品,使用之后即失效,必须再次购买。

示例:钓鱼App中的鱼食

  • 非消耗型项目

只需购买一次,不会过期或随着使用而减少的产品。

示例:游戏 App 的赛道。

  • 自动续期订阅

允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。

示例:每月订阅提供流媒体服务的 App。

  • 非续期订阅

允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。

示例:为期一年的已归档文章目录订阅。

当用户购买一个 自动续期订阅非续期订阅 时,应用程序负责使它在所有用户的设备都可用,并使用户能够恢复过去的购买。

1.5 商品定价

商品价格并不是随意定制的,是以美元为单位计算的,最低0.99美元,对应6.00元人民币(以前最低6.00人民币,现在也推出了1.00人民币的商品)。

有一个价格列表,开发者可根据公司产品定价选择最接近的产品价格。

美国(USD) 中国大陆(CNY)
$0.99 ¥6.00
$1.99 ¥12.00
$3.99 ¥25.00
... ...

只有表中出现的价格可以选择,例如6元 12元,表中没有出现的价格是无法选择的(因为是以美元为单位的)。

1.6 产品ID

每个产品都有一个产品ID号,用来在程序中对某个商品进行定位。一般建议使用 AppBundle Identifier 后面再加一个产品名称。

例如建议 Bundle Identifier 为 com.XXX.XXX ,则产品ID为 com.XXX.XXX.productName(商品描述),
productName 可以是任意商品描述或缩写


恭喜你 到这里就成功创建了一个可供使用的内购商品了。通过创建的 产品ID 即可在程序中获取指定商品信息,和购买该商品了。

二、编程指南

2.1 常用类说明

获取产品信息
  • SKProduct

用来描述一个在 App Store Connect 里注册的内购商品的信息。

  • SKProductsRequest

可以检索 App Store Connect 上注册的产品列表的对象。

  • SKProductsResponse

对一个产品信息列表请求的响应对象。

  • SKProductSubscriptionPeriod

包含订阅持续时间的对象。

  • SKProductDiscount

订阅商品的折扣信息。

请求付款
  • SKPayment

对应用程序内购买附加功能商品的一次支付行为的描述对象。

  • SKMutablePayment

对应用程序内购买附加功能商品的一次支付行为的描述的可变对象。

  • SKPaymentQueue

一个处理对App Store购买行对象的队列(购买队列)

2.2 流程代码

工程内加入 StoreKit 库,同时在需要使用支付的文件内加入头文件

#import 

2.2.1 获取产品信息列表

    //商品ID数组
    NSArray *productIdArray = @[IAP_PRODUCT_ID_1,
                                IAP_PRODUCT_ID_2,
                                IAP_PRODUCT_ID_3,
                                IAP_PRODUCT_ID_4,
                                IAP_PRODUCT_ID_5];
    NSSet *productIdSet = [NSSet setWithArray:productIdArray];
    
    //创建商品信息获取请求
    SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdSet];
    
    //设置代理 
    productsRequest.delegate = self;    
    
    //开始请求
    [productsRequest start];

此处的 IAP_PRODUCT_ID_1 就是上文中提到的 产品ID

实现代理

@protocol SKProductsRequestDelegate 

@required
// Sent immediately before -requestDidFinish:
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response NS_AVAILABLE(10_7, 3_0);

@end

通过协议方法返回的 SKProductsResponse 获取 SKProduct 的数组

//SKProductsResponse 的属性

// Array of SKProduct instances.
@property(nonatomic, readonly) NSArray *products NS_AVAILABLE(10_7, 3_0);

SKProduct 包含了可购买商品的详细信息(包含商品的本地描述,价格,商品ID等详细信息)
,这些信息可用于展示给用户,供用户选择购买。

2.2.2 购买商品,首先监听支付状态

    //self 实现协议
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];

实现协议 SKPaymentTransactionObserver 的必要方法

@protocol SKPaymentTransactionObserver 
@required
// Sent when the transaction array has changed (additions or state changes).  Client should check state of transactions and finish as appropriate.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions NS_AVAILABLE(10_7, 3_0);

@optional
... 
...

@end

根据用户选择的商品 SKProduct 创建支付对象 SKPayment

    //创建支付对象 product为用户选择的商品的SKProduct对象
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
    
    //设置购买数量
    payment.quantity = quantity;
    
    //可记录一个字符串,用于帮助苹果检测不规则支付活动
    //payment.applicationUsername = [self encryptionString:userName];
    
    //将支付加入支付队列
    [[SKPaymentQueue defaultQueue]addPayment:payment];    

注意: 每个商品的单次购买数量不能超过10个,所以请结合公司业务设计每个商品,(以前就被购买个数不足的问题坑过,由于文档说明的地方很隐蔽,所以第一次都没有发现)

//SKPayment
@property(nonatomic, readonly) NSInteger quantity;
The default value is 1, the minimum value is 1, and the maximum value is 10.

当将支付加入支付队列后,会出现提示用户输入 Apple ID 密码以完成支付的弹窗,待用户进一步操作
SKPaymentTransactionObserver 协议的回调会被触发

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            // Call the appropriate custom method for the transaction state.
            //支付中
            case SKPaymentTransactionStatePurchasing:
                break;
            //支付失败
            case SKPaymentTransactionStateFailed:
                break;
            //支付成功
            case SKPaymentTransactionStatePurchased:
            //结束本次交易
            //[[SKPaymentQueue defaultQueue]finishTransaction:transaction]; //支付完成后调用(建议验证支付凭证有效后再调用)
                break;  
            //支付被恢复
            case SKPaymentTransactionStateRestored:
                break;
            //支付延迟(这种情况我还没有碰到过)
            case SKPaymentTransactionStateDeferred:
                break;
            default:
                break;
        }
    }
}

支付完成后需要调用 finishTransaction:

[[SKPaymentQueue defaultQueue]finishTransaction:transaction];

如果不调用,则每次启动app的时候都会有支付完成的回调上来。

这里建议验证交易凭证成功后再调用支付完成方法


虽然支付过程到这里就结束了,但是为了安全起见建议将支付凭证发送给服务器校验,获取校验结果后再结束支付并下发商品。下文中将介绍如何校验支付凭证。

2.2.3 沙盒测试账号

支付的代码加好之后,要开始测试一下了,但总不可能用真钱去购买吧。 别担心, 苹果提供了一种沙盒测试账号,可以随意购买商品而不用真正消费。
沙盒账号的创建方式:

登录 App Store Connect --> 用户和访问 --> 测试员

这里就可以创建用于测试的沙盒账号了。
沙盒账号使用起来和一个正常的 AppleID 账号几乎没有区别。
可以直接登录在设备上,也可以在苹果内购支付的时候再填写账号密码。

2.2.4 校验支付凭证

获取支付凭证

//获取支付凭证
    NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
    NSString *receiptStr = [receiptData base64EncodedStringWithOptions:0];

此处拿到的receiptStr可以传给公司自己的服务器进行校验。

也可以自己做校验
将凭证做成json格式 key = @"receipt-data"
然后转成NSData

    NSString *key = @"receipt-data";
    NSDictionary *dic = [[NSDictionary alloc]initWithObjects:@[receiptString] forKeys:@[key]];
    NSError *error;
    NSData *postData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];

将拿到的 postData 通过 POST 请求传给苹果服务器进行验证,这样即可获取凭证的校验结果。

下面是正式校验地址 和 沙盒测试校验地址

#if 1   //正式校验地址
    static NSString *productionURL = @"https://buy.itunes.apple.com/verifyReceipt";
#else   //沙盒测试校验地址
    static NSString *productionURL = @"https://sandbox.itunes.apple.com/verifyReceipt";
#endif

由于服务器不知道凭证是否由沙盒账号购买生成的,可先进行正式地址校验,如果校验失败再进行沙盒地址校验。

凭证返回结果内也会有是否是沙盒的提示信息。

2.3 漏单处理

在App实际上线后,我们发现经常会有漏单,总结后大致分为两种原因

  • 1.使用虚假交易凭证验证

建议使用https请求,并且加入带有时间戳的验证字符串,交易凭证本身也要一同加密并拼接在验证字符串后面。虽然会使上传数据明显增加,但能提高安全性 所以还是很有必要的。

  • 2.未收到来自客户端交易凭证验证请求。

猜测可能由于网络等问题造成,或者app本身收到的支付完成回调比较滞后。
如果是网络原因造成的交易凭证验证请求失败,可在验证请求发送失败时将交易凭证存在本地,待稍后或者下次App启动再行验证。

以上就是大致的支付流程,希望对刚入坑的同学有些帮助。:)

作者:张文宇 向日葵远程控制软件/花生壳/蒲公英路由器 iOS高级软件工程师

你可能感兴趣的:(In-App Purchase 实战)