appstore-precheck 1.1.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,272 @@
1
+ # Phase 4: Pierre deep review (28 semantic checks)
2
+
3
+ After Phase 3 (explaining every scan FAIL/WARN), Pierre runs a **read-only, project-wide
4
+ semantic review** of 28 guideline areas the static scanner cannot fully judge. Checks **1–22**
5
+ (minus Tier B items below) are high-confidence (Tier A); checks **4, 5, 7, 10, 15, and 28** are
6
+ heuristic advisory (Tier B v1 — higher false-positive risk, still useful pre-submit signals).
7
+
8
+ This is the **Review Simulator** layer: Pierre reads Swift, metadata, entitlements, screenshots,
9
+ xcstrings, paywall views, review notes, and fetches live privacy/support URLs — then cross-checks
10
+ claims against evidence.
11
+
12
+ **This phase does not change the GREEN/YELLOW/RED verdict.** Verdict counts come only from
13
+ Phases 0–2 (`FAIL:` / `WARN:` lines). Phase 4 emits `REVIEW-PASS:` or `REVIEW-FINDING:` lines
14
+ that Pierre explains in Phase 5 presentation.
15
+
16
+ ## Rules
17
+
18
+ - **Read-only:** never modify project files.
19
+ - **Evidence-based:** cite `file:line`, metadata path, screenshot filename, or fetched URL text.
20
+ If you cannot read something (private URL, missing file), say so — do not invent findings.
21
+ - **All 28 checks, every run:** report each item as `REVIEW-PASS:` or `REVIEW-FINDING:` — no skipping.
22
+ - **REVIEW-FINDING severity:** always `WARN` (advisory). Never emit `REVIEW-FINDING: … FAIL`.
23
+ A deep-review issue informs the human; it does not block the token by itself.
24
+ - **Tier B checks (4, 5, 7, 10, 15, 28):** prefer `REVIEW-PASS: … — not applicable` when the signal is absent;
25
+ when flagging, use cautious language ("may trigger review questions") — these are heuristics.
26
+ - **Deepen scan hits:** when Phase 1 already flagged a guideline, Phase 4 still runs the matching
27
+ deep check and adds semantic context (do not repeat the machine line verbatim — add what the
28
+ scanner could not see).
29
+ - **WebFetch:** use `WebFetch` (or equivalent) on `privacy_url.txt` and `support_url.txt` when
30
+ present. If fetch fails, `REVIEW-FINDING: … WARN — could not fetch privacy policy; verify manually`.
31
+ - **Screenshots:** read at least one PNG/JPEG per primary locale; compare visible UI/features to
32
+ metadata claims.
33
+ - **Language:** write Pierre's 2–3 sentence explanations in the user's conversation language.
34
+
35
+ ## Output format
36
+
37
+ For each of the 28 checks (in table order):
38
+
39
+ ```
40
+ REVIEW-PASS: <guideline> — <one-line why it looks OK, with evidence pointer>
41
+ ```
42
+
43
+ or
44
+
45
+ ```
46
+ REVIEW-FINDING: <guideline> WARN — <one-line concrete mismatch or gap, with evidence pointer>
47
+ Pierre: <2–3 sentences: why Apple cares, what you found, what to fix or verify>
48
+ ```
49
+
50
+ If a check is **not applicable** (e.g. no HealthKit, no VPN, no contest copy), still report:
51
+
52
+ ```
53
+ REVIEW-PASS: <guideline> — not applicable (<reason>)
54
+ ```
55
+
56
+ ---
57
+
58
+ ## The 28 checks (guideline order)
59
+
60
+ | # | Guideline | Deep question | Primary sources |
61
+ |---|-----------|---------------|-----------------|
62
+ | 1 | **1.2.1** | UGC present → is there a real report/block/moderation UI flow, not just keywords in copy? | Swift navigation, moderation views, §22 scan context |
63
+ | 2 | **1.4.1** | Health/medical/wellness claims in metadata or UI without appropriate disclaimers or HealthKit compliance? | metadata, Swift HealthKit usage, onboarding copy |
64
+ | 3 | **2.1** | Metadata/marketing claims match implemented features (AI, offline, ad-block, sync, etc.)? | metadata, description, Swift feature grep |
65
+ | 4 | **2.1** | Login-gated app → App Review demo account / notes **actionable** (credentials, steps, not placeholder)? | `review_information/`, `.reviewPrepNotes`, §31 scan context |
66
+ | 5 | **2.2** | Store-facing copy or UI still says beta / test / preview / work-in-progress? | metadata, release_notes, Swift UI strings |
67
+ | 6 | **2.3.2** | Primary category plausible for app type (game vs utility vs health, etc.)? | fastlane `primary_category`, metadata tone, code structure |
68
+ | 7 | **2.3.4** | App preview assets present → features shown match the shipped app and metadata? | preview video paths, metadata, Swift UI |
69
+ | 8 | **2.3.5** | Screenshots show features the app actually ships; no misleading device frames or competitor UI? | screenshot images, metadata, Swift UI |
70
+ | 9 | **2.3.6** | Pricing/subscription language in metadata matches paywall (free vs paid, trial terms)? | metadata, paywall Swift, xcstrings |
71
+ | 10 | **2.3.9** | Incentivized review copy ("rate 5 stars", "review for reward") in metadata or UI? | metadata, onboarding, paywall, §25 scan context |
72
+ | 11 | **2.3.11–2.3.13** | Cross-locale metadata consistent (feature lists, trial terms, support/privacy URLs, pricing claims)? | all `fastlane/metadata/*` locales |
73
+ | 12 | **3.1.1** | Digital goods sold or unlocked via external purchase links (web checkout, Stripe in WebView)? | Swift WebView/paywall, metadata, entitlements |
74
+ | 13 | **3.1.2** | Subscription/trial/auto-renew/cancel disclosures are **legible sentences**, not keyword stubs? | paywall views, xcstrings, String Catalog |
75
+ | 14 | **4.2.1–4.2.2** | App is more than a thin shell: meaningful navigation, native affordances, not a lone WebView brochure? | Swift UI structure, §12/§35 scan context |
76
+ | 15 | **4.5.1–4.5.3** | Push or HomeKit entitlement → used as intended (no spam-push promises; HomeKit without home UI)? | entitlements, Info.plist, metadata, Swift |
77
+ | 16 | **4.8** | Third-party login present → Sign in with Apple offered, or a documented exempt case (enterprise, existing account, etc.)? | login Swift, SDK imports, §14 scan context |
78
+ | 17 | **5.1.1(i)** | Privacy policy text (fetched) matches data collection in code, PrivacyInfo, and App Privacy narrative? | fetch privacy URL, PrivacyInfo, SDK imports |
79
+ | 18 | **5.1.1(ii)** | Purpose strings are specific and tied to a visible feature (not empty, generic, or copy-paste)? | Info.plist, permission usage in Swift |
80
+ | 19 | **5.1.1(iii)** | Data/permission requests proportionate to stated app purpose (no obvious over-collection)? | permissions, SDKs vs metadata promise |
81
+ | 20 | **5.1.1(iv)** | Permission denial handled gracefully — no infinite re-prompt loops or hard blocks without explanation? | location/camera/notification/auth flows in Swift |
82
+ | 21 | **5.1.2** | ATT prompt, `NSUserTrackingUsageDescription`, privacy policy tracking section, and ad SDK usage align? | Info.plist, policy fetch, ad SDK imports |
83
+ | 22 | **5.1.3** | HealthKit data not used for advertising/marketing; sync paths respect health-data rules? | HealthKit + analytics/ad SDK co-use |
84
+ | 23 | **5.1.4** | Kids-audience signals → parental gate before external links/purchases/account areas? | metadata kids wording, parental gate UI |
85
+ | 24 | **5.4** | VPN/NetworkExtension → on-screen disclosure text visible in UI strings (not only Info.plist)? | Swift strings, NetworkExtension usage |
86
+ | 25 | **5.2.1–5.2.3** | Obvious third-party trademark/brand misuse in metadata, assets, or UI copy? | metadata, asset filenames, Swift strings |
87
+ | 26 | **5.3.1–5.3.3** | Contest/sweepstakes/lottery copy → official rules/eligibility/disclosure present in metadata? | description, keywords, in-app contest UI |
88
+ | 27 | **5.6.2–5.6.3** | Developer identity consistent: app name, support URL content, bundle/marketing domain match? | fetch support URL, metadata, legal/footer copy |
89
+ | 28 | **5.6.4–5.6.7** | Rating/review manipulation dark patterns (withhold features until 5 stars, direct write-review links without `requestReview`)? | Swift, metadata, §25 scan context |
90
+
91
+ ---
92
+
93
+ ## Per-check procedure (detail)
94
+
95
+ ### 1 — 1.2.1 UGC moderation UI
96
+
97
+ 1. If no UGC signals (posts, comments, chat, uploads), mark not applicable.
98
+ 2. Search Swift for report/block/flag/moderate flows and screens reachable from content.
99
+ 3. Compare to metadata promises ("community", "share", "chat").
100
+ 4. Flag if UGC exists but moderation is only mentioned in text, not implemented in UI.
101
+
102
+ ### 2 — 1.4.1 Health / medical claims
103
+
104
+ 1. Scan metadata and onboarding for diagnose, treat, cure, clinical, FDA, blood pressure, etc.
105
+ 2. If HealthKit present, check disclaimers ("not a medical device") where claims exist.
106
+ 3. Flag unsubstantiated treatment claims without appropriate health disclaimers.
107
+
108
+ ### 3 — 2.1 Claims ↔ code
109
+
110
+ 1. Extract feature claims from name, subtitle, description, keywords (AI, ML, offline, block ads, VPN, etc.).
111
+ 2. Grep Swift / imports for matching implementation.
112
+ 3. Flag prominent metadata claims with no code evidence.
113
+
114
+ ### 4 — 2.1 Review notes / demo account quality *(Tier B v1)*
115
+
116
+ 1. If the app is login-gated (`SecureField`, Login/SignIn views) or scan §31 flagged missing demo:
117
+ read `fastlane/metadata/*/review_information/` (username, password, notes) and
118
+ `.appstore-precheck.json` → `reviewPrepNotes` if set.
119
+ 2. Flag empty notes, placeholder credentials (`test`, `demo`, `changeme`, `TBD`), or notes that
120
+ do not explain how to reach core features (subscription paywall, Screen Time blocking, etc.).
121
+ 3. If the app is not login-gated and has no account wall, mark not applicable.
122
+
123
+ ### 5 — 2.2 Beta / test language *(Tier B v1)*
124
+
125
+ 1. Grep store metadata, `release_notes.txt`, and user-visible Swift strings (not code comments) for:
126
+ `beta`, `testflight`, `test flight`, `preview`, `pre-release`, `work in progress`, `WIP`,
127
+ `under development`, `not final`, `experimental`.
128
+ 2. Exclude legitimate internal keys and developer log strings not shown to users.
129
+ 3. Flag any store-facing copy implying the App Store build is unfinished or a beta.
130
+
131
+ ### 6 — 2.3.2 Category fit
132
+
133
+ 1. Read primary category if present in fastlane or `.appstore-precheck.json`.
134
+ 2. Infer app type from code (game loop, utility, reader, social).
135
+ 3. Flag obvious mismatch (arcade game filed as Productivity).
136
+
137
+ ### 7 — 2.3.4 App preview consistency *(Tier B v1)*
138
+
139
+ 1. Look for app preview assets under `fastlane/metadata/*/preview*` or `*.mov` / `*.mp4` in metadata trees.
140
+ 2. If no preview assets in-repo, mark not applicable (previews may live only in App Store Connect).
141
+ 3. If previews exist: compare visible features/captions to metadata and Swift UI; flag previews
142
+ showing features absent from the build.
143
+
144
+ ### 8 — 2.3.5 Screenshots vs reality
145
+
146
+ 1. Open ≥1 screenshot per primary locale.
147
+ 2. List visible features (tabs, paywall, login, maps, etc.).
148
+ 3. Flag screenshots showing features absent from the build or metadata.
149
+
150
+ ### 9 — 2.3.6 Pricing language
151
+
152
+ 1. Compare metadata "free", trial, and price claims to paywall/subscription UI strings.
153
+ 2. Flag "completely free" metadata when IAP/paywall exists without clear disclosure.
154
+
155
+ ### 10 — 2.3.9 Incentivized review *(Tier B v1)*
156
+
157
+ 1. Grep metadata, onboarding, and paywall strings for: `rate us`, `leave a review`, `5 star`,
158
+ `five star`, `review and get`, `gift card`, `reward for review`, `write a review to unlock`.
159
+ 2. Cross-check scan §25 (custom review prompt) — Phase 4 adds semantic context if §25 passed.
160
+ 3. Flag quid-pro-quo review incentives or star-rating manipulation copy.
161
+
162
+ ### 11 — 2.3.11–2.3.13 Locale parity (semantic)
163
+
164
+ 1. Beyond scan's file-presence check: compare trial terms, feature bullets, and pricing claims across locales.
165
+ 2. Flag material omissions (trial mentioned in en-US only, different feature lists).
166
+
167
+ ### 12 — 3.1.1 External digital purchase
168
+
169
+ 1. Search for external checkout URLs, Stripe/PayPal in WebView, "subscribe on our website".
170
+ 2. Flag digital unlocks that bypass StoreKit without 3.1.1(a) entitlement context.
171
+
172
+ ### 13 — 3.1.2 Disclosure quality
173
+
174
+ 1. Read paywall disclosure strings (Swift + xcstrings).
175
+ 2. Flag if trial/auto-renew/cancel info is a single keyword, lorem, or unreadably dense.
176
+ 3. Require human-readable sentences covering trial length, renewal, and cancellation path.
177
+
178
+ ### 14 — 4.2.1–4.2.2 Minimum functionality (semantic)
179
+
180
+ 1. Map primary user journeys (launch → core action).
181
+ 2. Flag single-screen WebView brochure, template placeholder flows, or no native navigation beyond §12 minimum.
182
+
183
+ ### 15 — 4.5.1–4.5.3 Push / HomeKit abuse *(Tier B v1)*
184
+
185
+ 1. Read entitlements and Info.plist for push notifications and HomeKit.
186
+ 2. **Push:** if push entitlement present, scan metadata/UI for spam patterns ("notify every hour",
187
+ "unlimited reminders") unrelated to user-initiated alerts.
188
+ 3. **HomeKit:** if HomeKit framework imported, confirm home-automation UI exists; flag HomeKit
189
+ import with no home-related features (possible misuse).
190
+ 4. If neither push nor HomeKit signals, mark not applicable.
191
+
192
+ ### 16 — 4.8 Sign in with Apple (context)
193
+
194
+ 1. If Google/Facebook/etc. login exists, confirm `ASAuthorizationAppleID` / Sign in with Apple button.
195
+ 2. If absent, assess exempt patterns (enterprise-only, password-only existing users) — flag uncertain cases for human review.
196
+
197
+ ### 17 — 5.1.1(i) Privacy policy accuracy
198
+
199
+ 1. Fetch privacy URL from primary locale metadata.
200
+ 2. Compare policy statements to: PrivacyInfo collected types, tracking domains, location/camera/health SDK usage.
201
+ 3. Flag direct contradictions ("we do not collect location" + CoreLocation).
202
+
203
+ ### 18 — 5.1.1(ii) Purpose string quality
204
+
205
+ 1. List every `NS*UsageDescription` in Info.plist.
206
+ 2. Flag empty, placeholder, or generic strings; flag mismatch with the feature that triggers the prompt.
207
+
208
+ ### 19 — 5.1.1(iii) Data minimization
209
+
210
+ 1. List sensitive permissions and SDKs.
211
+ 2. Compare to app category and metadata promise.
212
+ 3. Flag obvious overreach (contacts + photos + location for a calculator).
213
+
214
+ ### 20 — 5.1.1(iv) Permission denial UX
215
+
216
+ 1. Trace location/camera/photo/notification permission flows.
217
+ 2. Flag forced loops, dead-ends, or dark patterns after denial.
218
+
219
+ ### 21 — 5.1.2 ATT consistency
220
+
221
+ 1. If ad/attribution SDK or IDFA access: confirm ATT API usage and description text.
222
+ 2. Cross-check privacy policy tracking section vs implementation.
223
+
224
+ ### 22 — 5.1.3 HealthKit + ads
225
+
226
+ 1. If HealthKit imported: search for analytics/ad SDK sending health-derived signals.
227
+ 2. Flag health data paths combined with advertising identifiers.
228
+
229
+ ### 23 — 5.1.4 Parental gate
230
+
231
+ 1. If kids metadata or child-audience copy: find parental gate before web/links/IAP/account.
232
+ 2. Flag child positioning without age gate UI.
233
+
234
+ ### 24 — 5.4 VPN disclosure UI
235
+
236
+ 1. If NetworkExtension/NEVPNManager: search UI strings for data-collection disclosure required at launch/settings.
237
+ 2. Flag VPN capability with no user-visible disclosure copy.
238
+
239
+ ### 25 — 5.2.1–5.2.3 IP / trademarks
240
+
241
+ 1. Scan metadata and visible strings for other companies' brands used as if owned.
242
+ 2. Flag likely trademark misuse (not generic descriptive use).
243
+
244
+ ### 26 — 5.3.1–5.3.3 Contests
245
+
246
+ 1. If sweepstakes/contest/giveaway language: check for rules, eligibility, sponsor, no-purchase-necessary.
247
+ 2. Flag contest marketing without rules in metadata or in-app.
248
+
249
+ ### 27 — 5.6.2–5.6.3 Developer identity
250
+
251
+ 1. Fetch support URL; confirm it resolves and shows developer contact or support path.
252
+ 2. Compare app name, support domain, and privacy policy domain for consistency.
253
+ 3. Flag placeholder support pages or identity mismatch.
254
+
255
+ ### 28 — 5.6.4–5.6.7 Rating manipulation *(Tier B v1)*
256
+
257
+ 1. Grep Swift and metadata for: `itms-apps://` write-review URLs, `apps.apple.com/.../write-review`,
258
+ "rate 5 stars", "only enable after review", custom star-rating UI tied to App Store review.
259
+ 2. Compare to system `requestReview` / `SKStoreReviewController` usage (scan §25).
260
+ 3. Flag dark patterns that manipulate ratings or bypass Apple's review prompt API.
261
+
262
+ ---
263
+
264
+ ## Phase 5 presentation
265
+
266
+ After Phase 4, include in the final report:
267
+
268
+ 1. Trilingual verdict block (from scan counts only).
269
+ 2. Phase 3 commentary (every scan FAIL/WARN).
270
+ 3. Phase 4 summary table: 28 checks → count of `REVIEW-FINDING` vs `REVIEW-PASS` (note Tier B items 4, 5, 7, 10, 15, 28 if any fired).
271
+ 4. Phase 4 detail: every `REVIEW-FINDING` with Pierre explanation; optionally list `REVIEW-PASS` lines compactly.
272
+ 5. Verbatim Phase 1 scan output + verdict/token action.
@@ -153,24 +153,28 @@ CHECK_FAMILY="$(cfg_bool '.optionalChecks.familyControls')"
153
153
  echo "PASS: layout — ios='${IOS_DIR:-?}' metadata='${META_DIR:-?}' xcstrings='${XCSTRINGS:-?}' locales=${#LOCALES[@]}"
