所有文章
Deep dive阅读需 7 分钟

你的 App 可以直接弹出应用内退款申请表,客户点击提交后 Apple 会这样处理

Apple 的应用内退款申请让客户无需离开你的 App 就能申请退款,表单由 Apple 构建并审核。本文讲清 beginRefundRequest 返回什么、它会在你的服务器上启动哪些 CONSUMPTION_REQUEST 和 48 小时计时,以及这个按钮是否值得上线。

一只手拿着智能手机,屏幕显示账户设置页面,旁边放着纸质收据和一枚硬币,展示客户无需离开 App 即可发起的应用内退款申请

要点

  • Apple 的 beginRefundRequest 是一个 StoreKit 2 方法,它会在你的 App 内弹出 Apple 自己的退款表单。客户看到自己的购买详情和一组原因代码,选择其中一个,申请便提交给 Apple。表单不由你构建,结果也不由你决定。
  • 该调用返回 success 或 userCancelled 状态,或抛出 duplicateRequest 或 failed 错误。success 状态只表示 App Store 收到了申请,并不代表已批准。切勿在 success 时在界面上显示退款已确认。
  • 客户提交后,Apple 最多需要 48 小时来批准或拒绝。对于消耗型购买项目,它会先向你的服务器发送一个 CONSUMPTION_REQUEST,如果客户已同意,你有惯常的 12 小时用使用数据来回应。
  • 结果会以 App Store Server Notification 的形式送达你的服务器,也就是你早已接收的那条通知流。批准对应 REFUND 通知,拒绝对应 REFUND_DECLINED。应用内申请进入这条通知流的方式,与客户在 Apple 的 reportaproblem 页面发起的退款完全一致。
  • 该按钮从 iOS 15 和 iPadOS 15、Mac Catalyst 15 以及 visionOS 1 起可用,因此任何面向这些版本的 App 今天就能弹出它。
  • 从财务角度看,一笔你能申辩的退款,胜过一笔你无法申辩的拒付。把客户留在 Apple 的流程里会触发一个你能回应的 CONSUMPTION_REQUEST,而不是一笔最终生效、还要收取手续费的银行拒付。
  • Apple 的放置建议是从账户设置或帮助菜单里调用它,而非放在购买页面,这样不满的客户能找到它,又不会向其他所有人宣传退款。

Apple 允许客户在完全不离开你的 App 的情况下申请退款。一个 StoreKit 调用 beginRefundRequest 就会在你的界面内弹出 Apple 自己的退款表单,客户选好一个原因,申请便交给 Apple 审核。表单不由你构建,钱款你碰不到,结果也不由你决定。你得到的,是把退款入口放在心怀不满的客户所在之处的能力,而不是把他们流失给银行。这就是应用内退款申请,在你决定是否上线这个按钮之前,值得先弄清它是什么。

下面这部分才关系到你的收入。这个按钮本身不会退任何钱。它开启一次申请,Apple 最多花 48 小时来批准或拒绝,而对于消耗型购买,它会先向你的服务器发出一个 CONSUMPTION_REQUEST。所以这张表单不是白送。它是一个漏斗,把客户导入你早已能施加影响的那套退款审核,还能在争议变成你无法申辩的拒付之前,把它从银行卡网络里拉回来。

应用内退款申请表单到底是什么

beginRefundRequest 是一个 StoreKit 2 方法,它会在一个 window scene 中为某笔交易弹出退款申请表单。签名很简短:func beginRefundRequest(in scene: UIWindowScene) async throws -> Transaction.RefundRequestStatus。当你调用它时,系统会显示一张表单,列出客户的购买详情和一组供其选择的原因代码。这套界面由 Apple 构建和掌控。你只提供 scene 和交易,别无其他。

