@saleor/app-sdk
Version:
SDK for building great Saleor Apps
516 lines (492 loc) • 15.8 kB
JavaScript
;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;