154
154
 
155
155
  # ===================================================================
156
- # §1 — 5.1.1(v) Privacy Manifest Required Reason API parity
156
+ # §1 — 5.1.1 Privacy Manifest / Required Reason API parity
157
+ # Apple documents the Required Reason API rules under 5.1.1 (Data Collection and
158
+ # Storage) + the privacy-manifest developer docs; it is NOT roman sub-item (v).
159
+ # (v) "Account Sign-In" is a different rule — its account-deletion requirement is
160
+ # checked separately in §38.
157
161
  # ===================================================================
158
162
  check_required_reason_api() {
159
163
  local cat="$1" pattern="$2" hits declared
160
164
  hits=$(grep -rEl "$pattern" "$IOS_DIR" --include="*.swift" 2>/dev/null | head -3)
161
165
  declared=$(grep -c "NSPrivacyAccessedAPICategory${cat}" "$PRIVACY_FILE" 2>/dev/null)
162
166
  if [[ -n "$hits" && "${declared:-0}" -eq 0 ]]; then
163
- fail "5.1.1(v) Required Reason API — '$cat' used in code (e.g. $(echo "$hits" | head -1)) but not declared in PrivacyInfo.xcprivacy"
167
+ fail "5.1.1 Required Reason API — '$cat' used in code (e.g. $(echo "$hits" | head -1)) but not declared in PrivacyInfo.xcprivacy"
164
168
  elif [[ -z "$hits" && "${declared:-0}" -gt 0 ]]; then
165
- warn "5.1.1(v) PrivacyInfo — '$cat' declared but no code usage grepped (may be a false positive, verify manually)"
169
+ warn "5.1.1 PrivacyInfo — '$cat' declared but no code usage grepped (may be a false positive, verify manually)"
166
170
  elif [[ -n "$hits" && "${declared:-0}" -gt 0 ]]; then
167
- pass "5.1.1(v) Required Reason API — '$cat' parity OK"
171
+ pass "5.1.1 Required Reason API — '$cat' parity OK"
168
172
  fi
169
173
  }
