内购:怎么支持业务搞出的优惠活动?!

3,183 阅读9分钟

没做过苹果内购的开发者,也知道在苹果设备上虚拟商品必须使用内购进行售卖,比如App内容会员、解锁某个游戏关卡等。官方提供的内购的开发流程很简单:获取商品信息、用户支付以及支付校验后进行商品分发。

内购开发流程

为了刺激用户的购买,开发者也面对越来越多支付优惠活动的开发,所以如何支持是我们急需了解的模块。

概述

主要介绍苹果提供的三种内购优惠手段:

版本限制面对人群配置和分发
推介优惠(Introductory Offers)iOS 10+新用户1、App Store Connect 中选择时间 安排、地区、价格和时限
2、由苹果判断支付的Apple id是否满足新用户限制
促销优惠(Promotional Offers)iOS 12.2+App 内现有或之前的订阅者。未曾在 App 内订阅过的顾客无法使用此类优惠。1、App Store Connect 中决定业务逻辑、选择价格和时限
2、可由业务方控制是否为该用户分发优惠活动
优惠代码(offer codes)iOS14.2+新的、现有的或之前的订阅者通过任何数字或离线方式分发,以及在 App 内分发。使用直接 URL 或在 App 内兑换。一次性代码也可在 App Store 上兑换

推介优惠

该优惠类型是无需发版开发的。该优惠活动面向的是新用户,当满足后台优惠配置时间区间和系统版本时,可通过SKProduct获取对应的优惠信息,对应字段:

SKProduct.h
// 推介优惠详细信息
@property(nonatomic, readonly, nullable) SKProductDiscount *introductoryPrice API_AVAILABLE(ios(11.2), macos(10.13.2), watchos(6.2), visionos(1.0));

SKProductDiscount

优惠信息类需要关注的几个参数字段:

1、type: SKProductDiscountType 类型

typedef NS_ENUM(NSUInteger, SKProductDiscountType) {
    SKProductDiscountTypeIntroductory,  // 推介优惠类型
    SKProductDiscountTypeSubscription,  // 促销优惠类型
}

由此可知,两种优惠类型使用的均是SKProductDiscount类。

2、paymentMode: SKProductDiscountPaymentMode 支付模式

typedef NS_ENUM(NSUInteger, SKProductDiscountPaymentMode) {
    SKProductDiscountPaymentModePayAsYouGo,  // 随用随付
    SKProductDiscountPaymentModePayUpFront,  // 提前支付
    SKProductDiscountPaymentModeFreeTrial    // 免费试用
}

枚举值对应我们在后台配置时的选项:

StoreKit testing配置时选项

  • 随用随付

    订阅者在特定时限的每个结算周期享有折扣价;例如,对于标准续订价格为每月 15 元的会员,用户在前一个月能享受每月 9 元的订阅价格。这段时间结束后,用户需要按标准续订价格付费。如果你希望通过提供定期折扣而非永久折扣的方式吸引对价格敏感的用户,就可以选择提供这种选项。

  • 提前支付

    订阅者一次性支付特定时限的价格;例如,对于标准续订价格为每月 15元 的会员,前3个月提前支付享受 20 元的订阅价格。这段时间结束后,用户需要按标准续订价格付费。如果你希望为用户提供延长的优惠体验,让他们有时间在下次续订前享受订阅,就可以选择提供这种优惠方式。

  • 免费试用

    订阅者可以在特定时限内免费体验你的订阅;例如,对于标准续订价格为每月 15 元的vip,可以向用户提供3天的免费试用。他们的订阅会立即开始生效,但在优惠期限结束前,不会向用户收取费用。如果你希望用户不需要立即付费就能体验你的订阅项目,就可以选择提供这种优惠方式。

促销优惠

概述

该优惠类型是需要发版开发的。该优惠活动面向的是App内现有或之前的订阅者,开发流程如下:

促销优惠官方图

1、通过商品id请求该app后台配置好的商品信息:

// request information about products for your application
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:@[商品id]]; // 注意: 这块是商品id,而不是促销优惠id
[productsRequest start];

2、SKProductsRequestDelegate 代理方法返回商品id关联的商品信息,其中包括促销优惠信息:

@property(nonatomic, readonly) NSArray<SKProductDiscount *> *discounts API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), visionos(1.0));

该优惠信息使用和推介优惠相同的类SKProductDiscount(详细参数介绍如上)。

3、当用户满足促销优惠条件时,将Server返回的优惠信息构建SKPaymentDiscount参数:

SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
// 创建优惠
SKPaymentDiscount *skPaymentDiscount = [[SKPaymentDiscount alloc] initWithIdentifier:paymentDiscount.identifier keyIdentifier:paymentDiscount.keyIdentifier nonce:paymentDiscount.nonce signature:paymentDiscount.signature timestamp:paymentDiscount.timestamp];
// 在支付payment添加优惠入参
payment.paymentDiscount = paymentDiscount;

[[SKPaymentQueue defaultQueue] addPayment:payment];

实际代码的开发流程结束,正式进入调试阶段,这时候会发现开发容易调试不易,很多bug都是因为不注意细节导致的

不忍了

开发报错排查

问题1:促销优惠需要生成一个签名,签名的生成规则?

创建促销优惠SKPaymentDiscount时,有一系列的入参:

- (**instancetype**)initWithIdentifier:(NSString *)identifier keyIdentifier:(NSString *)keyIdentifier nonce:(NSUUID *)nonce signature:(NSString *)signature timestamp:(NSNumber *)timestamp API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), visionos(1.0));

