Цепочка intent исчерпана. Каждый package, который вы пробовали, вернулся пустым, и теперь посетитель без приложения стоит на маршруте, которому больше некуда его отправить, кроме стора — или вашей собственной веб-страницы взамен. Ошибитесь на этом последнем шаге, и это будет неотличимо от любой другой поломки: посетитель уходит, и ничто в вашей воронке не подскажет, действительно ли приложения нет, построена ли ваша ссылка на стор с ошибкой, или она ведёт на листинг, которого больше не существует — тот же диагностический туман, что и у постбэка, который так и не пришёл.
Ошибиться здесь можно тремя отдельными способами, и ни один из них не выглядит как ошибка, пока вы целенаправленно её не ищете.
Две формы
Store fallback на Android и iOS — это не одно и то же значение в разном синтаксисе. Одно выводится из того, что у вас уже есть; другое нужно найти и сохранить отдельно.
| Платформа | URL store fallback |
|---|---|
| Android | https://play.google.com/store/apps/details?id=<package> |
| iOS | https://apps.apple.com/app/id<numeric id> |
Fallback на Android детерминирован. Это та же строка package, которую вы уже использовали для построения цепочки intent://, просто переформулированная как query-параметр — если вы знаете com.instagram.android достаточно, чтобы таргетировать его, вы уже знаете и URL его страницы в Play Store, без отдельного поиска.
iOS не даёт такого сокращения. Числовой id — это отдельное, непрозрачное значение, которое App Store присваивает каждому листингу, и его нельзя вывести из названия приложения, bundle identifier или чего-либо ещё, что у вас, вероятно, уже есть под рукой. Числовые id App Store невозможно угадать. Вы ищете каждый вручную один раз и сохраняете его рядом с названием package — и, в отличие от package, никакая fallback-логика не восстановит его, если поиск так и не был сделан.
Эту асимметрию легко упустить, пока вы строите android-часть цепочки, потому что fallback для Android достаётся бесплатно в момент, когда вы уже выбрали package для таргетинга. Fallback для iOS не достаётся бесплатно ни с чем. Это отдельный, независимый этап сбора данных по каждому приложению, и пропустить его — не значит громко провалиться: это просто значит, что fallback для iOS тихо отсутствует именно тогда, когда цепочка в нём нуждается.
Когда отсутствующий id означает «исчез», а не «неизвестно»
Пустое поле id для iOS обычно означает лишь то, что его ещё никто не искал. Иногда это означает нечто другое: искать нечего, потому что самого листинга больше не существует.
Microsoft закрыла потребительский Skype в мае 2025 года. Приложение всё ещё можно установить на Android — его листинг в Play Store и название package не пострадали, — но листинга в App Store, к которому можно привязать id на iOS, больше нет. Это не временный пробел, ожидающий, пока разработчик внесёт правильное числовое значение. Это постоянное состояние именно для этой платформы, тогда как android-часть той же fallback-цепочки остаётся настолько же валидной, какой была всегда.
Это различие имеет практическое значение. Отсутствующий package на Android обычно означает «найти его», и исправление — просто пойти и найти строку. Отсутствующий id на iOS может означать то же самое, а может означать «этот листинг исчез и не вернётся», и обращаться со вторым случаем как с первым — оставляя его как TODO до следующего спринта — просто означает, что тот же мёртвый поиск повторят позже снова. Как только вы подтвердили, что листинг закрыт, а не просто не зафиксирован, уберите fallback для iOS для этого приложения полностью и дайте цепочке завершиться на вашей собственной веб-странице именно на iOS, оставив запись для Android нетронутой, поскольку Android ни разу не пострадал от того, что убрало листинг на iOS.
Shopee: когда отсутствие fallback лучше, чем fallback
У большинства приложений — один глобальный листинг в сторе на каждую платформу, и именно это делает таблицу с двумя формами выше пригодной как шаблон. У Shopee иначе. Она поставляет отдельный Android package на каждую страну — com.shopee. плюс код страны для каждого из одиннадцати рынков, от Индонезии и Бразилии до Чили и Аргентины, — потому что работает как одиннадцать отдельных национальных витрин, а не как один продукт с одним листингом.
Эта структура означает, что нет единой глобальной страницы Shopee, на которую можно сделать fallback. Выберите листинг любой одной страны как «тот самый» store fallback для Shopee — и вы попадёте правильно ровно для одного из тех одиннадцати рынков, а остальных десятерых посетителей отправите на листинг не на том языке, не в той валюте и, вполне возможно, даже не доступный для установки в их регионе.
Это как раз то решение, которое стоит принимать осознанно, а не по умолчанию: для Shopee отсутствие store fallback лучше, чем неправильный. Посетитель, который попадает на вашу собственную веб-страницу после неудачной попытки открыть приложение, по крайней мере попадает куда-то правильно, независимо от того, с какого из одиннадцати рынков он пришёл. Посетитель, направленный на листинг не той страны в App Store или Play Store, не может отличить «приложения нет» от «меня направили не в ту страну», а страница стора на иностранном языке выглядит как поломка так, как ваша собственная веб-страница не выглядит. Пропустите шаг со стором именно для Shopee и дайте цепочке завершиться там, где она действительно верна везде: на вашей странице, а не на догадке о том, к какому из одиннадцати сторов принадлежит посетитель.
Та же структура с одиннадцатью package, которая заставляет вас пробовать com.shopee.id, com.shopee.br, com.shopee.vn и остальные по очереди на Android, — это именно то, что делает единый store fallback невозможным ни на одной платформе. Нет единой Shopee, на которую можно сделать fallback, потому что нет единой Shopee — их одиннадцать, и вопрос fallback имеет чёткий ответ только для приложений, где это не так.
Где на самом деле место fallback
Store fallback — это не только выбор URL, но и вопрос времени, и ошибка со временем тихо ломает кандидатов, которые идут перед ним.
S.browser_fallback_url в ссылке intent:// принимает только значения http и https; значение market://, помещённое туда, Android просто отбрасывает. А Chrome требует настоящего пользовательского жеста, чтобы вообще запустить навигацию intent:// — без него Chrome полностью пропускает приложение и переходит прямо на S.browser_fallback_url — то же тихое приземление в браузере, которое возникает всюду, где пропадает пользовательский жест, — что снаружи страницы выглядит идентично «приложения нет».
Соедините эти два поведения — и последствие легко упустить, пока оно не стоит вам кандидата. Если вы прикрепите fallback URL к первому package в цепочке с несколькими package, посетитель, у которого просто нет этого первого package — но установлен второй, — будет вытолкнут в браузер до того, как второго кандидата вообще попробуют. Fallback срабатывает слишком рано и съедает конверсию, которая должна была состояться. Это касается каждого шага в цепочке, включая последний: ни один кандидат не несёт собственный fallback URL.
Fallback принадлежит после того, как исчерпаны все кандидаты, а не встроен в отдельный шаг, и именно ваша собственная страница — а не цепочка intent — решает, когда этот момент наступил. Сначала попробуйте каждый package по очереди; только когда последний провалился, отправляйте посетителя на ссылку стора, и только если эта ссылка действительно существует и ведёт в правильную страну.
Перед тем как запускать fallback
| Проверка | Готово |
|---|---|
Ни один кандидат в цепочке intent не несёт собственный browser_fallback_url, включая последний | ☐ |
| Каждый числовой id iOS найден и сохранён по каждому приложению, никогда не выведен и не угадан | ☐ |
| Закрытый листинг iOS убран из цепочки именно для него, запись для Android оставлена без изменений | ☐ |
| Приложения без единой глобальной страницы стора полностью пропускают store fallback, а не выбирают один рынок | ☐ |
| Ссылка на стор срабатывает только после того, как все package-кандидаты испробованы и провалились | ☐ |
FAQ
Можно ли просто вставить market:// в S.browser_fallback_url и положиться на Android?
Нет. S.browser_fallback_url принимает только значения http и https — URL market://, помещённый туда, отбрасывается, а не конвертируется, так что параметр ведёт себя так, будто вы его вообще не задавали.
Что произойдёт, если посетитель тапнет ссылку без настоящего пользовательского жеста?
Chrome требует пользовательский жест, чтобы запустить навигацию intent://. Без него Chrome вообще не пытается открыть приложение — он сразу переходит на S.browser_fallback_url, и снаружи страницы это неотличимо от того, что приложения действительно нет.
Всегда ли безопасно трактовать отсутствующий id App Store как «ещё не искали»?
Только пока вы не проверили. По умолчанию трактуйте его как неизвестный, но подтвердите это, прежде чем строить автоматизацию вокруг повторного поиска — некоторые id отсутствуют потому, что самого листинга больше нет, и закрытому приложению нечего искать.
Где живёт это правило
Держите правило порядка в одном месте, а не повторяйте его в каждой цепочке intent, которую вы строите: сначала попробуйте каждого package-кандидата, без fallback URL ни на одном из них, и только передавайте посетителя на ссылку стора — или никуда, как в случае с Shopee — когда весь список исчерпан. Streams от DarkCore применяет этот порядок по умолчанию, отдельно по каждой платформе, так что закрытый листинг iOS или приложение с привязкой к стране без глобальной страницы стора — это правило маршрутизации, которое вы настраиваете один раз, а не сбой, который находите уже в продакшене.