UNPKG

@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
'use strict'; 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;