Install
openclaw skills install expo-app-store-screenshotsCapture and prepare App Store / Google Play screenshots for any React Native / Expo app. Drives iOS Simulator and Android device/emulator via deep links, locks the status bar to a clean marketing state, captures the standard set of screens per locale, and resizes to store-target dimensions. Use when the user asks to (re)generate or refresh store screenshots, add a new locale, add a new screen, or upload screenshots to App Store Connect or Google Play. Also trigger for adjacent phrasing like "marketing screenshots", "store listing screenshots", "screen capture for the app stores", or when the user mentions `xcrun simctl`, `adb screencap`, or paths like `screenshots/<locale>/<device>/`.
openclaw skills install expo-app-store-screenshotsEnd-to-end runbook for marketing screenshots that ship to the iOS App Store and Google Play. The skill does not hard-code app identity — it discovers the deep-link scheme + iOS bundle ID + Android package from the project's Expo config, and takes everything else as parameters.
| Platform | Needed |
|---|---|
| iOS | Xcode CLI (xcrun simctl) |
| Android | Android Platform Tools (adb) |
| Resize | ImageMagick 7+ (magick) |
| Detect | jq and (optional) npx expo for app.config.{ts,js} projects |
| Upload | Python 3.9+ with requests, pyjwt[crypto] (App Store) and google-auth (Play) |
screenshots/<locale>/<device>/NN-<device>-<screen>.png
<locale>: BCP-47 tag — en-US, zh-CN, ja-JP, …<device>: iphone, ipad, android-phone, android-tabletNN: zero-padded ordinal so files sort the same in Finder and the store back-office<screen>: kebab-case slug for the page (sign-in, home, settings, …)| Device | Required size | Notes |
|---|---|---|
iphone | 1284×2778 | App Store 6.5" display. Capture on iPhone 16 Pro Max sim (1320×2868) and resize. |
ipad | 2064×2752 | App Store 13" display. iPad Pro 13" M4 captures natively at this size. |
android-phone | 1440×3120 | Google Play phone (9:19.5). Pixel 7+/8+/9 Pro class captures natively. |
For other targets, look up the current Apple / Google specs and pass the size through to assets/resize.sh.
assets/)| Script | Purpose |
|---|---|
assets/detect-app-config.sh | Discover APP_SCHEME, IOS_BUNDLE_ID, ANDROID_PACKAGE from the Expo config. |
assets/detect-routes.sh | Walk an Expo Router app/ (or src/app/) tree and print every route as <url>\t<group>\t<source-file>. |
assets/ios-status-bar.sh | Lock / clear the iOS Simulator status bar (9:41, charged, full bars). |
assets/ios-capture.sh | One screenshot: openurl → settle → simctl io screenshot. |
assets/android-status-bar.sh | Enter / exit Android system UI demo mode (clean clock, battery, signal). |
assets/android-capture.sh | One screenshot: am start deep link → settle → screencap + adb pull. |
assets/resize.sh | Batch resize a directory of PNGs to a target WxH, idempotent. |
assets/write-summary.sh | Write summary.md into a device folder (model, OS, resolution, screen list). |
assets/upload-app-store.py | Upload one (locale, device) folder to App Store Connect via the API. |
assets/upload-play-store.py | Upload one (locale, image-type) folder to Google Play via the Publisher API. |
Each script accepts -h-style usage on bad input. Read the file headers for full arg lists.
Capture runs in three phases: phase 1 takes the unauth screens (sign-in, sign-up, etc.) while the demo account is signed out, then you manually sign in across all devices, then phase 2 takes the auth-required screens. This avoids round-tripping through the sign-in flow during automation and keeps both states clean.
Script paths in the bash blocks below are written relative to the skill root (assets/...). Resolve them to wherever your agent installed the skill before running.
Pre-flight
.app/.apk from weeks ago will either crash on missing native modules (NativeModule.X is null) or — worse — quietly render an old UI, and you won't notice until the screenshots ship. Cold-launch the app and visually confirm it matches today's source before capturing. If it doesn't, rebuild and reinstall (for Expo: pnpm --filter <app> prebuild --clean && pnpm --filter <app> ios && pnpm --filter <app> android, or whatever your pipeline is).<locale> you're capturing (or rely on system locale if the app inherits it).adb targets emulators and physical phones identically; every script here works against a plugged-in Pixel/Galaxy/etc. just by running adb devices first. If multiple devices are attached, pass -s <serial> to the capture/status-bar scripts.xcrun simctl is simulator-only. For a real iPhone, the path is Xcode-driven (xcrun devicectl device install, Xcode for deep-link launch, xcrun devicectl device screenshot on Xcode 16+) and not wired into these scripts. Prefer rebuilding the dev client or installing a release .app to the simulator instead.Discover app identity
eval "$(bash assets/detect-app-config.sh path/to/app)"
echo "$APP_SCHEME $ANDROID_PACKAGE"
If detection fails (custom config plugin, monorepo quirks), set the three env vars by hand.
Split screens by auth state. Two bash arrays of NN slug deep-path rows. The NN ordering keeps both arrays disjoint so filenames sort correctly together.
For Expo Router projects only: rather than guessing deep-link paths from memory, dump every route under
app/(orsrc/app/) first so you don't miss anything the team added since the last screenshot pass:bash assets/detect-routes.sh path/to/app # TSV: <url-path>\t<group>\t<source-file>Expo Router collapses
(group)segments out of the user-visible URL —app/(auth)/sign-in.tsxdeep-links as/sign-in. The(group)column is a useful auth-state hint ((auth),(app),(tabs)usually gate;(public),(onboarding)usually don't), but confirm against the matching_layout.tsxwhere redirect logic actually lives. For non-Expo-Router apps, read the project's own router config.
UNAUTH_SCREENS=(
"01 sign-in /sign-in"
"02 sign-up /sign-up"
)
AUTH_SCREENS=(
"03 home /"
"04 agent-plaza /agent"
"05 profile /user"
"06 settings /settings"
"07 agent-create /agent/create"
"08 product /product"
)
Set up — lock status bars on all devices once. Persists across app launches and across both phases.
LOCALE=en-US
IPHONE_UDID=<...>; IPAD_UDID=<...> # `xcrun simctl list devices` to find them
bash assets/ios-status-bar.sh "$IPHONE_UDID"
bash assets/ios-status-bar.sh "$IPAD_UDID"
bash assets/android-status-bar.sh enter
capture_set() {
local udid="$1" device="$2" platform="$3"; shift 3
for row in "$@"; do
read -r nn slug path <<<"$row"
local out="screenshots/$LOCALE/$device/$nn-$device-$slug.png"
if [[ "$platform" == ios ]]; then
bash assets/ios-capture.sh "$udid" "$APP_SCHEME://$path" "$out"
else
bash assets/android-capture.sh "$APP_SCHEME://$path" "$out" "$ANDROID_PACKAGE"
fi
done
}
Phase 1 — capture unauth screens. App must be signed out on every device.
capture_set "$IPHONE_UDID" iphone ios "${UNAUTH_SCREENS[@]}"
capture_set "$IPAD_UDID" ipad ios "${UNAUTH_SCREENS[@]}"
capture_set "" android-phone android "${UNAUTH_SCREENS[@]}"
Manual sign-in. Open the simulator/emulator windows, complete the sign-in flow with the demo account on iPhone, iPad, and Android. Confirm you land on the post-sign-in home page on all three before continuing. (Status bar stays locked — no need to re-run step 4.)
Phase 2 — capture auth screens.
capture_set "$IPHONE_UDID" iphone ios "${AUTH_SCREENS[@]}"
capture_set "$IPAD_UDID" ipad ios "${AUTH_SCREENS[@]}"
capture_set "" android-phone android "${AUTH_SCREENS[@]}"
Resize & verify. iPad is already 2064×2752 native, no resize needed.
bash assets/resize.sh "screenshots/$LOCALE/iphone" 1284x2778
bash assets/resize.sh "screenshots/$LOCALE/android-phone" 1440x3120
identify "screenshots/$LOCALE/iphone"/*.png # expect 1284x2778
identify "screenshots/$LOCALE/ipad"/*.png # expect 2064x2752
identify "screenshots/$LOCALE/android-phone"/*.png # expect 1440x3120
bash assets/android-status-bar.sh exit # release demo mode
Per-device summary. Drop a summary.md into each device folder so reviewers and future-you can tell at a glance which hardware/OS produced these and which screen each PNG corresponds to. Run after step 8 so the recorded resolution reflects the resized output.
ALL_SCREENS=( "${UNAUTH_SCREENS[@]}" "${AUTH_SCREENS[@]}" )
bash assets/write-summary.sh ios "$IPHONE_UDID" "screenshots/$LOCALE/iphone" "$LOCALE" "${ALL_SCREENS[@]}"
bash assets/write-summary.sh ios "$IPAD_UDID" "screenshots/$LOCALE/ipad" "$LOCALE" "${ALL_SCREENS[@]}"
bash assets/write-summary.sh android - "screenshots/$LOCALE/android-phone" "$LOCALE" "${ALL_SCREENS[@]}"
The script auto-detects model + OS from simctl/adb and reads the resolution off any PNG already in the folder. Pass - for the Android target when only one device/emulator is attached, otherwise pass the serial.
Both upload scripts upload one (locale, device-or-image-type) directory per invocation. Re-running replaces the contents of that slot — pass --keep-existing to append instead. Loop in shell to cover multiple locales/devices.
upload-app-store.pyPre-reqs:
.p8, the Key ID, and the Issuer ID.zh-CN → zh-Hans, zh-TW → zh-Hant. Map before invoking.pip install 'pyjwt[crypto]' requests
export ASC_KEY_ID=ABC1234567
export ASC_ISSUER_ID=11111111-2222-3333-4444-555555555555
export ASC_KEY_PATH=$HOME/.appstoreconnect/AuthKey_ABC1234567.p8
UPLOAD=assets/upload-app-store.py
APP_ID=1234567890
# en-US (BCP-47 == ASC code), iPhone + iPad
python3 "$UPLOAD" --app-id "$APP_ID" --locale en-US --device iphone --dir screenshots/en-US/iphone
python3 "$UPLOAD" --app-id "$APP_ID" --locale en-US --device ipad --dir screenshots/en-US/ipad
# zh-CN folder → zh-Hans on App Store Connect
python3 "$UPLOAD" --app-id "$APP_ID" --locale zh-Hans --device iphone --dir screenshots/zh-CN/iphone
python3 "$UPLOAD" --app-id "$APP_ID" --locale zh-Hans --device ipad --dir screenshots/zh-CN/ipad
Default device → display-type mapping (override with --device iphone-65|iphone-67|iphone-69|ipad-129):
iphone → APP_IPHONE_67 (1284×2778 / 1290×2796)ipad → APP_IPAD_PRO_3GEN_129 (2064×2752 / 2048×2732)upload-play-store.pyPre-reqs:
pip install google-auth requests
export PLAY_CREDENTIALS=$HOME/.gcloud/play-service-account.json
UPLOAD=assets/upload-play-store.py
PKG=$ANDROID_PACKAGE # e.g. com.example.myapp (use detect-app-config.sh to populate)
python3 "$UPLOAD" --package "$PKG" --locale en-US --image-type phoneScreenshots --dir screenshots/en-US/android-phone
python3 "$UPLOAD" --package "$PKG" --locale zh-CN --image-type phoneScreenshots --dir screenshots/zh-CN/android-phone
Image-type values: phoneScreenshots, sevenInchScreenshots, tenInchScreenshots, tvScreenshots, wearScreenshots. Each slot caps at 8 images on Play; the script does not enforce that — the commit step will fail if you exceed it.
The script opens an edit, replaces the (locale, image-type) slot, then commits. If the commit fails, the edit is abandoned automatically by Play after a short TTL — re-run.
xcrun simctl openurl and adb shell am start ... -d both want a full URL. With Expo Router, paths nest under the scheme as <scheme>:///<path> (note the triple slash — empty host).-s <serial> to android-status-bar.sh and android-capture.sh; both pass remaining args through to adb.adb exec-out screencap -p > file corrupts bytes on shells that translate CRLF. The capture script uses screencap to a remote path then adb pull, which is byte-safe.simctl status_bar booted targets whichever simulator is currently booted — convenient when only one sim is running.resize.sh skips files already at the target size, so re-running is cheap.mkdir -p screenshots/<new>/{iphone,ipad,android-phone}.LOCALE=<new>."NN slug /path" to the SCREENS array.