@aspectly/core 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 +188 -0
- package/dist/AspectlyBridge-hGuDjcI6.d.mts +242 -0
- package/dist/AspectlyBridge-hGuDjcI6.d.ts +242 -0
- package/dist/browser.d.mts +24 -0
- package/dist/browser.d.ts +24 -0
- package/dist/browser.js +398 -0
- package/dist/browser.js.map +1 -0
- package/dist/browser.mjs +396 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/index.d.mts +54 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +420 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +412 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +71 -0
- package/src/AspectlyBridge.test.ts +78 -0
- package/src/AspectlyBridge.ts +44 -0
- package/src/BridgeBase.test.ts +132 -0
- package/src/BridgeBase.ts +63 -0
- package/src/BridgeCore.test.ts +254 -0
- package/src/BridgeCore.ts +138 -0
- package/src/BridgeInternal.test.ts +403 -0
- package/src/BridgeInternal.ts +305 -0
- package/src/browser.ts +27 -0
- package/src/index.ts +28 -0
- package/src/types.ts +134 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { BridgeBase } from './BridgeBase';
|
|
3
|
+
import { BridgeInternal } from './BridgeInternal';
|
|
4
|
+
|
|
5
|
+
describe('BridgeBase', () => {
|
|
6
|
+
let mockBridge: BridgeInternal;
|
|
7
|
+
let bridgeBase: BridgeBase;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockBridge = {
|
|
11
|
+
supports: vi.fn().mockReturnValue(true),
|
|
12
|
+
isAvailable: vi.fn().mockReturnValue(true),
|
|
13
|
+
send: vi.fn().mockResolvedValue({ result: 'value' }),
|
|
14
|
+
subscribe: vi.fn().mockReturnValue(1),
|
|
15
|
+
unsubscribe: vi.fn(),
|
|
16
|
+
init: vi.fn().mockResolvedValue(true),
|
|
17
|
+
} as unknown as BridgeInternal;
|
|
18
|
+
|
|
19
|
+
bridgeBase = new BridgeBase(mockBridge);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('supports', () => {
|
|
23
|
+
it('should delegate to internal bridge', () => {
|
|
24
|
+
bridgeBase.supports('testMethod');
|
|
25
|
+
|
|
26
|
+
expect(mockBridge.supports).toHaveBeenCalledWith('testMethod');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return the result from internal bridge', () => {
|
|
30
|
+
(mockBridge.supports as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
|
31
|
+
|
|
32
|
+
expect(bridgeBase.supports('unknown')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('isAvailable', () => {
|
|
37
|
+
it('should delegate to internal bridge', () => {
|
|
38
|
+
bridgeBase.isAvailable();
|
|
39
|
+
|
|
40
|
+
expect(mockBridge.isAvailable).toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return the result from internal bridge', () => {
|
|
44
|
+
(mockBridge.isAvailable as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
|
45
|
+
|
|
46
|
+
expect(bridgeBase.isAvailable()).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('send', () => {
|
|
51
|
+
it('should delegate to internal bridge with method and params', async () => {
|
|
52
|
+
await bridgeBase.send('testMethod', { foo: 'bar' });
|
|
53
|
+
|
|
54
|
+
expect(mockBridge.send).toHaveBeenCalledWith('testMethod', { foo: 'bar' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should use empty object as default params', async () => {
|
|
58
|
+
await bridgeBase.send('testMethod');
|
|
59
|
+
|
|
60
|
+
expect(mockBridge.send).toHaveBeenCalledWith('testMethod', {});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return the result from internal bridge', async () => {
|
|
64
|
+
(mockBridge.send as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
65
|
+
message: 'Hello',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const result = await bridgeBase.send<{ message: string }>('greet');
|
|
69
|
+
|
|
70
|
+
expect(result).toEqual({ message: 'Hello' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should propagate errors from internal bridge', async () => {
|
|
74
|
+
const error = { error_type: 'REJECTED', error_message: 'Failed' };
|
|
75
|
+
(mockBridge.send as ReturnType<typeof vi.fn>).mockRejectedValue(error);
|
|
76
|
+
|
|
77
|
+
await expect(bridgeBase.send('failing')).rejects.toEqual(error);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('subscribe', () => {
|
|
82
|
+
it('should delegate to internal bridge', () => {
|
|
83
|
+
const listener = vi.fn();
|
|
84
|
+
bridgeBase.subscribe(listener);
|
|
85
|
+
|
|
86
|
+
expect(mockBridge.subscribe).toHaveBeenCalledWith(listener);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return subscription index', () => {
|
|
90
|
+
(mockBridge.subscribe as ReturnType<typeof vi.fn>).mockReturnValue(5);
|
|
91
|
+
|
|
92
|
+
const result = bridgeBase.subscribe(vi.fn());
|
|
93
|
+
|
|
94
|
+
expect(result).toBe(5);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('unsubscribe', () => {
|
|
99
|
+
it('should delegate to internal bridge', () => {
|
|
100
|
+
const listener = vi.fn();
|
|
101
|
+
bridgeBase.unsubscribe(listener);
|
|
102
|
+
|
|
103
|
+
expect(mockBridge.unsubscribe).toHaveBeenCalledWith(listener);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('init', () => {
|
|
108
|
+
it('should delegate to internal bridge with handlers', async () => {
|
|
109
|
+
const handlers = {
|
|
110
|
+
greet: async () => ({ message: 'hello' }),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await bridgeBase.init(handlers);
|
|
114
|
+
|
|
115
|
+
expect(mockBridge.init).toHaveBeenCalledWith(handlers);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should work without handlers', async () => {
|
|
119
|
+
await bridgeBase.init();
|
|
120
|
+
|
|
121
|
+
expect(mockBridge.init).toHaveBeenCalledWith(undefined);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return the result from internal bridge', async () => {
|
|
125
|
+
(mockBridge.init as ReturnType<typeof vi.fn>).mockResolvedValue(false);
|
|
126
|
+
|
|
127
|
+
const result = await bridgeBase.init();
|
|
128
|
+
|
|
129
|
+
expect(result).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { BridgeHandlers, BridgeListener } from './types';
|
|
2
|
+
import type { BridgeInternal } from './BridgeInternal';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BridgeBase provides the public API for bridge communication.
|
|
6
|
+
* It wraps BridgeInternal and exposes a clean interface for consumers.
|
|
7
|
+
*/
|
|
8
|
+
export class BridgeBase {
|
|
9
|
+
protected bridge: BridgeInternal;
|
|
10
|
+
|
|
11
|
+
constructor(bridge: BridgeInternal) {
|
|
12
|
+
this.bridge = bridge;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if a method is supported by the other side
|
|
17
|
+
* @param method Method name to check
|
|
18
|
+
*/
|
|
19
|
+
public supports = (method: string): boolean => this.bridge.supports(method);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if the bridge is available (initialized)
|
|
23
|
+
*/
|
|
24
|
+
public isAvailable = (): boolean => this.bridge.isAvailable();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Send a request to invoke a method on the other side
|
|
28
|
+
* @param method Method name to invoke
|
|
29
|
+
* @param params Parameters to pass
|
|
30
|
+
* @returns Promise resolving with the result
|
|
31
|
+
*/
|
|
32
|
+
public send = <TResult = unknown>(
|
|
33
|
+
method: string,
|
|
34
|
+
params: object = {}
|
|
35
|
+
): Promise<TResult> => {
|
|
36
|
+
return this.bridge.send<TResult>(method, params);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Subscribe to all result events
|
|
41
|
+
* @param listener Callback for result events
|
|
42
|
+
* @returns Subscription index
|
|
43
|
+
*/
|
|
44
|
+
public subscribe = (listener: BridgeListener): number => {
|
|
45
|
+
return this.bridge.subscribe(listener);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Unsubscribe from result events
|
|
50
|
+
* @param listener The listener to remove
|
|
51
|
+
*/
|
|
52
|
+
public unsubscribe = (listener: BridgeListener): void => {
|
|
53
|
+
return this.bridge.unsubscribe(listener);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize the bridge with handlers
|
|
58
|
+
* @param handlers Map of method names to handler functions
|
|
59
|
+
* @returns Promise resolving when initialization is complete
|
|
60
|
+
*/
|
|
61
|
+
public init = (handlers?: BridgeHandlers): Promise<boolean> =>
|
|
62
|
+
this.bridge.init(handlers);
|
|
63
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { BridgeCore } from './BridgeCore';
|
|
3
|
+
|
|
4
|
+
describe('BridgeCore', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.clearAllMocks();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe('wrapBridgeEvent', () => {
|
|
10
|
+
it('should wrap an event in the bridge protocol format', () => {
|
|
11
|
+
const event = { method: 'test', params: { foo: 'bar' } };
|
|
12
|
+
const wrapped = BridgeCore.wrapBridgeEvent(event);
|
|
13
|
+
|
|
14
|
+
expect(typeof wrapped).toBe('string');
|
|
15
|
+
const parsed = JSON.parse(wrapped);
|
|
16
|
+
expect(parsed.type).toBe('BridgeEvent');
|
|
17
|
+
expect(parsed.event).toEqual(event);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should handle complex nested objects', () => {
|
|
21
|
+
const event = {
|
|
22
|
+
method: 'test',
|
|
23
|
+
params: {
|
|
24
|
+
nested: { deep: { value: 123 } },
|
|
25
|
+
array: [1, 2, 3],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const wrapped = BridgeCore.wrapBridgeEvent(event);
|
|
29
|
+
const parsed = JSON.parse(wrapped);
|
|
30
|
+
expect(parsed.event).toEqual(event);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('wrapListener', () => {
|
|
35
|
+
it('should parse valid bridge events', () => {
|
|
36
|
+
const listener = vi.fn();
|
|
37
|
+
const wrappedListener = BridgeCore.wrapListener(listener);
|
|
38
|
+
const event = { method: 'test' };
|
|
39
|
+
const data = JSON.stringify({ type: 'BridgeEvent', event });
|
|
40
|
+
|
|
41
|
+
wrappedListener(data);
|
|
42
|
+
|
|
43
|
+
expect(listener).toHaveBeenCalledWith(event);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should ignore non-string data', () => {
|
|
47
|
+
const listener = vi.fn();
|
|
48
|
+
const wrappedListener = BridgeCore.wrapListener(listener);
|
|
49
|
+
|
|
50
|
+
wrappedListener(undefined);
|
|
51
|
+
wrappedListener(123 as unknown as string);
|
|
52
|
+
|
|
53
|
+
expect(listener).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should ignore empty strings', () => {
|
|
57
|
+
const listener = vi.fn();
|
|
58
|
+
const wrappedListener = BridgeCore.wrapListener(listener);
|
|
59
|
+
|
|
60
|
+
wrappedListener('');
|
|
61
|
+
|
|
62
|
+
expect(listener).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle iOS wrapped JSON (with single quotes)', () => {
|
|
66
|
+
const listener = vi.fn();
|
|
67
|
+
const wrappedListener = BridgeCore.wrapListener(listener);
|
|
68
|
+
const event = { method: 'test' };
|
|
69
|
+
const data = `'${JSON.stringify({ type: 'BridgeEvent', event })}'`;
|
|
70
|
+
|
|
71
|
+
wrappedListener(data);
|
|
72
|
+
|
|
73
|
+
expect(listener).toHaveBeenCalledWith(event);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should ignore non-JSON data', () => {
|
|
77
|
+
const listener = vi.fn();
|
|
78
|
+
const wrappedListener = BridgeCore.wrapListener(listener);
|
|
79
|
+
|
|
80
|
+
wrappedListener('not json');
|
|
81
|
+
wrappedListener('[1, 2, 3]');
|
|
82
|
+
|
|
83
|
+
expect(listener).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should ignore events with wrong type', () => {
|
|
87
|
+
const listener = vi.fn();
|
|
88
|
+
const wrappedListener = BridgeCore.wrapListener(listener);
|
|
89
|
+
const data = JSON.stringify({ type: 'WrongType', event: {} });
|
|
90
|
+
|
|
91
|
+
wrappedListener(data);
|
|
92
|
+
|
|
93
|
+
expect(listener).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should handle invalid JSON gracefully', () => {
|
|
97
|
+
const listener = vi.fn();
|
|
98
|
+
const wrappedListener = BridgeCore.wrapListener(listener);
|
|
99
|
+
|
|
100
|
+
wrappedListener('{invalid json}');
|
|
101
|
+
|
|
102
|
+
expect(listener).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('browserListener', () => {
|
|
107
|
+
it('should extract data from MessageEvent', () => {
|
|
108
|
+
const listener = vi.fn();
|
|
109
|
+
const browserListener = BridgeCore.browserListener(listener);
|
|
110
|
+
const event = { method: 'test' };
|
|
111
|
+
const messageEvent = {
|
|
112
|
+
data: JSON.stringify({ type: 'BridgeEvent', event }),
|
|
113
|
+
} as MessageEvent;
|
|
114
|
+
|
|
115
|
+
browserListener(messageEvent);
|
|
116
|
+
|
|
117
|
+
expect(listener).toHaveBeenCalledWith(event);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should ignore events without data', () => {
|
|
121
|
+
const listener = vi.fn();
|
|
122
|
+
const browserListener = BridgeCore.browserListener(listener);
|
|
123
|
+
|
|
124
|
+
browserListener({ data: undefined } as MessageEvent);
|
|
125
|
+
browserListener({} as MessageEvent);
|
|
126
|
+
|
|
127
|
+
expect(listener).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('webViewListener', () => {
|
|
132
|
+
it('should extract data from WebView nativeEvent', () => {
|
|
133
|
+
const listener = vi.fn();
|
|
134
|
+
const webViewListener = BridgeCore.webViewListener(listener);
|
|
135
|
+
const event = { method: 'test' };
|
|
136
|
+
const webViewMessage = {
|
|
137
|
+
nativeEvent: {
|
|
138
|
+
data: JSON.stringify({ type: 'BridgeEvent', event }),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
webViewListener(webViewMessage);
|
|
143
|
+
|
|
144
|
+
expect(listener).toHaveBeenCalledWith(event);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should ignore messages without nativeEvent', () => {
|
|
148
|
+
const listener = vi.fn();
|
|
149
|
+
const webViewListener = BridgeCore.webViewListener(listener);
|
|
150
|
+
|
|
151
|
+
webViewListener({});
|
|
152
|
+
webViewListener({ nativeEvent: {} });
|
|
153
|
+
webViewListener({ nativeEvent: { data: undefined } });
|
|
154
|
+
|
|
155
|
+
expect(listener).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('sendEvent', () => {
|
|
160
|
+
let originalParent: Window;
|
|
161
|
+
let originalRNW: typeof window.ReactNativeWebView;
|
|
162
|
+
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
originalParent = window.parent;
|
|
165
|
+
originalRNW = window.ReactNativeWebView;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
afterEach(() => {
|
|
169
|
+
Object.defineProperty(window, 'parent', {
|
|
170
|
+
value: originalParent,
|
|
171
|
+
writable: true,
|
|
172
|
+
configurable: true,
|
|
173
|
+
});
|
|
174
|
+
(window as any).ReactNativeWebView = originalRNW;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should send to iframe parent when available', () => {
|
|
178
|
+
(window as any).ReactNativeWebView = undefined;
|
|
179
|
+
const mockParent = {
|
|
180
|
+
postMessage: vi.fn(),
|
|
181
|
+
};
|
|
182
|
+
Object.defineProperty(window, 'parent', {
|
|
183
|
+
value: mockParent,
|
|
184
|
+
writable: true,
|
|
185
|
+
configurable: true,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const event = { method: 'test' };
|
|
189
|
+
BridgeCore.sendEvent(event);
|
|
190
|
+
|
|
191
|
+
expect(mockParent.postMessage).toHaveBeenCalledWith(
|
|
192
|
+
expect.any(String),
|
|
193
|
+
'*'
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should send to ReactNativeWebView when available', () => {
|
|
198
|
+
const mockRNW = {
|
|
199
|
+
postMessage: vi.fn(),
|
|
200
|
+
};
|
|
201
|
+
(window as any).ReactNativeWebView = mockRNW;
|
|
202
|
+
|
|
203
|
+
const event = { method: 'test' };
|
|
204
|
+
BridgeCore.sendEvent(event);
|
|
205
|
+
|
|
206
|
+
expect(mockRNW.postMessage).toHaveBeenCalledWith(expect.any(String));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should not send when window.parent === window (top level)', () => {
|
|
210
|
+
(window as any).ReactNativeWebView = undefined;
|
|
211
|
+
Object.defineProperty(window, 'parent', {
|
|
212
|
+
value: window,
|
|
213
|
+
writable: true,
|
|
214
|
+
configurable: true,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const event = { method: 'test' };
|
|
218
|
+
// This should not throw
|
|
219
|
+
expect(() => BridgeCore.sendEvent(event)).not.toThrow();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('subscribe', () => {
|
|
224
|
+
it('should subscribe to message events', () => {
|
|
225
|
+
const listener = vi.fn();
|
|
226
|
+
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
|
|
227
|
+
|
|
228
|
+
const unsubscribe = BridgeCore.subscribe(listener);
|
|
229
|
+
|
|
230
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
|
231
|
+
'message',
|
|
232
|
+
expect.any(Function)
|
|
233
|
+
);
|
|
234
|
+
expect(typeof unsubscribe).toBe('function');
|
|
235
|
+
|
|
236
|
+
addEventListenerSpy.mockRestore();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should return cleanup function that removes listener', () => {
|
|
240
|
+
const listener = vi.fn();
|
|
241
|
+
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
|
242
|
+
|
|
243
|
+
const unsubscribe = BridgeCore.subscribe(listener);
|
|
244
|
+
unsubscribe();
|
|
245
|
+
|
|
246
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
247
|
+
'message',
|
|
248
|
+
expect.any(Function)
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
removeEventListenerSpy.mockRestore();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level bridge core handling platform-specific message passing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type Event = unknown;
|
|
6
|
+
|
|
7
|
+
interface BridgeCoreEvent {
|
|
8
|
+
type: string;
|
|
9
|
+
event: Event;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface WebViewMessage {
|
|
13
|
+
nativeEvent?: {
|
|
14
|
+
data?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type BridgeCoreListener = (event: Event) => void;
|
|
19
|
+
|
|
20
|
+
declare global {
|
|
21
|
+
interface Window {
|
|
22
|
+
ReactNativeWebView?: {
|
|
23
|
+
postMessage: (message: string) => void;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* BridgeCore handles the low-level platform detection and message serialization.
|
|
30
|
+
* It provides static methods for wrapping events, creating listeners, and sending messages.
|
|
31
|
+
*/
|
|
32
|
+
export class BridgeCore {
|
|
33
|
+
private static BRIDGE_EVENT_TYPE = 'BridgeEvent';
|
|
34
|
+
|
|
35
|
+
private static isJSONObject = (str: string): boolean => {
|
|
36
|
+
return str.startsWith('{') && str.endsWith('}');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Wraps an event in the bridge protocol format
|
|
41
|
+
*/
|
|
42
|
+
public static wrapBridgeEvent = (event: Event): string => {
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
event,
|
|
45
|
+
type: BridgeCore.BRIDGE_EVENT_TYPE,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a listener wrapper that parses incoming messages
|
|
51
|
+
*/
|
|
52
|
+
static wrapListener =
|
|
53
|
+
(listener: BridgeCoreListener) =>
|
|
54
|
+
(data?: string): void => {
|
|
55
|
+
if (typeof data !== 'string') {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!data) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let processedData = data;
|
|
62
|
+
// iOS wraps JSON with additional quotes
|
|
63
|
+
if (processedData.startsWith("'") && processedData.endsWith("'")) {
|
|
64
|
+
processedData = processedData.substring(1, processedData.length - 1);
|
|
65
|
+
}
|
|
66
|
+
if (!BridgeCore.isJSONObject(processedData)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const eventData: BridgeCoreEvent = JSON.parse(processedData);
|
|
71
|
+
if (!eventData || eventData.type !== BridgeCore.BRIDGE_EVENT_TYPE) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
listener(eventData.event);
|
|
75
|
+
} catch {
|
|
76
|
+
// Ignore parse errors
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates a browser-specific message event listener
|
|
82
|
+
*/
|
|
83
|
+
static browserListener = (listener: BridgeCoreListener) => {
|
|
84
|
+
const triggerEvent = BridgeCore.wrapListener(listener);
|
|
85
|
+
return (originalEvent: MessageEvent): void => {
|
|
86
|
+
if (!originalEvent?.data) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
triggerEvent(originalEvent.data);
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates a React Native WebView message listener
|
|
95
|
+
*/
|
|
96
|
+
static webViewListener = (listener: BridgeCoreListener) => {
|
|
97
|
+
const triggerEvent = BridgeCore.wrapListener(listener);
|
|
98
|
+
return (originalEvent: WebViewMessage): void => {
|
|
99
|
+
if (!originalEvent?.nativeEvent?.data) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
triggerEvent(originalEvent.nativeEvent.data);
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sends an event to the parent context (WebView or iframe parent)
|
|
108
|
+
*/
|
|
109
|
+
static sendEvent = (event: Event): void => {
|
|
110
|
+
const bridgeEvent = BridgeCore.wrapBridgeEvent(event);
|
|
111
|
+
if (typeof window === 'undefined') {
|
|
112
|
+
console.warn('Window is undefined');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const RNW = window.ReactNativeWebView;
|
|
116
|
+
if (typeof RNW?.postMessage === 'function') {
|
|
117
|
+
RNW.postMessage(`'${bridgeEvent}'`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (window.parent === window) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
window.parent.postMessage(bridgeEvent, '*');
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Subscribes to window message events
|
|
128
|
+
* @returns Cleanup function to unsubscribe
|
|
129
|
+
*/
|
|
130
|
+
static subscribe = (listener: BridgeCoreListener): VoidFunction => {
|
|
131
|
+
const browserListener = BridgeCore.browserListener(listener);
|
|
132
|
+
if (typeof window === 'undefined' || !window.addEventListener) {
|
|
133
|
+
return () => {};
|
|
134
|
+
}
|
|
135
|
+
window.addEventListener('message', browserListener);
|
|
136
|
+
return () => window.removeEventListener('message', browserListener);
|
|
137
|
+
};
|
|
138
|
+
}
|