← 所有文章

同意的生命週期:年齡驗證之後真正要處理的事

2025 年 11 月 4 日,Apple 為 App Store Server Notifications 新增了一種通知類型。它的觸發條件不是付款,而是家長的一項決定:RESCIND_CONSENT,代表「家長或監護人已撤回對孩童使用 App 的同意」。12

在該服務的 23 種類型中,只有它的 payload 帶的是 appData 物件而非交易資訊,而 appData 內含一筆已簽署的 app transaction,「即使顧客未進行任何 App 內購買」也依然存在。319 換句話說,一個從未賣出任何東西的 App,第一次有了架設通知端點的理由。

社群媒體聲明那篇談的是如何讀取一個人的年齡區間:所需的 entitlement、各道關口,以及回傳的邊界值代表什麼。那些就當作已成定局,我們從下一步開始談。年齡驗證回答的是這個人幾歲;本文的三個 API 回答的是誰來核可、既有的核可在您的 App 變更後是否仍然有效,以及監護人收回核可時會發生什麼事。21012

重點摘要

  • 年齡驗證的下游有三個 API,Apple 是成套推出的:PermissionKit 底下的 Significant Change API、StoreKit 的 AppStore.ageRatingCode,以及 RESCIND_CONSENT 伺服器通知。45
  • Apple 將這四項工具綁定的對象是德州、猶他州與路易斯安那州,而不只有德州;它為這三州公布的每個日期都已經過去,而 Apple 將這項義務的範圍界定為「在法律要求的特定地區」。5815
  • 決定您是否需要動用這一整套機制的關口,是 26.4 新增的 AgeRangeService.requiredRegulatoryFeatures;而它所把守的完整流程,寫下來需要 iOS 26.5。736
  • 何謂「重大變更」,Apple 拒絕定義,而且明講了四次,一律要您去問律師。它唯一公開的具體範例,還把出處歸給法條:Apple 寫的是德州法律將 App 年齡分級的變更視為重大變更。48
  • 您的分級可能在您沒有送出任何版本的情況下改變。2026 年 6 月 18 日,Apple 取消了澳洲的 15+ 分級,並給了越南一套全新的四級制,兩者都沒有要求任何開發者送出任何東西。937
  • 撤回是沒人會去實作的那條分支,而 Apple 自己的文件也幾乎沒有實作它:在伺服器團隊真正會去讀的那一頁上,RESCIND_CONSENT 沒有出現在任何一張生命週期表格裡。2

授予、重新授予、撤回。前兩條路徑,Apple 用一個範例專案和一套沙箱測試矩陣寫得清清楚楚。第三條只拿到一個列舉值和四個欄位——後來我發現,這差不多也正是它在我自己程式碼裡得到的關注程度。

Apple 公布了四次的時間表

以下每個日期都出自 Apple 自家的開發者新聞,而事件的先後順序,比其中任何單一條目都更值得注意。

Apple 於 2025 年 10 月 8 日宣布德州 SB2420,標註「自 2026 年 1 月 1 日起」,而它描述的是自己的因應方式而非法條原文:18 歲以下使用者的新 Apple 帳戶將加入「家人共享」群組,而且「家長或監護人必須針對未成年人所有的 App Store 下載、App 購買,以及使用 Apple App 內購買機制的交易提供同意」。13 11 月 4 日,Apple 為這些工具命名,並隨 26.2 beta 推出。4 12 月 23 日,計畫停擺:「地方法院近期核發的禁制令中止了德州州法 SB2420 的執行……Apple 將暫停先前宣布的實作計畫。」14 2026 年 6 月 3 日重新啟動:「由於法院近期裁定解除德州法律 SB 2420 的禁制令……這些變更將自 2026 年 6 月 4 日起生效。」5

一條法規、八個月的來回擺盪,而那些 API 從頭到尾一動也沒動。它們在 26.2 就已推出,整段禁制令期間都能在沙箱測試,禁令解除時它們早就等在那裡。14 把 12 月的暫停當成可以延後處理的許可,等於白白丟掉五個月的準備期。

其餘的版圖在 2026 年 2 月 24 日揭曉,範圍超出德州。

司法管轄區 Apple 表示適用的日期 Apple 表示適用的內容
澳洲、巴西、新加坡 2026 年 2 月 24 日 Apple 封鎖 18+ 分級 App 的下載,「除非已透過合理方式確認其為成年人」15
猶他州 2026 年 5 月 6 日 新 Apple 帳戶的年齡分類於請求時提供15
德州 2026 年 6 月 4 日 新 Apple 帳戶「現已受該法律規範」:代表 18 歲以下未成年人同意下載、App 內購買與重大變更,且監護人可撤回5
路易斯安那州 2026 年 7 月 1 日 新 Apple 帳戶的年齡分類於請求時提供15

德州的特殊之處在門檻,不在工具。Apple 將德州描述為「代表 18 歲以下未成年人」的同意,並公布其分類為「未滿 13 歲、13-15 歲、16-17 歲或 18 歲以上」,這正好就是該 API 產出的結果:「您最多可指定三道年齡關卡,藉此建立最多四個可能的年齡區間。」4529 對猶他州和路易斯安那州,Apple 提的是依請求提供年齡分類,接著又表示這四項工具「已擴充,以協助開發者滿足路易斯安那州與猶他州的合規義務」,其中就點名了 Significant Change API。15 工具並非德州專用。至於義務是不是,Apple 不願明說:它把這項責任的範圍界定為「在法律要求的特定地區」,然後把問題丟給律師。8

不要把這張表當成法律陳述或合規判定。它記錄的是 Apple 宣布了什麼,以及 Apple 為此標註的日期。我是工程師,不是律師,以下所有內容都止於 API 的邊界。

那份把合規問題轉給律師的 Q&A,對發版規劃倒是帶來好消息:被問到這一切是否改變審查流程時,Apple 回答「不會,App Review 流程沒有任何變更」。8 這些義務落在指定司法管轄區的執行期,不在送審階段。

所以分工很乾淨。您的 App 在德州、猶他州或路易斯安那州是否負有同意義務,是要問當地執業律師的問題。要用哪個 SDK 建置則不是:Apple 明確表示「您必須使用 iOS 26.2 與 iPadOS 26.2 或更新版本的 SDK,搭配 Xcode 26.2 (17C52) 或更新版本建置您的 App」才碰得到這些框架,而 iOS 18 或更舊版本上的既有帳戶「不會受到影響」。8

Apple 不會告訴您「重大」是什麼意思

定義的空缺就坐落在這項功能的正中央,而 Apple 有四個地方把它原封不動丟了回來,而不是把它填上:符號頁面(「由您依適用法規判定何謂重大更新」)、12兩則新聞(「判定 App 何時發生重大變更是開發者的責任」)、45以及那份 Q&A——被直接問到條款或隱私政策變更算不算時,答案是「視情況而定。由您依適用法律判定何謂重大 App 更新」。8

Apple 只給了一個實例,而且出處來自法條而非 Apple 自己:「德州州法將 App 年齡分級的變更視為重大變更,開發者應在 App Store Connect 中維持年齡分級選項的最新狀態。」4

Apple 真正有定義的,是您要寫的那串文字。SignificantAppUpdateTopic 把好例子和壞例子並排放,這在符號頁面上算是罕見的坦白:

// Specific
let topic = SignificantAppUpdateTopic(
     description: "This update adds video calling and location sharing features."
)

// Vague
let topic = SignificantAppUpdateTopic(
    description: "We've made improvements to the app."
)

Apple 圍繞這段程式碼給出的指示相當直白:「請使用精簡、易懂的語言,清楚說明您的 App 有何變更。家長與監護人在決定是否授予權限時會看到這段描述。」12 那些罐頭發版說明,如今要擺到一位正在決定自己孩子是否繼續使用的家長面前——這使它成為多數 App 這輩子會發出的、賭注最高的一段更新日誌文字。

答案雖然含糊,隨之而來的義務卻一點也不含糊,儘管 Apple 對觸發時機留了餘地。Apple 要您「負責在必要時阻止對 App 或功能的存取」,接著斬釘截鐵地表示「在家長提供同意之前,必須阻止孩童存取該重大更新,這可能涵蓋所有 App 與帳戶資料,或特定功能」。8「所有 App 與帳戶資料」這幾個字承擔了很重的分量。Apple 把波及範圍留給您決定,並把整個帳戶標為上限。

狀態機,以及 Apple 自家範例的破綻

Apple 發布了一個範例專案〈Implementing age assurance and permissions〉,這是整套流程唯一的端到端說明。6 把它當作狀態機來讀,會浮現四條分支,以及一段值得重寫的程式碼。