Apple 对放置位置的建议很明确。从账户设置或帮助菜单里调用这个函数,这样想退款的客户就能在他们寻求支持的地方找到它。它随 iOS 15 和 iPadOS 15、Mac Catalyst 15 以及 visionOS 1 一同发布,因此任何面向这些版本的 App 今天就能弹出它。

打开表单的两种方式

有两个入口。你可以对一笔你已持有的具体交易调用 beginRefundRequest(in:),这会把表单限定在那一笔购买上。当你希望客户退掉某个产品的购买时,也可以按产品标识符打开表单。无论哪种方式,表单、原因列表和裁决都归 Apple 所有。你的职责止于弹出它并读取结果。

调用返回什么,以及哪里可能出错

这个方法是 async throws,因此它要么返回一个状态,要么抛出一个错误。两者都是很短的清单,也都值得处理,好让表单关闭后你的界面能说出真实的情况。

结果类型含义
successRefundRequestStatusApp Store 已收到退款申请。它已提交,尚未批准
userCancelledRefundRequestStatus客户未提交便关闭了表单。什么都没发送
duplicateRequestRefundRequestErrorApp Store 已存在这笔购买的退款申请
failedRefundRequestError提交本身失败了。让客户再试一次

客户点击提交后,你的服务器上会发生什么

表单关闭是流程的开始,而非结束。Apple 会审核申请,最多花 48 小时批准或拒绝。对于消耗型应用内购买,在裁决之前,App Store 会向你的服务器发送一个 CONSUMPTION_REQUEST 通知,索要使用数据。如果客户已同意分享该数据,你通过 Send Consumption Information 接口回应。如果他们未同意,Apple 自己的指示是根本不要回应这条通知。

一旦 Apple 作出裁决,结果便以 App Store Server Notification 的形式落到你的服务器上。这就是你早已接收的那条通知流,应用内申请进入它的方式,与客户在 Apple 的 reportaproblem 页面发起的退款完全一致。申请虽在你的 App 内发起,处理方式却毫无不同。

阶段触发什么你的动作计时
客户提交表单beginRefundRequest 返回 success记录下来,显示待处理,而非已退款即时
仅限消耗型,Apple 先询问CONSUMPTION_REQUEST 通知若客户已同意则发送消耗数据,否则保持沉默12 小时内回应
Apple 批准REFUND 通知撤销该笔交易的权益最多 48 小时裁决
Apple 拒绝REFUND_DECLINED 通知保留这笔销售,什么都不改最多 48 小时裁决
一只沙漏摆在智能手机和纸质收据旁边,展示客户向 Apple 提交应用内退款申请后最长 48 小时的等待

这个按钮让你付出什么,又能替你省下什么

一笔你能申辩的退款,胜过一笔你无法申辩的拒付

在你的 App 内找不到退款入口的客户不会就此放弃。他们会去找银行。银行卡拒付在银行处是最终生效的,它要收取一笔争议手续费,还会把裁决权从你和 Apple 双方手中都拿走。应用内退款申请把同一位客户留在 Apple 的系统里,在那里,一笔消耗型购买会触发一个你能回应的 CONSUMPTION_REQUEST,以及一个你能施加影响的裁决。用一笔可申辩的 Apple 审核换掉一笔不可申辩的拒付,就是这个按钮全部的财务理由。

你正在降低退款的门槛

诚实的另一面是,一个显眼的、一键可达的退款入口,会比一封埋在深处的支持邮件带来更多退款申请。其中有些申请原本根本不会发生。这是实实在在的代价,也正因如此,Apple 才告诉你把入口放在账户设置或帮助菜单里,而不是购买页面上。你想让已经不满的客户找到它,而不是让仅仅好奇的客户找到它。

从头到尾一直在烧钱的成本,是继续为一个已退款的账户提供服务

无论裁决走向如何,交付这一侧的计价表都会一直转,直到你根据结果采取行动。一个已退款的权益每多存活一小时,你就得继续为它背后的真实成本买单:算力、模型 API 调用、存储,以及任何与该客户用量挂钩的创作者或合作方分成。应用内申请改变不了这一点。收到 REFUND 通知时立即撤销才能改变。这个按钮有多便宜,取决于你如何处理它最终产生的那条通知。

