Highlight
App Store 的订阅优惠体系已经有了 introductory offers(拉新)、promotional offers(促活)和 offer codes(营销渠道分发)三种形态。今年的更新分两个方向:一是给现有 API 做了信息补全,二是推出全新的 win-back offers 用来挽回流失用户。
核心内容
订阅产品的核心难题是用户流失。一个用户取消自动续期后,开发者几乎没有任何手段再次触达他——promotional offers 需要自己验证资格、自己发推送,转化链路长且成本高。Apple 今年给出的解法是 win-back offers:一种全新的优惠类型,专门面向”订阅已过期且关闭了自动续期”的流失用户。
win-back offers 和以往优惠最大的区别在于:Apple 替你做用户分群和渠道分发。你在 App Store Connect 里配置资格条件(付费时长、流失时长、两次优惠间隔),Apple 自动匹配符合条件的用户,并在 App Store 的产品页、Today 标签页、订阅设置页等位置展示优惠卡片。用户点卡、确认支付,一气呵成。你甚至可以在 App Store Connect 里为 win-back offer 配置优先级,Apple 会用它来个性化推荐。
API 层面也有配套更新。Transaction 和 RenewalInfo 两个结构体新增了 offer 字段,把优惠 ID、类型、支付模式统一放在一起;StoreKit Views 新增了 subscriptionPromotionalOffer 和 preferredSubscriptionOffer 两个 View Modifier,让你可以直接在 SubscriptionStoreView 里控制展示哪个优惠;macOS 15 终于支持了 offer code redemption sheet。
详细内容
Transaction 和 RenewalInfo 新增 offer 字段
Transaction 结构体在 iOS 17.2 新增了 offer 成员,类型为可选值,包含 id、type、paymentMode 三个字段。只有当用户购买时使用了优惠,这个字段才会被填充。RenewalInfo 中的同名字段则从 iOS 18.0 起可用,表示下一个续期周期将应用的优惠(02:02)。
在 App Store Server API 侧,对应的字段是 offerIdentifier、offerType(已有)和新增的 offerDiscountType(新字段,历史交易也会回填)。offerDiscountType 的值对应 StoreKit 中的 paymentMode,区分 free trial / pay-as-you-go / pay-up-front(02:53)。
macOS 15 支持 offer code redemption sheet
// Present offer code redemption sheet on macOS - SwiftUI API
import SwiftUI
import StoreKit
struct MyView: View {
@State var showOfferCodeRedemption: Bool = false
var body: some View {
Button("Redeem Code") {
showOfferCodeRedemption = true
}
.offerCodeRedemption(isPresented: $showOfferCodeRedemption) { result in
// Handle result
}
}
}
@State var showOfferCodeRedemption:用布尔状态控制兑换弹窗的显示/隐藏.offerCodeRedemption(isPresented:):macOS 15 起与 iOS 同 API,Sheet 自动处理兑换流程result闭包:拿到兑换结果后做后续处理(04:25)
preferredSubscriptionOffer:在 SubscriptionStoreView 中选择展示哪个优惠
// Choose preferred offer in a SubscriptionStoreView
import SwiftUI
import StoreKit
struct MyView: View {
let groupID: String
var body: some View {
SubscriptionStoreView(groupID: groupID)
.preferredSubscriptionOffer { product, subscription, eligibleOffers in
let freeTrialOffer = eligibleOffers
.filter { $0.paymentMode == .freeTrial }
.max { lhs, rhs in
lhs.period.value < rhs.period.value
}
return freeTrialOffer ?? eligibleOffers.first
}
}
}
preferredSubscriptionOffer:闭包参数为 product、subscription、eligibleOffers,StoreKit 自动传入当前用户符合资格的所有优惠$0.paymentMode == .freeTrial:筛选出免费试用类优惠.max { lhs.period.value < rhs.period.value }:找周期最长的免费试用freeTrialOffer ?? eligibleOffers.first:没找到免费试用就回退到第一个可用优惠(20:15)
检查订阅资格与 win-back offer 展示逻辑
// Check subscription entitlement and offer eligibility
import StoreKit
func shouldShowMerchandising(
for groupID: String,
productIDs: [Product.ID]
) async throws -> MerchandisingVisibility {
// Get subscription status
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
// Check if the customer is already entitled to the subscription
let entitlement = SubscriptionEntitlement(for: statuses)
if entitlement.autoRenewalEnabled {
return .hidden
}
// Check for offers to show in merchandising UI
let products = try await Product.products(for: productIDs)
let isEligibleForIntroOffer = await Product.SubscriptionInfo.isEligibleForIntroOffer(for: groupID)
if isEligibleForIntroOffer {
let subscriptions = products.map {
($0, $0.subscription?.introductoryOffer)
}
return .visible(subscriptions)
}
// Check for eligible win-back offers
let purchasedStatus = statuses.first {
$0.transaction.unsafePayloadValue.ownershipType == .purchased
}
let renewalInfo = try purchasedStatus?.renewalInfo.payloadValue
let bestWinBackOfferID = renewalInfo?.eligibleWinBackOfferIDs.first
// Return the product with the offer if there is one
if let bestWinBackOfferID {
let subscriptions: [(Product, Product.SubscriptionOffer?)] = products.map {
let winBackOffer = $0.subscription?.winBackOffers.first {
$0.id == bestWinBackOfferID
}
return ($0, winBackOffer)
}
return .visible(subscriptions)
}
// Only return the product if there is no offer
return .visible(products.map { ($0, nil) })
}
Product.SubscriptionInfo.status(for:):拉取订阅组的当前状态SubscriptionEntitlement:自定义结构体,从状态列表中判断是否享有权益、是否开启自动续期isEligibleForIntroOffer:先查 introductory offer 资格,新用户优先展示试用$0.transaction.unsafePayloadValue.ownershipType == .purchased:只看”自己付费购买”的记录,排除 Family SharingrenewalInfo?.eligibleWinBackOfferIDs.first:Apple 返回的列表第一个就是最佳 win-back offer$0.subscription?.winBackOffers.first { $0.id == bestWinBackOfferID }:用 ID 在产品信息中匹配完整 offer 对象(23:05)
将 win-back offer 添加到购买
// Add a win-back offer to a purchase
import StoreKit
func purchase(
_ product: Product,
with offer: Product.SubscriptionOffer?
) async throws {
// Prepare the purchase options
var purchaseOptions: Set<Product.PurchaseOption> = []
// Add win-back offer to the purchase
if let offer, offer.type == .winBack {
purchaseOptions.insert(.winBackOffer(offer))
}
// Make the purchase
try await product.purchase(options: purchaseOptions)
}
.winBackOffer(offer):iOS 18.0 新增的 PurchaseOption,把 win-back offer 对象传入购买流程product.purchase(options:):App Store 会在弹出支付 Sheet 前自动验证用户资格(25:26)
Streamlined Purchasing 和 PurchaseIntent
如果你在 App Store Connect 开启了 App Store 推广(默认开启),用户可以在 App Store 内直接完成 win-back offer 的购买,不用先打开 app。这个行为叫 streamlined purchasing。如果你需要在购买前执行自定义逻辑(比如展示引导页),可以在 App Store Connect 中关闭它,然后在 app 里用 PurchaseIntent API 接收用户在 App Store 发起的购买(28:48)。PurchaseIntent 在 iOS 18.0 新增了 offer 字段,仅当包含 win-back offer 时才会填充。
核心启发
-
做什么:为流失用户配置 win-back offers 分群策略。用”付费订阅时长 ≥ 3 个月 + 流失 2-6 个月”作为首批条件,在 App Store Connect 中创建一个免费试用型 win-back offer。为什么值得做:Apple 自动分发,你不用自己写资格验证和推送逻辑,覆盖面比 promotional offers 更广。怎么开始:在 App Store Connect 的订阅定价页找到 Win-Back Offers 标签,点击 Create Offer,配置资格条件和价格。
-
做什么:在 SubscriptionStoreView 中接入 preferredSubscriptionOffer,让 StoreKit Views 自动展示最优优惠。为什么值得做:当用户同时符合 introductory offer 和 win-back offer 时,你需要决定展示哪一个,这个 View Modifier 让你在几行代码里完成选择逻辑。怎么开始:在已有的 SubscriptionStoreView 上添加
.preferredSubscriptionOffer修饰符,闭包里按业务优先级返回合适的 SubscriptionOffer。 -
做什么:迁移 Transaction 和 RenewalInfo 中的优惠检查到新的
offer字段。为什么值得做:offer字段把优惠 ID、类型、支付模式统一在一起,是 Apple 推荐的查询方式,老的散列字段未来可能被废弃。怎么开始:将服务端解析 JWSTransaction 时对offerIdentifier+offerType的读取改为同时读取offerDiscountType,客户端侧将transaction.offer作为首选检查入口。 -
做什么:为 macOS 版应用添加 offer code redemption sheet 支持。为什么值得做:macOS 15 终于补上了这个能力,之前 macOS 用户只能手动输入优惠码,体验差。怎么开始:用
.offerCodeRedemption(isPresented:)SwiftUI 修饰符,代码与 iOS 完全一致。
关联 Session
- What’s new in StoreKit and In-App Purchase — StoreKit Views 新增控制样式、Transaction 新字段、StoreKit Testing 改进
- Explore App Store server APIs for In-App Purchase — 服务端新增 offerDiscountType 字段、消费信息上报和通知类型更新
- What’s new in App Store Connect — App Store Connect 的推荐提名、营销素材生成和 TestFlight 改进
评论
GitHub Issues · utterances