App Store 五次退件實錄:條款判讀與回覆紀律

這篇記錄一個 iOS App 被 Apple 退件五次才通過上架的完整過程:退件信寫的理由,跟工程上真正需要修改的地方,中間常常有落差,這篇把每一輪的落差攤開來看。適合正在自己送審 App、尤其是用 Expo/React Native 開發、且 App 裡有廣告 SDK、App 內購買(以下簡稱 IAP)或需要處理隱私問卷的人讀。讀完可以掌握退件信怎麼判讀、回覆信該遵守哪些紀律,以及 IAP 在審查環境裡幾個容易被忽略的坑。

這篇是 追追推推 追星族遠征紀錄 App 複盤 的延伸閱讀,那篇只留摘要,完整的條款對照與教訓收在這裡。

總帳

同一個 1.0.0 版本,前後送審六次,被退五次,第六次才通過:

項目數字
提交次數(1.0.0)6 次
退件次數5 次
通過日期2026-08-28
第一次送審到通過21 天
build 累計編號到 (13),通過的是 (12)
打了但從未送審的 build2 顆
同一顆 build 被退次數(6) 被退 2 次,中間零程式碼變更
審查裝置第 2 到 5 次每次都是 iPad Air 11-inch(M3)

最後一輪只花約兩天,前五輪各花 1 到 6 天不等。真正燒掉時間的不是等待,是前四輪裡有三輪其實解錯了層次,或把因果歸錯了對象。

五次退件,逐一拆解

每一輪都分成 Apple 信裡寫了什麼,跟工程上實際發生了什麼,因為這兩件事常常對不齊。

第一次:被判定含 VPN 功能

Apple 自動判定 binary 含 VPN 功能,要求說明收了什麼資料;但後台,也就是 App Store Connect(以下簡稱 ASC),標的案由是 2.1.0 App Completeness,跟信件內容完全脫鉤。

真正的原因不是程式碼,是靜態連結進 binary 的 Google Mobile Ads SDK,內含網路介面名稱字串,觸發了 Apple 的自動二進位掃描。修法不改程式,直接對送審那顆 .ipa 做二進位檢查證明沒有 VPN 相關符號,當天回覆重新提交。

這輪學到:後台的條款編號可能跟信件內容脫鉤,判讀要以信件文字為準;自動掃描看的是字串不是行為,沒寫的功能,依賴的 SDK 可能替你「寫」進了 binary。

第二次:被判定上傳了通訊錄

案由是 Guideline 5.1.2,內容是 App 存取裝置資料卻沒有事前告知與同意,信件開頭帶了一個 still 字,暗示這是上一輪沒解決的舊問題。

但這個 App 連通訊錄的用途說明字串都沒有,這封信其實是模板信。最可能的觸發點,是我方在上一輪回覆信裡自己寫了一句「有自家伺服器」,正好構成 5.1.2「上傳並需要同意」這條分支的標準形狀:開發者自承有伺服器,App 裡卻看不到對應的同意畫面。

修法是重打 build,五種語言的用途說明都補上「不會上傳」的句子,回覆信逐條對答,並第一次附上六張權限彈窗截圖佐證。

這輪學到:模板信的具體受詞可能完全錯,但審查員從選單挑出那個分支,代表他相信有東西被上傳,要找的是他為什麼這樣相信,不是反駁受詞本身。另外,回覆信裡多寫的每一句話都是下一輪的攻擊面,這輪的成因很可能是自己在上一輪招來的。

第三次:畫面被切掉、健康資料標示不清

這輪同時中了兩條。Guideline 4 說部分畫面被切掉、按鈕碰不到,信裡沒有截圖也沒點名頁面;Guideline 2.5.1 說用了 HealthKit 卻沒有在介面上清楚標示。

這輪學到:裝置不支援就靜默隱藏功能,是好的工程直覺,但送審時會連合規標示一起藏起來;Apple 給的是症狀不是定位,找到一個確定的實證不代表找齊了,Apple 信裡寫的是複數 screens。

第四次:同一句話被退第二次