你该不该上线应用内退款申请

把它放在支持所在之处,而非销售所在之处

遵循 Apple 的放置建议。账户设置和帮助菜单才是合适的归处。付费墙旁边的一个退款链接会训练人们期待拿回钱,还会招来那种你本无需提供的好奇型退款。

在信任它之前,先在沙盒里测试完整流程

你可以在沙盒和 Xcode 的 StoreKit 测试中模拟整条路径,把一个申请从待处理推向已批准或已拒绝。批准会向你的服务器投递一个 REFUND 通知,拒绝则投递 REFUND_DECLINED,因此你能在真实客户点击提交之前,先证明你的处理程序反应正确。

处理好每一种结果,绝不夸大其词

在 success 时显示待处理,在 failed 时提供重试,在 userCancelled 时说明什么都没变,并把 duplicateRequest 当作一条平静的提示,表示客户先前的申请仍然有效。唯一会造成伤害的错误,是在你手里只有一份已提交的申请时,却告诉客户退款已经办妥。

RefundHalt 如何处理后续

应用内表单是 Apple 的。它之后的一切是你的,而那正是 RefundHalt 负责运行的部分。当客户从你的 App 内提交退款时,RefundHalt 会为消耗型购买捕获 CONSUMPTION_REQUEST,并在 12 小时窗口内用有助于 Apple 裁决的用量证据回应它。当 Apple 作出裁决,它会在 REFUND 时撤销、在 REFUND_DECLINED 时原封不动地保留访问权,每一步都精确对应那笔交易。你既能提供更友好的应用内退款入口,又不必把审核、证据或撤销留给手忙脚乱的人工处理。

常见问题解答

beginRefundRequest 是做什么的?
它会在你的 App 内为某笔具体交易弹出 Apple 的退款申请表单。客户看到自己的购买详情和一组原因代码,选择其中一个,申请便交给 Apple。该方法返回 success 或 userCancelled 状态,或抛出 duplicateRequest 或 failed。它本身不会退掉这笔购买,因为 Apple 会审核申请,最多花 48 小时裁决。
应用内退款申请会立即退钱吗?
不会。success 结果表示 App Store 收到了申请,而非已批准。Apple 最多花 48 小时批准或拒绝,对于消耗型购买,它会先通过 CONSUMPTION_REQUEST 通知向你的服务器索要使用数据。在 success 时给客户显示一个待处理状态,绝不要显示退款已确认。
哪个 iOS 版本支持应用内退款申请?
iOS 15 和 iPadOS 15、Mac Catalyst 15 以及 visionOS 1。StoreKit 2 方法 beginRefundRequest(in:) 从这些版本起可用,因此任何面向 iOS 15 或更高版本的 App 都能从 App 内弹出 Apple 的退款表单。
我该把应用内退款按钮放在哪里?
Apple 的建议是从账户设置或帮助菜单里调用它,而非购买页或付费墙页面。这样退款入口就放在了不满客户寻求支持的地方,又不会向那些本不打算退款的客户宣传退款。
应用内退款是否优于客户去联系银行?
就你的收入而言,通常是的。银行拒付是最终生效的,还要收取手续费,并把 Apple 和你都排除在裁决之外。应用内退款申请把客户留在 Apple 的流程里,在那里,一笔消耗型购买会触发一个你能回应的 CONSUMPTION_REQUEST,以及一个你能施加影响的审核。一笔可申辩的退款胜过一笔不可申辩的拒付。

来源和延伸阅读

RefundHalt

App Store 和 Google Play 退款自动驾驶

继续阅读

下一项退款请求已经在路上。

读完又一封关于未能申辩退款的支持邮件所需的时间,足够您完成 RefundHalt 设置。