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
|
||||
```
|
||||
|
||||
## Docs
|
||||
|
||||
- [AppBridge](./docs/app-bridge.md) - communication layer between Saleor App and Dashboard
|
||||
|
||||
## Development
|
||||
|
||||
### 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
|
||||
import { actions, DispatchResponseEvent, createApp, HandshakeEvent } from ".";
|
||||
import { actions, DispatchResponseEvent, HandshakeEvent, AppBridge } from ".";
|
||||
|
||||
const handshakeEvent: HandshakeEvent = {
|
||||
payload: {
|
||||
|
@ -26,20 +26,20 @@ const handshakeEvent: HandshakeEvent = {
|
|||
type: "handshake",
|
||||
};
|
||||
|
||||
describe("createApp", () => {
|
||||
describe("AppBridge", () => {
|
||||
const domain = "saleor.domain.host";
|
||||
let app = createApp(domain);
|
||||
let appBridge = new AppBridge();
|
||||
|
||||
beforeEach(() => {
|
||||
app = createApp(domain);
|
||||
appBridge = new AppBridge();
|
||||
});
|
||||
|
||||
it("correctly sets the domain", () => {
|
||||
expect(app.getState().domain).toEqual(domain);
|
||||
it("correctly sets the default domain, if not set in constructor", () => {
|
||||
expect(appBridge.getState().domain).toEqual(domain);
|
||||
});
|
||||
|
||||
it("authenticates", () => {
|
||||
expect(app.getState().ready).toBe(false);
|
||||
expect(appBridge.getState().ready).toBe(false);
|
||||
|
||||
const token = "test-token";
|
||||
fireEvent(
|
||||
|
@ -50,13 +50,13 @@ describe("createApp", () => {
|
|||
})
|
||||
);
|
||||
|
||||
expect(app.getState().ready).toBe(true);
|
||||
expect(app.getState().token).toEqual(token);
|
||||
expect(appBridge.getState().ready).toBe(true);
|
||||
expect(appBridge.getState().token).toEqual(token);
|
||||
});
|
||||
|
||||
it("subscribes to an event and returns unsubscribe function", () => {
|
||||
const callback = vi.fn();
|
||||
const unsubscribe = app.subscribe("handshake", callback);
|
||||
const unsubscribe = appBridge.subscribe("handshake", callback);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
|
@ -91,8 +91,8 @@ describe("createApp", () => {
|
|||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(callback).toHaveBeenCalledWith(handshakeEvent.payload);
|
||||
expect(app.getState().token).toEqual(handshakeEvent.payload.token);
|
||||
expect(app.getState().id).toEqual("appid");
|
||||
expect(appBridge.getState().token).toEqual(handshakeEvent.payload.token);
|
||||
expect(appBridge.getState().id).toEqual("appid");
|
||||
|
||||
unsubscribe();
|
||||
|
||||
|
@ -105,11 +105,11 @@ describe("createApp", () => {
|
|||
);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
expect(app.getState().token).toEqual("123");
|
||||
expect(appBridge.getState().token).toEqual("123");
|
||||
});
|
||||
|
||||
it("persists domain", () => {
|
||||
expect(app.getState().domain).toEqual(domain);
|
||||
expect(appBridge.getState().domain).toEqual(domain);
|
||||
});
|
||||
|
||||
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", () =>
|
||||
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", () => {
|
||||
const cb1 = vi.fn();
|
||||
const cb2 = vi.fn();
|
||||
|
||||
app.subscribe("handshake", cb1);
|
||||
app.subscribe("handshake", cb2);
|
||||
appBridge.subscribe("handshake", cb1);
|
||||
appBridge.subscribe("handshake", cb2);
|
||||
|
||||
fireEvent(
|
||||
window,
|
||||
|
@ -155,7 +155,7 @@ describe("createApp", () => {
|
|||
expect(cb1).toHaveBeenCalledTimes(1);
|
||||
expect(cb2).toHaveBeenCalledTimes(1);
|
||||
|
||||
app.unsubscribeAll();
|
||||
appBridge.unsubscribeAll();
|
||||
|
||||
fireEvent(
|
||||
window,
|
||||
|
@ -168,4 +168,12 @@ describe("createApp", () => {
|
|||
expect(cb1).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: {},
|
||||
});
|
||||
|
||||
export type AppBridgeOptions = {
|
||||
targetDomain?: string;
|
||||
};
|
||||
|
||||
const getDefaultOptions = (): AppBridgeOptions => ({
|
||||
targetDomain: new URL(window.location.href).searchParams.get("domain") || "",
|
||||
});
|
||||
|
||||
export class AppBridge {
|
||||
private state = new AppBridgeStateContainer();
|
||||
|
||||
|
@ -58,16 +66,19 @@ export class AppBridge {
|
|||
|
||||
private subscribeMap = createEmptySubscribeMap();
|
||||
|
||||
constructor(private targetDomain?: string) {
|
||||
private combinedOptions = getDefaultOptions();
|
||||
|
||||
constructor(options: AppBridgeOptions = {}) {
|
||||
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."
|
||||
);
|
||||
}
|
||||
|
||||
if (!targetDomain) {
|
||||
this.targetDomain = new URL(window.location.href).searchParams.get("domain") || "";
|
||||
}
|
||||
this.combinedOptions = {
|
||||
...this.combinedOptions,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (!this.refererOrigin) {
|
||||
// TODO probably throw
|
||||
|
@ -168,7 +179,7 @@ export class AppBridge {
|
|||
const path = window.location.pathname || "";
|
||||
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() {
|
||||
|
|
|
@ -7,7 +7,7 @@ export * from "./events";
|
|||
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;
|
||||
|
|
Loading…
Reference in a new issue