In-App Purchase 实战

2019-04-30  本文已影响0人  DanceFlame

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

最近公司的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 常用类说明

获取产品信息

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

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

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

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

订阅商品的折扣信息。

请求付款

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

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

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

2.2 流程代码

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

#import <StoreKit/StoreKit.h>

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];
    
    //设置代理 <SKProductsRequestDelegate>
    productsRequest.delegate = self;    
    
    //开始请求
    [productsRequest start];

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

实现代理

@protocol SKProductsRequestDelegate <SKRequestDelegate>

@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<SKProduct *> *products NS_AVAILABLE(10_7, 3_0);

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

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

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

实现协议 SKPaymentTransactionObserver 的必要方法

@protocol SKPaymentTransactionObserver <NSObject>
@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<SKPaymentTransaction *> *)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<SKPaymentTransaction *> *)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实际上线后,我们发现经常会有漏单,总结后大致分为两种原因

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

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

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

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

上一篇 下一篇

猜你喜欢

热点阅读