Создать APK бесплатно

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://, так что страница с собственной маршрутизацией продолжает работать.