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

240 lines (237 loc) 11.5 kB
import { __assign } from 'tslib'; import { PermissionType } from '@shopify/app-bridge-core/client'; import { isAppBridgeAction, isPermitted, isPerformanceOrWebVitalsAction } from '@shopify/app-bridge-core/actions/validator'; import { createTransportListener, Context } from '@shopify/app-bridge-core'; import { addAndRemoveFromCollection } from '@shopify/app-bridge-core/util/collection'; import { fromAction, Action, permissionAction } from '@shopify/app-bridge-core/actions/Error'; import { PREFIX, SEPARATOR } from '@shopify/app-bridge-core/actions/constants'; import { isLoadReducerCompleteAction, apiClientLoad, apiClientUnload } from './actions.js'; import { isValidConfig, InvalidConfigError, throwInvalidConfigError } from './clientValidator.js'; import { isReducerLoaded, createActionsQueue } from './store/index.js'; function buildMiddleware(key, dispatchClientEventHandlerRef, errorHandler) { var transports = []; var transportListener = createTransportListener(); var subscribe = transportListener.createSubscribeHandler(); var hostListener = createTransportListener(); var hostSubscribe = hostListener.createSubscribeHandler(); var store; var actionsQueue; var receivedMessageFromClient = false; var middleware = function (hostStore) { store = hostStore; actionsQueue = createActionsQueue(store); return function (next) { return function (action) { if (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 (isPermitted(features, action, PermissionType.Subscribe)) { transport.dispatch({ payload: action, type: 'dispatch', }); } } } var appState = store.getState()[key]; if (isAppBridgeAction(action)) { if (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 (isLoadReducerCompleteAction(action)) { actionsQueue.resolve(action.payload.feature); } return next(action); }; }; }; function provideApplicationInterface(data) { var config = data.config; var clientHandlers = createClientHandlers(); store.dispatch(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 (!isValidConfig(source, config)) { if (errorHandler) return errorHandler(InvalidConfigError(source, config, action)); 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 || !isPermitted(features, action, PermissionType.Dispatch)) { store.dispatch(permissionAction(action)); return; } transportListener.handleActionDispatch(action); if (isReducerLoaded(appState, action)) { store.dispatch(__assign(__assign({}, action), { source: source })); } else { actionsQueue.add(to.context, action); } if (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 = __assign(__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 = fromAction('Unknown message type. Expected `dispatch` or `getState`.', Action.INVALID_ACTION, message); if (errorHandler) return errorHandler(error); throw error; } } }); var detach = 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(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[Context.Main] = {}, _a[Context.Modal] = {}, _a); return _b = {}, _b[Context.Main] = createSubscriptionsHandler(subscriptions[Context.Main]), _b[Context.Modal] = createSubscriptionsHandler(subscriptions[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] || []; 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(PREFIX).concat(SEPARATOR).concat(group.toUpperCase()).concat(SEPARATOR).concat(action); newFeaturesAvailable[group] = (_a = newFeaturesAvailable[group]) !== null && _a !== void 0 ? _a : {}; newFeaturesAvailable[group][action] = feature; newFeaturesAvailable[group][actionType] = feature; }); }); return newFeaturesAvailable; } export { buildMiddleware };