所有文章
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。