UNPKG

@saleor/app-sdk

Version:
516 lines (492 loc) 15.8 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _chunkEJ6YJ4BDjs = require('./chunk-EJ6YJ4BD.js'); // src/app-bridge/actions.ts var ActionType = { /** * Ask Dashboard to redirect - either internal or external route */ redirect: "redirect", /** * Ask Dashboard to send a notification toast */ notification: "notification", /** * Ask Dashboard to update deep URL to preserve app route after refresh */ updateRouting: "updateRouting", /** * Inform Dashboard that AppBridge is ready */ notifyReady: "notifyReady", /** * Request one or more permissions from the Dashboard * * Available from 3.15 */ requestPermission: "requestPermissions" }; function withActionId(action) { try { const actionId = globalThis.crypto.randomUUID(); return { ...action, payload: { ...action.payload, actionId } }; } catch (e) { throw new Error("Failed to generate action ID. Please ensure you are using https or localhost"); } } function createRedirectAction(payload) { return withActionId({ payload, type: "redirect" }); } function createNotificationAction(payload) { return withActionId({ type: "notification", payload }); } function createUpdateRoutingAction(payload) { return withActionId({ type: "updateRouting", payload }); } function createNotifyReadyAction() { return withActionId({ type: "notifyReady", payload: {} }); } function createRequestPermissionsAction(permissions, redirectPath) { return withActionId({ type: "requestPermissions", payload: { permissions, redirectPath } }); } var actions = { Redirect: createRedirectAction, Notification: createNotificationAction, UpdateRouting: createUpdateRoutingAction, NotifyReady: createNotifyReadyAction, RequestPermissions: createRequestPermissionsAction }; // src/app-bridge/app-iframe-params.ts var AppIframeParams = { APP_ID: "id", THEME: "theme", DOMAIN: "domain", SALEOR_API_URL: "saleorApiUrl", LOCALE: "locale" }; // src/app-bridge/events.ts var EventType = { handshake: "handshake", response: "response", redirect: "redirect", theme: "theme", localeChanged: "localeChanged", tokenRefresh: "tokenRefresh" }; var DashboardEventFactory = { createThemeChangeEvent(theme) { return { payload: { theme }, type: "theme" }; }, createRedirectEvent(path) { return { type: "redirect", payload: { path } }; }, createDispatchResponseEvent(actionId, ok) { return { type: "response", payload: { actionId, ok } }; }, createHandshakeEvent(token, version = 1, saleorVersions) { return { type: "handshake", payload: { token, version, saleorVersion: _optionalChain([saleorVersions, 'optionalAccess', _ => _.core]), dashboardVersion: _optionalChain([saleorVersions, 'optionalAccess', _2 => _2.dashboard]) } }; }, createLocaleChangedEvent(newLocale) { return { type: "localeChanged", payload: { locale: newLocale } }; }, createTokenRefreshEvent(newToken) { return { type: "tokenRefresh", payload: { token: newToken } }; } }; // src/app-bridge/app-bridge.ts var _debug = require('debug'); var _debug2 = _interopRequireDefault(_debug); // src/util/extract-app-permissions-from-jwt.ts var _jose = require('jose'); var jose = _interopRequireWildcard(_jose); var extractAppPermissionsFromJwt = (jwtToken) => { const tokenDecoded = jose.decodeJwt(jwtToken); return tokenDecoded.permissions; }; // src/app-bridge/app-bridge-state.ts var AppBridgeStateContainer = class { constructor(options = {}) { this.state = { id: "", saleorApiUrl: "", ready: false, path: "/", theme: "light", locale: "en" }; this.state.locale = _nullishCoalesce(options.initialLocale, () => ( this.state.locale)); this.state.theme = _nullishCoalesce(options.initialTheme, () => ( this.state.theme)); } getState() { return this.state; } setState(newState) { this.state = { ...this.state, ...newState }; return this.state; } }; // src/app-bridge/constants.ts var SSR = typeof window === "undefined"; // src/app-bridge/app-bridge.ts var DISPATCH_RESPONSE_TIMEOUT = 1e3; var debug = _debug2.default.debug("app-sdk:AppBridge"); function eventStateReducer(state, event) { switch (event.type) { case EventType.handshake: { const userJwtPayload = _chunkEJ6YJ4BDjs.extractUserFromJwt.call(void 0, event.payload.token); const appPermissions = extractAppPermissionsFromJwt(event.payload.token); return { ...state, ready: true, token: event.payload.token, saleorVersion: event.payload.saleorVersion, dashboardVersion: event.payload.dashboardVersion, user: { email: userJwtPayload.email, permissions: userJwtPayload.userPermissions }, appPermissions }; } case EventType.redirect: { return { ...state, path: event.payload.path }; } case EventType.theme: { return { ...state, theme: event.payload.theme }; } case EventType.localeChanged: { return { ...state, locale: event.payload.locale }; } case EventType.tokenRefresh: { return { ...state, token: event.payload.token }; } case EventType.response: { return state; } default: { console.warn(`Invalid event received: ${_optionalChain([event, 'optionalAccess', _3 => _3.type])}`); return state; } } } var createEmptySubscribeMap = () => ({ handshake: {}, response: {}, redirect: {}, theme: {}, localeChanged: {}, tokenRefresh: {} }); var getLocaleFromUrl = () => new URL(window.location.href).searchParams.get(AppIframeParams.LOCALE) || void 0; var getSaleorApiUrlFromUrl = () => new URL(window.location.href).searchParams.get(AppIframeParams.SALEOR_API_URL) || ""; var getThemeFromUrl = () => { const value = new URL(window.location.href).searchParams.get(AppIframeParams.THEME); switch (value) { case "dark": case "light": return value; default: return void 0; } }; var getDefaultOptions = () => ({ saleorApiUrl: getSaleorApiUrlFromUrl(), initialLocale: _nullishCoalesce(getLocaleFromUrl(), () => ( "en")), autoNotifyReady: true, initialTheme: _nullishCoalesce(getThemeFromUrl(), () => ( void 0)) }); var AppBridge = class { constructor(options = {}) { this.refererOrigin = document.referrer ? new URL(document.referrer).origin : void 0; this.subscribeMap = createEmptySubscribeMap(); this.combinedOptions = getDefaultOptions(); debug("Constructor called with options: %j", options); if (SSR) { throw new Error( "AppBridge detected you're running this app in SSR mode. Make sure to call `new AppBridge()` when window object exists." ); } this.combinedOptions = { ...this.combinedOptions, ...options }; this.state = new AppBridgeStateContainer({ initialLocale: this.combinedOptions.initialLocale }); debug("Resolved combined AppBridge options: %j", this.combinedOptions); if (!this.refererOrigin) { console.warn("document.referrer is empty"); } if (!this.combinedOptions.saleorApiUrl) { debug("?saleorApiUrl was not found in iframe url"); } if (!this.combinedOptions.saleorApiUrl) { console.error("saleorApiUrl param was not found in iframe url"); } this.setInitialState(); this.listenOnMessages(); if (this.combinedOptions.autoNotifyReady) { this.sendNotifyReadyAction(); } } /** * Subscribes to an Event. * * @param eventType - Event type. * @param cb - Callback that executes when Event is registered. Called with Event payload object. * @returns Unsubscribe function. Call to unregister the callback. */ subscribe(eventType, cb) { debug("subscribe() called with event %s and callback %s", eventType, cb.name); const key = Symbol("Callback token"); this.subscribeMap[eventType][key] = cb; return () => { debug("unsubscribe called with event %s and callback %s", eventType, cb.name); delete this.subscribeMap[eventType][key]; }; } /** * Unsubscribe to all Events of type. * If type not provider, unsubscribe all * * @param eventType - (optional) Event type. If empty, all callbacks will be unsubscribed. */ unsubscribeAll(eventType) { if (eventType) { debug("unsubscribeAll called with event: %s", eventType); this.subscribeMap[eventType] = {}; } else { debug("unsubscribeAll called without argument"); this.subscribeMap = createEmptySubscribeMap(); } } /** * Dispatch event to dashboard */ async dispatch(action) { debug("dispatch called with action argument: %j", action); return new Promise((resolve, reject) => { if (!window.parent) { debug("window.parent doesn't exist, will throw"); reject(new Error("Parent window does not exist.")); return; } debug("Calling window.parent.postMessage with %j", action); window.parent.postMessage( { type: action.type, payload: action.payload }, "*" ); let timeoutId; const unsubscribe = this.subscribe(EventType.response, ({ actionId, ok }) => { debug( "Subscribing to %s with action id: %s and status 'ok' is: %s", EventType.response, actionId, ok ); if (action.payload.actionId === actionId) { debug("Received matching action id: %s. Will unsubscribe", actionId); unsubscribe(); clearTimeout(timeoutId); if (ok) { resolve(); } else { reject( new Error( "Action responded with negative status. This indicates the action method was not used properly." ) ); } } }); timeoutId = window.setTimeout(() => { unsubscribe(); reject(new Error("Action response timed out.")); }, DISPATCH_RESPONSE_TIMEOUT); }); } /** * Gets current state */ getState() { debug("getState() called and will return %j", this.state.getState()); return this.state.getState(); } sendNotifyReadyAction() { this.dispatch(actions.NotifyReady()).catch((e) => { console.error("notifyReady action failed"); console.error(e); }); } setInitialState() { debug("setInitialState() called"); const url = new URL(window.location.href); const id = url.searchParams.get(AppIframeParams.APP_ID) || ""; const path = window.location.pathname || ""; const state = { id, path, theme: this.combinedOptions.initialTheme, saleorApiUrl: this.combinedOptions.saleorApiUrl, locale: this.combinedOptions.initialLocale }; debug("setInitialState() will setState with %j", state); this.state.setState(state); } listenOnMessages() { debug("listenOnMessages() called"); window.addEventListener( "message", ({ origin, data }) => { debug("Received message from origin: %s and data: %j", origin, data); if (origin !== this.refererOrigin) { debug("Origin from message doesn't match refererOrigin. Function will return now"); return; } const newState = eventStateReducer(this.state.getState(), data); debug("Computed new state: %j. Will be set with setState", newState); this.state.setState(newState); const { type, payload } = data; if (EventType[type]) { Object.getOwnPropertySymbols(this.subscribeMap[type]).forEach((key) => { debug("Executing listener for event: %s and payload %j", type, payload); this.subscribeMap[type][key](payload); }); } } ); } }; // src/app-bridge/app-bridge-provider.tsx var _react = require('react'); var React = _interopRequireWildcard(_react); var debug2 = _debug2.default.debug("app-sdk:AppBridgeProvider"); var AppContext = React.createContext({ appBridge: void 0, mounted: false }); function AppBridgeProvider({ appBridgeInstance, ...props }) { debug2("Provider mounted"); const [appBridge, setAppBridge] = _react.useState.call(void 0, appBridgeInstance); _react.useEffect.call(void 0, () => { if (!appBridge) { debug2("AppBridge not defined, will create new instance"); setAppBridge(_nullishCoalesce(appBridgeInstance, () => ( new AppBridge()))); } else { debug2("AppBridge provided in props, will use this one"); } }, []); const contextValue = _react.useMemo.call(void 0, () => ({ appBridge, mounted: true }), [appBridge] ); return /* @__PURE__ */ React.createElement(AppContext.Provider, { value: contextValue, ...props }); } var useAppBridge = () => { const { appBridge, mounted } = _react.useContext.call(void 0, AppContext); const [appBridgeState, setAppBridgeState] = _react.useState.call(void 0, () => appBridge ? appBridge.getState() : null ); if (typeof window !== "undefined" && !mounted) { throw new Error("useAppBridge used outside of AppBridgeProvider"); } const updateState = _react.useCallback.call(void 0, () => { if (_optionalChain([appBridge, 'optionalAccess', _4 => _4.getState, 'call', _5 => _5()])) { debug2("Detected state change in AppBridge, will set new state"); setAppBridgeState(appBridge.getState()); } }, [appBridge]); _react.useEffect.call(void 0, () => { let unsubscribes = []; if (appBridge) { debug2("Provider mounted, will set up listeners"); unsubscribes = [ appBridge.subscribe("handshake", updateState), appBridge.subscribe("theme", updateState), appBridge.subscribe("response", updateState), appBridge.subscribe("redirect", updateState) ]; } return () => { debug2("Provider unmounted, will clean up listeners"); unsubscribes.forEach((unsubscribe) => unsubscribe()); }; }, [appBridge, updateState]); return { appBridge, appBridgeState }; }; exports.ActionType = ActionType; exports.actions = actions; exports.AppIframeParams = AppIframeParams; exports.EventType = EventType; exports.DashboardEventFactory = DashboardEventFactory; exports.AppBridge = AppBridge; exports.AppContext = AppContext; exports.AppBridgeProvider = AppBridgeProvider; exports.useAppBridge = useAppBridge;