背景
本来功能比较简单,没打算搞内购。上AppStore搜索一下,类似应用都有。然后我发现,我一直做PC端的开发,软件研发十几年居然不知内购业务是什么样,所以就带着试试看的目的,搞了一个。这个我是第一次搞,这篇都是我的理解,所有很可能有错误,主要参考官方文档(https://developer.apple.com/documentation/storekit/in-app_purchase)。
内购种类
按照我的理解,内购一共分为3种:
1、消耗品(Consumables)类型
拿游戏举个例子,买点卡。可以一直买,反复买的。
2、非消耗品(Non-consumables)类型
再拿游戏举个例子,解锁特定人物。买一次就可以啦。
3、订阅(Subscriptions)类型
按照苹果的文档一共有4种内购,订阅分为自动续订内购(Auto-renewable subscriptions)和非自动续订内购(Non-renewing subscriptions)。但我从实际使用角度,我们提供的都是自动续订的内购,只是用户可以选择取消自动续订。
我实际本次使用的是订阅类型。
内购生效范围
这里主要解决的问题是购买之后是当前设备生效?还是当前账号(是否和平台有关,iOS和Mac)生效?如果是当前设备生效,那么用户卸载后再次安装怎么处理的问题?
这个章节我没有找到官方的要求规范(实际上我没有仔细找),因为订阅类型是跟随账号的(在订阅有效期内而距离过期还有较长时间时,是不能再次订购的),所以我理解订阅是跟随账号的。
为了解决用户卸载后安装,同账号在其实设备上安装等问题,我把用户购买信息作为私有数据储存在iCloud中。
监视购买状态
主要就是创建一个实例,这个实例实现SKPaymentTransactionObserver协议。并且在程序启动时,启动这个监听。
SKPaymentTransactionObserver协议实现
示例代码如下:
class StoreObserver: NSObject {
// 实现简单单例模式
static let shared = StoreObserver()
private override init() {
// 继续初始化,使用私有,不允许外部创建
}
}
extension StoreObserver: SKPaymentTransactionObserver {
func paymentQueue(_queue:SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// 购买状态更新回调
}
}
需要注意的是SKPaymentTransactionObserver协议继承自NSObjectProtocol,所以StoreObserver才继承NSObject,这样NSObjectProtocol协议就默认实现啦。
PS: SKPaymentTransactionObserver协议的一些可选方法没有实现
启动监听
示例代码:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_application:UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey:Any]?) ->Bool{
SKPaymentQueue.default().add(StoreObserver.shared)
// 可以在这里发起异步请求订购信息
return true
}
func applicationWillTerminate(_ application: UIApplication) {
SKPaymentQueue.default().remove(StoreObserver.shared)
}
// ...
}
请求支持的订购商品
主要是实现SKProductsRequestDelegate协议
示例代码:
class StoreManager: NSObject {
static let shared = StoreManager()
private override init() {
// ...
}
func fetchProducts() {
let ids: [String]
// 初始化ids,使用在Connect上配置的ID
productRequest = SKProductsRequest(productIdentifiers: Set(ids))
productRequest.delegate = self
productRequest.start()
}
func buy() {
let payment: SKMutablePayment
// 初始化payment
SKPaymentQueue.default().add(payment)
}
}
extension StoreManager: SKProductsRequestDelegate {
func productsRequest(_request:SKProductsRequest, didReceive response:SKProductsResponse) {
// ...
}
}
extension StoreManager: SKRequestDelegate {
func request(_request:SKRequest, didFailWithError error:Error) {
// ...
}
}
StoreManager类也是一个单例
PS: SKProductsRequestDelegate协议的一些可选方法没有实现
Connect配置
支持的商品需要在Connect上配置
购买有效性校验
示例代码:
func checkReceipt(isSandbox :Bool) {
DispatchQueue.global().asyncAfter(deadline:DispatchTime.now() + .seconds(10)) {
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
var verifyUrl :URL?
if isSandbox {
verifyUrl =URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")
} else {
verifyUrl =URL(string: "https://buy.itunes.apple.com/verifyReceipt")
}
if let url = verifyUrl {
do {
let receiptData =try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
let receiptString = receiptData.base64EncodedString(options:.endLineWithLineFeed)
var request =URLRequest(url: url)
let body = String(format: "{\"receipt-data\" : \"%@\", \"exclude-old-transactions\" : false, \"password\" : \"这里写Connect里设置的密码\"}", receiptString)
request.httpBody = body.data(using: .utf8)
request.httpMethod ="POST"
let session = URLSession.shared.dataTask(with: request) { recvData, response, erro rin
if error != nil{
// 网络错误
return
}
if let data = recvData {
if let jsonData = try?JSONSerialization.jsonObject(with: data, options: .allowFragments) as? DictionaryAny> {
if let status :Int = jsonData["status"] as? Int {
if status == 21007 {
if !isSandbox {
checkReceipt(isSandbox:true)
}
} else if status == 21002 || status == 21005 || status == 21009 {
checkReceipt(isSandbox: isSandbox)
} else if status == 0 {
// 解析数据
}
}
}
}
}
session.resume()
} catch {}
}
} else {
fetchReceipt()
}
}
}
关联文档
第一篇 开发构想
第二篇 编辑器控件
第三篇 导入导出功能
第五篇 本地化