@krishp/one-auth 0.0.1
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 +674 -0
- package/README.md +273 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +1 -0
- package/dist/index.umd.js +1 -0
- package/dist/types/index.d.ts +80 -0
- package/dist/useEventLink.d.ts +5 -0
- package/dist/window/index.d.ts +21 -0
- package/package.json +58 -0
- package/src/index.ts +1 -0
- package/src/types/index.d.ts +80 -0
- package/src/useEventLink.tsx +339 -0
- package/src/window/index.ts +123 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { ConnectionRecord, EventLinkProps, EventProps } from "./types";
|
|
2
|
+
import { createWindow, EventLinkWindow, VISIBLE_IFRAME_ID } from "./window";
|
|
3
|
+
|
|
4
|
+
// Track processed messages to prevent duplicates (defense-in-depth)
|
|
5
|
+
const processedMessages = new Set<string>();
|
|
6
|
+
const MESSAGE_EXPIRY_MS = 5000;
|
|
7
|
+
|
|
8
|
+
// One-shot guard so we only handle a given OAuth return once per page
|
|
9
|
+
// load. The hook can be called multiple times across React re-renders;
|
|
10
|
+
// without this guard we'd open multiple check iframes for the same
|
|
11
|
+
// return.
|
|
12
|
+
let oauthReturnHandled = false;
|
|
13
|
+
|
|
14
|
+
// Query param names used by the same-window OAuth redirect flow.
|
|
15
|
+
// These appear on the parent app's URL after the user comes back from
|
|
16
|
+
// the OAuth provider. The package detects them on init, processes the
|
|
17
|
+
// result, and strips them from the URL.
|
|
18
|
+
const RETURN_STATE_PARAM = "one_auth_state";
|
|
19
|
+
const RETURN_ERROR_PARAM = "one_auth_error";
|
|
20
|
+
|
|
21
|
+
// Separator used between the original OAuth state and the base64url
|
|
22
|
+
// encoded return URL. Tilde is in the URL "unreserved" set so it
|
|
23
|
+
// survives URL encoding intact, and it's not used by base64url so
|
|
24
|
+
// splitting is unambiguous.
|
|
25
|
+
const STATE_SEPARATOR = "~";
|
|
26
|
+
|
|
27
|
+
// ---- base64url helpers (no deps) -------------------------------------
|
|
28
|
+
|
|
29
|
+
function base64urlEncode(str: string): string {
|
|
30
|
+
return btoa(str)
|
|
31
|
+
.replace(/\+/g, "-")
|
|
32
|
+
.replace(/\//g, "_")
|
|
33
|
+
.replace(/=+$/, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---- OAuth URL state injection ---------------------------------------
|
|
37
|
+
|
|
38
|
+
// Replaces the `state` query param in the OAuth provider URL with one
|
|
39
|
+
// that has the return URL appended. Falls back to a string replace if
|
|
40
|
+
// the URL constructor can't parse the input (very unusual but
|
|
41
|
+
// defensive).
|
|
42
|
+
function injectReturnUrlIntoState(
|
|
43
|
+
oauthUrl: string,
|
|
44
|
+
originalState: string,
|
|
45
|
+
returnUrl: string
|
|
46
|
+
): string {
|
|
47
|
+
const newState = `${originalState}${STATE_SEPARATOR}${base64urlEncode(returnUrl)}`;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(oauthUrl);
|
|
50
|
+
parsed.searchParams.set("state", newState);
|
|
51
|
+
return parsed.toString();
|
|
52
|
+
} catch {
|
|
53
|
+
return oauthUrl
|
|
54
|
+
.replace(
|
|
55
|
+
`state=${encodeURIComponent(originalState)}`,
|
|
56
|
+
`state=${encodeURIComponent(newState)}`
|
|
57
|
+
)
|
|
58
|
+
.replace(
|
|
59
|
+
// Some OAuth URLs include the state without URL-encoding it
|
|
60
|
+
`state=${originalState}`,
|
|
61
|
+
`state=${encodeURIComponent(newState)}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- URL cleanup -----------------------------------------------------
|
|
67
|
+
|
|
68
|
+
function stripReturnParamsFromUrl() {
|
|
69
|
+
if (typeof window === "undefined") return;
|
|
70
|
+
try {
|
|
71
|
+
const url = new URL(window.location.href);
|
|
72
|
+
let changed = false;
|
|
73
|
+
if (url.searchParams.has(RETURN_STATE_PARAM)) {
|
|
74
|
+
url.searchParams.delete(RETURN_STATE_PARAM);
|
|
75
|
+
changed = true;
|
|
76
|
+
}
|
|
77
|
+
if (url.searchParams.has(RETURN_ERROR_PARAM)) {
|
|
78
|
+
url.searchParams.delete(RETURN_ERROR_PARAM);
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
if (changed) {
|
|
82
|
+
window.history.replaceState({}, document.title, url.toString());
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// No-op: history.replaceState is best-effort cleanup.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- OAuth return handler --------------------------------------------
|
|
90
|
+
|
|
91
|
+
// Opens the auth iframe in "checkState" mode after the user comes back
|
|
92
|
+
// from the OAuth provider. The iframe at auth.withone.ai polls
|
|
93
|
+
// /v1/connections/oauth/check?state=X and renders a loading spinner
|
|
94
|
+
// followed by a success or failure screen. When the backend reports a
|
|
95
|
+
// final status the iframe posts LINK_SUCCESS / LINK_ERROR back here
|
|
96
|
+
// immediately so the consumer's callback fires in parallel with the
|
|
97
|
+
// visible UI. The iframe stays visible until the user dismisses it
|
|
98
|
+
// with the X button (which fires EXIT_EVENT_LINK). NO auto-close.
|
|
99
|
+
function handleOAuthReturn(props: EventLinkProps, state: string) {
|
|
100
|
+
const checkWindow = new EventLinkWindow({
|
|
101
|
+
...props,
|
|
102
|
+
checkState: state,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let cleaned = false;
|
|
106
|
+
let resultDelivered = false;
|
|
107
|
+
|
|
108
|
+
const handler = (event: MessageEvent) => {
|
|
109
|
+
if (typeof window === "undefined") return;
|
|
110
|
+
|
|
111
|
+
const iframe = document.getElementById(
|
|
112
|
+
VISIBLE_IFRAME_ID
|
|
113
|
+
) as HTMLIFrameElement | null;
|
|
114
|
+
|
|
115
|
+
// Only accept messages from this iframe instance.
|
|
116
|
+
if (!iframe || event.source !== iframe.contentWindow) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const eventData = (event as unknown as EventProps).data;
|
|
121
|
+
if (!eventData?.messageType) return;
|
|
122
|
+
|
|
123
|
+
if (eventData.messageType === "LINK_SUCCESS") {
|
|
124
|
+
if (!resultDelivered) {
|
|
125
|
+
resultDelivered = true;
|
|
126
|
+
try {
|
|
127
|
+
props.onSuccess?.(eventData.message as ConnectionRecord);
|
|
128
|
+
} catch {
|
|
129
|
+
/* consumer callback errors are not our problem */
|
|
130
|
+
}
|
|
131
|
+
stripReturnParamsFromUrl();
|
|
132
|
+
}
|
|
133
|
+
} else if (eventData.messageType === "LINK_ERROR") {
|
|
134
|
+
if (!resultDelivered) {
|
|
135
|
+
resultDelivered = true;
|
|
136
|
+
try {
|
|
137
|
+
props.onError?.(eventData.message as string);
|
|
138
|
+
} catch {
|
|
139
|
+
/* consumer callback errors are not our problem */
|
|
140
|
+
}
|
|
141
|
+
stripReturnParamsFromUrl();
|
|
142
|
+
}
|
|
143
|
+
} else if (eventData.messageType === "EXIT_EVENT_LINK") {
|
|
144
|
+
// User clicked X. Tear down everything. If onSuccess/onError
|
|
145
|
+
// hasn't fired yet (user dismissed during polling), fire
|
|
146
|
+
// onClose so the consumer knows the user bailed.
|
|
147
|
+
if (!resultDelivered) {
|
|
148
|
+
try {
|
|
149
|
+
props.onClose?.();
|
|
150
|
+
} catch {
|
|
151
|
+
/* ignore */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
cleanup();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
function cleanup() {
|
|
159
|
+
if (cleaned) return;
|
|
160
|
+
cleaned = true;
|
|
161
|
+
if (typeof window !== "undefined") {
|
|
162
|
+
window.removeEventListener("message", handler);
|
|
163
|
+
}
|
|
164
|
+
checkWindow.closeLink();
|
|
165
|
+
stripReturnParamsFromUrl();
|
|
166
|
+
// Reset the module-level guard so the NEXT OAuth flow on this page
|
|
167
|
+
// can be detected. Without this, back-to-back OAuth flows in an
|
|
168
|
+
// SPA (no full page reload between them) would silently skip the
|
|
169
|
+
// second return detection.
|
|
170
|
+
oauthReturnHandled = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (typeof window !== "undefined") {
|
|
174
|
+
window.addEventListener("message", handler);
|
|
175
|
+
}
|
|
176
|
+
checkWindow.openLink();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Fired when the callback page redirects back with `?one_auth_error=`.
|
|
180
|
+
// The OAuth provider returned an error (e.g., the user denied consent)
|
|
181
|
+
// and the callback page knows there's nothing to poll. We just notify
|
|
182
|
+
// the consumer and strip the URL params.
|
|
183
|
+
function handleOAuthReturnError(props: EventLinkProps, errorMessage: string) {
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
try {
|
|
186
|
+
props.onError?.(errorMessage);
|
|
187
|
+
} finally {
|
|
188
|
+
stripReturnParamsFromUrl();
|
|
189
|
+
}
|
|
190
|
+
}, 0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function detectOAuthReturn(props: EventLinkProps) {
|
|
194
|
+
if (typeof window === "undefined") return;
|
|
195
|
+
if (oauthReturnHandled) return;
|
|
196
|
+
|
|
197
|
+
let params: URLSearchParams;
|
|
198
|
+
try {
|
|
199
|
+
params = new URLSearchParams(window.location.search);
|
|
200
|
+
} catch {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const errorParam = params.get(RETURN_ERROR_PARAM);
|
|
205
|
+
const stateParam = params.get(RETURN_STATE_PARAM);
|
|
206
|
+
|
|
207
|
+
// No return params — nothing to do.
|
|
208
|
+
if (!errorParam && !stateParam) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
oauthReturnHandled = true;
|
|
213
|
+
|
|
214
|
+
if (errorParam) {
|
|
215
|
+
handleOAuthReturnError(props, errorParam);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (stateParam) {
|
|
220
|
+
handleOAuthReturn(props, stateParam);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---- Main hook -------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
export const useEventLink = (props: EventLinkProps) => {
|
|
227
|
+
// Detect OAuth return on every call. The module-level guard ensures
|
|
228
|
+
// we only actually process a return once per page load, even if the
|
|
229
|
+
// hook is called from multiple components or re-renders.
|
|
230
|
+
detectOAuthReturn(props);
|
|
231
|
+
|
|
232
|
+
const linkWindow = createWindow({ ...props });
|
|
233
|
+
|
|
234
|
+
let messageHandler: ((event: MessageEvent) => void) | null = null;
|
|
235
|
+
let isListenerActive = false;
|
|
236
|
+
|
|
237
|
+
const handleMessage = (event: MessageEvent) => {
|
|
238
|
+
if (typeof window === "undefined") return;
|
|
239
|
+
|
|
240
|
+
const iFrameWindow = document.getElementById(VISIBLE_IFRAME_ID) as HTMLIFrameElement;
|
|
241
|
+
if (!iFrameWindow || iFrameWindow.style.display !== "block") return;
|
|
242
|
+
|
|
243
|
+
// Only accept messages from our iframe instance.
|
|
244
|
+
if (event.source !== iFrameWindow.contentWindow) return;
|
|
245
|
+
|
|
246
|
+
const eventData = (event as unknown as EventProps).data;
|
|
247
|
+
if (!eventData?.messageType) return;
|
|
248
|
+
|
|
249
|
+
// Deduplication: prevent processing same message type within expiry window
|
|
250
|
+
const dedupeKey = `${eventData.messageType}-${JSON.stringify(eventData.message ?? eventData.url ?? "")}`;
|
|
251
|
+
if (processedMessages.has(dedupeKey)) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
processedMessages.add(dedupeKey);
|
|
255
|
+
setTimeout(() => processedMessages.delete(dedupeKey), MESSAGE_EXPIRY_MS);
|
|
256
|
+
|
|
257
|
+
switch (eventData.messageType) {
|
|
258
|
+
case "EXIT_EVENT_LINK":
|
|
259
|
+
props.onClose?.();
|
|
260
|
+
setTimeout(() => {
|
|
261
|
+
close();
|
|
262
|
+
}, 200);
|
|
263
|
+
break;
|
|
264
|
+
case "LINK_SUCCESS":
|
|
265
|
+
props.onSuccess?.(eventData.message as ConnectionRecord);
|
|
266
|
+
break;
|
|
267
|
+
case "LINK_ERROR":
|
|
268
|
+
props.onError?.(eventData.message as string);
|
|
269
|
+
break;
|
|
270
|
+
case "OAUTH_REDIRECT": {
|
|
271
|
+
// Same-window OAuth redirect flow. The iframe asks the parent
|
|
272
|
+
// to navigate to the OAuth provider URL. We capture the
|
|
273
|
+
// current page URL, encode it into the OAuth state parameter,
|
|
274
|
+
// tear down the iframe, and navigate.
|
|
275
|
+
const oauthUrl = eventData.url;
|
|
276
|
+
const oauthState = eventData.state;
|
|
277
|
+
if (!oauthUrl || !oauthState) {
|
|
278
|
+
props.onError?.("Invalid OAuth redirect message");
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
const returnUrl = window.location.href;
|
|
282
|
+
const navigateUrl = injectReturnUrlIntoState(
|
|
283
|
+
oauthUrl,
|
|
284
|
+
oauthState,
|
|
285
|
+
returnUrl
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Detach our message listener and close the iframe before
|
|
289
|
+
// navigation. We close explicitly (instead of relying on the
|
|
290
|
+
// navigation to destroy it) so we leave a clean DOM state.
|
|
291
|
+
if (messageHandler && isListenerActive) {
|
|
292
|
+
window.removeEventListener("message", messageHandler);
|
|
293
|
+
isListenerActive = false;
|
|
294
|
+
messageHandler = null;
|
|
295
|
+
}
|
|
296
|
+
linkWindow.closeLink();
|
|
297
|
+
|
|
298
|
+
window.location.href = navigateUrl;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const open = () => {
|
|
305
|
+
// Remove existing listener first (defensive)
|
|
306
|
+
if (messageHandler && isListenerActive) {
|
|
307
|
+
window.removeEventListener("message", messageHandler);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
messageHandler = handleMessage;
|
|
311
|
+
|
|
312
|
+
if (typeof window !== "undefined") {
|
|
313
|
+
window.addEventListener("message", messageHandler);
|
|
314
|
+
isListenerActive = true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
linkWindow.openLink();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const close = () => {
|
|
321
|
+
// Clean up listener and dedup state when closing
|
|
322
|
+
if (typeof window !== "undefined" && messageHandler && isListenerActive) {
|
|
323
|
+
window.removeEventListener("message", messageHandler);
|
|
324
|
+
isListenerActive = false;
|
|
325
|
+
messageHandler = null;
|
|
326
|
+
}
|
|
327
|
+
// Only clear the EXIT dedup key so re-opening works immediately.
|
|
328
|
+
// LINK_SUCCESS / LINK_ERROR dedup keys stay to prevent duplicate callbacks.
|
|
329
|
+
for (const key of processedMessages) {
|
|
330
|
+
if (key.startsWith("EXIT_EVENT_LINK")) {
|
|
331
|
+
processedMessages.delete(key);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
linkWindow.closeLink();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return { open, close };
|
|
339
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { EventLinkWindowProps } from "../types";
|
|
2
|
+
|
|
3
|
+
// Capability flag sent to the auth iframe so it knows the parent
|
|
4
|
+
// supports the same-window OAuth redirect flow. Older iframes that
|
|
5
|
+
// don't read this flag will keep using the popup behavior. Older
|
|
6
|
+
// packages (without this flag) make the iframe fall back to popups.
|
|
7
|
+
const PACKAGE_CAPABILITIES = {
|
|
8
|
+
oauthRedirect: true,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// DOM ID for the auth iframe. Constant so the package can find and
|
|
12
|
+
// remove its own iframe deterministically across re-mounts and BFCache
|
|
13
|
+
// edge cases.
|
|
14
|
+
export const VISIBLE_IFRAME_ID = "event-link";
|
|
15
|
+
|
|
16
|
+
export class EventLinkWindow {
|
|
17
|
+
private linkTokenEndpoint: string;
|
|
18
|
+
private linkHeaders?: object;
|
|
19
|
+
private baseUrl?: string;
|
|
20
|
+
private onClose?: () => void;
|
|
21
|
+
private title?: string;
|
|
22
|
+
private imageUrl?: string;
|
|
23
|
+
private companyName?: string;
|
|
24
|
+
private selectedConnection?: string;
|
|
25
|
+
private showNameInput?: boolean;
|
|
26
|
+
private appTheme?: "dark" | "light";
|
|
27
|
+
private checkState?: string;
|
|
28
|
+
|
|
29
|
+
constructor(props: EventLinkWindowProps) {
|
|
30
|
+
this.linkTokenEndpoint = props.token.url;
|
|
31
|
+
this.linkHeaders = props.token.headers;
|
|
32
|
+
this.baseUrl = props.baseUrl;
|
|
33
|
+
this.onClose = props.onClose;
|
|
34
|
+
this.title = props.title;
|
|
35
|
+
this.imageUrl = props.imageUrl;
|
|
36
|
+
this.companyName = props.companyName;
|
|
37
|
+
this.selectedConnection = props.selectedConnection;
|
|
38
|
+
this.showNameInput = props.showNameInput;
|
|
39
|
+
this.appTheme = props.appTheme;
|
|
40
|
+
this.checkState = props.checkState;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private _getBaseUrl() {
|
|
44
|
+
if (this.baseUrl) {
|
|
45
|
+
return this.baseUrl;
|
|
46
|
+
}
|
|
47
|
+
return "https://auth.withone.ai";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private _buildPayload() {
|
|
51
|
+
return {
|
|
52
|
+
linkTokenEndpoint: this.linkTokenEndpoint,
|
|
53
|
+
linkHeaders: this.linkHeaders,
|
|
54
|
+
title: this.title,
|
|
55
|
+
imageUrl: this.imageUrl,
|
|
56
|
+
companyName: this.companyName,
|
|
57
|
+
selectedConnection: this.selectedConnection,
|
|
58
|
+
showNameInput: this.showNameInput,
|
|
59
|
+
appTheme: this.appTheme,
|
|
60
|
+
// Internal — tells the iframe what the parent supports
|
|
61
|
+
capabilities: PACKAGE_CAPABILITIES,
|
|
62
|
+
// Internal — when present, iframe goes straight to status check
|
|
63
|
+
checkState: this.checkState,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public openLink() {
|
|
68
|
+
// Defensive: if a previous instance is still in the DOM (e.g.,
|
|
69
|
+
// from a double open() call), remove it first. Two iframes with
|
|
70
|
+
// the same id would both receive postMessages and cause weird
|
|
71
|
+
// race conditions.
|
|
72
|
+
const existing = document.getElementById(VISIBLE_IFRAME_ID);
|
|
73
|
+
if (existing) {
|
|
74
|
+
existing.remove();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const container = document.createElement("iframe");
|
|
78
|
+
|
|
79
|
+
const payload = this._buildPayload();
|
|
80
|
+
const jsonString = JSON.stringify(payload);
|
|
81
|
+
|
|
82
|
+
const base64Encoded = btoa(jsonString);
|
|
83
|
+
const urlParams = { data: base64Encoded };
|
|
84
|
+
const queryString = new URLSearchParams(urlParams).toString();
|
|
85
|
+
|
|
86
|
+
const url = `${this._getBaseUrl()}?${queryString}`;
|
|
87
|
+
|
|
88
|
+
document.body.appendChild(container);
|
|
89
|
+
container.style.height = "100%";
|
|
90
|
+
container.style.width = "100%";
|
|
91
|
+
container.style.position = "fixed";
|
|
92
|
+
container.style.display = "hidden";
|
|
93
|
+
container.style.visibility = "hidden";
|
|
94
|
+
container.style.zIndex = "9999";
|
|
95
|
+
container.style.backgroundColor = "transparent";
|
|
96
|
+
container.style.inset = "0px";
|
|
97
|
+
container.style.borderWidth = "0px";
|
|
98
|
+
container.id = VISIBLE_IFRAME_ID;
|
|
99
|
+
container.style.overflow = "hidden auto";
|
|
100
|
+
container.src = url;
|
|
101
|
+
|
|
102
|
+
container.onload = () => {
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
container.style.display = "block";
|
|
105
|
+
container.style.visibility = "visible";
|
|
106
|
+
}, 100);
|
|
107
|
+
container.contentWindow?.postMessage(payload, url);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public closeLink() {
|
|
112
|
+
const iFrameWindow = document.getElementById(
|
|
113
|
+
VISIBLE_IFRAME_ID
|
|
114
|
+
) as HTMLIFrameElement;
|
|
115
|
+
if (iFrameWindow) {
|
|
116
|
+
iFrameWindow.remove();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const createWindow = (props: EventLinkWindowProps) => {
|
|
122
|
+
return new EventLinkWindow(props);
|
|
123
|
+
};
|