diff --git a/package-lock.json b/package-lock.json index aeacb87fa..182374a4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,7 +116,7 @@ "@percy/cli": "^1.21.0", "@percy/cypress": "^3.1.2", "@release-it/bumper": "^2.0.0", - "@saleor/app-sdk": "0.37.3", + "@saleor/app-sdk": "0.39.1", "@types/apollo-upload-client": "^17.0.2", "@types/color-convert": "^2.0.0", "@types/debug": "^4.1.7", @@ -7913,9 +7913,9 @@ } }, "node_modules/@saleor/app-sdk": { - "version": "0.37.3", - "resolved": "https://registry.npmjs.org/@saleor/app-sdk/-/app-sdk-0.37.3.tgz", - "integrity": "sha512-DFAjiEFBFIDWDqZvaXar8860zDgSZih2n7gT3e+aOpLIfnmMZ0nL6+aNS3A7ym2IStx8oUoPI1RFZBIeuF9+hg==", + "version": "0.39.1", + "resolved": "https://registry.npmjs.org/@saleor/app-sdk/-/app-sdk-0.39.1.tgz", + "integrity": "sha512-PBREZWwkk+DHCBhOZzsuFdp7/vLItxfAqKD+gcZ5QxzSAt82S0/FS6ii7igpW3H+bEiK4oBUjMxgUyhJMOXQHw==", "dev": true, "dependencies": { "@changesets/cli": "^2.26.0", @@ -41291,9 +41291,9 @@ } }, "@saleor/app-sdk": { - "version": "0.37.3", - "resolved": "https://registry.npmjs.org/@saleor/app-sdk/-/app-sdk-0.37.3.tgz", - "integrity": "sha512-DFAjiEFBFIDWDqZvaXar8860zDgSZih2n7gT3e+aOpLIfnmMZ0nL6+aNS3A7ym2IStx8oUoPI1RFZBIeuF9+hg==", + "version": "0.39.1", + "resolved": "https://registry.npmjs.org/@saleor/app-sdk/-/app-sdk-0.39.1.tgz", + "integrity": "sha512-PBREZWwkk+DHCBhOZzsuFdp7/vLItxfAqKD+gcZ5QxzSAt82S0/FS6ii7igpW3H+bEiK4oBUjMxgUyhJMOXQHw==", "dev": true, "requires": { "@changesets/cli": "^2.26.0", diff --git a/package.json b/package.json index 724141a3f..fc5a833d7 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@percy/cli": "^1.21.0", "@percy/cypress": "^3.1.2", "@release-it/bumper": "^2.0.0", - "@saleor/app-sdk": "0.37.3", + "@saleor/app-sdk": "0.39.1", "@types/apollo-upload-client": "^17.0.2", "@types/color-convert": "^2.0.0", "@types/debug": "^4.1.7", diff --git a/src/apps/components/AppFrame/AppFrame.tsx b/src/apps/components/AppFrame/AppFrame.tsx index 570c8da31..1c24d3253 100644 --- a/src/apps/components/AppFrame/AppFrame.tsx +++ b/src/apps/components/AppFrame/AppFrame.tsx @@ -1,4 +1,5 @@ import { useAppDashboardUpdates } from "@dashboard/apps/components/AppFrame/useAppDashboardUpdates"; +import { useUpdateAppToken } from "@dashboard/apps/components/AppFrame/useUpdateAppToken"; import { AppDetailsUrlQueryParams, prepareFeatureFlagsList, @@ -76,6 +77,15 @@ export const AppFrame: React.FC = ({ const featureFlags = useMemo(() => prepareFeatureFlagsList(flags), [flags]); + useUpdateAppToken({ + postToExtension, + appToken, + /** + * If app is not ready, ignore this flow + */ + enabled: handshakeDone, + }); + return ( <> {!handshakeDone && ( diff --git a/src/apps/components/AppFrame/useUpdateAppToken.test.ts b/src/apps/components/AppFrame/useUpdateAppToken.test.ts new file mode 100644 index 000000000..30948fb1c --- /dev/null +++ b/src/apps/components/AppFrame/useUpdateAppToken.test.ts @@ -0,0 +1,80 @@ +import { useUpdateAppToken } from "@dashboard/apps/components/AppFrame/useUpdateAppToken"; +import { renderHook } from "@testing-library/react-hooks"; + +describe("useUpdateAppToken", function () { + const postMessage = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it("Doesnt do anything if disabled", () => { + const { waitFor } = renderHook(props => useUpdateAppToken(props), { + initialProps: { + enabled: true, + appToken: "initialToken", + postToExtension: postMessage, + }, + }); + + return waitFor(() => { + expect(postMessage).not.toHaveBeenCalled(); + }); + }); + + it("Doesnt do anything if re-rendered, but token stays the same between renders", () => { + const localPostMessage = jest.fn(); + + const { rerender, waitFor } = renderHook( + props => useUpdateAppToken(props), + { + initialProps: { + enabled: true, + appToken: "initialToken", + postToExtension: postMessage, + }, + }, + ); + + rerender({ + enabled: true, + appToken: "initialToken", + // simulate props change due to reference change + postToExtension: localPostMessage, + }); + + return waitFor(() => { + expect(postMessage).not.toHaveBeenCalled(); + expect(localPostMessage).not.toHaveBeenCalled(); + }); + }); + + it("Calls postMessage if token changes in props and enabled", async () => { + const { rerender, waitFor } = renderHook( + props => useUpdateAppToken(props), + { + initialProps: { + enabled: true, + appToken: "initialToken", + postToExtension: postMessage, + }, + }, + ); + + rerender({ + enabled: true, + appToken: "updatedToken", + // simulate props change due to reference change + postToExtension: postMessage, + }); + + return waitFor(() => { + expect(postMessage).toHaveBeenCalledWith({ + type: "tokenRefresh", + payload: { + token: "updatedToken", + }, + }); + }); + }); +}); diff --git a/src/apps/components/AppFrame/useUpdateAppToken.ts b/src/apps/components/AppFrame/useUpdateAppToken.ts new file mode 100644 index 000000000..910d7e616 --- /dev/null +++ b/src/apps/components/AppFrame/useUpdateAppToken.ts @@ -0,0 +1,56 @@ +import { createAppsDebug } from "@dashboard/apps/apps-debug"; +import { DashboardEventFactory, Events } from "@saleor/app-sdk/app-bridge"; +import { useEffect, useRef } from "react"; + +/** + * https://usehooks.com/usePrevious/ + */ +function usePreviousValue(value) { + const ref = useRef(); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +} + +interface Args { + enabled: boolean; + appToken: string; + postToExtension: (events: Events) => void; +} + +const debug = createAppsDebug("useUpdateAppToken"); + +/** + * Listens on appToken changes and pushes it to the App if changed. + */ +export const useUpdateAppToken = ({ + enabled, + appToken, + postToExtension, +}: Args) => { + const cachedToken = usePreviousValue(appToken); + + useEffect(() => { + if (!enabled) { + return; + } + + if (!cachedToken) { + /** + * Missing cache token means its first render, so we dont want to send it to app + */ + return; + } + + if (cachedToken !== appToken) { + debug("Will send new token to the app"); + /** + * Ensure running only when token changes. If token changes, send it to app + */ + postToExtension(DashboardEventFactory.createTokenRefreshEvent(appToken)); + } + }, [enabled, appToken, cachedToken, postToExtension]); +}; diff --git a/testUtils/setup.ts b/testUtils/setup.ts index 6a61385a2..0ae5254b4 100644 --- a/testUtils/setup.ts +++ b/testUtils/setup.ts @@ -31,3 +31,12 @@ window.__SALEOR_CONFIG__ = { process.env.TZ = "UTC"; configure({ testIdAttribute: "data-test-id" }); + +/** + * https://github.com/inrupt/solid-client-authn-js/issues/1676 + * + * Fixes (hacks) "TextEncoder is not defined" error which is likely bug in jsdom + */ +import { TextDecoder, TextEncoder } from "util"; +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder;