iOS 应用内购买的推介优惠、促销优惠、优惠代码的处理

苹果应用内购买的自动续订订阅,主要有三种优惠方式,三种可以同时提供。

  • 推介优惠(Introductory Offers)
  • 促销优惠(Promotional Offers)
  • 优惠代码(Offer Codes)

其中促销优惠(Promotional Offer)是苹果在 2019 年出的一种促销优惠方案。

最近开发的时候发现,网上几乎没有相关的文章,所以记录一下,方便其他人更快地实现。
文章中提到的一些文档和链接都可以在最后一章“附录”中找到。
关于应用内购买,可以查看我另一篇文章 iOS In-App Purchase(IAP) 流程与实现

一、三种优惠的区别

苹果在文档中介绍了几种优惠的区别:

优惠对比中英.jpg

跟产品沟通时,需要了解产品想要的是哪种优惠。网上对三种优惠的翻译都各不相同。

二、实现

下面主要讲下客户端的实现,服务端可以参考其他文档。

2.1 配置

苹果官方文档已经详细说明了如何配置和各种配置的注意点,这里就不再赘述和截图说明了。

对于促销优惠,需要生成购买项目密钥(文档中有说明),然后下载私有密钥,后续需要用到。只能下载一次,请妥善保管已下载的密钥。

2.2 客户端开发

2.2.1 推介优惠

是否享有推介优惠,是由苹果根据 Apple 账号决定的。
不同产品希望的享有逻辑会不同,根据产品策略不同,会有不一样的开发流程。

我们是最简单的场景,新用户都可以享受到推介优惠,不处理用户切换 Apple 账号的场景。
客户端不用开发,配置后,点击购买,系统的购买弹窗上就会显示出优惠信息。复杂的业务逻辑在后端。

试用三天

2.2.2 促销优惠

购买商品时可以传入商品支持的订阅优惠,在支付弹窗中就会显示相关信息。

2.2.2.1 生成优惠 SKPaymentDiscount

购买商品时,需要先生成一个优惠 SKPaymentDiscount。我们看下 SKPaymentDiscount 的初始化方法:

public init(identifier: String, keyIdentifier: String, nonce: UUID, signature: String, timestamp: NSNumber)

初始化方法中需要几个字段:

  1. identifier
    A string used to uniquely identify a discount offer for a product.
    优惠 ID,苹果后台新建的优惠最后的字段
    在 APP - 分发 - 营利(订阅)- 点击订阅组 - 点击某个订阅 - 订阅价格(有效的订阅优惠)- 点击优惠

  2. keyIdentifier
    A string that identifies the key used to generate the signature.
    密钥ID,苹果后台新建的密钥的ID
    在 用户和访问 - 集成 - 密钥(APP 内购买项目)- 密钥 ID

  3. nonce
    A universally unique ID (UUID) value that you define.
    UUID,服务器生成
    这种格式 58780c93-31e0-4a21-af9c-a34fec006c73。python 里是调用 uuid.uuid4()。

  4. signature
    A string representing the properties of a specific promotional offer, cryptographically signed.
    签名,服务器生成

  5. timestamp
    The date and time of the signature's creation in milliseconds, formatted in Unix epoch time.
    服务器时间戳,单位毫秒

let discount = SKPaymentDiscount(identifier: xxx, keyIdentifier: xxx, nonce: xxx, signature: xxx, timestamp: NSNumber(integerLiteral: xxx))
  • 使用 python 生成签名进行自测

先将上一章提到的“私有密钥”(SubscriptionKey_XXXXXXXXXX.p8)从 .p8 格式转成 .der 格式:

openssl pkcs8 -nocrypt -in SubscriptionKey_xxxxxxxx.p8 -out cert.der -outform der

再将以下 python 脚本保存在同个目录中,修改其中的 bundle_idkey_idproductofferapplication_username 并执行。

脚本来自参考的文章,新增和修改了部分注释

# pip3 install ecdsa

import json
import uuid
import time
import hashlib
import base64

from ecdsa import SigningKey
from ecdsa.util import sigencode_der

bundle_id = 'com.xxx.xxx' # bundle ID
key_id = 'XXXXXXXXXX' # 私钥 ID
product = 'sp_3' # 订阅商品 ID
offer = '3day_test' # 优惠 ID
application_username = '' # Should be the same you use when making purchases
nonce = uuid.uuid4()
timestamp = int(round(time.time() * 1000))

payload = '\u2063'.join([bundle_id,
                        key_id,
                        product,
                        offer,
                        application_username,
                        str(nonce), # Should be lower case
                        str(timestamp)])

# Read the key file
with open('cert.der', 'rb') as myfile:
  der = myfile.read()
signing_key = SigningKey.from_der(der)
signature = signing_key.sign(payload.encode('utf-8'),
                            hashfunc=hashlib.sha256,
                            sigencode=sigencode_der)
encoded_signature = base64.b64encode(signature)
print(str(encoded_signature, 'utf-8'), str(nonce), str(timestamp), key_id)

控制台打印结果中,第一个为签名,第二个为 nonce,第三个为 timestamp,第四个为 私钥 ID。此时将所有信息写死在代码中就可以自测购买流程了。

2.2.2.2 使用优惠购买

/// StoreKit
let payment = SKMutablePayment(product: product)
payment.applicationUsername = usernameHash
payment.paymentDiscount = discountOffer

SKPaymentQueue.default().add(payment)

/// SwiftyStoreKit
SwiftyStoreKit.purchaseProduct(product, quantity: 1, paymentDiscount: paymentDiscount, completion: { [weak self] result in
                                   
})

其中 product 需要自行去获取,下面是使用 StoreKit 和 SwiftyStoreKit 去获取的代码:

/// StoreKit
let productRequest = SKProductsRequest(productIdentifiers: Set<String>(arrayLiteral: productId))
productRequest.delegate = self
productRequest.start()

extension XXX: SKProductsRequestDelegate {
    func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
        if let product = response.products.first { /// 获取返回的商品

        }
    }
}

/// SwiftyStoreKit
SwiftyStoreKit.retrieveProductsInfo([productId], completion: { result in

    if let product = result.retrievedProducts.first {

    }
})

2.2.3 优惠代码

优惠代码比较简单,就不赘述,附录中有官方的相关文档可以查阅。

三、附录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349