All entries

April 11, 2026

07f685777e6f676066441f6ac7f52a2fb531568c93cdc5f19a652b47311dd262
Previous: d934d47f Bundle: 89.3 KB

Added mobile safe area inset support via a CSS custom property when the MobileBridgeNext flag is enabled.

Highlights

  • Bootstrap now injects a --shopify-safe-area-inset-bottom CSS custom property initialized to 0px when the MobileBridgeNext feature flag is active.
  • The safe area inset bottom value is automatically subscribed to from the host and kept in sync as a CSS variable on the root element.
  • A body::after pseudo-element is added to reserve physical space at the bottom of the page equal to the safe area inset height.
6 files changed +43 -30

Infrastructure Changes

REPORT.md
+5 -5
@@ -1,14 +1,14 @@
# Shopify App Bridge — Unminification Report

Generated: 2026-03-26T08:14:27.154Z
Generated: 2026-04-11T08:10:05.932Z

## Files

| File | Size | Lines | Type |
|------|------|-------|------|
| _bootstrap.js | 31.2KB | 1106 | Infrastructure |
| _bootstrap.js | 31.9KB | 1122 | Infrastructure |
| _remote-ui.js | 6.0KB | 254 | Infrastructure |
| _utilities.js | 69.3KB | 2574 | Infrastructure |
| _utilities.js | 69.2KB | 2571 | Infrastructure |
| _web-vitals.js | 11.6KB | 527 | Infrastructure |
| analytics.js | 211B | 11 | Module |
| app.js | 346B | 15 | Module |
@@ -18,7 +18,7 @@ Generated: 2026-03-26T08:14:27.154Z
| id-token.js | 640B | 28 | Module |
| index.js | 2.2KB | 48 | Index |
| intents.js | 2.7KB | 89 | Module |
| internal-only.js | 528B | 26 | Module |
| internal-only.js | 539B | 26 | Module |
| loading.js | 605B | 31 | Module |
| navigation.js | 416B | 19 | Module |
| picker.js | 451B | 18 | Module |
@@ -46,7 +46,7 @@ Generated: 2026-03-26T08:14:27.154Z
| user.js | 940B | 37 | Module |
| visibility.js | 973B | 34 | Module |
| web-vitals.js | 1.8KB | 64 | Module |
| **Total** | **169.3KB** | **6435** | |
| **Total** | **169.8KB** | **6448** | |

## Pipeline Stages


modules/_bootstrap.js
+18 -2
@@ -604,7 +604,7 @@
      }
      function e() {
        document.head.appendChild(jn());
        const t = U();
        const t = createDeferred();
        window.top?.postMessage(
          {
            type: 'load',
@@ -662,7 +662,7 @@
          });
        })(t);
        window.addEventListener('beforeunload', () => {
          const t = U();
          const t = createDeferred();
          window.top?.postMessage(
            {
              type: 'unload',
@@ -1040,7 +1040,23 @@
    })(p, b, l);
    y.resolve(t);
  })();
  (function (t) {
    if (!g('MobileBridgeNext')) return;
    const n = document.createElement('style');
    n.textContent =
      "\n    :root { --shopify-safe-area-inset-bottom: 0px; }\n    body::after { content: ''; display: block; height: var(--shopify-safe-area-inset-bottom); }\n  ";
    document.head.appendChild(n);
    t.then((t) => {
      const n = t?.safeAreaInsets?.bottom;
      if (!n) return;
      const e = (t) => {
        document.documentElement.style.setProperty('--shopify-safe-area-inset-bottom', t + 'px');
      };
      e(n.value ?? 0);
      n.subscribe?.((t) => e(t));
    });
  })(P);
  p.send('Loading.stop');
  w.ready = Promise.resolve();
})();

