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

這篇逐輪對照一款 iOS App 在同一個版號上被退件五次的過程:Apple 信裡寫的理由,與真正該改的東西。讀者需要對審查條款編號(2.1、4、5.1.1、5.1.2、2.5.1)、StoreKit、TestFlight 有基本認識,讀完能拿到一套「回覆訊息、備註欄、二進位驗證」的操作紀律。

這是 追追推推 追星族遠征紀錄 App 複盤 的延伸閱讀,完整的條款對照放在這裡。

信裡寫的理由與真正該改的東西經常不同層次

審查裝置從頭到尾都是同一款:iPad Air 11-inch(M3),流程也是同一個循環:

flowchart TD
    A[送出審查] --> B{收到結果}
    B -->|退件| C[逐字比對信件內容與後台條款碼]
    C --> D[本地驗證找出真正原因]
    D --> E[回覆訊息說明成因與修法]
    E --> F[備註欄記錄本輪差異]
    F --> G[重新提交]
    G --> B
    B -->|通過| H[結束]

後台標的條款碼,可能跟信件內容完全脫鉤

第一次退件,Apple 的自動化訊息判定 binary 含 VPN 功能,要求說明收了什麼資料、用途、是否分享第三方;後台標的案由卻是 2.1.0 App Completeness,跟信件內容對不上。

App 裡一行 VPN 程式碼都沒有。來源是靜態連結進 binary 的 Google Mobile Ads SDK,內含網路介面名字串(tap、tun、ppp、ipsec、utun)與 HTTPProxy、HTTPSProxy 鍵,觸發了 Apple 的自動二進位掃描。判斷分支要看信件內容,後台條款編號的字面定義套不上去。

修法不改程式、不重打 build,只對送審那顆 .ipa 做二進位檢查,證明 NEVPNManager 等符號皆為 0。自動掃描比對的是 binary 裡的字串,跟 App 實際有沒有這個功能無關。

模板信的受詞可能是錯的,但審查員相信有東西存在

第二次退件引用 Guideline 5.1.2,指控 App 上傳了使用者通訊錄,開頭句還帶了 still,暗示是上一輪沒解決的問題。這個 App 連 NSContactsUsageDescription 都沒有,收到的是一封模板信,同樣的文字在 2017 年就有完全沒碰受保護資料的案例收到過。

推定的觸發點是上一輪回覆信裡自己寫的一句話:“our own Apple WeatherKit proxy server”,正好構成 5.1.2「上傳/同意」分支的標準形狀:開發者自承有自家伺服器,App 內卻看不到上傳同意畫面。回覆信裡多寫的每一句,都是下一輪的攻擊面。

修法是五語 purpose string 補上「不會上傳」的句子,並附上權限彈窗截圖逐條對答。

隱藏式降級路徑,在審查員眼中就是功能不存在

第三次退件同時踩到兩條:Guideline 4 說畫面被切掉、按鈕碰不到,信中沒有截圖也沒點名頁面;2.5.1 說用了 HealthKit 卻沒在介面上清楚標示。

Guideline 4 的成因是月曆元件的 cellW = (pageWidth - spacing.xs * 6) / 7,7 格加 6 個間距在代數上剛好等於容器寬,零容錯。iPad 相容模式的縮放比例讓版面往上捨入一個 sub-pixel,星期六那一欄整個消失。修法是 cellW 改用 Math.floor。

2.5.1 的標示其實存在,但全 App 三處「Apple 健康」字樣都掛在 if (!isHealthModuleAvailable()) return null; 下游。HKHealthStore.isHealthDataAvailable() 在 iPad 上回傳 false,審查員看到的介面裡一個「健康」字都沒有。修法是把標示釘進設定頁常駐節,不掛任何顯示條件。

Apple 給的是症狀,信裡寫的又是複數 screens,找到一個實證不代表找齊了。

同一句話被退第二次代表上一輪解錯層次

第四次退件也是兩條並發。5.1.1(iv) 說 GDPR 表單出現在使用者已經選了「Ask App Not to Track」之後,Apple 這次罕見地直接給出口句,指明只要把表單順序排在 ATT 之前就不必改文案。成因是 initAds() 把順序寫死成 ATT、UMP、initialize,兩個彈窗都必要、都沒錯,錯的只有先後順序。修法是兩段對調,不加任何條件。

Guideline 4 這次跟上一輪逐字相同,只有 iPadOS 版本從 26.5.2 變成 26.6。上一輪把最外層 View 換成 ScrollView,只讓主要行動「捲得到」,沒讓它「第一屏就完整可見」。判準因此改成「審查員在任何狀態下第一眼看到的畫面是正常的」,捲得到不算解決。回覆信裡逐字寫過 “that container scrolls” 又被同一句退,再寫一次等於告訴審查員沒改。

這一輪還踩到一個不可逆的操作事故:在 ASC 誤將舊提交關閉,整串 Resolution Center 對話全部歸屬舊提交,新審查員點不進去,後續只能把重點濃縮進備註欄。

修法必須先假設審查員帶著已購買狀態進來

