@notabene/javascript-sdk 1.34.0 → 2.0.0-next.11
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/README.md +319 -261
- package/dist/notabene.cjs +1 -0
- package/dist/notabene.d.ts +1136 -0
- package/dist/notabene.js +296 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/package.json +45 -62
- package/src/__tests__/notabene.test.ts +159 -0
- package/src/arbitraries.ts +13 -0
- package/src/components/EmbeddedComponent.ts +217 -0
- package/src/components/__tests__/EmbeddedComponent.test.ts +188 -0
- package/src/ivms/types.ts +232 -152
- package/src/notabene.ts +264 -284
- package/src/types.ts +609 -78
- package/src/utils/MessageEventManager.ts +54 -0
- package/src/utils/__tests__/MessageEventManager.test.ts +95 -0
- package/src/utils/arbitraries.ts +26 -0
- package/src/utils/caip.ts +16 -0
- package/src/utils/urls.ts +15 -0
- package/.editorconfig +0 -10
- package/.envrc.template +0 -1
- package/.eslintignore +0 -3
- package/.eslintrc.js +0 -13
- package/.gitlab-ci.yml +0 -74
- package/.husky/commit-msg +0 -4
- package/.husky/pre-commit +0 -4
- package/.husky/pre-push +0 -4
- package/.nvmrc +0 -1
- package/.prettierrc +0 -4
- package/.releaserc.json +0 -16
- package/.tool-versions +0 -3
- package/.vscode/extensions.json +0 -7
- package/.vscode/settings.json +0 -11
- package/CODEOWNERS +0 -1
- package/dist/es/index.js +0 -1
- package/dist/lib/index.js +0 -376
- package/jest.config.js +0 -5
- package/public/index.html +0 -93
- package/public/js/.gitkeep +0 -0
- package/src/lib/index.ts +0 -2
- package/src/module/index.ts +0 -10
- package/src/zoidComponentProps.ts +0 -81
- package/tsconfig.json +0 -18
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CMType,
|
|
3
|
+
ComponentRequest,
|
|
4
|
+
HMType,
|
|
5
|
+
HostMessage,
|
|
6
|
+
ValidationError,
|
|
7
|
+
} from '../types';
|
|
8
|
+
import {
|
|
9
|
+
MessageCallback,
|
|
10
|
+
MessageEventManager,
|
|
11
|
+
} from '../utils/MessageEventManager';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An embedded Notabene component
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export default class EmbeddedComponent<V, R> {
|
|
19
|
+
private _url: string;
|
|
20
|
+
private _value: V;
|
|
21
|
+
private _errors: ValidationError[] = [];
|
|
22
|
+
|
|
23
|
+
private iframe?: HTMLIFrameElement;
|
|
24
|
+
private eventManager: MessageEventManager;
|
|
25
|
+
private modal?: HTMLDialogElement;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates an instance of EmbeddedComponent.
|
|
29
|
+
* @param url - The URL of the embedded component
|
|
30
|
+
* @param value - The initial transaction value
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
constructor(url: string, value: V) {
|
|
34
|
+
this._url = url;
|
|
35
|
+
this._value = value;
|
|
36
|
+
this.eventManager = new MessageEventManager();
|
|
37
|
+
this.on(CMType.INVALID, (message) => {
|
|
38
|
+
if (message.type === CMType.INVALID) {
|
|
39
|
+
this._errors = message.errors;
|
|
40
|
+
this._value = message.value as V;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
this.on(CMType.RESIZE, (message) => {
|
|
44
|
+
if (message.type === CMType.RESIZE) {
|
|
45
|
+
if (this.iframe) this.iframe.style.height = `${message.height}px`;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Gets the URL of the embedded component
|
|
51
|
+
* @returns The URL of the embedded component
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
public get url(): string {
|
|
55
|
+
return this._url;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Gets the current transaction value
|
|
59
|
+
* @returns The current transaction value
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
public get value(): V {
|
|
63
|
+
return this._value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public get errors(): ValidationError[] {
|
|
67
|
+
return this._errors;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Opens the component URL in the current window
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
public open() {
|
|
75
|
+
document.location.href = this.url;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Mounts the component to a parent element
|
|
80
|
+
* @param parentId - The ID of the parent element
|
|
81
|
+
* @throws Will throw an error if the parent element is not found
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
mount(parentId: string) {
|
|
85
|
+
const parent = document.querySelector(parentId);
|
|
86
|
+
if (!parent) throw new Error(`parentID ${parentId} not found`);
|
|
87
|
+
this.embed(parent);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Embeds the component into a parent element
|
|
91
|
+
* @param parent - The parent element to embed the component into
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
embed(parent: Element) {
|
|
95
|
+
this.iframe = document.createElement('iframe');
|
|
96
|
+
this.iframe.src = this.url;
|
|
97
|
+
this.iframe.style.width = '100%';
|
|
98
|
+
this.iframe.style.height = '100px';
|
|
99
|
+
this.iframe.style.border = 'none';
|
|
100
|
+
this.iframe.style.overflow = 'hidden';
|
|
101
|
+
this.iframe.setAttribute('allowtransparency', 'true');
|
|
102
|
+
this.iframe.style.backgroundColor = 'transparent';
|
|
103
|
+
parent.appendChild(this.iframe);
|
|
104
|
+
|
|
105
|
+
window.addEventListener('message', (event: MessageEvent) => {
|
|
106
|
+
if (event.source !== this.iframe?.contentWindow) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log('received message from iframe', event.data);
|
|
110
|
+
this.eventManager?.setPort(event.ports[0]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.iframe?.contentWindow?.focus();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Sends a message to the embedded component
|
|
118
|
+
* @param message - The message to send
|
|
119
|
+
*/
|
|
120
|
+
send(message: HostMessage): void {
|
|
121
|
+
this.eventManager.send(message);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Adds an event listener for a specific message type
|
|
126
|
+
* @param messageType - The type of message to listen for
|
|
127
|
+
* @param callback - The callback function to execute when the message is received
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
on(messageType: string, callback: MessageCallback): void {
|
|
131
|
+
this.eventManager.on(messageType, callback);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Removes an event listener for a specific message type
|
|
136
|
+
* @param messageType - The type of message to stop listening for
|
|
137
|
+
* @param callback - The callback function to remove
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
off(messageType: string, callback: MessageCallback): void {
|
|
141
|
+
this.eventManager.off(messageType, callback);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Updates the transaction value and sends an update message to the component
|
|
146
|
+
* @param value - The new transaction value
|
|
147
|
+
*/
|
|
148
|
+
update(value: V) {
|
|
149
|
+
this._value = value;
|
|
150
|
+
this.send({ type: HMType.UPDATE, value: value as ComponentRequest });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Waits for the component to complete and returns the transaction response
|
|
155
|
+
* @returns A promise that resolves with the transaction response
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
completion(): Promise<R> {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
this.on(CMType.COMPLETE, (message) => {
|
|
161
|
+
resolve((message as any).response);
|
|
162
|
+
});
|
|
163
|
+
this.on('error', (message) => {
|
|
164
|
+
reject(new Error((message as any).message));
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Opens the component in a modal dialog
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
openModal(): Promise<R> {
|
|
174
|
+
if (this.modal) {
|
|
175
|
+
this.closeModal();
|
|
176
|
+
}
|
|
177
|
+
this.modal = document.createElement('dialog');
|
|
178
|
+
this.modal.style.border = 'none';
|
|
179
|
+
this.modal.style.backgroundColor = 'transparent';
|
|
180
|
+
this.modal.style.maxWidth = '100vw';
|
|
181
|
+
this.modal.style.maxHeight = '100vh';
|
|
182
|
+
this.modal.style.width = '600px';
|
|
183
|
+
this.modal.style.height = '800px';
|
|
184
|
+
|
|
185
|
+
document.body.appendChild(this.modal);
|
|
186
|
+
this.embed(this.modal);
|
|
187
|
+
this.on(CMType.CANCEL, () => {
|
|
188
|
+
this.closeModal();
|
|
189
|
+
});
|
|
190
|
+
this.on(CMType.CLOSE, () => {
|
|
191
|
+
this.closeModal();
|
|
192
|
+
});
|
|
193
|
+
this.on(CMType.COMPLETE, () => {
|
|
194
|
+
this.closeModal();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
this.modal.showModal();
|
|
198
|
+
this.modal.addEventListener('click', () => {
|
|
199
|
+
// console.log('click');
|
|
200
|
+
this.closeModal();
|
|
201
|
+
});
|
|
202
|
+
return this.completion();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Closes the modal dialog
|
|
206
|
+
*
|
|
207
|
+
*/
|
|
208
|
+
|
|
209
|
+
closeModal(): void {
|
|
210
|
+
if (this.modal) {
|
|
211
|
+
console.log('closeModal');
|
|
212
|
+
this.modal?.close();
|
|
213
|
+
this.modal.remove();
|
|
214
|
+
this.modal = undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
CMType,
|
|
4
|
+
HMType,
|
|
5
|
+
HostMessage,
|
|
6
|
+
Transaction,
|
|
7
|
+
TransactionResponse,
|
|
8
|
+
Withdrawal,
|
|
9
|
+
} from '../../types';
|
|
10
|
+
import EmbeddedComponent from '../EmbeddedComponent';
|
|
11
|
+
|
|
12
|
+
const url = 'https://example.com';
|
|
13
|
+
const parentId = 'parent-container';
|
|
14
|
+
const value: Partial<Transaction> = { amountDecimal: 100 };
|
|
15
|
+
|
|
16
|
+
describe('EmbeddedComponent', () => {
|
|
17
|
+
describe('creation', () => {
|
|
18
|
+
let component: EmbeddedComponent<
|
|
19
|
+
Partial<Withdrawal>,
|
|
20
|
+
TransactionResponse<Withdrawal>
|
|
21
|
+
>;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
component = new EmbeddedComponent<
|
|
24
|
+
Partial<Withdrawal>,
|
|
25
|
+
TransactionResponse<Withdrawal>
|
|
26
|
+
>(url, { amountDecimal: 100 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('sets value', () => {
|
|
30
|
+
expect(component.value).toStrictEqual(value);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('sets url', () => {
|
|
34
|
+
expect(component.url).toBe(url);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('iframe', () => {
|
|
39
|
+
let document: Document;
|
|
40
|
+
let parentElement: HTMLElement;
|
|
41
|
+
let iframe: HTMLIFrameElement;
|
|
42
|
+
let window: Window;
|
|
43
|
+
let iframeWindow: Window;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
parentElement = {
|
|
47
|
+
appendChild: vi.fn(),
|
|
48
|
+
} as unknown as HTMLElement;
|
|
49
|
+
|
|
50
|
+
iframe = {
|
|
51
|
+
style: {},
|
|
52
|
+
src: '',
|
|
53
|
+
contentWindow: iframeWindow,
|
|
54
|
+
setAttribute: vi.fn(),
|
|
55
|
+
addEventListener: vi.fn(),
|
|
56
|
+
} as unknown as HTMLIFrameElement;
|
|
57
|
+
|
|
58
|
+
document = {
|
|
59
|
+
createElement: vi.fn().mockReturnValue(iframe),
|
|
60
|
+
querySelector: vi.fn().mockReturnValue(parentElement),
|
|
61
|
+
} as unknown as Document;
|
|
62
|
+
|
|
63
|
+
window = {
|
|
64
|
+
addEventListener: vi.fn(),
|
|
65
|
+
} as unknown as Window;
|
|
66
|
+
|
|
67
|
+
vi.stubGlobal('document', document);
|
|
68
|
+
vi.stubGlobal('window', window);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.unstubAllGlobals();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('mount', () => {
|
|
76
|
+
const component = new EmbeddedComponent<
|
|
77
|
+
Partial<Withdrawal>,
|
|
78
|
+
TransactionResponse<Withdrawal>
|
|
79
|
+
>(url, value);
|
|
80
|
+
component.mount(parentId);
|
|
81
|
+
expect(iframe?.src).toBe(url);
|
|
82
|
+
expect(parentElement.appendChild).toHaveBeenCalledWith(iframe);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('on', () => {
|
|
86
|
+
const component = new EmbeddedComponent<
|
|
87
|
+
Partial<Withdrawal>,
|
|
88
|
+
TransactionResponse<Withdrawal>
|
|
89
|
+
>(url, value);
|
|
90
|
+
const messageType = 'customEvent';
|
|
91
|
+
const callback = vi.fn();
|
|
92
|
+
component.on(messageType, callback);
|
|
93
|
+
component.mount(parentId);
|
|
94
|
+
|
|
95
|
+
// Simulate receiving a message of the specified type
|
|
96
|
+
// and assert that the callback is called with the correct payload.
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('off', () => {
|
|
100
|
+
const component = new EmbeddedComponent<
|
|
101
|
+
Partial<Withdrawal>,
|
|
102
|
+
TransactionResponse<Withdrawal>
|
|
103
|
+
>(url, value);
|
|
104
|
+
const messageType = 'customEvent';
|
|
105
|
+
const callback = vi.fn();
|
|
106
|
+
component.on(messageType, callback);
|
|
107
|
+
|
|
108
|
+
// Simulate receiving a message of the specified type
|
|
109
|
+
// and assert that the callback is called with the correct payload.
|
|
110
|
+
|
|
111
|
+
component.off(messageType, callback);
|
|
112
|
+
|
|
113
|
+
// Simulate receiving another message of the specified type
|
|
114
|
+
// and assert that the callback is not called.
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('connected to iframe', () => {
|
|
118
|
+
let messageHandler: (event: MessageEvent) => void;
|
|
119
|
+
let port: MessagePort;
|
|
120
|
+
let handshake: MessageEvent;
|
|
121
|
+
let component: EmbeddedComponent<
|
|
122
|
+
Partial<Withdrawal>,
|
|
123
|
+
TransactionResponse<Withdrawal>
|
|
124
|
+
>;
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
port = {
|
|
128
|
+
postMessage: vi.fn(),
|
|
129
|
+
start: vi.fn(),
|
|
130
|
+
onmessage: null,
|
|
131
|
+
} as unknown as MessagePort;
|
|
132
|
+
handshake = {
|
|
133
|
+
source: iframe.contentWindow,
|
|
134
|
+
ports: [port],
|
|
135
|
+
} as unknown as MessageEvent;
|
|
136
|
+
window.addEventListener = vi
|
|
137
|
+
.fn()
|
|
138
|
+
.mockImplementation((event, handler) => {
|
|
139
|
+
messageHandler = handler;
|
|
140
|
+
});
|
|
141
|
+
component = new EmbeddedComponent<
|
|
142
|
+
Partial<Withdrawal>,
|
|
143
|
+
TransactionResponse<Withdrawal>
|
|
144
|
+
>(url, value);
|
|
145
|
+
component.mount(parentId);
|
|
146
|
+
|
|
147
|
+
expect(messageHandler).toBeDefined();
|
|
148
|
+
messageHandler(handshake);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('send', () => {
|
|
152
|
+
const message: HostMessage = {
|
|
153
|
+
type: HMType.UPDATE,
|
|
154
|
+
value: {
|
|
155
|
+
requestId: 'id',
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
component.send(message);
|
|
159
|
+
expect(port.postMessage).toHaveBeenCalledWith(message);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('update', () => {
|
|
163
|
+
const newValue: Partial<Transaction> = { amountDecimal: 200 };
|
|
164
|
+
component.update(newValue);
|
|
165
|
+
|
|
166
|
+
expect(component.value).toBe(newValue);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// TODO not sure this actually does what I want it to do
|
|
170
|
+
test('completion', async () => {
|
|
171
|
+
const response = { valid: true, value: 'completed' };
|
|
172
|
+
expect(port.onmessage).toBeDefined();
|
|
173
|
+
|
|
174
|
+
const promise = component.completion();
|
|
175
|
+
// @ts-expect-error - we've already verified that port exists
|
|
176
|
+
port.onmessage({
|
|
177
|
+
data: { type: CMType.COMPLETE, response },
|
|
178
|
+
} as MessageEvent);
|
|
179
|
+
|
|
180
|
+
promise.then((result) => {
|
|
181
|
+
expect(result).toBe(response);
|
|
182
|
+
});
|
|
183
|
+
return promise;
|
|
184
|
+
// Assert that the completion promise resolves with the expected response.
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|