// iOS privacy-sensitive APIs and the Info.plist purpose string each one needs (Apple: "Requesting access to protected // resources"; a missing string makes the app crash on the first request and App Store Connect rejects the upload // with ITMS-90683). `code` finds the API in app sources, `packages` the npm / pub / Unity packages that call it. // Checked 2026-10-11 against developer.apple.com/documentation/bundleresources/information-property-list. export interface PermissionDef { /** Info.plist keys; any one of them satisfies the check (e.g. calendars before / since iOS 17). */ keys: string[]; what: string; code: RegExp; packages: string[]; } export const IOS_PERMISSIONS: PermissionDef[] = [ { keys: ['NSCameraUsageDescription'], what: 'camera', code: /\bAVCaptureDevice\b|sourceType\s*=\s*\.camera|UIImagePickerController\.SourceType\.camera|\bWebCamTexture\b|\bVNDocumentCameraViewController\b/, packages: ['expo-camera', 'react-native-vision-camera', 'react-native-camera', '@capacitor/camera', 'image_picker', 'camera', 'mobile_scanner', 'expo-image-picker', 'react-native-image-picker', 'cordova-plugin-camera', '@capacitor-mlkit/barcode-scanning'] }, { keys: ['NSMicrophoneUsageDescription'], what: 'microphone', code: /\bAVAudioRecorder\b|requestRecordPermission|\.record\b.*AVAudioSession|AVAudioApplication\.requestRecordPermission|\bMicrophone\.Start\b|\bAudioEffectRecord\b/, packages: ['expo-av', 'react-native-audio-recorder-player', 'record', 'flutter_sound', 'cordova-plugin-media-capture', '@capacitor-community/voice-recorder'] }, { keys: ['NSPhotoLibraryUsageDescription'], what: 'photo library', code: /\bPHPhotoLibrary\.(requestAuthorization|shared)|\bPHAsset\.fetch|\bALAssetsLibrary\b/, packages: ['expo-media-library', 'photo_manager', '@react-native-camera-roll/camera-roll', 'image_picker', '@capacitor/camera'] }, { keys: ['NSPhotoLibraryAddUsageDescription', 'NSPhotoLibraryUsageDescription'], what: 'saving to the photo library', code: /UIImageWriteToSavedPhotosAlbum|UISaveVideoAtPathToSavedPhotosAlbum|PHAssetChangeRequest|PHAssetCreationRequest/, packages: ['gallery_saver', 'image_gallery_saver'] }, { keys: ['NSLocationWhenInUseUsageDescription', 'NSLocationAlwaysAndWhenInUseUsageDescription'], what: 'location', code: /\bCLLocationManager\b|requestWhenInUseAuthorization|requestAlwaysAuthorization|\bInput\.location\.Start\b|\bCLLocationUpdate\b/, packages: ['expo-location', '@capacitor/geolocation', 'geolocator', 'location', '@react-native-community/geolocation', 'react-native-geolocation-service', 'cordova-plugin-geolocation'] }, { keys: ['NSContactsUsageDescription'], what: 'contacts', code: /\bCNContactStore\b|\bABAddressBook/, packages: ['expo-contacts', 'react-native-contacts', 'flutter_contacts', 'contacts_service', '@capacitor-community/contacts', 'cordova-plugin-contacts'] }, { keys: ['NSCalendarsFullAccessUsageDescription', 'NSCalendarsWriteOnlyAccessUsageDescription', 'NSCalendarsUsageDescription'], what: 'calendars', code: /\bEKEventStore\b/, packages: ['expo-calendar', 'device_calendar', 'react-native-calendar-events'] }, { keys: ['NSRemindersFullAccessUsageDescription', 'NSRemindersUsageDescription'], what: 'reminders', code: /requestFullAccessToReminders|entityType:\s*\.reminder|EKEntityType\.reminder/, packages: [] }, { keys: ['NSBluetoothAlwaysUsageDescription'], what: 'Bluetooth', code: /\bCBCentralManager\b|\bCBPeripheralManager\b/, packages: ['react-native-ble-plx', 'flutter_blue_plus', 'flutter_blue', '@capacitor-community/bluetooth-le', 'cordova-plugin-ble-central'] }, { keys: ['NSFaceIDUsageDescription'], what: 'Face ID', code: /deviceOwnerAuthenticationWithBiometrics|evaluatePolicy\(/, packages: ['expo-local-authentication', 'local_auth', 'react-native-biometrics', '@capgo/capacitor-native-biometric'] }, { keys: ['NSMotionUsageDescription'], what: 'motion and fitness', code: /\bCMMotionActivityManager\b|\bCMPedometer\b|\bCMAltimeter\b/, packages: ['expo-sensors'] }, { keys: ['NSSpeechRecognitionUsageDescription'], what: 'speech recognition', code: /\bSFSpeechRecognizer\b/, packages: ['@react-native-voice/voice', 'speech_to_text', 'expo-speech-recognition'] }, { keys: ['NSHealthShareUsageDescription'], what: 'Health data', code: /\bHKHealthStore\b/, packages: ['react-native-health', 'health', '@kingstinct/react-native-healthkit'] }, { keys: ['NSUserTrackingUsageDescription'], what: 'App Tracking Transparency', code: /requestTrackingAuthorization|ATTrackingStatusBinding\.RequestAuthorizationTracking/, packages: ['expo-tracking-transparency', 'react-native-tracking-transparency', 'app_tracking_transparency', '@capacitor-community/app-tracking-transparency', 'com.unity.ads.ios-support'] }, ]; /** A purpose string that says nothing (empty, a placeholder, or a generic template Apple rejects under 5.1.1(ii)). */ export function vaguePurpose(s: string): 'empty' | 'vague' | null { const t = s.trim(); if (!t) return 'empty'; if (/^(todo|tbd|fixme|placeholder|lorem|test|xxx+|\.+|-+|description|usage description|\$\([A-Z_]+\))$/i.test(t)) return 'empty'; // Expo / plugin defaults like "Allow $(PRODUCT_NAME) to access your camera" name no reason if (t.length < 20 || /^(allow|let) \$\(PRODUCT_NAME\) to access your [a-z ]+\.?$/i.test(t) || /^(this app|we) (needs?|requires?|uses?) (access to )?(your )?[a-z ]+\.?$/i.test(t)) return 'vague'; return null; }