modules/_utilities.js Truncated
+17 -20
@@ -190,11 +190,11 @@ function subscribeAllErrors({ keys, held, handler, keyEvent: i = 'keydown' }) {
    });
  };
}
function U() {
function createDeferred() {
  const t = window.shopify.config.host;
  return 'https://' + atob(t);
}
const createDeferred = Symbol();
const U = Symbol();
const N = Symbol();
const j = Symbol();
const D = Symbol();
@@ -243,7 +243,7 @@ function G(t, { onChange, filter: e = () => true }) {
  function o(t) {
    const n = 'target' in t ? t.target : t;
    if (n) {
      if (!n[createDeferred]) {
      if (!n[U]) {
        if ('values' in n) {
          n[j] = n.values;
        }
@@ -262,7 +262,7 @@ function G(t, { onChange, filter: e = () => true }) {
  function r(t) {
    const n = t.target;
    if (n) {
      n[createDeferred] = true;
      n[U] = true;
      i();
    }
  }
@@ -270,7 +270,7 @@ function G(t, { onChange, filter: e = () => true }) {
    const n = t.target;
    if (X(n)) {
      for (const t of n.elements) {
        t[createDeferred] = false;
        t[U] = false;
        o(t);
      }
      i();
@@ -294,10 +294,10 @@ function G(t, { onChange, filter: e = () => true }) {
        if (D in t) {
          t.checked = t[D];
        }
        t[createDeferred] = false;
        t[U] = false;
        o(t);
        K(t);
        t[createDeferred] = false;
        t[U] = false;
      }
      i();
    }
@@ -402,7 +402,7 @@ function X(t) {
}
function J(t) {
  return (
    t[createDeferred] === true &&
    t[U] === true &&
    (('value' in t && t.value !== (t[N] ?? t.defaultValue)) ||
      ('values' in t && t.values !== (t[j] ?? t.defaultValue)) ||
      ('checked' in t && t.checked !== (t[D] ?? t.defaultChecked)))
@@ -732,19 +732,19 @@ class Mt {
    this[deepClone]();
  }
}
class safeAsyncCall {
class xt {
  constructor(t) {
    this.complete = t;
  }
}
function xt(t) {
function safeAsyncCall(t) {
  return typeof t != 'object' || t === null
    ? t
    : Array.isArray(t)
      ? t.map((t) => xt(t))
      ? t.map((t) => safeAsyncCall(t))
      : Object.keys(t).reduce((n, e) => {
          const i = t[e];
          n[e] = xt(i);
          n[e] = safeAsyncCall(i);
          return n;
        }, {});
}
@@ -756,8 +756,8 @@ async function SHOPIFY_PROTOCOLS(t) {
}
const ALL_PROTOCOLS = ['shopify:', 'app:', 'extension:'];
const WINDOW_TARGETS = [...ALL_PROTOCOLS, 'https:', 'http:'];
const Ut = ['_self', '_top', '_parent', '_blank'];
const CLICKABLE_TAGS = ['_self', '_top', '_parent', '_blank'];
const CLICKABLE_TAGS = ['a', 's-link', 's-button', 's-clickable'];
const Ut = ['a', 's-link', 's-button', 's-clickable'];
const SIMULATING_CLICK = Symbol('SIMULATING_CLICK');
function parseUrl(t, n) {
  addEventListener('click', function e(i) {
@@ -784,10 +784,7 @@ function Dt(t, n = true) {
}
function qt(t, n, e) {
  const i = Dt(t);
  const o = [
  const o = [...Ut.map((t) => 'ui-nav-menu > ' + t), ...Ut.map((t) => t)].join(',');
    ...CLICKABLE_TAGS.map((t) => 'ui-nav-menu > ' + t),
    ...CLICKABLE_TAGS.map((t) => t),
  ].join(',');
  return Array.from(document.querySelectorAll(o)).filter((t) => {
    const o = t.getAttribute('href');
    const r = t.getAttribute('target') ?? '_self';
@@ -860,7 +857,7 @@ const zt = ({ internalApiPromise, saveBarManager, rpcEventTarget }) => {
        if (!t) return;
        let n = t;
        for (; n; ) {
          if (n instanceof Element && CLICKABLE_TAGS.includes(n.nodeName.toLowerCase())) {
          if (n instanceof Element && Ut.includes(n.nodeName.toLowerCase())) {
            if (n.getAttribute('href') == null) {
              n = n.parentNode;
              continue;
@@ -938,7 +935,7 @@ const zt = ({ internalApiPromise, saveBarManager, rpcEventTarget }) => {
  function c(e, i, o = '', a = false) {
    let s = i;
    const c = Dt(e);
    if (!Ut.includes(s) || !WINDOW_TARGETS.includes(c.protocol)) return false;
    if (!CLICKABLE_TAGS.includes(s) || !WINDOW_TARGETS.includes(c.protocol)) return false;
    const l = `${c.pathname}${c.search}`;
    if (c.protocol === 'shopify:' && !l.startsWith('/admin/'))
      throw Error(`Invalid URL: expected '/admin/*', received: '${l}'.`);

Diff truncated at 200 lines

Module Changes

modules/intents.js
+1 -1
@@ -54,7 +54,7 @@ const intentsModule = ({ api, protocol, internalApiPromise }) => {
      if (!i) throw Error('Cannot invoke intent');
      if (!i.intents?.invoke || typeof i.intents.invoke != 'function')
        throw Error('Intents are not supported');
      return new safeAsyncCall(i.intents.invoke(t, n));
      return new xt(i.intents.invoke(t, n));
    },
  };
  if (it()) {

modules/internal-only.js
+1 -1
@@ -6,7 +6,7 @@
const internalOnlyModule = ({ api, internalApiPromise }) => {
  const e = {
    async show(t, e) {
      const i = xt(e);
      const i = safeAsyncCall(e);
      const o = await internalApiPromise;
      if (o && o.internalModal) {
        await o.internalModal.show?.(t, i);

modules/title-bar.js
+1 -1
@@ -24,7 +24,7 @@ const titleBarModule = ({ protocol, internalApiPromise }) => {
  function r(t) {
    const n = document.querySelector('s-page');
    if (n) {
      const e = `${On}, ${Mn}, ${Rn}`;
      const e = `${On}, ${Mn}, ${xn}`;
      const i = Array.from(n.querySelectorAll(e)).find((n) => n[vn] == t);
      if (i) return void i.click();
      const o = Array.from(document.querySelectorAll('s-menu, s-button-group'));