@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 +20 -0
- package/README.md +184 -0
- package/dist/index.d.mts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +50 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +69 -0
- package/src/index.ts +27 -0
- package/src/useAspectlyIframe.test.tsx +143 -0
- package/src/useAspectlyIframe.tsx +127 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
};
|