Ariver
2026-07-02 01bab5422712299b49f656e6b4c42a3bf314f519
Release 8.3.5 free tag order persistence
8 files modified
166 ■■■■■ changed files
CODEGRAPH.md 6 ●●●● patch | view | raw | blame | history
src/Apptag/ContentView.swift 15 ●●●● patch | view | raw | blame | history
src/Apptag/DataLayer.swift 1 ●●●● patch | view | raw | blame | history
src/Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
src/Apptag/PreferencesView.swift 9 ●●●●● patch | view | raw | blame | history
src/CHANGELOG.md 6 ●●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-07-01-pro-custom-container-quota-prd.md 80 ●●●●● patch | view | raw | blame | history
src/Scripts/pro_custom_container_quota_qa.sh 45 ●●●● patch | view | raw | blame | history
CODEGRAPH.md
@@ -42,7 +42,7 @@
  - Theme tab 只写视觉偏好 `appGridThemeID` 和炫彩主题调色值,炫彩滑轨只能影响 AppGrid 背景,不得改变标签、排序、备注或分类数据。
  - 免费用户预览 Pro 主题时,Theme tab 状态行和当前预览主题卡片显示基于 `ProThemePreviewState.endsAt` 推导的 `MM:SS` 倒计时;倒计时到期只退出预览,不弹阻塞提示。
  - Pro tab 展示免费版 / Pro 版对比表;对比表不显示独立标题,Pro 列标题前必须有线型皇冠标识。
  - Pro tab 对比表包含自定义标签颜色、彩色容器 / 彩色网格、高级主题、自定义快捷键、自定义应用备注和数据导入导出 / 分类与布局备份恢复;免费列锁定功能显示“不可用”,彩色容器 / 彩色网格和高级主题显示“只可预览 5 分钟”,备注显示“免费 3 个”;Pro 列未购买时显示带勾的“支持”,备注行显示带勾的“无限备注”,已购买后功能行显示带勾的“已解锁”。表格内免费列和 Pro 列的表头及状态胶囊都在列内左对齐;所有状态胶囊使用与“免费 3 个”一致的中性样式,不显示皇冠图标或黄色 Pro 背景。
  - Pro tab 对比表包含可维护标签数量、容器内 App 图标位置排序、自定义标签颜色、彩色容器 / 彩色网格、高级主题、自定义快捷键、自定义应用备注和数据导入导出 / 分类与布局备份恢复;标签列表自身顺序不列入 Pro 对比,因为免费用户也可持久保存。免费列锁定功能显示“不可用”,彩色容器 / 彩色网格和高级主题显示“只可预览 5 分钟”,备注显示“免费 3 个”;Pro 列未购买时显示带勾的“支持”,备注行显示带勾的“无限备注”,已购买后功能行显示带勾的“已解锁”。表格内免费列和 Pro 列的表头及状态胶囊都在列内左对齐;所有状态胶囊使用与“免费 3 个”一致的中性样式,不显示皇冠图标或黄色 Pro 背景。
  - About tab 保留顶部固定 Pro 状态行;主内容只保留应用信息、帮助文档和联系信息,不放 Pro 汇总卡或免费 / Pro 差异表。
## AppGrid 显示与交互
@@ -202,7 +202,7 @@
- `src/Scripts/pro_localization_qa.sh`
  - Pro 文案 29 语种静态门禁;检查 `pro.*`、设置页 Pro 状态/对比表、自定义标签颜色、Quick Search 快捷键页和气泡提示设置键完整、占位符完整、无生成污染和非英文语言无英文兜底复制。
- `src/Scripts/pro_feature_gate_qa.sh`
  - Pro 功能源码门禁;检查 StoreKit 权益、老用户自动 Pro、主题门禁、导入导出硬拦截、3 条备注额度、自定义标签颜色、自定义快捷键、排序预览、Pro UI 提示、设置页线型图标、轻量 Pro 权益行、Pro 页签、免费列“不支持”状态和 About 页不回归 Pro 汇总卡。
  - Pro 功能源码门禁;检查 StoreKit 权益、老用户自动 Pro、主题门禁、导入导出硬拦截、3 条备注额度、自定义标签颜色、自定义快捷键、容器内 App 排序预览、Pro UI 提示、设置页线型图标、轻量 Pro 权益行、Pro 页签、免费列“不支持”状态和 About 页不回归 Pro 汇总卡。