第一條分支最省事。AgeRangeService.requiredRegulatoryFeatures 回傳 Set<AgeRangeService.RegulatoryFeature>,共三個成員:declaredAgeRangeRequiredsignificantAppChangeRequiresParentalConsentsignificantAppChangeRequiresAdultNotification7 Apple 的範例先檢查它,而且「當兩項功能皆不存在時,App 會完全跳過整個流程」。6 Apple 把這個屬性描述為反映一個人的「地區與帳戶設定」,在我讀來這像是一種承諾:指定司法管轄區之外的使用者會拿到空集合——只是 Apple 並沒有做出任何這類保證。7

其餘分支則依年齡、以及年齡的認定方式而分岔:

對象 所需的法規功能 Apple 範例的做法
未成年人 significantAppChangeRequiresParentalConsent 向監護人送出 PermissionQuestion6
成年人,已確認方式 significantAppChangeRequiresAdultNotification 顯示系統的確認面板6
成年人,無已確認方式 任一項 阻擋存取,「直到本人在『設定』中驗證帳戶為止」6
拒絕分享 任一項 剖析了該狀況,卻沒有為它提供任何分支6

第三列才是值得停下來細想的。一位從未在 Apple 帳戶綁定付款方式的成年人,會被 Apple 的參考實作擋在門外——而這套流程本來是為了保護兒童而設計的。Apple 沒有提供替代分支,也沒有任何覆寫方式。

把這些與已公布的宣告拼起來,路由邏輯長這樣。成年人與未成年人的分岔依循 Apple 的範例加上沙箱測試矩陣:18 歲以上的帳戶回傳 lowerBound 為 18、無上限,ageRangeDeclarationconfirmedselfDeclared。請留意可用版本下限:讀取 .confirmed 要付出 iOS 26.5 的代價,比這套流程中其他所有東西都晚一個版本。61136

import DeclaredAgeRange
import PermissionKit
import SwiftUI

@available(iOS 26.5, *)
struct SignificantChangeGate: View {
    enum Phase {
        case checking
        case clear
        case awaitingGuardian(PermissionQuestion<SignificantAppUpdateTopic>)
        case blocked
    }

    let changeDescription: String
    @Environment(\.requestAgeRange) private var requestAgeRange
    @Environment(\.showSignificantUpdateAcknowledgment) private var acknowledge
    @State private var phase: Phase = .checking

    var body: some View {
        switch phase {
        case .checking:
            ProgressView().task { await resolve() }
        case .clear:
            ChangedFeatureView()
        case .awaitingGuardian(let question):
            PermissionButton(question: question) { Text("Ask a parent to approve") }
        case .blocked:
            AccountVerificationPrompt()
        }
    }

    private func resolve() async {
        let features = (try? await AgeRangeService.shared.requiredRegulatoryFeatures) ?? []
        guard !features.isEmpty else {
            phase = .clear                 // nothing applies to this person
            return
        }
        guard let response = try? await requestAgeRange(ageGates: 18),
              case let .sharing(range) = response else {
            phase = .blocked               // declined or unavailable: Apple documents no branch
            return
        }
        let isAdult = range.lowerBound == 18
        let isConfirmed = range.ageRangeDeclaration == .confirmed

        switch (isAdult, isConfirmed) {
        case (true, true) where features.contains(.significantAppChangeRequiresAdultNotification):
            try? await acknowledge(updateDescription: changeDescription)
            phase = .clear
        case (true, false):
            phase = .blocked               // adult with no confirmed method
        case (true, true):
            phase = .clear
        case (false, _):
            guard features.contains(.significantAppChangeRequiresParentalConsent) else {
                phase = .clear
                return
            }
            let topic = SignificantAppUpdateTopic(description: changeDescription)
            phase = .awaitingGuardian(PermissionQuestion(significantAppUpdateTopic: topic))
        }
    }
}

送出問題只是簡單的一半。收回答案才是 Apple 範例出破綻的地方,而那段程式碼短到足以逐行細看:6

for await response in AskCenter.shared.responses(for: SignificantAppUpdateTopic.self) {
    guard response.choice.answer == .approval else {
        return
    }
    versionManager.handleAllChanges()
}

for await 裡寫 return,等於放棄整個序列。只要一次拒絕,App 就不再監聽該主題,直到下次啟動為止——於是一位按下「拒絕」、一分鐘後又改變心意的監護人,把核可送進了一條沒人在讀的串流。請改寫成 continue,並把該次拒絕記錄下來。把這段程式碼讀成缺陷而非刻意設計,是我的推論;無論如何,修正的代價就是一個關鍵字。設計監聽器時,請假定背景啟動不會發生:只有它所取代的那個已棄用序列曾經承諾過這件事。2021

「待處理」是一種狀態,而且它的逾時不由您掌控

生命週期的中段才是設計工作真正落腳之處,有五項已載於文件的行為在形塑它。先從 Apple 只回答了一半的那項開始:PermissionQuestion.expirationDateOptional<Date>,過了這個日期之後,「收到問題的人便無法再回應」,而 Apple 沒有公布任何預設值。16

孩童可以在監護人看到問題之前就取消:「在送出請求流程的任何時點,孩童都可以選擇取消該請求……在這種情況下,系統不會針對該特定問題向呼叫端 App 傳遞任何回應。」17 沒有回應、沒有錯誤、沒有回呼。除非您自己設逾時,否則您的待處理狀態會永遠掛在那裡。

向成年人詢問會拋出例外。沙箱測試那篇文件補上了符號頁面留白的一個案例:「對成年使用者呼叫 AskCenter.ask(_:) 會拋出此錯誤,因為他們不符合家長權限請求的條件。」11 AskError.notAvailable 是列舉中兩個在自身頁面上沒有任何摘要說明的案例之一,而它正是成年人會觸發的那一個。18 請在詢問之前依年齡分流,而不是靠捕捉例外來處理。

核可狀態該放在 iCloud,而不是 App 容器。Apple 的範例把每一次已確認的變更寫進 NSUbiquitousKeyValueStore,並監聽 didChangeExternallyNotification,「讓其他裝置不會再次呈現同一套流程」。6 家長在 iPhone 上核可之後,不該在 iPad 上再冒出第二次請求,而 PermissionKit 完全不會替您處理這件事。

範例中最出色的細節,解決了一個多數團隊會做錯的問題。全新安裝的使用者不該去同意一項他們從未經歷過的變更,所以 Apple 讀取 AppTransaction.originalAppVersion,並「自動將 App 在該版本或更早引入的所有變更標記為已處理」。619 因此同意狀態的追蹤粒度是「每項變更、每個人」:要用帶有引入版本的變更識別碼,而不是一個布林值。

您的分級可能在沒有任何版本送出的情況下改變

AppStore.ageRatingCode 是一個非同步回傳 Int?static var,自 26.2 起在 iOS、iPadOS、macOS、tvOS、visionOS 與 watchOS 上皆可使用。10 Apple 載明的用途是比對,而非解讀:「請使用此屬性取得您 App 的年齡分級,並與最後已知的年齡分級比對,以檢查它是否已變更。」10 您能做的就只有比對,因為 Apple 在文件的任何地方都沒有公布整數到分級級距的對照表。您無法詢問自己是不是 13+,只能問自己和上次是否相同——所以真正的約定是:您必須保存前一個值,並自行處理第一次執行的情況。

一旦注意到分級會在沒有發版的情況下改變,App 為何要監看自己的分級也就不言而喻了。2026 年 5 月 21 日,Apple 宣布自 6 月 18 日起,「15+ 年齡分級將不再於澳洲 App Store 提供」,並依既有問卷答案為越南導入一套地區專屬的四級制。9 兩項變更都落地了:App Store Connect Help 的澳洲表格現在只列 16+ 與 R 18+,沒有 15+ 那一列,而越南也有了自己的表格。37 兩者都沒有要求任何送審、任何版本或任何開發者動作,而 Apple 寫的是德州法律把分級變更算作重大變更。4

拿您自己發動的分級變更來對照:「當開發者更新 App 的年齡分級時,該分級會在版本上線後於所有使用者裝置上更新。」4 您自己的變更搭的是您可以埋點量測的發版;Apple 的變更則不然——而這正是這個屬性要填補的缺口。

測試上的問題比一個缺口更麻煩。Apple 員工在開發者論壇的一則回覆講得很直接:「在您的 App 從 Xcode 與沙箱環境(包含 TestFlight)建置並執行的開發過程中,0 是預期值。」22 零不是 nil,所以 Apple 自家文件中的範例會通過它的 guard let,把 0 當成有效分級交回去——而這正是發起那串討論的開發者在實體裝置上看到的情況。1022

順著推下去。把 0 存成基準值,第一次從 App Store 啟動時就會讀到真實的代碼、判定有變更,然後為了一件根本沒發生的事去請家長重新同意。把 0 當成一個絕不寫入基準值的哨兵值,是我從 Apple 這兩項陳述推導出來的做法,也是我唯一不敢不寫就出貨的那一行防禦性程式碼。

