Магазины: App Store, Google Play, RuStore и первая отправка
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Прошлые статьи закончились двумя файлами — .aab и .ipa — и проверенной иконкой. Остался последний шаг: отдать сборку людям. У «Подхода» три магазина: App Store, Google Play и RuStore. У каждого свой кабинет, свои анкеты и свой способ принять файл. В первые два сборку отправляет одна команда, eas submit; у RuStore пути в EAS нет, зато есть открытый API. Разберём, что завести до первой отправки, как eas submit выбирает сборку и настройки, что проверяют магазины и что происходит после «All done!»: где сборка ждёт, кто её смотрит и когда её увидят пользователи. И отдельно — ловушку, которая бьёт как раз по приложению, где главная копия данных живёт в телефоне: подписи Google Play и RuStore не совпадают.
Сама отправка занимает минуту, но это последняя минута долгой подготовки. Сборку некуда положить, пока в магазине нет карточки приложения, а карточку не завести без аккаунта разработчика. Команде из терминала нужен ещё и ключ, с которым магазин пустит её от вашего имени.
| App Store | Google Play | RuStore | |
|---|---|---|---|
| Аккаунт | Apple Developer Program, 99 долларов в год | Play Console, 25 долларов один раз | RuStore Консоль, регистрация бесплатна |
| Карточка приложения | запись в App Store Connect; если её нет, eas submit заведёт её сам, войдя в ваш аккаунт Apple | только руками: кнопка Create app в Play Console, через API приложение не создать | в консоли RuStore |
| Ключ для отправки из терминала | ключ App Store Connect API; eas-cli умеет создать его сам | JSON-ключ сервисного аккаунта Google Cloud с доступом к Play Console | ключ Public API: идентификатор и закрытый ключ |
| Кто отправляет | eas submit | eas submit | свой скрипт на Public API или вы сами в консоли |
| Проверка | App Review; по словам Apple, 90% заявок проверяют быстрее чем за сутки | проверка Google; у новых личных аккаунтов перед продакшном — обязательный закрытый тест | модерация; по опыту разработчиков — от нескольких часов до нескольких рабочих дней |
Ключ Google eas-cli просит при первой отправке — «A Google Service Account JSON key is required to upload your app to Google Play Store» — и может хранить его на серверах EAS, рядом с keystore из статьи «EAS Build». Тогда в репозитории ключа нет, а отправлять можно с любой машины.
Разработчику из России стоит заранее сверить с этой таблицей и деньги. Google с 26 декабря 2024 года не проводит платежи через Play Billing, если счёт для выплат в России. В справке при этом прямо сказано, что новые бесплатные приложения публиковать и обновлять можно. С 1 апреля 2026 года российские операторы отключили оплату покупок в App Store со счёта мобильного телефона, а это был самый ходовой в России способ платить. RuStore платит в рублях, но монетизацию через свои платёжные инструменты с 1 февраля 2026 года оставил только ИП и юрлицам. «Подход» бесплатный, и ни одно из этих ограничений его не касается. Платные функции потребовали бы отдельного разговора.
Карточку приложения в Google Play заводят руками. eas submit кладёт сборки в готовую карточку, но создать её не может.
Как отправлять, описывает секция submit в eas.json, и у автора она занимает одну строку: "submit": { "production": {} }. Пустой профиль — не ошибка: всё, чего в нём нет, допишет схема @expo/eas-json. Отправка последней сборки для Android:
Сводка перед отправкой — это разрешённый профиль отправки. Ни трек internal, ни статус COMPLETED в eas.json автора не написаны: их дописала схема. Строка Rollout: undefined — не опечатка статьи. eas-cli 22.2.0 кладёт поле в сводку, даже когда значения нет, и так его и печатает. «All done!» значит одно: Google Play принял файл. Кто и когда увидит сборку, решает трек — о треках секция «Что происходит после отправки».
Без флагов команда спрашивает, что отправлять: сборку с EAS, ссылку, файл или ID сборки. С --latest она берёт последнюю сборку платформы — но «последнюю» среди каких? В submit/utils/builds.js запрос к серверу несёт фильтр distribution: Store и четыре статуса: new, in queue, in progress и finished. Профиль отправки, если --profile не указан, ищется по имени профиля, которым собрана сборка. В подложке — разрешение профиля сборки и выбор файла из статьи «EAS Build» и eas.json автора, теперь вместе с секцией submit. Прогноз: что уйдёт в магазин по каждой из трёх команд и с какими настройками?
Для Android выбрана a1, хотя после неё были ещё две сборки. preview собирается с distribution: "internal" и под фильтр «для магазина» не подходит, а упавшая a3 отсеялась по статусу. Поэтому --latest не отправит в Google Play тестовый .apk, даже если он самый свежий. Для iOS выбрана i3, которая ещё собирается: недостроенная сборка фильтр проходит. Отправка встанет ждать её со строкой «Submitting your app to Apple App Store Connect: waiting for the associated build to complete». Если последняя сборка для магазина — не та, что вы хотели отправить, --latest об этом не спросит.
Третья команда называет сборку по ID. Фильтра здесь нет, профиля отправки preview тоже нет, и eas-cli одной строкой перешёл на production. В Google Play поехал бы .apk, а новые приложения Google Play с августа 2021 года принимает только в формате App Bundle. У iOS схема дописывает один язык. Он нужен, когда eas-cli сам заводит запись приложения в App Store Connect. ascAppId, Apple ID и ключ API он либо спросит, либо найдёт на серверах EAS.
| Поле профиля отправки | Что решает |
|---|---|
android.track | трек Google Play: internal (по умолчанию), alpha, beta, production или имя своего закрытого трека |
android.releaseStatus | completed (по умолчанию), draft, halted или inProgress — постепенный выкат |
android.rollout | доля пользователей от 0 до 1; схема требует её при inProgress и запрещает при остальных статусах |
android.changesNotSentForReview | создать выпуск, но не отправлять изменения на проверку, пока их не отправят из консоли |
android.serviceAccountKeyPath | путь к JSON-ключу, если он хранится не на серверах EAS |
ios.ascAppId, ios.appleId, ios.appleTeamId | какое приложение в App Store Connect и чей аккаунт |
ios.groups | группы внутренних тестировщиков TestFlight, которым сразу достанется сборка |
Слияние у профилей отправки ещё проще, чем у профилей сборки: extends раскрывается, но от каждого профиля берётся только секция нужной платформы, и сливается она в один уровень. Как собрать на этом профиль постепенного выката — первая задача к статье.
Первая отправка в Google Play по умолчанию уходит на внутренний тест, а не в продакшн. Так решает схема eas.json, а не вы.
Требования бывают двух видов. Одни проверяет автомат при загрузке, и отправка падает сразу. Другие смотрят люди на проверке, и отказ приходит через день. Вот главные для «Подхода»:
| Требование | Кто и когда проверяет | Как у «Подхода» |
|---|---|---|
С 31 августа 2026 года новые приложения и обновления должны нацеливаться на Android 16, targetSdk 36; отсрочку можно попросить до 1 ноября 2026 года | Google Play, при загрузке | React Native 0.86 ставит targetSdk = "36" в gradle/libs.versions.toml: требование выполнено без правок |
Новое приложение — в формате .aab | Google Play, при загрузке | профиль production даёт .aab |
| У каждой загрузки новый номер сборки | все три магазина, при загрузке; RuStore требует номер выше активной версии | номер ведёт EAS: autoIncrement и appVersionSource: "remote" из статьи «EAS Build» |
| Иконка без альфа-канала | App Store Connect, при загрузке | Expo снимает альфу сам — статья «Иконка, сплэш, имя, версия» |
Ответ про шифрование: ITSAppUsesNonExemptEncryption | App Store Connect: без ответа сборку нельзя тестировать | eas-cli спрашивает перед сборкой «iOS app only uses standard/exempt encryption?». Статичный конфиг он правит сам, а для app.config.js, как у автора, печатает «Add the following to …» с кусочком JSON — вписать его придётся руками |
| Удаление аккаунта внутри приложения | App Review, правило 5.1.1(v); Google Play требует ещё и веб-страницу для запроса на удаление, её указывают в анкете Data safety | в «Подходе» есть вход, значит, нужен и экран удаления аккаунта |
| Демо-аккаунт для проверяющего и включённый сервер | App Review, правило 2.1 | вход в приложение закрыт логином — проверяющему нужны готовые данные для входа |
| Анкеты о данных: App Privacy и Data safety | App Store Connect и Play Console, до отправки на проверку | дневник синхронизируется с сервером — это нужно указать в анкетах |
Отдельно проверяют карточку. Иконку для Google Play, 512 на 512, обещала прошлая статья.
| App Store | Google Play | |
|---|---|---|
| Иконка карточки | берётся из сборки — тот самый PNG 1024 | PNG 512 × 512, 32-битный, до 1024 КБ; квадрат в край, скругление и тень Google Play добавит сам |
| Скриншоты | от 1 до 10; нужен набор 6,9″ или 6,5″, например 1284 × 2778. Скриншоты iPad — только если приложение работает на iPad, а у автора supportsTablet: false | не меньше двух, стороны от 320 до 3840 пикселей; JPEG или 24-битный PNG без альфы |
| Обложка | нет | обязательна: 1024 × 500, JPEG или 24-битный PNG без альфы |
Иконку карточки Google Play скругляет сам, и радиус его маски уже менялся. Спецификация 2019 года обещала скругление в 20% стороны, нынешняя — 30%, то есть 153,6 пикселя из 512. У автора в icon-512.png тёмный квадрат скруглён радиусом 112 и лежит на белом, это мы выяснили в прошлой статье. В подложке он нарисован тем же SVG, что и там, а рядом — квадрат в край, как просит спецификация. Маску карточки изображает View с overflow: "hidden", как маску лаунчера в статье «Иконка, сплэш, имя, версия», а белые пиксели под маской считает функция подложки через холст страницы. Прогноз: у какой иконки под какой маской останутся белые углы?
Под нынешней маской белого нет: её скругление больше, чем 112 из 512, и Google срезает углы глубже, чем их скруглил автор. Под маской 2019 года по углам проступила бы белая кайма. Она тонкая: на диагонали угла скругление 112 отступает от края на 46,4 пикселя из 512, а маска 20% — на 42,4, так что белого остаётся полоска около четырёх пикселей. На экране примера иконка вчетверо меньше, кайма там уже пикселя и видна разве что светлым ободком на углу — поймал её счёт пикселей, а не глаз. Выходит, иконка автора проходит по совпадению: стоит Google снова уменьшить радиус, и кайма вернётся. Квадрат в край переживает любую маску, поэтому его и просят спецификации Google и Apple. Прозрачные углы тоже не выход: сквозь них на карточке проступит фон самого магазина.
Анкеты, демо-аккаунт и удаление аккаунта готовят до отправки на проверку: автомат их не ловит, отказ придёт от проверяющего.
В Google Play «All done!» значит, что на треке создан выпуск. Треков четыре вида: внутренний тест (internal) до 100 тестировщиков из списка, закрытый (alpha и свои закрытые треки), открытый (beta) и production. Первая отправка с профилем автора ляжет во внутренний тест. Expo пишет, что такой первый выпуск eas submit создаёт сам. Если же Google ответит «Only releases with status draft may be created on draft app», приложение ещё числится черновиком. Тогда API примет выпуск только с releaseStatus: "draft", а довести его придётся кнопкой в консоли.
До продакшна у нового личного аккаунта есть обязательная остановка. Если личный аккаунт в Play Console создан после 13 ноября 2023 года, приложение должно пройти закрытый тест: не меньше 12 тестировщиков, подписанных без перерыва хотя бы 14 дней. Только потом в консоли подают заявку на доступ к продакшну и отвечают на вопросы о тесте. Для «Подхода» это две недели, которые стоит заложить в план до даты релиза. Дальше выпуск в production уходит на проверку, а выкатить его можно постепенно: releaseStatus: "inProgress" с долей rollout. Долю поднимают в консоли, а halted останавливает выкат.
У App Store путь другой. eas submit кладёт .ipa в App Store Connect, и больше ничего:
Сборка сначала обрабатывается, об этом говорит сам eas-cli. Потом она появляется в TestFlight. Внутренние тестировщики из вашей команды получают её сразу, а внешним первую сборку показывают только после проверки бета-версии. На проверку в App Store eas submit не отправляет. Это делают в App Store Connect: создают версию, выбирают сборку, заполняют карточку и анкеты и отправляют заявку. После одобрения версию выпускают вручную, автоматически или не раньше заданной даты. Обновления, но не первую версию, можно выкатывать постепенно — семь дней, на 1, 2, 5, 10, 20, 50 и 100% пользователей с автоматическими обновлениями. Паузы в сумме — до 30 дней.
У RuStore шаги видны прямо в API: черновик версии, загрузка файлов, отправка на модерацию. Что будет после одобрения, решает publishType черновика: INSTANTLY (по умолчанию) — выпустить сразу, MANUAL — ждать вашей кнопки, DELAYED — выпустить в publishDateTime. Постепенный выкат задаёт partialValue: 5, 10, 25, 50, 75 или 100 процентов.
| App Store | Google Play | RuStore | |
|---|---|---|---|
| Куда ложится файл | App Store Connect, затем TestFlight | выпуск на треке, по умолчанию internal | черновик версии |
| Кто смотрит | бета-проверка для внешних тестировщиков, App Review для магазина | проверка Google; перед продакшном у новых личных аккаунтов — закрытый тест 12 × 14 | модерация RuStore |
| Кто нажимает «выпустить» | вы в App Store Connect или автоматически после одобрения | статус выпуска из профиля отправки или вы в консоли | publishType: сразу, вручную или по дате |
| Постепенный выкат | семь дней, только для обновлений | inProgress и rollout | partialValue |
У eas submit два магазина, и RuStore среди них нет. Вместо этого у RuStore есть Public API: получить токен, создать черновик версии, загрузить файл, отправить на модерацию. Ниже набросок по документации API. Ключей RuStore у нас нет, поэтому скрипт не запускался:
Токен живёт 900 секунд и передаётся в заголовке Public-Token. Черновик у приложения может быть только один. Главный APK помечается isMainApk=true: без такого файла версию на модерацию не отправить. Для .aab есть свой адрес, /aab. Но RuStore сам нарезает из набора APK и подписывает их ключом подписи приложения, а этот ключ нужно заранее загрузить в консоль. RuStore ключей не генерирует и не хранит их за вас.
Какой APK положить в form? Кажется, что подойдёт APK, подписанный тем же keystore из EAS, — собранный профилем с buildType: "apk" или вынутый из .aab. Здесь и ловушка. С Play App Signing Google Play подписывает APK для телефонов своим ключом подписи приложения. Keystore, который создал EAS, для Google — лишь ключ загрузки: им подписан отправленный .aab. RuStore же отдаёт APK с той подписью, с которой его загрузили. Android ставит обновление поверх установленного приложения, только если сертификат совпадает. Два телефона, на обоих «Подход» из Google Play. Прогноз: что увидят Анна и Борис?
У Анны Android отказал с INSTALL_FAILED_UPDATE_INCOMPATIBLE, и RuStore в таком случае предлагает удалить приложение и поставить заново. Для обычного приложения это просто повторный вход. Для «Подхода» — потеря: главная копия дневника и неотправленный журнал из главы 6 лежат в файле базы, а удаление приложения стирает его. Синхронизированное вернётся с сервера после входа, неотправленное — нет. Хуже того, ловушка работает в обе стороны: теперь у Анны подпись EAS, и следующее обновление из Google Play не встанет тоже.
Борис получил в RuStore APK с подписью Google, и оба магазина обновляют его по очереди. Такой файл выдаёт сама Play Console: в разделе Test and release → Latest releases and bundles выбрать набор, открыть вкладку Downloads и скачать строку «Signed, universal APK» — один APK, подписанный тем же ключом, что и приложение в Google Play. Справка Google прямо предлагает раздавать его в других магазинах и на сайтах, чтобы приложение везде было подписано одним ключом. Второй путь, из той же справки, — при создании приложения передать Google копию своего ключа подписи и подписывать им сборки для всех магазинов. Если ключ сгенерировал Google, .aab в RuStore отправить не выйдет: ключа подписи у вас нет. Остаётся универсальный APK.
Вопрос подписи решают до первой публикации во втором магазине. У тех, кто уже поставил приложение с другой подписью, обновление поверх не встанет — только переустановка.
Подпись и номер сборки — два замка, которыми Android стережёт уже установленное приложение: обновление встаёт поверх, только если сертификат тот же, а номер больше. Магазин при этом — не единственный путь нового кода на телефон. Следующая глава — об обновлениях по воздуху и о третьем замке, runtimeVersion. Этот замок решает, какому из установленных бинарников можно прислать новый бандл по воздуху, без новой сборки и без очереди на проверку.
- До первой отправки в каждом магазине заводят аккаунт, карточку приложения и ключ для терминала. App Store — 99 долларов в год, и запись в App Store Connect eas-cli создаст сам. Google Play — 25 долларов один раз, карточка только руками. RuStore — бесплатно, отправка через Public API.
eas submitберёт профиль из секцииsubmit, а пустое дописывает схема: Android по умолчанию уходит на трекinternalсо статусомcompleted.--latestвыбирает последнюю сборку сdistribution: store, в том числе недостроенную. Профиль отправки без--profileищется по имени профиля сборки, а если такого нет — берётся production.- Автомат при загрузке проверяет формат (
.aab),targetSdk(36 с 31 августа 2026 года), номер сборки и альфу в иконке iOS. Люди на проверке смотрят удаление аккаунта, демо-вход, анкеты о данных и карточку: иконку 512 для Google Play — квадратом в край, маску со скруглением 30% магазин наложит сам, — скриншоты и обложку 1024 × 500. - После отправки у каждого магазина свой путь. Google Play: трек, у новых личных аккаунтов закрытый тест 12 тестировщиков на 14 дней, постепенный выкат через
inProgressиrollout. App Store: обработка, TestFlight, заявка на App Review в App Store Connect, семидневный выкат обновлений. RuStore: черновик, файлы, модерация иpublishType. - Google Play с Play App Signing подписывает APK своим ключом, а RuStore отдаёт файл как загружен. APK с подписью keystore EAS в RuStore не обновит приложение, поставленное из Google Play, и переустановка сотрёт данные в телефоне. В RuStore кладут подписанный универсальный APK из Play Console или с самого начала подписывают все магазины своим ключом.
1/4В eas.json автора секция "submit": { "production": {} }. Куда попадёт сборка после eas submit --platform android --latest?
2/4Последней собрали preview для Android, до неё — production. Что отправит eas submit --platform android --latest?
3/4Анна поставила приложение из Google Play (подпись — ключ Google). В RuStore загрузили .apk той же сборки, подписанный keystore EAS, с большим versionCode. Что будет у Анны?
4/4eas-cli напечатал «Your binary has been successfully uploaded to App Store Connect!». Приложение уже на проверке в App Store?
Постепенный выкат на десятую часть
Версия 1.1 «Подхода» идёт в продакшн Google Play, но сначала пусть её получит десятая часть пользователей. Коллега завёл профиль отправки staged, а схема его не пропускает. В подложке — разрешение профиля из статьи и проверка секции android теми же правилами, что в схеме @expo/eas-json. Поправьте профиль.
Доля выката разрешена только постепенному выпуску. В схеме у rollout правило when("releaseStatus", { is: "inProgress", then: required(), otherwise: forbidden() }), а статус по умолчанию — completed. Значит, статус нужно назвать явно:
Теперь печатается { track: "production", releaseStatus: "inProgress", changesNotSentForReview: false, rollout: 0.1 }. Настоящий eas-cli проверяет весь eas.json при чтении, и ошибку из условия он напечатал бы с полным путём: «eas.json is not valid.» и строкой "submit.staged.android.rollout" is not allowed. Отправляют такой профиль командой eas submit --platform android --profile staged. Долю потом поднимают в Play Console, а если в первые дни пошли падения, выкат останавливают статусом halted. И помните про новый личный аккаунт: трек production откроется только после закрытого теста.
Где застрял релиз
Первая неделя после отправки. Для каждой ситуации скажите, что произошло и что делать.
| № | Ситуация |
|---|---|
| 1 | eas submit -p android кончился «All done!», но друзья приложение в Google Play не находят |
| 2 | Google Play Developer API ответил «Only releases with status draft may be created on draft app» |
| 3 | Сборка iOS час назад загружена в App Store Connect, но тестировщикам её не выдать: App Store Connect ждёт ответа про шифрование |
| 4 | Пользователь, поставивший приложение из Google Play, пишет: RuStore просит удалить «Подход» и поставить заново |
| 5 | App Review отклонил версию по правилу 5.1.1(v) |
| 6 | API RuStore отклонил загрузку APK: номер версии не выше активного |
1. Профиль отправки по умолчанию — track: "internal": выпуск лежит во внутреннем тесте, и видят его только тестировщики из списка. В продакшн ведёт track: "production". У личного аккаунта, созданного после 13 ноября 2023 года, туда пустят только после закрытого теста: 12 тестировщиков, 14 дней, заявка в консоли.
2. Приложение в Google Play ещё черновик, и API принимает только выпуск со статусом draft. Поставить в профиле отправки releaseStatus: "draft", отправить и довести выпуск в Play Console.
3. У app.config.js автора eas-cli не может сам вписать ITSAppUsesNonExemptEncryption и только печатает, что добавить. Ответить на вопрос в App Store Connect для этой сборки, а в конфиг дописать ios.infoPlist.ITSAppUsesNonExemptEncryption: false, если приложение использует только стандартное шифрование, — тогда следующие сборки пройдут без вопроса.
4. В RuStore лежит APK с подписью keystore EAS, а у пользователя — приложение из Google Play с подписью Google. Удалять его не нужно: обновления из Google Play встают как прежде, а RuStore перестанет предлагать переустановку, когда там появится подписанный универсальный APK из Play Console с номером выше лежащего. Замена файла ударит по другим — по тем, кто уже поставил «Подход» из RuStore с подписью EAS: обновление из RuStore у них больше не встанет. Им остаётся переустановка, и сначала синхронизация: пусть откроют приложение с сетью, чтобы неотправленный журнал ушёл на сервер. Чем раньше заменить файл, тем меньше таких пользователей.
5. В приложении есть регистрация, но нет удаления аккаунта. Нужен экран удаления внутри приложения, а для Google Play ещё и веб-страница для запроса, указанная в анкете Data safety.
6. RuStore принимает только версию с номером выше активной. Номер ведёт EAS (autoIncrement), но только для сборок EAS. Универсальный APK из Play Console несёт тот номер, с которым набор ушёл в Google Play. Поэтому в RuStore кладут APK той же сборки, которая уже прошла в Google Play, и порядок номеров в двух магазинах совпадает.