170
174
  if [[ -z "$IOS_DIR" ]]; then
171
175
  warn "layout — could not auto-detect iOS source dir; set .iosSourceDir in $CONFIG"
172
176
  elif [[ -z "$PRIVACY_FILE" ]]; then
173
- fail "5.1.1(v) PrivacyInfo.xcprivacy not found (required since May 2024 for apps using Required Reason APIs)"
177
+ fail "5.1.1 Required Reason API — PrivacyInfo.xcprivacy not found (required since May 2024 for apps using Required Reason APIs)"
174
178
  else
175
179
  check_required_reason_api "UserDefaults" 'UserDefaults|@AppStorage'
176
180
  check_required_reason_api "FileTimestamp" 'attributesOfItem|creationDate|modificationDate|\.fileCreationDate|\.fileModificationDate'
@@ -383,13 +387,13 @@ else
383
387
  fi
384
388
 
385
389
  # ===================================================================
386
- # §12 — 4.0 Minimum functionality — navigation hubs
390
+ # §12 — 4.2 Minimum functionality — navigation hubs
387
391
  # ===================================================================
388
392
  tab_count=$(grep -rcE 'TabView|NavigationStack|NavigationSplitView' "$IOS_DIR" --include="*.swift" 2>/dev/null | awk -F: '{sum+=$2} END {print sum+0}')
