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.
- package/CHANGELOG.md +118 -0
- package/README.md +216 -52
- package/package.json +2 -2
- package/skills/appstore-precheck/SKILL.md +200 -81
- package/skills/appstore-precheck/evals/README.md +3 -2
- package/skills/appstore-precheck/evals/evals.json +6 -3
- package/skills/appstore-precheck/guidelines-baseline.json +19 -7
- package/skills/appstore-precheck/references/methodology.md +79 -7
- package/skills/appstore-precheck/references/pierre-deep-review.md +272 -0
- package/skills/appstore-precheck/scripts/scan.sh +319 -10
- package/skills/appstore-precheck/scripts/verdict.sh +3 -3
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
394
|
+
warn "4.2 Minimum functionality — no TabView/NavigationStack found (heuristic, may be a false positive)"
|
|
391
395
|
else
|
|
392
|
-
pass "4.
|
|
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---"
|