Add docs and test
This commit is contained in:
parent
c4d672683c
commit
683f19b5c0
5 changed files with 158 additions and 26 deletions
|
@ -21,6 +21,10 @@ Feel free to play with SDK and to move its code to your app directly
|
||||||
npm i @saleor/app-sdk
|
npm i @saleor/app-sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- [AppBridge](./docs/app-bridge.md) - communication layer between Saleor App and Dashboard
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### How to link development version to your project
|
### How to link development version to your project
|
||||||
|
|
109
docs/app-bridge.md
Normal file
109
docs/app-bridge.md
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
# AppBridge
|
||||||
|
|
||||||
|
App bridge is an interface that connects App (running inside Dashboard) with Dashboard itself.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Create instance of AppBridge by running following code
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { AppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
|
||||||
|
const appBridge = new AppBridge(options);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Access app state:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { token, domain, ready, id } = appBridge.getState();
|
||||||
|
```
|
||||||
|
|
||||||
|
Available state represents `AppBridgeState`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type AppBridgeState = {
|
||||||
|
token?: string;
|
||||||
|
id: string;
|
||||||
|
ready: boolean;
|
||||||
|
domain: string;
|
||||||
|
path: string;
|
||||||
|
theme: ThemeType;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
Events are messages that originate in Saleor Dashboard. AppBridge can subscribe on events and app can react on them
|
||||||
|
|
||||||
|
### Subscribing to events
|
||||||
|
|
||||||
|
`subscribe(eventType, callback)` - can be used to listen to particular [event type](#available-event-types). It returns an unsubscribe function, which unregisters the callback.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const unsubscribe = appBridge.subscribe("handshake", (payload) => {
|
||||||
|
setToken(payload.token); // do something with event payload
|
||||||
|
|
||||||
|
const { token } = appState.getState(); // you can also get app's current state here
|
||||||
|
});
|
||||||
|
|
||||||
|
// unsubscribe when callback is no longer needed
|
||||||
|
unsubscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unsubscribing multiple listeners
|
||||||
|
|
||||||
|
`unsubscribeAll(eventType?)` - unregisters all callbacks of provided type. If no type was provided, it will remove all event callbacks.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// unsubscribe from all handshake events
|
||||||
|
appBridge.unsubscribeAll("handshake");
|
||||||
|
|
||||||
|
// unsubscribe from all events
|
||||||
|
appBridge.unsubscribeAll();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available event types
|
||||||
|
|
||||||
|
| Event type | Description |
|
||||||
|
| :---------- | :--------------------------------------------------------------------------- |
|
||||||
|
| `handshake` | Fired when iFrame containing the App is initialized or new token is assigned |
|
||||||
|
| `response` | Fired when Dashboard responds to an Action |
|
||||||
|
| `redirect` | Fired when Dashboard changes a subpath within the app path |
|
||||||
|
| `theme` | Fired when Dashboard changes the theme |
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
Actions expose a high-level API to communicate with Saleor Dashboard. They're exported under an `actions` namespace.
|
||||||
|
|
||||||
|
### Available methods
|
||||||
|
|
||||||
|
**`dispatch(action)`** - dispatches an Action. Returns a promise which resolves when action is successfully completed.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { actions } from "@saleor/app-sdk/app-bridge";
|
||||||
|
|
||||||
|
const handleRedirect = async () => {
|
||||||
|
await appBridge.dispatch(actions.Redirect({ to: "/orders" }));
|
||||||
|
|
||||||
|
console.log("Redirect complete!");
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRedirect();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available actions
|
||||||
|
|
||||||
|
| Action | Arguments | Description |
|
||||||
|
| :------------- | :--------------------------------------------------------------- | :---------- |
|
||||||
|
| `Redirect` | `to` (string) - relative (inside Dashboard) or absolute URL path | |
|
||||||
|
| | `newContext` (boolean) - should open in a new browsing context | |
|
||||||
|
| `Notification` | `status` (`info` / `success` / `warning` / `error` / undefined) | |
|
||||||
|
| | `title` (string / undefined) - title of the notification | |
|
||||||
|
| | `text` (string / undefined) - content of the notification | |
|
||||||
|
| | `apiMessage` (string / undefined) - error log from api | |
|
|
@ -16,7 +16,7 @@ Object.defineProperty(window, "location", {
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import { actions, DispatchResponseEvent, createApp, HandshakeEvent } from ".";
|
import { actions, DispatchResponseEvent, HandshakeEvent, AppBridge } from ".";
|
||||||
|
|
||||||
const handshakeEvent: HandshakeEvent = {
|
const handshakeEvent: HandshakeEvent = {
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -26,20 +26,20 @@ const handshakeEvent: HandshakeEvent = {
|
||||||
type: "handshake",
|
type: "handshake",
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("createApp", () => {
|
describe("AppBridge", () => {
|
||||||
const domain = "saleor.domain.host";
|
const domain = "saleor.domain.host";
|
||||||
let app = createApp(domain);
|
let appBridge = new AppBridge();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
app = createApp(domain);
|
appBridge = new AppBridge();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("correctly sets the domain", () => {
|
it("correctly sets the default domain, if not set in constructor", () => {
|
||||||
expect(app.getState().domain).toEqual(domain);
|
expect(appBridge.getState().domain).toEqual(domain);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("authenticates", () => {
|
it("authenticates", () => {
|
||||||
expect(app.getState().ready).toBe(false);
|
expect(appBridge.getState().ready).toBe(false);
|
||||||
|
|
||||||
const token = "test-token";
|
const token = "test-token";
|
||||||
fireEvent(
|
fireEvent(
|
||||||
|
@ -50,13 +50,13 @@ describe("createApp", () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(app.getState().ready).toBe(true);
|
expect(appBridge.getState().ready).toBe(true);
|
||||||
expect(app.getState().token).toEqual(token);
|
expect(appBridge.getState().token).toEqual(token);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("subscribes to an event and returns unsubscribe function", () => {
|
it("subscribes to an event and returns unsubscribe function", () => {
|
||||||
const callback = vi.fn();
|
const callback = vi.fn();
|
||||||
const unsubscribe = app.subscribe("handshake", callback);
|
const unsubscribe = appBridge.subscribe("handshake", callback);
|
||||||
|
|
||||||
expect(callback).not.toHaveBeenCalled();
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
@ -91,8 +91,8 @@ describe("createApp", () => {
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalledTimes(1);
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
expect(callback).toHaveBeenCalledWith(handshakeEvent.payload);
|
expect(callback).toHaveBeenCalledWith(handshakeEvent.payload);
|
||||||
expect(app.getState().token).toEqual(handshakeEvent.payload.token);
|
expect(appBridge.getState().token).toEqual(handshakeEvent.payload.token);
|
||||||
expect(app.getState().id).toEqual("appid");
|
expect(appBridge.getState().id).toEqual("appid");
|
||||||
|
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
|
|
||||||
|
@ -105,11 +105,11 @@ describe("createApp", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalledTimes(1);
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
expect(app.getState().token).toEqual("123");
|
expect(appBridge.getState().token).toEqual("123");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("persists domain", () => {
|
it("persists domain", () => {
|
||||||
expect(app.getState().domain).toEqual(domain);
|
expect(appBridge.getState().domain).toEqual(domain);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dispatches valid action", () => {
|
it("dispatches valid action", () => {
|
||||||
|
@ -131,18 +131,18 @@ describe("createApp", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return expect(app.dispatch(action)).resolves.toBeUndefined();
|
return expect(appBridge.dispatch(action)).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("times out after action response has not been registered", () =>
|
it("times out after action response has not been registered", () =>
|
||||||
expect(app.dispatch(actions.Redirect({ to: "/test" }))).rejects.toBeInstanceOf(Error));
|
expect(appBridge.dispatch(actions.Redirect({ to: "/test" }))).rejects.toBeInstanceOf(Error));
|
||||||
|
|
||||||
it("unsubscribes from all listeners", () => {
|
it("unsubscribes from all listeners", () => {
|
||||||
const cb1 = vi.fn();
|
const cb1 = vi.fn();
|
||||||
const cb2 = vi.fn();
|
const cb2 = vi.fn();
|
||||||
|
|
||||||
app.subscribe("handshake", cb1);
|
appBridge.subscribe("handshake", cb1);
|
||||||
app.subscribe("handshake", cb2);
|
appBridge.subscribe("handshake", cb2);
|
||||||
|
|
||||||
fireEvent(
|
fireEvent(
|
||||||
window,
|
window,
|
||||||
|
@ -155,7 +155,7 @@ describe("createApp", () => {
|
||||||
expect(cb1).toHaveBeenCalledTimes(1);
|
expect(cb1).toHaveBeenCalledTimes(1);
|
||||||
expect(cb2).toHaveBeenCalledTimes(1);
|
expect(cb2).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
app.unsubscribeAll();
|
appBridge.unsubscribeAll();
|
||||||
|
|
||||||
fireEvent(
|
fireEvent(
|
||||||
window,
|
window,
|
||||||
|
@ -168,4 +168,12 @@ describe("createApp", () => {
|
||||||
expect(cb1).toHaveBeenCalledTimes(1);
|
expect(cb1).toHaveBeenCalledTimes(1);
|
||||||
expect(cb2).toHaveBeenCalledTimes(1);
|
expect(cb2).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("attaches domain from options in constructor", () => {
|
||||||
|
appBridge = new AppBridge({
|
||||||
|
targetDomain: "https://foo.bar",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(appBridge.getState().domain).toEqual("https://foo.bar");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,6 +51,14 @@ const createEmptySubscribeMap = (): SubscribeMap => ({
|
||||||
theme: {},
|
theme: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type AppBridgeOptions = {
|
||||||
|
targetDomain?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDefaultOptions = (): AppBridgeOptions => ({
|
||||||
|
targetDomain: new URL(window.location.href).searchParams.get("domain") || "",
|
||||||
|
});
|
||||||
|
|
||||||
export class AppBridge {
|
export class AppBridge {
|
||||||
private state = new AppBridgeStateContainer();
|
private state = new AppBridgeStateContainer();
|
||||||
|
|
||||||
|
@ -58,16 +66,19 @@ export class AppBridge {
|
||||||
|
|
||||||
private subscribeMap = createEmptySubscribeMap();
|
private subscribeMap = createEmptySubscribeMap();
|
||||||
|
|
||||||
constructor(private targetDomain?: string) {
|
private combinedOptions = getDefaultOptions();
|
||||||
|
|
||||||
|
constructor(options: AppBridgeOptions = {}) {
|
||||||
if (SSR) {
|
if (SSR) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"AppBridge detected you're running this app in SSR mode. Make sure to call `new AppBridge()` when window object exists."
|
"AppBridge detected you're running this app in SSR mode. Make sure to call `new AppBridge()` when window object exists."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!targetDomain) {
|
this.combinedOptions = {
|
||||||
this.targetDomain = new URL(window.location.href).searchParams.get("domain") || "";
|
...this.combinedOptions,
|
||||||
}
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
if (!this.refererOrigin) {
|
if (!this.refererOrigin) {
|
||||||
// TODO probably throw
|
// TODO probably throw
|
||||||
|
@ -168,7 +179,7 @@ export class AppBridge {
|
||||||
const path = window.location.pathname || "";
|
const path = window.location.pathname || "";
|
||||||
const theme: ThemeType = url.searchParams.get("theme") === "light" ? "light" : "dark";
|
const theme: ThemeType = url.searchParams.get("theme") === "light" ? "light" : "dark";
|
||||||
|
|
||||||
this.state.setState({ domain: this.targetDomain, id, path, theme });
|
this.state.setState({ domain: this.combinedOptions.targetDomain, id, path, theme });
|
||||||
}
|
}
|
||||||
|
|
||||||
private listenOnMessages() {
|
private listenOnMessages() {
|
||||||
|
|
|
@ -7,7 +7,7 @@ export * from "./events";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use new AppBridge()
|
* @deprecated use new AppBridge(), createApp will be removed
|
||||||
*/
|
*/
|
||||||
export const createApp = (targetDomain?: string) => new AppBridge(targetDomain);
|
export const createApp = (targetDomain?: string) => new AppBridge({ targetDomain });
|
||||||
export default createApp;
|
export default createApp;
|
||||||
|
|
Loading…
Reference in a new issue