#!/bin/bash

set -euo pipefail

ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR"

submission_mode=false
freeze_mode=false
archive_path=""
ipa_path=""
scan_app_path=""
ipa_temp_dir=""

expected_team_id="ZX93RKSPHH"
expected_signing_authority="Apple Distribution: ZhiEra LLC (ZX93RKSPHH)"
expected_app_group="group.com.zhiera.kivo.shared"
expected_keychain_group="ZX93RKSPHH.com.zhiera.kivo"
expected_harfbuzz_version="14.2.0"

# Archive compatibility exceptions below are conditional on these source/build
# checks. Initialize them explicitly so a failed xcodebuild inspection can
# never turn into an implicit allowlist under `set -u`.
app_store_lite_build_condition_is_present=false
signal_ui_lite_build_condition_is_present=false
signal_service_kit_lite_build_condition_is_present=false
main_financial_sources_are_excluded=false
signal_ui_payment_engine_is_excluded=false
main_story_ui_is_excluded=false
signal_ui_story_ui_is_excluded=false
main_exclusions_line=""
signal_service_kit_exclusions_line=""

while (($#)); do
  case "$1" in
    --submission)
      submission_mode=true
      shift
      ;;
    --freeze)
      freeze_mode=true
      shift
      ;;
    --archive)
      archive_path="${2:?--archive requires an .xcarchive path}"
      shift 2
      ;;
    --ipa)
      ipa_path="${2:?--ipa requires an .ipa path}"
      shift 2
      ;;
    --scan-app)
      scan_app_path="${2:?--scan-app requires an .app path}"
      shift 2
      ;;
    *)
      echo "Unknown argument: $1" >&2
      exit 2
      ;;
  esac
done

failures=0
pass() { printf 'PASS  %s\n' "$1"; }
fail() { printf 'FAIL  %s\n' "$1" >&2; failures=$((failures + 1)); }
info() { printf 'INFO  %s\n' "$1"; }

plist_value() {
  local plist="$1"
  local key="$2"
  /usr/libexec/PlistBuddy -c "Print :$key" "$plist" 2>/dev/null || true
}

signed_entitlement() {
  local bundle="$1"
  local keypath="$2"
  codesign -d --entitlements :- "$bundle" 2>/dev/null \
    | plutil -extract "$keypath" raw -o - -- - 2>/dev/null \
    || true
}

profile_value() {
  local bundle="$1"
  local keypath="$2"
  security cms -D -i "$bundle/embedded.mobileprovision" 2>/dev/null \
    | plutil -extract "$keypath" raw -o - -- - 2>/dev/null \
    || true
}

verify_expected_value() {
  local label="$1"
  local actual="$2"
  local expected="$3"
  if [[ "$actual" == "$expected" ]]; then
    pass "$label is $expected"
  else
    fail "$label is '$actual' (expected '$expected')"
  fi
}

verify_distribution_bundle() {
  local bundle="$1"
  local label="$2"
  local expected_bundle_id="$3"
  local expected_executable="$4"
  local expected_profile="$5"
  local requires_push="$6"

  if [[ ! -d "$bundle" ]]; then
    fail "$label bundle is missing at $bundle"
    return
  fi

  local info_plist="$bundle/Info.plist"
  local executable="$bundle/$expected_executable"
  if [[ ! -f "$info_plist" || ! -x "$executable" ]]; then
    fail "$label is missing Info.plist or executable $expected_executable"
    return
  fi

  verify_expected_value "$label bundle identifier" \
    "$(plist_value "$info_plist" CFBundleIdentifier)" "$expected_bundle_id"
  verify_expected_value "$label version" \
    "$(plist_value "$info_plist" CFBundleShortVersionString)" "$release_version"
  verify_expected_value "$label build" \
    "$(plist_value "$info_plist" CFBundleVersion)" "$release_build"
  verify_expected_value "$label executable name" \
    "$(plist_value "$info_plist" CFBundleExecutable)" "$expected_executable"
  if [[ "$submission_mode" == true ]]; then
    verify_expected_value "$label processed export-compliance answer" \
      "$(plist_value "$info_plist" ITSAppUsesNonExemptEncryption)" "$main_export_answer"
  fi

  if codesign --verify --strict "$bundle" >/dev/null 2>&1; then
    pass "$label code signature is structurally valid"
  else
    fail "$label code signature is invalid"
  fi

  local signature_details
  signature_details="$(codesign -dv --verbose=4 "$bundle" 2>&1 || true)"
  if grep -F -q "TeamIdentifier=$expected_team_id" <<<"$signature_details"; then
    pass "$label signature uses team $expected_team_id"
  else
    fail "$label signature does not use team $expected_team_id"
  fi
  if grep -F -q "Authority=$expected_signing_authority" <<<"$signature_details"; then
    pass "$label signature uses the ZhiEra Apple Distribution identity"
  else
    fail "$label signature does not use '$expected_signing_authority'"
  fi

  if [[ ! -f "$bundle/embedded.mobileprovision" ]]; then
    fail "$label has no embedded distribution provisioning profile"
  else
    verify_expected_value "$label provisioning profile" \
      "$(profile_value "$bundle" Name)" "$expected_profile"
    verify_expected_value "$label profile team" \
      "$(profile_value "$bundle" TeamIdentifier.0)" "$expected_team_id"
    verify_expected_value "$label profile application identifier" \
      "$(profile_value "$bundle" Entitlements.application-identifier)" \
      "$expected_team_id.$expected_bundle_id"
    verify_expected_value "$label profile get-task-allow" \
      "$(profile_value "$bundle" Entitlements.get-task-allow)" "false"
    verify_expected_value "$label profile beta-reports-active" \
      "$(profile_value "$bundle" Entitlements.beta-reports-active)" "true"

    if [[ -n "$(profile_value "$bundle" ProvisionedDevices.0)" ]]; then
      fail "$label profile contains development/ad-hoc ProvisionedDevices"
    elif [[ "$(profile_value "$bundle" ProvisionsAllDevices)" == "true" ]]; then
      fail "$label profile is an enterprise profile"
    else
      pass "$label profile is an App Store distribution profile"
    fi
  fi

  verify_expected_value "$label signed application identifier" \
    "$(signed_entitlement "$bundle" application-identifier)" \
    "$expected_team_id.$expected_bundle_id"
  verify_expected_value "$label signed team entitlement" \
    "$(signed_entitlement "$bundle" 'com\.apple\.developer\.team-identifier')" \
    "$expected_team_id"
  verify_expected_value "$label signed get-task-allow" \
    "$(signed_entitlement "$bundle" get-task-allow)" "false"
  verify_expected_value "$label signed beta-reports-active" \
    "$(signed_entitlement "$bundle" beta-reports-active)" "true"
  verify_expected_value "$label signed App Group" \
    "$(signed_entitlement "$bundle" 'com\.apple\.security\.application-groups.0')" \
    "$expected_app_group"
  verify_expected_value "$label signed keychain group" \
    "$(signed_entitlement "$bundle" keychain-access-groups.0)" \
    "$expected_keychain_group"

  if [[ "$requires_push" == true ]]; then
    verify_expected_value "$label signed APNs environment" \
      "$(signed_entitlement "$bundle" aps-environment)" "production"
    verify_expected_value "$label profile APNs environment" \
      "$(profile_value "$bundle" Entitlements.aps-environment)" "production"
  fi
}