5.1.1(iv) 這條說 App 顯示追蹤同意彈窗的時機,晚於使用者已經在系統層選擇不要被追蹤。這輪 Apple 難得直接給了出口句,指明只要把 GDPR 表單排在 App Tracking Transparency(以下簡稱 ATT,蘋果系統層的追蹤同意彈窗)前面就行。原因是廣告初始化函式裡,ATT、Google 的 GDPR 同意彈窗、廣告初始化三步驟順序寫死了,修法就是把兩段對調。

同一輪還有 Guideline 4 逐字重複上一次的內容,只差 iPadOS 版本號不同。上一輪的修法把最外層容器換成可捲動視窗,只讓主要按鈕捲得到,沒讓它在第一屏就完整可見,審查員第一眼看到的仍是被切掉九成的按鈕。

這輪學到最重的一課:同一句話被退第二次,代表上一輪解錯了層次,不是解得不夠,判準要改寫成「審查員在任何狀態下第一眼看到的畫面是正常的」,捲得到不算解決。上一輪回覆信裡逐字寫過的句子,這輪不能再原樣寫一次,等於告訴審查員我沒改。

第五次:購買流程沒觸發

案由是 2.1(b),Apple 描述選了購買選項但付款流程沒有出現,App 直接導向內容,等於沒完成購買就開通了權益。這段描述逐字正確。

成因是 StoreKit 2(Apple 的購買框架)對已經擁有的非消耗型商品再次購買時,會直接回傳成功、不顯示任何付款畫面,而 App 把這個事件無條件當成剛完成一次新購買,開通權益並跳出感謝訊息。

真正的落差不在 Apple,在我方一開始的歸因錯誤:本地重現成功後,一度判定是審查帳號本來就已擁有這個商品,換了全新沙盒帳號重測,症狀完全相同,才推翻這個假設。回去翻本地檔案才挖出真正的根因:TestFlight 的購買紀錄預設記在裝置的真實 Apple ID 上,不是沙盒測試帳號,兩次換沙盒帳號的實驗都忘了先登出 Media & Purchases(裝置設定裡登入購買身分的那組帳號),改到的都是無效變數。正確登出後用全新沙盒帳號測試,付款畫面才正常出現。

這輪學到:重現症狀不等於證實成因,換一個變數症狀照舊,才知道原本歸因的那個變數不是原因;修法必須對「審查員帶著已購買狀態進來」這個前提免疫,因為 ASC 的清除購買紀錄功能只作用於開發者自建的測試帳號,審查員的購買紀錄清不掉。

以下這張圖是這五輪反覆磨出來的共通判讀流程:

flowchart TD
  A[收到退件信] --> B[逐字讀信件內文找開頭句]
  B --> C[對照ASC後台條款碼是否脫鉤]
  C --> D[本機重現或做二進位檢查]
  D --> E{能否證實成因}
  E -- 能 --> F[針對真正成因修法]
  E -- 不能 --> G[選對成因假設不敏感的修法]
  F --> H[回覆信用可驗證的語氣描述]
  G --> H
  H --> I[重新提交]

條款速查

五輪下來,踩到的條款集中在這幾條:

  • 2.1 / 2.1(b) Performance - App Completeness:Apple 常把需要補充資訊的自動訊息也歸在這裡,後台條款碼常跟信件內容脫鉤,2.1(b) 專指 IAP 有 bug。
  • 5.1.2 Legal - Privacy - Data Use and Sharing:至少有兩個獨立分支,一個管追蹤,一個管上傳與同意,要看信件開頭句判斷是哪一支。
  • 5.1.1(iv) Legal - Privacy - Data Collection and Storage:管的是同意流程的次序,以及是否尊重使用者已表達過的選擇。
  • 2.5.1 Performance - Software Requirements:用了 HealthKit 就必須在介面上清楚標示。
  • 4.0 Design Preamble:版面在審查裝置上被擠壓、切掉、按不到。

iPad 相容模式:最貴的一課

不上架 iPad 不是選項。Apple 開發技術支援的說法是:宣告只支援 iPhone 只是告訴 App Store 這是為 iPhone 設計的,iPad 使用者照樣能下載、以相容模式執行;官方文件也明講不要以為在專案裡移除支援目標裝置就能阻止該類型下載。

想用裝置能力宣告去擋 iPad,等於拿一個新退件理由換掉舊的,同一份技術支援文件也明講:App 若不是真的用到那項硬體能力,一樣會被 App Review 退件。