389
393
  if (( tab_count < 1 )); then
390
- warn "4.0 Minimum functionality — no TabView/NavigationStack found (heuristic, may be a false positive)"
394
+ warn "4.2 Minimum functionality — no TabView/NavigationStack found (heuristic, may be a false positive)"
391
395
  else
392
- pass "4.0 Minimum functionality — $tab_count navigation hub(s) found"
396
+ pass "4.2 Minimum functionality — $tab_count navigation hub(s) found"
393
397
  fi
394
398
 
395
399
  # ===================================================================
@@ -473,8 +477,8 @@ if [[ -d "$META_DIR" ]]; then
473
477
  [[ -s "$META_DIR/$loc/privacy_url.txt" ]] && privacy_found=1
474
478
  done
475
479
  if (( ${#LOCALES[@]} > 0 )); then
476
- [[ -z "$support_found" ]] && warn "2.3 Support URL — no non-empty support_url.txt in any locale under fastlane metadata; Apple requires a working support URL"
477
- [[ -z "$privacy_found" ]] && warn "2.3 Privacy URL — no non-empty privacy_url.txt in any locale under fastlane metadata; required for apps with accounts or in-app purchases"
480
+ [[ -z "$support_found" ]] && warn "2.3 Support URL — no non-empty support_url.txt in any locale under fastlane metadata; Apple requires a working support URL with developer contact info (1.5 / 2.3)"
481
+ [[ -z "$privacy_found" ]] && warn "2.3 Privacy URL — no non-empty privacy_url.txt in any locale under fastlane metadata; a privacy policy link is required for every app, and especially apps with accounts or in-app purchases (5.1.1(i))"
478
482
  fi
479
483
  url_ph=$(grep -rEnI 'example\.com|localhost|\bTODO\b|\bchangeme\b' "$META_DIR" --include='*_url.txt' 2>/dev/null | grep -v "^Binary file" | head -10)
480
484
  if [[ -n "$url_ph" ]]; then
@@ -523,4 +527,309 @@ if [[ -d "$META_DIR" ]]; then
523
527
  fi
524
528
  fi
525
529
 
530
+ # ===================================================================
531
+ # §21 — 3.1.1 Third-party payment SDK for digital goods
532
+ # ===================================================================
533
+ # Apple requires in-app purchase to unlock digital content/functionality. A
534
+ # third-party payment SDK (Stripe, Braintree, PayPal, …) is legitimate for
535
+ # PHYSICAL goods/services but a frequent rejection when used to sell digital
536
+ # content. We can't tell digital from physical statically, so this is advisory.
537
+ if [[ -n "$IOS_DIR" ]]; then
538
+ payment_sdk=$(grep -rlE 'import Stripe|StripePaymentSheet|StripeApplePay|import Braintree|BTPaymentFlow|import PayPal|PayPalCheckout|PayPalNativeCheckout|import Square|SquareInAppPayments|import Adyen|AdyenComponents|RazorpaySDK|import Paddle' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
539
+ if [[ -n "$payment_sdk" ]]; then
540
+ warn "3.1.1 Third-party payment SDK — '$(basename "$payment_sdk")' detected; selling digital content/functionality must use in-app purchase, not an external processor (3.1.1). Allowed only for physical goods/services — verify your offering."
541
+ fi
542
+ fi
543
+
544
+ # ===================================================================
545
+ # §22 — 1.2 User-generated content without moderation affordances
546
+ # ===================================================================
547
+ # Apps with UGC must provide: a content filter, a report mechanism, the ability
548
+ # to block abusive users, and published contact info. We detect a UGC signal and
549
+ # warn when no report/block/moderation affordance is found anywhere in the source.
550
+ if [[ -n "$IOS_DIR" ]]; then
551
+ ugc_signal=$(grep -rlE 'userGeneratedContent|\bUGC\b|StreamChat|MessageKit|SendbirdSDK|PubNub|postComment|submitComment|createPost|publishPost|uploadUserPhoto|uploadVideo' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
552
+ if [[ -n "$ugc_signal" ]]; then
553
+ if grep -rqiE 'report(Content|User|Abuse|Post|Comment|Message|Reason|ed)|block(ed)?User|unblockUser|moderat(e|ion|or)|flag(Content|Post|User|Comment|Message)|content.?filter' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
554
+ pass "1.2 UGC — user-generated content with report/block/moderation affordances present"
555
+ else
556
+ warn "1.2 UGC — user-generated content detected (e.g. $(basename "$ugc_signal")) but no report/block/moderation mechanism found; UGC apps must offer content filtering, a report mechanism, user blocking, and published contact info (1.2)"
557
+ fi
558
+ fi
559
+ fi
560
+
561
+ # ===================================================================
562
+ # §23 — 1.6 App Transport Security disabled globally
563
+ # ===================================================================
564
+ # NSAllowsArbitraryLoads=true turns off ATS for the whole app, weakening
565
+ # data-in-transit security and inviting a 1.6 / data-security question at review.
566
+ if [[ -f "$INFO_PLIST" ]]; then
567
+ if awk '/NSAllowsArbitraryLoads/{getline; if ($0 ~ /<true/) print "ON"}' "$INFO_PLIST" 2>/dev/null | grep -q ON; then
568
+ warn "1.6 App Transport Security — NSAllowsArbitraryLoads=true in Info.plist disables ATS app-wide; prefer per-domain exceptions, and expect a data-security justification request at review (1.6)"
569
+ fi
570
+ fi
571
+
572
+ # ===================================================================
573
+ # §24 — 4.9 Apple Pay recurring-payment disclosure
574
+ # ===================================================================
575
+ # Apps using Apple Pay for recurring payments must disclose the renewal term,
576
+ # what's provided, the charges, and how to cancel. Gated strictly on the Apple
577
+ # Pay recurring API (PKRecurringPaymentRequest) so it does NOT conflate StoreKit
578
+ # auto-renew copy or a one-time PassKit payment with recurring Apple Pay. We don't
579
+ # try to detect the disclosure text (too noisy) — we flag it for manual verify.
580
+ if [[ -n "$IOS_DIR" ]] && grep -rqE 'PKRecurringPaymentRequest' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
581
+ warn "4.9 Apple Pay — recurring Apple Pay (PKRecurringPaymentRequest) detected; verify you disclose the renewal term, what's provided, the charges, and how to cancel before purchase (4.9)"
582
+ fi
583
+
584
+ # ===================================================================
585
+ # §25 — 5.6.1 Custom App Store review prompt
586
+ # ===================================================================
587
+ # Apple disallows custom review prompts and direct write-review links — apps must
588
+ # use the system SKStoreReviewController / requestReview API.
589
+ if [[ -n "$IOS_DIR" ]]; then
590
+ review_link=$(grep -rlE 'write-review|action=write-review|itms-apps[^"]*review' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
591
+ if [[ -n "$review_link" ]]; then
592
+ if grep -rqE 'requestReview|SKStoreReviewController|\.requestReview' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
593
+ pass "5.6.1 App reviews — uses the system requestReview API alongside the review link"
594
+ else
595
+ warn "5.6.1 App reviews — a direct App Store review link/prompt was found (e.g. $(basename "$review_link")) but no system requestReview (SKStoreReviewController) call; Apple disallows custom review prompts (5.6.1)"
596
+ fi
597
+ fi
598
+ fi
599
+
600
+ # ===================================================================
601
+ # §26 — 2.3.1 Misleading marketing claims in metadata
602
+ # ===================================================================
603
+ # Marketing the app for things iOS apps can't actually do (virus/malware
604
+ # scanners, fake speed boosters) is a 2.3.1 removal vector.
605
+ if [[ -d "$META_DIR" ]]; then
606
+ mislead_re='virus scan|virus scanner|antivirus|anti-virus|malware (scan|remov|clean)|spyware remov|clean your (iphone|device)|speed booster|boost.*(speed|ram)|free money|guaranteed.*(win|prize)'
607
+ mislead_hits=$(grep -rEniI "$mislead_re" "$META_DIR" 2>/dev/null | grep -v "^Binary file" | head -10)
608
+ if [[ -n "$mislead_hits" ]]; then
609
+ warn "2.3.1 Misleading marketing — claims that often violate 2.3.1 (e.g. iOS virus/malware scanners, fake speed boosters) in metadata; verify the app truly delivers them or remove the claim:"
610
+ echo "$mislead_hits" | sed 's/^/ /'
611
+ fi
612
+ fi
613
+
614
+ # ===================================================================
615
+ # §27 — 2.3.8 "For Kids/Children" wording outside the Kids Category
616
+ # ===================================================================
617
+ # Terms implying a child audience in name/subtitle/keywords/description are
618
+ # reserved for the Kids Category (2.3.8 / 5.1.4).
619
+ if [[ -d "$META_DIR" ]]; then
620
+ kids_re='for kids|for children|for your (kid|child)|kids[[:space:]]?app|für kinder|para niños|pour enfants'
621
+ kids_hits=$(grep -rEniI "$kids_re" "$META_DIR" 2>/dev/null | grep -v "^Binary file" | head -10)
622
+ if [[ -n "$kids_hits" ]]; then
623
+ warn "2.3.8 'For Kids/Children' wording — terms implying a child audience are reserved for the Kids Category (2.3.8); if not enrolled, remove them from name/subtitle/keywords/description:"
624
+ echo "$kids_hits" | sed 's/^/ /'
625
+ fi
626
+ fi
627
+
628
+ # ===================================================================
629
+ # §28 — 4.4.1 Keyboard extension requiring full access
630
+ # ===================================================================
631
+ # Keyboards must stay functional without full network access / "full access",
632
+ # and may only collect data to enhance the keyboard.
633
+ kb_plist=$(grep -rlE 'com\.apple\.keyboard-service' --include='Info.plist' "${GREP_PRUNE[@]}" . 2>/dev/null | pick_shallowest)
634
+ if [[ -n "$kb_plist" ]]; then
635
+ if grep -A1 'RequestsOpenAccess' "$kb_plist" 2>/dev/null | grep -q '<true'; then
636
+ warn "4.4.1 Keyboard extension — RequestsOpenAccess=true in $kb_plist; keyboards must remain functional without full access and may only collect data to enhance the keyboard (4.4.1). Justify full access in your review notes."
637
+ else
638
+ pass "4.4.1 Keyboard extension — present without requiring full access"
639
+ fi
640
+ fi
641
+
642
+ # ===================================================================
643
+ # §29 — 5.1.3 Health data with an iCloud sync path
644
+ # ===================================================================
645
+ # Personal health information must not be stored in iCloud, and HealthKit data
646
+ # may not be used for advertising/marketing.
647
+ if [[ -n "$IOS_DIR" ]] && grep -rqE 'import HealthKit|HKHealthStore' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
648
+ if grep -rqE 'import CloudKit|CKRecord|CKContainer|NSUbiquitousKeyValueStore|NSUbiquitousContainer' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
649
+ warn "5.1.3 Health data — HealthKit and iCloud/CloudKit are both used; personal health information must not be stored in iCloud (5.1.3). Verify HealthKit data is not synced to iCloud."
650
+ else
651
+ pass "5.1.3 Health data — HealthKit used without an obvious iCloud sync path"
652
+ fi
653
+ fi
654
+
655
+ # ===================================================================
656
+ # §30 — 5.4 VPN apps (NetworkExtension)
657
+ # ===================================================================
658
+ # VPN apps must be offered by an organization account, declare data collection
659
+ # on-screen before use, and may not sell/share data.
660
+ if [[ -n "$IOS_DIR" ]]; then
661
+ vpn_use=$(grep -rlE 'NEVPNManager|NETunnelProviderManager|NEPacketTunnelProvider|NEVPNProtocol' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
662
+ if [[ -n "$vpn_use" ]]; then
663
+ warn "5.4 VPN — NetworkExtension/NEVPNManager usage detected (e.g. $(basename "$vpn_use")); VPN apps must be offered by an organization account, disclose data collection on-screen before use, and not sell/share data (5.4). Verify compliance."
664
+ fi
665
+ fi
666
+
667
+ # ===================================================================
668
+ # §31 — 2.1 Demo account for a credential login
669
+ # ===================================================================
670
+ # Apps behind a login must give App Review working credentials (a demo account
671
+ # or notes). We fire only on a credential-login signal (a password field or a
672
+ # Login/SignIn view), then look for demo creds in fastlane review_information or
673
+ # the reviewer-prep notes. Social-only logins are not gated here (too noisy).
674
+ if [[ -n "$IOS_DIR" ]]; then
675
+ auth_signal=$(grep -rlE 'SecureField' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
676
+ [[ -z "$auth_signal" ]] && auth_signal=$(find "$IOS_DIR" "${PRUNE[@]}" \( -name '*Login*View*.swift' -o -name '*SignIn*View*.swift' \) 2>/dev/null | head -1)
677
+ if [[ -n "$auth_signal" ]]; then
678
+ demo_present=""
679
+ ri="$META_DIR/review_information"
680
+ if [[ -d "$ri" ]]; then
681
+ for f in demo_user.txt demo_password.txt notes.txt; do [[ -s "$ri/$f" ]] && demo_present=1; done
682
+ fi
683
+ if [[ -z "$demo_present" && -n "$REVIEW_PREP" && -f "$REVIEW_PREP" ]]; then
684
+ grep -qiE 'demo|review.*(account|credential)|test.*account' "$REVIEW_PREP" 2>/dev/null && demo_present=1
685
+ fi
686
+ if [[ -n "$demo_present" ]]; then
687
+ pass "2.1 Demo account — credential login present and reviewer demo credentials/notes found"
688
+ else
689
+ warn "2.1 Demo account — a credential login was detected (e.g. $(basename "$auth_signal")) but no demo account/credentials for App Review found (fastlane review_information or .reviewPrepNotes); apps behind a login must give reviewers working credentials (2.1)"
690
+ fi
691
+ fi
692
+ fi
693
+
694
+ # ===================================================================
695
+ # §32 — 2.5.2 Executable code download / hot-patch
696
+ # ===================================================================
697
+ # Apps must be self-contained; native hot-patching frameworks (JSPatch, Rollout,
698
+ # DynamicCocoa) download code that changes features and are a removal vector.
699
+ # JS-bundle OTA for React Native (CodePush) is allowed, so we do NOT flag it.
700
+ if [[ -n "$IOS_DIR" ]]; then
701
+ hotcode=$(grep -rlE 'import JSPatch|JSPatch\.|[Jj][Ss][Pp]atch|import Rollout|Rollout\.|rollout\.io|DynamicCocoa|import SwiftPatch' "$IOS_DIR" --include="*.swift" --include="*.m" --include="*.h" 2>/dev/null | head -1)
702
+ if [[ -n "$hotcode" ]]; then
703
+ warn "2.5.2 Executable code — a hot-patch / remote-code framework was detected (e.g. $(basename "$hotcode")); apps may not download or run code that changes features (JSPatch/Rollout-style native hot-patching). Allowed JS-bundle OTA (e.g. React Native CodePush) is fine — verify this is not native hot-patching (2.5.2)"
704
+ fi
705
+ fi
706
+
707
+ # ===================================================================
708
+ # §33 — 2.5.4 Background modes declared but unused
709
+ # ===================================================================
710
+ # Declare only the UIBackgroundModes the app actually uses; a mode declared with
711
+ # no matching API is a frequent rejection. We parse the array and check each
712
+ # declared mode against its framework/API in Swift.
713
+ if [[ -f "$INFO_PLIST" && -n "$IOS_DIR" ]]; then
714
+ modes=$(awk '/<key>UIBackgroundModes<\/key>/{f=1;next} f&&/<\/array>/{f=0} f&&/<string>/{gsub(/.*<string>|<\/string>.*/,""); print}' "$INFO_PLIST" 2>/dev/null)
715
+ if [[ -n "$modes" ]]; then
716
+ unused=""
717
+ while IFS= read -r m; do
718
+ [[ -z "$m" ]] && continue
719
+ case "$m" in
720
+ location) grep -rqE 'CLLocationManager|CoreLocation' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused location" ;;
721
+ audio) grep -rqE 'AVAudioSession|AVPlayer|AVAudioPlayer|AVQueuePlayer|AVAudioEngine' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused audio" ;;
722
+ voip) grep -rqE 'PushKit|PKPushRegistry|CallKit|CXProvider' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused voip" ;;
723
+ fetch) grep -rqE 'BGAppRefreshTask|BackgroundTasks|setMinimumBackgroundFetchInterval|performFetchWithCompletionHandler' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused fetch" ;;
724
+ processing) grep -rqE 'BGProcessingTask|BackgroundTasks' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused processing" ;;
725
+ bluetooth-central|bluetooth-peripheral) grep -rqE 'CoreBluetooth|CBCentralManager|CBPeripheralManager' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused $m" ;;
726
+ remote-notification) grep -rqE 'didReceiveRemoteNotification|UNUserNotificationCenter|registerForRemoteNotifications' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused remote-notification" ;;
727
+ esac
728
+ done <<< "$modes"
729
+ if [[ -n "$unused" ]]; then
730
+ warn "2.5.4 Background modes —$unused declared in UIBackgroundModes but no matching API usage found in Swift; declare only the background modes the app actually uses (2.5.4)"
731
+ else
732
+ pass "2.5.4 Background modes — declared modes have matching API usage"
733
+ fi
734
+ fi
735
+ fi
736
+
737
+ # ===================================================================
738
+ # §34 — 3.1.5(a) Cryptocurrency wallet / exchange / mining
739
+ # ===================================================================
740
+ if [[ -n "$IOS_DIR" ]]; then
741
+ crypto_sdk=$(grep -rlE 'import Web3|web3swift|Web3Swift|WalletConnect|TrustWalletCore|CoinbaseWalletSDK|SolanaSwift|CryptoMining|coinhive|MoneroMiner' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
742
+ if [[ -n "$crypto_sdk" ]]; then
743
+ warn "3.1.5(a) Cryptocurrency — a crypto wallet/exchange/mining signal was detected (e.g. $(basename "$crypto_sdk")); wallets & exchanges have entity and licensing requirements, and on-device mining is not permitted (3.1.5(a)). Verify eligibility."
744
+ fi
745
+ fi
746
+
747
+ # ===================================================================
748
+ # §35 — 4.2.3 Web-wrapper / thin app
749
+ # ===================================================================
750
+ # A thin WKWebView wrapper around a website is rejected under minimum
751
+ # functionality. Heuristic: WKWebView present in a project with very few Swift
752
+ # files. WARN (verify) — this is the most false-positive-prone of the batch.
753
+ if [[ -n "$IOS_DIR" ]]; then
754
+ if grep -rqE 'WKWebView' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
755
+ swift_n=$(find "$IOS_DIR" "${PRUNE[@]}" -name '*.swift' 2>/dev/null | wc -l | tr -d ' ')
756
+ if (( swift_n > 0 && swift_n <= 4 )); then
757
+ warn "4.2.3 Minimum functionality — the app appears to be a WKWebView wrapper with only $swift_n Swift file(s); a thin wrapper around a website is rejected under 4.2.3. Add native value, or verify this is a real app rather than a repackaged site."
758
+ fi
759
+ fi
760
+ fi
761
+
762
+ # ===================================================================
763
+ # §36 — 4.2.7 Remote desktop / host-mirroring
764
+ # ===================================================================
765
+ if [[ -n "$IOS_DIR" ]]; then
766
+ remote_desktop=$(grep -rlE 'import libvncclient|LibVNC|VNCClient|RDPSession|RDPKit|RemoteDesktopClient|import FreeRDP|JumpDesktop' "$IOS_DIR" --include="*.swift" --include="*.m" 2>/dev/null | head -1)
767
+ if [[ -n "$remote_desktop" ]]; then
768
+ warn "4.2.7 Remote desktop — a remote-desktop/mirroring signal was detected (e.g. $(basename "$remote_desktop")); host-mirroring apps must only show/control the owner's host, display host content (not App Store content), and be free or use IAP (4.2.7). Verify."
769
+ fi
770
+ fi
771
+
772
+ # ===================================================================
773
+ # §37 — 4.4.2 Safari extension / content blocker
774
+ # ===================================================================
775
+ safari_ext=$(grep -rlE 'com\.apple\.Safari\.(content-blocker|web-extension|extension)' --include='Info.plist' "${GREP_PRUNE[@]}" . 2>/dev/null | pick_shallowest)
776
+ if [[ -n "$safari_ext" ]]; then
777
+ warn "4.4.2 Safari extension — a Safari content-blocker / web extension was detected ($safari_ext); it must use the extension APIs as intended, do only what it declares, and not include hidden analytics/ads or track without consent (4.4.2). Verify."
778
+ fi
779
+
780
+ # ===================================================================
781
+ # §38 — 5.1.1(v) Account Sign-In: account creation without in-app deletion
782
+ # ===================================================================
783
+ # Apple 5.1.1(v): apps that support account creation must also let users delete
784
+ # their account from within the app. Detect account-creation signals, then look
785
+ # for an in-app deletion path. Deletion via a web page is missed → WARN, not FAIL.
786
+ if [[ -n "$IOS_DIR" ]]; then
787
+ signup=$(grep -rlE 'createUser|signUp|signup|createAccount|registerUser|registerNewUser|Auth\.auth\(\)\.createUser' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
788
+ if [[ -n "$signup" ]]; then
789
+ if grep -rqiE 'delete.?account|account.?deletion|closeAccount|deleteUser|removeAccount|deleteMyAccount' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
790
+ pass "5.1.1(v) Account deletion — account creation present and an in-app account-deletion path was found"
791
+ else
792
+ warn "5.1.1(v) Account deletion — account creation was detected (e.g. $(basename "$signup")) but no in-app account-deletion path found; apps that support account creation must let users delete their account from within the app (5.1.1(v))"
793
+ fi
794
+ fi
795
+ fi
796
+
797
+ # ===================================================================
798
+ # §39 — 5.1.4 Kids audience with third-party ads / analytics
799
+ # ===================================================================
800
+ # Kids Category apps may not include third-party advertising or analytics and
801
+ # must include a parental gate. We fire when the metadata targets a child
802
+ # audience AND an ad/analytics SDK is linked. tracking_sdk/analytics_sdk come
803
+ # from §3/§19.
804
+ if [[ -d "$META_DIR" && -n "$IOS_DIR" ]]; then
805
+ if grep -rqEiI 'for kids|for children|kids[[:space:]]?app|für kinder|para niños|pour enfants' "$META_DIR" 2>/dev/null; then
806
+ if [[ -n "$tracking_sdk" || -n "$analytics_sdk" ]]; then
807
+ warn "5.1.4 Kids — the metadata targets a child audience and a third-party ads/analytics SDK is linked (e.g. $(basename "${tracking_sdk:-$analytics_sdk}")); Kids Category apps may not include third-party advertising or analytics and must offer a parental gate (5.1.4)"
808
+ fi
809
+ fi
810
+ fi
811
+
812
+ # ===================================================================
813
+ # §40 — 5.3.4 Real-money gambling
814
+ # ===================================================================
815
+ if [[ -d "$META_DIR" ]]; then
816
+ gamble_re='real[ -]?money|gambling|casino|sportsbook|sports[ -]?betting|place[ -].*bets|wager(ing)?|roulette.*real'
817
+ gamble_hits=$(grep -rEniI "$gamble_re" "$META_DIR" 2>/dev/null | grep -v "^Binary file" | head -10)
818
+ if [[ -n "$gamble_hits" ]]; then
819
+ warn "5.3.4 Gambling — real-money gaming language in metadata; real-money gambling/lotteries need the proper licenses, must be geo-restricted to permitted regions, and must be free on the App Store (5.3.4):"
820
+ echo "$gamble_hits" | sed 's/^/ /'
821
+ fi
822
+ fi
823
+
824
+ # ===================================================================
825
+ # §41 — 5.5 Mobile Device Management
826
+ # ===================================================================
827
+ if [[ -n "$IOS_DIR" ]]; then
828
+ mdm_sig=$(grep -rlE 'import DeviceManagement|MDMConfiguration|ManagedAppConfiguration|com\.apple\.mdm' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
829
+ [[ -z "$mdm_sig" ]] && mdm_sig=$(grep -rlE 'com\.apple\.configuration\.managed' --include='*.plist' "${GREP_PRUNE[@]}" . 2>/dev/null | pick_shallowest)
830
+ if [[ -n "$mdm_sig" ]]; then
831
+ warn "5.5 MDM — a Mobile Device Management signal was detected (e.g. $(basename "$mdm_sig")); MDM apps require a commercial enterprise/education entity, may request the MDM capability only for that purpose, and must not sell or use the data for other ends (5.5). Verify eligibility."
832
+ fi
833
+ fi
834
+
526
835
  echo "---END-OF-SCAN---"