@aspectly/web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Zhan Isaakian
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # @aspectly/web
2
+
3
+ React hooks for embedding iframes and communicating with them using the Aspectly bridge protocol.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # npm
9
+ npm install @aspectly/web
10
+
11
+ # pnpm
12
+ pnpm add @aspectly/web
13
+
14
+ # yarn
15
+ yarn add @aspectly/web
16
+ ```
17
+
18
+ ## Overview
19
+
20
+ `@aspectly/web` provides React hooks for the parent page to embed iframes and establish bidirectional communication with them. The iframe content should use `@aspectly/core` to communicate back.
21
+
22
+ ## Quick Start
23
+
24
+ ### Parent Page (Host)
25
+
26
+ ```tsx
27
+ import { useAspectlyIframe } from '@aspectly/web';
28
+
29
+ function App() {
30
+ const [bridge, loaded, Iframe] = useAspectlyIframe({
31
+ url: 'https://example.com/widget'
32
+ });
33
+
34
+ useEffect(() => {
35
+ if (loaded) {
36
+ // Initialize bridge with handlers
37
+ bridge.init({
38
+ getUser: async () => ({ name: 'John', id: 123 }),
39
+ saveData: async (params) => {
40
+ console.log('Saving:', params);
41
+ return { success: true };
42
+ }
43
+ });
44
+ }
45
+ }, [loaded, bridge]);
46
+
47
+ const handleSendMessage = async () => {
48
+ try {
49
+ const result = await bridge.send('greet', { name: 'World' });
50
+ console.log('Response:', result);
51
+ } catch (error) {
52
+ console.error('Error:', error);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <div>
58
+ <h1>Parent App</h1>
59
+ <Iframe style={{ width: '100%', height: 400 }} />
60
+ <button onClick={handleSendMessage} disabled={!loaded}>
61
+ Send Message
62
+ </button>
63
+ </div>
64
+ );
65
+ }
66
+ ```
67
+
68
+ ### iframe Content (Widget)
69
+
70
+ The iframe should use `@aspectly/core`:
71
+
72
+ ```typescript
73
+ // Inside the iframe
74
+ import { AspectlyBridge } from '@aspectly/core';
75
+
76
+ const bridge = new AspectlyBridge();
77
+
78
+ // Initialize with handlers
79
+ await bridge.init({
80
+ greet: async (params: { name: string }) => {
81
+ return { message: `Hello, ${params.name}!` };
82
+ }
83
+ });
84
+
85
+ // Call parent methods
86
+ const user = await bridge.send('getUser');
87
+ console.log('User:', user);
88
+ ```
89
+
90
+ ## API Reference
91
+
92
+ ### useAspectlyIframe
93
+
94
+ ```typescript
95
+ const [bridge, loaded, IframeComponent] = useAspectlyIframe(options);
96
+ ```
97
+
98
+ #### Options
99
+
100
+ | Property | Type | Required | Description |
101
+ |----------|------|----------|-------------|
102
+ | `url` | `string` | Yes | URL to load in the iframe |
103
+ | `timeout` | `number` | No | Handler execution timeout in ms (default: 100000) |
104
+
105
+ #### Returns
106
+
107
+ | Index | Type | Description |
108
+ |-------|------|-------------|
109
+ | 0 | `BridgeBase` | Bridge instance for communication |
110
+ | 1 | `boolean` | Whether the iframe has finished loading |
111
+ | 2 | `FunctionComponent` | React component to render the iframe |
112
+
113
+ ### IframeComponent Props
114
+
115
+ The returned iframe component accepts all standard `<iframe>` HTML attributes plus:
116
+
117
+ | Prop | Type | Description |
118
+ |------|------|-------------|
119
+ | `style` | `CSSProperties` | Custom styles (border: 0 is applied by default) |
120
+ | `onError` | `(error: unknown) => void` | Optional error handler |
121
+
122
+ ## Patterns
123
+
124
+ ### Checking Method Support
125
+
126
+ ```tsx
127
+ const handleAction = async () => {
128
+ if (bridge.supports('advancedFeature')) {
129
+ await bridge.send('advancedFeature', { data: 'value' });
130
+ } else {
131
+ // Fallback for older widget versions
132
+ await bridge.send('basicFeature', { data: 'value' });
133
+ }
134
+ };
135
+ ```
136
+
137
+ ### Subscribing to Events
138
+
139
+ ```tsx
140
+ useEffect(() => {
141
+ const handleEvent = (result) => {
142
+ console.log('Bridge event:', result.method, result.data);
143
+ };
144
+
145
+ bridge.subscribe(handleEvent);
146
+
147
+ return () => bridge.unsubscribe(handleEvent);
148
+ }, [bridge]);
149
+ ```
150
+
151
+ ### Error Handling
152
+
153
+ ```tsx
154
+ import { BridgeErrorType } from '@aspectly/web';
155
+
156
+ const handleSend = async () => {
157
+ try {
158
+ await bridge.send('action', { data: 'value' });
159
+ } catch (error) {
160
+ if (error.error_type === BridgeErrorType.BRIDGE_NOT_AVAILABLE) {
161
+ console.log('Widget not ready yet');
162
+ } else if (error.error_type === BridgeErrorType.UNSUPPORTED_METHOD) {
163
+ console.log('Method not supported by widget');
164
+ }
165
+ }
166
+ };
167
+ ```
168
+
169
+ ## Security Considerations
170
+
171
+ - The bridge uses `postMessage` for communication
172
+ - By default, messages are sent with `'*'` origin - consider restricting this in production
173
+ - Always validate incoming data in your handlers
174
+ - Use HTTPS for iframe sources in production
175
+
176
+ ## Related Packages
177
+
178
+ - [`@aspectly/core`](../core) - Core bridge framework (used inside iframes)
179
+ - [`@aspectly/react-native`](../react-native) - React Native WebView integration
180
+ - [`@aspectly/react-native-web`](../react-native-web) - React Native Web + iframe support
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,68 @@
1
+ import { FunctionComponent, IframeHTMLAttributes, CSSProperties } from 'react';
2
+ import { BridgeOptions, BridgeBase } from '@aspectly/core';
3
+ export { AspectlyBridge, BridgeBase, BridgeErrorType, BridgeEventType, BridgeHandler, BridgeHandlers, BridgeListener, BridgeOptions, BridgeResultError, BridgeResultEvent, BridgeResultType } from '@aspectly/core';
4
+
5
+ /**
6
+ * Options for the useAspectlyIframe hook
7
+ */
8
+ interface UseAspectlyIframeOptions extends BridgeOptions {
9
+ /** URL to load in the iframe */
10
+ url: string;
11
+ }
12
+ /**
13
+ * Props for the iframe component
14
+ */
15
+ interface AspectlyIframeProps extends Omit<IframeHTMLAttributes<HTMLIFrameElement>, 'src' | 'onLoad'> {
16
+ /** Optional error handler */
17
+ onError?: (error: unknown) => void;
18
+ /** Custom styles */
19
+ style?: CSSProperties;
20
+ }
21
+ /**
22
+ * Return type for useAspectlyIframe hook
23
+ */
24
+ type UseAspectlyIframeReturn = [
25
+ /** Bridge instance for communication */
26
+ bridge: BridgeBase,
27
+ /** Whether the iframe has loaded */
28
+ loaded: boolean,
29
+ /** React component to render the iframe */
30
+ IframeComponent: FunctionComponent<AspectlyIframeProps>
31
+ ];
32
+ /**
33
+ * React hook for embedding an iframe and communicating with it via Aspectly bridge.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { useAspectlyIframe } from '@aspectly/web';
38
+ *
39
+ * function App() {
40
+ * const [bridge, loaded, Iframe] = useAspectlyIframe({
41
+ * url: 'https://example.com/widget'
42
+ * });
43
+ *
44
+ * useEffect(() => {
45
+ * if (loaded) {
46
+ * bridge.init({
47
+ * getData: async () => ({ user: 'John' })
48
+ * });
49
+ * }
50
+ * }, [loaded, bridge]);
51
+ *
52
+ * const handleClick = async () => {
53
+ * const result = await bridge.send('greet', { name: 'World' });
54
+ * console.log(result);
55
+ * };
56
+ *
57
+ * return (
58
+ * <div>
59
+ * <Iframe style={{ width: '100%', height: 400 }} />
60
+ * <button onClick={handleClick}>Send Message</button>
61
+ * </div>
62
+ * );
63
+ * }
64
+ * ```
65
+ */
66
+ declare const useAspectlyIframe: ({ url, timeout, }: UseAspectlyIframeOptions) => UseAspectlyIframeReturn;
67
+
68
+ export { type AspectlyIframeProps, type UseAspectlyIframeOptions, type UseAspectlyIframeReturn, useAspectlyIframe };
@@ -0,0 +1,68 @@
1
+ import { FunctionComponent, IframeHTMLAttributes, CSSProperties } from 'react';
2
+ import { BridgeOptions, BridgeBase } from '@aspectly/core';
3
+ export { AspectlyBridge, BridgeBase, BridgeErrorType, BridgeEventType, BridgeHandler, BridgeHandlers, BridgeListener, BridgeOptions, BridgeResultError, BridgeResultEvent, BridgeResultType } from '@aspectly/core';
4
+
5
+ /**
6
+ * Options for the useAspectlyIframe hook
7
+ */
8
+ interface UseAspectlyIframeOptions extends BridgeOptions {
9
+ /** URL to load in the iframe */
10
+ url: string;
11
+ }
12
+ /**
13
+ * Props for the iframe component
14
+ */
15
+ interface AspectlyIframeProps extends Omit<IframeHTMLAttributes<HTMLIFrameElement>, 'src' | 'onLoad'> {
16
+ /** Optional error handler */
17
+ onError?: (error: unknown) => void;
18
+ /** Custom styles */
19
+ style?: CSSProperties;
20
+ }
21
+ /**
22
+ * Return type for useAspectlyIframe hook
23
+ */
24
+ type UseAspectlyIframeReturn = [
25
+ /** Bridge instance for communication */
26
+ bridge: BridgeBase,
27
+ /** Whether the iframe has loaded */
28
+ loaded: boolean,
29
+ /** React component to render the iframe */
30
+ IframeComponent: FunctionComponent<AspectlyIframeProps>
31
+ ];
32
+ /**
33
+ * React hook for embedding an iframe and communicating with it via Aspectly bridge.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { useAspectlyIframe } from '@aspectly/web';
38
+ *
39
+ * function App() {
40
+ * const [bridge, loaded, Iframe] = useAspectlyIframe({
41
+ * url: 'https://example.com/widget'
42
+ * });
43
+ *
44
+ * useEffect(() => {
45
+ * if (loaded) {
46
+ * bridge.init({
47
+ * getData: async () => ({ user: 'John' })
48
+ * });
49
+ * }
50
+ * }, [loaded, bridge]);
51
+ *
52
+ * const handleClick = async () => {
53
+ * const result = await bridge.send('greet', { name: 'World' });
54
+ * console.log(result);
55
+ * };
56
+ *
57
+ * return (
58
+ * <div>
59
+ * <Iframe style={{ width: '100%', height: 400 }} />
60
+ * <button onClick={handleClick}>Send Message</button>
61
+ * </div>
62
+ * );
63
+ * }
64
+ * ```
65
+ */
66
+ declare const useAspectlyIframe: ({ url, timeout, }: UseAspectlyIframeOptions) => UseAspectlyIframeReturn;
67
+
68
+ export { type AspectlyIframeProps, type UseAspectlyIframeOptions, type UseAspectlyIframeReturn, useAspectlyIframe };
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var core = require('@aspectly/core');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/useAspectlyIframe.tsx
8
+ var useAspectlyIframe = ({
9
+ url,
10
+ timeout
11
+ }) => {
12
+ const iframeRef = react.useRef(null);
13
+ const [loaded, setLoaded] = react.useState(false);
14
+ const bridge = react.useMemo(() => {
15
+ return new core.BridgeInternal((event) => {
16
+ const bridgeEvent = core.BridgeCore.wrapBridgeEvent(event);
17
+ iframeRef.current?.contentWindow?.postMessage(bridgeEvent, "*");
18
+ }, { timeout });
19
+ }, [timeout]);
20
+ const publicBridge = react.useMemo(() => new core.BridgeBase(bridge), [bridge]);
21
+ react.useEffect(() => {
22
+ const unsubscribe = core.BridgeCore.subscribe(
23
+ bridge.handleCoreEvent
24
+ );
25
+ return () => unsubscribe();
26
+ }, [bridge]);
27
+ const onLoad = react.useCallback(() => setLoaded(true), []);
28
+ const IframeComponent = react.useCallback(
29
+ ({ style, ...props }) => {
30
+ return /* @__PURE__ */ jsxRuntime.jsx(
31
+ "iframe",
32
+ {
33
+ ...props,
34
+ onLoad,
35
+ ref: iframeRef,
36
+ style: {
37
+ border: 0,
38
+ ...style
39
+ },
40
+ src: url
41
+ }
42
+ );
43
+ },
44
+ [url, onLoad]
45
+ );
46
+ return [publicBridge, loaded, IframeComponent];
47
+ };
48
+
49
+ Object.defineProperty(exports, "AspectlyBridge", {
50
+ enumerable: true,
51
+ get: function () { return core.AspectlyBridge; }
52
+ });
53
+ Object.defineProperty(exports, "BridgeBase", {
54
+ enumerable: true,
55
+ get: function () { return core.BridgeBase; }
56
+ });
57
+ Object.defineProperty(exports, "BridgeErrorType", {
58
+ enumerable: true,
59
+ get: function () { return core.BridgeErrorType; }
60
+ });
61
+ Object.defineProperty(exports, "BridgeEventType", {
62
+ enumerable: true,
63
+ get: function () { return core.BridgeEventType; }
64
+ });
65
+ Object.defineProperty(exports, "BridgeResultType", {
66
+ enumerable: true,
67
+ get: function () { return core.BridgeResultType; }
68
+ });
69
+ exports.useAspectlyIframe = useAspectlyIframe;
70
+ //# sourceMappingURL=index.js.map
71
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useAspectlyIframe.tsx"],"names":["useRef","useState","useMemo","BridgeInternal","BridgeCore","BridgeBase","useEffect","useCallback","jsx"],"mappings":";;;;;;;AAkFO,IAAM,oBAAoB,CAAC;AAAA,EAChC,GAAA;AAAA,EACA;AACF,CAAA,KAAyD;AACvD,EAAA,MAAM,SAAA,GAAYA,aAA0B,IAAI,CAAA;AAChD,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAIC,eAAkB,KAAK,CAAA;AAEnD,EAAA,MAAM,MAAA,GAASC,cAAQ,MAAM;AAC3B,IAAA,OAAO,IAAIC,mBAAA,CAAe,CAAC,KAAA,KAAwB;AACjD,MAAA,MAAM,WAAA,GAAcC,eAAA,CAAW,eAAA,CAAgB,KAAK,CAAA;AACpD,MAAA,SAAA,CAAU,OAAA,EAAS,aAAA,EAAe,WAAA,CAAY,WAAA,EAAa,GAAG,CAAA;AAAA,IAChE,CAAA,EAAG,EAAE,OAAA,EAAS,CAAA;AAAA,EAChB,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,MAAM,YAAA,GAAeF,cAAQ,MAAM,IAAIG,gBAAW,MAAM,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEnE,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,cAAcF,eAAA,CAAW,SAAA;AAAA,MAC7B,MAAA,CAAO;AAAA,KACT;AACA,IAAA,OAAO,MAAM,WAAA,EAAY;AAAA,EAC3B,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,MAAM,SAASG,iBAAA,CAAY,MAAM,UAAU,IAAI,CAAA,EAAG,EAAE,CAAA;AAEpD,EAAA,MAAM,eAAA,GAA0DA,iBAAA;AAAA,IAC9D,CAAC,EAAE,KAAA,EAAO,GAAG,OAAM,KAA2B;AAC5C,MAAA,uBACEC,cAAA;AAAA,QAAC,QAAA;AAAA,QAAA;AAAA,UACE,GAAG,KAAA;AAAA,UACJ,MAAA;AAAA,UACA,GAAA,EAAK,SAAA;AAAA,UACL,KAAA,EAAO;AAAA,YACL,MAAA,EAAQ,CAAA;AAAA,YACR,GAAG;AAAA,WACL;AAAA,UACA,GAAA,EAAK;AAAA;AAAA,OACP;AAAA,IAEJ,CAAA;AAAA,IACA,CAAC,KAAK,MAAM;AAAA,GACd;AAEA,EAAA,OAAO,CAAC,YAAA,EAAc,MAAA,EAAQ,eAAe,CAAA;AAC/C","file":"index.js","sourcesContent":["import {\n FunctionComponent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n CSSProperties,\n IframeHTMLAttributes,\n} from 'react';\nimport {\n BridgeCore,\n BridgeInternal,\n BridgeBase,\n BridgeOptions,\n} from '@aspectly/core';\n\n/**\n * Options for the useAspectlyIframe hook\n */\nexport interface UseAspectlyIframeOptions extends BridgeOptions {\n /** URL to load in the iframe */\n url: string;\n}\n\n/**\n * Props for the iframe component\n */\nexport interface AspectlyIframeProps\n extends Omit<IframeHTMLAttributes<HTMLIFrameElement>, 'src' | 'onLoad'> {\n /** Optional error handler */\n onError?: (error: unknown) => void;\n /** Custom styles */\n style?: CSSProperties;\n}\n\n/**\n * Return type for useAspectlyIframe hook\n */\nexport type UseAspectlyIframeReturn = [\n /** Bridge instance for communication */\n bridge: BridgeBase,\n /** Whether the iframe has loaded */\n loaded: boolean,\n /** React component to render the iframe */\n IframeComponent: FunctionComponent<AspectlyIframeProps>\n];\n\n/**\n * React hook for embedding an iframe and communicating with it via Aspectly bridge.\n *\n * @example\n * ```tsx\n * import { useAspectlyIframe } from '@aspectly/web';\n *\n * function App() {\n * const [bridge, loaded, Iframe] = useAspectlyIframe({\n * url: 'https://example.com/widget'\n * });\n *\n * useEffect(() => {\n * if (loaded) {\n * bridge.init({\n * getData: async () => ({ user: 'John' })\n * });\n * }\n * }, [loaded, bridge]);\n *\n * const handleClick = async () => {\n * const result = await bridge.send('greet', { name: 'World' });\n * console.log(result);\n * };\n *\n * return (\n * <div>\n * <Iframe style={{ width: '100%', height: 400 }} />\n * <button onClick={handleClick}>Send Message</button>\n * </div>\n * );\n * }\n * ```\n */\nexport const useAspectlyIframe = ({\n url,\n timeout,\n}: UseAspectlyIframeOptions): UseAspectlyIframeReturn => {\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const [loaded, setLoaded] = useState<boolean>(false);\n\n const bridge = useMemo(() => {\n return new BridgeInternal((event: object): void => {\n const bridgeEvent = BridgeCore.wrapBridgeEvent(event);\n iframeRef.current?.contentWindow?.postMessage(bridgeEvent, '*');\n }, { timeout });\n }, [timeout]);\n\n const publicBridge = useMemo(() => new BridgeBase(bridge), [bridge]);\n\n useEffect(() => {\n const unsubscribe = BridgeCore.subscribe(\n bridge.handleCoreEvent as (event: unknown) => void\n );\n return () => unsubscribe();\n }, [bridge]);\n\n const onLoad = useCallback(() => setLoaded(true), []);\n\n const IframeComponent: FunctionComponent<AspectlyIframeProps> = useCallback(\n ({ style, ...props }: AspectlyIframeProps) => {\n return (\n <iframe\n {...props}\n onLoad={onLoad}\n ref={iframeRef}\n style={{\n border: 0,\n ...style,\n }}\n src={url}\n />\n );\n },\n [url, onLoad]\n );\n\n return [publicBridge, loaded, IframeComponent];\n};\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,50 @@
1
+ import { useRef, useState, useMemo, useEffect, useCallback } from 'react';
2
+ import { BridgeInternal, BridgeCore, BridgeBase } from '@aspectly/core';
3
+ export { AspectlyBridge, BridgeBase, BridgeErrorType, BridgeEventType, BridgeResultType } from '@aspectly/core';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ // src/useAspectlyIframe.tsx
7
+ var useAspectlyIframe = ({
8
+ url,
9
+ timeout
10
+ }) => {
11
+ const iframeRef = useRef(null);
12
+ const [loaded, setLoaded] = useState(false);
13
+ const bridge = useMemo(() => {
14
+ return new BridgeInternal((event) => {
15
+ const bridgeEvent = BridgeCore.wrapBridgeEvent(event);
16
+ iframeRef.current?.contentWindow?.postMessage(bridgeEvent, "*");
17
+ }, { timeout });
18
+ }, [timeout]);
19
+ const publicBridge = useMemo(() => new BridgeBase(bridge), [bridge]);
20
+ useEffect(() => {
21
+ const unsubscribe = BridgeCore.subscribe(
22
+ bridge.handleCoreEvent
23
+ );
24
+ return () => unsubscribe();
25
+ }, [bridge]);
26
+ const onLoad = useCallback(() => setLoaded(true), []);
27
+ const IframeComponent = useCallback(
28
+ ({ style, ...props }) => {
29
+ return /* @__PURE__ */ jsx(
30
+ "iframe",
31
+ {
32
+ ...props,
33
+ onLoad,
34
+ ref: iframeRef,
35
+ style: {
36
+ border: 0,
37
+ ...style
38
+ },
39
+ src: url
40
+ }
41
+ );
42
+ },
43
+ [url, onLoad]
44
+ );
45
+ return [publicBridge, loaded, IframeComponent];
46
+ };
47
+
48
+ export { useAspectlyIframe };
49
+ //# sourceMappingURL=index.mjs.map
50
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useAspectlyIframe.tsx"],"names":[],"mappings":";;;;;;AAkFO,IAAM,oBAAoB,CAAC;AAAA,EAChC,GAAA;AAAA,EACA;AACF,CAAA,KAAyD;AACvD,EAAA,MAAM,SAAA,GAAY,OAA0B,IAAI,CAAA;AAChD,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,SAAkB,KAAK,CAAA;AAEnD,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAM;AAC3B,IAAA,OAAO,IAAI,cAAA,CAAe,CAAC,KAAA,KAAwB;AACjD,MAAA,MAAM,WAAA,GAAc,UAAA,CAAW,eAAA,CAAgB,KAAK,CAAA;AACpD,MAAA,SAAA,CAAU,OAAA,EAAS,aAAA,EAAe,WAAA,CAAY,WAAA,EAAa,GAAG,CAAA;AAAA,IAChE,CAAA,EAAG,EAAE,OAAA,EAAS,CAAA;AAAA,EAChB,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,MAAM,YAAA,GAAe,QAAQ,MAAM,IAAI,WAAW,MAAM,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEnE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,cAAc,UAAA,CAAW,SAAA;AAAA,MAC7B,MAAA,CAAO;AAAA,KACT;AACA,IAAA,OAAO,MAAM,WAAA,EAAY;AAAA,EAC3B,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,MAAM,SAAS,WAAA,CAAY,MAAM,UAAU,IAAI,CAAA,EAAG,EAAE,CAAA;AAEpD,EAAA,MAAM,eAAA,GAA0D,WAAA;AAAA,IAC9D,CAAC,EAAE,KAAA,EAAO,GAAG,OAAM,KAA2B;AAC5C,MAAA,uBACE,GAAA;AAAA,QAAC,QAAA;AAAA,QAAA;AAAA,UACE,GAAG,KAAA;AAAA,UACJ,MAAA;AAAA,UACA,GAAA,EAAK,SAAA;AAAA,UACL,KAAA,EAAO;AAAA,YACL,MAAA,EAAQ,CAAA;AAAA,YACR,GAAG;AAAA,WACL;AAAA,UACA,GAAA,EAAK;AAAA;AAAA,OACP;AAAA,IAEJ,CAAA;AAAA,IACA,CAAC,KAAK,MAAM;AAAA,GACd;AAEA,EAAA,OAAO,CAAC,YAAA,EAAc,MAAA,EAAQ,eAAe,CAAA;AAC/C","file":"index.mjs","sourcesContent":["import {\n FunctionComponent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n CSSProperties,\n IframeHTMLAttributes,\n} from 'react';\nimport {\n BridgeCore,\n BridgeInternal,\n BridgeBase,\n BridgeOptions,\n} from '@aspectly/core';\n\n/**\n * Options for the useAspectlyIframe hook\n */\nexport interface UseAspectlyIframeOptions extends BridgeOptions {\n /** URL to load in the iframe */\n url: string;\n}\n\n/**\n * Props for the iframe component\n */\nexport interface AspectlyIframeProps\n extends Omit<IframeHTMLAttributes<HTMLIFrameElement>, 'src' | 'onLoad'> {\n /** Optional error handler */\n onError?: (error: unknown) => void;\n /** Custom styles */\n style?: CSSProperties;\n}\n\n/**\n * Return type for useAspectlyIframe hook\n */\nexport type UseAspectlyIframeReturn = [\n /** Bridge instance for communication */\n bridge: BridgeBase,\n /** Whether the iframe has loaded */\n loaded: boolean,\n /** React component to render the iframe */\n IframeComponent: FunctionComponent<AspectlyIframeProps>\n];\n\n/**\n * React hook for embedding an iframe and communicating with it via Aspectly bridge.\n *\n * @example\n * ```tsx\n * import { useAspectlyIframe } from '@aspectly/web';\n *\n * function App() {\n * const [bridge, loaded, Iframe] = useAspectlyIframe({\n * url: 'https://example.com/widget'\n * });\n *\n * useEffect(() => {\n * if (loaded) {\n * bridge.init({\n * getData: async () => ({ user: 'John' })\n * });\n * }\n * }, [loaded, bridge]);\n *\n * const handleClick = async () => {\n * const result = await bridge.send('greet', { name: 'World' });\n * console.log(result);\n * };\n *\n * return (\n * <div>\n * <Iframe style={{ width: '100%', height: 400 }} />\n * <button onClick={handleClick}>Send Message</button>\n * </div>\n * );\n * }\n * ```\n */\nexport const useAspectlyIframe = ({\n url,\n timeout,\n}: UseAspectlyIframeOptions): UseAspectlyIframeReturn => {\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const [loaded, setLoaded] = useState<boolean>(false);\n\n const bridge = useMemo(() => {\n return new BridgeInternal((event: object): void => {\n const bridgeEvent = BridgeCore.wrapBridgeEvent(event);\n iframeRef.current?.contentWindow?.postMessage(bridgeEvent, '*');\n }, { timeout });\n }, [timeout]);\n\n const publicBridge = useMemo(() => new BridgeBase(bridge), [bridge]);\n\n useEffect(() => {\n const unsubscribe = BridgeCore.subscribe(\n bridge.handleCoreEvent as (event: unknown) => void\n );\n return () => unsubscribe();\n }, [bridge]);\n\n const onLoad = useCallback(() => setLoaded(true), []);\n\n const IframeComponent: FunctionComponent<AspectlyIframeProps> = useCallback(\n ({ style, ...props }: AspectlyIframeProps) => {\n return (\n <iframe\n {...props}\n onLoad={onLoad}\n ref={iframeRef}\n style={{\n border: 0,\n ...style,\n }}\n src={url}\n />\n );\n },\n [url, onLoad]\n );\n\n return [publicBridge, loaded, IframeComponent];\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@aspectly/web",
3
+ "version": "0.1.0",
4
+ "description": "Web/iframe integration for Aspectly bridge - React hooks for embedding and communicating with iframes",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.mts",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "sideEffects": false,
25
+ "keywords": [
26
+ "iframe",
27
+ "bridge",
28
+ "communication",
29
+ "react",
30
+ "hooks",
31
+ "postmessage",
32
+ "web"
33
+ ],
34
+ "author": "Zhan Isaakian <jeanisahkyan@gmail.com>",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/JeanIsahakyan/aspectly",
39
+ "directory": "packages/web"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/JeanIsahakyan/aspectly/issues"
43
+ },
44
+ "homepage": "https://github.com/JeanIsahakyan/aspectly/tree/main/packages/web#readme",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "peerDependencies": {
49
+ "react": ">=17.0.0"
50
+ },
51
+ "dependencies": {
52
+ "@aspectly/core": "0.1.0"
53
+ },
54
+ "devDependencies": {
55
+ "@types/react": "^18.2.0",
56
+ "react": "^18.2.0",
57
+ "tsup": "^8.0.1",
58
+ "typescript": "^5.3.2",
59
+ "rimraf": "^5.0.5"
60
+ },
61
+ "scripts": {
62
+ "build": "tsup",
63
+ "dev": "tsup --watch",
64
+ "typecheck": "tsc --noEmit",
65
+ "lint": "eslint src --ext .ts,.tsx",
66
+ "clean": "rimraf dist",
67
+ "test": "vitest run"
68
+ }
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,27 @@
1
+ // Main exports
2
+ export { useAspectlyIframe } from './useAspectlyIframe';
3
+
4
+ // Type exports
5
+ export type {
6
+ UseAspectlyIframeOptions,
7
+ UseAspectlyIframeReturn,
8
+ AspectlyIframeProps,
9
+ } from './useAspectlyIframe';
10
+
11
+ // Re-export core types for convenience
12
+ export {
13
+ AspectlyBridge,
14
+ BridgeBase,
15
+ BridgeErrorType,
16
+ BridgeEventType,
17
+ BridgeResultType,
18
+ } from '@aspectly/core';
19
+
20
+ export type {
21
+ BridgeHandler,
22
+ BridgeHandlers,
23
+ BridgeListener,
24
+ BridgeOptions,
25
+ BridgeResultError,
26
+ BridgeResultEvent,
27
+ } from '@aspectly/core';
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useAspectlyIframe } from './useAspectlyIframe';
4
+
5
+ // Mock @aspectly/core
6
+ vi.mock('@aspectly/core', () => ({
7
+ BridgeCore: {
8
+ wrapBridgeEvent: vi.fn((event) => JSON.stringify({ type: 'BridgeEvent', event })),
9
+ subscribe: vi.fn().mockReturnValue(vi.fn()),
10
+ },
11
+ BridgeInternal: vi.fn().mockImplementation(() => ({
12
+ init: vi.fn().mockResolvedValue(true),
13
+ send: vi.fn().mockResolvedValue({}),
14
+ subscribe: vi.fn(),
15
+ unsubscribe: vi.fn(),
16
+ supports: vi.fn(),
17
+ isAvailable: vi.fn(),
18
+ handleCoreEvent: vi.fn(),
19
+ })),
20
+ BridgeBase: vi.fn().mockImplementation((internal) => ({
21
+ init: internal.init,
22
+ send: internal.send,
23
+ subscribe: internal.subscribe,
24
+ unsubscribe: internal.unsubscribe,
25
+ supports: internal.supports,
26
+ isAvailable: internal.isAvailable,
27
+ })),
28
+ }));
29
+
30
+ describe('useAspectlyIframe', () => {
31
+ beforeEach(() => {
32
+ vi.clearAllMocks();
33
+ });
34
+
35
+ describe('hook return values', () => {
36
+ it('should return bridge, loaded state, and component', () => {
37
+ const { result } = renderHook(() =>
38
+ useAspectlyIframe({ url: 'https://example.com' })
39
+ );
40
+
41
+ const [bridge, loaded, IframeComponent] = result.current;
42
+
43
+ expect(bridge).toBeDefined();
44
+ expect(typeof loaded).toBe('boolean');
45
+ expect(typeof IframeComponent).toBe('function');
46
+ });
47
+
48
+ it('should initially have loaded as false', () => {
49
+ const { result } = renderHook(() =>
50
+ useAspectlyIframe({ url: 'https://example.com' })
51
+ );
52
+
53
+ const [, loaded] = result.current;
54
+ expect(loaded).toBe(false);
55
+ });
56
+ });
57
+
58
+ describe('bridge instance', () => {
59
+ it('should have init method', () => {
60
+ const { result } = renderHook(() =>
61
+ useAspectlyIframe({ url: 'https://example.com' })
62
+ );
63
+
64
+ const [bridge] = result.current;
65
+ expect(typeof bridge.init).toBe('function');
66
+ });
67
+
68
+ it('should have send method', () => {
69
+ const { result } = renderHook(() =>
70
+ useAspectlyIframe({ url: 'https://example.com' })
71
+ );
72
+
73
+ const [bridge] = result.current;
74
+ expect(typeof bridge.send).toBe('function');
75
+ });
76
+
77
+ it('should have subscribe method', () => {
78
+ const { result } = renderHook(() =>
79
+ useAspectlyIframe({ url: 'https://example.com' })
80
+ );
81
+
82
+ const [bridge] = result.current;
83
+ expect(typeof bridge.subscribe).toBe('function');
84
+ });
85
+
86
+ it('should have unsubscribe method', () => {
87
+ const { result } = renderHook(() =>
88
+ useAspectlyIframe({ url: 'https://example.com' })
89
+ );
90
+
91
+ const [bridge] = result.current;
92
+ expect(typeof bridge.unsubscribe).toBe('function');
93
+ });
94
+ });
95
+
96
+ describe('IframeComponent', () => {
97
+ it('should be a valid React component', () => {
98
+ const { result } = renderHook(() =>
99
+ useAspectlyIframe({ url: 'https://example.com' })
100
+ );
101
+
102
+ const [, , IframeComponent] = result.current;
103
+ expect(IframeComponent).toBeDefined();
104
+ expect(IframeComponent.name).toBeDefined();
105
+ });
106
+ });
107
+
108
+ describe('memoization', () => {
109
+ it('should return same bridge instance across re-renders', () => {
110
+ const { result, rerender } = renderHook(() =>
111
+ useAspectlyIframe({ url: 'https://example.com' })
112
+ );
113
+
114
+ const firstBridge = result.current[0];
115
+ rerender();
116
+ const secondBridge = result.current[0];
117
+
118
+ expect(firstBridge).toBe(secondBridge);
119
+ });
120
+
121
+ it('should return same component across re-renders with same url', () => {
122
+ const { result, rerender } = renderHook(() =>
123
+ useAspectlyIframe({ url: 'https://example.com' })
124
+ );
125
+
126
+ const firstComponent = result.current[2];
127
+ rerender();
128
+ const secondComponent = result.current[2];
129
+
130
+ expect(firstComponent).toBe(secondComponent);
131
+ });
132
+ });
133
+
134
+ describe('options', () => {
135
+ it('should accept timeout option', () => {
136
+ const { result } = renderHook(() =>
137
+ useAspectlyIframe({ url: 'https://example.com', timeout: 5000 })
138
+ );
139
+
140
+ expect(result.current).toBeDefined();
141
+ });
142
+ });
143
+ });
@@ -0,0 +1,127 @@
1
+ import {
2
+ FunctionComponent,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ CSSProperties,
9
+ IframeHTMLAttributes,
10
+ } from 'react';
11
+ import {
12
+ BridgeCore,
13
+ BridgeInternal,
14
+ BridgeBase,
15
+ BridgeOptions,
16
+ } from '@aspectly/core';
17
+
18
+ /**
19
+ * Options for the useAspectlyIframe hook
20
+ */
21
+ export interface UseAspectlyIframeOptions extends BridgeOptions {
22
+ /** URL to load in the iframe */
23
+ url: string;
24
+ }
25
+
26
+ /**
27
+ * Props for the iframe component
28
+ */
29
+ export interface AspectlyIframeProps
30
+ extends Omit<IframeHTMLAttributes<HTMLIFrameElement>, 'src' | 'onLoad'> {
31
+ /** Optional error handler */
32
+ onError?: (error: unknown) => void;
33
+ /** Custom styles */
34
+ style?: CSSProperties;
35
+ }
36
+
37
+ /**
38
+ * Return type for useAspectlyIframe hook
39
+ */
40
+ export type UseAspectlyIframeReturn = [
41
+ /** Bridge instance for communication */
42
+ bridge: BridgeBase,
43
+ /** Whether the iframe has loaded */
44
+ loaded: boolean,
45
+ /** React component to render the iframe */
46
+ IframeComponent: FunctionComponent<AspectlyIframeProps>
47
+ ];
48
+
49
+ /**
50
+ * React hook for embedding an iframe and communicating with it via Aspectly bridge.
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * import { useAspectlyIframe } from '@aspectly/web';
55
+ *
56
+ * function App() {
57
+ * const [bridge, loaded, Iframe] = useAspectlyIframe({
58
+ * url: 'https://example.com/widget'
59
+ * });
60
+ *
61
+ * useEffect(() => {
62
+ * if (loaded) {
63
+ * bridge.init({
64
+ * getData: async () => ({ user: 'John' })
65
+ * });
66
+ * }
67
+ * }, [loaded, bridge]);
68
+ *
69
+ * const handleClick = async () => {
70
+ * const result = await bridge.send('greet', { name: 'World' });
71
+ * console.log(result);
72
+ * };
73
+ *
74
+ * return (
75
+ * <div>
76
+ * <Iframe style={{ width: '100%', height: 400 }} />
77
+ * <button onClick={handleClick}>Send Message</button>
78
+ * </div>
79
+ * );
80
+ * }
81
+ * ```
82
+ */
83
+ export const useAspectlyIframe = ({
84
+ url,
85
+ timeout,
86
+ }: UseAspectlyIframeOptions): UseAspectlyIframeReturn => {
87
+ const iframeRef = useRef<HTMLIFrameElement>(null);
88
+ const [loaded, setLoaded] = useState<boolean>(false);
89
+
90
+ const bridge = useMemo(() => {
91
+ return new BridgeInternal((event: object): void => {
92
+ const bridgeEvent = BridgeCore.wrapBridgeEvent(event);
93
+ iframeRef.current?.contentWindow?.postMessage(bridgeEvent, '*');
94
+ }, { timeout });
95
+ }, [timeout]);
96
+
97
+ const publicBridge = useMemo(() => new BridgeBase(bridge), [bridge]);
98
+
99
+ useEffect(() => {
100
+ const unsubscribe = BridgeCore.subscribe(
101
+ bridge.handleCoreEvent as (event: unknown) => void
102
+ );
103
+ return () => unsubscribe();
104
+ }, [bridge]);
105
+
106
+ const onLoad = useCallback(() => setLoaded(true), []);
107
+
108
+ const IframeComponent: FunctionComponent<AspectlyIframeProps> = useCallback(
109
+ ({ style, ...props }: AspectlyIframeProps) => {
110
+ return (
111
+ <iframe
112
+ {...props}
113
+ onLoad={onLoad}
114
+ ref={iframeRef}
115
+ style={{
116
+ border: 0,
117
+ ...style,
118
+ }}
119
+ src={url}
120
+ />
121
+ );
122
+ },
123
+ [url, onLoad]
124
+ );
125
+
126
+ return [publicBridge, loaded, IframeComponent];
127
+ };