Ariver
2026-06-28 cfb93f5127af692ff74178329a8b635efc894f8b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/usr/bin/env bash
set -euo pipefail
 
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
 
python3 - "$ROOT_DIR" <<'PY'
import re
import sys
from pathlib import Path
 
root = Path(sys.argv[1])
app_dir = root / "Apptag"
 
 
def fail(message: str) -> None:
    raise SystemExit(f"FAIL: {message}")
 
 
def read(name: str) -> str:
    path = app_dir / name
    try:
        return path.read_text(encoding="utf-8")
    except Exception as error:
        fail(f"could not read {path}: {error}")
 
 
def require(source: str, needle: str, message: str) -> None:
    if needle not in source:
        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")
app = read("ApptagApp.swift")
theme = read("AppGridTheme.swift")
preferences = read("PreferencesView.swift")
content = read("ContentView.swift")
access_views = read("ProAccessViews.swift")
 
for case in [
    "premiumThemes",
    "layoutImport",
    "layoutExport",
    "unlimitedNotes",
    "persistentAppSorting",
]:
    require(pro, f"case {case}", f"missing ProFeature.{case}")
 
require(pro, 'static let lifetimeProductID = "com.taglauncher.pro.lifetime"', "StoreKit lifetime product id changed or missing")
require(pro, 'static let firstFreeProVersion = "8.1.0"', "legacy unlock cutoff must be 8.1.0")
require(pro, 'static let qaStateEnvKey = "TAGLAUNCHER_QA_PRO_STATE"', "QA Pro-state override is missing")
require(pro, "static let freeNoteLimit = 5", "free note limit must remain explicit")
require(pro, "static let freeThemes: Set<AppGridTheme> = [.defaultLight, .deepBlue, .black]", "free theme set must be default/deepBlue/black")
require(pro, "Product.products(for: [ProEntitlementConfig.lifetimeProductID])", "StoreKit product lookup is missing")
require(pro, "Transaction.currentEntitlements", "current entitlement restore/read path is missing")
require(pro, "Transaction.updates", "transaction update listener is missing")
require(pro, "AppTransaction.shared", "legacy original app version path is missing")
require(pro, "appTransaction.originalAppVersion", "legacy originalAppVersion check is missing")
require(pro, "appTransaction.originalPurchaseDate", "legacy originalPurchaseDate fallback is missing")
require(pro, "SemanticVersion(ProEntitlementConfig.firstFreeProVersion)", "legacy semantic version cutoff is missing")
require(pro, "ProEntitlementSnapshotStore", "sync entitlement snapshot for data-layer policy is missing")
require(pro, "manualNonEmptyNoteCount", "note quota must count existing manual notes")
require(pro, "manualNonEmptyNotePaths", "note quota must avoid double-counting existing notes")
 
effective_theme = function_body(pro, "static func effectiveTheme(for storedTheme: AppGridTheme) -> AppGridTheme")
require(effective_theme, "return .defaultLight", "locked premium theme must fall back to defaultLight")
require(effective_theme, "canUseTheme(storedTheme)", "effectiveTheme must check Pro theme entitlement")
 
require(app, "ProEntitlementCenter.shared.start()", "App startup does not start Pro entitlement center")
 
require(theme, "var requiresPro: Bool", "AppGridTheme does not expose Pro requirement")
require(theme, "ProEntitlementConfig.freeThemes.contains(self)", "theme Pro requirement must use central free theme set")
require(theme, "static var premiumThemes: [AppGridTheme]", "theme premium list is missing")
 
export_body = function_body(data_layer, "static func exportTo(_ url: URL) throws")
require(export_body, "try ProEntitlementPolicy.requireUnlocked(.layoutExport)", "layout export is not locked at data layer")
if export_body.find("try ProEntitlementPolicy.requireUnlocked(.layoutExport)") > export_body.find("data.write"):
    fail("layout export lock must run before writing data")
 
import_body = function_body(data_layer, "static func importFrom(_ url: URL) throws -> Store")
require(import_body, "try ProEntitlementPolicy.requireUnlocked(.layoutImport)", "layout import is not locked at data layer")
if import_body.find("try ProEntitlementPolicy.requireUnlocked(.layoutImport)") > import_body.find("Data(contentsOf: url)"):
    fail("layout import lock must run before reading user-selected file")
 
