对于商品落地页而言,价格无疑是最重要的信息。而 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 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) { } } } 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() } 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 { return } } fileprivate func request (_ request : SKRequest , didFailWithError error : Error ) { } }
其中 SKProduct 中包含了价格信息、货币单位和符号:
1 2 3 4 class SKProduct : NSObject { var price: NSDecimalNumber { get } var priceLocale: Locale { get } }
注意,这里的价格地区与 AppleID 分区相关。StoreKit 本身存在缓存,首次获取相对较慢。如果数据没有刷新,可以重新登录 AppleID 进行重试。
优惠 通常出于运营获客的目的,商品在特定时间段内购买可以享受折扣或者优惠。在 App Store Connect 后台,我们可以为订阅型商品创建「推介促销优惠(Introductory Offers)」和「促销优惠(Promotional Offers)」。前者每个 Apple ID 只能享受一次,后者由开发者的 Server 管理享受资格。因为官方的数据分析会有一定延迟,所以产品会希望 Server 能够准确打点用户的实际购买支付价格(需要考虑优惠)用以分析产品运营活动。很遗憾地是 Server 无法知道用户的实际支付价格,只能通过解析收据从 is_trial_period
和 is_in_intro_offer_period
判断用户是否享受了「推介促销优惠」,即需要 Native 将「推介促销优惠」后的价格上报给 Server。大体流程如下:
在验证收据时,通过 SKPaymentTransaction.payment.productIdentifier 获取到商品 ID。
使用 SKProductsRequest 通过商品 ID 获取到商品现存的优惠或者折扣信息。
如果存在「推介促销优惠」,则在验证收据时带上优惠后的价格信息,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 { var introductoryPrice: SKProductDiscount ? { get } var discounts: [SKProductDiscount ] { get } } class SKProductDiscount : NSObject { var price: NSDecimalNumber { get } var priceLocale: Locale { get } var identifier: String ? { get } var subscriptionPeriod: SKProductSubscriptionPeriod { get } var numberOfPeriods: Int { get } var paymentMode: SKProductDiscount .PaymentMode { get } var type: SKProductDiscount .`Type` { get } }
促销优惠的使用相对更为复杂,这里 是官方文档,流程描述如下:
在 App Store Connect 后台创建订阅密钥,下载该密钥并交给 Server。
在用户购买商品时,Server 判断用户可以享受的优惠信息。
Native 通过商品 ID 获取所有的促销优惠信息,如果该用户可以使用其中某个优惠,则将其 ID 信息发送给 Server 让其通过订阅密钥进行签名。
Native 通过签名信息生成 SKPaymentDiscount 实例对象,在将购买加入到 IAP 队列之前,赋值给 payment.paymentDiscount。
在验证收据时,Native 自然就可以通过 SKPaymentTransaction.payment.paymentDiscount.identifier 匹配到具体的促销优惠信息。(如果是「推介促销优惠」,paymentDiscount 为 nil。)
总结 其实从流程上来看,整个价格和优惠的适配过程并不复杂,关键在于可回归测试版本让人莫名其妙。最开始在购买过程中,Debug、AdHoc 和 TestFlight 版本上均无法正确展示「推介促销优惠」,在已经上线的 App Store 版本上可以使用优惠。在这个用于测试的版本上线后,TestFlight 版本就可以正常展示「推介促销优惠」了!WTF!相关论坛有不少人提出了如何测试优惠的相关问题(TestFlight 不可用),保不准是因为延迟或者其他乱七八糟的缓存问题导致的,或许我们都已经习惯了 IAP 带来的各种“惊喜”,🤣