Apple 的可用性中繼資料還藏著一項沒有任何說明頁面提及的發現。這四項能力的平台列,破洞落在不同位置:

能力 iOS / iPadOS macOS Mac Catalyst visionOS tvOS / watchOS
AppStore.ageRatingCode10 26.2 26.2 無此列 26.2 26.2
requiredRegulatoryFeatures7 26.4 26.4 26.4 無此列 無此列
SignificantAppUpdateTopicPermissionButton1226 26.2 26.2 26.2 26.2 無此列
showSignificantUpdateAcknowledgment27 26.4 無此列 26.4 無此列 無此列

由此浮現兩處不對稱。原生 macOS App 可以得知 significantAppChangeRequiresAdultNotification 適用於某個人,卻沒有任何 API 可以滿足它:showSignificantUpdateAcknowledgment(in:updateDescription:) 接受 UIWindowScene,且未公布 macOS 那一列,而 SwiftUI 的 SignificantUpdateAction 也停在同樣的三個平台。27 Apple 確實為鄰近的 requestAgeRange 呼叫提供了 NSWindow 版本,所以這項缺漏讀起來像是疏漏而非政策——這是我的推論。28

其次,偵測的觸及範圍超出了補救的範圍。ageRatingCode 公布了 tvOS 與 watchOS 列;任何用來請求同意的東西都沒有。10 那些目標平台能觀察到分級變更,卻沒有任何載於文件的回應方式,而 Return 兩者都有出貨:它的 tvOS 1.0.1 在 App Store Connect 中處於 READY_FOR_DISTRIBUTION,且已上架的 iOS App 內嵌了一個 watch App。30 這兩項解讀都假設 Apple 的中繼資料是完整的,而同一批中繼資料恰恰反駁了這點:ageRatingCode 沒有公布 Mac Catalyst 列,鄰近的每一個符號卻都有。10

當 Apple 已經擋下啟動,伺服器還能做什麼

Apple 用一句話說明了撤回行為:「當家長或監護人撤回其孩童存取某 App 的同意時,Apple 會阻止該 App 啟動。若要處理同意撤回,請使用 notificationType 中的 RESCIND_CONSENT 值。」8

請注意這兩句話的先後。系統已經擋下存取了,所以 RESCIND_CONSENT 並不是一個執行封鎖的掛鉤——把它做成那樣,等於浪費了它。它是您唯一能拿到的訊號,關於一位您的程式碼再也無法在其裝置上執行的使用者。Apple 對於該如何歸檔這個事件隻字未提,所以我找到最接近的類比是帳戶刪除請求:有訂閱要清算、有狀態要凍結,還有一個可能再度出現的家庭。

這份 payload 單薄得不尋常。appData 帶四個欄位:appAppleIdbundleIdenvironmentsignedAppTransactionInfo3 沒有交易、沒有訂閱、沒有續訂資訊,也沒有屬於您的任何帳戶識別碼。與人的連結,是透過那筆已簽署的 app transaction 建立的——App Store 會「為每個下載您 App 的 Apple 帳戶,以及支援家人共享的 App 的每位家庭群組成員」產生 appTransactionID19 所以免費 App 確實握有一把以帳戶為單位、可長期比對的鍵值;而一個從未儲存過這把鍵值的 App,收到的會是一則它無法歸屬到任何人身上的通知。

傳輸層沒有意外:在 App Store Connect 中為每個環境設定一組第 2 版 URL,走 TLS 1.2 以上,17.0.0.0/8 加入允許清單,成功回應為 200206,回 40x50x 則換來五次重試,時點為 1、12、24、48 與 72 小時,且僅限正式環境。2324

RESCIND_CONSENT 也不帶 subtype。已公布的 19 個 subtype 值,每一個都限定於某個具名的通知類型,而字串 RESCIND_CONSENT 在那一頁上完全沒有出現。25 更說明問題的是,Apple 的 notificationType 頁面在「Handle use cases for In-App Purchase life-cycle events」底下用八張表格對應了 40 個事件,RESCIND_CONSENT 一張都沒出現;這個值只存在於可能值清單中,該頁其他地方一概沒有。2 Apple 把這則通知記在年齡驗證的資料裡,卻從未把它接進伺服器團隊真正會讀的那份參考文件。

我自己的專案已經做錯了什麼

在寫別人的程式碼之前,我先搜了自己的。篩選規則是機械式的,而問題就出在這條規則:我自己 CLAUDE.md 裡「Active Projects」表格中,每一列背後有 Xcode 專案的,總共七個專案、508 個檔案,涵蓋所有 Swift 檔、entitlements 檔、property list 與 project.pbxproj31 PermissionKitAskCenterSignificantAppUpdateTopicFamilyControlsManagedSettingsDeviceActivityCKSharesharedCloudDatabaseageRatingCode 一律零命中。七個當中有五個有 App Store Connect 記錄。Water 和 Yawara 都沒有,也從未上架,所以這兩個請當成程式碼來讀,而不是任何人手上的 App。31 Ace Citizenship 看起來最像會有未成年使用者的地方,實際上卻最不可能:它的配套網站把 N-400 資格寫成「年滿 18 歲」,而這個 App 從頭到尾都沒有問過出生日期。31

接著我把完全相同的樣式跑遍 ~/Projects 底下的每一個儲存庫——這才是我一開始就該跑的掃描。同一批樣式命中 21 個檔案,其中七個是程式碼,而那七個裡有六個坐落在同一個 iOS 專案裡,一個註冊表從來沒為它開過一列的專案。32

Randori 是一款柔術訓練紀錄 App,而它握有的同意介面,正是這份樣式清單當初要抓的那一種:CKSharesharedCloudDatabaseConnectionStore.swiftRandoriApp.swiftCKPostTransport.swiftProfileCardView.swiftCloudShareSheet.swiftSocialContracts.swift 共 20 處命中;App 已在執行時,接受流程走 userDidAcceptCloudKitShareWith,冷啟動則走 connectionOptions.cloudKitShareMetadata32 Randori 還為「掛上別人的名字」這件事自製了一套同意原語:TagConsentStore 採失敗即關閉,所以只要對方尚未發布標記政策,該名選手就完全無法被標記。32

於是,唯一擁有真實同意介面的專案,自己寫了一本帳,卻沒有採用 Apple 的任何一項。它欠著三件我還沒做的事。啟用那些被自家契約按住不動的人對人介面,正是 SignificantAppUpdateTopic 的教科書案例。ageRatingCode 沒有任何已儲存的基準值,而一個尚未發布的 1.0 只曾從 Xcode、沙箱或 TestFlight 執行過——那恰恰是 Apple 說這個屬性會回傳 0 的地方。22 撤回則無處可落:Randori 什麼都不賣,也沒有自己的伺服器。32

更有意思的發現是,最初那七個專案中,已經有三個以另一個舊名稱出貨了監護人同意機制。

「先詢問後購買」是同樣的機制、同樣的形狀,而 Apple 的措辭也幾乎一模一樣:「使用『先詢問後購買』時,當孩童想要進行合資格的購買或下載,系統會將購買請求傳送給家長或監護人。」34 它以 Product.PurchaseResult.pending 的形式浮現,而核可是透過 Transaction.updates 送達,而非在呼叫端返回——因為該序列承載的是「在 App 之外發生的交易,例如『先詢問後購買』的交易」。35 拒絕則什麼都不會送達:「您的 App 不會收到交易,因為您拒絕了『先詢問後購買』。」34

七個專案中有三個在賣東西,而它們處理待處理狀態的方式各不相同:33

App 商品 .pending 的處理方式
ResumeGeni 月訂閱 具名的 .pending 結果,附有載明文件的清算路徑
Reps 訂閱,兩個級距 case .userCancelled, .pending: 併成同一條分支
Ace Citizenship 非消耗性商品 獨立的 case .pending: 回傳 false,與取消完全相同

三個裡有兩個把「監護人正在斟酌」回報成「使用者拒絕了」,而使用者實際看到的畫面比一個錯誤的標籤更糟。Reps 只在 purchase 回傳 true 時才關閉付費牆;Ace 也只在自家呼叫回傳成功時才放行。碰到 .pending,兩邊的呼叫都回傳不出任何可據以行動的東西,於是兩道付費牆就這麼開著:沒有錯誤、沒有提示、沒有轉圈——一次點下去毫無反應的死點。33 幾分鐘後家長核可了,交易落進一個監聽器,而介面從頭到尾未曾承認有人發過請求。

那條被併掉的分支,正是 PermissionKit 會在更高賭注下引誘您犯的同一個錯,而在 Apple 為這個模式配上第二個 API 之前,我已經犯過兩次。待處理不是什麼罕見分支。待處理就是同意流程從 App 內部看起來的樣子,而正確的預設做法,是一個您要繪製出來的狀態,而不是一個您回傳出去的值。

