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:
Lukasz Ostrowski 2022-12-05 17:17:17 +01:00 committed by GitHub
parent b39081ed9a
commit 95eac760ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 98 additions and 27 deletions

View file

@ -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 | |

View file

@ -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,
}; };

View file

@ -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();
});
}); });

View file

@ -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");

View file

@ -24,7 +24,6 @@ export const useRoutePropagator = () => {
?.dispatch( ?.dispatch(
actions.UpdateRouting({ actions.UpdateRouting({
newRoute: url, newRoute: url,
strategy: "replace",
}) })
) )
.catch(() => { .catch(() => {