verify_privacy_manifests() {
  local app="$1"
  local label="$2"
  local required_manifests=(
    "$app/PrivacyInfo.xcprivacy"
    "$app/PlugIns/SignalNSE.appex/PrivacyInfo.xcprivacy"
    "$app/PlugIns/SignalShareExtension.appex/PrivacyInfo.xcprivacy"
    "$app/Frameworks/SignalServiceKit.framework/PrivacyInfo.xcprivacy"
    "$app/Frameworks/WalletCore.framework/PrivacyInfo.xcprivacy"
  )
  local manifest
  for manifest in "${required_manifests[@]}"; do
    if [[ -f "$manifest" ]] && plutil -lint "$manifest" >/dev/null; then
      pass "$label contains valid ${manifest#$app/}"
    else
      fail "$label is missing valid required manifest ${manifest#$app/}"
    fi
  done

  local manifest_count=0
  while IFS= read -r -d '' manifest; do
    manifest_count=$((manifest_count + 1))
    plutil -lint "$manifest" >/dev/null \
      || fail "$label contains invalid privacy manifest ${manifest#$app/}"
  done < <(find "$app" -name PrivacyInfo.xcprivacy -type f -print0 2>/dev/null)
  if ((manifest_count >= ${#required_manifests[@]})); then
    pass "$label contains $manifest_count validatable privacy manifests"
  else
    fail "$label contains only $manifest_count privacy manifests"
  fi

  verify_required_reason_manifest \
    "$app/Frameworks/SignalServiceKit.framework/PrivacyInfo.xcprivacy" \
    "$label SignalServiceKit.framework" \
    'NSPrivacyAccessedAPICategoryFileTimestamp=C617.1' \
    'NSPrivacyAccessedAPICategoryDiskSpace=E174.1' \
    'NSPrivacyAccessedAPICategoryUserDefaults=1C8F.1,CA92.1'
  verify_required_reason_manifest \
    "$app/Frameworks/WalletCore.framework/PrivacyInfo.xcprivacy" \
    "$label WalletCore.framework" \
    'NSPrivacyAccessedAPICategoryFileTimestamp=C617.1' \
    'NSPrivacyAccessedAPICategorySystemBootTime=35F9.1'
}

verify_required_reason_manifest() {
  local manifest="$1"
  local label="$2"
  shift 2

  if [[ ! -f "$manifest" ]]; then
    fail "$label is missing its privacy manifest"
    return
  fi
  if ! /usr/bin/plutil -lint "$manifest" >/dev/null; then
    fail "$label privacy manifest is invalid"
    return
  fi

  if /usr/bin/plutil -convert json -o - "$manifest" \
    | /usr/bin/ruby -rjson -e '
      document = JSON.parse(STDIN.read)
      abort "NSPrivacyTracking must be false" unless document["NSPrivacyTracking"] == false
      abort "tracking domains must be empty" unless document["NSPrivacyTrackingDomains"] == []
      abort "collected data types must be empty" unless document["NSPrivacyCollectedDataTypes"] == []

      entries = document.fetch("NSPrivacyAccessedAPITypes")
      actual = {}
      entries.each do |entry|
        category = entry.fetch("NSPrivacyAccessedAPIType")
        abort "duplicate category #{category}" if actual.key?(category)
        actual[category] = entry.fetch("NSPrivacyAccessedAPITypeReasons").sort
      end

      expected = ARGV.each_with_object({}) do |specification, result|
        category, reasons = specification.split("=", 2)
        result[category] = reasons.split(",").sort
      end
      unless actual == expected
        abort "required-reason declaration mismatch: actual=#{actual.inspect} expected=#{expected.inspect}"
      end
    ' -- "$@"
  then
    pass "$label declares only its reviewed Required Reason APIs"
  else
    fail "$label privacy manifest does not match its reviewed Required Reason APIs"
  fi
}

format_artifact_matches() {
  local app="$1"
  sed "s#^$app/##" \
    | sed '/^$/d' \
    | LC_ALL=C sort -u \
    | head -n 12 \
    | tr '\n' ' '
}

report_layer_scan() {
  local app="$1"
  local label="$2"
  local matches="$3"
  local pattern="$4"
  if [[ -n "$matches" ]]; then
    local marker_inventory
    local matched_file
    marker_inventory="$(
      while IFS= read -r matched_file; do
        [[ -f "$matched_file" ]] || continue
        rg -a -i -o "$pattern" "$matched_file" 2>/dev/null || true
      done <<<"$matches" \
        | LC_ALL=C sort -fu \
        | head -n 12 \
        | tr '\n' ' '
    )"
    fail "$label contains forbidden marker(s) [$marker_inventory] in: $(format_artifact_matches "$app" <<<"$matches")"
  else
    pass "$label has no forbidden disabled-product or upstream runtime marker"
  fi
}

record_reviewed_binary_compatibility() {
  local binary="$1"
  local label="$2"
  local pattern="$3"
  local category="$4"
  [[ -f "$binary" ]] || return

  local marker_count
  local digest
  marker_count="$(rg -a -i -o "$pattern" "$binary" 2>/dev/null | wc -l | tr -d ' ' || true)"
  digest="$(shasum -a 256 "$binary" | awk '{print $1}')"
  if [[ "$marker_count" =~ ^[1-9][0-9]*$ ]]; then
    info "$label retains $marker_count reviewed $category compatibility marker(s); binary sha256=$digest"
  else
    pass "$label contains no reviewed $category compatibility markers (sha256=$digest)"
  fi
}

verify_sanitized_localizations() {
  local app="$1"
  local label="$2"
  local financial_keys_manifest="Scripts/app_store_lite_excluded_financial_localization_keys.txt"
  local paid_backup_keys_manifest="Scripts/app_store_lite_excluded_paid_backup_localization_keys.txt"
  local localization_count=0
  local leaked_files
  local missing_free_backup_files
  local strings_file

  while IFS= read -r -d '' strings_file; do
    localization_count=$((localization_count + 1))
  done < <(find "$app" -mindepth 2 -maxdepth 2 -type f -path '*.lproj/Localizable.strings' -print0 2>/dev/null)

  # Compare parsed plist keys against the exact reviewed manifest. This is
  # intentionally not a word/prefix scan: Kivo's local identity-wallet copy
  # and ordinary notification badges remain legitimate product resources.
  leaked_files="$(/usr/bin/ruby -rjson -ropen3 -rset -e '
    excluded = ARGV.shift(2).flat_map { |path| File.readlines(path, chomp: true) }.to_set
    ARGV.each do |path|
      json, status = Open3.capture2e("plutil", "-convert", "json", "-o", "-", path)
      unless status.success?
        puts "#{path}\tUNREADABLE"
        next
      end
      leaked = JSON.parse(json).keys.select { |key| excluded.include?(key) }.sort
      puts "#{path}\t#{leaked.first} (+#{leaked.length - 1} more)" unless leaked.empty?
    end
  ' "$financial_keys_manifest" "$paid_backup_keys_manifest" \
    "$app"/*.lproj/Localizable.strings 2>/dev/null || true)"

  # Free encrypted backup and restore remain part of App Store Lite. These
  # representative keys must survive the exact paid-product sanitization in
  # every locale, preventing a future broad backup-prefix filter.
  missing_free_backup_files="$(/usr/bin/ruby -rjson -ropen3 -e '
    required = %w[
      BACKUP_ONBOARDING_INTRO_TITLE
      BACKUP_RESTORE_MODAL_TITLE
      BACKUP_SETTINGS_MANUAL_BACKUP_BUTTON_TITLE
      CHOOSE_BACKUP_PLAN_FREE_PLAN_TITLE
      ONBOARDING_CHOOSE_RESTORE_METHOD_BACKUPS_TITLE
    ]
    ARGV.each do |path|
      json, status = Open3.capture2e("plutil", "-convert", "json", "-o", "-", path)
      unless status.success?
        puts "#{path}\tUNREADABLE"
        next
      end
      missing = required - JSON.parse(json).keys
      puts "#{path}\t#{missing.join(",")}" unless missing.empty?
    end
  ' "$app"/*.lproj/Localizable.strings 2>/dev/null || true)"

  if [[ "$localization_count" == 47 ]]; then
    pass "$label contains all 47 expected localized product catalogues"
  else
    fail "$label contains $localization_count localized product catalogues (expected 47)"
  fi
  if [[ -n "$leaked_files" ]]; then
    fail "$label still contains exact reviewed financial or paid-backup localization keys in: $(format_artifact_matches "$app" <<<"$leaked_files")"
  else
    pass "$label omits all 509 financial and 91 paid-backup localization keys"
  fi
  if [[ -n "$missing_free_backup_files" ]]; then
    fail "$label lost required free-backup or restore localization keys in: $(format_artifact_matches "$app" <<<"$missing_free_backup_files")"
  else
    pass "$label retains free-backup and restore localization keys in all 47 catalogues"
  fi
}

verify_deferred_product_localizations() {
  local app="$1"
  local label="$2"
  local deferred_keys_manifest="Scripts/app_store_lite_excluded_deferred_localization_keys.txt"
  local deferred_key
  local leaked_files

  if [[ ! -f "$deferred_keys_manifest" ]]; then
    fail "$label cannot verify deferred-product localizations because $deferred_keys_manifest is missing"
    return
  fi
  deferred_key="$(<"$deferred_keys_manifest")"

  leaked_files="$(/usr/bin/ruby -rjson -ropen3 -e '
    key = ARGV.shift
    ARGV.each do |path|
      json, status = Open3.capture2e("plutil", "-convert", "json", "-o", "-", path)
      unless status.success?
        puts "#{path}\tUNREADABLE"
        next
      end
      puts path if JSON.parse(json).key?(key)
    end
  ' "$deferred_key" "$app"/*.lproj/Localizable.strings 2>/dev/null || true)"

  if [[ -n "$leaked_files" ]]; then
    fail "$label contains deferred-product localization key $deferred_key in: $(format_artifact_matches "$app" <<<"$leaked_files")"
  else
    pass "$label omits deferred-product localization key $deferred_key"
  fi
}

verify_sanitized_plural_localizations() {
  local app="$1"
  local label="$2"
  local excluded_plural_keys_manifest="Scripts/app_store_lite_excluded_paid_backup_plural_keys.txt"
  local localization_count=0
  local leaked_files
  local strings_file

  while IFS= read -r -d '' strings_file; do
    localization_count=$((localization_count + 1))
  done < <(find "$app" -mindepth 2 -maxdepth 2 -type f -path '*.lproj/PluralAware.stringsdict' -print0 2>/dev/null)

  leaked_files="$(/usr/bin/ruby -rjson -ropen3 -rset -e '
    excluded = File.readlines(ARGV.shift, chomp: true).to_set
    ARGV.each do |path|
      json, status = Open3.capture2e("plutil", "-convert", "json", "-o", "-", path)
      unless status.success?
        puts "#{path}\tUNREADABLE"
        next
      end
      leaked = JSON.parse(json).keys.select { |key| excluded.include?(key) }.sort
      puts "#{path}\t#{leaked.join(",")}" unless leaked.empty?
    end
  ' "$excluded_plural_keys_manifest" "$app"/*.lproj/PluralAware.stringsdict 2>/dev/null || true)"

  if [[ "$localization_count" == 47 ]]; then
    pass "$label contains all 47 expected plural-aware catalogues"
  else
    fail "$label contains $localization_count plural-aware catalogues (expected 47)"
  fi
  if [[ -n "$leaked_files" ]]; then
    fail "$label still contains the paid-backup plural localization key in: $(format_artifact_matches "$app" <<<"$leaked_files")"
  else
    pass "$label omits the paid-backup plural localization key in all 47 catalogues"
  fi
}

verify_disabled_financial_resources() {
  local app="$1"
  local label="$2"
  local loose_matches
  local asset_catalog
  local asset_info
  local asset_names
  local asset_failures=""
  local forbidden_asset_name_pattern='"Name"[[:space:]]*:[[:space:]]*"(mobilecoin-24|payment|payment-28|payments-lock|credit-or-debit-card|giphy_logo|paypal-logo|logo_ideal|badge-gifting-promo-image|gift-bow|gift-thumbnail|sustainer-heart|signal-backups-48|official-wallpaper|custom-story-dark-36|custom-story-light-36|tab-stories|gift|badge-multi|official|official-no-color|stories|stories-fill-compact)"'

  loose_matches="$(find "$app" \
    \( -path '*/_CodeSignature' \) -prune -o \
    -type f \( \
      -name 'add-money.json' -o \
      -name 'cash-out.json' -o \
      -name 'mobilecoin-24.*' -o \
      -name 'payment.pdf' -o \
      -name 'payment-28.*' -o \
      -name 'payments-lock.*' -o \
      -name 'story_viewer_onboarding_1.json' -o \
      -name 'story_viewer_onboarding_2.json' -o \
      -name 'story_viewer_onboarding_3.json' -o \
      -name 'Hatsuishi-UPM800.otf' -o \
      -name 'sonarping.mp3' \
    \) -print 2>/dev/null || true)"
  if [[ -n "$loose_matches" ]]; then
    fail "$label contains exact disabled financial resource files: $(format_artifact_matches "$app" <<<"$loose_matches")"
  else
    pass "$label contains no exact disabled financial resource files"
  fi

  while IFS= read -r -d '' asset_catalog; do
    asset_info="$(xcrun assetutil --info "$asset_catalog" 2>/dev/null || true)"
    if [[ -z "$asset_info" ]]; then
      asset_failures+="$asset_catalog"$'\t'"UNREADABLE"$'\n'
      continue
    fi
    asset_names="$(rg -o "$forbidden_asset_name_pattern" <<<"$asset_info" \
      | sed -E 's/^.*"(mobilecoin-24|payment|payment-28|payments-lock|credit-or-debit-card|giphy_logo|paypal-logo|logo_ideal|badge-gifting-promo-image|gift-bow|gift-thumbnail|sustainer-heart|signal-backups-48|official-wallpaper|custom-story-dark-36|custom-story-light-36|tab-stories|gift|badge-multi|official|official-no-color|stories|stories-fill-compact)"$/\1/' \
      | LC_ALL=C sort -u \
      | tr '\n' ',' \
      | sed 's/,$//' \
      || true)"
    if [[ -n "$asset_names" ]]; then
      asset_failures+="$asset_catalog"$'\t'"$asset_names"$'\n'
    fi
  done < <(find "$app" -type f -name Assets.car -print0 2>/dev/null)

  if [[ -n "$asset_failures" ]]; then
    fail "$label compiled asset catalogues contain exact disabled financial renditions: $(format_artifact_matches "$app" <<<"$asset_failures")"
  else
    pass "$label compiled asset catalogues omit the reviewed exact disabled financial, third-party, gifting, Stories, and upstream-brand asset names"
  fi
}

resolve_hb_shape() {
  if [[ -n "${KIVO_HB_SHAPE:-}" && -x "$KIVO_HB_SHAPE" ]]; then
    printf '%s\n' "$KIVO_HB_SHAPE"
  elif command -v hb-shape >/dev/null 2>&1; then
    command -v hb-shape
  elif [[ -x /opt/homebrew/bin/hb-shape ]]; then
    printf '%s\n' /opt/homebrew/bin/hb-shape
  elif [[ -x /usr/local/bin/hb-shape ]]; then
    printf '%s\n' /usr/local/bin/hb-shape
  fi
}

resolve_hb_subset() {
  if [[ -n "${KIVO_HB_SUBSET:-}" && -x "$KIVO_HB_SUBSET" ]]; then
    printf '%s\n' "$KIVO_HB_SUBSET"
  elif command -v hb-subset >/dev/null 2>&1; then
    command -v hb-subset
  elif [[ -x /opt/homebrew/bin/hb-subset ]]; then
    printf '%s\n' /opt/homebrew/bin/hb-subset
  elif [[ -x /usr/local/bin/hb-subset ]]; then
    printf '%s\n' /usr/local/bin/hb-subset
  fi
}

verify_signal_symbol_font_artifacts() {
  local app="$1"
  local label="$2"
  local hb_shape
  local hb_shape_version
  local font_count
  local font
  local signal_logo_scalar
  local official_badge_scalar
  local sentinel_scalars
  local signal_logo_shape
  local official_badge_shape
  local sentinel_shape

  hb_shape="$(resolve_hb_shape)"
  if [[ -z "$hb_shape" ]]; then
    fail "$label cannot verify symbol-font trademark glyphs because hb-shape is unavailable"
    return
  fi

  hb_shape_version="$($hb_shape --version | sed -n '1p')"
  if [[ "$hb_shape_version" == "hb-shape (HarfBuzz) $expected_harfbuzz_version" ]]; then
    pass "$label symbol-font verifier uses pinned HarfBuzz $expected_harfbuzz_version"
  else
    fail "$label symbol-font verifier uses '$hb_shape_version' (expected HarfBuzz $expected_harfbuzz_version)"
    return
  fi

  font_count="$(find "$app" -type f -name 'SignalSymbols-*.otf' 2>/dev/null | wc -l | tr -d ' ')"
  if [[ "$font_count" == 3 ]]; then
    pass "$label contains exactly the three reviewed SignalSymbols weights"
  else
    fail "$label contains $font_count SignalSymbols fonts (expected exactly 3)"
  fi

  signal_logo_scalar="$(printf '\356\200\200')"
  official_badge_scalar="$(printf '\356\202\206')"
  sentinel_scalars="$(printf '\356\200\201\356\201\243\356\201\260')"

  while IFS= read -r -d '' font; do
    signal_logo_shape="$($hb_shape "$font" "$signal_logo_scalar")"
    official_badge_shape="$($hb_shape "$font" "$official_badge_scalar")"
    sentinel_shape="$($hb_shape "$font" "$sentinel_scalars")"
    if grep -q '\.notdef=' <<<"$signal_logo_shape" &&
      grep -q '\.notdef=' <<<"$official_badge_shape" &&
      ! grep -q '\.notdef=' <<<"$sentinel_shape"; then
      pass "$label $(basename "$font") omits U+E000/U+E086 and retains generic control glyphs"
    else
      fail "$label $(basename "$font") retains a blocked trademark glyph or lost a generic control glyph"
    fi
  done < <(find "$app" -type f -name 'SignalSymbols-*.otf' -print0 2>/dev/null)
}

scan_release_artifact() {
  local app="$1"
  local label="$2"
  local main_executable="$app/Kivo"
  local main_info_plist="$app/Info.plist"
  local forbidden_path_pattern='(^|/)(LibMobileCoin|MobileCoin)(\.framework|\.bundle|/|$)|(^|/)signal-messenger\.cer$|(^|/)(activate-payments|about-mobilecoin|paymentSpinner)\.json$|Kivo(DEX|AssetLeaderboard|PublicFeed|PublicProfile|MarketTokens|ExploreViewController|EVMChainRegistry|ExternalWallet)'
  # These are product routes/endpoints or exact product-surface assets. Generic
  # transaction/signing words are intentionally absent so WalletCore and
  # backup/protocol schemas cannot make the scan pass or fail by accident.
  local product_hard_content_pattern='https?://(support\.)?signal\.org|https?://signal\.me/|https?://signal\.group/|support@signal\.org|signal private messenger|Kivo(DEX|AssetLeaderboard|PublicFeed|PublicProfile|MarketTokens|ExploreViewController|EVMChainRegistry|ExternalWallet)|KIVO_(DEX|MARKET|SWAP|RWA|ASSET_LEADERBOARD|PUBLIC_FEED|EXTERNAL_WALLET)|tokenized[-_ ]?securit|v1/(archives|donation)/redeem-receipt|PaymentActionSheets|PaymentOnboarding|Donation-Permit|receipt_credentials'
  local financial_endpoint_pattern='web3\.okx\.com|api\.okx\.com|api\.binance\.com'
  local hard_content_pattern="$product_hard_content_pattern|$financial_endpoint_pattern"
  local main_gated_compatibility_pattern='ATTACHMENT_KEYBOARD_PAYMENT|didTap(ActivatePayments|SendPayment)|CONTACT_SUPPORT_FILTER_(PAYMENTS|DONATIONS_AND_BADGES)(_SHORT)?|DONAT(E|ION)_ON_BEHALF_OF_A_FRIEND|SETTINGS_PAYMENTS_(LOCK_SWITCH_LABEL|SECURITY_[A-Z0-9_]+)'
  local signal_ui_engine_pattern='MobileCoinAPI|PaymentsProcessor|PaymentsReconciliation|getMobileCoinAPI|prepareOutgoingPayment|initiateOutgoingPayment|maximumPaymentAmount|updateCurrentPaymentBalanceIfNecessary'
  local signal_ui_stub_pattern='KivoDisabledPaymentReceiptAmount|PAYMENTS_LOCK_(FIRST_TIME|LOCAL_BIOMETRY)'
  local protocol_compatibility_pattern='LegacyPaymentReceiptUnavailableHelper|MobileCoinTxoIdentification|SSKProto(DataMessage|SyncMessage).*(Payment|GiftBadge)|BackupProto_(GiftBadge|PaymentNotification)|ChatItem_(GiftBadge|PaymentNotification)|ArchivedPayment|amountMob|feeMob|donationSubscriberData|payment_notification|gift_badge|DonationPermit(Response|Request)?|transactionDetails'
  local extension_compatibility_pattern='LegacyPaymentReceiptUnavailableHelper|mobileCoinHelper'
  local info_plist_digest_before
  local path_matches
  local content_matches
  local framework
  local framework_name
  local framework_matches=""
  local extension
  local extension_matches=""
  local dylib_matches=""
  local resources_matches
  local private_api_marker
  local private_api_marker_matches=""
  local story_ui_marker
  local story_ui_marker_matches=""
  local -a private_api_binaries=("$main_executable")

  info_plist_digest_before="$(
    find "$app" -type f -name Info.plist -print 2>/dev/null \
      | LC_ALL=C sort \
      | while IFS= read -r artifact_info_plist; do
          shasum -a 256 "$artifact_info_plist"
        done \
      | shasum -a 256 \
      | awk '{print $1}'
  )"

  local invalid_info_plists=""
  local artifact_info_plist
  while IFS= read -r -d '' artifact_info_plist; do
    if ! plutil -lint "$artifact_info_plist" >/dev/null 2>&1; then
      invalid_info_plists+="${artifact_info_plist#$app/}"$'\n'
    fi
  done < <(find "$app" -type f -name Info.plist -print0 2>/dev/null)
  if [[ -n "$invalid_info_plists" ]]; then
    fail "$label contains invalid Info.plist file(s): $(tr '\n' ' ' <<<"$invalid_info_plists")"
  else
    pass "$label contains only valid Info.plist files"
  fi

  if [[ -f "$main_info_plist" ]]; then
    verify_expected_value "$label main bundle identifier" \
      "$(plist_value "$main_info_plist" CFBundleIdentifier)" "com.zhiera.kivo"
    verify_expected_value "$label main version" \
      "$(plist_value "$main_info_plist" CFBundleShortVersionString)" "$release_version"
    verify_expected_value "$label main build" \
      "$(plist_value "$main_info_plist" CFBundleVersion)" "$release_build"

    local artifact_device_family
    artifact_device_family="$(
      plutil -extract UIDeviceFamily json -o - -- "$main_info_plist" 2>/dev/null \
        | tr -d '[:space:]' \
        || true
    )"
    verify_expected_value "$label main device family" "$artifact_device_family" '[1]'
  else
    fail "$label main Info.plist is missing"
  fi

  local expected_extensions=$'SignalNSE.appex\nSignalShareExtension.appex'
  local actual_extensions
  actual_extensions="$(
    find "$app/PlugIns" -mindepth 1 -maxdepth 1 -type d -name '*.appex' \
      -exec basename {} \; 2>/dev/null \
      | LC_ALL=C sort
  )"
  if [[ "$actual_extensions" == "$expected_extensions" ]]; then
    pass "$label contains exactly the reviewed notification and share extensions"
  else
    fail "$label extension set is '$(tr '\n' ' ' <<<"$actual_extensions")' (expected SignalNSE.appex and SignalShareExtension.appex)"
  fi

  local extension_contracts=(
    'SignalNSE.appex:com.zhiera.kivo.notification-service'
    'SignalShareExtension.appex:com.zhiera.kivo.share-extension'
  )
  local extension_contract
  for extension_contract in "${extension_contracts[@]}"; do
    local extension_name="${extension_contract%%:*}"
    local extension_bundle_id="${extension_contract#*:}"
    local extension_info_plist="$app/PlugIns/$extension_name/Info.plist"
    if [[ -f "$extension_info_plist" ]]; then
      verify_expected_value "$label $extension_name bundle identifier" \
        "$(plist_value "$extension_info_plist" CFBundleIdentifier)" "$extension_bundle_id"
      verify_expected_value "$label $extension_name version" \
        "$(plist_value "$extension_info_plist" CFBundleShortVersionString)" "$release_version"
      verify_expected_value "$label $extension_name build" \
        "$(plist_value "$extension_info_plist" CFBundleVersion)" "$release_build"
    else
      fail "$label $extension_name Info.plist is missing"
    fi
  done

  path_matches="$(find "$app" -print 2>/dev/null \
    | sed "s#^$app/##" \
    | rg -i "$forbidden_path_pattern" \
    || true)"
  if [[ -n "$path_matches" ]]; then
    fail "$label contains forbidden financial/upstream paths: $(head -n 12 <<<"$path_matches" | tr '\n' ' ')"
  else
    pass "$label contains no forbidden financial/upstream paths"
  fi

  content_matches="$(rg -a -i -l "$hard_content_pattern" "$main_executable" 2>/dev/null || true)"
  report_layer_scan "$app" "$label main executable" "$content_matches" "$hard_content_pattern"

  if [[ -d "$app/PlugIns" ]]; then
    while IFS= read -r -d '' extension; do
      content_matches="$(rg -a -i -l "$hard_content_pattern" "$extension" 2>/dev/null || true)"
      [[ -z "$content_matches" ]] || extension_matches+="$content_matches"$'\n'

      local extension_executable
      extension_executable="$(plist_value "$extension/Info.plist" CFBundleExecutable)"
      if [[ -f "$extension/$extension_executable" ]]; then
        private_api_binaries+=("$extension/$extension_executable")
      fi
      if [[ "$signal_service_kit_lite_build_condition_is_present" == true &&
        "$payment_receive_gates_are_present" == true &&
        "$gift_receive_gates_are_present" == true ]]; then
        record_reviewed_binary_compatibility \
          "$extension/$extension_executable" \
          "$label $(basename "$extension")" \
          "$extension_compatibility_pattern" \
          "SignalServiceKit linkage"
      elif rg -a -i -q "$extension_compatibility_pattern" \
        "$extension/$extension_executable" 2>/dev/null; then
        fail "$label $(basename "$extension") retains legacy payment linkage without every reviewed Lite protocol gate"
      fi
    done < <(find "$app/PlugIns" -mindepth 1 -maxdepth 1 -type d -name '*.appex' -print0 2>/dev/null)
  fi
  report_layer_scan "$app" "$label app-extension layer" "$extension_matches" "$hard_content_pattern"

  if [[ -d "$app/Frameworks" ]]; then
    while IFS= read -r -d '' framework; do
      framework_name="$(basename "$framework")"
      local framework_hard_pattern="$hard_content_pattern"
      # WalletCore ships generic chain metadata and performs only local
      # address/key/transaction work for Kivo. Provider names and RPC registry
      # URLs inside that exact third-party library are not Kivo product routes.
      if [[ "$framework_name" == WalletCore.framework ||
        "$framework_name" == WalletCoreSwiftProtobuf.framework ]]; then
        framework_hard_pattern="$product_hard_content_pattern"
      fi
      content_matches="$(rg -a -i -l "$framework_hard_pattern" "$framework" 2>/dev/null || true)"
      [[ -z "$content_matches" ]] || framework_matches+="$content_matches"$'\n'

      local framework_executable
      framework_executable="$(plist_value "$framework/Info.plist" CFBundleExecutable)"
      if [[ -f "$framework/$framework_executable" ]]; then
        private_api_binaries+=("$framework/$framework_executable")
      fi
      case "$framework_name" in
        SignalServiceKit.framework)
          if [[ "$signal_service_kit_lite_build_condition_is_present" == true &&
            "$payment_receive_gates_are_present" == true &&
            "$gift_receive_gates_are_present" == true ]]; then
            record_reviewed_binary_compatibility \
              "$framework/$framework_executable" \
              "$label SignalServiceKit.framework" \
              "$protocol_compatibility_pattern" \
              "protocol/database/backup"
          elif rg -a -i -q "$protocol_compatibility_pattern" \
            "$framework/$framework_executable" 2>/dev/null; then
            fail "$label SignalServiceKit.framework retains legacy financial protocol symbols without every reviewed Lite receive gate"
          fi
          ;;
        LibSignalClient.framework)
          record_reviewed_binary_compatibility \
            "$framework/$framework_executable" \
            "$label LibSignalClient.framework" \
            "$protocol_compatibility_pattern" \
            "libsignal backup-schema"
          ;;
        SignalUI.framework)
          if rg -a -i -q "$signal_ui_engine_pattern" "$framework/$framework_executable" 2>/dev/null; then
            fail "$label SignalUI.framework still contains the legacy MobileCoin network/transaction engine"
          else
            pass "$label SignalUI.framework excludes the legacy MobileCoin network/transaction engine"
          fi
          if [[ "$signal_ui_lite_build_condition_is_present" == true &&
            "$signal_ui_payment_engine_is_excluded" == true ]]; then
            record_reviewed_binary_compatibility \
              "$framework/$framework_executable" \
              "$label SignalUI.framework" \
              "$signal_ui_stub_pattern" \
              "compile-time-disabled UI stub"
          elif rg -a -i -q "$signal_ui_stub_pattern" \
            "$framework/$framework_executable" 2>/dev/null; then
            fail "$label SignalUI.framework retains payment UI compatibility symbols without the reviewed Lite compile boundary"
          fi
          ;;
        WalletCore.framework|WalletCoreSwiftProtobuf.framework)
          record_reviewed_binary_compatibility \
            "$framework/$framework_executable" \
            "$label $framework_name" \
            'SigningInput|TransactionCompiler|CoinType|HDWallet|PrivateKey|PublicKey' \
            "generic offline wallet/signing-library"
          ;;
      esac
    done < <(find "$app/Frameworks" -mindepth 1 -maxdepth 1 -type d -name '*.framework' -print0 2>/dev/null)
  fi
  report_layer_scan "$app" "$label framework layer" "$framework_matches" "$hard_content_pattern"

  local dylib
  while IFS= read -r -d '' dylib; do
    private_api_binaries+=("$dylib")
    content_matches="$(rg -a -i -l "$hard_content_pattern" "$dylib" 2>/dev/null || true)"
    [[ -z "$content_matches" ]] || dylib_matches+="$content_matches"$'\n'
  done < <(find "$app" -type f -name '*.dylib' -print0 2>/dev/null)
  report_layer_scan "$app" "$label standalone dylib layer" "$dylib_matches" "$hard_content_pattern"

  resources_matches="$(find "$app" \
    \( -path "$app/Frameworks" -o -path "$app/PlugIns" -o -path '*/_CodeSignature' \) -prune -o \
    -type f ! -path "$main_executable" ! -name '*.dylib' \
    -exec rg -a -i -l "$hard_content_pattern" {} + 2>/dev/null \
    || true)"
  report_layer_scan "$app" "$label first-party resource layer" "$resources_matches" "$hard_content_pattern"

  verify_sanitized_localizations "$app" "$label"
  verify_deferred_product_localizations "$app" "$label"
  verify_sanitized_plural_localizations "$app" "$label"
  verify_disabled_financial_resources "$app" "$label"
  verify_signal_symbol_font_artifacts "$app" "$label"

  # Kivo intentionally omits upstream runtime workarounds that dynamically
  # invoke non-public UIKit/SpringBoard interfaces. Check both the decoded
  # identifiers and their historical obfuscated payloads so a future merge
  # cannot silently restore them to the submitted executable.
  for private_api_marker in \
    'isInterfaceAutorotationDisabled' \
    'UIScrollToDismissSupport' \
    'supportForScreen:' \
    'finishScrollViewTransition' \
    'com.apple.springboard.ringerstate' \
    'setWantsVolumeButtonEvents:' \
    '_UIApplicationVolumeDownButtonDownNotification' \
    '_UIApplicationVolumeDownButtonUpNotification' \
    '_UIApplicationVolumeUpButtonDownNotification' \
    '_UIApplicationVolumeUpButtonUpNotification' \
    'SystemVolumeDidChange' \
    'MPVolumeControllerSystemDataSource' \
    'TextLayoutCanvasView' \
    '_UIConstraintBasedLayoutLogUnsatisfiable' \
    '_UIBarBackground' \
    '_UIVisualEffectSubview' \
    '_UIBadgeView' \
    'com.apple.CloudDocsUI.AddToiCloudDrive' \
    'egVaAAZ2BHdydHZSBwYBBAEGcgZ6AQBVegVyc312dQ==' \
    'ZlpkdAQBfX1lAVV6BX56BQVkBwICAQQG' \
    'BQcCAgEEBlcBBGR0BHZ2AEs=' \
    'd3oAegV5ZHQEAX19Z3p2CWUEcgAFegZ6AQA=' \
    'dAF+P3ICAn12PwUCBHoAeHMBcgR1PwR6AHh2BAUGcgZ2' \
    'BXYGaHIABgVnAX0HfnZTBwYGAQBWCHYABgVL' \
    'ZAsFBnZ+ZwF9B352VXp1VHlyAHh2' \
    'cGZaUgICfXp0cgZ6AQBnAX0HfnZVAQkAUwcGBgEAVQEJAF8BBnp3enRyBnoBAA==' \
    'cGZaUgICfXp0cgZ6AQBnAX0HfnZVAQkAUwcGBgEAZgJfAQZ6d3p0cgZ6AQA=' \
    'cGZaUgICfXp0cgZ6AQBnAX0HfnZmAlMHBgYBAFUBCQBfAQZ6d3p0cgZ6AQA=' \
    'cGZaUgICfXp0cgZ6AQBnAX0HfnZmAlMHBgYBAGYCXwEGend6dHIGegEA'
  do
    local private_api_binary
    for private_api_binary in "${private_api_binaries[@]}"; do
      if rg -a -F -q "$private_api_marker" "$private_api_binary" 2>/dev/null; then
        private_api_marker_matches+="$private_api_marker:${private_api_binary#$app/}"$'\n'
      fi
    done
  done
  if [[ -n "$private_api_marker_matches" ]]; then
    fail "$label executable layer contains non-public API marker(s): $(tr '\n' ' ' <<<"$private_api_marker_matches")"
  else
    pass "$label executable layer omits reviewed non-public API markers"
  fi

  local sdwebimage_spi_matches=""
  for private_spi_string in 'deviceInfoForKey:' 'CGSVGDocument' 'CGPDFPage'; do
    local private_spi_binary
    for private_spi_binary in "${private_api_binaries[@]}"; do
      if strings -a "$private_spi_binary" 2>/dev/null | grep -F -x -q "$private_spi_string"; then
        sdwebimage_spi_matches+="$private_spi_string:${private_spi_binary#$app/}"$'\n'
      fi
    done
  done
  if [[ -n "$sdwebimage_spi_matches" ]]; then
    fail "$label executable layer contains SDWebImage private-selector payload(s): $(tr '\n' ' ' <<<"$sdwebimage_spi_matches")"
  else
    pass "$label executable layer omits reviewed SDWebImage private selectors"
  fi

  local internal_export_marker_matches=""
  for internal_export_marker in \
    'InternalSettingsViewController' \
    'ConversationInternalViewController' \
    'Export Database (internal)' \
    'Delete Session' \
    'Your database password has been copied to the clipboard' \
    'NO ONE AT SIGNAL CAN MAKE YOU DO THIS'
  do
    local internal_export_binary
    for internal_export_binary in "${private_api_binaries[@]}"; do
      if rg -a -F -q "$internal_export_marker" "$internal_export_binary" 2>/dev/null; then
        internal_export_marker_matches+="$internal_export_marker:${internal_export_binary#$app/}"$'\n'
      fi
    done
  done
  if [[ -n "$internal_export_marker_matches" ]]; then
    fail "$label executable layer contains internal database-export/debug UI marker(s): $(tr '\n' ' ' <<<"$internal_export_marker_matches")"
  else
    pass "$label executable layer omits internal database-export/debug UI"
  fi

  local privacy_submission_marker_matches=""
  for privacy_submission_marker in \
    'v1/call_quality_survey' \
    'CallQualitySurveyManager' \
    'SubmitCallQualitySurveyRequest' \
    'SurveyDebugLogViewController' \
    'DebugLogUploader'
  do
    local privacy_submission_binary
    for privacy_submission_binary in "${private_api_binaries[@]}"; do
      if rg -a -F -q "$privacy_submission_marker" "$privacy_submission_binary" 2>/dev/null; then
        privacy_submission_marker_matches+="$privacy_submission_marker:${privacy_submission_binary#$app/}"$'\n'
      fi
    done
  done
  if [[ -n "$privacy_submission_marker_matches" ]]; then
    fail "$label executable layer contains disabled survey/diagnostic submission marker(s): $(tr '\n' ' ' <<<"$privacy_submission_marker_matches")"
  else
    pass "$label executable layer omits call-survey and diagnostic-uploader implementations"
  fi

  # A source filename can survive in simulator debug metadata even when the
  # type itself is removed by conditional compilation. Inspect demangled
  # symbols for the paid subscription fetch/redemption implementations rather
  # than treating a debug-path string as executable behavior.
  local paid_subscription_symbol_matches=""
  local paid_subscription_binary
  for paid_subscription_binary in "${private_api_binaries[@]}"; do
    local demangled_paid_subscription_symbols
    demangled_paid_subscription_symbols="$(
      nm -gjU "$paid_subscription_binary" 2>/dev/null \
        | xcrun swift-demangle 2>/dev/null \
        | rg 'SignalServiceKit\.(SubscriptionFetcher|SubscriptionRedemptionNecessityChecker)' \
        || true
    )"
    if [[ -n "$demangled_paid_subscription_symbols" ]]; then
      paid_subscription_symbol_matches+="${paid_subscription_binary#$app/}:$(head -n 3 <<<"$demangled_paid_subscription_symbols" | tr '\n' ' ')"$'\n'
    fi
  done
  if [[ -n "$paid_subscription_symbol_matches" ]]; then
    fail "$label executable layer contains paid subscription fetch/redemption symbols: $(tr '\n' ' ' <<<"$paid_subscription_symbol_matches")"
  else
    pass "$label executable layer omits paid subscription fetch/redemption symbols"
  fi

  # The free encrypted-backup implementation intentionally fetches the exact
  # shared configuration route below. Every other v1/subscription route is a
  # paid donation/backup surface and must be absent from the submitted binary.
  # Inspect printable strings separately so the allowed route is explicit and
  # a broad regex cannot make the free backup feature fail the release gate.
  local paid_subscription_endpoint_matches=""
  for paid_subscription_binary in "${private_api_binaries[@]}"; do
    local unexpected_subscription_endpoints
    unexpected_subscription_endpoints="$(
      strings -a "$paid_subscription_binary" 2>/dev/null \
        | rg -i 'v1/subscription/' \
        | rg -ivx 'v1/subscription/configuration' \
        || true
    )"
    if [[ -n "$unexpected_subscription_endpoints" ]]; then
      paid_subscription_endpoint_matches+="${paid_subscription_binary#$app/}:$(head -n 6 <<<"$unexpected_subscription_endpoints" | tr '\n' ' ')"$'\n'
    fi
    if strings -a "$paid_subscription_binary" 2>/dev/null \
      | grep -F -x -q 'com.zhiera.kivo.backups.media.disabled'; then
      paid_subscription_endpoint_matches+="${paid_subscription_binary#$app/}:com.zhiera.kivo.backups.media.disabled"$'\n'
    fi
  done
  if [[ -n "$paid_subscription_endpoint_matches" ]]; then
    fail "$label executable layer contains paid subscription endpoint/product marker(s): $(tr '\n' ' ' <<<"$paid_subscription_endpoint_matches")"
  else
    pass "$label executable layer retains only the reviewed free-backup subscription configuration endpoint"
  fi

  # Compatibility models such as StoryMessage and StoryContextViewState remain
  # for database/backup ABI stability. The submitted product must not contain
  # any Stories presentation, creation, or sharing controller.
  for story_ui_marker in \
    'StoriesViewController' \
    'StoryPageViewController' \
    'StorySharing' \
    'NewStoryHeaderView' \
    'MyStorySettingsViewController' \
    'TextStoryComposerView' \
    'LinkPreviewAttachmentViewController' \
    'ComposerTypeSelectionControl'
  do
    local story_ui_binary
    for story_ui_binary in "${private_api_binaries[@]}"; do
      if rg -a -F -q "$story_ui_marker" "$story_ui_binary" 2>/dev/null; then
        story_ui_marker_matches+="$story_ui_marker:${story_ui_binary#$app/}"$'\n'
      fi
    done
  done
  if [[ -n "$story_ui_marker_matches" ]]; then
    fail "$label executable layer contains excluded Stories UI marker(s): $(tr '\n' ' ' <<<"$story_ui_marker_matches")"
  else
    pass "$label executable layer omits Stories presentation and sharing UI"
  fi

  if [[ "$app_store_lite_build_condition_is_present" == true &&
    "$main_financial_sources_are_excluded" == true &&
    "$signal_ui_payment_engine_is_excluded" == true &&
    "$payment_receive_gates_are_present" == true &&
    "$payment_ui_gates_are_present" == true &&
    "$gift_receive_gates_are_present" == true &&
    "$gift_ui_gates_are_present" == true ]]; then
    record_reviewed_binary_compatibility \
      "$main_executable" \
      "$label main executable" \
      "$main_gated_compatibility_pattern" \
      "compile-time/source-gated legacy message-schema"
  elif rg -a -i -q "$main_gated_compatibility_pattern" "$main_executable" 2>/dev/null; then
    fail "$label main executable retains legacy payment UI markers without every reviewed Lite source gate"
  fi

  local info_plist_digest_after
  info_plist_digest_after="$(
    find "$app" -type f -name Info.plist -print 2>/dev/null \
      | LC_ALL=C sort \
      | while IFS= read -r artifact_info_plist; do
          shasum -a 256 "$artifact_info_plist"
        done \
      | shasum -a 256 \
      | awk '{print $1}'
  )"
  if [[ "$info_plist_digest_after" == "$info_plist_digest_before" ]]; then
    pass "$label Info.plist set was not mutated by artifact verification"
  else
    fail "$label Info.plist set changed during artifact verification"
  fi
}

