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:
alexandrev-tibco
2026-07-02 22:11:20 +02:00
parent 9d56d420aa
commit 19e80c912b
10 changed files with 1515 additions and 0 deletions
+88
View File
@@ -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")