@shopify/app-bridge-core
Version:
**[Join our team and work on libraries like this one.](https://www.shopify.ca/careers)**
193 lines (190 loc) • 6.87 kB
JavaScript
import { fromAction, AppActionType, invalidOriginAction } from './actions/Error/index.js';
import { isAppMessage, isAppBridgeAction } from './actions/validator.js';
import { MessageType } from './client/types.js';
import { addAndRemoveFromCollection } from './util/collection.js';
import { isUnframed } from './util/env.js';
var Context;
(function (Context) {
Context["Modal"] = "Modal";
Context["Main"] = "Main";
})(Context || (Context = {}));
/**
* Create a MessageTransport from a Frame.
* @remarks
* Used on the host-side to create a postMessage MessageTransport.
* @beta
*/
function fromFrame(frame, localOrigin, context) {
const handlers = [];
const { host, window: frameWindow } = frame;
if (!host) {
throw fromAction('App frame is undefined', AppActionType.WINDOW_UNDEFINED);
}
if (isUnframed && window.MobileWebView) {
Object.assign(window.MobileWebView, {
postMessageToIframe(message, origin) {
frameWindow?.postMessage(message, origin);
if (isDispatchAction(message)) {
host.postMessage(JSON.stringify(message.payload), location.origin);
}
},
updateIframeUrl(newUrl) {
const currentWindowLocation = window.location;
const { location: frameWindowLocation } = frame.window || {};
try {
const newUrlOrigin = new URL(newUrl).origin;
if (newUrlOrigin === localOrigin && frameWindowLocation) {
frameWindowLocation.replace(newUrl);
}
else {
currentWindowLocation.href = newUrl;
}
}
catch (_) {
// Noop
}
},
});
}
host.addEventListener('message', (event) => {
if (event.source === host || !isAppMessage(event)) {
return;
}
if (event.origin !== localOrigin) {
const errorMessage = `Message origin '${event.origin}' does not match app origin '${localOrigin}'.`;
const payload = invalidOriginAction(errorMessage);
const message = {
type: 'dispatch',
payload,
};
frameWindow?.postMessage(message, event.origin);
return;
}
if (isUnframed && window.MobileWebView) {
const payload = JSON.stringify({
id: 'unframed://fromClient',
origin: localOrigin,
data: event.data,
});
window.MobileWebView.postMessage(payload);
return;
}
for (const handler of handlers) {
handler(event);
}
});
return {
context,
localOrigin,
frameWindow,
hostFrame: host,
dispatch(message) {
frameWindow?.postMessage(message, localOrigin);
},
subscribe(handler) {
return addAndRemoveFromCollection(handlers, handler);
},
};
}
/**
* Create a MessageTransport from a parent window.
* @remarks
* Used on the client-side to create a postMessage MessageTransport.
* @internalremarks
* In unframed mode, message should be dispatched via MobileWebView.postMessage instead of postMessage.
* @beta
*/
function fromWindow(contentWindow, localOrigin) {
const handlers = [];
if (typeof window !== undefined) {
window.addEventListener('message', (event) => {
if ((window === contentWindow && !isUnframed) ||
event.source !== contentWindow ||
!(isAppBridgeAction(event.data.payload) || isAppMessage(event))) {
return;
}
for (const handler of handlers) {
handler(event);
}
});
}
return {
localOrigin,
hostFrame: contentWindow,
dispatch(message) {
if (!message.source?.host) {
return;
}
if (isUnframed && window && window.MobileWebView) {
const payload = JSON.stringify({
id: 'unframed://fromClient',
origin: localOrigin,
data: message,
});
window.MobileWebView.postMessage(payload);
return;
}
const messageOrigin = new URL(`https://${message.source.host}`).origin;
contentWindow.postMessage(message, messageOrigin);
},
subscribe(handler) {
return addAndRemoveFromCollection(handlers, handler);
},
};
}
function createTransportListener() {
const listeners = [];
const actionListeners = {};
function createSubscribeHandler(dispatcher) {
function subscribe() {
if (arguments.length < 2) {
// eslint-disable-next-line prefer-rest-params
return addAndRemoveFromCollection(listeners, { callback: arguments[0] });
}
// eslint-disable-next-line prefer-rest-params
const [type, callback, id] = Array.from(arguments);
const actionCallback = { callback, id };
const payload = { type, id };
if (!Object.prototype.hasOwnProperty.call(actionListeners, type)) {
actionListeners[type] = [];
}
if (dispatcher) {
dispatcher(MessageType.Subscribe, payload);
}
return addAndRemoveFromCollection(actionListeners[type], actionCallback, () => {
if (dispatcher) {
dispatcher(MessageType.Unsubscribe, payload);
}
});
}
return subscribe;
}
return {
createSubscribeHandler,
handleMessage(message) {
listeners.forEach((listener) => listener.callback(message));
},
handleActionDispatch({ type, payload }) {
let hasCallback = false;
if (Object.prototype.hasOwnProperty.call(actionListeners, type)) {
for (const listener of actionListeners[type]) {
const { id, callback } = listener;
const matchId = payload && payload.id === id;
if (matchId || !id) {
callback(payload);
hasCallback = true;
}
}
}
return hasCallback;
},
};
}
function isDispatchAction(message) {
return (message !== null &&
typeof message === 'object' &&
!Array.isArray(message) &&
message.type === 'dispatch' &&
typeof message.payload === 'object');
}
export { Context, createTransportListener, fromFrame, fromWindow };