verify_release_app() {
  local app="$1"
  local label="$2"

  if [[ ! -d "$app" ]]; then
    fail "$label does not contain Kivo.app"
    return
  fi

  if codesign --verify --deep --strict "$app" >/dev/null 2>&1; then
    pass "$label passes codesign --verify --deep --strict"
  else
    fail "$label fails codesign --verify --deep --strict"
  fi

  verify_distribution_bundle "$app" "$label main app" \
    com.zhiera.kivo Kivo "Kivo App Store TF" true
  verify_distribution_bundle "$app/PlugIns/SignalNSE.appex" "$label notification extension" \
    com.zhiera.kivo.notification-service SignalNSE "Kivo NSE App Store TF" false
  verify_distribution_bundle "$app/PlugIns/SignalShareExtension.appex" "$label share extension" \
    com.zhiera.kivo.share-extension SignalShareExtension "Kivo Share App Store TF" false

  local extension_count
  extension_count="$(find "$app/PlugIns" -mindepth 1 -maxdepth 1 -type d -name '*.appex' 2>/dev/null \
    | wc -l \
    | tr -d ' ')"
  if [[ "$extension_count" == 2 ]]; then
    pass "$label contains exactly the reviewed NSE and Share extensions"
  else
    fail "$label contains $extension_count app extensions (expected exactly 2)"
  fi

  local expected_archive_commit
  local artifact_commit
  expected_archive_commit="$(git rev-parse --short=12 HEAD) (App Store Build $release_build)"
  artifact_commit="$(plist_value "$app/Info.plist" BuildDetails:KivoCommit)"
  verify_expected_value "$label BuildDetails source mapping" \
    "$artifact_commit" "$expected_archive_commit"

  verify_privacy_manifests "$app" "$label"
  scan_release_artifact "$app" "$label"
}

verify_frozen_source() {
  local source_status
  local release_tag
  source_status="$(git status --porcelain=v1 --untracked-files=all \
    --ignore-submodules=none -- . ':(exclude)Pods')"
  if [[ -z "$source_status" ]]; then
    pass "release source tree is clean apart from verified CocoaPods-generated files"
  else
    fail "release source tree contains uncommitted source changes: $source_status"
  fi

  release_tag="kivo-ios-${release_version}-build${release_build}"
  if [[ "$(git rev-parse -q --verify "refs/tags/$release_tag^{commit}" 2>/dev/null || true)" != "$(git rev-parse HEAD)" ]]; then
    fail "HEAD is not exactly tagged as $release_tag"
    return
  fi
  if [[ "$(git cat-file -t "refs/tags/$release_tag" 2>/dev/null || true)" == tag ]]; then
    pass "HEAD has annotated release tag $release_tag"
  else
    fail "$release_tag is not an annotated release tag"
  fi
}

verify_localization_manifest_contract() {
  local path="$1"
  local expected_count="$2"
  local expected_sha256="$3"
  local label="$4"

  if [[ ! -f "$path" ]]; then
    fail "$label manifest is missing at $path"
    return
  fi

  local line_count
  local unique_count
  local actual_sha256
  line_count="$(wc -l <"$path" | tr -d ' ')"
  unique_count="$(LC_ALL=C sort -u "$path" | wc -l | tr -d ' ')"
  actual_sha256="$(shasum -a 256 "$path" | awk '{print $1}')"

  if [[ "$line_count" == "$expected_count" ]] &&
    [[ "$unique_count" == "$expected_count" ]] &&
    LC_ALL=C sort -c "$path" >/dev/null 2>&1 &&
    [[ "$actual_sha256" == "$expected_sha256" ]]; then
    pass "$label manifest is the reviewed sorted unique $expected_count-key set (sha256=$actual_sha256)"
  else
    fail "$label manifest contract mismatch (lines=$line_count unique=$unique_count sha256=$actual_sha256)"
  fi
}

