diff --git a/docs/app-bridge.md b/docs/app-bridge.md index 588dd78..cdcb12a 100644 --- a/docs/app-bridge.md +++ b/docs/app-bridge.md @@ -19,6 +19,7 @@ type AppBridgeOptions = { targetDomain?: string; saleorApiUrl?: string; initialLocale?: LocaleCode; + autoNotifyReady?: boolean; }; ``` @@ -153,11 +154,13 @@ 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 | | +| 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 | | +| `NotifyReady` | | Inform Dashboard that AppBridge is ready | +| `UpdateRouting` | `newRoute` - current path of App to be set in URL | | diff --git a/src/app-bridge/actions.ts b/src/app-bridge/actions.ts index a427d4f..b1fa206 100644 --- a/src/app-bridge/actions.ts +++ b/src/app-bridge/actions.ts @@ -4,9 +4,22 @@ import { Values } from "./helpers"; // Using constants over Enums, more info: https://fettblog.eu/tidy-typescript-avoid-enums/ export const 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", } as const; export type ActionType = Values; @@ -77,7 +90,6 @@ function createNotificationAction(payload: NotificationPayload): NotificationAct export type UpdateRoutingPayload = { newRoute: string; - strategy: "replace" | "push"; }; 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 = { Redirect: createRedirectAction, Notification: createNotificationAction, UpdateRouting: createUpdateRoutingAction, + NotifyReady: createNotifyReadyAction, }; diff --git a/src/app-bridge/app-bridge.test.ts b/src/app-bridge/app-bridge.test.ts index 63b1eea..8e0e2df 100644 --- a/src/app-bridge/app-bridge.test.ts +++ b/src/app-bridge/app-bridge.test.ts @@ -3,7 +3,14 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { LocaleCode } from "../locales"; // eslint-disable-next-line -import { actions, AppBridge, DispatchResponseEvent, HandshakeEvent, ThemeEvent } from "."; +import { + actions, + ActionType, + AppBridge, + DispatchResponseEvent, + HandshakeEvent, + ThemeEvent, +} from "."; // mock document.referrer const origin = "http://example.com"; @@ -41,11 +48,37 @@ const delay = (timeout: number) => 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", () => { let appBridge = new AppBridge(); beforeEach(() => { appBridge = new AppBridge(); + vi.spyOn(console, "error").mockImplementation(() => { + // noop + }); }); it("correctly sets the default domain, if not set in constructor", () => { @@ -152,20 +185,7 @@ describe("AppBridge", () => { const target = "/test"; const action = actions.Redirect({ to: target }); - window.addEventListener("message", (event) => { - if (event.data.type === action.type) { - fireEvent( - window, - new MessageEvent("message", { - data: { - type: "response", - payload: { ok: true, actionId: action.payload.actionId }, - } as DispatchResponseEvent, - origin, - }) - ); - } - }); + mockDashboardActionResponse(action.type, action.payload.actionId); return expect(appBridge.dispatch(action)).resolves.toBeUndefined(); }); @@ -232,4 +252,14 @@ describe("AppBridge", () => { 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(); + }); }); diff --git a/src/app-bridge/app-bridge.ts b/src/app-bridge/app-bridge.ts index 25527dd..49be511 100644 --- a/src/app-bridge/app-bridge.ts +++ b/src/app-bridge/app-bridge.ts @@ -1,7 +1,7 @@ import debugPkg from "debug"; import { LocaleCode } from "../locales"; -import { Actions } from "./actions"; +import { Actions, actions } from "./actions"; import { AppBridgeState, AppBridgeStateContainer } from "./app-bridge-state"; import { AppIframeParams } from "./app-iframe-params"; import { SSR } from "./constants"; @@ -71,6 +71,11 @@ export type AppBridgeOptions = { targetDomain?: string; saleorApiUrl?: string; 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(), saleorApiUrl: getSaleorApiUrlFromUrl(), initialLocale: getLocaleFromUrl() ?? "en", + autoNotifyReady: true, }); export class AppBridge { @@ -145,6 +151,10 @@ export class AppBridge { this.setInitialState(); this.listenOnMessages(); + + if (this.combinedOptions.autoNotifyReady) { + this.sendNotifyReadyAction(); + } } /** @@ -256,6 +266,13 @@ export class AppBridge { return this.state.getState(); } + sendNotifyReadyAction() { + this.dispatch(actions.NotifyReady()).catch((e) => { + console.error("notifyReady action failed"); + console.error(e); + }); + } + private setInitialState() { debug("setInitialState() called"); diff --git a/src/app-bridge/next/route-propagator.tsx b/src/app-bridge/next/route-propagator.tsx index af42aa7..456371a 100644 --- a/src/app-bridge/next/route-propagator.tsx +++ b/src/app-bridge/next/route-propagator.tsx @@ -24,7 +24,6 @@ export const useRoutePropagator = () => { ?.dispatch( actions.UpdateRouting({ newRoute: url, - strategy: "replace", }) ) .catch(() => {