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

class 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 上的内购项目。

Loading In-App Product Identifiers

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 = .currency
        formatter.locale = self.priceLocale
        return formatter.string(from: self.price)
    }
}
完成 App 内购买

Requesting a Payment from the App Store

根据获取到的 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 validationServer-side validation
Validates authenticity of receiptYesYes
Includes renewal transactionsYesYes
Includes additional user subscription informationNoYes
Handles renewals without client dependencyNoYes
Resistant to device clock changeNoYes
获取收据
// 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)]

1597823563007.jpg

链接 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?) in
    if 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 内显示评级和评价弹窗。

WX20200819-161304@2x.png

/** 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

}

四、参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值