reorder_body = function_body(data_layer, "static func reorderApps(inContainer containerID: String, orderedPaths: [String])")
require(reorder_body, "guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return }", "app sorting persistence is not locked")
if reorder_body.find("guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return }") > reorder_body.find("var store = TagDatabase.load()"):
    fail("app sorting lock must run before loading/writing store")
 
note_body = function_body(data_layer, "static func setAppNote(_ note: String, for path: String) -> ProNoteSaveDecision")
require(note_body, "ProEntitlementPolicy.noteSaveDecision", "note save does not check Pro quota")
require(note_body, "guard case .allow = decision else", "blocked note save must return without mutating store")
if note_body.find("guard case .allow = decision else") > note_body.find("let previousStore = store"):
    fail("note quota decision must happen before store mutation snapshot")
 
export_ui = function_body(preferences, "private func exportTags()")
require(export_ui, "guard proEntitlement.isUnlocked else", "export UI must prompt Pro before file panel")
require(export_ui, "presentSettingsProPrompt(for: .layoutExport)", "export UI prompt feature is missing")
if export_ui.find("guard proEntitlement.isUnlocked else") > export_ui.find("prepareDataFilePanelPresentation()"):
    fail("export UI must check Pro before opening file panel")
 
import_ui = function_body(preferences, "private func importTags()")
require(import_ui, "guard proEntitlement.isUnlocked else", "import UI must prompt Pro before file panel")
require(import_ui, "presentSettingsProPrompt(for: .layoutImport)", "import UI prompt feature is missing")
if import_ui.find("guard proEntitlement.isUnlocked else") > import_ui.find("prepareDataFilePanelPresentation()"):
    fail("import UI must check Pro before opening file panel")
 
require(preferences, "ProStatusPill(text: tr(\"pro.card.badge\"), style: .locked, compact: true)", "locked data actions must show compact Pro pill")
require(preferences, "presentSettingsProPrompt(for: .premiumThemes)", "premium theme selection prompt is missing")
require(preferences, "proEntitlement.startThemePreview(for: theme)", "premium theme free preview is missing")
require(preferences, "proEntitlement.stopThemePreview()", "theme preview cleanup is missing")
require(pro, "func remainingSeconds(at date: Date = Date()) -> Int", "theme preview remaining-seconds helper is missing")
require(pro, "ceil(endsAt.timeIntervalSince(date))", "theme preview countdown must derive from endsAt")
require(preferences, "pro.theme.status.previewCountdown", "theme preview status countdown localization is missing")
require(preferences, "formatThemePreviewCountdown(seconds:", "theme preview countdown formatter is missing")
require(preferences, ".countdown(text: themePreviewCountdownText ?? \"00:00\")", "current preview theme card must show countdown")
require(preferences, ".onReceive(themePreviewCountdownTimer)", "theme preview countdown UI timer is missing")
require(preferences, ".font(.system(size: 10, weight: .semibold, design: .monospaced))", "theme preview card countdown must use monospaced digits")
 
require(content, "@ObservedObject private var proEntitlement = ProEntitlementCenter.shared", "ContentView does not observe Pro entitlement")
require(content, "ProEntitlementPolicy.effectiveTheme(for: appGridTheme)", "ContentView does not apply effective Pro theme")
require(content, "presentProPrompt(for: .unlimitedNotes", "note quota prompt is missing from AppGrid")
require(content, "tr(\"pro.sorting.previewToast\")", "free sorting preview toast is missing")
require(content, "if let preview = proEntitlement.themePreviewState", "theme preview acceptance path is missing")
require(content, "ProUpgradePromptView(", "AppGrid Pro upgrade prompt view is missing")
 
require(access_views, "struct ProStatusPill", "ProStatusPill view is missing")
require(access_views, "struct ProUpgradePromptView", "ProUpgradePromptView is missing")
require(access_views, "let onUnlock: () -> Void", "Pro upgrade prompt purchase callback is missing")
require(access_views, "let onRestore: () -> Void", "Pro upgrade prompt restore callback is missing")
require(content, "onUnlock: { proEntitlement.purchasePro() }", "AppGrid Pro prompt is not wired to purchase action")
require(content, "onRestore: { proEntitlement.restorePurchases() }", "AppGrid Pro prompt is not wired to restore action")
 
print("PASS Pro feature gate QA: StoreKit entitlement, legacy unlock, theme gating, import/export locks, note quota, sorting preview, and Pro UI prompts are wired")
PY