iOS Distribute - NowStack Mobile

SkillDev tools

Prepare and submit NowStack Mobile iOS App Store releases. Use for real 6.5-inch app screenshots, metadata, IAP, review details, validation, and submit.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the iOS Distribute - NowStack Mobile skill

What this skill tells your AI

The instructions your AI receives, as published by melvynx/saveit.now in .agents/skills/ns-ios-distribute/SKILL.md and read by ahel’s review.

Take a TestFlight-ready app all the way to App Store review submission: store screenshots, metadata, IAP pricing/availability, age rating, review details with demo account, validation, and submit. If no build is on TestFlight yet, run ns-ios-testflight first.

Everything runs with repo tools and public CLIs only:

  • mobile-app/scripts/asc-api.mjs for all App Store Connect API calls (ASC_KEY_ID/ASC_ISSUER_ID/ASC_P8_PATH env vars).
  • asc CLI for uploads (screenshots, subscription assets) and validation.
  • mobile-app/scripts/store-screenshots.mjs + Maestro + xcrun simctl for captures.

Sequencing with the rest of the lifecycle:

  1. No processed build yet → run the ns-ios-testflight skill first.
  2. Screenshot capture details → .agents/skills/ns-generate-store-screenshots/SKILL.md.
  3. This file — version, metadata, IAP, compliance, review, submit. For deep platform specifics use .agents/skills/ns-ios-deploy-app/SKILL.md / .agents/skills/ns-deploy-android-app/SKILL.md.

<state_variables>

VariableSource
{asc_app_id}GET /v1/apps?filter[bundleId]={bundle_id}
{version_id}the appStoreVersions id in PREPARE_FOR_SUBMISSION
{version_loc_id}en-US appStoreVersionLocalizations id
{app_info_id} / {app_info_loc_id}from GET /v1/apps/{asc_app_id}/appInfos (+ its localizations)
{build_db_id}processed build id from GET /v1/builds?filter[app]={asc_app_id}
{subscription_id} / {sub_group_id}if the app sells a subscription
{screenshots_dir}dated output of the screenshot run, e.g. documents/store-screenshots/<date>/ios/iphone-65/en-US
</state_variables>