還有一件事值得記:Apple 沒有任何一句規定主要行動必須在第一屏、或不得依賴捲動,人機介面指南的立場其實相反,允許捲動,前提是要讓使用者看得出來這裡能捲。「主要行動常駐第一屏」是自選的解法,不是規定,這個區分要誠實帶進回覆信,但同一句話被退第二次,代表該做的是換解法,不是去跟審查員辯規定寫在哪裡。

IAP 送審的硬事實

幾個第一手撞出來、平常不會寫在文件裡的事實:

  • App Review 就是在沙盒環境審查 IAP,商品不需事先核准即可運作。
  • 審查員的沙盒購買紀錄是持久的,開發者清不掉。
  • StoreKit 2 對已擁有的非消耗型商品,會靜默回傳成功、不顯示任何付款畫面,這是第一手實測,Apple 官方文件對此沒有明文。任何「收到相符商品 ID 事件就發權益」的實作,在這個情境下都會在沒有付款畫面的狀況下開通權益,那正是 2.1(b) 條款定義的問題。
  • 舊版技術文件說重複購買會跳系統對話框,那描述的是 StoreKit 1 的行為,官方文件也會因為射程對不上版本而失效。

回覆信紀律

五輪換來的幾條硬規矩:

  • 沒測過的東西不寫成已驗證的句子,作法是不寫,不是用模糊措辭包裝。
  • 禁用全稱句,例如所有畫面都驗過、所有資料不離開裝置。這次的資料不離開裝置字面上就是假的,音樂搜尋功能會把使用者輸入的字串送出去,改成精確句:送到我方伺服器的只有行政區代碼與使用者主動輸入的搜尋字串。
  • 不寫跟 ASC 後台矛盾的話,隱私標籤裡有五格標了用於追蹤是,這時候寫我方不追蹤,審查員一鍵就能查出是假的。
  • 不引用官方文件去論證所以不用管 iPad,容易被讀成挑釁。
  • 推論寫進回覆信時,語氣要停在「這可能是原因」,不要寫成斷言。

Resolution Center 與備註欄的機制

隱私問卷與資料收集聲明

流程與驗證方法

被否決的替代方案

留著這張表是為了記住為什麼沒走那些路:

方案否決理由
不上架 iPad(移除支援目標裝置)不阻止 iPad 安裝,仍以相容模式執行,官方文件明列為不可行做法
用裝置能力宣告擋 iPad拿新退件理由換舊的,該宣告只能用於真正的硬體依賴,且上架後只能放寬不能加嚴
開啟平板全支援模式把已知的窄視窗換成全新未驗證的大版面,四次退件後不該開新戰線,且會動到原生層,讓前後兩顆 build 原生層逐位元組相同這條乾淨的驗收基準一起消失
GDPR 表單加一條保險條件前提是錯的,系統層把受限與拒絕壓成同一個值,使用者關掉系統追蹤詢問時,法規要求的同意表單會一次都不出現,等於用不必要的條件壓掉法規揭露
交易結清成功才開通權益敗方代價是結清失敗的人付了錢當場拿不到東西;勝方理由是使用者按下購買、StoreKit 回傳成功,權益即刻到手,未結清的交易會在下次進設定頁被補結清,可以自我修復
靠交易時間戳分辨新購買與已擁有型別沒有單位註解,同一份程式碼裡出現過毫秒與秒兩種前例,真正的序列化在遠端也不可讀,最後改用按下購買前的已擁有商品快照當決定性判準
申請正式申訴管道或線上諮詢列為可用而未用的武器,判準是留到第三次收到同一封信再動用,全程沒有真的用到

誠實的但書

通過不代表任何一條修法被證實有效。通過信裡沒有審查環境的段落,也沒說審查員實際走過哪些流程,只知道這次過了,不知道是哪一項修法起了作用。信末的制式段也不是審查員的觀察,第五次信末提到的協議問題,實查後確認協議本來就生效中,是罐頭文字。

另外兩點誠實標註:iPad 上健康資料 API 回傳不支援這件事,在這個案例裡至今仍是外部資訊,沒有經過第一手驗證;Apple 是否會跨審查回合沿用同一個沙盒帳號,目前也沒有公開說法。

相關連結