有一個 App 已經處在伺服器那一半的下游,這讓那條缺失的分支變得具體。ResumeGeni 的第 2 版端點自 2026 年 6 月 26 日以來至少記錄了 56 則通知,其中 55 則來自沙箱、1 則來自正式環境——我就是這樣確認兩組環境 URL 都已註冊,而不是只註冊了一組。它的處理程式點名了 13 種通知類型並派送其中八種,其餘五種只出現在註解裡。RESCIND_CONSENT 兩邊都不在,整個儲存庫的其他地方也找不到。33

常見問題

哪一個 API 是向監護人請求同意,哪一個是向成年人?

分屬不同框架,而且很容易搞反。家長同意走 PermissionKit:把 SignificantAppUpdateTopic 包進 PermissionQuestion(significantAppUpdateTopic:),用 PermissionButton 送出,透過 AskCenter.shared.responses(for:) 收回答覆。12162126 成年人確認則改走 Declared Age Range,也就是 showSignificantUpdateAcknowledgment(in:updateDescription:)27 請先檢查 requiredRegulatoryFeatures,因為透過 PermissionKit 詢問成年人會拋出 AskError.notAvailable711

沒有真實的家庭帳戶,要如何測試同意撤回?

啟用「開發者模式」,接著開啟「設定」→「開發者」→「沙箱 Apple 帳戶」,登入後選取該帳戶,點一下「管理」,選擇「撤回 App 同意」。輸入您的 bundle 識別碼並點一下「撤回同意」,系統會顯示「Notification Triggered」。11 只要已設定第 2 版 URL,您的伺服器就會收到 RESCIND_CONSENT,其中帶有 appData 物件,而該物件的 bundleIdenvironment 欄位可確認這則通知對應到正確的 App。311 沙箱每則通知只送一次且不重試,所以測試途中回了 50x 的端點不會有第二次機會。24

App 為什麼要在執行期監看自己的年齡分級?

因為 Apple 會在完全沒有您送審的情況下更改商店上的分級,而 Apple 寫的是德州法律將分級變更視為重大變更,接著要您用 Significant Change API 去請求家長同意。4 2026 年 6 月 18 日就是現成的例子:澳洲失去 15+ 級距,越南多了一套四級制,兩者都是依既有問卷答案套用到既有 App 上。937 Apple 沒有公布這個屬性的整數與分級級距之間的對照表,所以拿它跟您自己儲存的值比對,是它唯一支援的操作。10

系統已經擋了,而這正是多數實作忽略的重點。Apple 表示監護人撤回同意時,「Apple 會阻止該 App 啟動」,接著指引您用這則通知去處理該事件,而不是去執行封鎖。8 所以剩下的工作是伺服器形狀的:凍結帳戶狀態、清算任何訂閱,並停止向一台打不開您 App 的裝置推播。請用您對待帳戶刪除的方式對待它,而不是用您對待授權檢查失敗的方式。

重點整理

給 iOS 開發者: - 今天就去檢視您既有的 Product.PurchaseResult switch。「先詢問後購買」就是早已出貨的監護人同意機制,.pending 是它浮現的方式,而把該案例併進 .userCancelled,正是 PermissionKit 將在更高賭注下引誘您犯的那個缺陷。3435 - 如果您的流程需要區分已確認的成年人與自行宣告的成年人,請把預算抓在 iOS 26.5,而不是 26.4。36 - 抄 Apple 範例監聽器之前,先修掉那個 return;還有,絕不要讓 0 進到您儲存的 ageRatingCode 基準值裡。622

給要出貨到多個 Apple 平台的團隊: - 規劃工作之前,先看可用性表格。原生 macOS App 可以偵測到成年人確認機制適用,卻沒有任何 API 能把它呈現出來;tvOS 與 watchOS 讀得到 ageRatingCode,背後卻沒有任何同意 API。71027 - 把確認狀態依變更逐項存進 NSUbiquitousKeyValueStore,並用 AppTransaction.originalAppVersion 讓全新安裝的使用者免於面對早於他們的變更。619

給後端與發版負責人: - 在您需要之前,就先按帳戶記下 appTransactionID,然後即便是免費 App,也把第 2 版端點架起來。事後才建的端點,收到的撤回事件將無法歸屬到任何人身上。319 - 把重大變更的描述送到您產品文案負責人手上過一遍。那是監護人在決定您的 App 能否留住一位使用者時,唯一會讀到的一段文字。12


這個週期裡的四個執行點,有三個是踩在您控制得了的東西上:啟動畫面鍵值踩的是 SDK、@State 巨集踩的是工具鏈、社群媒體聲明踩的是一次送審。監護人同意踩的則是一份法院案件紀錄,和一張商店的分級表格。整個系列的彙整頁在 Apple 生態系系列

