
追星族看一場演出,要同時追抽選結果、訂機票住宿、算不同貨幣的花費、記歌單、存拍立得照片。這些東西散落在票務網站、記帳App、相簿和便利貼裡,演出結束就找不回來了。
我做了一個把這些通通收進同一本紀錄本的App,叫「追追推推」。我用一人 AI 協作開發的方式,從產品定位、設計系統、五語在地化,一路做到App Store送審,全程參與。這個專案真正難的不是寫功能,是「上架」——被Apple退了五次才過。
成果速覽
- 已上架App Store(2026年8月28日),供應174個地區,支援iOS 16.4以上裝置。
- 五次退件、六次提交、21天過關。每一次官方寫的理由,跟真正要改的東西幾乎都對不上。
- 首發就做好五種語言(繁中、簡中、日、英、韓)的完整翻譯,訂下規則:五語沒翻完不准送審。
- 規模:六萬三千行程式碼、1286個自動化測試,兩個月內533次提交。
- 使用者資料全部離線存在手機裡,不用登入,也沒有帳號後端。
- 老實說,這個App的功能本身不算新奇。它真正的價值,是我第一次在專案裡同時放入廣告與內購——這個決定把後面要面對的合規複雜度撐大了一整級,也是五輪退件的根源。
這本紀錄本裡有什麼,又刻意不做什麼
App收六類東西:票券日程、遠征記帳、歌單音源、圖鑑、相簿,還有代表使用者最支持的人「我推」,離線優先、免登入、無後端存資料。
不做雲端同步;拿掉票券「當選、落選」標籤;廣告只在主頁底部放一條橫幅;介面禁止出現日文假名,改寫成「我推」。目前沒有匯出或備份功能,換手機資料就不見,記在待辦清單上。
技術細節:架構與技術棧
React Native 0.86 + Expo SDK 57 + expo-router,TypeScript開strict模式。 資料層用expo-sqlite搭配Drizzle ORM(61支migration、25張表),UI狀態用zustand管理。 在地化用i18next,繁中是翻譯來源語言,時間一律以UTC ISO格式儲存。 變現:AdMob橫幅廣告 + StoreKit 2一次性買斷內購。 服務端只有Supabase的兩支Edge Function(天氣、音樂搜尋代理),其餘全在本機執行。
上架,是這個專案最貴的一課
Apple退回五次,官方理由跟真正要改的東西幾乎都對不上。
| 輪次 | Apple說的理由 | 真正原因 |
|---|---|---|
| 1 | 含VPN功能 | 廣告SDK字串觸發誤判 |
| 2 | 上傳通訊錄 | 未宣告權限,疑似回覆信用詞引發疑慮 |
| 3 | 畫面被切、健康資料未標示 | 格寬計算疏漏致溢位,標示掛在不顯示的卡片下 |
| 4 | 與第3輪相同 | 按鈕未在首屏可見 |
| 5 | 選購買但未觸發 | 已購商品被系統誤判為成功 |
還漏填「行銷網址」,官網連結因此消失、曝光量被鎖住,且發佈後永遠改不了,下一版唯一目的是補上它。完整條款對照見App Store 五次退件實錄:條款判讀與回覆紀律。
技術細節:五輪退件涉及的實際技術點
第1輪:廣告SDK內建的網路介面名稱字串,觸發Apple自動掃描系統誤判為VPN。 第2輪:回覆信中提及「代理伺服器」一詞,疑似觸發人工審查聯想。 第3輪:月曆格寬計算除法無容錯,iPad Split View下溢位;HealthKit使用聲明的顯示邏輯,掛在同一張因裝置判斷而不渲染的卡片上。 第5輪:StoreKit 2對已擁有商品的購買請求會靜默回傳成功,App誤判為新交易並卡在載入狀態。 行銷網址即App Store Connect的Marketing URL欄位,版本發佈後鎖定不可修改。
那些不出錯誤訊息、卻悄悄壞掉的地方
兩個月抓出十幾個這類問題,共同點是不出錯,東西卻悄悄不見。
最典型的一次:高解析度圖檔打包時因工具只認三種固定尺寸而整批被丟掉,讀不到圖的元件安靜不畫。這個洞活過三次組建、還撐過一次送審驗收,因為驗收裝置套的是不經打包的線上更新。
每一條讓程式安靜退回「看起來正常」的路徑,都是未來看不見的故障現場。完整清單見靜默失敗圖鑑:Expo 與 React Native 上那些不報錯的壞法。
技術細節:打包丟圖與OTA的差異
高解析度圖檔在git版本控制中完整保留,但EAS Build的資源打包流程只辨識預先定義的三種icon尺寸,其餘尺寸在打包階段被靜默捨棄。 對應元件在讀取圖檔失敗時吞掉錯誤並fallback為空白,不拋出例外。 此問題在OTA(透過expo-updates推送的線上更新)環境下不會出現,因為OTA只更新JS bundle,不會重跑原生打包流程,因此驗收裝置長期偵測不到。
設計:把規範寫進程式碼,不是留在對話紀錄裡
這個App的視覺語言只有一句話:場景完全無彩色,人是唯一被打光的東西,唯一的顏色來自使用者選的支持對象顏色。
靠人眼審查靠不住,所以我寫成三支自動補償對比度的函式,訂下硬規則:UI程式碼不准出現規範以外的顏色、字級、間距。這規則來自一次教訓——兩個月前拍板的限制只存在對話紀錄裡,從未寫進程式。從此新限制一律先寫進規範文件才准動手。
商標問題:畫面上的品牌名字,沒有工具幫你抓
App裡19個底片濾鏡直接拿相機廠牌機型名稱當顯示名稱,13個有商標風險,是有人隨口一問才啟動盤點。
關鍵是並排效應:單看一個名稱風險很弱,五個同品牌名稱排在一起就像官方授權系列。最後把顯示名稱欄位整個移除,因為位置留著遲早會被填回去。完整分析見UI 用了別人的品牌名:濾鏡預設的商標風險與改名執行。
一個人做專案,怎麼知道自己真的做完了
沒有團隊盯著,驗收機制得自己長出來。我訂了六階段任務狀態:自動檢查全過也只代表能進「待驗證」,真正完成要在實機跑過並自己回報。
每次新增測試都故意改回舊寫法確認會失敗,避免假綠燈。我也讓不同AI助手獨立覆核同一份成果,依可信度分級參考,有一次直接推翻我原本的結論。
沒加持續整合、沒加提交前檢查:這類工具是防止別人寫壞程式碼,一人一機下價值幾乎等於零,這是取捨不是偷懶。
技術細節:獨立覆核與測試紀律
覆核由獨立的AI agent執行,涵蓋死碼稽核四條檢查線與健康檢查六條檢查線,並依可信度分為不同等級。 測試紀律採負向驗證:每次新增測試後刻意將實作改回舊寫法,確認斷言會轉紅,排除假綠燈疑慮。 六態任務狀態的硬規則:四道自動化閘門全綠僅代表可推進至「待驗」,最終完成仍需實機操作並回報原始文字說明。
可以帶走的幾個心得
- 上架複雜度隨外部服務變大:三個月前純本地App三天過關,這次多了廣告內購,退了五次。
- 退件理由通常只是症狀不是病根,是常態不是例外。
- 悄悄降級的容錯路徑最貴,最需要看見問題時它什麼都不說。
- 重現症狀不等於證實原因,變數排除到剩一個時更該懷疑清單本身漏了什麼。
- 「不支援iPad」不是逃生門,拿掉清單使用者還是裝得上;裝置不支援就不顯示,合規標示會跟著消失。
- 真正管用的防呆是從結構上切斷錯誤路徑,連驗收工具本身也要先驗證可信。
現在的狀態
1.0.0已於2026年8月28日上架,1.0.1於9月1日送審,目的是補上漏填的行銷網址,結果未知。
還有債沒還清:內購監聽機制未提升、一張資料表關聯規則靠呼叫順序硬撐、最新安裝包還沒人在真機跑過、匯出備份功能不存在。
延伸閱讀
- App Store 五次退件實錄:條款判讀與回覆紀律——條款對照與回覆信寫法
- 靜默失敗圖鑑:Expo 與 React Native 上那些不報錯的壞法——打包與更新的踩雷紀錄
- UI 用了別人的品牌名:濾鏡預設的商標風險與改名執行——商標風險分級與改名過程
相關
- NailNote 本地美甲筆記 App 複盤——同樣離線優先但無廣告內購
- Nailora 美甲情報 App 複盤——有後端、帳號、雙邊市場的對照
- AI 協作開發:非技術 PM 的驗收閘門——「四道閘門只夠到待驗證」規則出處