@daemux/store-automator 0.10.34 → 0.10.35
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.
|
@@ -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.35"
|
|
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.35",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"flutter",
|
|
18
18
|
"app-store",
|
package/package.json
CHANGED
|
@@ -66,27 +66,17 @@ def create_subscription_availability(
|
|
|
66
66
|
available_in_new: bool = True,
|
|
67
67
|
) -> dict | None:
|
|
68
68
|
"""Create availability with specified territories for a subscription."""
|
|
69
|
-
territory_data = [
|
|
70
|
-
{"type": "territories", "id": tid} for tid in territory_ids
|
|
71
|
-
]
|
|
69
|
+
territory_data = [{"type": "territories", "id": tid} for tid in territory_ids]
|
|
72
70
|
resp = requests.post(
|
|
73
71
|
f"{BASE_URL}/subscriptionAvailabilities",
|
|
74
|
-
json={
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"data": {"type": "subscriptions", "id": sub_id},
|
|
83
|
-
},
|
|
84
|
-
"availableTerritories": {
|
|
85
|
-
"data": territory_data,
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
},
|
|
72
|
+
json={"data": {
|
|
73
|
+
"type": "subscriptionAvailabilities",
|
|
74
|
+
"attributes": {"availableInNewTerritories": available_in_new},
|
|
75
|
+
"relationships": {
|
|
76
|
+
"subscription": {"data": {"type": "subscriptions", "id": sub_id}},
|
|
77
|
+
"availableTerritories": {"data": territory_data},
|
|
78
|
+
},
|
|
79
|
+
}},
|
|
90
80
|
headers=headers,
|
|
91
81
|
timeout=TIMEOUT,
|
|
92
82
|
)
|
|
@@ -172,26 +162,16 @@ def create_subscription_price(
|
|
|
172
162
|
"""Create a price entry for a subscription using a price point ID."""
|
|
173
163
|
resp = requests.post(
|
|
174
164
|
f"{BASE_URL}/subscriptionPrices",
|
|
175
|
-
json={
|
|
176
|
-
"
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
165
|
+
json={"data": {
|
|
166
|
+
"type": "subscriptionPrices",
|
|
167
|
+
"attributes": {"startDate": start_date, "preserveCurrentPrice": False},
|
|
168
|
+
"relationships": {
|
|
169
|
+
"subscription": {"data": {"type": "subscriptions", "id": sub_id}},
|
|
170
|
+
"subscriptionPricePoint": {
|
|
171
|
+
"data": {"type": "subscriptionPricePoints", "id": price_point_id},
|
|
181
172
|
},
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"data": {"type": "subscriptions", "id": sub_id},
|
|
185
|
-
},
|
|
186
|
-
"subscriptionPricePoint": {
|
|
187
|
-
"data": {
|
|
188
|
-
"type": "subscriptionPricePoints",
|
|
189
|
-
"id": price_point_id,
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
}
|
|
194
|
-
},
|
|
173
|
+
},
|
|
174
|
+
}},
|
|
195
175
|
headers=headers,
|
|
196
176
|
timeout=TIMEOUT,
|
|
197
177
|
)
|
|
@@ -221,72 +201,49 @@ def get_review_screenshot(headers: dict, sub_id: str) -> dict | None:
|
|
|
221
201
|
return resp.json().get("data")
|
|
222
202
|
|
|
223
203
|
|
|
224
|
-
def
|
|
225
|
-
headers: dict, sub_id: str,
|
|
204
|
+
def upload_review_screenshot(
|
|
205
|
+
headers: dict, sub_id: str,
|
|
206
|
+
file_name: str, file_data: bytes, md5_checksum: str,
|
|
226
207
|
) -> dict | None:
|
|
227
|
-
"""Reserve a review screenshot
|
|
208
|
+
"""Reserve, upload chunks, and commit a review screenshot in one operation."""
|
|
209
|
+
resource_type = "subscriptionAppStoreReviewScreenshots"
|
|
228
210
|
resp = requests.post(
|
|
229
|
-
f"{BASE_URL}/
|
|
230
|
-
json={
|
|
231
|
-
"
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
"relationships": {
|
|
238
|
-
"subscription": {
|
|
239
|
-
"data": {"type": "subscriptions", "id": sub_id},
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
}
|
|
243
|
-
},
|
|
211
|
+
f"{BASE_URL}/{resource_type}",
|
|
212
|
+
json={"data": {
|
|
213
|
+
"type": resource_type,
|
|
214
|
+
"attributes": {"fileName": file_name, "fileSize": len(file_data)},
|
|
215
|
+
"relationships": {
|
|
216
|
+
"subscription": {"data": {"type": "subscriptions", "id": sub_id}},
|
|
217
|
+
},
|
|
218
|
+
}},
|
|
244
219
|
headers=headers,
|
|
245
220
|
timeout=TIMEOUT,
|
|
246
221
|
)
|
|
247
222
|
if not resp.ok:
|
|
248
223
|
print_api_errors(resp, f"reserve screenshot for subscription {sub_id}")
|
|
249
224
|
return None
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
for op in upload_operations:
|
|
258
|
-
url = op["url"]
|
|
259
|
-
offset = op["offset"]
|
|
260
|
-
length = op["length"]
|
|
261
|
-
chunk = file_data[offset : offset + length]
|
|
225
|
+
reservation = resp.json().get("data")
|
|
226
|
+
if not reservation:
|
|
227
|
+
print(f"ERROR: Empty reservation response for subscription {sub_id}", file=sys.stderr)
|
|
228
|
+
return None
|
|
229
|
+
screenshot_id = reservation["id"]
|
|
230
|
+
for op in reservation["attributes"].get("uploadOperations", []):
|
|
231
|
+
chunk = file_data[op["offset"] : op["offset"] + op["length"]]
|
|
262
232
|
op_headers = {h["name"]: h["value"] for h in op.get("requestHeaders", [])}
|
|
263
|
-
|
|
264
|
-
if not
|
|
233
|
+
chunk_resp = requests.put(op["url"], headers=op_headers, data=chunk, timeout=TIMEOUT)
|
|
234
|
+
if not chunk_resp.ok:
|
|
265
235
|
print(
|
|
266
|
-
f"ERROR (upload chunk at offset {offset}): "
|
|
267
|
-
f"HTTP {
|
|
236
|
+
f"ERROR (upload chunk at offset {op['offset']}): "
|
|
237
|
+
f"HTTP {chunk_resp.status_code} - {chunk_resp.text[:200]}",
|
|
268
238
|
file=sys.stderr,
|
|
269
239
|
)
|
|
270
|
-
return
|
|
271
|
-
return True
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def commit_review_screenshot(
|
|
275
|
-
headers: dict, screenshot_id: str, md5_checksum: str,
|
|
276
|
-
) -> dict | None:
|
|
277
|
-
"""Commit an uploaded review screenshot by confirming its checksum."""
|
|
240
|
+
return None
|
|
278
241
|
resp = requests.patch(
|
|
279
|
-
f"{BASE_URL}/
|
|
280
|
-
json={
|
|
281
|
-
"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
"attributes": {
|
|
285
|
-
"sourceFileChecksum": md5_checksum,
|
|
286
|
-
"uploaded": True,
|
|
287
|
-
},
|
|
288
|
-
}
|
|
289
|
-
},
|
|
242
|
+
f"{BASE_URL}/{resource_type}/{screenshot_id}",
|
|
243
|
+
json={"data": {
|
|
244
|
+
"type": resource_type, "id": screenshot_id,
|
|
245
|
+
"attributes": {"sourceFileChecksum": md5_checksum, "uploaded": True},
|
|
246
|
+
}},
|
|
290
247
|
headers=headers,
|
|
291
248
|
timeout=TIMEOUT,
|
|
292
249
|
)
|
|
@@ -35,7 +35,6 @@ from asc_iap_api import (
|
|
|
35
35
|
update_localization,
|
|
36
36
|
)
|
|
37
37
|
from asc_subscription_setup import (
|
|
38
|
-
commit_review_screenshot,
|
|
39
38
|
create_subscription_availability,
|
|
40
39
|
create_subscription_price,
|
|
41
40
|
find_price_point_by_amount,
|
|
@@ -44,8 +43,7 @@ from asc_subscription_setup import (
|
|
|
44
43
|
get_subscription_availability,
|
|
45
44
|
get_subscription_prices,
|
|
46
45
|
list_all_territory_ids,
|
|
47
|
-
|
|
48
|
-
upload_screenshot_chunks,
|
|
46
|
+
upload_review_screenshot,
|
|
49
47
|
)
|
|
50
48
|
|
|
51
49
|
CURRENCY_TO_TERRITORY = {
|
|
@@ -225,27 +223,12 @@ def _sync_review_screenshot(
|
|
|
225
223
|
return
|
|
226
224
|
|
|
227
225
|
full_path = os.path.join(project_root, screenshot_path)
|
|
228
|
-
|
|
229
|
-
# Fallback: if configured path doesn't exist, pick first iPhone screenshot
|
|
230
226
|
if not os.path.isfile(full_path):
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
for f in sorted(os.listdir(ios_dir)):
|
|
235
|
-
if f.lower().endswith(".png") and "iphone" in f.lower():
|
|
236
|
-
fallback = os.path.join(ios_dir, f)
|
|
237
|
-
break
|
|
238
|
-
if not fallback:
|
|
239
|
-
for f in sorted(os.listdir(ios_dir)):
|
|
240
|
-
if f.lower().endswith(".png"):
|
|
241
|
-
fallback = os.path.join(ios_dir, f)
|
|
242
|
-
break
|
|
243
|
-
if fallback:
|
|
244
|
-
full_path = fallback
|
|
245
|
-
print(f" Using fallback screenshot: {os.path.basename(full_path)}")
|
|
246
|
-
else:
|
|
247
|
-
print(f" WARNING: Screenshot not found: {full_path}", file=sys.stderr)
|
|
227
|
+
full_path = _find_fallback_screenshot(project_root)
|
|
228
|
+
if not full_path:
|
|
229
|
+
print(f" WARNING: Screenshot not found: {screenshot_path}", file=sys.stderr)
|
|
248
230
|
return
|
|
231
|
+
print(f" Using fallback screenshot: {os.path.basename(full_path)}")
|
|
249
232
|
|
|
250
233
|
existing = get_review_screenshot(headers, sub_id)
|
|
251
234
|
if existing:
|
|
@@ -257,79 +240,66 @@ def _sync_review_screenshot(
|
|
|
257
240
|
|
|
258
241
|
file_name = os.path.basename(full_path)
|
|
259
242
|
md5_checksum = hashlib.md5(file_data).hexdigest()
|
|
260
|
-
|
|
261
|
-
reservation = reserve_review_screenshot(headers, sub_id, file_name, len(file_data))
|
|
262
|
-
if not reservation:
|
|
263
|
-
print(" WARNING: Failed to reserve screenshot upload", file=sys.stderr)
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
screenshot_id = reservation["id"]
|
|
267
|
-
upload_ops = reservation["attributes"].get("uploadOperations", [])
|
|
268
|
-
|
|
269
|
-
success = upload_screenshot_chunks(upload_ops, file_data)
|
|
270
|
-
if not success:
|
|
271
|
-
print(" WARNING: Screenshot chunk upload failed, skipping commit", file=sys.stderr)
|
|
272
|
-
return
|
|
273
|
-
result = commit_review_screenshot(headers, screenshot_id, md5_checksum)
|
|
243
|
+
result = upload_review_screenshot(headers, sub_id, file_name, file_data, md5_checksum)
|
|
274
244
|
if result:
|
|
275
245
|
print(f" Review screenshot uploaded: {file_name}")
|
|
276
246
|
else:
|
|
277
|
-
print(" WARNING: Failed to
|
|
247
|
+
print(" WARNING: Failed to upload screenshot", file=sys.stderr)
|
|
278
248
|
|
|
279
249
|
|
|
280
|
-
def
|
|
281
|
-
"""
|
|
250
|
+
def _find_fallback_screenshot(project_root: str) -> str | None:
|
|
251
|
+
"""Find the first iPhone PNG screenshot in fastlane/screenshots/ios/en-US/."""
|
|
252
|
+
ios_dir = os.path.join(project_root, "fastlane", "screenshots", "ios", "en-US")
|
|
253
|
+
if not os.path.isdir(ios_dir):
|
|
254
|
+
return None
|
|
255
|
+
files = sorted(os.listdir(ios_dir))
|
|
256
|
+
for f in files:
|
|
257
|
+
if f.lower().endswith(".png") and "iphone" in f.lower():
|
|
258
|
+
return os.path.join(ios_dir, f)
|
|
259
|
+
for f in files:
|
|
260
|
+
if f.lower().endswith(".png"):
|
|
261
|
+
return os.path.join(ios_dir, f)
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def main() -> None:
|
|
266
|
+
if len(sys.argv) < 2:
|
|
267
|
+
print(f"Usage: {sys.argv[0]} <path/to/iap_config.json>", file=sys.stderr)
|
|
268
|
+
sys.exit(1)
|
|
269
|
+
|
|
270
|
+
# Validate required environment variables
|
|
282
271
|
required_vars = [
|
|
283
|
-
"APP_STORE_CONNECT_KEY_IDENTIFIER",
|
|
284
|
-
"
|
|
285
|
-
"APP_STORE_CONNECT_PRIVATE_KEY",
|
|
286
|
-
"BUNDLE_ID",
|
|
287
|
-
"PROJECT_ROOT",
|
|
272
|
+
"APP_STORE_CONNECT_KEY_IDENTIFIER", "APP_STORE_CONNECT_ISSUER_ID",
|
|
273
|
+
"APP_STORE_CONNECT_PRIVATE_KEY", "BUNDLE_ID", "PROJECT_ROOT",
|
|
288
274
|
]
|
|
289
|
-
|
|
290
|
-
missing = [var for var, val in
|
|
275
|
+
env = {var: os.environ.get(var, "") for var in required_vars}
|
|
276
|
+
missing = [var for var, val in env.items() if not val]
|
|
291
277
|
if missing:
|
|
292
|
-
print(f"ERROR: Missing
|
|
278
|
+
print(f"ERROR: Missing env vars: {', '.join(missing)}", file=sys.stderr)
|
|
293
279
|
sys.exit(1)
|
|
294
|
-
|
|
280
|
+
key_id, issuer_id, private_key, bundle_id, project_root = env.values()
|
|
295
281
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
"""Load and validate the IAP config file."""
|
|
282
|
+
# Load IAP config
|
|
283
|
+
config_path = sys.argv[1]
|
|
299
284
|
if not os.path.isfile(config_path):
|
|
300
285
|
print(f"ERROR: IAP config file not found: {config_path}", file=sys.stderr)
|
|
301
286
|
sys.exit(1)
|
|
302
|
-
|
|
303
287
|
with open(config_path, "r", encoding="utf-8") as f:
|
|
304
288
|
config = json.load(f)
|
|
305
|
-
|
|
306
289
|
if not config.get("subscription_groups"):
|
|
307
290
|
print("WARNING: No subscription_groups found in config", file=sys.stderr)
|
|
308
291
|
|
|
309
|
-
return config
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
def main() -> None:
|
|
313
|
-
if len(sys.argv) < 2:
|
|
314
|
-
print(f"Usage: {sys.argv[0]} <path/to/iap_config.json>", file=sys.stderr)
|
|
315
|
-
sys.exit(1)
|
|
316
|
-
|
|
317
|
-
config_path = sys.argv[1]
|
|
318
|
-
key_id, issuer_id, private_key, bundle_id, project_root = validate_env()
|
|
319
|
-
|
|
320
292
|
token = get_jwt_token(key_id, issuer_id, private_key)
|
|
321
293
|
headers = {
|
|
322
294
|
"Authorization": f"Bearer {token}",
|
|
323
295
|
"Content-Type": "application/json",
|
|
324
296
|
}
|
|
325
297
|
|
|
326
|
-
config = load_iap_config(config_path)
|
|
327
298
|
app_id = get_app_id(headers, bundle_id)
|
|
328
299
|
print(f"App ID: {app_id} (Bundle: {bundle_id})")
|
|
329
300
|
|
|
330
301
|
existing_groups = list_subscription_groups(headers, app_id)
|
|
331
302
|
results = []
|
|
332
|
-
|
|
333
303
|
for group_config in config.get("subscription_groups", []):
|
|
334
304
|
result = sync_subscription_group(
|
|
335
305
|
headers, app_id, group_config, existing_groups, project_root,
|