Highlight
Session 以 Food Truck(甜甜圈外卖 App)为贯穿案例,介绍了 StoreKit 今年的多项更新。
核心内容
Food Truck 原来是一个付费 App。用户付过 4.99 美元后,可以配送甜甜圈、看基础社交动态、查看年度销售图表。后来作者把它改成免费下载,年度销售图表变成一次性内购,高级社交动态变成订阅。
问题马上出现。Alice 在 2.5 版本付费买过 App,Bob 在 8.2 版本免费下载。Alice 不应该因为商业模式变化失去原来买到的功能,Bob 则应该完成内购后再解锁高级内容。
StoreKit 今年给出的核心答案是 App Transaction。它把 App 本身的购买信息从旧收据里拆出来,用 JWS 签名,StoreKit 会自动验证。开发者可以读取 originalAppVersion,判断用户第一次下载 App 时的版本,再决定是否给老付费用户保留权益。
这场 session 的另一半是服务端。App Store Server API 的 Get Transaction History 增加了排序和过滤能力;App Store Server Notifications V2 增加测试通知和通知历史接口。它们解决的是服务器接入和故障恢复的问题:先确认通知 URL 是否能收到 TEST 通知,再在宕机后用历史接口补回漏掉的 V2 通知。
详细内容
用 App Transaction 识别老付费用户
(02:05)App Transaction 是验证 App 购买记录的新 API。它表示当前设备上这份 App 的签名购买信息,使用 JWS 签名,取代旧版 App Receipt 里的 App detail 部分。StoreKit 会自动验证,也允许开发者自行验证 JWS 签名。
(06:53)Food Truck 的迁移逻辑可以拆成三步:取 App Transaction,处理验证结果,读取原始 App 版本。
let result = await AppTransaction.shared
switch result {
case .unverified:
// 提示用户无法验证 App 购买,并提供刷新入口。
case .verified(let appTransaction):
let originalAppVersion = appTransaction.originalAppVersion
// 用 originalAppVersion 判断用户是否在 8.0 之前购买过付费版。
}
关键点:
AppTransaction.shared是 transcript 中给出的入口,用来获取 App Transaction 的VerificationResult。.unverified表示 StoreKit 没有完成验证,此时 session 建议提供刷新 App Transaction 的用户入口。.verified分支里才读取appTransaction,避免把未验证数据当作权益依据。originalAppVersion表示用户第一次下载这份 App 时的版本。Food Truck 用它判断是否早于 8.0。- 刷新 App Transaction 会触发用户认证,所以只能响应用户操作,不能后台静默调用。
这个 API 的价值不在“验证”两个字本身,而在迁移场景。付费 App 改成免费加内购后,旧用户和新用户混在一起。originalAppVersion 给了一个可解释的分界点:8.0 之前购买过 App 的用户保留年度销售图表,8.0 之后免费下载的用户按当前内购权益解锁。
StoreKit 模型新增可直接使用的属性
(09:01)StoreKit 模型新增三个属性:priceLocale、environment、recentSubscriptionStartDate。用 Xcode 14 构建后,这些属性可以回传到上一代系统,因为实现会编译进 App。
product.priceLocale
transaction.environment
renewalInfo.environment
subscriptionInfo.recentSubscriptionStartDate
关键点:
priceLocale用在商品价格计算之后的本地化格式化,例如把年度订阅价格换算成每月价格后继续使用 App Store 的价格区域。environment出现在 Transaction 和 Renewal Info 上,用来区分 Xcode、sandbox、production,避免把测试交易写进生产分析。recentSubscriptionStartDate表示最近一段连续订阅期的起点。- “连续订阅”允许两段订阅之间最多 60 天的空档,所以它不能用来计算用户已经订阅了多少天。
(13:21)在旧系统上的 StoreKit Testing in Xcode 里,这些属性可能返回 sentinel values。priceLocale 是 xx_XX,environment 是空字符串,recentSubscriptionStartDate 是 Date.distantPast。这些占位值只表示本地测试环境拿不到真实值,不应该进入业务判断。
SwiftUI 入口:优惠码兑换和评价请求
(15:01)订阅优惠码兑换变成 SwiftUI 友好的 view modifier。它需要一个绑定布尔值来开始展示兑换 sheet,sheet 关闭后会返回是否成功展示的结果。用户兑换成功产生的交易会进入 transaction listener,所以 App 启动时就要设置交易监听。
// SwiftUI offer code redemption sheet
// 使用绑定布尔值触发展示;兑换产生的交易进入 transaction listener。
offerCodeRedemption(isPresented: $isPresentingOfferCodeRedemption)
关键点:
- transcript 明确说这个 view modifier “just needs a binding Boolean”。
- 兑换 sheet 负责展示优惠码流程,交易结果不从 sheet 直接返回,而是进入 transaction listener。
- 这个能力从 iOS 16 开始可用。
(16:44)评价请求也有 SwiftUI 入口。SwiftUI 环境里新增 requestReview,它提供 RequestReviewAction,在合适时机把这个 action 当函数调用即可。
@Environment(\.requestReview) private var requestReview
requestReview()
关键点:
requestReview来自 SwiftUI environment value。- 它拿到的是
RequestReviewAction。 - 系统最多在 365 天内向同一用户显示三次提示。
- 不要因为用户点了某个按钮就强行请求评价;session 建议选择正向时刻,例如购买完成或游戏过关。
StoreKit Messages 控制 App Store 消息出现的时机
(17:48)StoreKit message 是由 App Store 分发的 sheet,用来向用户显示重要信息。典型原因是订阅涨价需要用户同意。默认情况下,App 进入前台时 StoreKit 会检查待展示消息,并直接覆盖在 App 上。
Food Truck 的例子是甜甜圈编辑器。用户正在编辑配送内容时,突然弹出价格同意 sheet 会打断操作。解决办法是在需要控制展示时机的 view 里设置 message listener,把消息先存起来,等离开编辑器后再显示。
// 在需要延迟展示消息的 view 中监听 StoreKit messages。
// 收到消息后先加入 pendingMessages,稍后再显示。
pendingMessages.append(message)
// 在合适的父 view 中使用 SwiftUI 环境里的 displayStoreKitMessage 展示。
displayStoreKitMessage(message)
关键点:
- session 说明待处理消息会在 App 每次进入前台时发送给 App。
- 如果没有设置 message listener,StoreKit 会立刻展示 sheet。
- 已监听的场景可以把消息收进
pendingMessages,等用户离开敏感流程再展示。 displayStoreKitMessage是 SwiftUI environment value,拿到的是DisplayMessageAction。- 同一条消息可能多次送达,因为消息只有展示给用户后才会被标记为已读。
迁移旧版 applicationUsername 到 appAccountToken
(22:23)旧版 StoreKit 用 applicationUsername 把交易和开发者自己的用户账号关联起来。它接受任意字符串,但 Apple 建议传 UUID 字符串;如果不是 UUID 字符串,StoreKit 不保证会持久保存。
// 旧版 StoreKit 支付时设置 UUID 字符串形式的 applicationUsername。
applicationUsername = userAccountUUID.uuidString
// 现代 StoreKit APIs 中对应的是 appAccountToken,要求 UUID 格式。
appAccountToken = userAccountUUID
关键点:
applicationUsername是开发者创建的字符串,用来关联交易和自有账号系统。- 传 UUID 字符串时,App Store server 会把它保存为
appAccountToken。 - 之后在 App Store Server API 的 signed transaction info 和 V2 App Store Server Notifications 里都能看到这个 UUID。
- 这让旧版 StoreKit 迁移到现代 StoreKit APIs 时,不必丢掉已有交易和账号的关联关系。
Get Transaction History 支持更精确的增量同步
(30:33)Get Transaction History 原本可以用 originalTransactionId 拉取某个用户的完整购买历史。响应分页,每页最多 20 条 signed transactions,并带回 revision token。后续请求带上 revision,就能继续拉下一页。
GET /inApps/v1/history/{transactionId}?revision={revision}
关键点:
transactionId是这个接口的路径参数;在本 session 的迁移语境里,查询某个用户购买历史时可以传该用户的 original transaction ID。revision用来请求下一页,也可以存起来做后续增量同步。hasMore为 true 时继续请求下一页。- 结果按 modified date 排序,所以被撤销的旧交易可能因为
revocationDate和revocationReason更新而重新出现在后续结果中。
(33:20)今年的增强是排序和过滤。开发者可以从请求一开始就缩小范围,减少服务器处理时间和网络请求次数。
GET /inApps/v1/history/{transactionId}?productType=NON_CONSUMABLE&startDate=1640995200000&excludeRevoked=true
关键点:
productType=NON_CONSUMABLE对应 session 里的例子:只取非消耗型内购。startDate用毫秒时间戳限制起始时间。excludeRevoked=true排除已撤销交易。- 后续分页请求必须保留完全相同的查询参数,只替换或追加上一页返回的
revision。 productType、productId、subscriptionGroupIdentifier支持多值过滤,可以重复定义参数。
用测试通知验证服务器配置
(36:15)接入 App Store Server Notifications V2 时,过去常见做法是创建 sandbox 账号,完成一次订阅购买,再看服务器能不能收到 SUBSCRIBED / INITIAL_BUY。这一步混入了太多变量:账号、购买流程、服务器 URL、回调处理都可能出问题。
WWDC22 新增 Request a Test Notification endpoint。开发者可以直接请求 App Store server 向 App Store Connect 里配置的 URL 发送 TEST 类型的 V2 notification。
POST /inApps/v1/notifications/test
关键点:
- 这个接口可在 sandbox 和 production 调用,用来测试对应环境保存的通知 URL。
- 成功调用会返回 HTTP 200。
- 响应里包含
testNotificationToken,用来标识这次测试通知。 - 真正的
TESTnotification 是异步发送的,HTTP 200 只表示请求已经提交。 - TEST payload 没有交易相关字段,尤其没有
signedTransactionInfo。
(40:12)如果 TEST 通知没有到达,可以用 Get Test Notification Status endpoint 查询这次发送的结果。
GET /inApps/v1/notifications/test/{testNotificationToken}
关键点:
- 路径里的
testNotificationToken来自前一个接口。 - 响应的
signedPayload是 App Store server 尝试发送的 TEST notification payload。 firstSendAttemptResult表示第一次发送结果。SUCCESS表示 App Store server 从开发者服务器收到了 HTTP 200。- 失败时会返回具体错误值,帮助定位 URL、证书、网络或响应码问题。
用通知历史恢复服务器宕机期间的遗漏
(42:17)App Store server 原本会在生产环境失败后最多重试 5 次。长时间宕机可能错过最后一次重试;短暂故障也可能漏掉少量通知,而且开发者不知道漏了哪些用户。
Get Notification History endpoint 解决的是恢复问题。它可以按日期范围拉取 App 的 V2 notification 历史,无论当时开发者服务器是否成功收到。
POST /inApps/v1/notifications/history
{
"startDate": 1654041600000,
"endDate": 1656633600000,
"notificationType": "DID_RENEW",
"notificationSubtype": "BILLING_RECOVERY"
}
关键点:
- 请求体包含
startDate和endDate,返回首次尝试发送时间落在该窗口内的通知。 - 最早只能取请求日期之前六个月内的通知历史。
- 可以用
notificationType和notificationSubtype过滤。 - 也可以提供
originalTransactionId,只查某个用户的通知历史。 - 后续分页请求通过 query parameter 传
paginationToken,请求体保持不变。 - 响应的
notificationHistory最多包含 20 条通知,按最旧优先排列。 - 每条记录里的
signedPayload与原始通知 payload 相同,firstSendAttemptResult可以帮助分析当时为什么漏收。
核心启发
-
做什么:给付费转免费 App 增加“老用户权益保留”检查。 为什么值得做:App Transaction 提供
originalAppVersion,可以识别用户第一次下载 App 的版本。 怎么开始:在启动后的权益刷新流程里调用AppTransaction.shared,验证通过后用版本阈值决定是否解锁历史付费内容。 -
做什么:把 StoreKit 交易上报按环境分流。 为什么值得做:
environment能区分 Xcode、sandbox、production,避免测试数据污染收入分析。 怎么开始:在发送交易或 renewal info 到服务器前读取environment,把非 production 数据写入测试表或直接跳过生产统计。 -
做什么:为订阅 App 做“通知 URL 自检”后台工具。 为什么值得做:Request a Test Notification 和 Get Test Notification Status 可以独立验证 App Store server 是否能访问你的回调地址。 怎么开始:在运维面板里触发
POST /inApps/v1/notifications/test,保存testNotificationToken,再轮询 status endpoint 显示发送结果。 -
做什么:给服务器宕机后增加通知补偿任务。 为什么值得做:Get Notification History 能拉回六个月内的 V2 notification 历史,覆盖重试失败或短暂故障漏掉的事件。 怎么开始:记录故障起止时间,调用通知历史接口,逐条解码
signedPayload,按notificationType和交易 ID 幂等更新用户权益。 -
做什么:在编辑、结算、游戏关卡等关键流程延迟展示 StoreKit message。 为什么值得做:价格上涨同意 sheet 这类 App Store 消息很重要,但默认前台展示可能打断用户操作。 怎么开始:在敏感 view 中设置 message listener,把消息写入
pendingMessages,流程结束后再用displayStoreKitMessage展示。
关联 Session
- Explore in-app purchase integration and migration — 继续讲服务端迁移,包含 JWT、signed transaction 验证和从 verifyReceipt 迁移。
- Implement proactive in-app purchase restore — 讲如何在 App 启动时主动恢复用户订阅和内购权益。
- Discover Benchmarks in App Analytics — 讲 App Store Connect 里的分析和 benchmark,适合和内购转化数据一起看。
评论
GitHub Issues · utterances