verify_app_store_resource_sanitizer_phase() {
  local project_file="Signal.xcodeproj/project.pbxproj"
  local signal_target_block
  local signal_app_store_configuration
  local sanitizer_block
  local resource_phase_line
  local sanitizer_phase_line

  signal_target_block="$(sed -n \
    '/D221A088169C9E5E00537ABF \/\* Signal \*\/ = {/,/^\t\t};/p' \
    "$project_file")"
  sanitizer_block="$(sed -n \
    '/A12B12C0FFEE120000000001 \/\* Run Script: sanitize App Store resources \*\/ = {/,/^\t\t};/p' \
    "$project_file")"
  signal_app_store_configuration="$(sed -n \
    '/D221A0BE169C9E5F00537ABF \/\* App Store Release \*\/ = {/,/^\t\t};/p' \
    "$project_file")"
  resource_phase_line="$(grep -n -F 'D221A087169C9E5E00537ABF /* Resources */' \
    <<<"$signal_target_block" | cut -d: -f1 || true)"
  sanitizer_phase_line="$(grep -n -F 'A12B12C0FFEE120000000001 /* Run Script: sanitize App Store resources */' \
    <<<"$signal_target_block" | cut -d: -f1 || true)"

  if [[ -n "$resource_phase_line" && -n "$sanitizer_phase_line" &&
    "$sanitizer_phase_line" -gt "$resource_phase_line" ]]; then
    pass "Signal target runs the App Store resource sanitizer after Copy Bundle Resources"
  else
    fail "Signal target does not run the App Store resource sanitizer after Copy Bundle Resources"
  fi

  if grep -F -q 'name = "Run Script: sanitize App Store resources";' <<<"$sanitizer_block" &&
    grep -F -q 'shellScript = "/usr/bin/ruby \"${PROJECT_DIR}/Scripts/sanitize_app_store_resources.rb\"\n";' \
      <<<"$sanitizer_block"; then
    pass "resource sanitizer build phase invokes the reviewed repository script"
  else
    fail "resource sanitizer build phase is missing or invokes an unreviewed command"
  fi

  if /usr/bin/ruby -c Scripts/sanitize_app_store_resources.rb >/dev/null 2>&1; then
    pass "App Store resource sanitizer has valid Ruby syntax"
  else
    fail "App Store resource sanitizer has invalid Ruby syntax"
  fi

  local expected_legacy_asset_paths=(
    Signal/LegacyPayments.xcassets/Contents.json
    Signal/LegacyPayments.xcassets/mobilecoin-24.imageset/Contents.json
    Signal/LegacyPayments.xcassets/mobilecoin-24.imageset/mobilecoin-24.pdf
    Signal/LegacyPayments.xcassets/payment.imageset/Contents.json
    Signal/LegacyPayments.xcassets/payment.imageset/payment.pdf
    Signal/LegacyPayments.xcassets/payment-28.imageset/Contents.json
    Signal/LegacyPayments.xcassets/payment-28.imageset/payment-28.pdf
    Signal/LegacyPayments.xcassets/payments-lock.imageset/Contents.json
    Signal/LegacyPayments.xcassets/payments-lock.imageset/payments-lock.svg
  )
  local missing_legacy_assets=""
  local legacy_asset_path
  for legacy_asset_path in "${expected_legacy_asset_paths[@]}"; do
    [[ -f "$legacy_asset_path" ]] || missing_legacy_assets+="$legacy_asset_path"$'\n'
  done

  if [[ -z "$missing_legacy_assets" ]] &&
    [[ ! -e Signal/Images.xcassets/mobilecoin-24.imageset ]] &&
    [[ ! -e Signal/Images.xcassets/payments-lock.imageset ]] &&
    [[ ! -e Signal/Symbols.xcassets/payment/payment.imageset ]] &&
    [[ ! -e Signal/Symbols.xcassets/payment/payment-28.imageset ]] &&
    grep -F -q 'LegacyPayments.xcassets in Resources' "$project_file"; then
    pass "legacy payment assets are retained in a dedicated non-Lite compatibility catalogue"
  else
    fail "legacy payment assets are missing, duplicated in shared catalogues, or not in the resource phase: $(tr '\n' ' ' <<<"$missing_legacy_assets")"
  fi

  if grep -F -q 'LegacyPayments.xcassets,' <<<"$signal_app_store_configuration" &&
    grep -E -q '"?add-money\.json"?,' <<<"$signal_app_store_configuration" &&
    grep -E -q '"?cash-out\.json"?,' <<<"$signal_app_store_configuration"; then
    pass "App Store Release excludes the dedicated payment catalogue and payment animations without breaking other configurations"
  else
    fail "App Store Release does not exclude every reviewed legacy payment resource"
  fi

  if grep -F -q 'case settingsWallet' SignalUI/Appearance/Theme+Icons.swift &&
    grep -F -A2 -q 'case .settingsWallet:' SignalUI/Appearance/Theme+Icons.swift &&
    ! grep -F -A2 'case .settingsWallet:' SignalUI/Appearance/Theme+Icons.swift | grep -F -q 'return "payment"' &&
    grep -F -q 'icon: .settingsWallet' Signal/src/ViewControllers/AppSettings/AppSettingsViewController.swift &&
    [[ "$(grep -F -c 'icon: .settingsWallet' Signal/Kivo/KivoWalletSettingsViewController.swift)" -eq 2 ]]; then
    pass "App Store Lite wallet surfaces use a non-legacy key icon retained in the shared asset catalogue"
  else
    fail "App Store Lite wallet surfaces still depend on the excluded legacy payment icon"
  fi

  if grep -F -A70 '34A954C4271A471300B05242 /* App Store Release */' "$project_file" | grep -F -q 'TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Kivo.app/Kivo";' &&
    grep -F -A70 'D221A0C1169C9E5F00537ABF /* App Store Release */' "$project_file" | grep -F -q 'BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Kivo.app/Kivo";' &&
    grep -F -A70 'F9C5C8B1289451B900548EEE /* App Store Release */' "$project_file" | grep -F -q 'TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Kivo.app/Kivo";'; then
    pass "App Store Release test bundles target the Kivo executable"
  else
    fail "one or more App Store Release test bundles still target the obsolete Signal executable"
  fi

  lite_test_mobilecoin_block="$(sed -n '/^#if KIVO_APP_STORE_LITE$/,/^#endif$/p' \
    SignalServiceKit/TestUtils/MockSSKEnvironment.swift)"

  if [[ -x Scripts/test_kivo_app_store_lite_backup_settings ]] &&
    [[ -f Signal.xcodeproj/xcshareddata/xcschemes/Kivo-Backup-Tests.xcscheme ]] &&
    grep -F -q 'buildConfiguration = "Testable Release"' \
      Signal.xcodeproj/xcshareddata/xcschemes/Kivo-Backup-Tests.xcscheme &&
    grep -F -q -- '-configuration "App Store Release"' \
      Scripts/test_kivo_app_store_lite_backup_settings &&
    grep -F -q 'ENABLE_TESTABILITY=YES' \
      Scripts/test_kivo_app_store_lite_backup_settings &&
    grep -F -q 'EXCLUDED_SOURCE_FILE_NAMES=$(inherited) DonationReceiptCredentialRedemptionJobFinderTest.swift OWSRequestFactoryTest.swift PendingMonthlyIDEALDonationCodableTest.swift StripeTest.swift' \
      Scripts/test_kivo_app_store_lite_backup_settings &&
    grep -F -q 'OTHER_SWIFT_FLAGS=$(inherited) -DTESTABLE_BUILD' \
      Scripts/test_kivo_app_store_lite_backup_settings &&
    grep -F -q 'GCC_PREPROCESSOR_DEFINITIONS=$(inherited) TESTABLE_BUILD=1' \
      Scripts/test_kivo_app_store_lite_backup_settings &&
    grep -F -q 'only-testing:SignalServiceKitTests/BackupSettingsStoreTests' \
      Scripts/test_kivo_app_store_lite_backup_settings &&
    grep -F -q 'mobileCoinHelper = LegacyPaymentReceiptUnavailableHelper()' \
      <<<"$lite_test_mobilecoin_block" &&
    grep -F -q 'mobileCoinHelper = MobileCoinHelperMock()' \
      <<<"$lite_test_mobilecoin_block" &&
    grep -F -q 'SetCurrentAppContext(TestAppContext(), isRunningTests: true)' \
      SignalServiceKit/tests/MessageBackup/BackupSettingsStoreTests.swift; then
    pass "Lite backup tests use an invocation-only testability override without weakening the shipping configuration"
  else
    fail "Lite backup test harness is missing or can pollute the shipping App Store configuration"
  fi

  verify_localization_manifest_contract \
    "Scripts/app_store_lite_excluded_financial_localization_keys.txt" \
    509 \
    "127402bda7956488c6823dbcffa1199f3a0628f8a382f3a3bea50d8b73d279ab" \
    "financial localization"
  verify_localization_manifest_contract \
    "Scripts/app_store_lite_excluded_paid_backup_localization_keys.txt" \
    91 \
    "19c7c3b5950a29e4aac256a0e2288aa5402d5ad01b2e267e107d16722d57f562" \
    "paid-backup localization"
  verify_localization_manifest_contract \
    "Scripts/app_store_lite_excluded_paid_backup_plural_keys.txt" \
    1 \
    "9f96a9b97d6cf582972977d0687d7e0ca2b7b41d01086a87fb6d28c84c015b74" \
    "paid-backup plural localization"
  verify_localization_manifest_contract \
    "Scripts/app_store_lite_excluded_deferred_localization_keys.txt" \
    1 \
    "e1d3333f3be4e9a08514bc42de19bb83f9acf19a2c161ee74a3d73612dde8e21" \
    "deferred-product localization"

  if /usr/bin/ruby -e '
    source = File.read(ARGV.fetch(0))
    manifests = {
      "app_store_lite_excluded_financial_localization_keys.txt" => 509,
      "app_store_lite_excluded_paid_backup_localization_keys.txt" => 91,
      "app_store_lite_excluded_paid_backup_plural_keys.txt" => 1,
      "app_store_lite_excluded_deferred_localization_keys.txt" => 1,
    }
    manifests.each do |filename, expected_count|
      pattern = /filename:\s*"#{Regexp.escape(filename)}",\s*expected_count:\s*#{expected_count},/m
      abort "missing manifest contract for #{filename}" unless source.match?(pattern)
    end
    required_contracts = [
      /excluded_keys\s*=\s*financial_keys\s*\+\s*paid_backup_keys\s*\+\s*deferred_product_keys/,
      /App Store Lite Localizable\.strings manifests overlap/,
      /Dir\.glob\(File\.join\(resources_path, "\*\.lproj", "Localizable\.strings"\)\)\.sort/,
      /Dir\.glob\(File\.join\(resources_path, "\*\.lproj", "PluralAware\.stringsdict"\)\)\.sort/,
      /Expected paid-backup plural keys missing before sanitizing/,
    ]
    abort "sanitizer does not apply every reviewed manifest" unless required_contracts.all? { |pattern| source.match?(pattern) }
  ' Scripts/sanitize_app_store_resources.rb; then
    pass "resource sanitizer applies all four reviewed manifests to Localizable and PluralAware catalogues"
  else
    fail "resource sanitizer does not reference every reviewed manifest/count or both localization catalogue types"
  fi
}

verify_signal_symbol_brand_boundary() {
  local project_file="Signal.xcodeproj/project.pbxproj"
  local signal_ui_target_block
  local signal_ui_app_store_configuration
  local sanitizer_block
  local resource_phase_line
  local sanitizer_phase_line
  local hb_subset
  local hb_subset_version

  signal_ui_target_block="$(sed -n \
    '/34A954A7271A471200B05242 \/\* SignalUI \*\/ = {/,/^\t\t};/p' \
    "$project_file")"
  signal_ui_app_store_configuration="$(sed -n \
    '/34A954C0271A471300B05242 \/\* App Store Release \*\/ = {/,/^\t\t};/p' \
    "$project_file")"
  sanitizer_block="$(sed -n \
    '/A12B12C0FFEE140000000001 \/\* Run Script: sanitize Kivo symbol fonts \*\/ = {/,/^\t\t};/p' \
    "$project_file")"
  resource_phase_line="$(grep -n -F '34A954A6271A471200B05242 /* Resources */' \
    <<<"$signal_ui_target_block" | cut -d: -f1 || true)"
  sanitizer_phase_line="$(grep -n -F 'A12B12C0FFEE140000000001 /* Run Script: sanitize Kivo symbol fonts */' \
    <<<"$signal_ui_target_block" | cut -d: -f1 || true)"

  if [[ -n "$resource_phase_line" && -n "$sanitizer_phase_line" &&
    "$sanitizer_phase_line" -gt "$resource_phase_line" ]]; then
    pass "SignalUI subsets App Store symbol fonts after Copy Bundle Resources"
  else
    fail "SignalUI does not subset App Store symbol fonts after Copy Bundle Resources"
  fi

  if grep -F -q 'name = "Run Script: sanitize Kivo symbol fonts";' <<<"$sanitizer_block" &&
    grep -F -q 'shellScript = "/bin/sh \"${PROJECT_DIR}/Scripts/sanitize_kivo_signal_symbol_fonts\"\n";' \
      <<<"$sanitizer_block"; then
    pass "SignalUI symbol-font phase invokes the reviewed repository script"
  else
    fail "SignalUI symbol-font phase is missing or invokes an unreviewed command"
  fi

  if grep -F -q 'ENABLE_USER_SCRIPT_SANDBOXING = NO;' <<<"$signal_ui_app_store_configuration"; then
    pass "SignalUI App Store configuration permits the reviewed product-resource rewrite"
  else
    fail "SignalUI App Store configuration would sandbox the reviewed product-resource rewrite"
  fi

  if sh -n Scripts/sanitize_kivo_signal_symbol_fonts &&
    grep -F -q "expected_harfbuzz_version=\"hb-subset (HarfBuzz) $expected_harfbuzz_version\"" \
      Scripts/sanitize_kivo_signal_symbol_fonts &&
    grep -F -q "'--unicodes-=E000,E086'" Scripts/sanitize_kivo_signal_symbol_fonts &&
    grep -F -q 'cmp -s "$subset_path" "$repeat_path"' Scripts/sanitize_kivo_signal_symbol_fonts; then
    pass "symbol-font sanitizer has valid syntax, pinned tooling, blocked codepoints, and a determinism check"
  else
    fail "symbol-font sanitizer contract is incomplete"
  fi

  hb_subset="$(resolve_hb_subset)"
  if [[ -z "$hb_subset" ]]; then
    fail "pinned hb-subset is unavailable for the App Store symbol-font build phase"
  else
    hb_subset_version="$($hb_subset --version | sed -n '1p')"
    if [[ "$hb_subset_version" == "hb-subset (HarfBuzz) $expected_harfbuzz_version" ]]; then
      pass "App Store symbol-font build uses pinned HarfBuzz $expected_harfbuzz_version"
    else
      fail "App Store symbol-font build uses '$hb_subset_version' (expected HarfBuzz $expected_harfbuzz_version)"
    fi
  fi

  if /usr/bin/ruby -e '
    symbols = File.read("SignalUI/Appearance/SignalSymbols.swift")
    details = File.read("Signal/ConversationView/Components/CVComponentThreadDetails.swift")
    abort "official badge enum remains available to Lite" unless symbols.match?(
      /#if !KIVO_APP_STORE_LITE\s+case officialBadge = "\\u\{E086\}"\s+#endif/m,
    )
    abort "Signal logo enum remains available to Lite" unless symbols.match?(
      /#if !KIVO_APP_STORE_LITE\s+case signal = "\\u\{E000\}"\s+#endif/m,
    )
    abort "Lite official label does not use the neutral SF Symbol" unless details.match?(
      /#if KIVO_APP_STORE_LITE.*?UIImage\(systemName: "checkmark\.seal\.fill"\).*?#else\s+let symbol = SignalSymbol\.officialBadge/sm,
    )
  '; then
    pass "App Store Lite has no code path to the removed logo/official glyphs"
  else
    fail "App Store Lite still exposes a logo/official glyph code path"
  fi
}

verify_callkit_brand_boundary() {
  if /usr/bin/ruby -e '
    source = File.read("Signal/Calls/CallKitCallManager.swift")
    required = [
      %q{static let kAnonymousCallHandlePrefix = "Kivo:"},
      %q{static let kGroupThreadCallHandlePrefix = "KivoGroup:"},
      %q{static let kCallLinkCallHandlePrefix = "KivoCall:"},
      %q{private static let legacyAnonymousCallHandlePrefix = "Signal:"},
      %q{private static let legacyGroupThreadCallHandlePrefix = "SignalGroup:"},
      %q{private static let legacyCallLinkCallHandlePrefix = "SignalCall:"},
    ]
    abort "missing current or legacy CallKit prefix" unless required.all? { |value| source.include?(value) }
    abort "legacy prefix literal duplicated" unless source.scan(/"Signal:"/).length == 1
    abort "legacy group prefix literal duplicated" unless source.scan(/"SignalGroup:"/).length == 1
    abort "legacy call-link prefix literal duplicated" unless source.scan(/"SignalCall:"/).length == 1

    writer = source[/func createCallHandleWithSneakyTransaction.*?(?=\n    static func callTargetForHandleWithSneakyTransaction)/m]
    abort "missing CallKit handle writer" unless writer
    abort "writer accesses a legacy prefix" if writer.include?("legacy") || writer.match?(/Signal(?:Group|Call)?:/)
    abort "writer omits a Kivo prefix" unless %w[
      kAnonymousCallHandlePrefix
      kGroupThreadCallHandlePrefix
      kCallLinkCallHandlePrefix
    ].all? { |name| writer.include?(name) }

    abort "legacy anonymous handles are not readable" unless source.include?("legacyAnonymousCallHandlePrefix,")
    abort "legacy group handles are not readable" unless source.include?("legacyGroupThreadCallHandlePrefix,")
    abort "legacy call-link handles are not readable" unless source.include?("legacyCallLinkCallHandlePrefix,")
    abort "group compatibility decoder is bypassed" unless source.include?(
      "suffix(in: handle, matching: groupThreadCallHandlePrefixes)",
    )
  ' ; then
    pass "CallKit writes only Kivo-prefixed handles and retains legacy Signal prefixes for read compatibility"
  else
    fail "CallKit handle branding or legacy-read boundary is incomplete"
  fi
}

verify_paid_backup_lite_source_boundary() {
  local paid_backup_keys_manifest="Scripts/app_store_lite_excluded_paid_backup_localization_keys.txt"
  local lite_source
  local paid_key_matches
  local paid_runtime_matches
  local required_pattern
  local missing_free_source=""
  local source_files=(
    Signal/Backups/BackupEnablingManager.swift
    Signal/Backups/BackupPlanOptionView.swift
    Signal/Backups/BackupPlanTermsAndConditionsView.swift
    Signal/Backups/BackupPlanUpsellConfiguration.swift
    Signal/Backups/BackupPlanUpsellViewController.swift
    Signal/Backups/BackupSettingsViewController.swift
    Signal/Backups/BackupSubscriptionAlreadyRedeemedSheet.swift
    Signal/Backups/ChooseBackupPlanViewController.swift
    Signal/Backups/LinkedDeviceBackupSettingsViewController.swift
    Signal/src/ViewControllers/AppSettings/AppSettingsViewController.swift
    SignalServiceKit/Network/API/Requests/OWSRequestFactory.swift
    SignalServiceKit/Network/API/Requests/OWSRequestFactory+Backups.swift
    SignalServiceKit/Backups/Settings/BackupPlanManager.swift
    SignalServiceKit/Backups/Settings/BackupSettingsStore.swift
    SignalServiceKit/Environment/AppSetup.swift
    SignalServiceKit/Subscriptions/Backups/BackupSubscriptionRedeemer.swift
    SignalServiceKit/Subscriptions/SubscriptionFetcher.swift
  )

  # clang's preprocessing pass evaluates these Swift #if blocks without
  # compiling dependencies. Scan the exact code that remains when the Lite
  # condition is true, rather than treating protocol/state compatibility names
  # such as BackupPlan.paid as reachable paid UI.
  if lite_source="$({
    local source_file
    for source_file in "${source_files[@]}"; do
      xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE -D'targetEnvironment(x)=0' "$source_file" || exit 1
    done
  } 2>/dev/null)"; then
    pass "paid-backup sources preprocess successfully for KIVO_APP_STORE_LITE"
  else
    fail "paid-backup sources could not be preprocessed for the Lite boundary"
    return
  fi

  paid_key_matches="$(rg -n -F -f "$paid_backup_keys_manifest" <<<"$lite_source" || true)"
  if [[ -n "$paid_key_matches" ]]; then
    fail "Lite-active backup source still references paid-only localization keys: $(head -n 12 <<<"$paid_key_matches" | tr '\n' ' ')"
  else
    pass "Lite-active backup source references none of the 91 paid-only localization keys"
  fi

  local paid_runtime_pattern='import StoreKit|struct[[:space:]]+BackupPlanPaidOptionView|struct[[:space:]]+BackupPlanTermsAndConditionsView|class[[:space:]]+BackupPlanUpsellViewController|struct[[:space:]]+BackupPlanUpsellView|class[[:space:]]+BackupSubscriptionAlreadyRedeemedSheet|struct[[:space:]]+BackupSubscriptionLoadedView|struct[[:space:]]+PaidSubscription|func[[:space:]]+addPaidHeroContent|BackupPlanPaidOptionView[[:space:]]*\(|BackupPlanTermsAndConditionsView[[:space:]]*\(|(^|[^[:alnum:]_])BackupPlanUpsellViewController[[:space:]]*(\.|\()|(^|[^[:alnum:]_])BackupSubscriptionAlreadyRedeemedSheet[[:space:]]*\(|purchaseNewSubscription[[:space:]]*\(|redeemSubscriptionIfNecessary[[:space:]]*\(|subscriptionDisplayPrice[[:space:]]*\(|SubscriptionFetcher[[:space:]]*\(|AppStore[[:space:]]*\.[[:space:]]*showManageSubscriptions|BackupSubscriptionRedeemer|ReceiptCredentialManager|archives/redeem-receipt|subscriptionReceiptCredentialsRequest|setSubscriberID[[:space:]]*\(|Donation-Permit|backupRedeemReceiptCredential'
  paid_runtime_matches="$(rg -n "$paid_runtime_pattern" <<<"$lite_source" || true)"
  if [[ -n "$paid_runtime_matches" ]]; then
    fail "Lite-active backup source still contains paid StoreKit/subscription UI or network entrypoints: $(head -n 12 <<<"$paid_runtime_matches" | tr '\n' ' ')"
  else
    pass "Lite-active backup source excludes paid StoreKit/subscription UI and network entrypoints"
  fi

  for required_pattern in \
    'struct BackupPlanFreeOptionView' \
    'await setBackupPlan { _ in .free }' \
    'viewModel.performManualBackup()' \
    'backupsViewController = BackupOnboardingCoordinator(' \
    'backupType: .remote,'
  do
    if ! grep -F -q "$required_pattern" <<<"$lite_source"; then
      missing_free_source+="$required_pattern"$'\n'
    fi
  done
  if [[ -n "$missing_free_source" ]]; then
    fail "Lite source lost reviewed free-backup entrypoint(s): $(tr '\n' ' ' <<<"$missing_free_source")"
  else
    pass "Lite source retains free-plan selection, manual backup, and the Settings backup entrypoint"
  fi

  if rg -q 'case remote' Signal/Registration/UserInterface/RegistrationChooseRestoreMethodViewController.swift &&
    rg -q 'presenter\?\.didChooseRestoreMethod\(method: \.remote\)' \
      Signal/Registration/UserInterface/RegistrationChooseRestoreMethodViewController.swift &&
    rg -q 'RegistrationChooseRestoreMethodViewController\.swift in Sources' \
      Signal.xcodeproj/project.pbxproj; then
    pass "free remote-backup restore remains reachable during registration"
  else
    fail "free remote-backup restore source or App Store target membership is missing"
  fi

  if grep -F -q 'var appStoreLiteCompatiblePlan: BackupPlan {' \
    SignalServiceKit/Backups/Settings/BackupSettingsStore.swift &&
    grep -F -q 'case .paid, .paidExpiringSoon, .paidAsTester:' \
      SignalServiceKit/Backups/Settings/BackupSettingsStore.swift &&
    grep -F -q 'return persistedBackupPlan(tx: tx).appStoreLiteCompatiblePlan' \
      SignalServiceKit/Backups/Settings/BackupSettingsStore.swift &&
    grep -F -q 'let newBackupPlan = newBackupPlan.appStoreLiteCompatiblePlan' \
      SignalServiceKit/Backups/Settings/BackupSettingsStore.swift &&
    grep -F -q 'func migrateAppStoreLiteBackupPlanIfNeeded' \
      SignalServiceKit/Backups/Settings/BackupPlanManager.swift &&
    grep -F -q 'oldBackupPlan: persistedBackupPlan' \
      SignalServiceKit/Backups/Settings/BackupPlanManager.swift &&
    grep -F -q 'backupPlanManager.migrateAppStoreLiteBackupPlanIfNeeded' \
      SignalServiceKit/Environment/AppSetup.swift; then
    pass "Lite backup plans fail closed to free and legacy paid/offload state is migrated at launch"
  else
    fail "Lite backup plan normalization or launch migration is incomplete"
  fi

  if grep -F -q 'let backupPlan = restoredBackupPlan.appStoreLiteCompatiblePlan' \
      SignalServiceKit/Backups/Archiving/Archivers/AccountData/BackupArchiveAccountDataArchiver.swift &&
    grep -F -q 'context.backupPlan = backupPlan' \
      SignalServiceKit/Backups/Archiving/Archivers/AccountData/BackupArchiveAccountDataArchiver.swift; then
    pass "Lite backup restore uses one normalized plan for persistence and attachment eligibility"
  else
    fail "Lite backup restore can diverge between its persisted plan and attachment eligibility context"
  fi

  local lite_paid_identity_source
  local paid_identity_files=(
    SignalServiceKit/Backups/Archiving/Archivers/AccountData/BackupArchiveAccountDataArchiver.swift
    SignalServiceKit/StorageService/StorageServiceProto+Sync.swift
    Signal/AppLaunch/AppEnvironment.swift
    SignalServiceKit/ZeroKnowledge/BackupAuthCredentialManager.swift
  )
  if lite_paid_identity_source="$({
    local source_file
    for source_file in "${paid_identity_files[@]}"; do
      xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE "$source_file" || exit 1
    done
  } 2>/dev/null)"; then
    local paid_identity_matches
    paid_identity_matches="$(rg -n \
      'backupSubscriptionManager\.(getIAPSubscriberData|restoreIAPSubscriberData|redeemSubscriptionIfNecessary)|waitForAuthCredentialDependency\(\.(renewBackupEntitlementForTestFlight|redeemBackupSubscriptionViaIAP)' \
      <<<"$lite_paid_identity_source" || true)"
    if [[ -n "$paid_identity_matches" ]]; then
      fail "Lite-active backup paths still archive, restore, sync, or redeem paid subscriber identity: $(head -n 12 <<<"$paid_identity_matches" | tr '\n' ' ')"
    else
      pass "Lite-active backup paths exclude paid subscriber identity and entitlement redemption"
    fi
  else
    fail "paid-backup identity paths could not be preprocessed for the Lite boundary"
  fi

  local lite_subscription_manager_source
  if lite_subscription_manager_source="$(
    xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE \
      SignalServiceKit/Subscriptions/Backups/BackupSubscriptionManager.swift 2>/dev/null
  )" &&
    grep -F -q 'func getIAPSubscriberData(tx: DBReadTransaction) -> IAPSubscriberData? { nil }' \
      <<<"$lite_subscription_manager_source" &&
    grep -F -q 'func restoreIAPSubscriberData(_ iapSubscriberData: IAPSubscriberData, tx: DBWriteTransaction) {}' \
      <<<"$lite_subscription_manager_source" &&
    ! grep -F -q 'KeyValueStore(collection: "BackupSubscriptionManager")' \
      <<<"$lite_subscription_manager_source"; then
    pass "Lite subscription compatibility manager ignores legacy and restored IAP identity"
  else
    fail "Lite subscription compatibility manager can still read or persist IAP identity"
  fi
}