- `src/Scripts/pro_theme_preview_countdown_qa.sh`
  - Pro 主题 / 彩色显示模式预览倒计时门禁;检查状态行倒计时、当前预览卡片 `MM:SS`、到期清理、29 语种 `%time%` 占位符和禁止写死体验时长。
- `src/Scripts/pro_display_mode_preview_qa.sh`
@@ -227,7 +227,7 @@
- 不得擅自改变源码根目录边界:App 源码只在 `src/` 下维护。
- 不得把主题、视觉偏好、窗口状态混入 `TagDatabase` 的分类/标签业务数据。
- 主题变化只允许影响 AppGrid 视觉,不得改变标签数据、排序、备注、SmartStart/category scheme、Quick Search、拖拽语义或导入导出。
- Pro 门禁不得写脏免费用户真实数据:主题预览不写 `appGridThemeID`,排序预览不写 `containerAppOrder`,第 6 个新备注不写 `appNotes` / `appNoteMetadata`,导入 / 导出拦截必须早于文件面板和文件读写。
- Pro 门禁不得写脏免费用户真实数据:主题预览不写 `appGridThemeID`,容器内 App 排序预览不写 `containerAppOrder`,第 6 个新备注不写 `appNotes` / `appNoteMetadata`,导入 / 导出拦截必须早于文件面板和文件读写。
- 自定义标签颜色是 Pro 功能:免费用户可继续使用基础色,已有自定义色继续显示,不能新增或修改为新自定义色;改回基础色必须允许且会清除 `customColor`。
- Pro 状态 UI 必须优先使用设置页签下方固定位置的统一 header;不得在各 tab 内重复塞第二条 Pro 引导。About 不承载 Pro 汇总卡或免费 / Pro 差异说明,Data 不承载营销式大卡,避免挤压数据管理布局。
- 免费主题固定为默认、深蓝、黑色;其它主题只能预览或在 Pro 解锁后持久化。
src/Apptag/ContentView.swift
@@ -1630,7 +1630,7 @@
            .onEnded { _ in
                guard canReorderTag(tagName) else { return }
                dragItem = nil
                persistTagOrderIfAllowed()
                persistTagOrder()
            }
    }