參考資料


  1. Apple, App Store Server Notifications changelog。在 2025 年 11 月 4 日的標題底下,New features:「Updated the responseBodyV2DecodedPayload to include the new payload object, appData」與「Added the notification type RESCIND_CONSENT to notificationType」。變更記錄中接下來的兩筆是 2025 年 12 月 10 日與 2026 年 4 月 27 日,都未觸及同意機制。2026 年 7 月 26 日自 Apple 文件 JSON 讀取,因為 HTML 頁面是透過 JavaScript 算繪的。 

  2. Apple, notificationType, App Store Server Notifications。RESCIND_CONSENT 定義的出處(「A notification type that indicates the parent or guardian has withdrawn consent for a child’s app usage」),也是本文所用計數的出處:該頁公布 23 個可能值(CONSUMPTION_REQUEST、DID_CHANGE_RENEWAL_PREF、DID_CHANGE_RENEWAL_STATUS、DID_FAIL_TO_RENEW、DID_RENEW、EXPIRED、EXTERNAL_PURCHASE_TOKEN、GRACE_PERIOD_EXPIRED、METADATA_UPDATE、MIGRATION、OFFER_REDEEMED、ONE_TIME_CHARGE、PRICE_CHANGE、PRICE_INCREASE、REFUND、REFUND_DECLINED、REFUND_REVERSED、RENEWAL_EXTENDED、RENEWAL_EXTENSION、RESCIND_CONSENT、REVOKE、SUBSCRIBED、TEST)。字串 RESCIND_CONSENT 在該頁 payload 中恰好出現一次,位於可能值清單內;「Handle use cases for In-App Purchase life-cycle events」底下的八張表格合計有 40 個事件列(含各自標題分別為 4、6、7、7、8、6、6 與 4 列),沒有任何一列提到它。請注意 REVOKE 指的是家人共享權益的喪失,而非同意的撤回:「an In-App Purchase the customer was entitled to through Family Sharing is no longer available through sharing」。2026 年 7 月 26 日自 Apple 文件 JSON 讀取。 

  3. Apple, appData, App Store Server Notifications,於 2.19 版引入。以下內容的出處:「The appData object is part of the responseBodyV2DecodedPayload. This object is present in the payload when the notificationType is RESCIND_CONSENT」,以及四個屬性:appAppleId(「available for apps that users download from the App Store. It isn’t present in the sandbox environment」)、bundleIdenvironmentsignedAppTransactionInfo(一個 JWSAppTransaction)。responseBodyV2DecodedPayload 頁面獨立陳述了這項排他性,將 appData 描述為一個「appears when the notificationType is RESCIND_CONSENT」的欄位,並補充「The data, appData, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields」。這兩頁是本文稱 RESCIND_CONSENT 為唯一 payload 帶有 appData 的通知類型的依據;字串 appDatanotificationType 頁面上完全沒有出現。 

  4. Apple, Next steps for apps distributed in Texas, Apple Developer News,2025 年 11 月 4 日。以下內容的出處:德州年齡分類(「under 13, 13-15, 16-17, or over 18」)、Apple 使用的框架名稱(「the Significant Change API under the PermissionKit framework」)、開發者責任句(「It’s the developer’s responsibility to determine when there’s a significant change to their app」)、年齡分級範例(「Texas state law considers a change in the age rating of an app to be a significant change, and developers should keep their age rating selections current in App Store Connect. When a developer updates their app’s age rating, the rating is updated on all user devices once the version is live」)、StoreKit 的定位(「Developers can use a new property type in StoreKit to automatically check when their app’s age rating has changed on a user’s device and then use the Significant Change API to request parental consent」)、撤回行為(「A parent or guardian in Texas can withdraw consent for any app, which will block launching of the app on the child or teen’s device」),以及四項「Next steps」實作清單。也是將 beta 可用性繫於 iOS 26.2 與 iPadOS 26.2 的出處。2026 年 7 月 26 日擷取。 

  5. Apple, Update for Apps Distributed in Texas, Apple Developer News,2026 年 6 月 3 日。以下內容的出處:禁制令解除(「Due to a recent court ruling lifting an injunction on Texas law SB 2420, new Apple Accounts in Texas are now subject to the law」)、適用範圍(「age assurance and parent or guardian consent on behalf of minors under the age of 18 for downloads, Apple In-App Purchases, and significant changes associated with an app. Parents or guardians will also be able to revoke their consent for any app they previously approved for their child」)、生效日期(「These changes will go into effect starting June 4, 2026」)、再次重申的開發者責任提醒,以及同樣的四項實作清單。2026 年 7 月 26 日擷取。 

  6. Apple, Implementing age assurance and permissions, Declared Age Range 範例程式碼。可用性:iOS 26.5、iPadOS 26.5、Mac Catalyst 26.5、Xcode 27.0 beta;文章要求在執行前先於「a device with iOS 26.4 or later」登入 iCloud。以下內容的出處:空集合行為(「When neither feature is present, the app skips the flow entirely」)、18 歲的年齡關卡、四個剖析出的類別(.minor.verifiedAdult 描述為「an adult with a confirmed payment method」、.unverifiedAdult 為「an adult without account verification」、.declinedSharing)、未驗證成年人的結果(「the app sets the phase to .blocked and prevents access until the person verifies their account in Settings」)、未成年人路徑建立 SignificantAppUpdateTopicPermissionQuestionPermissionButton 送出、本文逐字引用的 AskCenter.shared.responses(for:) 監聽器程式碼、拒絕行為(「When the parent denies the request, the app prevents the minor from using it」)、搭配 didChangeExternallyNotificationNSUbiquitousKeyValueStore 確認狀態追蹤(「so other devices don’t present the same flow again」),以及 originalAppVersion 豁免(「People who install the app when a significant change is already present don’t need to acknowledge it」)。該專案另需 Declared Age Range 能力與 iCloud 鍵值儲存服務。2026 年 7 月 26 日自 Apple 文件 JSON 讀取。 

  7. Apple, AgeRangeService.requiredRegulatoryFeaturesAgeRangeService.RegulatoryFeature, Declared Age Range。兩者皆自 iOS 26.4、iPadOS 26.4、Mac Catalyst 26.4 與 macOS 26.4 起可用,未列 visionOS、tvOS 或 watchOS。宣告:var requiredRegulatoryFeatures: Set<AgeRangeService.RegulatoryFeature> { get async throws },在「if the regulatory feature’s service is unavailable」時拋出 notAvailable。該列舉恰好公布三個案例:declaredAgeRangeRequired(「Indicates the person is required to share their age range with your app」)、significantAppChangeRequiresAdultNotification(「Indicates that adult users must acknowledge your app’s significant change」)與 significantAppChangeRequiresParentalConsent(「Indicates a parent or guardian is required to acknowledge and consent to a significant app change」)。 

  8. Apple, Age assurance frameworks Q&A, Apple Developer Support。以下內容的出處:SDK 下限(「you must build your app against the iOS 26.2 and iPadOS 26.2 SDKs, or later, with Xcode 26.2 (17C52) or later」)、舊帳戶的排除條款(「Existing Apple Accounts running iOS 18 and iPadOS 18, or earlier … won’t be affected」,其省略的子句指明「including adult and child accounts for kids and teens」)、責任歸屬的答覆(「Yes, developers are responsible for their own age restrictions」與「For questions about your compliance obligations, consult your legal counsel」)、本文引用兩次的地區範圍界定(「In certain regions, where legally required, Apple uses age assurance methods to confirm an Apple Account holder’s age and shares age categories with you through the Declared Age Range API. In those regions, you must check the age of the people using your app」,後文重述為「In regions where legally required, you need to check the age of the people using your app with the Declared Age Range API」)、存取義務(「For significant app updates, you’re responsible for preventing access to your app or features when required, and for handling the response from the parent or guardian. Until the parent provides consent, the child must be prevented from accessing the significant update, which may include all app and account data or specific features」)、撤回行為(「When a parent or guardian revokes consent for their child to access an app, Apple will prevent the app from launching. To handle consent revocations, use the RESCIND_CONSENT value from notificationType」)、App Review 的答覆(「No, there are no changes to the App Review process」),以及條款與隱私政策的答覆(「It depends. You determine what constitutes a significant app update based on applicable laws」)。2026 年 7 月 26 日讀取。 

  9. Apple, Upcoming changes to age ratings in Australia and Vietnam, Apple Developer News,2026 年 5 月 21 日。以下內容的出處:「Starting June 18, 2026, age ratings on the App Store will be updated in Australia and Vietnam」、澳洲的變更(「The 15+ age rating will no longer be available on the App Store in Australia. Apps currently rated 15+ with the following content descriptors will be updated to 16+」)及其三項內容描述元(無限制的網路存取;頻繁的醫療或治療資訊;戰利品箱),以及越南的變更(「To align with Article 38 of Vietnam Decree 147, apps available on the App Store in Vietnam will require a region-specific age rating. Based on your age rating questionnaire responses in App Store Connect, your app will receive one of four ratings (00+(all ages), 12+, 16+, or 18+)」)。兩項變更都沒有要求開發者送出版本。2026 年 7 月 26 日擷取。 

  10. Apple, AppStore.ageRatingCode, StoreKit。宣告 static var ageRatingCode: Int? { get async },自 iOS 26.2、iPadOS 26.2、macOS 26.2、tvOS 26.2、visionOS 26.2 與 watchOS 26.2 起可用,未列 Mac Catalyst。回傳值:「An integer representing the current age rating code, or nil if the age rating is unavailable」。比對定位的出處(「Use this property to fetch the age rating for your app and compare it with the last known age rating to check if it has changed」),以及串接 PermissionKit 的出處(「If your app’s age rating has changed, consider informing parents or guardians by using the Significant Change API」)——這句話中「Significant Change API」是連結文字,Apple 的 SignificantAppUpdateTopic 頁面是連結目標。Apple 在該頁的範例,是圍繞該屬性的一個 guard let,在值不可用時印出訊息。2026 年 7 月 26 日於 Apple 文件中搜尋這些整數與分級級距(4+、9+、13+、16+、18+)的對照,在該頁、AppStore 型別頁面,以及 App Store Connect Help 的年齡分級參考中都沒有找到。 

  11. Apple, Testing age assurance in sandbox, StoreKit。以下內容的出處:裝置上的操作路徑(「設定」→「開發者」→「沙箱 Apple 帳戶」→「管理」,接著是「Age Assurance or Revoke App Consent」)、六列測試矩陣(其中 18 歲以上各列回傳下限 18、無上限,年齡宣告為 selfDeclaredconfirmed)、詢問成年人的行為(「For 18+ test cases, PermissionKit throws AskError.notAvailable rather than returning a PermissionChoice. Calling AskCenter.ask(_:) for an adult user throws this error because they don’t meet the requirements for parental permission requests」)、以確認訊息「Notification Triggered」與「A notification will be sent to the developer server soon」作結的撤回步驟,以及 payload 註記(「your server receives a RESCIND_CONSENT notificationType. The notification payload includes an appData object with app metadata, including the bundleId and environment fields」)。矩陣中的三列未成年人分別是未滿 13 歲已核可、13 至 15 歲已核可、16 至 17 歲已拒絕,年齡宣告皆為 guardianDeclared。 

  12. Apple, SignificantAppUpdateTopic, PermissionKit。自 iOS 26.2、iPadOS 26.2、Mac Catalyst 26.2、macOS 26.2 與 visionOS 26.2 起可用;宣告為 struct SignificantAppUpdateTopic,符合 QuestionTopic,具 init(description: String)。以下內容的出處:定義的推託(「You determine what constitutes a significant update based on applicable regulations」)、描述撰寫指引(「Use concise, understandable language that clearly explains what changed in your app. Parents and guardians see this description when deciding whether to grant permission」),以及本文逐字重現的 Specific/Vague 程式碼中的兩則註解。 

  13. Apple, New requirements for apps available in Texas, Apple Developer News,2025 年 10 月 8 日。以下內容的出處:最初的公告(「Beginning January 1, 2026, a new state law in Texas … introduces age assurance requirements for app marketplaces and developers」,省略處指的是 SB2420)、家人共享要求(「All new Apple Accounts for users under the age of 18 will be required to join a Family Sharing group, and parents or guardians will need to provide consent for all App Store downloads, app purchases, and transactions using Apple’s In-App Purchase system by the minor」),以及對猶他州與路易斯安那州的預告(「Similar requirements will come into effect later next year」)。2026 年 7 月 26 日擷取。 

  14. Apple, Update on age requirements for apps distributed in Texas, Apple Developer News,2025 年 12 月 23 日。以下內容的出處:禁制令(「A recent injunction issued by a district court suspended enforcement of Texas state law SB2420 … In light of this ruling, Apple will pause previously announced implementation plans and monitor the ongoing legal process」)、四項工具在沙箱中持續可用,以及它們對猶他州與路易斯安那州的擴充。2026 年 7 月 26 日擷取。 

  15. Apple, Age requirements for apps distributed in Brazil, Australia, Singapore, Utah, and Louisiana, Apple Developer News,2026 年 2 月 24 日。以下內容的出處:18+ 下載限制(「Starting February 24, 2026, Apple will block users in Australia, Brazil, and Singapore from downloading apps rated 18+ unless they have been confirmed to be adults through reasonable methods. The App Store will perform this confirmation automatically. However, developers may have separate obligations to independently confirm that their users are adults」)、猶他州與路易斯安那州的日期(「For users with new Apple Accounts in Utah as of May 6, 2026, and in Louisiana as of July 1, 2026, age categories will be shared with the developer’s app when requested through the Declared Age Range API」)、本文引用的擴充句及其後的四個連結(「The tools we previously announced have been expanded to help developers meet compliance obligations for Louisiana and Utah, including:」Declared Age Range API、PermissionKit 底下的 Significant Change API、StoreKit 中新的年齡分級屬性型別、App Store Server Notifications)、巴西戰利品箱的後果,以及 Significant Update Action 的首次公開命名(「Developers can use the Declared Age Range API to present significant update notifications to adults in these states through the Significant Update Action, now in beta」)。2026 年 7 月 26 日擷取。 

  16. Apple, PermissionQuestionexpirationDate, PermissionKit。final class PermissionQuestion<Topic> where Topic : QuestionTopic,自 iOS 26.0 起可用,具四個初始化器:init(handle:)init(handles:)init(communicationTopic:)init(significantAppUpdateTopic:),最後一個於 iOS 26.2 引入,描述為建立「a permission question that asks parents or guardians for permission to continue using your app after a significant update」。expirationDate 宣告為 final var expirationDate: Date?,討論段為「Once the date passes, the person that receives the question can no longer respond」。Apple 未公布該屬性的任何預設值,也未針對重大更新主題提供任何設定指引。 

  17. Apple, Creating a communication experience, PermissionKit。本文引用的取消行為出處:「At any point during the send request flow, the child has the option to cancel the request, and decide not to send the question to their parent or guardian. In this scenario, the system doesn’t deliver a response to the calling app for that specific question」。也是該框架 iMessage 限制的出處,PermissionKit 首頁以 Important 提示陳述:「Communication experiences using the PermissionKit framework are only available using iMessage」。請注意該文只記錄了 CommunicationTopic 流程;Apple 未針對重大更新流程發布對應文章,而該文的程式碼範例呼叫的 CommunicationLimits.current.permissionResponses,截至 2026 年 7 月 26 日在 Apple 文件中回傳 404。 

  18. Apple, AskError, PermissionKit。enum AskError,符合 LocalizedError,共六個案例。四個有摘要說明且於 iOS 26.1 登場:unknowncommunicationLimitsNotEnabled(「Indicates communication limits isn’t enabled to send permission requests」)、contactSyncNotSetupinvalidQuestion。兩個在自身頁面上沒有摘要:iOS 26.1 的 systemError(underlyingError:),以及 iOS 26.2 的 notAvailable,後者與 SignificantAppUpdateTopic 同時登場。notAvailable 的意義只出現在註 11 引用的沙箱測試文章中;systemError 至少在簽章中點出了成因。communicationLimitsNotEnabledcontactSyncNotSetup 是否也會在重大更新的詢問中出現,文件未說明。 

  19. Apple, AppTransaction 上的 appTransactionIDoriginalAppVersion, StoreKit。以下內容的出處:識別碼語意(「The App Store generates a single, globally unique appTransactionID for each Apple Account that downloads your app and for each family group member for apps that support Family Sharing」)、它在重新下載、退款、重新購買與商店地區變更後的穩定性、它在 App Store Server Notifications 第 2 版 payload 中的存在,以及對免費 App 至關重要的那一句:「The appTransactionID is available even if a customer makes no in-app purchases」。originalAppVersion 是「The app version that the customer originally purchased from the App Store」,在 macOS 上帶 CFBundleShortVersionString、其他平台帶 CFBundleVersion,且在沙箱環境中一律為 1.0。對應的伺服器端型別是 App Store Server API 中的 appTransactionId,於 1.15 版引入。 

  20. Apple, CommunicationLimitsupdates, PermissionKit。updates 宣告為 final var updates: some AsyncSequence<PermissionResponse<CommunicationTopic>, Never> { get },摘要為「Registers the communication topic with the system, so your app can be launched on-demand in the background to receive permission updates」。Apple 文件將 updates 與兩個 CommunicationLimits.ask(_:in:) 多載歸在「Deprecated APIs」標題下;該類別本身則因 isKnownHandle(_:)knownHandles(in:) 仍屬現行。取代它的序列丟掉了那項承諾:AskCenter.responses(for:) 的摘要與討論段完全沒有提到背景啟動,而這正是本文所指出的文件缺口全貌。 

  21. Apple, AskCenterresponses(for:), PermissionKit,兩者皆自 iOS 26.2、iPadOS 26.2、Mac Catalyst 26.2、macOS 26.2 與 visionOS 26.2 起可用。AskCenter 是透過 static let shared 取得的 final class,描述為將「your questions through the appropriate family sharing channels」路由,並在「parents make their decisions」時將回應送回您的 App。responses(for:) 宣告為 final func responses<Topic>(for topicType: Topic.Type) -> some AsyncSequence<PermissionResponse<Topic>, Never> where Topic : QuestionTopic,摘要為「Registers the topic type with the system and returns an asynchronous sequence of responses」,完全未提背景啟動。存在四個 ask(_:in:) 多載:兩個在 iOS、iPadOS 與 visionOS 上接受 UIViewController,兩個在 macOS 上接受 NSWindow,每種主題型別各一。PermissionResponse 公開 choicequestionPermissionChoice.Answer 恰好公布兩個案例,approvaldenial。 

  22. Apple 員工在 AppStore.ageRatingCode always returns 0 on real device 的回覆,Apple Developer Forums。原始貼文為 2026 年 4 月,回報在執行 iOS 26.4 的實體裝置上、已登入沙箱帳戶且已於 App Store Connect 設定年齡分級的情況下讀到 0。該回覆的作者標示為 Apple Staff,內容為:「The ageRatingCode API should be used to observe changes to your app’s age rating over time by comparing to its last known value. If your app’s age rating code has changed, consider informing parents or guardians by using the Significant Change API」,以及「A value of 0 is expected during development of your app when it is built and run from Xcode and Sandbox environments (including TestFlight)」。2026 年 7 月 26 日擷取兩次,措辭一致;該論壇透過 JavaScript 算繪,且以相對時間戳「1w」而非日期顯示回覆的時間,因此此處不報告發布日期。開發者論壇的回覆是比文件頁面更弱的證據,而 Apple 針對 ageRatingCode 的文件完全沒有提到 0 這個值。本文從中得出的推論——把 0 存為基準值會在第一個 App Store 版本上製造出一次假的變更——是我依據該回覆與 Apple 記載的比對模式所做的推斷。 

  23. Apple, Enabling App Store Server Notifications。以下內容的出處:TLS 下限(「your server must support the Transport Layer Security (TLS) 1.2 protocol or later」)、在 App Store Connect 中依環境設定 URL、連接埠限制(443,或 1024 以上),以及允許清單網段(「add the IP address subnet 17.0.0.0/8」,且「applies to both the sandbox and the production environments」)。姊妹篇 Receiving App Store Server Notifications 說明了經 JWS 簽署的 signedPayload,且截至 2026 年 7 月 26 日只提到 data 物件,未提及 appData。 

  24. Apple, Responding to App Store Server Notifications。以下內容的出處:成功代碼(「Send HTTP 200, or any HTTP code between 200 and 206」)、重試觸發條件(「Send HTTP 50x or 40x to have the App Store retry the notification」)、第 2 版重試排程(「it retries five times, at 1, 12, 24, 48, and 72 hours after the previous attempt」)、沙箱限制(「Retry notifications are available only in the production environment. In the sandbox environment, the App Store server attempts to send the notification one time」),以及透過 Get-Notification-History 的補救路徑。 

  25. Apple, subtype, App Store Server Notifications。該頁公布 19 個可能值(ACCEPTED、ACTIVE_TOKEN_REMINDER、AUTO_RENEW_DISABLED、AUTO_RENEW_ENABLED、BILLING_RECOVERY、BILLING_RETRY、CREATED、DOWNGRADE、FAILURE、GRACE_PERIOD、INITIAL_BUY、PENDING、PRICE_INCREASE、PRODUCT_NOT_FOR_SALE、RESUBSCRIBE、SUMMARY、UPGRADE、UNREPORTED、VOLUNTARY),每一個都限定於某個具名的通知類型。字串 RESCIND_CONSENT 在該頁 payload 的任何位置都未出現,2026 年 7 月 26 日讀取。 

  26. Apple, PermissionButton, PermissionKit。@MainActor @preconcurrency struct PermissionButton<Topic, Label> where Topic : QuestionTopic, Label : View,自 iOS 26.2、iPadOS 26.2、Mac Catalyst 26.2、macOS 26.2 與 visionOS 26.2 起可用,具兩個 init(question:label:) 多載,分別受限於 CommunicationTopicSignificantAppUpdateTopic。它取代了 CommunicationLimitsButton,Apple 在框架頁面上將後者列於「Deprecated APIs」之下。 

  27. Apple, AgeRangeService.showSignificantUpdateAcknowledgment(in:updateDescription:)SignificantUpdateAction,以及 SwiftUI 環境值 showSignificantUpdateAcknowledgment。該方法宣告為 @MainActor func showSignificantUpdateAcknowledgment(in windowScene: UIWindowScene, updateDescription: String) async throws,公布 iOS 26.4、iPadOS 26.4 與 Mac Catalyst 26.4,未列 macOS;AgeRangeService 在「Displaying update acknowledgments」底下只列出這一個多載。SignificantUpdateAction 與該環境值公布相同的三個平台。Apple 在該方法上的 Important 提示:「Before calling this function, check RegulatoryFeature to determine if a person must acknowledge your significant app change」。該環境值的討論段另外提到,您要「Call this action from a Button or onAppear(perform:)」。 

  28. Apple, requestAgeRange(ageGates:::in:), Declared Age Range。Apple 公布兩個多載:一個在 iOS 26.0、iPadOS 26.0 與 Mac Catalyst 26.0 上接受 in viewController: UIViewController,另一個在 macOS 26.0 上接受 in window: NSWindow。年齡區間請求存在 NSWindow 版本,而註 27 所引的確認表單卻沒有,這是把 macOS 的缺漏讀成疏漏而非政策決定的依據;Apple 兩種說法都未曾發布。 

  29. Apple, Requesting people’s age range information in your app, Declared Age Range。本文引用的關卡算術出處(「您最多可指定三道年齡關卡,藉此建立最多四個可能的年齡區間」)、間距限制(「每個區間的長度至少必須為兩年」),以及邊界語意(「當 lowerBound 值為 nil 時,代表此人低於您設定的最低年齡關卡」與「當 upperBoundnil 時,代表此人達到或超過您設定的最高年齡關卡」)。以 13、16、18 設定關卡,回傳的正好是 Apple 為德州公布的那四個區間,且兩個有界區間都滿足兩年下限:13 至 15 跨三年,16 至 17 跨兩年。該文另警告,一個人的帳戶所屬地區會「決定系統用來回傳年齡區間的那幾道年齡關卡,而這可能與您在請求中指定的年齡關卡不同」。2026 年 7 月 26 日自 Apple 文件 JSON 讀取。 

  30. 作者調查,2026 年 7 月 26 日:平台相關主張的建立方式。平台值讀自各專案的 SUPPORTED_PLATFORMS 建置設定,而非部署目標鍵值(Xcode 無論目標為何都會寫入後者):Reps 宣告 appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator 加上另一個 watchos watchsimulator target,Ace Citizenship 只宣告 iphoneos iphonesimulator。這個方法會低報平台,而 Return 就是證據:Return 唯一的 SUPPORTED_PLATFORMS 值是 iphoneos iphonesimulator macosx xros xrsimulator,而它的 TV 與 watch target 帶的是 SDKROOT = appletvosSDKROOT = watchos,所以掃 SUPPORTED_PLATFORMS 兩者都看不到。因此本文中每一項「有出貨到平台 X」的主張,都出自 App Store Connect 而非建置設定。Return:TV_OS 1.0 與 1.0.1 皆為 READY_FOR_DISTRIBUTION,IOS 與 MAC_OS 1.0.1 亦然,且 iOS target 透過 Embed Watch Content 階段內嵌了 ReturnWatch Watch Appcom.941apps.Return.watchkitapp)——watch App 就是這樣上到手腕的。Reps:IOS 1.1 與 MAC_OS 1.1 為 READY_FOR_DISTRIBUTION,TV_OS 從未有任何版本到達該狀態,TV_OS 1.2 自 2026 年 6 月 2 日起停在 WAITING_FOR_REVIEW,所以 Reps 出貨的是 iOS 與 macOS。 

  31. 作者調查,2026 年 7 月 26 日,於 macOS 26.5.2(build 25F84),搭配 Xcode 26.6(build 17F113)與 Swift 6.3.3。篩選規則在此載明,好讓範圍可被檢核與重現,而非請人照單全收:我自己的 agent 設定檔(~/.claude/CLAUDE.md)中「Active Projects」表格內背後有 Xcode 專案的每一列,恰好得出七個:RepsReturnBanana List(以 Get Bananas 之名上架)、Ace-CitizenshipWaterResumeGeniAppYawara。這條規則同時也是本次調查的瑕疵,因為 Randori 在該表格中沒有任何一列,而 Randori 正是後來證明最關鍵的專案。七個當中有五個有 App Store Connect 記錄(Reps 6776044339、Return 6756242021、Get Bananas 6756241534、Ace Citizenship 6532592671、ResumeGeni 6771154645);WaterYawara 在該帳戶的 18 個 App 中都找不到,所以兩者都從未送審,稱它們為 App 都言過其實。各專案的 Swift 檔數:77、57、55、26、34、71 與 143。同意機制掃描涵蓋 *.swift*.entitlements*.plistproject.pbxproj:85、65、63、34、38、74 與 149,合計 508。要重現這些數字,需要排除的目錄名稱是八個而非六個:buildDerivedData.buildPods.gitworktrees,以及僅在 Reps 中的 .venvReps/.venvReps/server/.venv 共 142 個 property list)與 .xcode-state-backups(18 個)。只排除前六個,Reps 會讀成 245、總數會讀成 668,所以這份排除清單是有承重作用的,而它所依賴的每個名稱都印在這裡。16 個樣式,全部區分大小寫:PermissionKitCommunicationLimitsAskPermissionAskCenterSignificantAppUpdateTopicSignificantUpdateActionPermissionTopiccom.apple.developer.family-controlsFamilyControlsManagedSettingsDeviceActivityAuthorizationCenterCKSharesharedCloudDatabasepublicCloudDatabaseageRatingCode。七個專案中零檔案命中,含 AskCenter 在內。兩個對照樣式跑過完全相同的管線回傳非零(StoreKit 在 Reps 命中三個檔案,CKContainer|NSPersistentCloudKitContainer|SwiftData 在 Banana List 命中 17 個),我因此確定該管線確實在讀檔,而不是無聲失敗。Ace Citizenship 的導覽流程(IntroCarouselViewWelcomeViewAddStateViewAddRepresentativeView)不含任何年齡或出生日期欄位,其 PrivacyInfo.xcprivacy 宣告了空的 NSPrivacyCollectedDataTypes;「年滿 18 歲」的資格說明出自 ~/Projects/acecitizenship.app/content/blog/n400-application-guide.md。 

  32. 作者調查,2026 年 7 月 26 日:擴大掃描與 Randori。擴大掃描以同樣的 16 個樣式跑遍 ~/Projects 底下每一個儲存庫,排除同樣的八個名稱加上 node_modules,並排除被 gitignore 的路徑,共命中 21 個檔案。其中七個是程式碼:Randori/Randori/ 底下六個 Swift 檔,以及 _archive/Oishii-AI/Oishii AI/Services/CloudKitManager.swiftpublicCloudDatabase,位於一個已封存的專案中)。另外 14 個是文字或機器狀態:九份 Randori 的規劃與設計文件、本站 content/blog/ 中的三篇文章(其中一篇是本文的草稿)、一則 Obsidian 移交筆記,以及由指令碼而非人寫入的 obsidian-signals/40-Projects/blakecrosley-com/wwdc-2026/.pulse_state.json。Randori 的識別碼是 com.wayofyawara.randori,App Store Connect app 6789693294,1.0 版處於 PREPARE_FOR_SUBMISSION。CKSharesharedCloudDatabase 在六個檔案共命中 20 行:ConnectionStore.swift(10)、RandoriApp.swift(5)、ProfileCardView.swift(2),以及 CKPostTransport.swiftCloudShareSheet.swiftSocialContracts.swift 各一行。ConnectionStore 在標頭註解中自述其形狀為「one zone (ProfileCardZone), one record type (ConnectionCard), one share」,同時持有 privateCloudDatabasesharedCloudDatabase,並在第 355 行實作 accept(_ metadata: CKShare.Metadata)、第 547 行實作 ensureOutboundShare(),以及按帳戶的封鎖與解除封鎖。RandoriApp.swift 在 app delegate(第 73 行)與 window scene delegate(第 97 行,註解為「Warm: the app is running when the link is opened」)上都實作了 userDidAcceptCloudKitShareWith,而 RandoriSceneDelegate.scene(_:willConnectTo:options:) 在第 88 行、註解「Cold start: the invitation rides the connection options」底下讀取 connectionOptions.cloudKitShareMetadata——本文因此把冷啟動歸於 connection options 而非接受回呼。TagConsentStore 是這個 App 自己的同意帳本,範圍限定於 iCloud 帳戶,其載明的預設是失敗即關閉:政策記錄尚未送達的夥伴無法被標記。Randori/Randori.entitlements 請求 CloudKit、HealthKit 與 aps-environment。除了那兩個 CloudKit 共享樣式之外,所有同意相關樣式在該儲存庫都回傳零,StoreKitsignedPayloadnotificationType 亦然,所以這個 App 什麼都不賣,也沒有自己的伺服器;docs/asc-metadata.md 準備的是免費定價與 4+ 年齡分級。 

  33. 作者調查,2026 年 7 月 26 日:StoreKit 呼叫點與伺服器證據。StoreKit 出現在三個專案:Reps/Reps/Services/RepsProStore.swift(自動續訂,兩個級距)、Ace Citizenship/StoreKitManager.swift(一項非消耗性商品)與 ResumeGeni/Subscription/(一項月訂閱,伺服器端把關)。表格中引用的 .pending 處理分別位於 RepsProStore.swift:134case .userCancelled, .pending:)、StoreKitManager.swift:69-73(獨立的 case .pending:,其 return false 在第 73 行)與 SubscriptionStore.swift:209-212(具名的 pending 結果)。真正把使用者晾在原地的是呼叫點:RepsProPaywallView.swift:359-361 只在 if await store.purchase(plan) 內部關閉,ContentView.swift:484-485 只在 if success 內部解鎖,所以待處理購買回傳的 false 在兩個 App 的畫面上都不會造成任何改變。ResumeGeni 則走到 PaywallView.swift:526,一條 .pending 分支,會顯示提示「Waiting for approval. You’ll get access once it’s approved.」。ResumeGeni 的 App Store Server Notifications 端點是 ~/Projects/resumegeni/app/routers/appstore.py 中的 POST /api/appstore/notificationsPOST 一筆 {"signedPayload":"probe"} 會回傳 HTTP 400 {"status":"invalid"},而這只有通過啟用旗標、進到 Apple 的憑證鏈驗證器之後才到得了,GET 則回 405。兩組環境 URL 都已註冊,而其證據是實際送達而非一份操作手冊:941-analytics D1 資料庫的 funnel_events 表中,有 56 列 platform='ios' 且中繼資料帶有 notification_type,時間跨越 2026-06-26T15:48:51Z 至 2026-07-25T16:40:11Z,其中 55 列的 environmentsandbox,1 列為 production(2026-07-21T18:08:21Z)。請把 56 視為下限,因為該服務只把五個事件名稱對應到 funnel 列;實際觀察到的四種類型是 DID_RENEW(42)、SUBSCRIBED(6)、EXPIRED(5)與 DID_CHANGE_RENEWAL_STATUS(3)。app/services/app_store_notification_service.py 點名了 13 種通知類型,其中八種在 _funnel_event_name(第 75 至 89 行)中有可執行的派送分支:SUBSCRIBED、OFFER_REDEEMED、DID_RENEW、REFUND、REVOKE、EXPIRED、DID_CHANGE_RENEWAL_STATUS 與 DID_FAIL_TO_RENEW。其餘五種只出現在註解裡:第 73 行的 PRICE_INCREASE、RENEWAL_EXTENDED 與 METADATA_UPDATE,第 187 行的 EXTERNAL_PURCHASE_TOKEN 與 TEST。RESCIND_CONSENTCONSUMPTION_REQUESTREFUND_DECLINEDREFUND_REVERSED 在整個儲存庫都零命中。為求完整,也說明什麼不算證據:docs/SUBSCRIPTION_GO_LIVE.md:83 確實寫了「務必同時設定正式環境與沙箱環境兩組 URL」,但那一行位於一個註明該步驟「需要您重新驗證」的標題之下,所以它記錄的是意圖而非已完成的註冊,上述註冊主張改以實際送達的通知為依據。 

  34. Apple, Testing Ask to Buy in Xcode, StoreKit。Apple 對該機制描述的出處(「With Ask to Buy, when a child wants to make an eligible purchase or download, the system sends the purchase request to the parent or guardian」),以及拒絕行為的出處(「Your app doesn’t receive a transaction because you declined Ask to Buy」)。該文也記錄了 StoreKit 設定編輯器中 Purchase Options 底下的「先詢問後購買」開關,而交易管理員會顯示 Pending Ask to Buy、Ask to Buy Approved 與 Ask to Buy Declined。 

  35. Apple, Product.PurchaseResult.pendingTransaction.updates, StoreKit,兩者皆自 iOS 15.0 起可用。以下內容的出處:該案例的摘要(「The purchase is pending, and requires action from the customer」)、解決路徑(「If a pending purchase succeeds, StoreKit delivers the resulting Transaction in the transaction updates」),以及該序列的用途(「This sequence receives transactions that occur outside of the app, such as Ask to Buy transactions, offer code redemptions, and purchases that customers make in the App Store」)。Product.PurchaseResult 列舉公布三個案例:success(_:)pendinguserCancelled。Apple 在該列舉頁面上的範例,把 pending 分支註解為「The purchase requires action from the customer. If the transaction completes, it’s available through Transaction.updates」。 

  36. 上方路由程式碼中的每一個符號,皆於 2026 年 7 月 26 日對照 Apple 文件 JSON 驗證。AgeRangeService.sharedstatic let shared: AgeRangeService(iOS 26.0)。SwiftUI 環境值為 requestAgeRangevar requestAgeRange: DeclaredAgeRangeAction { get }(iOS 26.0),以及 showSignificantUpdateAcknowledgmentvar showSignificantUpdateAcknowledgment: SignificantUpdateAction { get }(iOS 26.4)。程式碼中的 @available(iOS 26.5, *) 下限並非來自這兩者:AgeRangeService.AgeRangeDeclaration.confirmed 公布 iOS 26.5、iPadOS 26.5、Mac Catalyst 26.5 與 macOS 26.5,比確認動作晚一個版本,所以任何要區分已確認成年人的程式碼都會繼承這個較高的下限。Apple 自家的範例專案也公布同樣的 26.5 可用性。6 AgeRangeService.AgeRange 公開 lowerBoundupperBound、宣告為 var ageRangeDeclaration: AgeRangeService.AgeRangeDeclaration?ageRangeDeclaration,以及 activeParentalControls。對該 optional 做 == .confirmed 比較是合法的,因為依其關係區段,AgeRangeDeclaration 符合 EquatableHashablePermissionButton 的初始化器是 init(question: PermissionQuestion<Topic>, @ViewBuilder label: @escaping () -> Label),在此處使用的多載中受限於 SignificantAppUpdateTopic26 ChangedFeatureViewAccountVerificationPrompt 是您自己視圖的佔位名稱,不是 Apple 的符號。 

  37. Apple, Age ratings values and definitions, App Store Connect Help,2026 年 7 月 26 日讀取,作為註 9 所宣布的 2026 年 6 月 18 日變更已然生效的佐證來源。在「Australia age rating values」底下,表格現在只公布 16+ 與 R 18+ 兩個分級,沒有 15+ 那一列。另有一個獨立的「Vietnam age rating values」區段,開頭為「As required by Article 38 of Vietnam Decree 147」,其 00+ 列定義為「contain no objectionable material but may contain instances of the following content」的 App,並列出家長控制、年齡驗證、使用者產生內容、訊息與聊天、廣告,以及不頻繁的競賽。Apple 5 月 21 日針對澳洲遷移所列的描述元清單,與本頁目前的 16+ 觸發清單並不吻合;這項出入我尚未釐清,也不倚賴它,因為此處提出的主張僅止於 15+ 級距已消失,且越南表格確實存在。 

相關文章

App Store 的社群媒體勾選欄,以及它的代價

2026年9月上路的 App Store 規定,要求每一次送審都必須聲明社群媒體功能。13 歲以下的豁免條款,代價是一項 entitlement、一套 API,外加一段分支邏輯。

9 分鐘閱讀

Xcode 27 捨棄 Intel:哪些終止、哪些仍能出貨

Xcode 27 僅能在 Apple silicon 上執行,仍可將 Universal app 回溯部署至 macOS 12,卻在部署目標達 27.0 時悄悄把 x86_64 從 ARCHS_STANDARD 移除。

12 分鐘閱讀