verify_internal_tools_lite_source_boundary() {
  local lite_source
  local source_files=(
    Signal/AppLaunch/AppDelegate.swift
    Signal/AppLaunch/SignalApp.swift
    Signal/Registration/UserInterface/RegistrationNavigationController.swift
    Signal/Provisioning/UserInterface/ProvisioningController.swift
    Signal/src/ViewControllers/AppSettings/AppSettingsViewController.swift
    Signal/src/ViewControllers/DatabaseRecoveryViewController.swift
    Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift
    Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift
  )

  if lite_source="$({
    local source_file
    for source_file in "${source_files[@]}"; do
      xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE -D'targetEnvironment(x)=0' "$source_file" || exit 1
    done
  } 2>/dev/null)"; then
    pass "internal-tool entrypoint sources preprocess successfully for KIVO_APP_STORE_LITE"
  else
    fail "internal-tool entrypoint sources could not be preprocessed for the Lite boundary"
    return
  fi

  local internal_pattern='InternalSettingsViewController|ConversationInternalViewController|Export Database \(internal\)|debugOnly_keyData|UIPasteboard\.general\.string[[:space:]]*=[[:space:]]*password|Your database password has been copied|numberOfTapsRequired[[:space:]]*=[[:space:]]*8|setUpDebugLogsGesture'
  local internal_matches
  internal_matches="$(rg -n "$internal_pattern" <<<"$lite_source" || true)"
  if [[ -n "$internal_matches" ]]; then
    fail "Lite-active source retains internal settings or database-export code: $(head -n 12 <<<"$internal_matches" | tr '\n' ' ')"
  else
    pass "Lite-active source excludes internal settings and database-export code"
  fi

  if /usr/bin/ruby -e '
    source = File.read(ARGV.fetch(0))
    app_store = source[/D221A0BE169C9E5F00537ABF \/\* App Store Release \*\/ = \{.*?\n\t\t\};/m]
    abort "missing main App Store Release configuration" unless app_store
    %w[ConversationInternalViewController.swift FlagsViewController.swift Internal*.swift TestingViewController.swift].each do |entry|
      abort "missing App Store exclusion: #{entry}" unless app_store.include?(entry)
    end
  ' Signal.xcodeproj/project.pbxproj; then
    pass "main App Store Release excludes all internal settings controllers"
  else
    fail "main App Store Release does not exclude every internal settings controller"
  fi
}

verify_remote_content_lite_source_boundary() {
  local lite_source
  local source_files=(
    Signal/AppLaunch/AppDelegate.swift
    Signal/Megaphones/RemoteAnnouncementFetcher.swift
    Signal/Megaphones/RemoteMegaphoneFetcher.swift
    Signal/Megaphones/RemoteReleaseNotesFetcher.swift
    Signal/Megaphones/RemoteReleaseNotesFetchingManager.swift
    SignalServiceKit/Messages/Stories/SystemStoryManager.swift
  )

  if lite_source="$({
    local source_file
    for source_file in "${source_files[@]}"; do
      xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE -D'targetEnvironment(x)=0' "$source_file" || exit 1
    done
  } 2>/dev/null)"; then
    pass "remote-content entrypoint sources preprocess successfully for KIVO_APP_STORE_LITE"
  else
    fail "remote-content entrypoint sources could not be preprocessed for the Lite boundary"
    return
  fi

  local remote_content_pattern='RemoteReleaseNotesFetchingManager|syncRemoteReleaseNotes[[:space:]]*\(|urlSessionForUpdates2[[:space:]]*\(|fetch(Megaphone|Announcement)Translation[[:space:]]*\(|static/release-notes|dynamic/release-notes'
  local remote_content_matches
  remote_content_matches="$(rg -n "$remote_content_pattern" <<<"$lite_source" || true)"
  if [[ -n "$remote_content_matches" ]]; then
    fail "Lite-active source retains remote announcement or system-story fetch paths: $(head -n 12 <<<"$remote_content_matches" | tr '\n' ' ')"
  else
    pass "Lite-active source excludes remote announcements and system-story downloads"
  fi

  if /usr/bin/ruby -e '
    app = File.read("Signal/AppLaunch/AppDelegate.swift")
    story = File.read("SignalServiceKit/Messages/Stories/SystemStoryManager.swift")
    abort "remote release notes scheduler is not compile-disabled" unless app.match?(/#if !KIVO_APP_STORE_LITE\s+let remoteReleaseNotesFetchingManager.*?syncRemoteReleaseNotes\(\).*?#endif/m)
    abort "system-story startup is not compile-disabled" unless story.match?(/#if !KIVO_APP_STORE_LITE\s+if CurrentAppContext\(\)\.isMainApp.*?enqueueOnboardingStoryDownload\(\).*?#endif/m)
    abort "system-story public enqueue is not a Lite no-op" unless story.match?(/enqueueOnboardingStoryDownload\(\).*?#if KIVO_APP_STORE_LITE\s+.*?return Task \{\}/m)
    abort "system-story network method does not fail closed in Lite" unless story.match?(/downloadOnboardingStoryIfUndownloaded\(\).*?#if KIVO_APP_STORE_LITE\s+throw OWSAssertionError\("Stories are disabled in Kivo App Store Lite"\)/m)
  '; then
    pass "remote announcements and system-story downloads have explicit Lite compile-time gates"
  else
    fail "remote announcements or system-story downloads are missing an explicit Lite compile-time gate"
  fi
}

verify_default_sticker_lite_source_boundary() {
  local source_file="SignalServiceKit/Messages/Stickers/DefaultStickers.swift"
  local lite_source

  if lite_source="$(xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE "$source_file" 2>/dev/null)"; then
    pass "default-sticker source preprocesses successfully for KIVO_APP_STORE_LITE"
  else
    fail "default-sticker source could not be preprocessed for the Lite boundary"
    return
  fi

  if /usr/bin/ruby -e '
    source = STDIN.read
    abort unless source.match?(/private static let allPacks: \[DefaultStickerPack\] = \{\s+return \[\]\s+\}\(\)/m)
    abort if source.include?("10b8a4a9b6a82316959be1d54ffcb800")
    abort if source.include?("3e129d6bf66a19bc98476a46d80ed48e41d9ec93358fe9221dda11a1b6d0f7e4")
  ' <<<"$lite_source"; then
    pass "App Store Lite contains no remotely hosted default sticker pack"
  else
    fail "App Store Lite still contains a remote default-sticker definition"
  fi

  if /usr/bin/ruby -e '
    source = File.read(ARGV.fetch(0))
    pack_id = "10b8a4a9b6a82316959be1d54ffcb800"
    abort "non-Lite Kivo Moods pack definition was removed" unless source.include?(pack_id)
    abort "non-Lite auto-install setting changed" unless source.include?("shouldAutoInstall: true")
    abort "Lite pack-list compile gate is incomplete" unless source.match?(
      /private static let allPacks:.*?#if KIVO_APP_STORE_LITE\s+.*?return \[\]\s+#else\s+.*?#{pack_id}.*?#endif/m,
    )
  ' "$source_file"; then
    pass "default-sticker Lite gate removes Kivo Moods without changing non-Lite behavior"
  else
    fail "default-sticker Lite exclusion or non-Lite behavior is incomplete"
  fi
}

verify_call_quality_lite_source_boundary() {
  local lite_source
  if lite_source="$({
    xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE -D'targetEnvironment(x)=0' \
      Signal/Calls/GroupCall.swift || exit 1
    xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE -D'targetEnvironment(x)=0' \
      Signal/Calls/IndividualCallService.swift || exit 1
  } 2>/dev/null)"; then
    pass "call-completion entrypoints preprocess successfully for KIVO_APP_STORE_LITE"
  else
    fail "call-completion entrypoints could not be preprocessed for the Lite boundary"
    return
  fi

  local survey_matches
  survey_matches="$(rg -n 'CallQualitySurvey|call_quality_survey|presentCallQualitySurvey' <<<"$lite_source" || true)"
  if [[ -z "$survey_matches" ]]; then
    pass "Lite-active call completion excludes the call-quality survey and diagnostic submission flow"
  else
    fail "Lite-active call completion retains call-quality survey code: $(head -n 12 <<<"$survey_matches" | tr '\n' ' ')"
  fi

  if grep -F -q 'CallQualitySurvey*.swift' <<<"$main_exclusions_line" &&
    grep -F -q 'CallQualitySurvey.pb.swift' <<<"$signal_service_kit_exclusions_line"; then
    pass "App Store targets exclude call-quality survey UI, request, and protobuf sources"
  else
    fail "App Store targets do not exclude every call-quality survey source family"
  fi
}

verify_badge_lite_source_boundary() {
  local lite_source
  if lite_source="$({
    xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE -D'targetEnvironment(x)=0' \
      Signal/ConversationView/Loading/CVAvatarBuilder.swift || exit 1
    xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE -D'targetEnvironment(x)=0' \
      Signal/src/ViewControllers/AppSettings/Profile/ProfileSettingsViewController.swift || exit 1
  } 2>/dev/null)"; then
    pass "avatar and profile sources preprocess successfully for KIVO_APP_STORE_LITE"
  else
    fail "avatar and profile sources could not be preprocessed for the Lite boundary"
    return
  fi

  local badge_runtime_matches
  badge_runtime_matches="$(rg -n 'primaryBadge|fetchBadgeContent|fetchImageFromBadgeAssets|DonationSubscriptionManager|badgeStore' <<<"$lite_source" || true)"
  if [[ -z "$badge_runtime_matches" ]] &&
    grep -F -q 'badgeImage = nil' <<<"$lite_source" &&
    grep -F -q 'let badgeImage: UIImage? = nil' <<<"$lite_source" &&
    grep -F -q 'let visibleBadgeChange: OptionalChange<[String]> = .setTo([])' <<<"$lite_source"; then
    pass "Lite-active avatar and profile paths neither load nor display legacy badges"
  else
    fail "Lite-active avatar or profile source can still load/display a legacy badge: $(head -n 12 <<<"$badge_runtime_matches" | tr '\n' ' ')"
  fi
}

verify_diagnostic_upload_lite_source_boundary() {
  local entry_source
  local debug_source
  local source_files=(
    Signal/AppLaunch/AppDelegate.swift
    Signal/Debugging/ContactSupportActionSheet.swift
    Signal/Notifications/NotificationActionHandler.swift
    Signal/Provisioning/UserInterface/ProvisioningController.swift
    Signal/Registration/UserInterface/RegistrationNavigationController.swift
    Signal/Registration/RegistrationCoordinatorBackupErrorPresenter.swift
    Signal/src/ViewControllers/AppSettings/ComposeSupportEmailOperation.swift
    Signal/src/ViewControllers/AppSettings/ContactSupportViewController.swift
    Signal/src/ViewControllers/AppSettings/HelpViewController.swift
    Signal/src/ViewControllers/DatabaseRecoveryViewController.swift
    "Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift"
    SignalServiceKit/Notifications/NotificationPresenterImpl.swift
  )

  if entry_source="$({
    local source_file
    for source_file in "${source_files[@]}"; do
      xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE -D'targetEnvironment(x)=0' "$source_file" || exit 1
    done
  } 2>/dev/null)"; then
    pass "diagnostic/support entrypoints preprocess successfully for KIVO_APP_STORE_LITE"
  else
    fail "diagnostic/support entrypoints could not be preprocessed for the Lite boundary"
    return
  fi

  local upload_entry_matches
  upload_entry_matches="$(rg -n 'promptToSubmitLogs|DebugLogUploader|attemptUpload|requireUpload|uploadDebugLogWithTimeout|uploadLogsWithUI|Submit debug log|Submit Debug Log|Internal-only' <<<"$entry_source" || true)"
  if [[ -z "$upload_entry_matches" ]] &&
    grep -F -q 'logUrl: nil' <<<"$entry_source"; then
    pass "Lite-active support and recovery entrypoints offer email without diagnostic upload"
  else
    fail "Lite-active support or recovery entrypoint retains diagnostic-upload behavior: $(head -n 12 <<<"$upload_entry_matches" | tr '\n' ' ')"
  fi

  local launch_failure_log_prompt_matches
  launch_failure_log_prompt_matches="$(rg -n \
    'APP_LAUNCH_FAILURE_(ALERT_MESSAGE|CORRUPT_REGISTRATION_MESSAGE|RESTORE_FAILED_MESSAGE|LAST_LAUNCH_CRASHED_MESSAGE)' \
    <<<"$entry_source" || true)"
  local launch_support_localization_count=0
  local launch_support_localization_missing=""
  local launch_support_strings_file
  while IFS= read -r launch_support_strings_file; do
    if rg -q '^"SUPPORT_EMAIL_ERROR_ALERT_DESCRIPTION" =' "$launch_support_strings_file"; then
      launch_support_localization_count=$((launch_support_localization_count + 1))
    else
      launch_support_localization_missing+="$launch_support_strings_file"$'\n'
    fi
  done < <(find Signal/translations -mindepth 2 -maxdepth 2 -name Localizable.strings | LC_ALL=C sort)
  if [[ -z "$launch_failure_log_prompt_matches" ]] &&
    grep -F -q 'SUPPORT_EMAIL_ERROR_ALERT_DESCRIPTION' <<<"$entry_source" &&
    [[ "$launch_support_localization_count" == 47 ]] &&
    [[ -z "$launch_support_localization_missing" ]]; then
    pass "Lite launch-failure UI offers localized email support without requesting diagnostic logs"
  else
    fail "Lite launch-failure UI retains a diagnostic-log prompt or lacks its 47-language support fallback: $(head -n 8 <<<"$launch_failure_log_prompt_matches$launch_support_localization_missing" | tr '\n' ' ')"
  fi

  if debug_source="$(xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE \
    Signal/Debugging/DebugLogs.swift 2>/dev/null)" &&
    ! rg -q 'DebugLogUploader|uploadLogsWithUI|uploadDebugLogWithTimeout' <<<"$debug_source" &&
    grep -F -A4 'func uploadLogs() async throws(DebugLogsError) -> URL {' <<<"$debug_source" \
      | grep -F -q 'throw DebugLogsError.uploadDisabled'; then
    pass "Lite diagnostic compatibility API fails closed and contains no uploader implementation"
  else
    fail "Lite diagnostic compatibility API does not fail closed or still contains an uploader"
  fi

  if grep -F -q 'DebugLogPreviewViewController.swift' <<<"$main_exclusions_line"; then
    pass "main App Store target excludes the diagnostic-log preview controller"
  else
    fail "main App Store target still compiles the diagnostic-log preview controller"
  fi
}

