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.
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'));