appship-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gunwoo Ko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # AppShip
2
+
3
+ > AppShip is an open-source AI release assistant that analyzes your mobile app and prepares everything required for App Store and Google Play submission.
4
+
5
+ Ship your mobile app without spending hours inside App Store Connect and Google Play Console.
6
+
7
+ **Status: MVP 3 implemented (early development).** React Native, Expo, Flutter, native iOS, and native Android projects. See [docs/PRD.md](./docs/PRD.md) and [docs/TRD.md](./docs/TRD.md) for the full plan.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g appship-cli # installs the `appship` command
13
+ appship --help
14
+
15
+ # or run one-off without installing
16
+ npx appship-cli init
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ ```bash
22
+ appship init # analyze your React Native project, ask a few questions
23
+ appship generate # generate store metadata, privacy docs, legal drafts into .appship/
24
+ appship localize ko-KR ja-JP # translate generated listings into more locales
25
+ appship export fastlane # export listings to fastlane deliver/supply layouts (offline)
26
+ appship export ci # generate GitHub Actions workflows (readiness gate + release)
27
+ appship upload ios # upload a built .ipa to TestFlight via fastlane
28
+ appship upload android # upload a built .aab to a Play track (default: internal)
29
+ appship screenshots flows # turn the screenshot plan into Maestro flows (offline)
30
+ appship screenshots capture # run the flows on a device/emulator via Maestro
31
+ appship submit ios # submit the latest uploaded build for App Store review
32
+ appship submit android # promote a tested Play track to production review
33
+ appship doctor # check submission readiness and rejection risks (offline)
34
+ appship review analyze r.txt # turn a store rejection message into a fix plan
35
+ appship rules update # fetch the latest policy rules & SDK signatures
36
+ ```
37
+
38
+ `export fastlane` maps everything under `.appship/` into `fastlane/metadata/` (deliver) and `fastlane/metadata/android/` (supply), plus starter `Appfile`/`Fastfile` upload lanes — existing fastlane files are never overwritten without `--force`. Upload with `bundle exec fastlane ios upload_metadata` / `... android upload_metadata`.
39
+
40
+ `export ci` writes two GitHub Actions workflows: a readiness gate that runs `appship doctor` on every PR (works out of the box — doctor is offline), and a `workflow_dispatch` release pipeline that builds, uploads via `appship upload --yes`, and optionally submits. The build steps are project-specific, so they carry `TODO(appship)` markers for you to finish (Android is pre-filled for gradle/Expo prebuild; iOS signing needs your fastlane match setup). Existing workflow files are never overwritten without `--force`.
41
+
42
+ `upload` finds your newest built artifact (or takes `--ipa`/`--aab`), shows exactly what will run, asks for confirmation (`--yes` to skip, required in CI), then delegates to the fastlane lanes from `export fastlane`. fastlane owns store auth — set up an App Store Connect API key / Play service account per fastlane's docs.
43
+
44
+ `submit` is the confirmation-gated final step. It first runs the doctor checks for the target store and refuses on error-severity failures (`--force` to override), shows exactly what will run, then asks for confirmation (`--yes` to skip, required in CI). iOS submits the latest build already on App Store Connect (`--build-number` to pick one) with manual release after approval; Android promotes a tested track (`--from-track`, default `internal`) to `--track` (default `production`). Like `upload`, it delegates to the `:submit_review` fastlane lanes from `export fastlane` — re-export with `--force` if your Fastfile predates them.
45
+
46
+ `screenshots flows` generates one [Maestro](https://maestro.mobile.dev) flow per screen in the plan; each flow carries a navigation TODO that only you can fill in (appship can't know how to reach your screens). `capture` refuses to run while TODOs remain, then runs `maestro test` and drops PNGs into `.appship/screenshots/raw/`. Your edited flows are never overwritten without `--force`.
47
+
48
+ `review analyze` reads a rejection message you copied from App Store Connect / Play Console into a text file, and produces per-issue fix steps, the appship commands that help, and (for issues resolvable by replying) a draft response to the review team — written to `.appship/review/rejection-analysis.md`. Guidelines are only cited when the message cites them; nothing is invented beyond the message and the scanned project facts.
49
+
50
+ Store policies change between releases, so the doctor rules and SDK signature database are plain data files that can be refreshed without a new CLI version: `appship rules update` downloads them from this repository into `~/.appship/data-cache` (schema-validated before anything is written; nothing partial is ever observable), `rules status` shows what is in use, and `rules reset` returns to the bundled files. If a cached file ever stops parsing (e.g. you run a much newer rule format with an old CLI), appship silently falls back to its bundled data.
51
+
52
+ `generate`, `localize`, and `review analyze` call an AI provider — Anthropic by default (`ANTHROPIC_API_KEY` or `ant auth login`), and any OpenAI-compatible endpoint via `ai.provider` in appship.yml: `gemini` (free keys at [aistudio.google.com](https://aistudio.google.com/apikey), `GEMINI_API_KEY`, default model `gemini-2.5-flash`), `openai` (`OPENAI_API_KEY`), `ollama` (local, set `ai.model`), or `openai-compatible` (set `ai.base_url` + `ai.model` — works with OpenRouter, Groq, and self-hosted servers). Store character limits are validated with retry regardless of provider. `init` and `doctor` run fully offline. Translations go through the same validation as generation; App Review notes are copied, not translated.
53
+
54
+ Supported projects: React Native CLI, Expo (managed), Flutter, native iOS (`.xcodeproj` at the root), and native Android (`settings.gradle` + `app/` module) — identity and permissions are read from native files (`Info.plist`, `AndroidManifest.xml`, `project.pbxproj`, `build.gradle`), from `app.json`'s `expo` config, or from `pubspec.yaml`. SDK detection matches npm packages, Dart/Flutter packages, CocoaPods, and Gradle Maven coordinates, plus source patterns in JS/TS, Dart, Swift, Kotlin, and Java.
55
+
56
+ ## Using doctor as a CI gate
57
+
58
+ `appship doctor` exits with code 1 when any error-severity check fails, so it works as a release gate:
59
+
60
+ ```yaml
61
+ # .github/workflows/release-readiness.yml
62
+ name: Release readiness
63
+ on: [pull_request]
64
+ jobs:
65
+ doctor:
66
+ runs-on: ubuntu-latest
67
+ steps:
68
+ - uses: actions/checkout@v4
69
+ - uses: actions/setup-node@v4
70
+ with: { node-version: 22 }
71
+ - run: npx appship-cli doctor # fails the job on ✗ errors
72
+ ```
73
+
74
+ Add `--json` for machine-readable output; the full report is also written to `.appship/checklist/release-readiness.json`.
75
+
76
+ ## Principles
77
+
78
+ 1. **AI never invents facts** — every finding carries evidence (file paths), and anything code can't prove is confirmed by you, not assumed.
79
+ 2. **Your source code stays local** — scanning happens on your machine; only a summary (project type, permissions, SDK list) is sent to the AI provider.
80
+ 3. **Nothing is submitted without explicit confirmation.**
81
+
82
+ ## Development
83
+
84
+ ```bash
85
+ npm install
86
+ npm run dev -- --help # run the CLI
87
+ npm test # run tests
88
+ npm run typecheck
89
+ ```
90
+
91
+ ## License
92
+
93
+ MIT
@@ -0,0 +1,47 @@
1
+ # doctor rules for the App Store (TRD §7).
2
+ # severity: error -> ✗ (weight 3), warning -> ⚠ (weight 1)
3
+
4
+ - id: apple-name-length
5
+ store: app-store
6
+ category: length
7
+ severity: error
8
+ target: app-store/*/name.txt
9
+ check: { type: max_length, value: 30 }
10
+ message: "App name exceeds Apple's 30 character limit"
11
+
12
+ - id: apple-subtitle-length
13
+ store: app-store
14
+ category: length
15
+ severity: error
16
+ target: app-store/*/subtitle.txt
17
+ check: { type: max_length, value: 30 }
18
+ message: "Subtitle exceeds Apple's 30 character limit"
19
+
20
+ - id: apple-keywords-length
21
+ store: app-store
22
+ category: length
23
+ severity: error
24
+ target: app-store/*/keywords.txt
25
+ check: { type: max_length, value: 100 }
26
+ message: "Keywords exceed Apple's 100 character limit"
27
+
28
+ - id: account-deletion-required
29
+ store: app-store
30
+ category: consistency
31
+ severity: error
32
+ condition: { finding: login_detected }
33
+ check: { type: file_exists, value: legal/account-deletion.md }
34
+ message: "User registration detected but no account deletion flow found"
35
+ guideline: "Apple Guideline 5.1.1"
36
+ fix_suggestions:
37
+ - "Add account deletion under Settings > Account"
38
+ - "Mention the deletion path in App Review Notes"
39
+
40
+ - id: unresolved-confirm-placeholders
41
+ store: app-store
42
+ category: consistency
43
+ severity: error
44
+ target: "**/*.md"
45
+ check: { type: not_contains, value: "[CONFIRM:" }
46
+ message: "Generated document still contains unresolved [CONFIRM] placeholders"
47
+ pass_message: "No unresolved [CONFIRM] placeholders"
@@ -0,0 +1,51 @@
1
+ # doctor rules for Google Play (TRD §7).
2
+
3
+ - id: play-title-length
4
+ store: google-play
5
+ category: length
6
+ severity: error
7
+ target: google-play/*/title.txt
8
+ check: { type: max_length, value: 30 }
9
+ message: "App title exceeds Google Play's 30 character limit"
10
+
11
+ - id: play-short-description-length
12
+ store: google-play
13
+ category: length
14
+ severity: error
15
+ target: google-play/*/short-description.txt
16
+ check: { type: max_length, value: 80 }
17
+ message: "Short description exceeds Google Play's 80 character limit"
18
+
19
+ - id: play-full-description-length
20
+ store: google-play
21
+ category: length
22
+ severity: error
23
+ target: google-play/*/full-description.txt
24
+ check: { type: max_length, value: 4000 }
25
+ message: "Full description exceeds Google Play's 4000 character limit"
26
+
27
+ - id: play-no-emoji-in-title
28
+ store: google-play
29
+ category: policy
30
+ severity: warning
31
+ target: google-play/*/title.txt
32
+ check: { type: no_emoji }
33
+ message: "Emoji in the title may violate Google Play metadata policy"
34
+
35
+ - id: data-safety-confirm-markers
36
+ store: google-play
37
+ category: consistency
38
+ severity: error
39
+ target: google-play/data-safety.yml
40
+ check: { type: not_contains, value: "confirm_before_submitting" }
41
+ message: "data-safety.yml still contains confirm_before_submitting markers — resolve them before filling the Play Console form"
42
+ pass_message: "Data Safety confirm markers resolved"
43
+
44
+ - id: play-unresolved-confirm-placeholders
45
+ store: google-play
46
+ category: consistency
47
+ severity: error
48
+ target: legal/*.md
49
+ check: { type: not_contains, value: "[CONFIRM:" }
50
+ message: "Generated legal documents still contain unresolved [CONFIRM] placeholders"
51
+ pass_message: "No unresolved [CONFIRM] placeholders in legal documents"
@@ -0,0 +1,376 @@
1
+ # SDK signature database (TRD §6).
2
+ # Declarative — adding a signature must never require a code change.
3
+ #
4
+ # confidence rules applied by the scanner:
5
+ # dependencies match -> high
6
+ # source_patterns only -> medium
7
+ # config_keys only -> low
8
+
9
+ - id: firebase-analytics
10
+ name: Firebase Analytics
11
+ category: analytics
12
+ detect:
13
+ dependencies:
14
+ - "@react-native-firebase/analytics"
15
+ - "firebase_analytics"
16
+ - "FirebaseAnalytics"
17
+ - "com.google.firebase:firebase-analytics"
18
+ source_patterns:
19
+ - "analytics\\(\\)\\.logEvent"
20
+ - "FirebaseAnalytics\\.instance"
21
+ - "Analytics\\.logEvent"
22
+ - "Firebase\\.analytics"
23
+ config_keys: []
24
+ data_safety:
25
+ collects: [device_id, app_interactions]
26
+ purpose_defaults: [analytics]
27
+ shared_default: true
28
+ requires_confirmation: false
29
+
30
+ - id: sentry
31
+ name: Sentry
32
+ category: crash-reporting
33
+ detect:
34
+ dependencies:
35
+ - "@sentry/react-native"
36
+ - "sentry_flutter"
37
+ - "Sentry"
38
+ - "io.sentry:sentry-android"
39
+ source_patterns:
40
+ - "Sentry\\.init"
41
+ - "SentryFlutter\\.init"
42
+ - "SentrySDK\\.start"
43
+ config_keys: []
44
+ data_safety:
45
+ collects: [crash_logs, diagnostics, device_id]
46
+ purpose_defaults: [app_functionality]
47
+ shared_default: true
48
+ requires_confirmation: false
49
+
50
+ - id: revenuecat
51
+ name: RevenueCat
52
+ category: payments
53
+ detect:
54
+ dependencies:
55
+ - "react-native-purchases"
56
+ - "purchases_flutter"
57
+ - "RevenueCat"
58
+ - "com.revenuecat.purchases:purchases"
59
+ source_patterns:
60
+ - "Purchases\\.configure"
61
+ config_keys: []
62
+ data_safety:
63
+ collects: [purchase_history]
64
+ purpose_defaults: [app_functionality]
65
+ shared_default: true
66
+ requires_confirmation: false
67
+
68
+ - id: location
69
+ name: Location APIs
70
+ category: device-api
71
+ detect:
72
+ dependencies:
73
+ - "@react-native-community/geolocation"
74
+ - "react-native-geolocation-service"
75
+ - "geolocator"
76
+ - "location"
77
+ source_patterns:
78
+ - "Geolocation\\.getCurrentPosition"
79
+ - "watchPosition"
80
+ - "Geolocator\\."
81
+ config_keys:
82
+ - NSLocationWhenInUseUsageDescription
83
+ - android.permission.ACCESS_FINE_LOCATION
84
+ - android.permission.ACCESS_COARSE_LOCATION
85
+ data_safety:
86
+ collects: [location]
87
+ purpose_defaults: [app_functionality]
88
+ shared_default: false
89
+ # precise vs approximate vs local-only cannot be decided from code
90
+ requires_confirmation: true
91
+
92
+ - id: amplitude
93
+ name: Amplitude
94
+ category: analytics
95
+ detect:
96
+ dependencies:
97
+ - "@amplitude/analytics-react-native"
98
+ - "@amplitude/react-native"
99
+ - "amplitude_flutter"
100
+ - "AmplitudeSwift"
101
+ - "com.amplitude:analytics-android"
102
+ source_patterns:
103
+ - "amplitude\\.(init|track)"
104
+ config_keys: []
105
+ data_safety:
106
+ collects: [device_id, app_interactions]
107
+ purpose_defaults: [analytics]
108
+ shared_default: true
109
+ requires_confirmation: false
110
+
111
+ - id: mixpanel
112
+ name: Mixpanel
113
+ category: analytics
114
+ detect:
115
+ dependencies:
116
+ - "mixpanel-react-native"
117
+ - "mixpanel_flutter"
118
+ - "Mixpanel-swift"
119
+ - "com.mixpanel.android:mixpanel-android"
120
+ source_patterns:
121
+ - "new Mixpanel\\("
122
+ - "Mixpanel\\.initialize"
123
+ config_keys: []
124
+ data_safety:
125
+ collects: [device_id, app_interactions]
126
+ purpose_defaults: [analytics]
127
+ shared_default: true
128
+ requires_confirmation: false
129
+
130
+ - id: firebase-crashlytics
131
+ name: Firebase Crashlytics
132
+ category: crash-reporting
133
+ detect:
134
+ dependencies:
135
+ - "@react-native-firebase/crashlytics"
136
+ - "firebase_crashlytics"
137
+ - "FirebaseCrashlytics"
138
+ - "com.google.firebase:firebase-crashlytics"
139
+ source_patterns:
140
+ - "crashlytics\\(\\)"
141
+ - "FirebaseCrashlytics\\.instance"
142
+ - "Crashlytics\\.crashlytics"
143
+ config_keys: []
144
+ data_safety:
145
+ collects: [crash_logs, diagnostics, device_id]
146
+ purpose_defaults: [app_functionality]
147
+ shared_default: true
148
+ requires_confirmation: false
149
+
150
+ - id: google-signin
151
+ name: Google Sign-In
152
+ category: auth
153
+ detect:
154
+ dependencies:
155
+ - "@react-native-google-signin/google-signin"
156
+ - "google_sign_in"
157
+ - "GoogleSignIn"
158
+ - "com.google.android.gms:play-services-auth"
159
+ source_patterns:
160
+ - "GoogleSignin\\."
161
+ - "GoogleSignIn\\("
162
+ config_keys: []
163
+ data_safety:
164
+ collects: [email, name, account_info]
165
+ purpose_defaults: [app_functionality]
166
+ shared_default: true
167
+ requires_confirmation: false
168
+
169
+ - id: apple-signin
170
+ name: Apple Sign-In
171
+ category: auth
172
+ detect:
173
+ dependencies:
174
+ - "@invertase/react-native-apple-authentication"
175
+ - "sign_in_with_apple"
176
+ source_patterns:
177
+ - "appleAuth\\."
178
+ - "SignInWithApple\\."
179
+ - "ASAuthorizationAppleIDProvider"
180
+ config_keys: []
181
+ data_safety:
182
+ collects: [email, name]
183
+ purpose_defaults: [app_functionality]
184
+ shared_default: false
185
+ requires_confirmation: false
186
+
187
+ - id: facebook-sdk
188
+ name: Facebook SDK
189
+ category: auth
190
+ detect:
191
+ dependencies:
192
+ - "react-native-fbsdk-next"
193
+ - "react-native-fbsdk"
194
+ - "flutter_facebook_auth"
195
+ - "facebook_app_events"
196
+ - "FacebookLogin"
197
+ - "com.facebook.android:facebook-android-sdk"
198
+ source_patterns:
199
+ - "LoginManager\\.logInWithPermissions"
200
+ config_keys: []
201
+ data_safety:
202
+ collects: [device_id, app_interactions, account_info]
203
+ purpose_defaults: [app_functionality, advertising]
204
+ shared_default: true
205
+ requires_confirmation: false
206
+
207
+ - id: stripe
208
+ name: Stripe
209
+ category: payments
210
+ detect:
211
+ dependencies:
212
+ - "@stripe/stripe-react-native"
213
+ - "flutter_stripe"
214
+ - "StripePaymentSheet"
215
+ - "com.stripe:stripe-android"
216
+ source_patterns:
217
+ - "useStripe\\(|initPaymentSheet"
218
+ config_keys: []
219
+ data_safety:
220
+ collects: [payment_info, purchase_history]
221
+ purpose_defaults: [app_functionality]
222
+ shared_default: true
223
+ requires_confirmation: false
224
+
225
+ - id: admob
226
+ name: AdMob
227
+ category: ads
228
+ detect:
229
+ dependencies:
230
+ - "react-native-google-mobile-ads"
231
+ - "google_mobile_ads"
232
+ - "Google-Mobile-Ads-SDK"
233
+ - "com.google.android.gms:play-services-ads"
234
+ source_patterns:
235
+ - "BannerAd|InterstitialAd|RewardedAd"
236
+ - "MobileAds\\.instance"
237
+ - "GADMobileAds|MobileAds\\.initialize"
238
+ config_keys: []
239
+ data_safety:
240
+ collects: [advertising_id, device_id]
241
+ purpose_defaults: [advertising]
242
+ shared_default: true
243
+ requires_confirmation: false
244
+
245
+ - id: appsflyer
246
+ name: AppsFlyer
247
+ category: ads
248
+ detect:
249
+ dependencies:
250
+ - "react-native-appsflyer"
251
+ - "appsflyer_sdk"
252
+ - "AppsFlyerFramework"
253
+ - "com.appsflyer:af-android-sdk"
254
+ source_patterns:
255
+ - "appsFlyer\\."
256
+ - "AppsFlyerLib"
257
+ config_keys: []
258
+ data_safety:
259
+ collects: [advertising_id, device_id, app_interactions]
260
+ purpose_defaults: [analytics, advertising]
261
+ shared_default: true
262
+ requires_confirmation: false
263
+
264
+ - id: onesignal
265
+ name: OneSignal
266
+ category: push
267
+ detect:
268
+ dependencies:
269
+ - "react-native-onesignal"
270
+ - "onesignal_flutter"
271
+ - "OneSignalXCFramework"
272
+ - "com.onesignal:OneSignal"
273
+ source_patterns:
274
+ - "OneSignal\\."
275
+ config_keys: []
276
+ data_safety:
277
+ collects: [device_id]
278
+ purpose_defaults: [app_functionality]
279
+ shared_default: true
280
+ requires_confirmation: false
281
+
282
+ - id: firebase-messaging
283
+ name: Firebase Messaging
284
+ category: push
285
+ detect:
286
+ dependencies:
287
+ - "@react-native-firebase/messaging"
288
+ - "firebase_messaging"
289
+ - "FirebaseMessaging"
290
+ - "com.google.firebase:firebase-messaging"
291
+ source_patterns:
292
+ - "messaging\\(\\)\\.(getToken|onMessage)"
293
+ - "FirebaseMessaging\\.instance"
294
+ - "Messaging\\.messaging"
295
+ config_keys: []
296
+ data_safety:
297
+ collects: [device_id]
298
+ purpose_defaults: [app_functionality]
299
+ shared_default: true
300
+ requires_confirmation: false
301
+
302
+ - id: microphone
303
+ name: Microphone
304
+ category: device-api
305
+ detect:
306
+ dependencies:
307
+ - "record"
308
+ - "flutter_sound"
309
+ - "speech_to_text"
310
+ source_patterns: []
311
+ config_keys:
312
+ - NSMicrophoneUsageDescription
313
+ - android.permission.RECORD_AUDIO
314
+ data_safety:
315
+ collects: [voice_or_audio]
316
+ purpose_defaults: [app_functionality]
317
+ shared_default: false
318
+ # recorded audio may be processed locally only — confirm
319
+ requires_confirmation: true
320
+
321
+ - id: camera
322
+ name: Camera
323
+ category: device-api
324
+ detect:
325
+ dependencies:
326
+ - "react-native-vision-camera"
327
+ - "react-native-camera"
328
+ - "react-native-image-picker"
329
+ - "camera"
330
+ - "image_picker"
331
+ source_patterns: []
332
+ config_keys:
333
+ - NSCameraUsageDescription
334
+ - android.permission.CAMERA
335
+ data_safety:
336
+ collects: [photos_and_videos]
337
+ purpose_defaults: [app_functionality]
338
+ shared_default: false
339
+ requires_confirmation: true
340
+
341
+ - id: contacts
342
+ name: Contacts
343
+ category: device-api
344
+ detect:
345
+ dependencies:
346
+ - "react-native-contacts"
347
+ - "flutter_contacts"
348
+ source_patterns: []
349
+ config_keys:
350
+ - NSContactsUsageDescription
351
+ - android.permission.READ_CONTACTS
352
+ data_safety:
353
+ collects: [contacts]
354
+ purpose_defaults: [app_functionality]
355
+ shared_default: false
356
+ requires_confirmation: true
357
+
358
+ - id: advertising-id
359
+ name: Advertising ID / Tracking
360
+ category: device-api
361
+ detect:
362
+ dependencies:
363
+ - "react-native-tracking-transparency"
364
+ - "app_tracking_transparency"
365
+ - "com.google.android.gms:play-services-ads-identifier"
366
+ source_patterns:
367
+ - "requestTrackingPermission"
368
+ - "ATTrackingManager"
369
+ config_keys:
370
+ - NSUserTrackingUsageDescription
371
+ - com.google.android.gms.permission.AD_ID
372
+ data_safety:
373
+ collects: [advertising_id]
374
+ purpose_defaults: [advertising]
375
+ shared_default: true
376
+ requires_confirmation: true