<critical_safety>

  • Every mutation against App Store Connect is externally visible. Confirm with the user before: uploading screenshots, changing pricing/availability, and ABOVE ALL the final review submission.
  • Never print or commit ASC credentials. documents/store-screenshots/ is gitignored — keep it that way.
  • Screenshots must contain only demo data (the seeded demo account), never real user content.
  • Screenshots must be raw captures of the real app UI. Do not create screenshot-only routes, fake marketing screens, fake text overlays, fake cards, fake product claims, or composite promo images unless the user explicitly asks for framed marketing assets.
  • If a previous agent created /store/*, store/[shot], or any other fake screenshot route, stop and remove/ignore it before capturing or uploading App Store screenshots.
  • Metadata values (URLs, support email, price) come from site-config.ts — do not invent them. </critical_safety>

<asc_entity_map> App Store Connect data model — knowing which entity owns which field saves hours:

FieldEntityEndpoint
description, keywords, promotionalText, supportUrl, marketingUrlappStoreVersionLocalizationsPATCH /v1/appStoreVersionLocalizations/{id}
subtitle, privacyPolicyUrlappInfoLocalizationsPATCH /v1/appInfoLocalizations/{id}
primaryCategoryappInfosPATCH /v1/appInfos/{id} (relationships)
copyright, release typeappStoreVersionsPATCH /v1/appStoreVersions/{id}
attached buildappStoreVersions relationshipPATCH /v1/appStoreVersions/{id}/relationships/build
age rating answersageRatingDeclarationsPATCH /v1/ageRatingDeclarations/{id}
contact + demo accountappStoreReviewDetailsPOST /v1/appStoreReviewDetails
content rightsappsasc apps update --id {asc_app_id} --content-rights ...
</asc_entity_map>
  • No processed build → run the ns-ios-testflight skill first.
  • Resolve {version_id}: GET /v1/apps/{asc_app_id}/appStoreVersions?filter[appStoreState]=PREPARE_FOR_SUBMISSION. If none exists, create it: POST /v1/appStoreVersions with {platform: "IOS", versionString: "<version>"} + app relationship.
  • Read site-config.ts: urls (website/privacy/terms), support email, payment product IDs, price.
  • The web app must be LIVE (privacy + terms URLs are review gates) — if not, stop and point to docs/production-checklist.md stage 2.
  1. Real app UI only: capture the application screens as a user would see them: onboarding, auth, signed-in home/dashboard, real feature screen, real paywall, and real settings/account/privacy screen. Demo data is allowed; fake screenshot-only pages, fake cards, marketing layouts, and invented overlay copy are not allowed.
  2. Demo data/session: create/verify a seed function (e.g. convex/seedStoreDemo.ts, app-specific) so real screens show clean demo content. Sign in with the built-in review account appstoretest@email.com / OTP 123456 for auth-gated screens. Never use real personal data.
  3. Paywall capture: temporarily set EXPO_PUBLIC_ALLOW_DEV_PAYMENT_BYPASS=false so the real paywall renders (restore afterwards). The paywall screenshot must be the actual in-app paywall, not a recreated promo screen.
  4. Device: always use the App Store "iPhone 6.5 Display" slot: iPhone 11 Pro Max / XS Max class simulator → native 1242x2688 = Apple display type IPHONE_65. Do not use iPhone 14 Plus / iPhone 15 Pro Max / 6.7-inch screenshots unless the user explicitly asks for another slot.
xcrun simctl create "store-iphone-65" "com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro-Max" <runtime>
cd mobile-app && npm run screenshots:store:ios            # dry-run first, always
npm run screenshots:store:ios -- --execute                # after user confirms
  1. Maestro flows live in mobile-app/maestro/store/ (Maestro needs JDK 17: export JAVA_HOME=/opt/homebrew/opt/openjdk@17). Robust flow patterns: extendedWaitUntil: {visible: "<text>", timeout: 10000} before every tap; conditional paywall dismissal with runFlow: when: visible:.
  2. Validate: sips -g pixelWidth -g pixelHeight *.png — every file must be exactly 1242x2688. Then visual QA each PNG (read the images): real app screen, no fake route, no dev overlays, no status-bar clutter, no placeholder copy, no invented claims.

Write metadata (draft it from docs/templates/app-store-metadata.example.json + site-config.ts, show the user for approval first):

  • PATCH /v1/appStoreVersionLocalizations/{version_loc_id} — description (append Terms of Use: <terms-url> and Privacy Policy: <privacy-url> at the end — Apple requires visible terms for subscription apps), keywords (100 chars max, comma-separated, no spaces after commas), promotionalText, supportUrl, marketingUrl.
  • PATCH /v1/appInfoLocalizations/{app_info_loc_id} — subtitle (30 chars max), privacyPolicyUrl.
  • PATCH /v1/appInfos/{app_info_id} — primaryCategory relationship (e.g. {"type":"appCategories","id":"LIFESTYLE"}).
  • PATCH /v1/appStoreVersions/{version_id} — copyright (<year> <company name>).

Upload screenshots:

asc screenshots upload --version-localization "{version_loc_id}" \
  --path "{screenshots_dir}" --device-type "IPHONE_65"
# names + descriptions testers/reviewers see
asc subscriptions groups localizations create --group {sub_group_id} --locale en-US --name "<group display name>"
asc subscriptions localizations create --subscription-id {subscription_id} --app {asc_app_id} \
  --locale en-US --name "<plan name>" --description "<one-line benefit>"

# pricing: set one base price, propagate everywhere
asc subscriptions pricing equalize --subscription-id {subscription_id} \
  --base-price "<price>" --base-territory "<ISO3>" --confirm

# availability: if "app availability not found", initialize via the v2 API with ALL territory ids
node mobile-app/scripts/asc-api.mjs GET "/v1/territories?limit=200"   # collect ids
# POST /v2/appAvailabilities with app relationship + territoryAvailabilities for every territory

# review screenshot for the subscription/IAP (use the real in-app paywall capture)
asc subscriptions review screenshots create --subscription-id {subscription_id} \
  --file "{screenshots_dir}/<paywall>.png"

Target state: subscription shows READY_TO_SUBMIT. If a screenshot asset gets stuck in processing, delete it and re-upload.

age rating: fetch, then PATCH honest answers (everything NONE/false except what the app has)

node mobile-app/scripts/asc-api.mjs GET "/v1/appInfos/{app_info_id}/ageRatingDeclaration" node mobile-app/scripts/asc-api.mjs PATCH /v1/ageRatingDeclarations/ /tmp/age-req.json


- **Export compliance**: add `"ITSAppUsesNonExemptEncryption": false` to `infoPlist` in `mobile-app/app.config.ts` (standard HTTPS-only apps) so every build auto-answers it.
- **App Privacy questionnaire is WEB-ONLY** — the API cannot complete it. Give the user the direct link `https://appstoreconnect.apple.com/apps/{asc_app_id}/distribution/privacy` plus the exact declarations to click, derived from what the app actually collects (this boilerplate baseline: email + user content, linked to identity, no tracking). This is the one manual gate before submission.
</phase>

<phase n="F" title="Review details, validate, submit">
```bash
# review details — the demo account is built into the boilerplate auth
node mobile-app/scripts/asc-api.mjs POST /v1/appStoreReviewDetails /tmp/review-req.json

review-req.json: contact name/email/phone from the user, demoAccountName: "appstoretest@email.com", demoAccountPassword: "123456", demoAccountRequired: true, and notes explaining the OTP flow: "Sign in with the email OTP flow: enter appstoretest@email.com, the one-time code is fixed to 123456 for this account." Describe the core flow in 2-3 sentences.

asc validate --app {asc_app_id} --version "<version>" --platform IOS --output json

Fix every severity: error (the phases above cover all standard ones). When clean AND the user confirms App Privacy is published:

asc review submit --app {asc_app_id} --version "<version>" --dry-run --output table
asc review submit --app {asc_app_id} --version "<version>" --confirm     # only after explicit user confirmation

Report: version, build number, what was submitted, expected review timeline, and how to monitor (asc status --app {asc_app_id}).

<failure_modes>

  • asc validate error: missing screenshots → wrong display type or wrong dimensions. App Store "iPhone 6.5 Display" = IPHONE_65; screenshots must be exactly 1242x2688.
  • Screenshots show a fake /store route, marketing card, or invented overlay copy → discard the set. Capture the real app screens only, with demo data if needed.
  • pricing availability edit: app availability not found → availability was never initialized; use POST /v2/appAvailabilities (v2!) with all territories, then retry.
  • Subscription review screenshot stuck in AWAITING_UPLOAD/processing → delete the asset and re-upload the PNG.
  • Paywall screenshot shows "Continue (Dev)"EXPO_PUBLIC_ALLOW_DEV_PAYMENT_BYPASS was true during capture; set it false, relaunch, recapture.
  • Maestro types stray characters into inputs → erase and retype (eraseText: 50 then inputText), keep one input per flow step.
  • Wrong app loads in simulator → another Metro on port 8081; kill it or pin ports (see docs/troubleshooting.md).
  • Review rejection: terms not visible → subscription apps must link Terms of Use in the description or EULA field; Phase C appends it.
  • Submission blocked on App Privacy → web-only questionnaire not published yet; it must show "published" before submit. </failure_modes>

<success_metrics>

  • All screenshots are 1242x2688 raw captures of the real app UI, demo data only, uploaded to the right localization + IPHONE_65 display type.
  • Metadata, category, copyright, age rating, content rights all set via API (idempotent — re-running is safe).
  • Subscription (if any) READY_TO_SUBMIT with localized name, price, availability, and review screenshot.
  • asc validate returns zero errors.
  • Review submitted only after explicit user confirmation, with the demo account in review notes. </success_metrics>

Signals

GitHub stars
31
Forks
5
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
ns-ios-distribute
Source
github.com/melvynx/saveit.now