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、内购流程

  1. 用户通过触发APP内购买行为,通过StoreKit连接App Store,发送用户需要购买的商品标识,开始处理支付事务;
  2. App Store完成支付,再通过StoreKit框架通知APP,传回用户购买的商品和收据;
  3. 为了验证收据,你需要将收据发送到自己的服务器,通过自己的服务器与App Store进行验证(也可以在APP中与App Store验证,但是不安全);
  4. 自己的服务器验证收据有效后,通知APP进行相关UI更新操作。
  5. 对自动订阅类型的商品,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 内购买项目

当用户购买了非消耗型项目、自动续期订阅、非续期订阅,并希望在其他设备上使用时,可以通过 SKPaymentQueuerestoreCompletedTransactions() 来恢复。

@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 进行验证
  1. 构建 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.
  1. 构建 HTTP POST 请求
  • URL:

    • https://sandbox.itunes.apple.com/verifyReceipt (沙盒环境)
    • https://buy.itunes.apple.com/verifyReceipt (App Store)

重要

验证收据时先调用生产 URL 地址,当返回的状态码为 21007 时再去调用沙盒环境 URL 地址,这样可以确保不必在测试、审核或者 App Store 环境中进行地址切换。

  1. 解析响应

响应格式可以参照苹果文档 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 框架介绍相关推荐

  1. iOS 内购项目的App Store推广

    iOS 11以后的用户可以在App Store内的下载页面内直接购买应用的内购商品,这项功能苹果称作做Promoting In-App Purchases,如果你的App需要在App Store推广自 ...

  2. iOS内购(IAP)自动续订订阅

    一.介绍 iOS 的 App 内购类型有四种: 消耗型商品:只可使用一次的产品,使用之后即失效,必须再次购买. 示例:钓鱼 App 中的鱼食. 非消耗型商品:只需购买一次,不会过期或随着使用而减少的产 ...

  3. java集成ios内购\与ios退款通知处理

    使用ios内购,需在项目数据库建立虚拟币相关表(虚拟币余额表.充值面额表.充值订单表等)上代码 苹果IAP内购验证工具类 IosVerifyUtil import javax.net.ssl.*; i ...

  4. U3D如何添加IOS内购,自制内购小插件

    参考资料:http://www.cocoachina.com/bbs/read.php?tid-69165-fpage-2.html 网上很多IAP的教程,但是较少有结合U3D的教程.所以我在此进行简 ...

  5. iOS流媒体直播整个框架介绍(HLS、RTSP)

    iOS流媒体直播整个框架介绍(HLS.RTSP) 目录技术文章2016年7月17日 一.HTTP(WebService) 基于HTTP的渐进下载Progressive Download流媒体播放仅是在 ...

  6. iOS内购-防越狱破解刷单

    ---------------------------2018.10.16更新--------------------------- 最近我们公司丢单率上涨,尤其是10月份比9月份来说丢单率翻了3倍, ...

  7. IOS内购经常遇到的一些问题,和一些容易混淆的点。

    Q1:内购和Apple Pay的区别? A1:内购是内购,Apple Pay是Apple Pay.我不知道有多少人第一次接触时,会把这俩概念混淆掉,这里你可以简单这么理解,虚拟的物品就是用内购,实际的 ...

  8. iOS 内购(In-App Purchase)详解

    iOS 内购(In-App Purchase)详解 概述 IAP 全称:In-App Purchase,是指苹果 App Store 的应用内购买,是苹果为 App 内购买虚拟商品或服务提供的一套交易 ...

  9. Unity iOS内购

    前言:最近项目需要切换到iOS平台做一些提交审核和支付对接相关的工作,上一篇刚分享了最新的iOS10提交审核的一些坑,这篇分享一些内购相关的流程. Unity iOS内购 思路: Unity调用iOS ...

最新文章

  1. python websocket异步高并发_Python3.5异步和多个websocket服务器
  2. 《预训练周刊》第19期:歧义短语的类量子语境性研究、自然语言处理中prompt方法的系统综述...
  3. Android性能优化工具
  4. SAP凭证冲销BAPI用法
  5. 阿里高级技术专家:整洁的应用架构“长”什么样?
  6. linux 编译java并打包
  7. 使用dva脚手架(dva-cli)快速构建React项目
  8. win7 64位运行不了服务器,G6-e标准包可以装在win7 64位系统上吗?现在提示不能登陆到服务器...
  9. php 网关接口,[PHP] 通用网关接口CGI 的运行原理
  10. LuoguP1041 传染病控制
  11. Vmware 15 安装 win7 虚拟机 (初学者操作与详解教程)
  12. paip.提升用户体验----c++ c# 配色方案
  13. GIS三维可视化技术在输电领域的应用研讨
  14. 基于网络爬虫技术的网络新闻分析
  15. 微分几何与广义相对论教程
  16. 眼部化妆品、护肤品亚马逊要求的BCOP眼刺激性测试是什么
  17. 手把手教程|构建无服务器通用文本识别功能
  18. 产品设计 【网站转化率与漏斗模型】
  19. 企业IT管理员IE11升级指南【8】—— Win7 IE8和Win7 IE11对比
  20. c语言中if( k1)的含义,C语言:我的按键程序K1键按下没有反应,其他两个都有反应...

热门文章

  1. 前端CSS-设置鼠标图标
  2. mysql 星 拓扑,高性能MySQL:复制拓扑
  3. 浅谈阅读工具Kindle的合理利用
  4. FISCO-BCOS 十四、使用Caliper进行压力测试fisco-bcos
  5. Unity文字冒险游戏项目实战
  6. 从零开始学 Web 之 HTML5(一)HTML5概述,语义化标签
  7. arduino nano 简单实现蓝牙模块与手机进行通信
  8. php中怎么给文字加颜色,PHP水印类,支持添加图片、文字、填充颜色区域的实现...
  9. Atlassian Crowd实现JIRA、Confluence、Bamboo和Fisheye and Crucible单点登录
  10. 正点原子linux资料pdf,正点原子阿尔法linux开发板光盘a盘4、参考devicetree 2.pdf