ASO tooling: compositores de marketing + captura UITest
- testCaptureScreenshots en PortfolioJournalUITests: captura tabs con datos demo, idioma vía SCREENSHOT_LANG (usa el modo --screenshots del commit siguiente) - Scripts/aso: compositores de screenshots de App Store (frame_layout con perspectiva 3D + panorama multi-panel, variantes premium/tilt), layouts JSON iPhone/iPad, y asc.py (helper API App Store Connect para estado/screenshots) - Fix test roto pre-existente: OnboardingQuickStartView.appSettingsURL restaurado Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Minimal App Store Connect API helper for screenshots.
|
||||
|
||||
Auth pulls the key from `pass` (appstore/api-key-id, appstore/issuer-id,
|
||||
appstore/api-key-p8). Read-only `status` command lists the editable iOS version
|
||||
and the screenshot sets for a locale so we know what exists before uploading.
|
||||
|
||||
Usage:
|
||||
python asc.py status [locale] # default en-US
|
||||
python asc.py upload <locale> <display_type> <img1> <img2> ... # replaces that set
|
||||
"""
|
||||
import sys, time, json, hashlib, subprocess
|
||||
import jwt, requests
|
||||
|
||||
BASE = "https://api.appstoreconnect.apple.com/v1"
|
||||
BUNDLE = "com.alexandrevazquez.PortfolioJournal"
|
||||
|
||||
|
||||
def _pass(k):
|
||||
return subprocess.check_output(["pass", "show", k]).decode().strip()
|
||||
|
||||
|
||||
def token():
|
||||
kid = _pass("appstore/api-key-id")
|
||||
iss = _pass("appstore/issuer-id")
|
||||
key = _pass("appstore/api-key-p8")
|
||||
payload = {"iss": iss, "iat": int(time.time()), "exp": int(time.time()) + 1100,
|
||||
"aud": "appstoreconnect-v1"}
|
||||
return jwt.encode(payload, key, algorithm="ES256", headers={"kid": kid, "typ": "JWT"})
|
||||
|
||||
|
||||
def H():
|
||||
return {"Authorization": f"Bearer {token()}", "Content-Type": "application/json"}
|
||||
|
||||
|
||||
def get(path, **params):
|
||||
r = requests.get(f"{BASE}{path}", headers=H(), params=params)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def app_id():
|
||||
d = get("/apps", **{"filter[bundleId]": BUNDLE})
|
||||
return d["data"][0]["id"]
|
||||
|
||||
|
||||
def editable_version(aid):
|
||||
d = get(f"/apps/{aid}/appStoreVersions", **{"filter[platform]": "IOS", "limit": 10})
|
||||
for v in d["data"]:
|
||||
st = v["attributes"]["appStoreState"]
|
||||
if st in ("PREPARE_FOR_SUBMISSION", "DEVELOPER_REJECTED", "REJECTED",
|
||||
"METADATA_REJECTED", "WAITING_FOR_REVIEW", "READY_FOR_REVIEW"):
|
||||
return v
|
||||
return d["data"][0] if d["data"] else None
|
||||
|
||||
|
||||
def loc_id(version_id, locale):
|
||||
d = get(f"/appStoreVersions/{version_id}/appStoreVersionLocalizations",
|
||||
**{"limit": 50})
|
||||
for l in d["data"]:
|
||||
if l["attributes"]["locale"] == locale:
|
||||
return l["id"]
|
||||
return None
|
||||
|
||||
|
||||
def sets_for_loc(lid):
|
||||
return get(f"/appStoreVersionLocalizations/{lid}/appScreenshotSets", **{"limit": 50})["data"]
|
||||
|
||||
|
||||
def status(locale="en-US"):
|
||||
aid = app_id()
|
||||
v = editable_version(aid)
|
||||
print("app id:", aid)
|
||||
print("version:", v["attributes"]["versionString"], "| state:", v["attributes"]["appStoreState"], "| id:", v["id"])
|
||||
lid = loc_id(v["id"], locale)
|
||||
print(f"locale {locale} localization id:", lid)
|
||||
if not lid:
|
||||
return
|
||||
for s in sets_for_loc(lid):
|
||||
dt = s["attributes"]["screenshotDisplayType"]
|
||||
shots = get(f"/appScreenshotSets/{s['id']}/appScreenshots", **{"limit": 50})["data"]
|
||||
print(f" set {dt}: {len(shots)} screenshots (set id {s['id']})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else "status"
|
||||
if cmd == "status":
|
||||
status(sys.argv[2] if len(sys.argv) > 2 else "en-US")
|
||||
Reference in New Issue
Block a user