@@ -2669,22 +2669,15 @@
        let hadDragState = tagNavDragModeActive || tagNavDragItem != nil
        guard hadDragState else { return }
        if tagNavDragModeActive && tagNavReorderDidMove {
            persistTagOrderIfAllowed()
            persistTagOrder()
        }
        tagNavDragModeActive = false
        tagNavDragItem = nil
        tagNavReorderDidMove = false
    }
    private func persistTagOrderIfAllowed() {
        guard proEntitlement.isUnlocked else {
            showDropWarning(message: tr("pro.tagSorting.previewToast"))
            return
        }
        guard TagEditor.reorderTags(draggedTagNames) else {
            showDropWarning(message: tr("pro.tagSorting.previewToast"))
            return
        }
    private func persistTagOrder() {
        _ = TagEditor.reorderTags(draggedTagNames)
    }
    private func cancelTagNavReorderVisualState() {
src/Apptag/DataLayer.swift
@@ -1943,7 +1943,6 @@
    /// Persist a new tag display order.
    @discardableResult
    static func reorderTags(_ names: [String]) -> Bool {
        guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return false }
        var store = TagDatabase.load()
        let previousStore = store
        store.tagOrder = names
src/Apptag/Info.plist
@@ -19,7 +19,7 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>8.3.2</string>
    <string>8.3.5</string>
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
@@ -32,7 +32,7 @@
        </dict>
    </array>
    <key>CFBundleVersion</key>
    <string>20260702.0325</string>
    <string>20260702.1046</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
    <key>LSMinimumSystemVersion</key>
src/Apptag/PreferencesView.swift
@@ -1049,15 +1049,6 @@
                proUnlockedStyle: .neutral
            ),
            ProTabComparisonFeature(
                title: tr("pro.feature.tagOrderPersistence"),
                freeStatusKey: "settings.proStatus.previewOnly",
                proLockedStatusKey: "settings.proStatus.supported",
                proUnlockedStatusKey: "settings.proStatus.supported",
                freeStyle: .neutral,
                proLockedStyle: .neutral,
                proUnlockedStyle: .neutral
            ),
            ProTabComparisonFeature(
                title: tr("pro.feature.containerAppOrderPersistence"),
                freeStatusKey: "settings.proStatus.previewOnly",
                proLockedStatusKey: "settings.proStatus.supported",
src/CHANGELOG.md
@@ -2,6 +2,12 @@
## [Unreleased]
## [8.3.5] — 2026-07-02
- 标签列表自身顺序不再作为 Pro 权益:免费用户拖动标签后也会持久保存容器显示顺序
- Pro 页免费 / Pro 对比表移除“自定义排序:标签在列表中的位置”这一行,保留容器内 App 图标位置排序、自定义快捷键等其它 Pro 权益
- 版本号更新为 `8.3.5`,Build 更新为 `20260702.1046`
## [8.3.2] — 2026-07-02
- 修复 App Store 从付费下载切换为免费下载 + Pro 内购时的旧付费用户迁移边界:`8.3.2` 之前通过 App Store 付费下载过 TagLauncher 的用户会自动保留 Pro 权益
src/Docs/Requirements/2026-07-01-pro-custom-container-quota-prd.md
@@ -1,18 +1,18 @@
# PRD:自定义标签额度、排序保存与 Pro 转化
日期:2026-07-01
状态:产品方案讨论稿,待研发评审与实现
状态:已实现,2026-07-02 按 8.3.5 版本决策修订
适用产品:TagLauncher macOS App
## 1. 功能名
免费版自定义标签额度、标签排序预览与 Pro 无限容器管理
免费版自定义标签额度与 Pro 无限容器管理
## 2. 需求描述
TagLauncher 的核心价值是用标签 / 容器管理 Mac 应用。为了提高 Pro 付费转化,需要把 Pro 权益更直接地绑定到核心功能上:免费用户可以体验默认智能分类和基础整理能力,但当用户开始搭建自己的长期容器体系时,应自然触发 Pro 升级理由。
本需求不采用“维护次数”限制。免费版限制的是可个性化维护的自定义标签 / 容器数量,以及标签拖动排序的保存能力。
本需求不采用“维护次数”限制。免费版限制的是可个性化维护的自定义标签 / 容器数量;标签列表自身拖动排序不再作为 Pro 权益,免费用户也可以保存容器显示顺序。
## 3. 背景与目标
@@ -26,12 +26,12 @@
| 目标 | 说明 |
|:---|:---|
| 提升 Pro 转化 | 把升级触点放在用户开始扩展自定义容器体系、希望沉淀长期排序习惯时。 |
| 提升 Pro 转化 | 把升级触点放在用户开始扩展自定义容器体系、需要容器内 App 图标排序等高级维护能力时。 |
| 保留新用户激活价值 | 默认智能分类仍可免费使用,避免下载后立刻被付费墙挡住。 |
| 规则可解释 | 用户应能理解“免费可维护 6 个自定义容器,Pro 不限”。 |
| 避免数据被扣押 | 老用户和导入数据不能被删除或隐藏;超额时应保留查看和启动能力。 |
| 避免惩罚试错 | 不限制改名、改色等操作次数,避免用户不敢整理。 |
| 保持体验可感知 | 免费用户可以预览标签拖动排序效果,但保存长期排序结果需要 Pro。 |
| 保持体验可感知 | 免费用户可以保存标签列表自身顺序;容器内 App 图标排序仍沿用免费预览、Pro 保存的规则。 |
## 4. 术语定义
@@ -43,8 +43,8 @@
| 自定义维护容器 | 用户对默认智能分类进行个性化维护后形成的容器,或用户自建容器。 | 计入 |
| 个性化维护 | 会让默认分类变成用户长期自定义容器的操作,例如改名、改基础颜色、手动调整应用归属。 | 会触发计入 |
| 减法操作 | 删除、隐藏、清理、合并,目的是减少或恢复到免费额度内。 | 不新增额度 |
| 标签拖动排序预览 | 用户拖动标签并看到临时顺序变化,但该顺序不作为长期结果保存。 | 不计入 |
| 标签排序保存 | 用户拖动标签后的顺序被保留,并在后续打开、刷新或重启后继续生效。 | Pro 权益 |
| 标签列表排序保存 | 用户拖动标签后的容器显示顺序被保留,并在后续打开、刷新或重启后继续生效。 | 免费可用 |
| 容器内 App 图标排序保存 | 用户拖动容器内 App 图标后的顺序被长期保留。 | Pro 权益 |
## 5. 产品结论
@@ -60,7 +60,21 @@
Pro 用户可维护不限数量的自定义标签 / 容器。
标签拖动排序的保存能力属于 Pro 权益。免费用户可以拖动标签预览排序效果,但不能保存新的标签顺序。
标签列表自身排序保存能力对免费用户开放。免费用户拖动标签后也会保存新的容器显示顺序。
容器内 App 图标排序保存仍属于 Pro 权益。免费用户可以预览容器内 App 图标排序效果,但不能保存新的容器内图标顺序。
### 5.1.1 2026-07-02 / 8.3.5 决策更新
用户反馈后确认:`自定义排序:标签在列表中的位置(=容器显示位置)` 不再作为 Pro 锁定项。
研发与 QA 口径:
- Pro 对比页删除这一行。
- `TagEditor.reorderTags` 不再检查 Pro 权益,免费用户也可保存标签列表顺序。
- `ContentView` 不再在标签拖动结束时展示“仅预览 / 升级 Pro”提示。
- `自定义排序:容器内 App 图标位置` 仍保留 Pro 权益,不在本次调整中放开。
- `自定义快捷键` 仍保留 Pro 权益,不在本次调整中放开。
### 5.2 不采用方案
@@ -251,33 +265,27 @@
| 功能 | 免费版 | Pro 版 |
|:---|:---|:---|
| 自定义标签 / 容器维护 | 6 个 | 不限数量 |
| 标签排序保存 | 仅预览 | 可保存 |
| 标签列表排序保存 | 可保存 | 可保存 |
| 容器内 App 图标排序保存 | 仅预览 | 可保存 |
### 10.7 标签拖动排序保存
### 10.7 标签列表拖动排序保存
标签拖动排序应作为独立的 Pro 权益处理,交互参考已实现的“在容器内部拖动改变 App 图标顺序,免费版用户只能预览”。
标签列表自身拖动排序免费可用。用户拖动标签后,新的容器显示顺序应持久保存。
| 用户状态 | 行为 |
|:---|:---|
| 免费用户 | 可以拖动标签,并看到临时排序效果。 |
| 免费用户拖动结束 | 不保存新的标签顺序,展示 Pro 提示。 |
| 免费用户重新打开 / 刷新 / 触发标签列表重建 | 标签顺序恢复为当前已保存顺序。 |
| 免费用户 | 可以拖动标签,并保存新的标签顺序。 |
| 免费用户拖动结束 | 保存新的标签顺序,不展示 Pro 提示。 |
| 免费用户重新打开 / 刷新 / 触发标签列表重建 | 标签顺序保持为上次保存结果。 |
| Pro 用户 | 可以拖动标签,拖动结束后保存新的标签顺序。 |
| Pro 用户重新打开 / 刷新 / 触发标签列表重建 | 标签顺序保持为上次保存结果。 |
免费用户的提示方式应与现有 App 图标排序预览提示保持一致,不新增独立视觉样式。
建议文案:
> 当前仅为预览。升级 Pro 后可保存标签排序。
交互边界:
- 不在拖动开始前阻断用户。
- 免费用户拖动过程应保持顺畅,可以看到排序预览。
- 拖动结束后不保存新的顺序。
- 免费用户拖动过程应保持顺畅,拖动结束后保存新的顺序。
- 历史已保存的标签顺序不被清空。
- 本需求不改变容器内部 App 图标排序的现有规则。
- 本需求不改变容器内部 App 图标排序的现有 Pro 规则。
## 11. 用户旅程
@@ -312,13 +320,13 @@
4. 用户尝试新增第 11 个标签时,提示升级 Pro。
5. 用户可删除或合并标签,回到免费额度内。
### 11.5 免费用户预览标签排序
### 11.5 免费用户保存标签列表排序
1. 免费用户希望调整标签展示顺序。
2. 用户拖动标签,界面展示拖动后的临时排序效果。
2. 用户拖动标签,界面展示拖动后的排序效果。
3. 用户松手完成拖动。
4. 系统提示当前仅为预览,保存标签排序需要 Pro。
5. 用户重新打开、刷新或触发标签列表重建后,标签顺序恢复为当前已保存顺序。
4. 系统保存新的标签顺序。
5. 用户重新打开、刷新或触发标签列表重建后,标签顺序保持为上次保存结果。
### 11.6 Pro 用户保存标签排序
@@ -337,8 +345,8 @@
| TAG-QUOTA-04 | 作为免费用户,我希望删除或合并标签后恢复额度,以便继续整理。 | 删除自定义维护容器后释放额度。 |
| TAG-QUOTA-05 | 作为 Pro 用户,我希望不受标签数量限制,以便建立完整应用容器体系。 | Pro 用户可创建和维护不限数量的自定义标签。 |
| TAG-QUOTA-06 | 作为老用户,我希望已有标签不被删除,以便继续访问我的整理结果。 | 超额历史标签仍可查看和启动。 |
| TAG-SORT-01 | 作为免费用户,我希望能拖动标签看看排序效果,以便判断升级 Pro 后是否有价值。 | 免费用户可拖动标签并看到临时排序效果。 |
| TAG-SORT-02 | 作为免费用户,我希望知道为什么排序没有保存,以便理解 Pro 权益边界。 | 免费用户拖动结束后看到“仅预览,保存需 Pro”的提示。 |
| TAG-SORT-01 | 作为免费用户,我希望能保存标签列表顺序,以便按自己的浏览习惯组织容器。 | 免费用户拖动标签后,新的标签顺序被保存并持续生效。 |
| TAG-SORT-02 | 作为免费用户,我希望标签列表排序不会突然弹出 Pro 提示,以便保持整理流程顺畅。 | 免费用户拖动标签结束后不出现标签排序 Pro 提示。 |
| TAG-SORT-03 | 作为 Pro 用户,我希望保存自定义标签顺序,以便长期保持自己的容器浏览习惯。 | Pro 用户拖动标签后,新的标签顺序被保存并持续生效。 |
## 13. 验收标准
@@ -357,10 +365,10 @@
| QA-TQ-10 | Pro 页功能对比展示“自定义标签 / 容器维护:免费 6 个,Pro 不限”。 |
| QA-TQ-11 | 免费用户可看到自定义容器额度计数,例如“已使用 X / 6”或“当前还可维护 X 个”。 |
| QA-TQ-12 | 触发第 7 个自定义维护动作时不创建临时标签、不保存临时改动、不进入预览,只展示“升级 Pro”和“管理已有标签”。 |
| QA-TS-01 | 免费用户可以拖动标签,并看到排序预览效果。 |
| QA-TS-02 | 免费用户拖动标签结束后,不保存新的标签顺序。 |
| QA-TS-03 | 免费用户重新打开、刷新或触发标签列表重建后,标签顺序恢复为当前已保存顺序。 |
| QA-TS-04 | 免费用户拖动标签结束后,看到与 App 图标排序预览一致风格的 Pro 提示。 |
| QA-TS-01 | 免费用户可以拖动标签,并保存新的标签顺序。 |
| QA-TS-02 | 免费用户拖动标签结束后,不出现标签排序 Pro 提示。 |
| QA-TS-03 | 免费用户重新打开、刷新或触发标签列表重建后,标签顺序保持为上次保存结果。 |
| QA-TS-04 | Pro 页免费 / Pro 对比表不再展示“自定义排序:标签在列表中的位置”。 |
| QA-TS-05 | Pro 用户拖动标签结束后,新的标签顺序被保存。 |
| QA-TS-06 | Pro 用户重新打开、刷新或触发标签列表重建后,标签顺序保持为上次保存结果。 |
| QA-TS-07 | 历史已保存的标签顺序不因本需求上线被清空。 |
@@ -375,7 +383,7 @@
| 自动删除超额标签 | 不删除、不隐藏老用户或导入数据中的超额标签。 |
| 标签图标能力 | 本需求不新增标签图标或上传图标。 |
| App 图标排序规则 | 不调整容器内部 App 图标排序的现有 Pro 预览 / 保存规则。 |
| 新增排序视觉样式 | 不为标签排序新增一套独立提示视觉,沿用现有 Pro 提示体系。 |
| 新增排序视觉样式 | 不为容器内 App 图标排序新增一套独立提示视觉,沿用现有 Pro 提示体系。 |
| 订阅模式 | 本需求不改变 Pro 一次性买断策略。 |
## 15. 指标建议
@@ -388,8 +396,8 @@
| 触达后 7 天购买率 | 衡量该权益对转化的真实贡献。 |
| 触达后流失率 / 卸载率 | 监控限制是否过重。 |
| 删除 / 合并标签行为 | 判断用户是否能理解并管理额度。 |
| 标签排序预览触达率 | 衡量免费用户是否会尝试调整标签顺序。 |
| 标签排序预览后的 Pro 点击率 | 衡量“保存标签排序”权益是否具备转化价值。 |
| 容器内 App 图标排序预览触达率 | 衡量免费用户是否会尝试调整容器内图标顺序。 |
| 容器内 App 图标排序预览后的 Pro 点击率 | 衡量“保存容器内 App 图标排序”权益是否具备转化价值。 |
## 16. 决策记录
src/Scripts/pro_custom_container_quota_qa.sh
@@ -26,6 +26,25 @@
        fail(message)
def function_body(source: str, signature: str) -> str:
    start = source.find(signature)
    if start == -1:
        fail(f"missing function signature: {signature}")
    brace = source.find("{", start)
    if brace == -1:
        fail(f"missing function body for: {signature}")
    depth = 0
    for index in range(brace, len(source)):
        char = source[index]
        if char == "{":
            depth += 1
        elif char == "}":
            depth -= 1
            if depth == 0:
                return source[brace + 1:index]
    fail(f"unterminated function body for: {signature}")
pro = read("ProEntitlement.swift")
data_layer = read("DataLayer.swift")
tag_editor = read("TagEditorView.swift")
@@ -57,7 +76,9 @@
require(data_layer, "static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) -> TagEditorMutationResult", "moveApp must return a mutation result")
require(data_layer, "TagDatabase.markCustomMaintainedIfNeeded", "membership changes must mark maintained system tags")
require(data_layer, "static func reorderTags(_ names: [String]) -> Bool", "tag order persistence must return whether it saved")
require(data_layer, "guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return false }", "tag order persistence must be Pro-gated")
reorder_tags_body = function_body(data_layer, "static func reorderTags(_ names: [String]) -> Bool")
if "ProEntitlementPolicy.isUnlocked(.persistentAppSorting)" in reorder_tags_body:
    fail("tag list order persistence must be available to free users")
require(tag_editor, "onCustomContainerQuotaExceeded", "TagEditorView must expose quota-blocked callback")
require(tag_editor, "onCustomContainerQuotaStatusChanged", "TagEditorView must expose quota status refresh callback")
@@ -66,8 +87,10 @@
require(tag_editor, "guard handleMutationResult(result) else { return }", "TagEditorView must not update local state before a save succeeds")
require(tag_editor, "blockedCustomContainerQuota", "TagEditorView must surface quota blocks")
require(content, "persistTagOrderIfAllowed()", "ContentView must route tag order save through a Pro gate")
require(content, "pro.tagSorting.previewToast", "free tag sorting preview toast is missing")
require(content, "persistTagOrder()", "ContentView must persist tag list order")
persist_tag_order_body = function_body(content, "private func persistTagOrder()")
if "proEntitlement.isUnlocked" in persist_tag_order_body or "pro.tagSorting.previewToast" in persist_tag_order_body:
    fail("free users must be able to persist tag list order without a Pro preview toast")
require(content, "presentCustomContainerQuotaPrompt", "ContentView must show custom container quota prompt")
require(content, "customContainerQuotaAccessory", "ContentView tag editor must show quota count")
require(content, "@State private var customContainerQuotaStatus = ProEntitlementPolicy.customContainerQuotaStatus()", "ContentView quota count must be backed by SwiftUI state")
@@ -84,19 +107,19 @@
require(preferences, "onCustomContainerQuotaStatusChanged: refreshSettingsCustomContainerQuotaStatus", "Settings must refresh quota count after TagEditorView saves")
require(preferences, "settingsCustomContainerQuotaOverviewStatus = quotaStatus", "Settings scanApps must initialize quota count from the same store")
require(preferences, 'title: tr("pro.feature.maintainableTagCount")', "Pro comparison must include maintainable tag count row")
require(preferences, 'title: tr("pro.feature.tagOrderPersistence")', "Pro comparison must include tag order row")
if 'title: tr("pro.feature.tagOrderPersistence")' in preferences:
    fail("Pro comparison must not list tag list order; it is available to free users")
require(preferences, 'title: tr("pro.feature.containerAppOrderPersistence")', "Pro comparison must include container app order row")
first_three = [
first_two = [
    preferences.find('title: tr("pro.feature.maintainableTagCount")'),
    preferences.find('title: tr("pro.feature.tagOrderPersistence")'),
    preferences.find('title: tr("pro.feature.containerAppOrderPersistence")'),
]
custom_color_index = preferences.find('title: tr("pro.feature.customTagColors")')
if any(index == -1 for index in first_three) or custom_color_index == -1:
if any(index == -1 for index in first_two) or custom_color_index == -1:
    fail("could not locate Pro comparison feature rows")
if not (first_three[0] < first_three[1] < first_three[2] < custom_color_index):
    fail("new Pro comparison rows must be the first three rows")
if not (first_two[0] < first_two[1] < custom_color_index):
    fail("Pro comparison rows must begin with tag quota and container app order")
required_l10n = [
    "pro.feature.customContainerQuota",
@@ -106,9 +129,7 @@
    "pro.customContainers.quotaUnlimitedShort",
    "pro.customContainers.quotaPrompt",
    "pro.customContainers.manageExisting",
    "pro.tagSorting.previewToast",
    "pro.feature.maintainableTagCount",
    "pro.feature.tagOrderPersistence",
    "pro.feature.containerAppOrderPersistence",
    "settings.proStatus.maxSixTags",
    "settings.proStatus.unlimitedTags",
@@ -139,5 +160,5 @@
if zh_hans.get("settings.proStatus.unlimitedTags") != "✅没有限制":
    fail("zh-Hans unlimited tag status must include the approved leading checkmark")
print("PASS Pro custom container quota QA: quota gate, tag-order preview, Pro table rows, and 29-language keys are wired")
print("PASS Pro custom container quota QA: quota gate, free tag-order persistence, Pro table rows, and 29-language keys are wired")
PY