@shopify/app-bridge-host
Version:
App Bridge Host contains components and middleware to be consumed by the app's host, as well as the host itself. The middleware and `Frame` component are responsible for facilitating communication between the client and host, and used to act on actions se
242 lines (238 loc) • 11.6 kB
JavaScript
;
var tslib = require('tslib');
var client = require('@shopify/app-bridge-core/client');
var validator = require('@shopify/app-bridge-core/actions/validator');
var appBridgeCore = require('@shopify/app-bridge-core');
var collection = require('@shopify/app-bridge-core/util/collection');
var Error = require('@shopify/app-bridge-core/actions/Error');
var constants = require('@shopify/app-bridge-core/actions/constants');
var actions = require('./actions.js');
var clientValidator = require('./clientValidator.js');
var store_index = require('./store/index.js');
function buildMiddleware(key, dispatchClientEventHandlerRef, errorHandler) {
var transports = [];
var transportListener = appBridgeCore.createTransportListener();
var subscribe = transportListener.createSubscribeHandler();
var hostListener = appBridgeCore.createTransportListener();
var hostSubscribe = hostListener.createSubscribeHandler();
var store;
var actionsQueue;
var receivedMessageFromClient = false;
var middleware = function (hostStore) {
store = hostStore;
actionsQueue = store_index.createActionsQueue(store);
return function (next) {
return function (action) {
if (validator.isAppBridgeAction(action) && receivedMessageFromClient) {
for (var _i = 0, transports_1 = transports; _i < transports_1.length; _i++) {
var transport = transports_1[_i];
var features = store.getState()[key].features[transport.context];
if (validator.isPermitted(features, action, client.PermissionType.Subscribe)) {
transport.dispatch({
payload: action,
type: 'dispatch',
});
}
}
}
var appState = store.getState()[key];
if (validator.isAppBridgeAction(action)) {
if (store_index.isReducerLoaded(appState, action)) {
/**
* Triggers the host listeners related to this action
*/
hostListener.handleActionDispatch(action);
}
else {
/**
* This queue fixes a race condition on Shopify POS which
* dispatches POS info and other information directly from the host
* via globalStore.dispatch.
* See https://github.com/Shopify/app-bridge/issues/2722
*/
var appBridgeAction = action;
actionsQueue.addHostAction(appBridgeAction);
}
}
if (actions.isLoadReducerCompleteAction(action)) {
actionsQueue.resolve(action.payload.feature);
}
return next(action);
};
};
};
function provideApplicationInterface(data) {
var config = data.config;
var clientHandlers = createClientHandlers();
store.dispatch(actions.apiClientLoad(config));
return {
attach: function (to) {
var _this = this;
var contextualClientHandlers = clientHandlers[to.context];
contextualClientHandlers.unsubscribe();
actionsQueue.clear(to.context);
var unsubscribe = to.subscribe(function (event) {
var context = to.context;
var message = event.data;
var type = message === null || message === void 0 ? void 0 : message.type;
var action = message === null || message === void 0 ? void 0 : message.payload;
var source = message === null || message === void 0 ? void 0 : message.source;
if (!clientValidator.isValidConfig(source, config)) {
if (errorHandler)
return errorHandler(clientValidator.InvalidConfigError(source, config, action));
clientValidator.throwInvalidConfigError(source, config, action);
}
if (to.frameWindow !== event.source) {
return;
}
receivedMessageFromClient = true;
transportListener.handleMessage(message);
switch (type) {
case 'dispatch': {
var appState = _this.getState();
var features = appState.features[context];
if (!features || !validator.isPermitted(features, action, client.PermissionType.Dispatch)) {
store.dispatch(Error.permissionAction(action));
return;
}
transportListener.handleActionDispatch(action);
if (store_index.isReducerLoaded(appState, action)) {
store.dispatch(tslib.__assign(tslib.__assign({}, action), { source: source }));
}
else {
actionsQueue.add(to.context, action);
}
if (validator.isPerformanceOrWebVitalsAction(action)) {
break;
}
if (dispatchClientEventHandlerRef === null || dispatchClientEventHandlerRef === void 0 ? void 0 : dispatchClientEventHandlerRef.current) {
var appId = config.appId, shopId = config.shopId;
dispatchClientEventHandlerRef.current({
action: action,
appId: appId,
shopId: shopId,
});
}
break;
}
case 'getState': {
var defaultState = _this.getState();
var features = defaultState.features[context];
var state = tslib.__assign(tslib.__assign({}, defaultState), { features: appendLegacyAction(features), context: context });
to.dispatch({
type: type,
payload: state,
});
break;
}
case 'subscribe':
contextualClientHandlers.subscribe(action);
break;
case 'unsubscribe':
contextualClientHandlers.unsubscribe(action);
break;
default: {
var error = Error.fromAction('Unknown message type. Expected `dispatch` or `getState`.', Error.Action.INVALID_ACTION, message);
if (errorHandler)
return errorHandler(error);
throw error;
}
}
});
var detach = collection.addAndRemoveFromCollection(transports, to, unsubscribe);
return function (unload) {
if (unload === void 0) { unload = true; }
var origin = new URL(config.url).origin;
var removed = detach() && !transports.find(function (transport) { return transport.localOrigin === origin; });
contextualClientHandlers.unsubscribe();
if (removed) {
actionsQueue.clear(to.context);
}
if (removed && unload) {
store.dispatch(actions.apiClientUnload(config));
}
};
},
dispatch: function (action) {
store.dispatch(action);
},
getState: function () {
return store.getState()[key];
},
hostSubscribe: hostSubscribe,
subscribe: subscribe,
isTransportSubscribed: function (context, type, id) {
return clientHandlers[context].isSubscribed(type, id);
},
};
}
middleware.load = provideApplicationInterface;
return middleware;
}
function createClientHandlers() {
var _a, _b;
var subscriptions = (_a = {}, _a[appBridgeCore.Context.Main] = {}, _a[appBridgeCore.Context.Modal] = {}, _a);
return _b = {},
_b[appBridgeCore.Context.Main] = createSubscriptionsHandler(subscriptions[appBridgeCore.Context.Main]),
_b[appBridgeCore.Context.Modal] = createSubscriptionsHandler(subscriptions[appBridgeCore.Context.Modal]),
_b;
}
function createSubscriptionsHandler(initialSubscriptions) {
var subscriptions = initialSubscriptions;
return {
isSubscribed: function (type, id) {
var contextSubscribers = subscriptions[type] || [];
var subscribers = contextSubscribers.filter(function (sub) { return sub.id === id; });
return subscribers.length > 0;
},
subscribe: function (payload) {
var type = payload.type;
if (!subscriptions[type]) {
subscriptions[type] = [];
}
var eventSubscriptions = subscriptions[type] || [];
collection.addAndRemoveFromCollection(eventSubscriptions, payload);
},
unsubscribe: function (payload) {
if (!payload) {
Object.keys(subscriptions).forEach(function (key) { return delete subscriptions[key]; });
return;
}
var type = payload.type, id = payload.id;
var eventSubscriptions = subscriptions[type];
if (!eventSubscriptions) {
return;
}
if (id) {
var index = eventSubscriptions.findIndex(function (sub) { return sub.id === id; });
if (index >= 0) {
return eventSubscriptions.splice(index);
}
}
eventSubscriptions.pop();
},
};
}
/**
* Support both Action and ActionType
* See https://github.com/Shopify/app-bridge/issues/2228
*/
function appendLegacyAction(featuresAvailable) {
if (!featuresAvailable)
return;
var newFeaturesAvailable = {};
var groups = Object.keys(featuresAvailable);
groups.forEach(function (group) {
var actions = Object.keys(featuresAvailable[group]);
actions.forEach(function (action) {
var _a;
var feature = featuresAvailable[group][action];
var actionType = "".concat(constants.PREFIX).concat(constants.SEPARATOR).concat(group.toUpperCase()).concat(constants.SEPARATOR).concat(action);
newFeaturesAvailable[group] = (_a = newFeaturesAvailable[group]) !== null && _a !== void 0 ? _a : {};
newFeaturesAvailable[group][action] = feature;
newFeaturesAvailable[group][actionType] = feature;
});
});
return newFeaturesAvailable;
}
exports.buildMiddleware = buildMiddleware;