第五次退件說「選了購買,購買流程沒觸發,App 直接跳到內容,沒完成 IAP 流程」,Apple 這次的描述逐字正確。

成因是 StoreKit 2 對「已擁有的非消耗型商品」再次購買時會直接回成功,不顯示任何 UI,而 App 把這個事件無條件當成「剛完成一次新購買」,開通權益並彈出感謝訊息。IAP 相關的四支 JS 檔自驗收起零改動,套件版本也從未變過,變的是帳號狀態:TestFlight 的購買預設記在裝置的真實 Apple ID,關鍵在於要先登出 Media & Purchases,登出後用全新沙盒帳號,付款畫面正常出現。

ASC 的 Clear Purchase History 只作用於開發者自建的測試帳號,審查員的購買紀錄清不掉,所以修法必須對「審查員帶著已購買狀態進來」這個前提免疫。

回覆信先承認 Apple 的描述每個字都對,再解釋 App 把「帳號已擁有」誤判成「剛完成新購買」,並刻意保留一段預告:審查帳號已擁有此商品時,點購買列仍然不會出現付款表單,那是 StoreKit 的行為。

條款編號的射程各不相同,判斷靠信件開頭句

條款編號只標明射程,同一個編號底下可能掛著好幾種完全不同的成因。

不上架 iPad 不能繞開 iPad 的審查

Apple DTS 的逐字說法是:UIDeviceFamily: 1 只告訴 App Store 這是為 iPhone 設計的,iPad 使用者照樣能下載並以相容模式執行。想用 UIRequiredDeviceCapabilities 加 telephony 或 healthkit 擋 iPad,等於拿一個新退件理由換掉舊的。

Apple 沒有任何一句規定主要行動必須在第一屏、或不得依賴捲動,HIG 的立場其實相反,允許捲動,前提是看得出來能捲。「主要行動常駐」屬於自選解法,同一句話被退第二次就代表要改解法,跟審查員辯沒有用。

IAP 審查是在沙盒環境進行的,商品不需事先核准

App Review 就是在 sandbox 審 IAP,商品不需事先核准即可運作,而審查員的 sandbox 購買紀錄是持久的,開發者清不掉。

StoreKit 2 對已擁有的非消耗型商品會靜默回成功、不出任何 UI,這點 Apple 官方無明文,是第一手實測結果。任何「收到相符 productId 事件就發權益」的實作,在這個情境下都會在沒有付款畫面的情況下開通,那正是 2.1(b) 的定義。

備註欄與回覆訊息是兩個機制,功能不能互相取代

拒審信把「回覆訊息」與「重新提交」並列要求,這是兩個動作。讓審查續行靠回覆訊息;build 一個位元組沒變就重新提交,同一套自動掃描很可能再觸發一次。射程也不同:訊息串是一次性對話,備註留在版本上,接手的審查員、下次改版、自動掃描都讀得到。

回覆信只寫已經驗證過的事

沒測過的就不寫成驗證句,也不用模糊措辭包裝。禁用全稱句,例如「所有畫面都驗過」「所有資料不離開裝置」:本案的「資料不離開裝置」字面上是假的,音樂搜尋會把使用者輸入的字串送出去,精確句是「送到伺服器的只有行政區代碼與使用者主動輸入的搜尋字串」。

也不寫與 ASC 後台矛盾的話:本案隱私標籤六格中有五格標「用於追蹤=是」,此時寫 “we do not track” 是審查員一鍵可查的假聲明。引官方文件論證所以不用管 iPad,容易被讀成挑釁。推論的語氣停在 “the likely cause”,不寫成斷言。

隱私申報寧可過度,不要不足

「連結身分」與「用於追蹤」是兩條獨立的軸,不要綁在一起改。過度申報不會被罰,申報不足才會導致退件或下架,有疑慮就往「是」填。第三方 SDK 收集的算開發者收集,官方原文明講開發者對第三方函式庫存取隱私敏感資料負全責。

能用免費手段查完的,就不要送 build

build 只驗得到程式碼,設定層的錯誤用免費手段就查得完。字串在二進位裡的鄰近性只能證歸屬,不能證觸發。成因沒證實時,選一個不論假設成不成立都該修的修法,讓修法對成因假設不敏感。

退件信只存在 ASC 後台,Apple 的截圖放在下載資料夾會被洗掉,收到當天、動手分析前要先做完信件全文逐字、附件原檔、時間軸、送出紀錄這四步留存,通過信同樣要存。

想繞開 iPad 的幾條路都在動手前被否決

通過不代表任何一條修法被證實有效

通過信沒有 Review Environment 段,不會說審查員走過哪些流程,只知道這次過了,不知道是哪一項讓它過的。信末的制式段也不是審查員的觀察:最後一封信末提到的協議問題,實查後確認協議生效中,是罐頭文字。

「iPad 上 isHealthDataAvailable() 回 false」至今仍是外部資訊,未經第一手驗證;Apple 是否跨審查回合沿用同一個 sandbox 帳號,也沒有公開說法。

相關連結