verify_lite_deletion_and_recovery_error_boundaries() {
  local deletion_source
  local recovery_source

  if deletion_source="$(xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE \
    Signal/src/ViewControllers/AppSettings/Account/DeleteAccountConfirmationViewController.swift \
    2>/dev/null)"; then
    pass "account-deletion source preprocesses successfully for KIVO_APP_STORE_LITE"
  else
    fail "account-deletion source could not be preprocessed for the Lite boundary"
    return
  fi

  if ! rg -q 'donationSubscriptionManager|cancelSubscription|Found subscriber ID' \
      <<<"$deletion_source" &&
    grep -F -A4 'func deleteDonationSubscriptionIfNecessary() async throws {' \
      <<<"$deletion_source" | grep -F -q 'return'; then
    pass "Lite account deletion never contacts a legacy donation-subscription endpoint"
  else
    fail "Lite account deletion retains legacy donation-subscription cancellation behavior"
  fi

  if recovery_source="$({
    xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE \
      Signal/src/ViewControllers/DatabaseRecoveryViewController.swift || exit 1
    xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE \
      SignalServiceKit/Notifications/NotificationPresenterImpl.swift || exit 1
  } 2>/dev/null)"; then
    pass "recovery-error sources preprocess successfully for KIVO_APP_STORE_LITE"
  else
    fail "recovery-error sources could not be preprocessed for the Lite boundary"
    return
  fi

  if ! rg -q \
      'DATABASE_RECOVERY_RECOVERY_FAILED_DESCRIPTION|BACKUPS_MEDIA_ERROR_NOTIFICATION_BODY|\.submitDebugLogsForBackupsMediaError' \
      <<<"$recovery_source" &&
    grep -F -q 'SOMETHING_WENT_WRONG_TRY_AGAIN_LATER_ERROR' <<<"$recovery_source"; then
    pass "Lite recovery errors no longer direct users to an unavailable diagnostic uploader"
  else
    fail "Lite recovery errors retain a diagnostic-log instruction or action"
  fi
}

verify_recovery_phrase_background_boundary() {
  local source='Signal/Kivo/KivoWalletOnboardingViewController.swift'
  local import_controller
  local recovery_controller
  import_controller="$(sed -n \
    '/final class KivoWalletImportViewController:/,/final class KivoRecoveryPhraseViewController:/p' \
    "$source")"
  recovery_controller="$(sed -n \
    '/final class KivoRecoveryPhraseViewController:/,/private final class KivoInsetLabel:/p' \
    "$source")"

  if grep -F -q 'UIApplication.willResignActiveNotification' <<<"$import_controller" &&
    grep -F -q 'secretTextView.text = nil' <<<"$import_controller" &&
    grep -F -q 'passwordField.text = nil' <<<"$import_controller" &&
    grep -F -q 'hideSecret()' <<<"$import_controller"; then
    pass "wallet-import UI clears credentials before Kivo becomes inactive"
  else
    fail "wallet-import UI does not enforce the reviewed resign-active credential clearing boundary"
  fi

  if grep -F -q 'private var recoveryWords: [String]' <<<"$recovery_controller" &&
    grep -F -q 'UIApplication.willResignActiveNotification' <<<"$recovery_controller" &&
    grep -F -q 'recoveryWords.removeAll(keepingCapacity: false)' <<<"$recovery_controller" &&
    grep -F -q 'phraseLabel.text = nil' <<<"$recovery_controller" &&
    grep -F -q 'verificationFields.forEach { $0.text = nil }' <<<"$recovery_controller" &&
    grep -F -q 'navigationController?.popViewController(animated: false)' <<<"$recovery_controller"; then
    pass "recovery-phrase UI clears plaintext and exits before Kivo becomes inactive"
  else
    fail "recovery-phrase UI does not enforce the reviewed background-clear-and-exit boundary"
  fi
}

verify_sdwebimage_public_api_patch() {
  local helper='Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.m'
  local metadata='Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.m'
  local macros='Pods/SDWebImage/SDWebImage/Private/SDInternalMacros.h'

  if rg -q 'patch_sdwebimage_public_api\(installer\)' Podfile &&
    rg -q 'patch_sdwebimage_public_api\.rb' Podfile &&
    [[ -f Scripts/patch_sdwebimage_public_api.rb ]]; then
    pass "Pod installation reproducibly applies the reviewed SDWebImage public-API patch"
  else
    fail "Pod installation does not reproducibly apply the SDWebImage public-API patch"
  fi

  if ! rg -q 'SD_SEL_SPI\((deviceInfoForKey:|CGSVGDocument|CGPDFPage)\)' "$helper" "$metadata" &&
    rg -q 'return self\.sd_imageFormat == SDImageFormatSVG \|\| self\.sd_imageFormat == SDImageFormatPDF;' "$metadata" &&
    rg -q '#if SD_MAC' "$macros"; then
    pass "installed SDWebImage iOS sources contain no reviewed private-selector calls"
  else
    fail "installed SDWebImage iOS sources still contain a reviewed private-selector call"
  fi
}

verify_deferred_asset_source_boundary() {
  local deferred='Signal/DeferredAppStoreAssets.xcassets'
  local asset
  local required_assets=(
    badge-gifting-promo-image.imageset
    badge-multi.imageset
    credit-or-debit-card.imageset
    custom-story-dark-36.imageset
    custom-story-light-36.imageset
    gift-bow.imageset
    gift-thumbnail.imageset
    gift.imageset
    giphy_logo.imageset
    logo_ideal.imageset
    official-no-color.imageset
    official-wallpaper.imageset
    official.imageset
    paypal-logo.imageset
    signal-backups-48.imageset
    stories-fill-compact.imageset
    stories.imageset
    sustainer-heart.imageset
    tab-stories.imageset
  )
  local missing_assets=""

  for asset in "${required_assets[@]}"; do
    [[ -d "$deferred/$asset" ]] || missing_assets+="$asset"$'\n'
  done
  if [[ -z "$missing_assets" ]]; then
    pass "all reviewed third-party, gifting, Stories, and upstream-branded assets are isolated in the deferred catalogue"
  else
    fail "reviewed deferred assets are missing from the isolated catalogue: $(tr '\n' ' ' <<<"$missing_assets")"
  fi

  if /usr/bin/ruby -e '
    source = File.read(ARGV.fetch(0))
    main = source[/D221A0BE169C9E5F00537ABF \/\* App Store Release \*\/ = \{.*?\n\t\t\};/m]
    share = source[/453518751FC635DD00210559 \/\* App Store Release \*\/ = \{.*?\n\t\t\};/m]
    abort "missing App Store Release configuration" unless main && share
    abort "main does not exclude deferred assets" unless main.include?("DeferredAppStoreAssets.xcassets")
    abort "share does not exclude deferred assets" unless share.include?("DeferredAppStoreAssets.xcassets")
    abort "Stories onboarding JSON is not excluded" unless %w[
      story_viewer_onboarding_1.json
      story_viewer_onboarding_2.json
      story_viewer_onboarding_3.json
    ].all? { |name| main.include?(name) }
    abort "unused sonar audio is not excluded" unless main.include?("sonarping.mp3")
    abort "deferred catalogue is not a main resource input" unless source.include?("A12B12C0FFEE130000000002 /* DeferredAppStoreAssets.xcassets in Resources */")
    abort "deferred catalogue is not a share resource input" unless source.include?("A12B12C0FFEE130000000003 /* DeferredAppStoreAssets.xcassets in Resources */")
  ' Signal.xcodeproj/project.pbxproj; then
    pass "main and Share App Store configurations exclude the deferred catalogue and Stories-only loose resources"
  else
    fail "App Store resource isolation is not completely configured"
  fi
}

verify_shared_asset_brand_residue() {
  if /usr/bin/ruby -e '
    roots = ARGV
    files = roots.flat_map do |root|
      Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH)
    end.select { |path| File.file?(path) }
    leaks = files.select do |path|
      File.basename(path).match?(/signal/i) || File.binread(path).match?(/signal/i)
    end
    unless leaks.empty?
      warn leaks.sort.join("\n")
      exit 1
    end
  ' Signal/Images.xcassets Signal/Symbols.xcassets Signal/NSE-Images.xcassets; then
    pass "shared App Store asset catalogues contain no Signal-named filenames or raw metadata"
  else
    fail "shared App Store asset catalogues retain a Signal-named filename or raw metadata string"
  fi
}

verify_shared_asset_pdf_integrity() {
  local pdf
  local expected_sha256
  local actual_sha256
  local reviewed_pdfs=(
    'Signal/Symbols.xcassets/member-label/tag-22.imageset/tag-22.pdf:0540cb9bfca8750c1bf4819d920ad9782ab6207fd3a174e0e3ba831bd604d542'
    'Signal/Symbols.xcassets/polls/poll-win.imageset/poll-win.pdf:16cd8d7e38ed1e150b421e54a4b483ae693a9a94ff12150f1bda074df55a8348'
  )

  for pdf in "${reviewed_pdfs[@]}"; do
    expected_sha256="${pdf##*:}"
    pdf="${pdf%%:*}"
    actual_sha256="$(shasum -a 256 "$pdf" | cut -d ' ' -f 1)"
    if [[ "$actual_sha256" == "$expected_sha256" ]] &&
      grep -a -F -q '/FirstChar 0' "$pdf" &&
      grep -a -F -q '/LastChar 0' "$pdf" &&
      ! grep -a -F -q '/LastChar 1' "$pdf"; then
      pass "reviewed asset PDF has a valid one-glyph Type3 width range: $pdf"
    else
      fail "reviewed asset PDF has changed or restored its invalid Type3 width range: $pdf"
    fi
  done
}

verify_app_store_device_scope() {
  if /usr/bin/ruby -e '
    source = File.read(ARGV.fetch(0))
    configurations = {
      "main" => "D221A0BE169C9E5F00537ABF",
      "notification extension" => "342FFE9227245852000AC89F",
      "share extension" => "453518751FC635DD00210559",
    }
    configurations.each do |label, identifier|
      block = source[/#{identifier} \/\* App Store Release \*\/ = \{.*?\n\t\t\};/m]
      abort "missing #{label} App Store Release configuration" unless block
      abort "#{label} is not iPhone-only" unless block.include?("TARGETED_DEVICE_FAMILY = 1;")
      abort "#{label} enables Mac Catalyst" unless block.include?("SUPPORTS_MACCATALYST = NO;")
      abort "#{label} enables Designed for Mac" unless block.include?("SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;")
      abort "#{label} enables Designed for Vision Pro" unless block.include?("SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;")
    end
  ' Signal.xcodeproj/project.pbxproj; then
    pass "main app and extensions lock App Store Release to the reviewed iPhone-only scope"
  else
    fail "App Store Release device scope is broader or inconsistent"
  fi
}

if [[ "$submission_mode" == true && -z "$archive_path" ]]; then
  fail "--submission requires --archive /absolute/path/to/Kivo.xcarchive"
fi

ringrtc_setup_line="$(
  grep -n -F 'Pods/SignalRingRTC/bin/set-up-for-cocoapods' \
    Scripts/bootstrap_kivo_open_source_dependencies |
    head -n 1 |
    cut -d: -f1 || true
)"
pod_install_line="$(
  grep -n -F 'bundle exec pod install' Scripts/bootstrap_kivo_open_source_dependencies |
    head -n 1 |
    cut -d: -f1 || true
)"

if grep -q "LIBSIGNAL_COMMIT=\"ed3a21d54b0caf1684eb1bf1dba62fff64c784ef\"" Scripts/bootstrap_kivo_open_source_dependencies &&
  grep -q "support_files_group.source_tree = 'SOURCE_ROOT'" Podfile &&
  [[ "$ringrtc_setup_line" =~ ^[0-9]+$ ]] &&
  [[ "$pod_install_line" =~ ^[0-9]+$ ]] &&
  ((ringrtc_setup_line < pod_install_line)); then
  pass "open-source bootstrap prepares RingRTC before CocoaPods and keeps LibSignalClient paths relocatable"
else
  fail "open-source dependency order or relocatable LibSignalClient path fix is missing"
fi

if Scripts/verify_kivo_pods_install; then
  pass "Pods matches the frozen gitlink, lockfile, reviewed SDWebImage patch, generated paths, and canonical digest"
else
  fail "Pods installation does not match the reviewed App Store dependency state"
fi

expected_locales="en-US ms th zh-Hans zh-Hant"
actual_locales="$(find fastlane/metadata -mindepth 1 -maxdepth 1 -type d -not -empty -exec basename {} \; | LC_ALL=C sort | tr '\n' ' ' | sed 's/ $//')"
if [[ "$actual_locales" == "$expected_locales" ]]; then
  pass "App Store metadata contains only reviewed locales"
else
  fail "metadata locales are '$actual_locales' (expected '$expected_locales')"
fi

