@daemux/store-automator 0.10.46 → 0.10.47
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/.claude-plugin/marketplace.json +2 -2
- package/package.json +1 -1
- package/plugins/store-automator/.claude-plugin/plugin.json +1 -1
- package/plugins/store-automator/agents/appstore-meta-creator.md +52 -2
- package/templates/CLAUDE.md.template +13 -4
- package/templates/fastlane/ios/Fastfile.template +9 -0
- package/templates/scripts/ci/ios/upload-privacy.sh +35 -0
- package/templates/scripts/sync_iap_ios.py +27 -11
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "App Store & Google Play automation for Flutter apps",
|
|
8
|
-
"version": "0.10.
|
|
8
|
+
"version": "0.10.47"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "store-automator",
|
|
13
13
|
"source": "./plugins/store-automator",
|
|
14
14
|
"description": "3 agents for app store publishing: reviewer, meta-creator, media-designer",
|
|
15
|
-
"version": "0.10.
|
|
15
|
+
"version": "0.10.47",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"flutter",
|
|
18
18
|
"app-store",
|
package/package.json
CHANGED
|
@@ -15,8 +15,9 @@ You are a senior ASO (App Store Optimization) specialist and localization expert
|
|
|
15
15
|
5. SAVE all files to fastlane/metadata/ in the correct directory structure
|
|
16
16
|
6. GENERATE fastlane/app_rating_config.json based on app content analysis
|
|
17
17
|
7. GENERATE fastlane/data_safety.csv based on app data collection analysis
|
|
18
|
-
8. GENERATE fastlane/
|
|
19
|
-
9.
|
|
18
|
+
8. GENERATE fastlane/app_privacy_details.json based on app data collection analysis (Apple App Privacy)
|
|
19
|
+
9. GENERATE fastlane/metadata/review_information/ contact files for App Store review team
|
|
20
|
+
10. Verify character limits are respected in every language
|
|
20
21
|
|
|
21
22
|
## Files You Create
|
|
22
23
|
|
|
@@ -137,6 +138,52 @@ SECURITY_PRACTICES_DATA_ENCRYPTED_IN_TRANSIT,true
|
|
|
137
138
|
SECURITY_PRACTICES_DATA_DELETION_REQUEST,true
|
|
138
139
|
```
|
|
139
140
|
|
|
141
|
+
### App Privacy Details (fastlane/app_privacy_details.json)
|
|
142
|
+
|
|
143
|
+
Apple App Privacy declarations uploaded via fastlane's `upload_app_privacy_details_to_app_store` action. Analyze the app's data collection (same analysis as data_safety.csv) and generate a JSON array declaring each collected data category, its purposes, and protection level.
|
|
144
|
+
|
|
145
|
+
**Format:** JSON array of objects. Each object has `category`, `purposes` (array), and `data_protections` (array).
|
|
146
|
+
|
|
147
|
+
**If the app does NOT collect any data:**
|
|
148
|
+
```json
|
|
149
|
+
[
|
|
150
|
+
{
|
|
151
|
+
"data_protections": ["DATA_NOT_COLLECTED"]
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**If the app collects data, one entry per data type:**
|
|
157
|
+
```json
|
|
158
|
+
[
|
|
159
|
+
{
|
|
160
|
+
"category": "EMAIL_ADDRESS",
|
|
161
|
+
"purposes": ["APP_FUNCTIONALITY"],
|
|
162
|
+
"data_protections": ["DATA_LINKED_TO_YOU"]
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"category": "CRASH_DATA",
|
|
166
|
+
"purposes": ["APP_FUNCTIONALITY"],
|
|
167
|
+
"data_protections": ["DATA_NOT_LINKED_TO_YOU"]
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Available categories:** `EMAIL_ADDRESS`, `USER_ID`, `NAME`, `PHONE_NUMBER`, `PHYSICAL_ADDRESS`, `OTHER_CONTACT_INFO`, `PURCHASE_HISTORY`, `PAYMENT_INFO`, `CREDIT_INFO`, `OTHER_FINANCIAL_INFO`, `PRECISE_LOCATION`, `COARSE_LOCATION`, `HEALTH`, `FITNESS`, `SENSITIVE_INFO`, `EMAILS_OR_TEXT_MESSAGES`, `PHOTOS_OR_VIDEOS`, `AUDIO_DATA`, `GAMEPLAY_CONTENT`, `CUSTOMER_SUPPORT`, `OTHER_USER_CONTENT`, `BROWSING_HISTORY`, `SEARCH_HISTORY`, `PRODUCT_INTERACTION`, `ADVERTISING_DATA`, `OTHER_USAGE_DATA`, `CRASH_DATA`, `PERFORMANCE_DATA`, `OTHER_DIAGNOSTIC_DATA`, `DEVICE_ID`, `OTHER_DATA_TYPES`.
|
|
173
|
+
|
|
174
|
+
**Available purposes:** `THIRD_PARTY_ADVERTISING`, `DEVELOPERS_ADVERTISING`, `ANALYTICS`, `PRODUCT_PERSONALIZATION`, `APP_FUNCTIONALITY`, `OTHER_PURPOSES`.
|
|
175
|
+
|
|
176
|
+
**Available data_protections:** `DATA_LINKED_TO_YOU`, `DATA_NOT_LINKED_TO_YOU`, `DATA_USED_TO_TRACK_YOU`, `DATA_NOT_COLLECTED`.
|
|
177
|
+
|
|
178
|
+
**Analysis checklist (mirrors data_safety.csv analysis):**
|
|
179
|
+
1. Authentication (firebase_auth) -- EMAIL_ADDRESS, USER_ID (linked, app functionality)
|
|
180
|
+
2. Messaging/content (cloud_firestore) -- OTHER_USER_CONTENT (linked, app functionality)
|
|
181
|
+
3. Payments (in_app_purchase) -- PURCHASE_HISTORY (linked, app functionality)
|
|
182
|
+
4. Analytics (firebase_analytics) -- PRODUCT_INTERACTION, DEVICE_ID (not linked, analytics)
|
|
183
|
+
5. Crash reporting (firebase_crashlytics) -- CRASH_DATA (not linked, app functionality)
|
|
184
|
+
6. Location services -- PRECISE_LOCATION or COARSE_LOCATION
|
|
185
|
+
7. Default to `DATA_NOT_COLLECTED` if no data collection SDKs are found
|
|
186
|
+
|
|
140
187
|
## Apple ASO Guidelines
|
|
141
188
|
|
|
142
189
|
### Name (name.txt)
|
|
@@ -239,6 +286,7 @@ af, am, ar, hy-AM, az-AZ, eu-ES, be, bn-BD, bg, my-MM, ca, zh-HK, zh-CN, zh-TW,
|
|
|
239
286
|
```
|
|
240
287
|
fastlane/
|
|
241
288
|
app_rating_config.json
|
|
289
|
+
app_privacy_details.json
|
|
242
290
|
data_safety.csv
|
|
243
291
|
metadata/
|
|
244
292
|
copyright.txt
|
|
@@ -288,6 +336,8 @@ fastlane/
|
|
|
288
336
|
- data_safety.csv has matching DATA_SHARED entries for every DATA_COLLECTED category
|
|
289
337
|
- data_safety.csv has DATA_USAGE purpose entries for every collected data type
|
|
290
338
|
- data_safety.csv includes SECURITY_PRACTICES entries for encryption and deletion
|
|
339
|
+
- app_privacy_details.json exists and is valid JSON array with category, purposes, and data_protections for each entry
|
|
340
|
+
- app_privacy_details.json categories match the data collection analysis (consistent with data_safety.csv)
|
|
291
341
|
- review_information/ directory has all 7 files (first_name, last_name, phone_number, email_address, demo_user, demo_password, notes)
|
|
292
342
|
- review_information/phone_number.txt uses valid US format (+1 followed by 10 digits with valid area code)
|
|
293
343
|
- review_information/email_address.txt uses the actual domain from ci.config.yaml
|
|
@@ -58,10 +58,19 @@ IAP and subscription configuration. Durations: ONE_WEEK to ONE_YEAR. Intro offer
|
|
|
58
58
|
### fastlane/screenshots/
|
|
59
59
|
Store screenshots by platform and device size. Generated by app-designer via Stitch MCP.
|
|
60
60
|
|
|
61
|
-
### App Privacy
|
|
62
|
-
App Privacy declarations must be set
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
### App Privacy Setup
|
|
62
|
+
App Privacy declarations must be set before the first app submission.
|
|
63
|
+
|
|
64
|
+
**Automated (recommended):**
|
|
65
|
+
1. Generate `fastlane/app_privacy_details.json` (the appstore-meta-creator agent does this)
|
|
66
|
+
2. Run locally: `cd fastlane/ios && bundle exec fastlane upload_privacy_ios`
|
|
67
|
+
3. Requires Apple ID auth (set APPLE_ID and APPLE_TEAM_NAME env vars)
|
|
68
|
+
4. Privacy declarations persist across app updates (one-time setup)
|
|
69
|
+
|
|
70
|
+
**Manual:**
|
|
71
|
+
Go to App Store Connect > Your App > App Privacy and fill out the form.
|
|
72
|
+
|
|
73
|
+
Note: This cannot run in CI with API key auth. Use FASTLANE_SESSION for CI automation (expires ~30 days).
|
|
65
74
|
|
|
66
75
|
### App Icon Setup
|
|
67
76
|
The app icon must be set before the first release. Flutter creates projects with a default icon that MUST be replaced.
|
|
@@ -116,4 +116,13 @@ platform :ios do
|
|
|
116
116
|
script = "#{ROOT_DIR}/scripts/sync_iap_ios.py"
|
|
117
117
|
sh("python3", script, config_path) if File.exist?(config_path) && File.exist?(script)
|
|
118
118
|
end
|
|
119
|
+
|
|
120
|
+
lane :upload_privacy_ios do
|
|
121
|
+
upload_app_privacy_details_to_app_store(
|
|
122
|
+
username: ENV.fetch("APPLE_ID", ""),
|
|
123
|
+
team_name: ENV.fetch("APPLE_TEAM_NAME", ""),
|
|
124
|
+
app_identifier: ENV["BUNDLE_ID"],
|
|
125
|
+
json_path: "#{ROOT_DIR}/fastlane/app_privacy_details.json"
|
|
126
|
+
)
|
|
127
|
+
end
|
|
119
128
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Uploads Apple App Privacy declarations via fastlane.
|
|
3
|
+
# Requires Apple ID auth (not API key), so this typically runs locally.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/../common/read-config.sh"
|
|
8
|
+
source "$SCRIPT_DIR/../common/ci-notify.sh"
|
|
9
|
+
|
|
10
|
+
echo "=== iOS App Privacy Upload ==="
|
|
11
|
+
|
|
12
|
+
# --- Check if privacy JSON exists ---
|
|
13
|
+
PRIVACY_JSON="$PROJECT_ROOT/fastlane/app_privacy_details.json"
|
|
14
|
+
|
|
15
|
+
if [ ! -f "$PRIVACY_JSON" ]; then
|
|
16
|
+
ci_skip "No app_privacy_details.json found at $PRIVACY_JSON"
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo " Found: $PRIVACY_JSON"
|
|
20
|
+
|
|
21
|
+
# --- Privacy upload requires Apple ID auth (not API key) ---
|
|
22
|
+
if [ -z "${APPLE_ID:-}" ]; then
|
|
23
|
+
echo "Skipping privacy upload: APPLE_ID not set (requires Apple ID auth, not API key)"
|
|
24
|
+
echo "Run locally: cd fastlane/ios && bundle exec fastlane upload_privacy_ios"
|
|
25
|
+
ci_skip "APPLE_ID not set (requires Apple ID auth)"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# --- Export required env vars ---
|
|
29
|
+
export BUNDLE_ID="$BUNDLE_ID"
|
|
30
|
+
|
|
31
|
+
echo "Uploading App Privacy details for $BUNDLE_ID..."
|
|
32
|
+
cd "$PROJECT_ROOT/fastlane/ios"
|
|
33
|
+
bundle exec fastlane upload_privacy_ios
|
|
34
|
+
|
|
35
|
+
ci_done "App Privacy declarations uploaded to App Store Connect"
|
|
@@ -21,7 +21,11 @@ import os
|
|
|
21
21
|
import sys
|
|
22
22
|
import time
|
|
23
23
|
|
|
24
|
+
import requests
|
|
25
|
+
|
|
24
26
|
from asc_iap_api import (
|
|
27
|
+
BASE_URL,
|
|
28
|
+
TIMEOUT,
|
|
25
29
|
create_group_localization,
|
|
26
30
|
create_localization,
|
|
27
31
|
create_subscription,
|
|
@@ -32,6 +36,7 @@ from asc_iap_api import (
|
|
|
32
36
|
get_subscription_localizations,
|
|
33
37
|
list_subscription_groups,
|
|
34
38
|
list_subscriptions_in_group,
|
|
39
|
+
print_api_errors,
|
|
35
40
|
update_group_localization,
|
|
36
41
|
update_localization,
|
|
37
42
|
)
|
|
@@ -138,6 +143,18 @@ def sync_subscription_group(
|
|
|
138
143
|
_sync_availability(headers, sub_id, sub_config)
|
|
139
144
|
_sync_pricing(headers, sub_id, sub_config)
|
|
140
145
|
_sync_review_screenshot(headers, sub_id, sub_config, project_root)
|
|
146
|
+
# Patch subscription to trigger Apple's state re-evaluation
|
|
147
|
+
resp = requests.patch(
|
|
148
|
+
f"{BASE_URL}/subscriptions/{sub_id}",
|
|
149
|
+
json={"data": {
|
|
150
|
+
"type": "subscriptions", "id": sub_id,
|
|
151
|
+
"attributes": {"reviewNote": "", "familySharable": False},
|
|
152
|
+
}},
|
|
153
|
+
headers=headers,
|
|
154
|
+
timeout=TIMEOUT,
|
|
155
|
+
)
|
|
156
|
+
if not resp.ok:
|
|
157
|
+
print_api_errors(resp, f"touch subscription {sub_id}")
|
|
141
158
|
sub_results.append({"product_id": sub_config["product_id"], "id": sub_id})
|
|
142
159
|
|
|
143
160
|
return {"group": ref_name, "group_id": group_id, "subscriptions": sub_results}
|
|
@@ -230,18 +247,17 @@ def _apply_equalized_prices(
|
|
|
230
247
|
skipped = 0
|
|
231
248
|
failed = 0
|
|
232
249
|
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
failed += 1
|
|
241
|
-
print(f" WARNING: Failed to set base price for {base_territory}", file=sys.stderr)
|
|
242
|
-
return created, skipped, failed
|
|
250
|
+
# Always set base territory price to ensure it matches config.
|
|
251
|
+
# Apple's preserveCurrentPrice=False handles updates; if the price
|
|
252
|
+
# is already correct this is effectively a no-op re-confirmation.
|
|
253
|
+
result = create_subscription_price(headers, sub_id, base_point["id"], base_territory)
|
|
254
|
+
if result:
|
|
255
|
+
created += 1
|
|
256
|
+
print(f" Set base price {base_amount} {base_currency} for {base_territory}")
|
|
243
257
|
else:
|
|
244
|
-
|
|
258
|
+
failed += 1
|
|
259
|
+
print(f" WARNING: Failed to set base price for {base_territory}", file=sys.stderr)
|
|
260
|
+
return created, skipped, failed
|
|
245
261
|
|
|
246
262
|
# Get equalized prices for all other territories
|
|
247
263
|
equalized = get_price_point_equalizations(headers, base_point["id"])
|