appstore-precheck 1.1.1 → 1.5.2

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.
@@ -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,315 @@ 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
+ # Anchor to the exact key: `NSAllowsArbitraryLoads</key>` (followed by `<`), so the
568
+ # narrower scoped exceptions NSAllowsArbitraryLoadsInWebContent / ...ForMedia — which
569
+ # do NOT disable ATS app-wide and are the recommended alternative — don't false-fire.
570
+ if awk '/NSAllowsArbitraryLoads</{getline; if ($0 ~ /<true/) print "ON"}' "$INFO_PLIST" 2>/dev/null | grep -q ON; then
571
+ 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)"
572
+ fi
573
+ fi
574
+
575
+ # ===================================================================
576
+ # §24 — 4.9 Apple Pay recurring-payment disclosure
577
+ # ===================================================================
578
+ # Apps using Apple Pay for recurring payments must disclose the renewal term,
579
+ # what's provided, the charges, and how to cancel. Gated strictly on the Apple
580
+ # Pay recurring API (PKRecurringPaymentRequest) so it does NOT conflate StoreKit
581
+ # auto-renew copy or a one-time PassKit payment with recurring Apple Pay. We don't
582
+ # try to detect the disclosure text (too noisy) — we flag it for manual verify.
583
+ if [[ -n "$IOS_DIR" ]] && grep -rqE 'PKRecurringPaymentRequest' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
584
+ 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)"
585
+ fi
586
+
587
+ # ===================================================================
588
+ # §25 — 5.6.1 Custom App Store review prompt
589
+ # ===================================================================
590
+ # Apple disallows custom review prompts and direct write-review links — apps must
591
+ # use the system SKStoreReviewController / requestReview API.
592
+ if [[ -n "$IOS_DIR" ]]; then
593
+ review_link=$(grep -rlE 'write-review|action=write-review|itms-apps[^"]*review' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
594
+ if [[ -n "$review_link" ]]; then
595
+ if grep -rqE 'requestReview|SKStoreReviewController|\.requestReview' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
596
+ pass "5.6.1 App reviews — uses the system requestReview API alongside the review link"
597
+ else
598
+ 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)"
599
+ fi
600
+ fi
601
+ fi
602
+
603
+ # ===================================================================
604
+ # §26 — 2.3.1 Misleading marketing claims in metadata
605
+ # ===================================================================
606
+ # Marketing the app for things iOS apps can't actually do (virus/malware
607
+ # scanners, fake speed boosters) is a 2.3.1 removal vector.
608
+ if [[ -d "$META_DIR" ]]; then
609
+ 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)'
610
+ mislead_hits=$(grep -rEniI "$mislead_re" "$META_DIR" 2>/dev/null | grep -v "^Binary file" | head -10)
611
+ if [[ -n "$mislead_hits" ]]; then
612
+ 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:"
613
+ echo "$mislead_hits" | sed 's/^/ /'
614
+ fi
615
+ fi
616
+
617
+ # ===================================================================
618
+ # §27 — 2.3.8 "For Kids/Children" wording outside the Kids Category
619
+ # ===================================================================
620
+ # Terms implying a child audience in name/subtitle/keywords/description are
621
+ # reserved for the Kids Category (2.3.8 / 5.1.4).
622
+ if [[ -d "$META_DIR" ]]; then
623
+ kids_re='for kids|for children|for your (kid|child)|kids[[:space:]]?app|für kinder|para niños|pour enfants'
624
+ kids_hits=$(grep -rEniI "$kids_re" "$META_DIR" 2>/dev/null | grep -v "^Binary file" | head -10)
625
+ # Cross-gate with §39 (5.1.4): when an ads/analytics SDK is also linked, §39 fires the
626
+ # more specific Kids finding for the same wording signal — don't double-count it here
627
+ # (one root signal must not cost two WARNs against the 5-WARN YELLOW threshold).
628
+ if [[ -n "$kids_hits" && -z "${tracking_sdk:-}" && -z "${analytics_sdk:-}" ]]; then
629
+ 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:"
630
+ echo "$kids_hits" | sed 's/^/ /'
631
+ fi
632
+ fi
633
+
634
+ # ===================================================================
635
+ # §28 — 4.4.1 Keyboard extension requiring full access
636
+ # ===================================================================
637
+ # Keyboards must stay functional without full network access / "full access",
638
+ # and may only collect data to enhance the keyboard.
639
+ kb_plist=$(grep -rlE 'com\.apple\.keyboard-service' --include='Info.plist' "${GREP_PRUNE[@]}" . 2>/dev/null | pick_shallowest)
640
+ if [[ -n "$kb_plist" ]]; then
641
+ if grep -A1 'RequestsOpenAccess' "$kb_plist" 2>/dev/null | grep -q '<true'; then
642
+ warn "4.4.1 Keyboard extension — RequestsOpenAccess=true in $(basename "$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."
643
+ else
644
+ pass "4.4.1 Keyboard extension — present without requiring full access"
645
+ fi
646
+ fi
647
+
648
+ # ===================================================================
649
+ # §29 — 5.1.3 Health data with an iCloud sync path
650
+ # ===================================================================
651
+ # Personal health information must not be stored in iCloud, and HealthKit data
652
+ # may not be used for advertising/marketing.
653
+ if [[ -n "$IOS_DIR" ]] && grep -rqE 'import HealthKit|HKHealthStore' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
654
+ if grep -rqE 'import CloudKit|CKRecord|CKContainer|NSUbiquitousKeyValueStore|NSUbiquitousContainer' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
655
+ 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."
656
+ else
657
+ pass "5.1.3 Health data — HealthKit used without an obvious iCloud sync path"
658
+ fi
659
+ fi
660
+
661
+ # ===================================================================
662
+ # §30 — 5.4 VPN apps (NetworkExtension)
663
+ # ===================================================================
664
+ # VPN apps must be offered by an organization account, declare data collection
665
+ # on-screen before use, and may not sell/share data.
666
+ if [[ -n "$IOS_DIR" ]]; then
667
+ vpn_use=$(grep -rlE 'NEVPNManager|NETunnelProviderManager|NEPacketTunnelProvider|NEVPNProtocol' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
668
+ if [[ -n "$vpn_use" ]]; then
669
+ 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."
670
+ fi
671
+ fi
672
+
673
+ # ===================================================================
674
+ # §31 — 2.1 Demo account for a credential login
675
+ # ===================================================================
676
+ # Apps behind a login must give App Review working credentials (a demo account
677
+ # or notes). We fire only on a credential-login signal (a password field or a
678
+ # Login/SignIn view), then look for demo creds in fastlane review_information or
679
+ # the reviewer-prep notes. Social-only logins are not gated here (too noisy).
680
+ if [[ -n "$IOS_DIR" ]]; then
681
+ auth_signal=$(grep -rlE 'SecureField' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
682
+ [[ -z "$auth_signal" ]] && auth_signal=$(find "$IOS_DIR" "${PRUNE[@]}" \( -name '*Login*View*.swift' -o -name '*SignIn*View*.swift' \) 2>/dev/null | head -1)
683
+ if [[ -n "$auth_signal" ]]; then
684
+ demo_present=""
685
+ ri="$META_DIR/review_information"
686
+ if [[ -d "$ri" ]]; then
687
+ for f in demo_user.txt demo_password.txt notes.txt; do [[ -s "$ri/$f" ]] && demo_present=1; done
688
+ fi
689
+ if [[ -z "$demo_present" && -n "$REVIEW_PREP" && -f "$REVIEW_PREP" ]]; then
690
+ grep -qiE 'demo|review.*(account|credential)|test.*account' "$REVIEW_PREP" 2>/dev/null && demo_present=1
691
+ fi
692
+ if [[ -n "$demo_present" ]]; then
693
+ pass "2.1 Demo account — credential login present and reviewer demo credentials/notes found"
694
+ else
695
+ 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)"
696
+ fi
697
+ fi
698
+ fi
699
+
700
+ # ===================================================================
701
+ # §32 — 2.5.2 Executable code download / hot-patch
702
+ # ===================================================================
703
+ # Apps must be self-contained; native hot-patching frameworks (JSPatch, Rollout,
704
+ # DynamicCocoa) download code that changes features and are a removal vector.
705
+ # JS-bundle OTA for React Native (CodePush) is allowed, so we do NOT flag it.
706
+ if [[ -n "$IOS_DIR" ]]; then
707
+ 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)
708
+ if [[ -n "$hotcode" ]]; then
709
+ 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)"
710
+ fi
711
+ fi
712
+
713
+ # ===================================================================
714
+ # §33 — 2.5.4 Background modes declared but unused
715
+ # ===================================================================
716
+ # Declare only the UIBackgroundModes the app actually uses; a mode declared with
717
+ # no matching API is a frequent rejection. We parse the array and check each
718
+ # declared mode against its framework/API in Swift.
719
+ if [[ -f "$INFO_PLIST" && -n "$IOS_DIR" ]]; then
720
+ modes=$(awk '/<key>UIBackgroundModes<\/key>/{f=1;next} f&&/<\/array>/{f=0} f&&/<string>/{gsub(/.*<string>|<\/string>.*/,""); print}' "$INFO_PLIST" 2>/dev/null)
721
+ if [[ -n "$modes" ]]; then
722
+ unused=""
723
+ while IFS= read -r m; do
724
+ [[ -z "$m" ]] && continue
725
+ case "$m" in
726
+ location) grep -rqE 'CLLocationManager|CoreLocation' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused location" ;;
727
+ audio) grep -rqE 'AVAudioSession|AVPlayer|AVAudioPlayer|AVQueuePlayer|AVAudioEngine|AVKit|VideoPlayer|AVPlayerViewController|MPMusicPlayerController|MPNowPlayingInfoCenter' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused audio" ;;
728
+ voip) grep -rqE 'PushKit|PKPushRegistry|CallKit|CXProvider' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused voip" ;;
729
+ fetch) grep -rqE 'BGAppRefreshTask|BGTaskScheduler|BackgroundTasks|setMinimumBackgroundFetchInterval|performFetchWithCompletionHandler' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused fetch" ;;
730
+ processing) grep -rqE 'BGProcessingTask|BGTaskScheduler|BackgroundTasks' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused processing" ;;
731
+ bluetooth-central|bluetooth-peripheral) grep -rqE 'CoreBluetooth|CBCentralManager|CBPeripheralManager' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused $m" ;;
732
+ remote-notification) grep -rqE 'didReceiveRemoteNotification|UNUserNotificationCenter|registerForRemoteNotifications' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused remote-notification" ;;
733
+ esac
734
+ done <<< "$modes"
735
+ if [[ -n "$unused" ]]; then
736
+ 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)"
737
+ else
738
+ pass "2.5.4 Background modes — declared modes have matching API usage"
739
+ fi
740
+ fi
741
+ fi
742
+
743
+ # ===================================================================
744
+ # §34 — 3.1.5(a) Cryptocurrency wallet / exchange / mining
745
+ # ===================================================================
746
+ if [[ -n "$IOS_DIR" ]]; then
747
+ crypto_sdk=$(grep -rlE 'import Web3|web3swift|Web3Swift|WalletConnect|TrustWalletCore|CoinbaseWalletSDK|SolanaSwift|CryptoMining|coinhive|MoneroMiner' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
748
+ if [[ -n "$crypto_sdk" ]]; then
749
+ 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."
750
+ fi
751
+ fi
752
+
753
+ # ===================================================================
754
+ # §35 — 4.2.3 Web-wrapper / thin app
755
+ # ===================================================================
756
+ # A thin WKWebView wrapper around a website is rejected under minimum
757
+ # functionality. Heuristic: WKWebView present in a project with very few Swift
758
+ # files. WARN (verify) — this is the most false-positive-prone of the batch.
759
+ if [[ -n "$IOS_DIR" ]]; then
760
+ if grep -rqE 'WKWebView' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
761
+ swift_n=$(find "$IOS_DIR" "${PRUNE[@]}" -name '*.swift' 2>/dev/null | wc -l | tr -d ' ')
762
+ if (( swift_n > 0 && swift_n <= 4 )); then
763
+ 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."
764
+ fi
765
+ fi
766
+ fi
767
+
768
+ # ===================================================================
769
+ # §36 — 4.2.7 Remote desktop / host-mirroring
770
+ # ===================================================================
771
+ if [[ -n "$IOS_DIR" ]]; then
772
+ 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)
773
+ if [[ -n "$remote_desktop" ]]; then
774
+ 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."
775
+ fi
776
+ fi
777
+
778
+ # ===================================================================
779
+ # §37 — 4.4.2 Safari extension / content blocker
780
+ # ===================================================================
781
+ safari_ext=$(grep -rlE 'com\.apple\.Safari\.(content-blocker|web-extension|extension)' --include='Info.plist' "${GREP_PRUNE[@]}" . 2>/dev/null | pick_shallowest)
782
+ if [[ -n "$safari_ext" ]]; then
783
+ warn "4.4.2 Safari extension — a Safari content-blocker / web extension was detected ($(basename "$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."
784
+ fi
785
+
786
+ # ===================================================================
787
+ # §38 — 5.1.1(v) Account Sign-In: account creation without in-app deletion
788
+ # ===================================================================
789
+ # Apple 5.1.1(v): apps that support account creation must also let users delete
790
+ # their account from within the app. Detect account-creation signals, then look
791
+ # for an in-app deletion path. Deletion via a web page is missed → WARN, not FAIL.
792
+ if [[ -n "$IOS_DIR" ]]; then
793
+ signup=$(grep -rlE 'createUser|signUp|signup|createAccount|registerUser|registerNewUser|Auth\.auth\(\)\.createUser' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
794
+ if [[ -n "$signup" ]]; then
795
+ if grep -rqiE 'delete.?account|account.?deletion|closeAccount|deleteUser|removeAccount|deleteMyAccount' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
796
+ pass "5.1.1(v) Account deletion — account creation present and an in-app account-deletion path was found"
797
+ else
798
+ 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))"
799
+ fi
800
+ fi
801
+ fi
802
+
803
+ # ===================================================================
804
+ # §39 — 5.1.4 Kids audience with third-party ads / analytics
805
+ # ===================================================================
806
+ # Kids Category apps may not include third-party advertising or analytics and
807
+ # must include a parental gate. We fire when the metadata targets a child
808
+ # audience AND an ad/analytics SDK is linked. tracking_sdk/analytics_sdk come
809
+ # from §3/§19.
810
+ if [[ -d "$META_DIR" && -n "$IOS_DIR" ]]; then
811
+ if grep -rqEiI 'for kids|for children|kids[[:space:]]?app|für kinder|para niños|pour enfants' "$META_DIR" 2>/dev/null; then
812
+ if [[ -n "$tracking_sdk" || -n "$analytics_sdk" ]]; then
813
+ 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)"
814
+ fi
815
+ fi
816
+ fi
817
+
818
+ # ===================================================================
819
+ # §40 — 5.3.4 Real-money gambling
820
+ # ===================================================================
821
+ if [[ -d "$META_DIR" ]]; then
822
+ gamble_re='real[ -]?money|gambling|casino|sportsbook|sports[ -]?betting|place[ -].*bets|wager(ing)?|roulette.*real'
823
+ gamble_hits=$(grep -rEniI "$gamble_re" "$META_DIR" 2>/dev/null | grep -v "^Binary file" | head -10)
824
+ if [[ -n "$gamble_hits" ]]; then
825
+ 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):"
826
+ echo "$gamble_hits" | sed 's/^/ /'
827
+ fi
828
+ fi
829
+
830
+ # ===================================================================
831
+ # §41 — 5.5 Mobile Device Management
832
+ # ===================================================================
833
+ if [[ -n "$IOS_DIR" ]]; then
834
+ mdm_sig=$(grep -rlE 'import DeviceManagement|MDMConfiguration|ManagedAppConfiguration|com\.apple\.mdm' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
835
+ [[ -z "$mdm_sig" ]] && mdm_sig=$(grep -rlE 'com\.apple\.configuration\.managed' --include='*.plist' "${GREP_PRUNE[@]}" . 2>/dev/null | pick_shallowest)
836
+ if [[ -n "$mdm_sig" ]]; then
837
+ 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."
838
+ fi
839
+ fi
840
+
526
841
  echo "---END-OF-SCAN---"
@@ -5,8 +5,8 @@
5
5
  # the .precheck-pass token action — so the verdict is machine-testable, not just an
6
6
  # agent judgement.
7
7
  #
8
- # GREEN 0 FAIL and <=2 WARN -> token: write
9
- # YELLOW 0 FAIL and >=3 WARN -> token: hold (needs explicit human confirmation)
8
+ # GREEN 0 FAIL and <=4 WARN -> token: write
9
+ # YELLOW 0 FAIL and >=5 WARN -> token: hold (needs explicit human confirmation)
10
10
  # RED >=1 FAIL -> token: remove
11
11
  #
12
12
  # Usage:
@@ -49,7 +49,7 @@ passes=$(count '^PASS:')
49
49
 
50
50
  if (( fails >= 1 )); then
51
51
  verdict="RED"; token="remove"; code=1
52
- elif (( warns >= 3 )); then
52
+ elif (( warns >= 5 )); then
53
53
  verdict="YELLOW"; token="hold"; code=2
54
54
  else
55
55
  verdict="GREEN"; token="write"; code=0