JavaScript bridge API
Запуск, ярлыки и кнопка «Назад»
Как было открыто приложение, ярлыки при долгом нажатии на значок, запрос оценки в приложении и сообщение оболочке, что странице нужно нажатие «Назад».
AppMint.requestReview помощник#
AppMint.requestReview(options)
Пример
Просит Google Play показать собственное окно оценки внутри приложения (Play In-App Review) и сообщает, что произошло.
Возвращает: Promise, который всегда завершается успешно {result, openedStore, message?}. result — это 'shown' (Play показал свою форму — он никогда не сообщает, поставил ли пользователь оценку), 'not_from_play' (эта установка не из Google Play: APK установлен вручную или тестовая установка, где Play не показывает форму), 'quota' (Play решил не показывать её сейчас: квота для этого пользователя исчерпана или он уже оставил отзыв) или 'error' (Play не удалось запросить: нет Play Store, нет сети). С {openStore: true} страница приложения в Play открывается, если форма не появилась, а openedStore равно true. Требуется: ничего включать не нужно.
Любой код:
// A "Rate us" button: the sheet when Play shows it, the Play listing otherwise.
document.getElementById('rate-btn').onclick = async function () {
if (!(window.AppMint && AppMint.requestReview)) { location.href = 'https://play.google.com/store/apps/details?id=com.example.app'; return; }
var r = await AppMint.requestReview({ openStore: true });
if (r.result === 'shown' || r.openedStore) showToast('Thank you!');
};Спросите после момента успеха, а не по кнопке, никогда не выходя из приложения:
async function onOrderDelivered(count) {
if (count !== 3 || !(window.AppMint && AppMint.requestReview)) return;
var r = await AppMint.requestReview(); // no openStore: nothing opens if Play says no
if (r.result === 'not_from_play') console.log('Test install: Play only shows the sheet for Play installs');
}Когда создатель включил Просить пользователей оценить приложение (шаг «Отображение»), приложение само спрашивает в спокойный момент и сообщает странице, что произошло:
window.addEventListener('appmint:review', function (e) {
// e.detail = { result, openedStore: false, source: 'auto' }
analytics.track('rate_prompt', { result: e.detail.result });
});Примечания: Google Play ограничивает частоту показа окна и сам решает, появится ли оно; никогда не поощряйте отзыв и не обещайте окно на кнопке. Проверяйте на установке с внутреннего тестового трека Play — APK, установленный вручную, всегда отвечает not_from_play.
Без кода: ссылка appmint://review. Кнопка или ссылка с этим адресом запрашивает то же окно (и открывает страницу в Play, когда Play его не покажет, потому что пользователь сам попросил оценить). В сообщении внутри приложения поместите её на кнопку в центре отправки (так делает шаблон Оцените нас); на странице используйте как обычную ссылку:
<a href="appmint://review" class="rate-link">Rate us on Google Play</a>Из скрипта, например после третьего завершённого заказа:
function maybeAskForRating(ordersDone) {
if (ordersDone === 3) location.href = 'appmint://review'; // the app runs it; nothing is loaded
}Узнайте, что выбрал пользователь в сообщении «Оцените нас» внутри приложения, до открытия окна:
window.addEventListener('appmint:inapp', function (e) {
var d = e.detail;
if (d.type === 'answered' && d.question === 'stars' && d.value <= 3) showFeedbackForm();
if (d.type === 'clicked' && d.link === 'appmint://review') console.log('Opening the Play review sheet');
});AppMint.setBackHandler помощник#
AppMint.setBackHandler(handler)
Пример
Даёт вашей странице право первого слова, когда пользователь нажимает кнопку «Назад» телефона (или использует жест «назад»). Нужно приложениям, которые меняют экраны, показывая и скрывая элементы, где история Android пуста и «Назад» закрыл бы приложение.
Возвращает: ничего. Ваш handler должен вернуть true (обработано, остаться здесь) или false (позволить приложению вернуться / выйти). Требуется: ничего. Устанавливается при старте документа, поэтому его можно зарегистрировать в самом первом скрипте.
if (window.AppMint && AppMint.setBackHandler) {
AppMint.setBackHandler(function () {
if (isMenuOpen) { closeMenu(); return true; } // consumed
if (currentScreen !== 'home') { goTo('home'); return true; }
return false; // nothing to undo: normal Back (history back, exit dialog, exit)
});
}
// Remove it again, for example when the screen that needed it closes:
// AppMint.setBackHandler(null);Событие appmint:back — отменяемое. Вызовите e.preventDefault(), чтобы поглотить нажатие. Оно срабатывает до обработчика, и его могут использовать любое число слушателей:
window.addEventListener('appmint:back', function (e) {
var sheet = document.querySelector('.bottom-sheet.open');
if (sheet) {
sheet.classList.remove('open');
e.preventDefault(); // consumed: the handler and the shell do nothing more
}
});Не дайте потерять наполовину написанную заметку: «Назад» показывает вашу собственную панель «Отменить изменения?» вместо выхода (блокирующий confirm() не уложился бы в окно ответа в 350 мс, и оболочка всё равно вернулась бы назад):
if (window.AppMint && AppMint.setBackHandler) {
var noteDirty = false;
document.getElementById('note').addEventListener('input', function () { noteDirty = true; });
AppMint.setBackHandler(function () {
if (!noteDirty) return false; // nothing to lose: normal Back
showDiscardBar(); // your in-page "Discard changes? / Keep editing" bar
return true; // stay here while the user decides
});
document.getElementById('discard-btn').onclick = function () {
noteDirty = false; // the next Back press passes straight through
hideDiscardBar();
closeEditor();
};
}Примечания: ответ должен быть синхронным: возврат Promise считается «не обработано». Отвечайте быстро — если страница не отвечает примерно за 350 мс, оболочка сама возвращается назад. Порядок при каждом нажатии: открытое боковое меню (собственная панель приложения) сначала закрывается, и страницу не спрашивают; затем слушатели appmint:back → ваш обработчик → встроенное закрытие диалогов оболочки (закрывает открытый <dialog>, диалог ARIA или полноэкранное окно) → назад по истории / подтверждение выхода / выход. Приложениям, использующим history.pushState для каждого экрана (React Router и подобные), это не нужно. Приложения, созданные ИИ, могут использовать onBackGesture из @/lib/appmintNative.
__pullRefreshEligible мост#
window.WebToApk.__pullRefreshEligible(eligible: Boolean)
См. [pullPageEligible] / [pullProbeJs]. Вызывается при каждом touchstart.
Пример
Внутренняя часть за обновлением потягиванием: собственная проба касаний оболочки вызывает её при каждом касании, чтобы сказать, должно ли потягивание вниз там обновлять страницу. Страницы никогда её не вызывают — они управляют обновлением потягиванием стандартными CSS и событиями касания.
Возвращает: ничего. Требуется: Включить обновление потягиванием, включённое на шаге 3 (Интеграция) в разделе Параметры отображения при сборке (без этого проба не устанавливается).
Официальный способ не дать жесту «потянуть для обновления» срабатывать на части вашей страницы — перехватите жест:
<style>
/* A map, drawing canvas or slider: this element owns vertical drags. */
#map, #canvas { touch-action: none; }
/* A horizontal carousel: pan-x also tells the shell "not a refresh". */
.carousel { touch-action: pan-x; }
</style>Или захватите это из обработчика касания, стандартным способом:
document.getElementById('canvas').addEventListener('touchstart', function (e) {
e.preventDefault(); // the page took this gesture: no pull-to-refresh
}, { passive: false });Не давайте поверхности с панорамированием и масштабированием обновлять страницу, захватив вертикальные перетаскивания только средствами CSS:
<div id="board" style="touch-action: none; height: 60vh"></div>
<script>
// Dragging on #board moves the board; a pull from the top of the rest of the page still refreshes.
document.getElementById('board').addEventListener('pointermove', function (e) { moveBoard(e); });
</script>Примечания: потягивание обновляет только когда ближайший прокручиваемый блок под пальцем (или сама страница) прокручен вверх, нажат только один палец и не действует ни одно из заявлений выше. Касание внутри iframe с другого источника никогда не обновляет. Не вызывайте этот метод сами.
getAppIcon мост#
window.WebToApk.getAppIcon(): String
Используемый значок запуска: «default» или одно из названий из getAvailableIcons().
Пример
Используемый значок запуска: 'default' или одно из названий из getAvailableIcons().
Возвращает: строку. 'default' в сборке без альтернативных значков.
if (window.WebToApk && WebToApk.getAppIcon) {
const current = WebToApk.getAppIcon(); // 'default', 'Dark', …
document.querySelectorAll('[data-icon]').forEach(el => {
el.classList.toggle('selected', el.dataset.icon === current);
});
}Переключайтесь на следующий значок с экрана настроек:
function nextIcon() {
if (!(window.WebToApk && WebToApk.getAvailableIcons && WebToApk.setAppIcon)) return;
const names = JSON.parse(WebToApk.getAvailableIcons());
if (names.length < 2) return; // no alternate icons in this build
const next = names[(names.indexOf(WebToApk.getAppIcon()) + 1) % names.length];
if (!WebToApk.setAppIcon(next)) alert('Could not change the icon.');
}Покажите название значка в подписи или скройте его вне приложения:
const label = document.getElementById('icon-label');
if (window.WebToApk && WebToApk.getAppIcon) {
const name = WebToApk.getAppIcon();
label.textContent = name === 'default' ? 'Classic icon' : name + ' icon';
} else {
label.hidden = true; // in a browser there is no launcher icon
}getAvailableIcons мост#
window.WebToApk.getAvailableIcons(): String
JSON-массив названий значков, которые принимает setAppIcon: сначала «default», затем альтернативные значки сборки.
Пример
Названия значков, которые принимает setAppIcon, в виде строки с JSON-массивом: сначала 'default', затем альтернативные значки из мастера сборки в порядке их добавления.
Возвращает: '["default","Dark","Gold"]' или '["default"]' в сборке без альтернативных значков.
Создайте выбор значка:
function iconPicker(container) {
if (!(window.WebToApk && WebToApk.getAvailableIcons)) return;
const names = JSON.parse(WebToApk.getAvailableIcons());
if (names.length < 2) { container.hidden = true; return; } // nothing to choose
const current = WebToApk.getAppIcon();
container.innerHTML = '';
names.forEach(name => {
const b = document.createElement('button');
b.textContent = name === 'default' ? 'Classic' : name;
b.className = name === current ? 'selected' : '';
b.onclick = () => { if (WebToApk.setAppIcon(name)) iconPicker(container); };
container.appendChild(b);
});
}
iconPicker(document.querySelector('#iconPicker'));Скройте выбор в сборке без альтернативных значков:
const picker = document.getElementById('iconPicker');
let names = ['default'];
if (window.WebToApk && WebToApk.getAvailableIcons) names = JSON.parse(WebToApk.getAvailableIcons());
picker.hidden = names.length < 2; // only 'default' means there is nothing to chooseПереключайте значок вместе с темой:
function matchIconToTheme(dark) {
if (!(window.WebToApk && WebToApk.getAvailableIcons && WebToApk.setAppIcon)) return;
const names = JSON.parse(WebToApk.getAvailableIcons());
const wanted = dark ? 'Dark' : 'default';
if (names.includes(wanted) && WebToApk.getAppIcon() !== wanted) WebToApk.setAppIcon(wanted);
}
matchIconToTheme(matchMedia('(prefers-color-scheme: dark)').matches);getLaunchUrl мост#
window.WebToApk.getLaunchUrl(): String
Глубокая ссылка, запустившая или возобновившая это приложение, или "", если оно открыто обычным образом. Читайте её при каждом монтировании приложения и сами выбирайте маршрут: const u = window.WebToApk?.getLaunchUrl?.(); if (u) router.navigate(new URL(u).pathname); Значение СОХРАНЯЕТСЯ, а не расходуется, так что поздно смонтированное приложение всё равно его увидит; более поздняя ссылка, пришедшая при работающем приложении, заменяет его и также вызывает 'appmint:deep-link'.
Пример
Глубокая ссылка, открывшая (или снова открывшая) приложение, в виде строки, или "", когда приложение открыто обычным образом.
Возвращает: строку синхронно. Требуется: ничего. Значение хранится, а не расходуется, поэтому его безопасно читать при каждом монтировании приложения.
Ссылка может прийти в трёх видах: https://yoursite.com/path, собственный appmint-shortcut://<package>/<route> оболочки (ярлыки лаунчера, виджет на главном экране) или ваша собственная схема myapp://segment/path. Эта вспомогательная функция превращает все три в маршрут:
function routeOf(link) {
var m = /^([a-z][a-z0-9+.-]*):\/\/([^\/?#]*)([^#]*)/i.exec(link || '');
if (!m) return '/';
var scheme = m[1].toLowerCase(), host = m[2], rest = m[3] || '';
if (scheme === 'http' || scheme === 'https' || scheme === 'appmint-shortcut') return rest || '/';
return '/' + host + rest; // myapp://product/42 -> /product/42
}
// 1. On start: the link that launched the app.
if (window.WebToApk && typeof window.WebToApk.getLaunchUrl === 'function') {
var link = window.WebToApk.getLaunchUrl();
if (link) goTo(routeOf(link));
}
// 2. While running: a new link replaces the old one and fires 'appmint:deep-link'.
window.addEventListener('appmint:deep-link', function (e) {
goTo(routeOf(e.detail.url)); // detail: { url }
});То же объявление передаётся и в глобальный хук, если вы предпочитаете один:
window.onAppMintDeepLink = function (detail) { goTo(routeOf(detail.url)); };Прочитайте реферальный код из ссылки вроде https://yoursite.com/invite?code=AB12 или myapp://invite?code=AB12 один раз на ссылку (используется routeOf выше):
function applyInvite(link) {
if (!link) return;
var done = '';
try { done = sessionStorage.getItem('handledLink') || ''; } catch (e) {}
if (link === done) return; // the same url is announced again after a reload
try { sessionStorage.setItem('handledLink', link); } catch (e) {}
var code = null;
try { code = new URL(link).searchParams.get('code'); } catch (e) { return; }
if (code && routeOf(link).indexOf('/invite') === 0) showInviteBanner(code);
}
if (window.WebToApk && typeof window.WebToApk.getLaunchUrl === 'function') {
applyInvite(window.WebToApk.getLaunchUrl());
window.addEventListener('appmint:deep-link', function (e) { applyInvite(e.detail.url); });
}Примечания: appmint:deep-link также срабатывает после каждой загрузки страницы, пока ссылка сохраняется, поэтому один и тот же url может прийти не раз. Игнорируйте ссылку, которую вы уже обработали.
Приложения в режиме сайта работают иначе: оболочка сама загружает входящую ссылку https:// (при холодном старте и во время работы), а ссылка с собственной схемой myapp://product/42 сначала переписывается на адрес вашего сайта (https://yoursite.com/product/42), поэтому getLaunchUrl() возвращает вид https://, и вашей странице обычно нечего выбирать. Нажатие на ярлык лаунчера или виджет открывает свой маршрут на вашем сайте (/today загружает https://yoursite.com/today), а getLaunchUrl() по-прежнему возвращает вид appmint-shortcut:// (см. setAppShortcuts). В приложениях HTML, ZIP и созданных ИИ (обслуживаемых изнутри приложения) оболочка никогда не переходит сама: ваша страница получает ссылку и выбирает по ней маршрут. Ссылка с собственной схемой остаётся там как пришла, потому что нет адреса сайта, на который её можно перенести. Приложения, созданные ИИ, могут использовать launchUrl() из @/lib/appmintNative.
rateApp мост#
window.WebToApk.rateApp(): String
Открывает процесс оценки на странице Play. `rateApp()` уже существовал, но достичь его мог только нативный пункт боковой панели, поэтому приложение, рисующее собственный интерфейс — любая сборка ИИ, где боковая панель выключена, — не могло попросить оценку. Это единственное действие магазина, которое страница на самом деле не может выполнить сама: `market://` — это intent, а не URL для перехода. Зависит от собственного переключателя создателя «Кнопка «Оценить приложение»», точно как пункт боковой панели и пункт «Поделиться» нижней навигации: выключенная оценка должна означать отсутствие поверхности оценки где-либо, иначе переключатель выглядит сломанным.
Пример
Открывает страницу приложения в Google Play, чтобы пользователь мог его оценить.
Возвращает: 'opened', когда страница в магазине открыта, 'off', когда создатель выключил «Оценить приложение» (тогда ничего не происходит). Требуется: переключатель Кнопка «Оценить приложение» на шаге 5 (Меню) при сборке (по умолчанию включён; он также добавляет пункт «Оценить приложение» в боковое меню). При выключении вызов ничего не делает и возвращает 'off'. Для окна оценки Google Play внутри приложения используйте AppMint.requestReview().
var rateBtn = document.getElementById('rate-btn');
if (window.WebToApk && typeof window.WebToApk.rateApp === 'function') {
rateBtn.onclick = function () { window.WebToApk.rateApp(); };
} else {
rateBtn.hidden = true; // in a browser
}Предложите оценку один раз, после того как пользователь завершил пять тренировок, из карточки, на которую он нажимает:
function maybeOfferRating(completedCount) {
if (!window.WebToApk || typeof window.WebToApk.rateApp !== 'function') return;
var asked = false;
try { asked = localStorage.getItem('rateAsked') === '1'; } catch (e) {}
if (asked || completedCount < 5) return;
var card = document.getElementById('rate-card'); // "Enjoying the app? Rate it on Google Play"
card.hidden = false;
card.querySelector('.rate-yes').onclick = function () {
window.WebToApk.rateApp(); // opens the Play listing
try { localStorage.setItem('rateAsked', '1'); } catch (e) {}
card.hidden = true;
};
card.querySelector('.rate-later').onclick = function () { card.hidden = true; };
}Строка «Оцените нас» на вашем экране настроек, которая говорит спасибо, когда пользователь возвращается из Google Play (возврат страницы на передний план — единственный сигнал, что пользователь вернулся):
document.getElementById('settings-rate').onclick = function () {
if (!window.WebToApk || typeof window.WebToApk.rateApp !== 'function') return;
window.WebToApk.rateApp();
document.addEventListener('visibilitychange', function onBack() {
if (document.visibilityState !== 'visible') return;
document.removeEventListener('visibilitychange', onBack);
showToast('Thanks for taking a look!'); // your own toast; the user may not have rated
});
};Примечания: Открывает страницу в Play Store (market://details?id=<package>) или сайт Play, если приложения Play Store нет. Это не карточка отзыва внутри приложения. Страница не может сама открыть market:// — это intent Android, а не веб-адрес. Просите оценку только после того, как пользователю что-то понравилось, и только по нажатию.
requestReview мост#
window.WebToApk.requestReview(optionsJson: String, callbackId: String)
Запрашивает у Play форму отзыва; [optionsJson] `{openStore?}`. Возвращает AppMint.requestReview [callbackId].
Пример
Внутренняя часть за AppMint.requestReview() — страницы используют этот помощник на промисах, который просит Google Play показать окно оценки в приложении и завершается с тем, что произошло.
Возвращает: ничего напрямую; результат завершает промис AppMint.requestReview(). Требуется: ничего включать не нужно. Окно появляется только в приложении, установленном из Google Play.
Публичный API:
async function askForRating() {
if (!(window.AppMint && AppMint.requestReview)) return; // a browser: no store here
var r = await AppMint.requestReview();
console.log(r.result); // 'shown' | 'not_from_play' | 'quota' | 'error'
}Спросите после удачного момента и предложите страницу магазина, если окно не появилось:
async function afterHappyMoment() {
if (!(window.AppMint && AppMint.requestReview)) return;
const r = await AppMint.requestReview({ openStore: true }); // {result, openedStore, message?}
if (r.result === 'shown') thankUser();
else if (r.openedStore) console.log('Opened the Play listing instead');
}Слушайте автоматический запрос и сообщайте об ошибке:
window.addEventListener('appmint:review', (e) => {
if (e.detail.source === 'auto') console.log('Rate prompt result:', e.detail.result);
});
AppMint.requestReview().then((r) => {
if (r.result === 'error') console.warn(r.message);
if (r.result === 'not_from_play') showMessage('Rating is available once the app is installed from Google Play.');
});setAppIcon мост#
window.WebToApk.setAppIcon(name: String): Boolean
Меняет значок запуска приложения на один из альтернативных значков, добавленных в мастере сборки, по названию, указанному там («default» = обычный значок). Возвращает false для названия, которого нет в этой сборке. Лаунчер перерисовывает значок в своё время (на некоторых телефонах несколько секунд), а некоторые лаунчеры закрывают приложение или перемещают его значок на главном экране при смене — переключайте с экрана настроек, а не посреди задачи.
Пример
Меняет значок запуска приложения на один из альтернативных значков, добавленных вами в мастере сборки (шаг «Основное», «Альтернативные значки приложения»), по названию, которое вы там дали. 'default' — ваш обычный значок.
Возвращает: true, когда переключение выполнено (или это уже тот значок), false для названия, которого нет в этой сборке. Требуется: альтернативные значки, добавленные в мастере (до 4). Некоторые лаунчеры перерисовывают значок несколько секунд, а некоторые закрывают приложение или перемещают его ярлык на главном экране при смене: переключайте с экрана настроек, а не посреди задачи. Ваши ярлыки лаунчера (setAppShortcuts) автоматически переопубликовываются под новым значком.
function useIcon(name) {
if (!(window.WebToApk && window.WebToApk.setAppIcon)) return false;
const ok = WebToApk.setAppIcon(name);
if (!ok) console.warn('no icon called', name);
return ok;
}
document.querySelector('#darkIcon').addEventListener('click', () => useIcon('Dark'));
document.querySelector('#normalIcon').addEventListener('click', () => useIcon('default'));Постройте выбор из значков, которые есть в этой сборке:
if (window.WebToApk && WebToApk.getAvailableIcons) {
const names = JSON.parse(WebToApk.getAvailableIcons()); // 'default' first, then your alternates
const current = WebToApk.getAppIcon();
names.forEach((n) => addRadio('icon', n, n === current, () => WebToApk.setAppIcon(n)));
}Проверьте ответ на название, которого нет в сборке:
function pickIcon(name) {
if (!(window.WebToApk && WebToApk.setAppIcon)) return showMessage('Icons can be changed in the app only');
if (!WebToApk.setAppIcon(name)) {
showMessage('Unknown icon "' + name + '". Choose one of: ' + JSON.parse(WebToApk.getAvailableIcons()).join(', '));
} else {
showMessage('Done. Your launcher may take a few seconds to redraw the icon.');
}
}Примечания: первое обновление, которое ДОБАВЛЯЕТ альтернативные значки, переносит запись лаунчера в новый компонент, поэтому ярлык на главном экране, созданный до этого обновления, возможно, придётся разместить заново.
setAppShortcuts мост#
window.WebToApk.setAppShortcuts(itemsJson: String)
Ярлыки лаунчера (долгое нажатие на значок приложения). До четырёх записей: `[{id, title, route}]`, где `route` — путь в приложении ('/new-note'). Нажатие открывает приложение с этим маршрутом как глубокой ссылкой запуска (`appmint-shortcut://<package><route>`), которую страница читает через `getLaunchUrl()` и обрабатывает так же, как любую другую глубокую ссылку. В приложении в режиме сайта оболочка сама открывает маршрут на сайте (`/new-note` → `https://<site>/new-note`). Передайте `[]`, чтобы убрать их. Android 7.1+; ниже ничего не делает.
Пример
Задаёт ярлыки лаунчера, которые появляются, когда пользователь нажимает на значок приложения и удерживает.
Возвращает: ничего. Требуется: ничего. Android 7.1+ (ниже ничего не делает).
itemsJson — это JSON-строка: массив до четырёх объектов {id, title, route}. route — путь внутри вашего приложения. Нажатие открывает приложение с appmint-shortcut://<package><route> в качестве ссылки запуска:
if (window.WebToApk && typeof window.WebToApk.setAppShortcuts === 'function') {
window.WebToApk.setAppShortcuts(JSON.stringify([
{ id: 'new-note', title: 'New note', route: '/new-note' },
{ id: 'search', title: 'Search', route: '/search' }
]));
}Прочитайте нажатие так же, как любую другую глубокую ссылку, при запуске и во время работы:
function shortcutRoute(link) {
var m = /^appmint-shortcut:\/\/[^\/?#]*([^#]*)/i.exec(link || '');
return m ? (m[1] || '/') : null;
}
if (window.WebToApk && window.WebToApk.getLaunchUrl) {
var r = shortcutRoute(window.WebToApk.getLaunchUrl());
if (r) goTo(r);
}
window.addEventListener('appmint:deep-link', function (e) {
var r = shortcutRoute(e.detail.url);
if (r) goTo(r);
});Удалите все ярлыки:
if (window.WebToApk && window.WebToApk.setAppShortcuts) window.WebToApk.setAppShortcuts('[]');Примечания: Каждый вызов заменяет весь список. Записи без title пропускаются; подпись обрезается до 25 символов (50 для длинной подписи). id по умолчанию равен appmint-<index>. Если у маршрута нет начального /, он добавляется. Значок — это значок приложения. Некорректный список игнорируется (записывается в лог), исключение не выбрасывается. Приложения, созданные ИИ, могут использовать shortcuts из @/lib/appmintNative. Приложения в режиме сайта: нажатие открывает маршрут на вашем сайте — /new-note загружает https://<your site>/new-note — а getLaunchUrl() по-прежнему возвращает ссылку appmint-shortcut://, так что страница с собственной маршрутизацией продолжает работать.