具体所需参数可参考官方文档

当创建 SKProductDiscount 失败时,这时候不用犹豫就是入参出现问题了(大概率是格式不注意)。

简单对几个参数举例:

  • nonce:格式为UUID
nonce
A one-time UUID value that your server generates. Generate a new nonce for every signature.
 The string representation of the nonce you use in the signature must be lowercase.

示例:c7440396-ce94-471d-905e-601927e6762f

  • timestamp: 毫秒
timestamp
A timestamp your server generates in UNIX time format, in milliseconds. The timestamp keeps the offer active for 24 hours.

当创建 SKProductDiscount 成功,但是支付时仍提示错误,定睛一看是以下的错误码:

SKErrorInvalidSignature API_AVAILABLE(ios(12.2), macos(10.14.4), watchos(6.2), visionos(1.0)),                             // The cryptographic signature provided is not valid
更具体的报错:
<SKPaymentQueue: 0x2808a8d90>: Payment completed with error: Error Domain=ASDServerErrorDomain Code=3903 "无法购买" UserInfo={NSLocalizedDescription=无法购买}
未能完成操作。(SKErrorDomain错误12。)

说明生成的签名有误!!这时候可以按照以下顺序验证:

1、签名生成时使用的私钥是否正确

2、私钥使用是正确时,一步步检查签名生成步骤,之前遇到过Server由于编码问题导致签名分隔符生成有问题。

(网上有一套python生成签名代码,已验证没有问题)

import json

import uuid

import time

import hashlib

import base64

from ecdsa import SigningKey

from ecdsa.util import sigencode_der

from pyasn1.codec.der import decoder
from pyasn1_modules import rfc5208, rfc2459

bundle_id = ''

key_id = ''

product = ''

offer = '' # This is the code set in ASC

application_username = '' # 为空时就直接空着就行

nonce = uuid.uuid4()  # 必须是小写

timestamp =  int(round(time.time() * 1000))

# 之前遇到过这一步生成的payload有问题,所以可以一步步和业务侧比对生成的结果
payload = '\u2063'.join([bundle_id,

                        key_id,

                        product,

                        offer,
                        
                        application_username,

                        str(nonce), 

                        str(timestamp)])

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)
问题2:支付过程中提示不符合享受优惠的条件。

不符合享受优惠的条件

出现以上错误的原因可能是:

当促销优惠生效时,并没有生效的推介优惠。这时候由于业务侧和Apple本身存在不同的账号系统,所以无法判断当前支付的apple id是否为新用户,也就是无法判断该apple id是否能使用促销优惠。

结果就是业务侧判断新用户的apple id满足了促销优惠的条件!!导致用户无法支付...

怎么办

其实从业务侧而言,优惠活动大部分都针对所有的群体,所以正常而言只需要注意在相同时间段内,同时存在有效的推介优惠和促销优惠即可。当然如果接入了StoreKit2的话,处理就非常简单了

优惠代码

该优惠类型是需要发版开发的。有两种实现方式:

一次性代码:适合小规模分发,有助于控制兑换人数

  • 可自行设置有效时间,有效性最长为6个月
  • 只能兑换一次,每个代码都是唯一的。
  • 兑换方式:1、通过兑换网址 2、通过app开发 3、App Store中输入代码进行兑换

自定代码:尤其适合大型的营销活动,可以轻松的向大量用户提供优惠

  • 可自行设置有效时间,有效性最长为6个月
  • 自行输入的优惠代码(如 SPRINGRPOMO),可供多名用户兑换
  • 兑换方式:1、通过兑换网址 2、通过app开发

官方介绍很简单,实际开发时也并不难:

1、官方文档: App Store Connect配置优惠代码

优惠代码的兑换方式中有一种可通过兑换网址进行的方式,这个兑换网址是App Store Connect生成优惠代码时,一起生成的:

内购优惠代码.png

2、配置完优惠代码后,实现调起App内兑换优惠代码入口

// Call this method to have StoreKit present a sheet enabling the user to redeem codes provided by your app.

- (**void**)presentCodeRedemptionSheet API_AVAILABLE(ios(14.0), visionos(1.0)) API_UNAVAILABLE(tvos, macos, watchos);

当用户支付后,可通过收据中标识报价的字段 offer_code_ref_name 判断。

App内兑换优惠代码的开发流程就结束了!!其中值得业务侧注意和关注的一点是:

当处于推介优惠有效期内时,优惠代码和推介优惠是否需要互斥?

在后台配置优惠代码时,可设置该优惠代码是否与推介优惠互斥,设置选项说明如下:

image.png

  • Redeem Introductory Offer and offer code
New users will redeem your app’s introductory offer first, then automatically renew to the offer they redeemed with the code.
新用户首先兑换应用的推介优惠,然后再自动续费时使用优惠代码。
  • Only Redeem Offer Code
New users will redeem the offer code first, then automatically renew to the standard subscription price without using the introductory offer. If they cancel and resubscribe at any point, they’re still eligible to redeem an introductory offer.
新用户首先将兑换优惠代码,然后自动续费时不会使用推介优惠,而是使用标准的定位价格。如果用户此时取消并且之后重新订阅,他们仍然有兑换推荐优惠的资格。

现在在开发内购时,可以很方便的使用 StoreKit Testing 进行测试,比如设置各种优惠活动、退款、购买中断等。可是如果不了解优惠活动原理,不了解如何通过StoreKit Testing测试,没有升级到StoreKit2,那么测试StoreKit内购并不是一件简单的事情。

好牛