required_metadata="description.txt keywords.txt name.txt privacy_url.txt release_notes.txt subtitle.txt support_url.txt"
for locale in $expected_locales; do
  for file in $required_metadata; do
    [[ -s "fastlane/metadata/$locale/$file" ]] || fail "missing fastlane/metadata/$locale/$file"
  done

  name="$(<"fastlane/metadata/$locale/name.txt")"
  subtitle="$(<"fastlane/metadata/$locale/subtitle.txt")"
  keywords="$(<"fastlane/metadata/$locale/keywords.txt")"
  description="$(<"fastlane/metadata/$locale/description.txt")"
  release_notes="$(<"fastlane/metadata/$locale/release_notes.txt")"
  ((${#name} <= 30)) || fail "$locale app name exceeds 30 characters"
  ((${#subtitle} <= 30)) || fail "$locale subtitle exceeds 30 characters"
  ((${#keywords} <= 100)) || fail "$locale keywords exceed 100 characters"
  ((${#description} <= 4000)) || fail "$locale description exceeds 4000 characters"
  ((${#release_notes} <= 4000)) || fail "$locale release notes exceed 4000 characters"
done
pass "metadata field lengths fit App Store limits"

if grep -R -E -i -q 'support\.signal\.org|github\.com/signalapp|\.invalid([/:]|$)|signal private messenger' fastlane/metadata; then
  fail "metadata still contains an upstream or placeholder destination"
else
  pass "metadata has no upstream support or placeholder destinations"
fi

for plist in \
  Signal/Signal-Info.plist \
  Signal/PrivacyInfo.xcprivacy \
  SignalServiceKit/PrivacyInfo.xcprivacy \
  SignalNSE/Info.plist \
  SignalNSE/PrivacyInfo.xcprivacy \
  SignalShareExtension/Info.plist \
  SignalShareExtension/PrivacyInfo.xcprivacy \
  ThirdParty/TrustWalletCore/PrivacyInfo.xcprivacy
do
  if plutil -lint "$plist" >/dev/null; then
    pass "$plist is valid"
  else
    fail "$plist is invalid"
  fi
done

verify_required_reason_manifest \
  SignalServiceKit/PrivacyInfo.xcprivacy \
  "SignalServiceKit source" \
  'NSPrivacyAccessedAPICategoryFileTimestamp=C617.1' \
  'NSPrivacyAccessedAPICategoryDiskSpace=E174.1' \
  'NSPrivacyAccessedAPICategoryUserDefaults=1C8F.1,CA92.1'
verify_required_reason_manifest \
  ThirdParty/TrustWalletCore/PrivacyInfo.xcprivacy \
  "TrustWalletCore vendored source" \
  'NSPrivacyAccessedAPICategoryFileTimestamp=C617.1' \
  'NSPrivacyAccessedAPICategorySystemBootTime=35F9.1'

if rg -q '<string>NSPrivacyCollectedDataTypePreciseLocation</string>' Signal/PrivacyInfo.xcprivacy; then
  pass "main privacy manifest declares user-selected precise-location sharing"
else
  fail "location sharing is reachable but the main privacy manifest omits Precise Location"
fi

invalid_strings=0
while IFS= read -r strings_file; do
  if ! plutil -lint "$strings_file" >/dev/null; then
    printf 'Invalid strings file: %s\n' "$strings_file" >&2
    invalid_strings=$((invalid_strings + 1))
  fi
done < <(find Signal/translations -name '*.strings' -type f -print)
if ((invalid_strings == 0)); then
  pass "all localized .strings files are valid"
else
  fail "$invalid_strings localized .strings files are invalid"
fi

camera_qr_localization_count=0
while IFS= read -r camera_strings_file; do
  camera_description="$(
    /usr/libexec/PlistBuddy -c 'Print :NSCameraUsageDescription' \
      "$camera_strings_file" 2>/dev/null || true
  )"
  case "$camera_description" in
    *QR* | *คิวอาร์โค้ด* | *二維碼* | *二维码*)
      camera_qr_localization_count=$((camera_qr_localization_count + 1))
      ;;
  esac
done < <(find Signal/translations -name InfoPlist.strings -type f -print | LC_ALL=C sort)
if [[ "$camera_qr_localization_count" == "47" ]]; then
  pass "camera permission describes QR scanning in all 47 localizations"
else
  fail "camera permission describes QR scanning in $camera_qr_localization_count of 47 localizations"
fi

for key in \
  KIVO_DELETE_ACCOUNT_PASSWORD_PLACEHOLDER \
  KIVO_DELETE_ACCOUNT_TITLE \
  KIVO_DELETE_ACCOUNT_DESCRIPTION \
  KIVO_DELETE_ACCOUNT_CONFIRM_TITLE
do
  count="$(rg -l "^\"${key}\" =" Signal/translations --glob 'Localizable.strings' | wc -l | tr -d ' ')"
  if [[ "$count" == "47" ]]; then
    pass "$key exists in all 47 localizations"
  else
    fail "$key exists in $count of 47 localizations"
  fi
done

open_source_locales=(en ms th zh_CN zh_HK zh_TW)
open_source_localization_count=0
for locale in "${open_source_locales[@]}"; do
  if rg -q '^"KIVO_OPEN_SOURCE_CELL" =' "Signal/translations/$locale.lproj/Localizable.strings"; then
    open_source_localization_count=$((open_source_localization_count + 1))
  fi
done
if ((open_source_localization_count == ${#open_source_locales[@]})) &&
  rg -q 'KivoURLs\.sourceCode' Signal/src/ViewControllers/AppSettings/HelpViewController.swift; then
  pass "About exposes the Kivo source page in primary release languages"
else
  fail "About source-page link or a primary release localization is missing"
fi

legal_entry_locales=(en fr ko ms th zh_CN zh_HK zh_TW)
legal_entry_localization_count=0
for locale in "${legal_entry_locales[@]}"; do
  legal_strings_file="Signal/translations/$locale.lproj/Localizable.strings"
  if rg -q '^"KIVO_TERMS_OF_SERVICE" =' "$legal_strings_file" &&
    rg -q '^"KIVO_PRIVACY_POLICY" =' "$legal_strings_file"; then
    legal_entry_localization_count=$((legal_entry_localization_count + 1))
  fi
done
help_source="Signal/src/ViewControllers/AppSettings/HelpViewController.swift"
wallet_onboarding_source="Signal/Kivo/KivoWalletOnboardingViewController.swift"
if ((legal_entry_localization_count == ${#legal_entry_locales[@]})) &&
  grep -F -q 'SFSafariViewController(url: KivoURLs.termsOfService)' "$help_source" &&
  grep -F -q 'SFSafariViewController(url: KivoURLs.privacyPolicy)' "$help_source" &&
  grep -F -q 'SFSafariViewController(url: KivoURLs.termsOfService)' "$wallet_onboarding_source" &&
  grep -F -q 'SFSafariViewController(url: KivoURLs.privacyPolicy)' "$wallet_onboarding_source" &&
  rg -U -q 'mode == \.create \|\| mode == \.accountRecoveryImport' "$wallet_onboarding_source"; then
  pass "Help and account onboarding expose direct Terms and Privacy Policy links"
else
  fail "Help or account onboarding lacks a direct localized Terms/Privacy entry"
fi

# Wallet-address discovery is one of the principal user-visible differences
# between Kivo and the upstream messenger. Do not let the Hong Kong or Taiwan
# storefront flow silently fall back to the English placeholder/error copy.
critical_wallet_discovery_keys=(
  KIVO_RECIPIENT_DISCOVERY_FOOTER
  KIVO_RECIPIENT_DISCOVERY_PLACEHOLDER
  KIVO_RECIPIENT_DISCOVERY_TITLE
  KIVO_RECIPIENT_SEARCH_PLACEHOLDER
  KIVO_WALLET_LOOKUP_ERROR_MESSAGE
  KIVO_WALLET_LOOKUP_INVALID_MESSAGE_FORMAT
  KIVO_WALLET_LOOKUP_INVALID_TITLE
  KIVO_WALLET_LOOKUP_NOT_FOUND_MESSAGE_FORMAT
  KIVO_WALLET_LOOKUP_NOT_FOUND_TITLE
  KIVO_WALLET_SEARCH_SECTION_TITLE
)
wallet_discovery_localization_failures=""
for locale in zh_HK zh_TW; do
  localized_strings="Signal/translations/$locale.lproj/Localizable.strings"
  for key in "${critical_wallet_discovery_keys[@]}"; do
    english_value="$(/usr/libexec/PlistBuddy -c "Print :$key" \
      Signal/translations/en.lproj/Localizable.strings 2>/dev/null || true)"
    localized_value="$(/usr/libexec/PlistBuddy -c "Print :$key" \
      "$localized_strings" 2>/dev/null || true)"
    if [[ -z "$localized_value" || "$localized_value" == "$english_value" ]]; then
      wallet_discovery_localization_failures+="$locale:$key"$'\n'
    fi
  done
done
if [[ -z "$wallet_discovery_localization_failures" ]]; then
  pass "Hong Kong and Taiwan wallet-discovery flows contain reviewed localized copy"
else
  fail "wallet-discovery copy is missing or still English in: $(tr '\n' ' ' <<<"$wallet_discovery_localization_failures")"
fi

if rg -q 'Kivoer' Signal/translations/fr.lproj/Localizable.strings ||
  rg -q 'Singal' Signal/translations/ko.lproj/Localizable.strings; then
  fail "French or Korean safety/account copy retains a known mechanical brand typo"
else
  pass "French and Korean safety/account copy omits known Kivoer/Singal typos"
fi

if rg -q 'static let newDeviceServiceIdentifier = "kivo-new-device"' \
    Signal/DeviceTransfer/DeviceTransfer.swift &&
  /usr/libexec/PlistBuddy -c 'Print :NSBonjourServices:0' Signal/Signal-Info.plist 2>/dev/null \
    | grep -Fxq '_kivo-new-device._tcp'; then
  pass "device-transfer Bonjour runtime and Info.plist identifiers match Kivo"
else
  fail "device-transfer Bonjour runtime and Info.plist identifiers do not match"
fi

if rg -q 'UIApplication\.shared\.open\(KivoURLs\.support\)' \
    Signal/QuickRestore/OutgoingDeviceRestoreInitialViewController.swift &&
  ! rg -q 'TODO: link to documentation' Signal; then
  pass "Quick Restore help resolves to Kivo support with no placeholder URL"
else
  fail "Quick Restore still has a missing or placeholder help URL"
fi

if rg -q 'Sticker-pack links are not available' release/app-store/public/addstickers/index.html &&
  ! rg -q 'data-open-kivo|preview the pack before installing|Discover and install' \
    release/app-store/public/addstickers/index.html &&
  ! rg -q '"/addstickers/"[[:space:]]*:' release/app-store/public/assets/open-kivo.js; then
  pass "local public site matches Lite external sticker-pack boundary"
else
  fail "local public site still advertises external sticker-pack installation"
fi

if ! rg -q 'github\.com/sowhat1010/Signal-iOS/issues' \
    release/app-store/public/support/index.html; then
  pass "public support page does not send users to an upstream-branded issue tracker"
else
  fail "public support page still links the upstream-branded issue tracker"
fi

for url in \
  https://kivo.it389.com/healthz \
  https://kivo.it389.com/privacy \
  https://kivo.it389.com/terms \
  https://kivo.it389.com/support \
  https://kivo.it389.com/source \
  https://kivo.it389.com/u/ \
  https://kivo.it389.com/group/ \
  https://kivo.it389.com/call/ \
  https://kivo.it389.com/addstickers/
do
  if curl --fail --location --silent --show-error --max-time 15 --output /dev/null "$url"; then
    pass "$url is reachable"
  else
    fail "$url is not reachable"
  fi
done

for page in privacy support; do
  if curl --fail --location --silent --show-error --max-time 15 \
    "https://kivo.it389.com/$page/" \
    | grep -F -q 'forgotten wallet password does not block'; then
    pass "public $page page matches the password-free account deletion flow"
  else
    fail "public $page page still contradicts the password-free account deletion flow"
  fi
done

build_settings="$(mktemp)"
signal_ui_build_settings="$(mktemp)"
signal_service_kit_build_settings="$(mktemp)"
cleanup() {
  rm -f "$build_settings" "$signal_ui_build_settings" "$signal_service_kit_build_settings"
  if [[ -n "$ipa_temp_dir" && -d "$ipa_temp_dir" ]]; then
    rm -rf "$ipa_temp_dir"
  fi
}
trap cleanup EXIT
if xcodebuild \
  -workspace Signal.xcworkspace \
  -scheme Signal \
  -configuration 'App Store Release' \
  -destination 'generic/platform=iOS' \
  -showBuildSettings >"$build_settings"; then
  grep -Eq '^ *PRODUCT_BUNDLE_IDENTIFIER = com\.zhiera\.kivo$' "$build_settings" \
    && pass "Release bundle identifier is com.zhiera.kivo" \
    || fail "Release bundle identifier is not com.zhiera.kivo"
  grep -Eq '^ *FULL_PRODUCT_NAME = Kivo\.app$' "$build_settings" \
    && pass "Release wrapper is Kivo.app" \
    || fail "Release wrapper is not Kivo.app"
  grep -Eq '^ *EXECUTABLE_NAME = Kivo$' "$build_settings" \
    && pass "Release executable is Kivo" \
    || fail "Release executable is not Kivo"

  if grep -E -q '^ *SWIFT_ACTIVE_COMPILATION_CONDITIONS = .*KIVO_APP_STORE_LITE' "$build_settings"; then
    app_store_lite_build_condition_is_present=true
    pass "main App Store target compiles with KIVO_APP_STORE_LITE"
  else
    fail "main App Store target is missing KIVO_APP_STORE_LITE"
  fi

  main_exclusions_line="$(grep -E '^ *EXCLUDED_SOURCE_FILE_NAMES =' "$build_settings" | head -n 1 || true)"
  main_financial_sources_are_excluded=true
  for exclusion in \
    'BadgeGifting*.swift' \
    'Donate*.swift' \
    'Donation*.swift' \
    'Payments*.swift' \
    'SendPayment*.swift'
  do
    if ! grep -F -q "$exclusion" <<<"$main_exclusions_line"; then
      main_financial_sources_are_excluded=false
    fi
  done
  if [[ "$main_financial_sources_are_excluded" == true ]]; then
    pass "main App Store target excludes the reviewed legacy financial UI source families"
  else
    fail "main App Store target does not exclude every reviewed legacy financial UI source family"
  fi

  main_story_ui_is_excluded=true
  for exclusion in \
    'GroupStory*.swift' \
    'HiddenStory*.swift' \
    'MyStor*.swift' \
    'PrivateStory*.swift' \
    'Stories*.swift' \
    'Story*.swift'
  do
    if ! grep -F -q "$exclusion" <<<"$main_exclusions_line"; then
      main_story_ui_is_excluded=false
    fi
  done
  if [[ "$main_story_ui_is_excluded" == true ]]; then
    pass "main App Store target excludes the reviewed Stories UI source families"
  else
    fail "main App Store target does not exclude every reviewed Stories UI source family"
  fi
else
  fail "could not read App Store Release build settings"
fi

if xcodebuild \
  -project Signal.xcodeproj \
  -target SignalUI \
  -configuration 'App Store Release' \
  -showBuildSettings >"$signal_ui_build_settings"; then
  if grep -E -q '^ *SWIFT_ACTIVE_COMPILATION_CONDITIONS = .*KIVO_APP_STORE_LITE' \
    "$signal_ui_build_settings"; then
    signal_ui_lite_build_condition_is_present=true
    pass "SignalUI App Store target compiles with KIVO_APP_STORE_LITE"
  else
    fail "SignalUI App Store target is missing KIVO_APP_STORE_LITE"
  fi

  signal_ui_exclusions_line="$(grep -E '^ *EXCLUDED_SOURCE_FILE_NAMES =' \
    "$signal_ui_build_settings" | head -n 1 || true)"
  signal_ui_payment_engine_is_excluded=true
  for exclusion in \
    'MobileCoinAPI+Configuration.swift' \
    'MobileCoinAPI.swift' \
    'MobileCoinHelperSDK.swift' \
    'PaymentActionSheets.swift' \
    'PaymentOnboarding.swift' \
    'PaymentsFormat+MobileCoin.swift' \
    'PaymentsImpl.swift' \
    'PaymentsProcessor.swift' \
    'PaymentsReconciliation.swift'
  do
    if ! grep -F -q "$exclusion" <<<"$signal_ui_exclusions_line"; then
      signal_ui_payment_engine_is_excluded=false
    fi
  done
  if [[ "$signal_ui_payment_engine_is_excluded" == true ]]; then
    pass "SignalUI App Store target excludes the reviewed MobileCoin network/transaction engine"
  else
    fail "SignalUI App Store target does not exclude every reviewed MobileCoin engine source"
  fi

  signal_ui_story_ui_is_excluded=true
  for exclusion in \
    'AllSignalConnectionsViewController.swift' \
    'ConnectionsEducationSheetViewController.swift' \
    'MyStorySettingsViewController.swift' \
    'NewGroupStoryViewController.swift' \
    'NewPrivateStoryConfirmViewController.swift' \
    'NewPrivateStoryRecipientsViewController.swift' \
    'NewStoryHeaderView.swift' \
    'SelectMyStoryRecipientsViewController.swift' \
    'StoryMessage+SignalUI.swift' \
    'StorySharing.swift'
  do
    if ! grep -F -q "$exclusion" <<<"$signal_ui_exclusions_line"; then
      signal_ui_story_ui_is_excluded=false
    fi
  done
  if [[ "$signal_ui_story_ui_is_excluded" == true ]]; then
    pass "SignalUI App Store target excludes the reviewed Stories UI sources"
  else
    fail "SignalUI App Store target does not exclude every reviewed Stories UI source"
  fi
else
  fail "could not read SignalUI App Store Release build settings"
fi

if xcodebuild \
  -project Signal.xcodeproj \
  -target SignalServiceKit \
  -configuration 'App Store Release' \
  -showBuildSettings >"$signal_service_kit_build_settings"; then
  if grep -E -q '^ *SWIFT_ACTIVE_COMPILATION_CONDITIONS = .*KIVO_APP_STORE_LITE' \
    "$signal_service_kit_build_settings"; then
    signal_service_kit_lite_build_condition_is_present=true
    pass "SignalServiceKit App Store target compiles with KIVO_APP_STORE_LITE"
  else
    fail "SignalServiceKit App Store target is missing KIVO_APP_STORE_LITE"
  fi
  signal_service_kit_exclusions_line="$(grep -E '^ *EXCLUDED_SOURCE_FILE_NAMES =' \
    "$signal_service_kit_build_settings" | head -n 1 || true)"
  if grep -F -q 'SubscriptionRedemptionNecessityChecker.swift' <<<"$signal_service_kit_exclusions_line"; then
    pass "SignalServiceKit App Store target excludes the legacy paid-subscription heartbeat source"
  else
    fail "SignalServiceKit App Store target still compiles the legacy paid-subscription heartbeat source"
  fi
else
  fail "could not read SignalServiceKit App Store Release build settings"
fi

release_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' Signal/Signal-Info.plist)"
if [[ -n "$release_version" ]]; then
  pass "main app uses version $release_version"
else
  fail "main app version is empty"
fi

release_build="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' Signal/Signal-Info.plist)"
if [[ "$release_build" =~ ^[1-9][0-9]*$ ]]; then
  pass "main app uses valid build $release_build"
else
  fail "main app build '$release_build' is not a positive integer"
fi

for plist in SignalNSE/Info.plist SignalShareExtension/Info.plist; do
  build="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$plist")"
  [[ "$build" == "$release_build" ]] \
    && pass "$plist matches main app build $release_build" \
    || fail "$plist uses build $build, expected $release_build to match the main app"
done

if [[ "$submission_mode" == false ]]; then
  if /usr/libexec/PlistBuddy -c 'Print :ITSAppUsesNonExemptEncryption' Signal/Signal-Info.plist >/dev/null 2>&1 ||
    /usr/libexec/PlistBuddy -c 'Print :ITSAppUsesNonExemptEncryption' SignalNSE/Info.plist >/dev/null 2>&1 ||
    /usr/libexec/PlistBuddy -c 'Print :ITSAppUsesNonExemptEncryption' SignalShareExtension/Info.plist >/dev/null 2>&1; then
    fail "an export-compliance plist answer is present without a completed decision record"
  else
    pass "export-compliance plist answer is withheld pending authorized determination"
  fi
fi

if rg -q 'signal-messenger\.cer in Resources' Signal.xcodeproj/project.pbxproj; then
  fail "upstream private CA certificate is still packaged"
else
  pass "upstream private CA certificate is not packaged"
fi

verify_app_store_resource_sanitizer_phase
verify_signal_symbol_brand_boundary
verify_callkit_brand_boundary
verify_paid_backup_lite_source_boundary
verify_internal_tools_lite_source_boundary
verify_remote_content_lite_source_boundary
verify_default_sticker_lite_source_boundary
verify_call_quality_lite_source_boundary
verify_badge_lite_source_boundary
verify_diagnostic_upload_lite_source_boundary
verify_lite_deletion_and_recovery_error_boundaries
verify_recovery_phrase_background_boundary
verify_sdwebimage_public_api_patch
verify_deferred_asset_source_boundary
verify_shared_asset_brand_residue
verify_shared_asset_pdf_integrity
verify_app_store_device_scope

deferred_sources=(
  Signal/Kivo/Platform
  Signal/Kivo/KivoEVMChainRegistry.swift
)
for deferred_source in "${deferred_sources[@]}"; do
  if [[ -e "$deferred_source" ]]; then
    fail "App Store Lite source still contains deferred feature path $deferred_source"
  else
    pass "deferred feature path is absent: $deferred_source"
  fi
done

if rg -q \
  'Kivo(DEX|AssetLeaderboard|PublicFeed|PublicProfile|MarketTokens|ExploreViewController|EVMChainRegistry)' \
  Signal.xcodeproj/project.pbxproj; then
  fail "App Store target still references a deferred finance, public-feed, or leaderboard type"
else
  pass "App Store target has no deferred finance, public-feed, or leaderboard references"
fi

if rg -q \
  'KivoExternalWallet|WalletConnect|externalWallet|外部钱包' \
  Signal/Kivo Signal/AppLaunch Signal.xcodeproj/project.pbxproj; then
  fail "App Store target still contains the deferred external-wallet flow"
else
  pass "App Store target has no external-wallet flow"
fi

if rg -q 'KivoProductFeatures\.signalDonationsEnabled' Signal/URLs/UrlOpener.swift; then
  pass "legacy donation callbacks are gated off for Kivo"
else
  fail "legacy donation callback parsing is not gated by the Kivo product flag"
fi

if rg -q 'KivoProductFeatures\.proxyAndCensorshipEnabled' Signal/URLs/UrlOpener.swift; then
  pass "legacy proxy links are gated off for Kivo"
else
  fail "legacy proxy links are not gated by the Kivo product flag"
fi

payment_receive_files=(
  SignalServiceKit/Messages/MessageReceiver.swift
  SignalServiceKit/Messages/DeviceSyncing/OWSIncomingSentMessageTranscript.swift
  SignalServiceKit/Payments/PaymentsHelperImpl.swift
)
payment_receive_gates_are_present=true
for source in "${payment_receive_files[@]}"; do
  if ! rg -q 'KivoProductFeatures\.legacyMobileCoinPaymentsEnabled' "$source"; then
    payment_receive_gates_are_present=false
  fi
done
if [[ "$payment_receive_gates_are_present" == true ]]; then
  pass "legacy MobileCoin receive and sync paths are gated off for Kivo"
else
  fail "a legacy MobileCoin receive or sync path is missing its Kivo product gate"
fi

payment_ui_files=(
  Signal/ConversationView/Components/CVComponentState.swift
  Signal/ConversationView/Components/CVComponentSystemMessage.swift
  Signal/ConversationView/ConversationViewController+CVComponentDelegate.swift
  Signal/ConversationView/ConversationViewController+MessageActionsDelegate.swift
  Signal/ConversationView/MessageActions.swift
  Signal/ConversationView/ConversationViewController+ConversationInputToolbarDelegate.swift
  Signal/src/ViewControllers/Payments/SendPaymentViewController.swift
)
payment_ui_gates_are_present=true
for source in "${payment_ui_files[@]}"; do
  if ! rg -q 'KivoProductFeatures\.legacyMobileCoinPaymentsEnabled' "$source"; then
    payment_ui_gates_are_present=false
  fi
done
if [[ "$payment_ui_gates_are_present" == true ]]; then
  pass "legacy MobileCoin rendering and actions are gated off for Kivo"
else
  fail "a legacy MobileCoin rendering or action path is missing its Kivo product gate"
fi

gift_receive_files=(
  SignalServiceKit/Messages/MessageReceiver.swift
  SignalServiceKit/Messages/DeviceSyncing/OWSIncomingSentMessageTranscript.swift
)
gift_receive_gates_are_present=true
for source in "${gift_receive_files[@]}"; do
  if ! rg -q 'KivoProductFeatures\.signalDonationsEnabled' "$source"; then
    gift_receive_gates_are_present=false
  fi
done
if [[ "$gift_receive_gates_are_present" == true ]]; then
  pass "legacy gift badge receive and sync paths are gated off for Kivo"
else
  fail "a legacy gift badge receive or sync path is missing its Kivo product gate"
fi

gift_ui_files=(
  Signal/ConversationView/Components/CVComponentState.swift
  Signal/ConversationView/Components/CVComponentGiftBadge.swift
  Signal/ConversationView/ConversationViewController+GiftBadges.swift
)
gift_ui_gates_are_present=true
for source in "${gift_ui_files[@]}"; do
  if ! rg -q 'KivoProductFeatures\.signalDonationsEnabled' "$source"; then
    gift_ui_gates_are_present=false
  fi
done
if [[ "$gift_ui_gates_are_present" == true ]]; then
  pass "legacy gift badge rendering and actions are gated off for Kivo"
else
  fail "a legacy gift badge rendering or action path is missing its Kivo product gate"
fi

app_setup_lite_source="$(xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE \
  SignalServiceKit/Environment/AppSetup.swift 2>/dev/null || true)"
if [[ -z "$app_setup_lite_source" ]]; then
  fail "AppSetup could not be preprocessed for the Lite job-queue boundary"
elif rg -q \
  'donationReceiptCredentialRedemptionJobQueue\.start|sendGiftBadgeJobQueue\.start' \
  <<<"$app_setup_lite_source"; then
  fail "Lite launch can restart a legacy donation or gift durable job"
else
  pass "Lite launch never starts legacy donation receipt or gift-badge job queues"
fi

profile_badge_lite_source="$({
  for source in \
    Signal/src/ViewControllers/AppSettings/Profile/ProfileSettingsViewController.swift \
    'Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift' \
    Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift \
    Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift \
    SignalUI/Views/ConversationAvatarView.swift
  do
    xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE "$source" || exit 1
  done
} 2>/dev/null || true)"
if [[ -z "$profile_badge_lite_source" ]]; then
  fail "profile-badge sources could not be preprocessed for the Lite boundary"
elif rg -q \
  'BADGE_CONFIGURATION_TITLE|CONVERSATION_SETTINGS_BADGES_(HEADER|FOOTER)|BadgeDetailsSheet[[:space:]]*\(' \
  <<<"$profile_badge_lite_source"; then
  fail "Lite-active profile/contact source still exposes a removed badge UI or localization key"
elif rg -U -q \
  '(?s)#if KIVO_APP_STORE_LITE[[:space:]]+badgeView\.image = nil.*?#else' \
  SignalUI/Views/ConversationAvatarView.swift &&
  rg -U -q \
  '(?s)fileprivate func fetchBadge.*?#if KIVO_APP_STORE_LITE[[:space:]]+return nil.*?#else' \
  SignalUI/Views/ConversationAvatarView.swift; then
  pass "Lite profile/contact badge entrypoints and rendering are compile-time disabled"
else
  fail "Lite avatar rendering lacks the reviewed fail-closed badge boundary"
fi

if rg -q 'if TSConstants\.isSystemContactsEnabled' \
  Signal/src/ViewControllers/AppSettings/Notifications/NotificationSettingsViewController.swift; then
  pass "contact-joined notification setting follows the system contacts boundary"
else
  fail "contact-joined notification setting is exposed while system contacts are disabled"
fi

support_source="Signal/src/ViewControllers/AppSettings/ContactSupportViewController.swift"
support_financial_switch_boundaries="$(rg -U -o \
  '(?s)#if !KIVO_APP_STORE_LITE\n[[:space:]]*case \.payments:.*?case \.donationsAndBadges:.*?#endif' \
  "$support_source" | grep -c '^#if !KIVO_APP_STORE_LITE' || true)"
if rg -U -q \
  '#if !KIVO_APP_STORE_LITE\n[[:space:]]*case payments\n[[:space:]]*case donationsAndBadges\n[[:space:]]*#endif\n[[:space:]]*case safetyAndAbuse' \
  "$support_source" &&
  [[ "$support_financial_switch_boundaries" == 3 ]] &&
  [[ "$(rg -c 'case \.payments:' "$support_source")" == 3 ]] &&
  [[ "$(rg -c 'case \.donationsAndBadges:' "$support_source")" == 3 ]] &&
  rg -U -q \
    '(?s)#if KIVO_APP_STORE_LITE\n[[:space:]]*let visibleFilters = Filter\.allCases\n#else\n[[:space:]]*let visibleFilters = Filter\.allCases\.filter.*?return filter != \.safetyAndAbuse.*?#endif' \
    "$support_source" &&
  rg -q 'KIVO_CONTACT_SUPPORT_FILTER_SAFETY_AND_ABUSE' Signal/translations/en.lproj/Localizable.strings; then
  pass "Lite support enum and menu compile out payments/donations and retain safety/abuse"
else
  fail "Lite support enum/filter boundary does not match the communications-only scope"
fi

safety_client="Signal/Kivo/KivoPlatformAccountDeletionClient.swift"
conversation_settings="Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController+Contents.swift"
safety_locales=(en ms th zh_CN zh_HK zh_TW)
safety_localization_count=0
for locale in "${safety_locales[@]}"; do
  strings_file="Signal/translations/$locale.lproj/Localizable.strings"
  if rg -q '^"KIVO_SAFETY_REPORT_ENTRY" =' "$strings_file" &&
    rg -q '^"KIVO_SAFETY_REPORT_SUCCESS_MESSAGE_FORMAT" =' "$strings_file" &&
    rg -q '^"KIVO_SAFETY_REPORT_PRIVACY_NOTICE" =' "$strings_file"; then
    safety_localization_count=$((safety_localization_count + 1))
  fi
done
if rg -q 'final class KivoSafetyReportClient' "$safety_client" &&
  rg -q 'endpoint\.appendPathComponent\("safety"\)' "$safety_client" &&
  rg -q 'endpoint\.appendPathComponent\("reports"\)' "$safety_client" &&
  rg -q 'case 201:' "$safety_client" &&
  rg -q 'selectedFilter == \.safetyAndAbuse' "$support_source" &&
  rg -q 'try await client\.submit' "$support_source" &&
  rg -q 'buildSafetyReportSection\(\)' "$conversation_settings" &&
  rg -q 'KivoSafetyReportTarget\(kind: \.group' "$conversation_settings" &&
  rg -q 'KivoSafetyReportTarget\(kind: \.contact' "$conversation_settings" &&
  ((safety_localization_count == ${#safety_locales[@]})); then
  pass "Lite exposes localized durable safety-report intake for contacts and groups"
else
  fail "Lite durable safety-report client, contextual entry, or primary localization is incomplete"
fi

if rg -q 'structured in-app safety reports expire no later than 90 days' \
    release/app-store/public/privacy/index.html &&
  rg -q 'Report a Safety Issue' release/app-store/public/support/index.html; then
  pass "local privacy and support pages disclose the structured safety-report boundary"
else
  fail "local privacy/support disclosure does not match structured safety reporting"
fi

if Scripts/verify_kivo_app_store_lite_stories; then
  pass "App Store Lite Stories compile-time boundary is intact"
else
  fail "App Store Lite Stories compile-time boundary is incomplete"
fi

private_api_lite_source="$({
  for source in \
    Signal/AppLaunch/WindowManager.swift \
    Signal/util/RingerSwitch.swift \
    Signal/util/VolumeButtons.swift \
    Signal/util/ScreenshotBlocking.swift \
    SignalServiceKit/Environment/AppSetup.swift \
    SignalServiceKit/Util/UIDevice+FeatureSupport.swift \
    SignalUI/Views/OWSNavigationBar.swift \
    Signal/ConversationView/ConversationBottomPanelView.swift \
    Signal/ConversationView/ConversationInputToolbar.swift \
    Signal/src/ViewControllers/HomeView/HomeTabBarController.swift
  do
    xcrun clang -E -P -x c -DKIVO_APP_STORE_LITE "$source" || exit 1
  done
} 2>/dev/null || true)"
private_api_lite_markers=(
  'egVaAAZ2BHdydHZSBwYBBAEGcgZ6AQBVegVyc312dQ=='
  'ZlpkdAQBfX1lAVV6BX56BQVkBwICAQQG'
  'BQcCAgEEBlcBBGR0BHZ2AEs='
  'd3oAegV5ZHQEAX19Z3p2CWUEcgAFegZ6AQA='
  'dAF+P3ICAn12PwUCBHoAeHMBcgR1PwR6AHh2BAUGcgZ2'
  'BXYGaHIABgVnAX0HfnZTBwYGAQBWCHYABgVL'
  'ZAsFBnZ+ZwF9B352VXp1VHlyAHh2'
  'cGZaUgICfXp0cgZ6AQBnAX0HfnZVAQkAUwcGBgEAVQEJAF8BBnp3enRyBnoBAA=='
  'cGZaUgICfXp0cgZ6AQBnAX0HfnZVAQkAUwcGBgEAZgJfAQZ6d3p0cgZ6AQA='
  'cGZaUgICfXp0cgZ6AQBnAX0HfnZmAlMHBgYBAFUBCQBfAQZ6d3p0cgZ6AQA='
  'cGZaUgICfXp0cgZ6AQBnAX0HfnZmAlMHBgYBAGYCXwEGend6dHIGegEA'
  'SystemVolumeDidChange'
  'MPVolumeControllerSystemDataSource'
  'TextLayoutCanvasView'
  'setValue(OWSTabBar(), forKey: "tabBar")'
  'orientationKey = "orientation"'
  'setValue(orientation.rawValue, forKey: orientationKey)'
  'setValue(view.layer, forKey: "layer")'
  '_UIConstraintBasedLayoutLogUnsatisfiable'
  '_UIBarBackground'
  '_UIVisualEffectSubview'
  '_UIBadgeView'
)
private_api_lite_found=""
for private_api_marker in "${private_api_lite_markers[@]}"; do
  if grep -F -q "$private_api_marker" <<<"$private_api_lite_source"; then
    private_api_lite_found+="$private_api_marker "$'\n'
  fi
done
if [[ -z "$private_api_lite_source" ]]; then
  fail "non-public API sources could not be preprocessed for the Lite boundary"
elif [[ -n "$private_api_lite_found" ]]; then
  fail "Lite-active source retains a non-public API payload: $(tr '\n' ' ' <<<"$private_api_lite_found")"
else
  pass "Lite compile-time boundary omits reviewed non-public API payloads"
fi

for key in \
  KivoPlatformAllowedAssetHosts \
  KivoPlatformAllowedUploadHosts \
  KivoPlatformAllowedWebHosts \
  KivoPlatformCDNBaseURL
do
  if /usr/libexec/PlistBuddy -c "Print :$key" Signal/Signal-Info.plist >/dev/null 2>&1; then
    fail "Signal-Info.plist still contains deferred platform key $key"
  else
    pass "Signal-Info.plist omits deferred platform key $key"
  fi
done

if /usr/libexec/PlistBuddy -c 'Print :NSContactsUsageDescription' Signal/Signal-Info.plist >/dev/null 2>&1; then
  fail "App Store Lite must not request Contacts permission"
elif rg -q '^\s*NSContactsUsageDescription\s*=' Signal/translations/*/InfoPlist.strings; then
  fail "a localized InfoPlist.strings still declares Contacts permission"
else
  pass "App Store Lite has no base or localized Contacts permission purpose string"
fi

if rg -i -q 'support\.signal\.org|signal private messenger' \
  Signal/Backups Signal/translations/*/InfoPlist.strings; then
  fail "reachable backup or permission copy still contains upstream Signal branding"
else
  pass "backup and permission copy has no upstream Signal branding"
fi

ug_product_brand_leaks="$(
  rg -n 'سىگنال' Signal/translations/ug.lproj/Localizable.strings \
    | rg -v 'ONBOARDING_VERIFICATION_HELP_BULLET_1' || true
)"
if rg -q 'Signal을' Signal/translations/ko.lproj/PluralAware.stringsdict ||
  [[ -n "$ug_product_brand_leaks" ]]; then
  fail "known long-tail localizations still expose the upstream product name"
else
  pass "known long-tail localization brand leaks are removed"
fi

if [[ "$freeze_mode" == true || "$submission_mode" == true ]]; then
  verify_frozen_source
fi

if [[ "$submission_mode" == true ]]; then
  main_export_answer="$(/usr/libexec/PlistBuddy -c 'Print :ITSAppUsesNonExemptEncryption' Signal/Signal-Info.plist 2>/dev/null || true)"
  nse_export_answer="$(/usr/libexec/PlistBuddy -c 'Print :ITSAppUsesNonExemptEncryption' SignalNSE/Info.plist 2>/dev/null || true)"
  share_export_answer="$(/usr/libexec/PlistBuddy -c 'Print :ITSAppUsesNonExemptEncryption' SignalShareExtension/Info.plist 2>/dev/null || true)"
  if [[ "$main_export_answer" =~ ^(true|false)$ &&
    "$nse_export_answer" == "$main_export_answer" &&
    "$share_export_answer" == "$main_export_answer" ]]; then
    pass "submission plists contain a consistent explicit export-compliance answer"
  else
    fail "submission plists have no consistent authorized export-compliance answer"
  fi

  if [[ "$release_version" == 1.0.0 && "$release_build" == 14 ]]; then
    pass "submission candidate is the controlled Kivo 1.0.0 (14) release"
  else
    fail "submission candidate is $release_version ($release_build), expected the controlled 1.0.0 (14) release"
  fi

  submission_approval_files=(
    release/app-store/age-rating-recommendation.md
    release/app-store/app-review-notes.md
    release/app-store/app-store-connect-fields.md
    release/app-store/asset-rights-inventory.md
    release/app-store/export-compliance.md
    release/app-store/screenshot-plan.md
    release/app-store/ugc-safety-operations.md
  )
  submission_placeholders="$(rg -n '\{\{REQUIRED_[A-Z0-9_]+\}\}' "${submission_approval_files[@]}" || true)"
  if [[ -n "$submission_placeholders" ]]; then
    fail "submission approval records still contain REQUIRED placeholders: $(head -n 12 <<<"$submission_placeholders" | tr '\n' ' ')"
  else
    pass "submission approval records contain no REQUIRED placeholders"
  fi

  review_notes_text="$(awk '
    /^```text$/ { if (!inside) { inside=1; next } }
    /^```$/ && inside { exit }
    inside { print }
  ' release/app-store/app-review-notes.md)"
  review_notes_character_count="$(printf '%s' "$review_notes_text" | LC_ALL=en_US.UTF-8 wc -m | tr -d ' ')"
  if [[ "$review_notes_character_count" =~ ^[0-9]+$ ]] &&
    ((review_notes_character_count > 0 && review_notes_character_count <= 4000)); then
    pass "reviewer-ready App Review Notes fit the 4,000-character limit ($review_notes_character_count)"
  else
    fail "reviewer-ready App Review Notes contain $review_notes_character_count characters (expected 1-4000)"
  fi

  deployed_addstickers="$(curl --fail --location --silent --show-error --max-time 15 \
    https://kivo.it389.com/addstickers/ || true)"
  if grep -F -q 'Sticker-pack links are not available' <<<"$deployed_addstickers" &&
    ! grep -E -q 'data-open-kivo|preview the pack before installing|Discover and install' \
      <<<"$deployed_addstickers"; then
    pass "deployed sticker page matches the submitted Lite client"
  else
    fail "deployed sticker page still advertises a flow absent from the submitted Lite client"
  fi

  exact_tag="kivo-ios-${release_version}-build${release_build}"
  if [[ "$(git rev-parse -q --verify "refs/tags/$exact_tag^{commit}" 2>/dev/null || true)" == "$(git rev-parse HEAD)" ]]; then
    if git verify-tag "$exact_tag" >/dev/null 2>&1; then
      pass "submission release tag $exact_tag has a valid cryptographic signature"
    elif [[ "$(git cat-file -t "refs/tags/$exact_tag" 2>/dev/null || true)" == tag ]]; then
      info "submission release tag $exact_tag is annotated but unsigned; the public archive checksum is the distribution integrity record"
    else
      fail "submission release tag $exact_tag is not an annotated tag"
    fi

    public_source_page="$(curl --fail --location --silent --show-error --max-time 15 \
      https://kivo.it389.com/source/ || true)"
    exact_commit="$(git rev-parse HEAD)"
    source_archive_name="Kivo-iOS-${release_version}-build${release_build}-source-$(git rev-parse --short=10 HEAD).tar.gz"
    source_archive_url="https://kivo.it389.com/source/releases/$source_archive_name"
    public_source_verified=false
    if grep -F -q "$exact_tag" <<<"$public_source_page" &&
      grep -F -q "$exact_commit" <<<"$public_source_page" &&
      grep -F -q "$source_archive_name" <<<"$public_source_page"; then
      pass "public source page maps $exact_tag and $exact_commit to $source_archive_name"

      public_source_sha="$(curl --fail --location --silent --show-error --max-time 120 \
        "$source_archive_url" | shasum -a 256 | awk '{print $1}' || true)"
      if [[ "$public_source_sha" =~ ^[0-9a-f]{64}$ ]] &&
        grep -F -q "$public_source_sha" <<<"$public_source_page"; then
        pass "public corresponding-source archive is downloadable and matches its published SHA-256"
        public_source_verified=true
      else
        fail "public corresponding-source archive is unavailable or does not match the checksum on the source page"
      fi
    else
      fail "public source page does not map the exact tag, commit, and source archive"
    fi
  else
    fail "HEAD is not exactly tagged as $exact_tag"
    public_source_verified=false
  fi

  if git branch -r --contains HEAD | grep -Eq '^ *origin/'; then
    pass "HEAD is present on an origin branch"
  elif [[ "$public_source_verified" == true ]]; then
    pass "exact source is publicly distributed from Kivo's HTTPS archive (a public Git branch is not the selected source-offer method)"
  else
    fail "HEAD has neither a public origin branch nor a verified Kivo corresponding-source archive"
  fi

  if command -v dig >/dev/null && [[ -n "$(dig +short MX zhiera.com)" ]]; then
    pass "zhiera.com publishes an MX record for support email"
  else
    fail "zhiera.com has no verified MX record for business@zhiera.com"
  fi

  public_privacy_page="$(curl --fail --location --silent --show-error --max-time 15 \
    https://kivo.it389.com/privacy/ || true)"
  public_support_page="$(curl --fail --location --silent --show-error --max-time 15 \
    https://kivo.it389.com/support/ || true)"
  if grep -F -q 'structured in-app safety reports expire no later than 90 days' \
      <<<"$public_privacy_page" &&
    grep -F -q 'Report a Safety Issue' <<<"$public_support_page"; then
    pass "published privacy/support pages match the submitted safety-report contract"
  else
    fail "published privacy/support pages do not match the submitted safety-report contract"
  fi
fi

if [[ -n "$archive_path" ]]; then
  if [[ -d "$archive_path" ]]; then
    verify_release_app "$archive_path/Products/Applications/Kivo.app" "archive"
  else
    fail "archive path does not exist: $archive_path"
  fi
fi

if [[ -n "$ipa_path" ]]; then
  if [[ ! -f "$ipa_path" ]]; then
    fail "IPA path does not exist: $ipa_path"
  else
    ipa_temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/kivo-ipa-verify.XXXXXX")"
    mkdir -p "$ipa_temp_dir/extracted"
    if unzip -q "$ipa_path" -d "$ipa_temp_dir/extracted"; then
      ipa_app_count="$(find "$ipa_temp_dir/extracted/Payload" \
        -mindepth 1 -maxdepth 1 -type d -name '*.app' 2>/dev/null \
        | wc -l \
        | tr -d ' ')"
      if [[ "$ipa_app_count" == 1 ]]; then
        ipa_app="$(find "$ipa_temp_dir/extracted/Payload" \
          -mindepth 1 -maxdepth 1 -type d -name '*.app' \
          | head -n 1)"
        verify_release_app "$ipa_app" "IPA"
      else
        fail "IPA contains $ipa_app_count top-level app bundles (expected exactly 1)"
      fi
    else
      fail "could not extract IPA: $ipa_path"
    fi
  fi
fi

if [[ -n "$scan_app_path" ]]; then
  if [[ -d "$scan_app_path" ]]; then
    verify_privacy_manifests "$scan_app_path" "unsigned App Store Release product"
    scan_release_artifact "$scan_app_path" "unsigned App Store Release product"
  else
    fail "product scan app path does not exist: $scan_app_path"
  fi
fi

if ((failures)); then
  printf '\nKivo App Store preflight failed with %d issue(s).\n' "$failures" >&2
  exit 1
fi

printf '\nKivo App Store preflight passed.\n'
