iOS 内购StoreKit 框架介绍
StoreKit 框架介绍
一、StoreKit 能做什么?
- In-App Purchase
- 提供和促进内容和服务的应用内购买。
- Apple Music
- 检查用户的Apple Music功能并提供订阅服务。
- Recommendations and reviews
- 为第三方内容提供推荐,让用户对你的应用进行评价和评论。
头文件一览
#import <StoreKit/SKAdNetwork.h>
#import <StoreKit/SKArcadeService.h>
#import <StoreKit/SKCloudServiceController.h> // Apple Music
#import <StoreKit/SKCloudServiceSetupViewController.h> // Apple Music
#import <StoreKit/SKDownload.h>
#import <StoreKit/SKError.h>
#import <StoreKit/SKPayment.h>
#import <StoreKit/SKPaymentDiscount.h>
#import <StoreKit/SKPaymentQueue.h>
#import <StoreKit/SKPaymentTransaction.h>
#import <StoreKit/SKProduct.h>
#import <StoreKit/SKProductDiscount.h>
#import <StoreKit/SKProductsRequest.h>
#import <StoreKit/SKProductStorePromotionController.h>
#import <StoreKit/SKReceiptRefreshRequest.h>
#import <StoreKit/SKRequest.h>
#import <StoreKit/SKStorefront.h>
#import <StoreKit/SKStoreProductViewController.h>
#import <StoreKit/SKStoreReviewController.h> // Recommendations and reviews
#import <StoreKit/StoreKitDefines.h>
我们将主要介绍日常开发中常用的内购和 APP 推荐评价功能。
二、In-App Purchase
为什么要使用In-App Purchase
(苹果内购)的方式购买在APP内提供的服务和内容?因为这是苹果强制规定的,每产生一份交易,苹果将会从中抽取30%
的佣金费用。苹果要求:
- 如果您想要在 app 内解锁特性或功能 (解锁方式有:订阅、游戏内货币、游戏关卡、优质内容的访问权限或解锁完整版等),则必须使用 App 内购买项目。
- App 不得使用自身机制来解锁内容或功能,如许可证密钥、增强现实标记、二维码等。
- App 及对应元数据不得包含指引用户使用非 App 内购买项目机制进行购买的按钮、外部链接或其他行动号召用语。
- …
- 如果 app 允许用户购买将在 app 之外使用的商品或服务,则必须使用 App 内购买项目以外的购买方式来收取相应款项,如 Apple Pay 或传统的信用卡入口。
具体的相关规则请查看App Store 审核指南-App 内购买项目。
1、内购流程
- 用户通过触发APP内购买行为,通过
StoreKit
连接App Store
,发送用户需要购买的商品标识,开始处理支付事务; App Store
完成支付,再通过StoreKit
框架通知APP,传回用户购买的商品和收据;- 为了验证收据,你需要将收据发送到自己的服务器,通过自己的服务器与
App Store
进行验证(也可以在APP中与App Store
验证,但是不安全); - 自己的服务器验证收据有效后,通知APP进行相关UI更新操作。
- 对自动订阅类型的商品,
App Store
还会将相关续订事件发送到服务器上。
2、内购商品类型
针对不同的商品特性,需要创建不同的内购商品。App Store Connect
提供了4中类型的抽象商品。
- 消耗型项目
- 用户可以购买各种消耗型项目 (例如游戏中的生命或宝石) 以继续 app 内进程。消耗型项目只可使用一次,使用之后即失效,必须再次购买。
- 非消耗型项目
- 用户可购买非消耗型项目以提升 app 内的功能。非消耗型项目只需购买一次,不会过期 (例如修图 app 中的其他滤镜)。
- 自动续期订阅
- 用户可购买固定时段内的服务或更新的内容 (例如云存储或每周更新的杂志)。除非用户选择取消,否则此类订阅会自动续期。
- 非续期订阅
- 用户可购买有时限性的服务或内容 (例如线上播放内容的季度订阅)。此类的订阅不会自动续期,用户需要逐次续订。
用户可以在App Store Connect
中添加适合自己商品的抽象商内购品。
对于非消耗型项目和自动续期订阅,苹果允许用户通过内购恢复的方式在多个设备间同步和恢复。当用户购买自动续期订阅或非续期订阅时,您的 app 应当让用户能够在所有设备上访问这一订阅,并让用户能够恢复以前购买的项目。
3、相关API使用
设置交易观察器和付款队列
为了完成苹果内购流程,你需要设置相应的交易观察器,即时监听当前交易的状态。
class StoreObserver: NSObject, SKPaymentTransactionObserver {....//Initialize the store observer.override init() {super.init()//Other initialization here.}//Observe transaction updates.func paymentQueue(_ queue: SKPaymentQueue,updatedTransactions transactions: [SKPaymentTransaction]) {//Handle transaction states here.}....
}
通过回调函数paymentQueue(_:updatedTransactions:)
,你可以通过获取SKPaymentTransaction
实例的var transactionState: SKPaymentTransactionState
字段来明确当前交易进度。
public enum SKPaymentTransactionState : Int {case purchasing = 0 // Transaction is being added to the server queue.case purchased = 1 // Transaction is in queue, user has been charged. Client should complete the transaction.case failed = 2 // Transaction was cancelled or failed before being added to the server queue.case restored = 3 // Transaction was restored from user's purchase history. Client should complete the transaction.@available(iOS 8.0, *)case deferred = 4 // The transaction is in the queue, but its final status is pending external action.
}
加入付款队列
let iapObserver = StoreObserver()import UIKit
import StoreKitclass AppDelegate: UIResponder, UIApplicationDelegate {....// Attach an observer to the payment queue.func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {SKPaymentQueue.default().add(iapObserver)return true}// Called when the application is about to terminate.func applicationWillTerminate(_ application: UIApplication) {// Remove the observer.SKPaymentQueue.default().remove(iapObserver)}....
}
启动时就将交易观察器添加到付款队列是很重要的。这可以确保在你的 APP 的整个生命周期内,都可以监听交易的相关事件通知,即使你当前不处在 APP 内。如
- 进行内购
- 后台进程订阅
- 交易中断(如果正在进行交易时杀死 APP,那么这次交易之后的状态会在启动 APP 后通过回调函数传回)
获取 App 内购买项目
在进行交易前,你需要先检查框架是否可用。
var isAuthorizedForPayments: Bool {return SKPaymentQueue.canMakePayments()
}
然后确保你已经在 App Store Connect 中创建了 App 内购买项目。如果还没有创建,可以参照下面链接。
创建 App 内购买项目
假设你已经创建好了 App 内购买项目,名叫 ProductA。在购买项目前,你需要知道 ProductA 的唯一标识,这是在创建内购项目时就要求输入的字段。对于需要展示在 APP 中购买的项目,你需要维护这些项目的唯一标识。你可以将这些标识维护在 APP 本地中,也可以交给服务器通过接口请求(推荐)获取。
现在我们通过 SKProductsRequest
类来获取 App Store 上的内购项目。
public protocol SKProductsRequestDelegate : SKRequestDelegate {// Sent immediately before -requestDidFinish:@available(iOS 3.0, *)func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse)
}// request information about products for your application
@available(iOS 3.0, *)
open class SKProductsRequest : SKRequest {// Set of string product identifiers@available(iOS 3.0, *)public init(productIdentifiers: Set<String>)@available(iOS 3.0, *)weak open var delegate: SKProductsRequestDelegate?
}@available(iOS 3.0, *)
open class SKProductsResponse : NSObject {// Array of SKProduct instances.@available(iOS 3.0, *)open var products: [SKProduct] { get }// Array of invalid product identifiers.@available(iOS 3.0, *)open var invalidProductIdentifiers: [String] { get }
}
可以看到,SKProductsRequest
初始化参数包含了一个标识数组,这个指的就是你创建的内购项目的商品标识。
fileprivate func fetchProducts(matchingIdentifiers identifiers: [String]) {// Create a set for the product identifiers.let productIdentifiers = Set(identifiers)// Initialize the product request with the above identifiers.productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)productRequest.delegate = self// Send the request to the App Store.productRequest.start()
}
productRequest
start 后是个异步操作,为了避免 productRequest
被提前释放,你需要强持有这个对象实例。
请求完成后,你可以通过代理func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse)
获取到 SKProductsResponse
对象。该对象包含了一个 SKProduct
实例的数组和一个不可用标识的数组。
// products contains products whose identifiers have been recognized by the App Store. As such, they can be purchased.
if !response.products.isEmpty {availableProducts = response.products
}// invalidProductIdentifiers contains all product identifiers not recognized by the App Store.
if !response.invalidProductIdentifiers.isEmpty {invalidProductIdentifiers = response.invalidProductIdentifiers
}
SKProduct
就是我们需要的东西,包含了内购商品的名称、价格等信息。你可以通过扩展 SKProduct
来显示当地货币价格。
extension SKProduct {/// - returns: The cost of the product formatted in the local currency.var regularPrice: String? {let formatter = NumberFormatter()formatter.numberStyle = .currencyformatter.locale = self.priceLocalereturn formatter.string(from: self.price)}
}
完成 App 内购买
根据获取到的 SKProduct
实例,我们创建一个 SKMutablePayment
对象。
open class SKMutablePayment : SKPayment {@available(iOS 7.0, *)open var applicationUsername: String?@available(iOS 12.2, *)@NSCopying open var paymentDiscount: SKPaymentDiscount?@available(iOS 3.0, *)open var productIdentifier: String@available(iOS 3.0, *)open var quantity: Int@available(iOS 3.0, *)open var requestData: Data?@available(iOS 8.3, *)open var simulatesAskToBuyInSandbox: Bool
}
// Use the corresponding SKProduct object returned in the array from SKProductsRequest.
let payment = SKMutablePayment(product: product)
payment.quantity = 2
设置购买数量为2,然后只需要简单地加入到交易队列中。
SKPaymentQueue.default().add(payment)
恢复 App 内购买项目
当用户购买了非消耗型项目、自动续期订阅、非续期订阅,并希望在其他设备上使用时,可以通过 SKPaymentQueue
的 restoreCompletedTransactions()
来恢复。
@available(iOS 3.0, *)
open func restoreCompletedTransactions()
SKPaymentQueue.default().restoreCompletedTransactions()
处理交易
还记得之前设置的交易观察器吗。当交易进行处于交易队列中时(购买内购项目还是恢复内购项目),StoreKit 就会回调交易观察器的代理方法。每个交易会有5个状态,你需要为每个状态建立对应的处理方法。
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {for transaction in transactions {switch transaction.transactionState {case .purchasing: break// Do not block the UI. Allow the user to continue using the app.case .deferred: print(Messages.deferred)// The purchase was successful.case .purchased: handlePurchased(transaction)// The transaction failed.case .failed: handleFailed(transaction)// There're restored products.case .restored: handleRestored(transaction)@unknown default: fatalError(Messages.unknownPaymentTransaction)}}
}
值得注意的是,当交易失败是除了用户主动取消的原因外,你需要为用户显示对应的错误提示。
// Do not send any notifications when the user cancels the purchase.
if (transaction.error as? SKError)?.code != .paymentCancelled {DispatchQueue.main.async {self.delegate?.storeObserverDidReceiveMessage(message)}
}
提供给购买内容和结束交易
在收到状态为SKPaymentTransactionState.purchased
或者SKPaymentTransactionState.restored
的交易后,开发者必须为用户提供已购买的功能或者内容。
对于未完成的交易,会一直存在交易队列中。StoreKit
会在 APP 再次启动或者从后台回到到前台时回调交易观察器的代理方法。因此会不断要求用户进行购买授权或者重复调用相关购买逻辑代码。
因此,在收到状态为SKPaymentTransactionState.purchased
或者SKPaymentTransactionState.restored
的交易后开发者应该关闭当前的交易。
// Finish the successful transaction.
SKPaymentQueue.default().finishTransaction(transaction)
4、收据验证
收据验证逻辑可以放在 APP 中或者服务端或者两者结合使用。购买的项目(已完成或者未完成)将会存放在收据中,直到你调用了finishTransaction(_:)
函数。如果需要维护用户的消费记录,你需要自建服务器管理用户的交易记录。对于非消耗型项目、自动续期订阅、非续期订阅项目将会永久保存在收据中。
具体采取哪种方式处理收据验证逻辑,苹果给出了以下建议:
On-device validation | Server-side validation | |
---|---|---|
Validates authenticity of receipt | Yes | Yes |
Includes renewal transactions | Yes | Yes |
Includes additional user subscription information | No | Yes |
Handles renewals without client dependency | No | Yes |
Resistant to device clock change | No | Yes |
获取收据
// Get the receipt if it's available
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {do {let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)print(receiptData)let receiptString = receiptData.base64EncodedString(options: [])// Read receiptData}catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
}
发送收据到 App Store 进行验证
- 构建 json 对象
{"receipt-data": "base64(receiptData)","password": "password","exclude-old-transactions": true
}
- receipt-data: (byte)(Required) The Base64 encoded receipt data.
- password: (string)(Required) Your app’s shared secret (a hexadecimal string).Use this field only for receipts that contain auto-renewable subscriptions.
- exclude-old-transactions: (boolean) Set this value to true for the response to include only the latest renewal transaction for any subscriptions.Use this field only for app receipts that contain auto-renewable subscriptions.
- 构建 HTTP POST 请求
- URL:
- https://sandbox.itunes.apple.com/verifyReceipt (沙盒环境)
- https://buy.itunes.apple.com/verifyReceipt (App Store)
重要
验证收据时先调用生产 URL 地址,当返回的状态码为 21007 时再去调用沙盒环境 URL 地址,这样可以确保不必在测试、审核或者 App Store 环境中进行地址切换。
- 解析响应
响应格式可以参照苹果文档 responseBody。
三、APP 推荐和评价功能
1、应用内下载其他 APP
如果你想在自己 APP 中为用户直接提供 App Store 的购买服务,可以通过使用 SKStoreProductViewController
类来实现。该类定义如下:
/* View controller to display iTunes Store product information */@available(iOS 6.0, *)
open class SKStoreProductViewController : UIViewController {// Delegate for product page events@available(iOS 6.0, *)weak open var delegate: SKStoreProductViewControllerDelegate?// Load product view for the product with the given parameters. See below for parameters (SKStoreProductParameter*).// Block is invoked when the load finishes.@available(iOS 6.0, *)open func loadProduct(withParameters parameters: [String : Any], completionBlock block: ((Bool, Error?) -> Void)? = nil)
}public protocol SKStoreProductViewControllerDelegate : NSObjectProtocol {// Sent after the page is dismissed@available(iOS 6.0, *)optional func productViewControllerDidFinish(_ viewController: SKStoreProductViewController)
}
在初始化时,我们需要设置 SKStoreProductParameterITunesItemIdentifier
,这个参数表示需要推荐的项目在 iTunes 中的唯一标识,是一个 NSNumber
实例。我们可以通过苹果提供的链接制作工具 linkmaker.itunes.apple.com 先搜索到需要展示的内容。
我们以QQ为例。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DNaGehsE-1682489374475)(null)]
链接 https://apps.apple.com/cn/app/qq/id444934666?mt=8 中 444934666 就是我们需要的标识。
var parametersDictionary = [SKStoreProductParameterITunesItemIdentifier: product.productIdentifier]// Create a store product view controller.
let store = SKStoreProductViewController()
store.delegate = self/*Attempt to load the selected product from the App Store. Display the store product view controller if success and print an error message,otherwise.*/
store.loadProduct(withParameters: parametersDictionary, completionBlock: {[unowned self] (result: Bool, error: Error?) inif result {self.present(store, animated: true, completion: {print("The store view controller was presented.")})} else {if let error = error {print("Error: \(error.localizedDescription)")}}})
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4hVU71RB-1682489374396)(null)]
最后,我们还要实现 SKStoreProductViewControllerDelegate
代理方法以便在购买结束时关闭推荐页面。
/// Used to dismiss the store view controller.func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {viewController.presentingViewController?.dismiss(animated: true, completion: {print("The store view controller was dismissed.")})}
2、应用内评级与评价
在 iOS 10.3+
之后,StoreKit 提供了 SKStoreReviewController
用于直接在 APP 内显示评级和评价弹窗。
/** Controller class to request a review from the current user */
SK_EXTERN_CLASS API_AVAILABLE(ios(10.3), macos(10.14)) API_UNAVAILABLE(watchos) __TVOS_PROHIBITED @interface SKStoreReviewController : NSObject/** Request StoreKit to ask the user for an app review. This may or may not show any UI.** Given this may not successfully present an alert to the user, it is not appropriate for use* from a button or any other user action. For presenting a write review form, a deep link is * available to the App Store by appending the query params "action=write-review" to a product URL.*/
+ (void)requestReview API_AVAILABLE(ios(10.3), macos(10.14)) API_UNAVAILABLE(watchos) __TVOS_PROHIBITED;@end
SKStoreReviewController.requestReview()
如果想要引导用户添加评论,可以通过 APP 在 App Store 中的链接上拼接参数 action=write-review
跳转 App Store 进行评价。
以 QQ 为例,其手机版 App Store 地址为
- https://apps.apple.com/cn/app/qq/id444934666?mt=8,
拼接参数后为
- https://apps.apple.com/cn/app/qq/id444934666?mt=8&action=write-review
guard let url = URL.init(string:"https://apps.apple.com/cn/app/qq/id444934666?mt=8&action=write-review") else {return;
}
UIApplication.shared.open(url, options: [:]) { _ in}
四、参考资料
- App 内购买项目
- Recommendations and Reviews
iOS 内购StoreKit 框架介绍相关推荐
- iOS 内购项目的App Store推广
iOS 11以后的用户可以在App Store内的下载页面内直接购买应用的内购商品,这项功能苹果称作做Promoting In-App Purchases,如果你的App需要在App Store推广自 ...
- iOS内购(IAP)自动续订订阅
一.介绍 iOS 的 App 内购类型有四种: 消耗型商品:只可使用一次的产品,使用之后即失效,必须再次购买. 示例:钓鱼 App 中的鱼食. 非消耗型商品:只需购买一次,不会过期或随着使用而减少的产 ...
- java集成ios内购\与ios退款通知处理
使用ios内购,需在项目数据库建立虚拟币相关表(虚拟币余额表.充值面额表.充值订单表等)上代码 苹果IAP内购验证工具类 IosVerifyUtil import javax.net.ssl.*; i ...
- U3D如何添加IOS内购,自制内购小插件
参考资料:http://www.cocoachina.com/bbs/read.php?tid-69165-fpage-2.html 网上很多IAP的教程,但是较少有结合U3D的教程.所以我在此进行简 ...
- iOS流媒体直播整个框架介绍(HLS、RTSP)
iOS流媒体直播整个框架介绍(HLS.RTSP) 目录技术文章2016年7月17日 一.HTTP(WebService) 基于HTTP的渐进下载Progressive Download流媒体播放仅是在 ...
- iOS内购-防越狱破解刷单
---------------------------2018.10.16更新--------------------------- 最近我们公司丢单率上涨,尤其是10月份比9月份来说丢单率翻了3倍, ...
- IOS内购经常遇到的一些问题,和一些容易混淆的点。
Q1:内购和Apple Pay的区别? A1:内购是内购,Apple Pay是Apple Pay.我不知道有多少人第一次接触时,会把这俩概念混淆掉,这里你可以简单这么理解,虚拟的物品就是用内购,实际的 ...
- iOS 内购(In-App Purchase)详解
iOS 内购(In-App Purchase)详解 概述 IAP 全称:In-App Purchase,是指苹果 App Store 的应用内购买,是苹果为 App 内购买虚拟商品或服务提供的一套交易 ...
- Unity iOS内购
前言:最近项目需要切换到iOS平台做一些提交审核和支付对接相关的工作,上一篇刚分享了最新的iOS10提交审核的一些坑,这篇分享一些内购相关的流程. Unity iOS内购 思路: Unity调用iOS ...
最新文章
- python websocket异步高并发_Python3.5异步和多个websocket服务器
- 《预训练周刊》第19期:歧义短语的类量子语境性研究、自然语言处理中prompt方法的系统综述...
- Android性能优化工具
- SAP凭证冲销BAPI用法
- 阿里高级技术专家:整洁的应用架构“长”什么样?
- linux 编译java并打包
- 使用dva脚手架(dva-cli)快速构建React项目
- win7 64位运行不了服务器,G6-e标准包可以装在win7 64位系统上吗?现在提示不能登陆到服务器...
- php 网关接口,[PHP] 通用网关接口CGI 的运行原理
- LuoguP1041 传染病控制
- Vmware 15 安装 win7 虚拟机 (初学者操作与详解教程)
- paip.提升用户体验----c++ c# 配色方案
- GIS三维可视化技术在输电领域的应用研讨
- 基于网络爬虫技术的网络新闻分析
- 微分几何与广义相对论教程
- 眼部化妆品、护肤品亚马逊要求的BCOP眼刺激性测试是什么
- 手把手教程|构建无服务器通用文本识别功能
- 产品设计 【网站转化率与漏斗模型】
- 企业IT管理员IE11升级指南【8】—— Win7 IE8和Win7 IE11对比
- c语言中if( k1)的含义,C语言:我的按键程序K1键按下没有反应,其他两个都有反应...
热门文章
- 前端CSS-设置鼠标图标
- mysql 星 拓扑,高性能MySQL:复制拓扑
- 浅谈阅读工具Kindle的合理利用
- FISCO-BCOS 十四、使用Caliper进行压力测试fisco-bcos
- Unity文字冒险游戏项目实战
- 从零开始学 Web 之 HTML5(一)HTML5概述,语义化标签
- arduino nano 简单实现蓝牙模块与手机进行通信
- php中怎么给文字加颜色,PHP水印类,支持添加图片、文字、填充颜色区域的实现...
- Atlassian Crowd实现JIRA、Confluence、Bamboo和Fisheye and Crucible单点登录
- 正点原子linux资料pdf,正点原子阿尔法linux开发板光盘a盘4、参考devicetree 2.pdf