Add AppBridgeProvider and events factory (#51)
* Add events factory * Add AppBridgeProvider * Rewrite useAppBridge, add tests
This commit is contained in:
parent
ac6900f35f
commit
cc18c3deac
7 changed files with 407 additions and 1 deletions
11
package.json
11
package.json
|
@ -18,6 +18,10 @@
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
|
@ -29,6 +33,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^8.17.1",
|
"@testing-library/dom": "^8.17.1",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
"@types/debug": "^4.1.7",
|
"@types/debug": "^4.1.7",
|
||||||
"@types/node": "^18.7.15",
|
"@types/node": "^18.7.15",
|
||||||
"@types/node-fetch": "^2.6.2",
|
"@types/node-fetch": "^2.6.2",
|
||||||
|
@ -55,7 +60,11 @@
|
||||||
"typescript": "^4.8.2",
|
"typescript": "^4.8.2",
|
||||||
"vite": "^3.1.0",
|
"vite": "^3.1.0",
|
||||||
"vitest": "^0.23.1",
|
"vitest": "^0.23.1",
|
||||||
"watchlist": "^0.3.1"
|
"watchlist": "^0.3.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"@types/react": "17.0.49",
|
||||||
|
"@types/react-dom": "^17.0.11"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,tsx}": "eslint --cache --fix",
|
"*.{js,ts,tsx}": "eslint --cache --fix",
|
||||||
|
|
|
@ -2,9 +2,12 @@ lockfileVersion: 5.4
|
||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
'@testing-library/dom': ^8.17.1
|
'@testing-library/dom': ^8.17.1
|
||||||
|
'@testing-library/react': ^13.4.0
|
||||||
'@types/debug': ^4.1.7
|
'@types/debug': ^4.1.7
|
||||||
'@types/node': ^18.7.15
|
'@types/node': ^18.7.15
|
||||||
'@types/node-fetch': ^2.6.2
|
'@types/node-fetch': ^2.6.2
|
||||||
|
'@types/react': 17.0.49
|
||||||
|
'@types/react-dom': ^17.0.11
|
||||||
'@types/uuid': ^8.3.4
|
'@types/uuid': ^8.3.4
|
||||||
'@typescript-eslint/eslint-plugin': ^5.36.1
|
'@typescript-eslint/eslint-plugin': ^5.36.1
|
||||||
'@typescript-eslint/parser': ^5.36.1
|
'@typescript-eslint/parser': ^5.36.1
|
||||||
|
@ -27,6 +30,8 @@ specifiers:
|
||||||
jsdom: ^20.0.0
|
jsdom: ^20.0.0
|
||||||
node-fetch: ^3.2.10
|
node-fetch: ^3.2.10
|
||||||
prettier: 2.7.1
|
prettier: 2.7.1
|
||||||
|
react: ^18.2.0
|
||||||
|
react-dom: 18.2.0
|
||||||
release-it: ^15.4.1
|
release-it: ^15.4.1
|
||||||
retes: ^0.32.0
|
retes: ^0.32.0
|
||||||
tsm: ^2.2.2
|
tsm: ^2.2.2
|
||||||
|
@ -48,9 +53,12 @@ dependencies:
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@testing-library/dom': 8.17.1
|
'@testing-library/dom': 8.17.1
|
||||||
|
'@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y
|
||||||
'@types/debug': 4.1.7
|
'@types/debug': 4.1.7
|
||||||
'@types/node': 18.7.15
|
'@types/node': 18.7.15
|
||||||
'@types/node-fetch': 2.6.2
|
'@types/node-fetch': 2.6.2
|
||||||
|
'@types/react': 17.0.49
|
||||||
|
'@types/react-dom': 17.0.17
|
||||||
'@types/uuid': 8.3.4
|
'@types/uuid': 8.3.4
|
||||||
'@typescript-eslint/eslint-plugin': 5.36.1_lbwfnm54o3pmr3ypeqp3btnera
|
'@typescript-eslint/eslint-plugin': 5.36.1_lbwfnm54o3pmr3ypeqp3btnera
|
||||||
'@typescript-eslint/parser': 5.36.1_yqf6kl63nyoq5megxukfnom5rm
|
'@typescript-eslint/parser': 5.36.1_yqf6kl63nyoq5megxukfnom5rm
|
||||||
|
@ -68,6 +76,8 @@ devDependencies:
|
||||||
husky: 8.0.1
|
husky: 8.0.1
|
||||||
jsdom: 20.0.0
|
jsdom: 20.0.0
|
||||||
prettier: 2.7.1
|
prettier: 2.7.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
release-it: 15.4.1
|
release-it: 15.4.1
|
||||||
tsm: 2.2.2
|
tsm: 2.2.2
|
||||||
tsup: 6.2.3_typescript@4.8.2
|
tsup: 6.2.3_typescript@4.8.2
|
||||||
|
@ -648,6 +658,20 @@ packages:
|
||||||
pretty-format: 27.5.1
|
pretty-format: 27.5.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@testing-library/react/13.4.0_biqbaboplfbrettd7655fr4n2y:
|
||||||
|
resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.0.0
|
||||||
|
react-dom: ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.18.9
|
||||||
|
'@testing-library/dom': 8.17.1
|
||||||
|
'@types/react-dom': 18.0.6
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0_react@18.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@tootallnate/once/1.1.2:
|
/@tootallnate/once/1.1.2:
|
||||||
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
|
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -724,12 +748,48 @@ packages:
|
||||||
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/prop-types/15.7.5:
|
||||||
|
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/react-dom/17.0.17:
|
||||||
|
resolution: {integrity: sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 17.0.49
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/react-dom/18.0.6:
|
||||||
|
resolution: {integrity: sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.0.18
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/react/17.0.49:
|
||||||
|
resolution: {integrity: sha512-CCBPMZaPhcKkYUTqFs/hOWqKjPxhTEmnZWjlHHgIMop67DsXywf9B5Os9Hz8KSacjNOgIdnZVJamwl232uxoPg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/prop-types': 15.7.5
|
||||||
|
'@types/scheduler': 0.16.2
|
||||||
|
csstype: 3.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/react/18.0.18:
|
||||||
|
resolution: {integrity: sha512-6hI08umYs6NaiHFEEGioXnxJ+oEhY3eRz8VCUaudZmGdtvPviCJB8mgaMxaDWAdPSYd4eFavrPk2QIolwbLYrg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/prop-types': 15.7.5
|
||||||
|
'@types/scheduler': 0.16.2
|
||||||
|
csstype: 3.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/responselike/1.0.0:
|
/@types/responselike/1.0.0:
|
||||||
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
|
resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.7.15
|
'@types/node': 18.7.15
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/scheduler/0.16.2:
|
||||||
|
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/uuid/8.3.4:
|
/@types/uuid/8.3.4:
|
||||||
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1457,6 +1517,10 @@ packages:
|
||||||
cssom: 0.3.8
|
cssom: 0.3.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/csstype/3.1.0:
|
||||||
|
resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/damerau-levenshtein/1.0.8:
|
/damerau-levenshtein/1.0.8:
|
||||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4205,6 +4269,16 @@ packages:
|
||||||
strip-json-comments: 2.0.1
|
strip-json-comments: 2.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/react-dom/18.2.0_react@18.2.0:
|
||||||
|
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.2.0
|
||||||
|
dependencies:
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
react: 18.2.0
|
||||||
|
scheduler: 0.23.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/react-is/16.13.1:
|
/react-is/16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4218,6 +4292,13 @@ packages:
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/react/18.2.0:
|
||||||
|
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dependencies:
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/readable-stream/1.1.14:
|
/readable-stream/1.1.14:
|
||||||
resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
|
resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4442,6 +4523,12 @@ packages:
|
||||||
xmlchars: 2.2.0
|
xmlchars: 2.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/scheduler/0.23.0:
|
||||||
|
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
|
||||||
|
dependencies:
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/semver-diff/4.0.0:
|
/semver-diff/4.0.0:
|
||||||
resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==}
|
resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
139
src/app-bridge/app-bridge-provider.test.tsx
Normal file
139
src/app-bridge/app-bridge-provider.test.tsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import { fireEvent, render, renderHook, waitFor } from "@testing-library/react";
|
||||||
|
import * as React from "react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { AppBridge } from "./app-bridge";
|
||||||
|
import { AppBridgeProvider, useAppBridge } from "./app-bridge-provider";
|
||||||
|
import { DashboardEventFactory } from "./events";
|
||||||
|
|
||||||
|
const origin = "http://example.com";
|
||||||
|
const domain = "saleor.domain.host";
|
||||||
|
|
||||||
|
Object.defineProperty(window.document, "referrer", {
|
||||||
|
value: origin,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(window, "location", {
|
||||||
|
value: {
|
||||||
|
href: `${origin}?domain=${domain}&id=appid`,
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("AppBridgeProvider", () => {
|
||||||
|
it("Mounts provider in React DOM", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AppBridgeProvider>
|
||||||
|
<div />
|
||||||
|
</AppBridgeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Mounts provider in React DOM with provided AppBridge instance", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<AppBridgeProvider
|
||||||
|
appBridgeInstance={
|
||||||
|
new AppBridge({
|
||||||
|
targetDomain: "https://test-domain",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</AppBridgeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("useAppBridge hook", () => {
|
||||||
|
it("App is defined when wrapped in AppBridgeProvider", async () => {
|
||||||
|
const { result } = renderHook(() => useAppBridge(), {
|
||||||
|
wrapper: AppBridgeProvider,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.appBridge).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Throws if not wrapped inside AppBridgeProvider", () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const mockConsoleError = vi.spyOn(console, "error");
|
||||||
|
/**
|
||||||
|
* Silent errors
|
||||||
|
*/
|
||||||
|
mockConsoleError.mockImplementation(() => {
|
||||||
|
// Silence
|
||||||
|
});
|
||||||
|
|
||||||
|
let appBridgeResult: AppBridge | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { result } = renderHook(() => useAppBridge(), {});
|
||||||
|
appBridgeResult = result.current.appBridge;
|
||||||
|
|
||||||
|
mockConsoleError.mockClear();
|
||||||
|
} catch (e) {
|
||||||
|
expect(mockConsoleError).toHaveBeenCalled();
|
||||||
|
expect(appBridgeResult).toBeUndefined();
|
||||||
|
}
|
||||||
|
|
||||||
|
mockConsoleError.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returned instance provided in Provider", () => {
|
||||||
|
const appBridge = new AppBridge({
|
||||||
|
targetDomain: "test-domain",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAppBridge(), {
|
||||||
|
wrapper: (props: {}) => <AppBridgeProvider {...props} appBridgeInstance={appBridge} />,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.appBridge?.getState().domain).toBe("test-domain");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Stores active state in React State", () => {
|
||||||
|
const appBridge = new AppBridge({
|
||||||
|
targetDomain: origin,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderCallback = vi.fn();
|
||||||
|
|
||||||
|
function TestComponent() {
|
||||||
|
const { appBridgeState } = useAppBridge();
|
||||||
|
|
||||||
|
renderCallback(appBridgeState);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AppBridgeProvider appBridgeInstance={appBridge}>
|
||||||
|
<TestComponent />
|
||||||
|
</AppBridgeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent(
|
||||||
|
window,
|
||||||
|
new MessageEvent("message", {
|
||||||
|
data: DashboardEventFactory.createThemeChangeEvent("light"),
|
||||||
|
origin,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return waitFor(() => {
|
||||||
|
expect(renderCallback).toHaveBeenCalledTimes(2);
|
||||||
|
expect(renderCallback).toHaveBeenCalledWith({
|
||||||
|
domain: "http://example.com",
|
||||||
|
id: "appid",
|
||||||
|
path: "",
|
||||||
|
ready: false,
|
||||||
|
theme: "light",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
93
src/app-bridge/app-bridge-provider.tsx
Normal file
93
src/app-bridge/app-bridge-provider.tsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import debugPkg from "debug";
|
||||||
|
import * as React from "react";
|
||||||
|
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { AppBridge } from "./app-bridge";
|
||||||
|
import { AppBridgeState } from "./app-bridge-state";
|
||||||
|
|
||||||
|
const debug = debugPkg.debug("app-sdk:AppBridgeProvider");
|
||||||
|
|
||||||
|
interface AppBridgeContext {
|
||||||
|
/**
|
||||||
|
* App can be undefined, because it gets initialized in Browser only
|
||||||
|
*/
|
||||||
|
appBridge?: AppBridge;
|
||||||
|
mounted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appBridgeInstance?: AppBridge;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AppContext = React.createContext<AppBridgeContext>({
|
||||||
|
appBridge: undefined,
|
||||||
|
mounted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experimental - try to use provider in app-sdk itself
|
||||||
|
* Consider monorepo with dedicated react package
|
||||||
|
*/
|
||||||
|
export function AppBridgeProvider({ appBridgeInstance, ...props }: React.PropsWithChildren<Props>) {
|
||||||
|
debug("Provider mounted");
|
||||||
|
const [appBridge, setAppBridge] = useState<AppBridge | undefined>(appBridgeInstance);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!appBridge) {
|
||||||
|
debug("AppBridge not defined, will create new instance");
|
||||||
|
setAppBridge(appBridgeInstance ?? new AppBridge());
|
||||||
|
} else {
|
||||||
|
debug("AppBridge provided in props, will use this one");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
(): AppBridgeContext => ({
|
||||||
|
appBridge,
|
||||||
|
mounted: true,
|
||||||
|
}),
|
||||||
|
[appBridge]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <AppContext.Provider value={contextValue} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppBridge = () => {
|
||||||
|
const { appBridge, mounted } = useContext(AppContext);
|
||||||
|
const [appBridgeState, setAppBridgeState] = useState<AppBridgeState | null>(() =>
|
||||||
|
appBridge ? appBridge.getState() : null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof window !== "undefined" && !mounted) {
|
||||||
|
throw new Error("useAppBridge used outside of AppBridgeProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateState = useCallback(() => {
|
||||||
|
if (appBridge?.getState()) {
|
||||||
|
debug("Detected state change in AppBridge, will set new state");
|
||||||
|
setAppBridgeState(appBridge.getState());
|
||||||
|
}
|
||||||
|
}, [appBridge]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unsubscribes: Array<Function> = [];
|
||||||
|
|
||||||
|
if (appBridge) {
|
||||||
|
debug("Provider mounted, will set up listeners");
|
||||||
|
|
||||||
|
unsubscribes = [
|
||||||
|
appBridge.subscribe("handshake", updateState),
|
||||||
|
appBridge.subscribe("theme", updateState),
|
||||||
|
appBridge.subscribe("response", updateState),
|
||||||
|
appBridge.subscribe("redirect", updateState),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debug("Provider unmounted, will clean up listeners");
|
||||||
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
};
|
||||||
|
}, [appBridge, updateState]);
|
||||||
|
|
||||||
|
return { appBridge, appBridgeState };
|
||||||
|
};
|
40
src/app-bridge/events.test.ts
Normal file
40
src/app-bridge/events.test.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { DashboardEventFactory } from "./events";
|
||||||
|
|
||||||
|
describe("DashboardEventFactory", () => {
|
||||||
|
it("Creates handshake event", () => {
|
||||||
|
expect(DashboardEventFactory.createHandshakeEvent("mock-token")).toEqual({
|
||||||
|
payload: {
|
||||||
|
token: "mock-token",
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
type: "handshake",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Creates redirect event", () => {
|
||||||
|
expect(DashboardEventFactory.createRedirectEvent("/new-path")).toEqual({
|
||||||
|
payload: {
|
||||||
|
path: "/new-path",
|
||||||
|
},
|
||||||
|
type: "redirect",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Creates dispatch response event", () => {
|
||||||
|
expect(DashboardEventFactory.createDispatchResponseEvent("123", true)).toEqual({
|
||||||
|
payload: {
|
||||||
|
actionId: "123",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
type: "response",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Creates theme change event", () => {
|
||||||
|
expect(DashboardEventFactory.createThemeChangeEvent("light")).toEqual({
|
||||||
|
payload: {
|
||||||
|
theme: "light",
|
||||||
|
},
|
||||||
|
type: "theme",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -67,3 +67,40 @@ export type PayloadOfEvent<
|
||||||
TEvent extends Events = Events
|
TEvent extends Events = Events
|
||||||
// @ts-ignore TODO - why this is not working with this tsconfig? Fixme
|
// @ts-ignore TODO - why this is not working with this tsconfig? Fixme
|
||||||
> = TEvent extends Event<TEventType, unknown> ? TEvent["payload"] : never;
|
> = TEvent extends Event<TEventType, unknown> ? TEvent["payload"] : never;
|
||||||
|
|
||||||
|
export const DashboardEventFactory = {
|
||||||
|
createThemeChangeEvent(theme: ThemeType): ThemeEvent {
|
||||||
|
return {
|
||||||
|
payload: {
|
||||||
|
theme,
|
||||||
|
},
|
||||||
|
type: "theme",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
createRedirectEvent(path: string): RedirectEvent {
|
||||||
|
return {
|
||||||
|
type: "redirect",
|
||||||
|
payload: {
|
||||||
|
path,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
createDispatchResponseEvent(actionId: string, ok: boolean): DispatchResponseEvent {
|
||||||
|
return {
|
||||||
|
type: "response",
|
||||||
|
payload: {
|
||||||
|
actionId,
|
||||||
|
ok,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
createHandshakeEvent(token: string, version: Version = 1): HandshakeEvent {
|
||||||
|
return {
|
||||||
|
type: "handshake",
|
||||||
|
payload: {
|
||||||
|
token,
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { AppBridge } from "./app-bridge";
|
||||||
export { AppBridge };
|
export { AppBridge };
|
||||||
|
|
||||||
export * from "./actions";
|
export * from "./actions";
|
||||||
|
export * from "./app-bridge-provider";
|
||||||
export * from "./events";
|
export * from "./events";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue