Add action that informs Dashboard that AppBridge is ready (#129)
* Add action that informs Dashboard that AppBridge is ready * Allow AppBridge to disable automated notifyReady event * Remove unused strategy field from RoutePropagator * Apply suggestions from code review Co-authored-by: Dawid <tarasiukdawid@gmail.com> Co-authored-by: Dawid <tarasiukdawid@gmail.com>
This commit is contained in:
parent
b39081ed9a
commit
95eac760ca
5 changed files with 98 additions and 27 deletions
|
@ -19,6 +19,7 @@ type AppBridgeOptions = {
|
||||||
targetDomain?: string;
|
targetDomain?: string;
|
||||||
saleorApiUrl?: string;
|
saleorApiUrl?: string;
|
||||||
initialLocale?: LocaleCode;
|
initialLocale?: LocaleCode;
|
||||||
|
autoNotifyReady?: boolean;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -153,11 +154,13 @@ handleRedirect();
|
||||||
|
|
||||||
### Available actions
|
### Available actions
|
||||||
|
|
||||||
| Action | Arguments | Description |
|
| Action | Arguments | Description |
|
||||||
| :------------- | :--------------------------------------------------------------- | :---------- |
|
| :-------------- | :--------------------------------------------------------------- | :--------------------------------------- |
|
||||||
| `Redirect` | `to` (string) - relative (inside Dashboard) or absolute URL path | |
|
| `Redirect` | `to` (string) - relative (inside Dashboard) or absolute URL path | |
|
||||||
| | `newContext` (boolean) - should open in a new browsing context | |
|
| | `newContext` (boolean) - should open in a new browsing context | |
|
||||||
| `Notification` | `status` (`info` / `success` / `warning` / `error` / undefined) | |
|
| `Notification` | `status` (`info` / `success` / `warning` / `error` / undefined) | |
|
||||||
| | `title` (string / undefined) - title of the notification | |
|
| | `title` (string / undefined) - title of the notification | |
|
||||||
| | `text` (string / undefined) - content of the notification | |
|
| | `text` (string / undefined) - content of the notification | |
|
||||||
| | `apiMessage` (string / undefined) - error log from api | |
|
| | `apiMessage` (string / undefined) - error log from api | |
|
||||||
|
| `NotifyReady` | | Inform Dashboard that AppBridge is ready |
|
||||||
|
| `UpdateRouting` | `newRoute` - current path of App to be set in URL | |
|
||||||
|
|
|
@ -4,9 +4,22 @@ import { Values } from "./helpers";
|
||||||
|
|
||||||
// Using constants over Enums, more info: https://fettblog.eu/tidy-typescript-avoid-enums/
|
// Using constants over Enums, more info: https://fettblog.eu/tidy-typescript-avoid-enums/
|
||||||
export const ActionType = {
|
export const ActionType = {
|
||||||
|
/**
|
||||||
|
* Ask Dashboard to redirect - either internal or external route
|
||||||
|
*/
|
||||||
redirect: "redirect",
|
redirect: "redirect",
|
||||||
|
/**
|
||||||
|
* Ask Dashboard to send a notification toast
|
||||||
|
*/
|
||||||
notification: "notification",
|
notification: "notification",
|
||||||
|
/**
|
||||||
|
* Ask Dashboard to update deep URL to preserve app route after refresh
|
||||||
|
*/
|
||||||
updateRouting: "updateRouting",
|
updateRouting: "updateRouting",
|
||||||
|
/**
|
||||||
|
* Inform Dashboard that AppBridge is ready
|
||||||
|
*/
|
||||||
|
notifyReady: "notifyReady",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ActionType = Values<typeof ActionType>;
|
export type ActionType = Values<typeof ActionType>;
|
||||||
|
@ -77,7 +90,6 @@ function createNotificationAction(payload: NotificationPayload): NotificationAct
|
||||||
|
|
||||||
export type UpdateRoutingPayload = {
|
export type UpdateRoutingPayload = {
|
||||||
newRoute: string;
|
newRoute: string;
|
||||||
strategy: "replace" | "push";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateRouting = ActionWithId<"updateRouting", UpdateRoutingPayload>;
|
export type UpdateRouting = ActionWithId<"updateRouting", UpdateRoutingPayload>;
|
||||||
|
@ -89,10 +101,20 @@ function createUpdateRoutingAction(payload: UpdateRoutingPayload): UpdateRouting
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Actions = RedirectAction | NotificationAction | UpdateRouting;
|
export type NotifyReady = ActionWithId<"notifyReady", {}>;
|
||||||
|
|
||||||
|
function createNotifyReadyAction(): NotifyReady {
|
||||||
|
return withActionId({
|
||||||
|
type: "notifyReady",
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Actions = RedirectAction | NotificationAction | UpdateRouting | NotifyReady;
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
Redirect: createRedirectAction,
|
Redirect: createRedirectAction,
|
||||||
Notification: createNotificationAction,
|
Notification: createNotificationAction,
|
||||||
UpdateRouting: createUpdateRoutingAction,
|
UpdateRouting: createUpdateRoutingAction,
|
||||||
|
NotifyReady: createNotifyReadyAction,
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { LocaleCode } from "../locales";
|
import { LocaleCode } from "../locales";
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import { actions, AppBridge, DispatchResponseEvent, HandshakeEvent, ThemeEvent } from ".";
|
import {
|
||||||
|
actions,
|
||||||
|
ActionType,
|
||||||
|
AppBridge,
|
||||||
|
DispatchResponseEvent,
|
||||||
|
HandshakeEvent,
|
||||||
|
ThemeEvent,
|
||||||
|
} from ".";
|
||||||
|
|
||||||
// mock document.referrer
|
// mock document.referrer
|
||||||
const origin = "http://example.com";
|
const origin = "http://example.com";
|
||||||
|
@ -41,11 +48,37 @@ const delay = (timeout: number) =>
|
||||||
setTimeout(res, timeout);
|
setTimeout(res, timeout);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockDashboardActionResponse = (actionType: ActionType, actionID: string) => {
|
||||||
|
function onMessage(event: MessageEvent) {
|
||||||
|
if (event.data.type === actionType) {
|
||||||
|
fireEvent(
|
||||||
|
window,
|
||||||
|
new MessageEvent("message", {
|
||||||
|
data: {
|
||||||
|
type: "response",
|
||||||
|
payload: { ok: true, actionId: actionID },
|
||||||
|
} as DispatchResponseEvent,
|
||||||
|
origin,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("message", onMessage);
|
||||||
|
|
||||||
|
return function cleanup() {
|
||||||
|
window.removeEventListener("message", onMessage);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
describe("AppBridge", () => {
|
describe("AppBridge", () => {
|
||||||
let appBridge = new AppBridge();
|
let appBridge = new AppBridge();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
appBridge = new AppBridge();
|
appBridge = new AppBridge();
|
||||||
|
vi.spyOn(console, "error").mockImplementation(() => {
|
||||||
|
// noop
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("correctly sets the default domain, if not set in constructor", () => {
|
it("correctly sets the default domain, if not set in constructor", () => {
|
||||||
|
@ -152,20 +185,7 @@ describe("AppBridge", () => {
|
||||||
const target = "/test";
|
const target = "/test";
|
||||||
const action = actions.Redirect({ to: target });
|
const action = actions.Redirect({ to: target });
|
||||||
|
|
||||||
window.addEventListener("message", (event) => {
|
mockDashboardActionResponse(action.type, action.payload.actionId);
|
||||||
if (event.data.type === action.type) {
|
|
||||||
fireEvent(
|
|
||||||
window,
|
|
||||||
new MessageEvent("message", {
|
|
||||||
data: {
|
|
||||||
type: "response",
|
|
||||||
payload: { ok: true, actionId: action.payload.actionId },
|
|
||||||
} as DispatchResponseEvent,
|
|
||||||
origin,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return expect(appBridge.dispatch(action)).resolves.toBeUndefined();
|
return expect(appBridge.dispatch(action)).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
@ -232,4 +252,14 @@ describe("AppBridge", () => {
|
||||||
|
|
||||||
window.location.href = currentLocationHref;
|
window.location.href = currentLocationHref;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("dispatches 'notifyReady' action when created", (done) => {
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
if (event.data.type === ActionType.notifyReady) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
appBridge = new AppBridge();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import debugPkg from "debug";
|
import debugPkg from "debug";
|
||||||
|
|
||||||
import { LocaleCode } from "../locales";
|
import { LocaleCode } from "../locales";
|
||||||
import { Actions } from "./actions";
|
import { Actions, actions } from "./actions";
|
||||||
import { AppBridgeState, AppBridgeStateContainer } from "./app-bridge-state";
|
import { AppBridgeState, AppBridgeStateContainer } from "./app-bridge-state";
|
||||||
import { AppIframeParams } from "./app-iframe-params";
|
import { AppIframeParams } from "./app-iframe-params";
|
||||||
import { SSR } from "./constants";
|
import { SSR } from "./constants";
|
||||||
|
@ -71,6 +71,11 @@ export type AppBridgeOptions = {
|
||||||
targetDomain?: string;
|
targetDomain?: string;
|
||||||
saleorApiUrl?: string;
|
saleorApiUrl?: string;
|
||||||
initialLocale?: LocaleCode;
|
initialLocale?: LocaleCode;
|
||||||
|
/**
|
||||||
|
* Should automatically emit Actions.NotifyReady.
|
||||||
|
* If app loading time is longer, this can be disabled and sent manually.
|
||||||
|
*/
|
||||||
|
autoNotifyReady?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -93,6 +98,7 @@ const getDefaultOptions = (): AppBridgeOptions => ({
|
||||||
targetDomain: getDomainFromUrl(),
|
targetDomain: getDomainFromUrl(),
|
||||||
saleorApiUrl: getSaleorApiUrlFromUrl(),
|
saleorApiUrl: getSaleorApiUrlFromUrl(),
|
||||||
initialLocale: getLocaleFromUrl() ?? "en",
|
initialLocale: getLocaleFromUrl() ?? "en",
|
||||||
|
autoNotifyReady: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class AppBridge {
|
export class AppBridge {
|
||||||
|
@ -145,6 +151,10 @@ export class AppBridge {
|
||||||
|
|
||||||
this.setInitialState();
|
this.setInitialState();
|
||||||
this.listenOnMessages();
|
this.listenOnMessages();
|
||||||
|
|
||||||
|
if (this.combinedOptions.autoNotifyReady) {
|
||||||
|
this.sendNotifyReadyAction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -256,6 +266,13 @@ export class AppBridge {
|
||||||
return this.state.getState();
|
return this.state.getState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendNotifyReadyAction() {
|
||||||
|
this.dispatch(actions.NotifyReady()).catch((e) => {
|
||||||
|
console.error("notifyReady action failed");
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private setInitialState() {
|
private setInitialState() {
|
||||||
debug("setInitialState() called");
|
debug("setInitialState() called");
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ export const useRoutePropagator = () => {
|
||||||
?.dispatch(
|
?.dispatch(
|
||||||
actions.UpdateRouting({
|
actions.UpdateRouting({
|
||||||
newRoute: url,
|
newRoute: url,
|
||||||
strategy: "replace",
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|
Loading…
Reference in a new issue