对于商品落地页而言,价格无疑是最重要的信息。而 IAP 商品却是在 App Store Connect 后台进行管理的,Server 是无法拿到相关信息的。一种最简单的方式是:在商品创建的同时,将商品的价格信息注册到 Server,然后 Frontend 与 Server 通信获取落地页信息。但这显然存在问题,因为它无法处理实时价格、价格国际化和折扣优惠。目前唯一的解决方式是 Native 通过 StoreKit 获取商品信息,并将其告知 Frontend。

价格国际化

如果你的 App 只在某一个地区或者国家上架且不会进行商品价格的调整,那么向 Server 注册价格让 Frontend 去获取的方式是最简单的展示价格的方式。但如果前提条件不成立的话,那么这就不是一个好的方式,而是应当让 Native 通过以下方式去获取并以 Javascript 回调同步给 Frontend:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

// 建议与 Frontend 通信的插件机制,通过正则匹配 url 路由
// 插件本身是以 ObjC 运行时的方式在 WebView 创建时动态注入
class IAPProductPlugin: WebViewPlugin, WebViewURLHandler {
let urlPattern = "^native.app://webview/iap_product"

override var urlHandler: WebViewURLHandler? { self }

func handle(_ url: URL) {
guard let idString = url.bay.getQuery(by: "product_ids") else { return }
ProductRequest.start(idString.toArray) {
// Javascript callback with specified data model.
}
}
}

private class ProductRequest: NSObject, SKProductsRequestDelegate {
private static var current: ProductRequest?

static func start(_ identifiers: [String], completion: @escaping FetchCompletion) {
current?.request?.cancel()

let request = ProductRequest(identifiers: identifiers)
self.current = request

request.start { result in
self.current = nil
completion(result)
}
}

private let identifiers: Set<String>
private var completion: FetchCompletion?
private var request: SKProductsRequest?
private var pendingProducts: [SKProduct] = []

private init(identifiers: [String]) {
self.identifiers = Set(identifiers)
}

private func start(_ completion: @escaping FetchCompletion) {
self.completion = { [weak self] in
self?.pendingProducts = []
completion($0)
}
let request = SKProductsRequest(productIdentifiers: identifiers)
self.request = request

request.delegate = self
request.start()
}

// MARK: - SKProductsRequestDelegate

fileprivate func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
pendingProducts.append(contentsOf: response.products)
}

fileprivate func requestDidFinish(_ request: SKRequest) {
let identifiers = Set(pendingProducts.map { $0.productIdentifier })
guard identifiers == self.identifiers else {
// handle failure
return
}

// handle success
}

fileprivate func request(_ request: SKRequest, didFailWithError error: Error) {
// handle failure
}
}

其中 SKProduct 中包含了价格信息、货币单位和符号:

1
2
3
4
class SKProduct: NSObject {
var price: NSDecimalNumber { get } // price.doubleValue
var priceLocale: Locale { get } // priceLocale.currencySymbol(¥) / priceLocale.currencyCode(CNY/JPY)
}

注意,这里的价格地区与 AppleID 分区相关。StoreKit 本身存在缓存,首次获取相对较慢。如果数据没有刷新,可以重新登录 AppleID 进行重试。

优惠

通常出于运营获客的目的,商品在特定时间段内购买可以享受折扣或者优惠。在 App Store Connect 后台,我们可以为订阅型商品创建「推介促销优惠(Introductory Offers)」和「促销优惠(Promotional Offers)」。前者每个 Apple ID 只能享受一次,后者由开发者的 Server 管理享受资格。因为官方的数据分析会有一定延迟,所以产品会希望 Server 能够准确打点用户的实际购买支付价格(需要考虑优惠)用以分析产品运营活动。很遗憾地是 Server 无法知道用户的实际支付价格,只能通过解析收据从 is_trial_periodis_in_intro_offer_period 判断用户是否享受了「推介促销优惠」,即需要 Native 将「推介促销优惠」后的价格上报给 Server。大体流程如下:

  1. 在验证收据时,通过 SKPaymentTransaction.payment.productIdentifier 获取到商品 ID。
  2. 使用 SKProductsRequest 通过商品 ID 获取到商品现存的优惠或者折扣信息。
  3. 如果存在「推介促销优惠」,则在验证收据时带上优惠后的价格信息,Server 通过分析收据,判断用户的实付价格并上报打点。

SKProduct 中的优惠信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SKProduct: NSObject {
// iOS 11.2+
var introductoryPrice: SKProductDiscount? { get } // 推介促销优惠,同一地区同一时间短只会存在一个。
// iOS 12.2+
var discounts: [SKProductDiscount] { get } // 促销优惠
}

// iOS 11.2+
class SKProductDiscount : NSObject {
var price: NSDecimalNumber { get }
var priceLocale: Locale { get }
// iOS 12.2+,推介促销优惠为 nil
var identifier: String? { get }
// 优惠周期
var subscriptionPeriod: SKProductSubscriptionPeriod { get }
var numberOfPeriods: Int { get }
// 支付方式:免费试用/先用后付/随用随付
var paymentMode: SKProductDiscount.PaymentMode { get }
// iOS 12.2+,优惠类型:推介促销优惠/促销优惠
var type: SKProductDiscount.`Type` { get }
}

促销优惠的使用相对更为复杂,这里是官方文档,流程描述如下:

  1. 在 App Store Connect 后台创建订阅密钥,下载该密钥并交给 Server。
  2. 在用户购买商品时,Server 判断用户可以享受的优惠信息。
  3. Native 通过商品 ID 获取所有的促销优惠信息,如果该用户可以使用其中某个优惠,则将其 ID 信息发送给 Server 让其通过订阅密钥进行签名。
  4. Native 通过签名信息生成 SKPaymentDiscount 实例对象,在将购买加入到 IAP 队列之前,赋值给 payment.paymentDiscount。
  5. 在验证收据时,Native 自然就可以通过 SKPaymentTransaction.payment.paymentDiscount.identifier 匹配到具体的促销优惠信息。(如果是「推介促销优惠」,paymentDiscount 为 nil。)

总结

其实从流程上来看,整个价格和优惠的适配过程并不复杂,关键在于可回归测试版本让人莫名其妙。最开始在购买过程中,Debug、AdHoc 和 TestFlight 版本上均无法正确展示「推介促销优惠」,在已经上线的 App Store 版本上可以使用优惠。在这个用于测试的版本上线后,TestFlight 版本就可以正常展示「推介促销优惠」了!WTF!相关论坛有不少人提出了如何测试优惠的相关问题(TestFlight 不可用),保不准是因为延迟或者其他乱七八糟的缓存问题导致的,或许我们都已经习惯了 IAP 带来的各种“惊喜”,🤣